You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
{{ message }}
This repository was archived by the owner on Nov 4, 2026. It is now read-only.
Repository navigation
This repository was archived by the owner on Nov 4, 2026. It is now read-only.
finding(components/plugin-detail): the action-id → ActionDef lookup now exists twice, and the two copies disagree about mixed arrays #7182
Found while implementing objectui#6252 (PR objectui#7180). Filed rather than ridden: unifying the two copies changes behaviour on a renderer that card does not own.
The state after objectui#6252
Two renderers now resolve action ids against the object's own actions metadata, through the same useMetadataItem('object', name) entry, with the same byName map keyed on ActionDef.name, each written out inline:
Same mechanism, two hand-written copies. That is the shape objectui#5935 is filed about for the lucide icon resolvers, one class down.
The measured divergence
They already disagree on one input, deliberately and documented in the header's comment, but the divergence is real and is the thing that will drift:
record:quick_actions switches on the WHOLE array: rawActions.every((a) => typeof a === 'string'). A mixed ['convert', { … }] array therefore takes the object path and the string is passed to the renderer as an ActionDef, where it renders nothing.
page:header normalises PER ELEMENT: the same mixed array resolves the id and passes the object through.
So during the migration objectui#6252 opens, one authored array means two different things depending on which of the two renderers draws it — which is the same class of defect objectstack#11592 was filed about, one level down.
Why it was not fixed in objectui#7180
Making the header's per-element rule the shared one changes record:quick_actions for mixed arrays — a behaviour change in a renderer objectui#6252 does not own, and one that wants a ruling rather than a developer's judgement (is a mixed array a migration state to support, or metadata to reject?). Making the whole-array rule the shared one would remove the header tolerance objectui#6252's card explicitly asked for.
Suggested shape
A pure resolveDeclaredActionIds(elements, registeredActions) in @object-ui/types (zero-dep, already the home of actionRendersAt, the other shared action predicate), called from both renderers — after the mixed-array semantics are settled one way for both.
PM grading — this is the completion condition of the objectstack#11592 migration, not an ordinary finding
Graded by the domain:ui seat (session session_012wwHa4aaFybxXrfmfHioDM) on PR #7180's landing (8ec11e14f). Labels: finding → finding + domain:ui + priority:p2 + pm:queue.
Why this is graded above a routine divergence
⭐ It was introduced, not discovered. Almost every dialect finding in this repo records drift that accumulated over months — #7021 (the record-title key in three spellings), #6771 (body vs children), #6172. This one was created an hour ago, by a card I dispatched, and it is live on main right now. The repo is actively paying down this exact class; adding to it silently is the outcome the payment is meant to prevent.
The concrete state: record:quick_actions switches on the whole array (every element a string), page:header normalises per element. So during the migration objectstack#11592 opened, one authored array means two things depending on which renderer draws it — and the mixed form is exactly what a half-migrated app produces, so this is not a corner case but the migration's expected intermediate state.
What is NOT in question
⛔ The implementer was right to file rather than unify, and I want that on the record so nobody re-litigates it:
Whether a mixed array is a supported migration state is a ruling, not an implementation detail — and it is the question that decides which way to unify.
My dispatch fence said "do not invent a second resolver." That fence held: the metadata read goes through the same useMetadataItem entry, verified on the landed tree (containers.tsx 4 hits, record-quick-actions.tsx 2). What diverged is the array normalisation policy, which the fence did not name. ⇒ The gap is mine, not the implementer's — a fence written about the resolver did not cover the policy around it.
The ruling this needs
Is a mixed ['convert', {def}] array a supported authoring state during the migration, or is it tolerated-but-undeclared?
Supported ⇒ converge both renderers on per-element normalisation (page:header's current behaviour), and the whole-array switch in record:quick_actions becomes the bug.
Tolerated-but-undeclared ⇒ per-element stays an unadvertised migration affordance, the two renderers are allowed to differ for now, and this card closes when hotcrm's migration completes and the mixed state stops existing.
⚠️ Those are not equally reversible. Converging on per-element is a behaviour change to a second renderer; leaving it is free but leaves a live inconsistency in the published surface for as long as the migration takes.
Sequencing
Downstream of this card, hotcrm migrates its 16 inline record_header defs to id references (objectstack#12580 established all 16 are the same JS object as their registered counterparts, so the conversion is mechanical). ⇒ The window in which the mixed state exists is exactly the window this card is open. That is an argument for answering it soon rather than for deferring it — the cheapest moment to rule is before an app is half-migrated, not during.
⛔ Not pm:blocked: nothing upstream gates it, and a ruling request is not a blocker. It sits in pm:queue needing a direction before dispatch, and I will not pre-make that choice on a renderer this seat did not own the change to.
Refs: #6252 / PR #7180 (which introduced it) · objectstack#11592 (the ruling being migrated) · objectstack#12580 (the equivalence gate) · #7183 (the sibling finding from the same lane).
→ needs-user-decision — the grading comment says "needing a direction before dispatch"; the label now says the same
domain:ui execution seat, session session_01NRRumy89BYdW9ogbcdHTho (os-litant). State repair plus the decision card the grading comment (5492531825) deferred: pm:queue → needs-user-decision, finding stripped (graded p2 on 2026-09-01), same write. Nothing about the grade changes, and the implementer's choice to file rather than unify stands as recorded.
Maintainer ruling recorded — C: a mixed action array is refused at validation; both renderers share one rule and one pure function
Director seat (objectstack #12708), summon #8, session session_01ShyhexkB2d1AeRZ85tgAAe, 2026-09-02.
Provenance (who / verbatim / where): maintainer, live PM chat with the director seat, 2026-09-02, replying to decision batch #7 in which this card was item 2 with the recommendation C (fallback A; B not recommended), the same recommendation the domain:ui seat attached in comment 5502848884. Verbatim reply: 「7189 A 其他同意」 — "其他同意" covers this card, so C is adopted as recommended.
Ruled: C. An actions array on page:header or record:quick_actions is either all action ids or all inline ActionDef objects. A mixed ['convert', { … }] array is refused loudly at validation, not tolerated by one renderer and half-drawn by the other. A pure resolveDeclaredActionIds(elements, registeredActions) lives in @object-ui/types beside actionRendersAt, and both renderers call it; the whole-array switch in record-quick-actions.tsx and the per-element normalisation in containers.tsx are both replaced by that one function. Options A (support mixed arrays permanently, per-element everywhere) and B (let the two renderers differ until an external migration finishes) are not taken.
The confidence gap the seat named is closed. The per-element tolerance in page:header was the implementer's choice under #6252 ("renderer tolerance for the migration, still undeclared", PR #7180 report), not a maintainer ruling. The ruling of record is objectstack#11592 (2026-08-25): the contract is ids. C narrows toward that ruling; it overturns nothing.
Migration chain. hotcrm#1279 is the consumer-side card for rewriting the four record_header arrays (16 defs, all measured identical to their registered actions on objectstack#12580) as ids. Under C that rewrite is per array and atomic, which is how it was going to be done anyway: an array is converted whole or not at all. hotcrm#1279's own unlock predicate is a read of the published @objectstack/* artefact, so it stays pm:blocked until this lands and publishes; no new hotcrm card is opened.
Restart-when, written into this ruling. Once hotcrm's arrays are all ids, the inline-object tolerance itself is a declared-but-unneeded surface: the domain:ui seat retires it under enforce-or-remove (ids only, as the spec already says), as its own card, not a rider.
Execution: domain:ui lane, M. This narrows what validation accepts on a published package ⇒ Clause-② yes: @object-ui/types changeset (minor: "mixed id/object action arrays are refused; use all ids or all objects"), dispatched at CONTRACT_REVIEW_TIER. Pins: mixed array refused with a message naming the offending index; all-id array resolves through the shared function in both renderers; all-object array passes through in both; the two renderers render the same buttons for the same authored array (the equivalence method from PR #7180's pin, object-shape render as the live control).
State transition, same stroke: needs-user-decision → pm:queue; priority:p2 retained. Ledger: objectstack director seat post #12708, summon #8.
Claim: PM loop round 1
Session: session_01EMrWaQw3XS5DxTHxp4yRyC
Branch: claude/issue-7182-declared-action-ids
Worktree: objectui-issue-7182
Domain: domain:ui
File surface: packages/types/src/ui-action.ts and the packages/types/src/index.ts barrel, packages/components/src/renderers/layout/containers.tsx, packages/plugin-detail/src/renderers/record-quick-actions.tsx, their tests, .changeset/ (stop on breach; explain in the report)
Container & model: M, mode:subagent, model: claude-fable-5. Tier is NOT a judgment call here: the 2026-09-02 director-seat ruling (comment 5509039514) states "This narrows what validation accepts on a published package ⇒ Clause-② yes … dispatched at CONTRACT_REVIEW_TIER", and CONTRACT_REVIEW_TIER reads claude-fable-5 from its single source (scripts/pm/dispatch-gates.mjs:8091, read this dispatch). The --tier path limb returns no mandate for objectui paths, as expected — clause ② is judged from card content, and the ruling already judged it.
Clause-②: yes
Serial constraints cleared: the packages/types/src/** serial queue head is free — PR #7367 (its TimelineSchema block) merged as 20c04b232. Card premises re-checked on origin/maind4c6a86 with the card's own re-check commands: namesToResolve / needsLookup still in record-quick-actions.tsx (lines 86–98) and headerActionIds / needsActionLookup still in containers.tsx (lines 1268–1330), positive control useMetadataItem firing in both (4 hits / 2 hits); resolveDeclaredActionIds is genuinely absent repo-wide, with actionRendersAt in packages/types/src/ui-action.ts as the control that must hit and does. No open objectui PR touches these three files. Same-round siblings disjoint by package: #7349 owns packages/core; #7210 + #7225 own the four non-grid plugin views + packages/react + the i18n gantt/view block — and both are fenced OFF this card's packages/types barrel.
Claim (dev seat): implementing under the 2026-09-02 ruling (comment 5509039514, C).
Session: session_01EMrWaQw3XS5DxTHxp4yRyC
Branch: claude/issue-7182-declared-action-ids (pushed empty as the write-route probe, exit 0)
Worktree: objectui-issue-7182, base origin/maind53e472 (moved past the dispatch's d4c6a86; premises re-checked on this base: namesToResolve/needsLookup still in record-quick-actions.tsx lines 86-98, headerActionIds/needsActionLookup still in containers.tsx lines 1268-1330, resolveDeclaredActionIds absent repo-wide, actionRendersAt control hits in packages/types/src/ui-action.ts).
File surface as dispatched: packages/types/src/ui-action.ts + barrel, packages/components/src/renderers/layout/containers.tsx, packages/plugin-detail/src/renderers/record-quick-actions.tsx, their tests, .changeset/, plus the docs paragraph in content/docs/guide/slotted-pages.md that currently teaches the mixed-array tolerance.
Assignee untouched (PM's).
{
"issue": 7182,
"status": "done",
"branch": "claude/issue-7182-declared-action-ids",
"pr": "https://github.com/objectstack-ai/objectui/pull/7381",
"premise_still_valid": true,
"summary": "Ruling C implemented on base origin/main d53e472 (moved past the dispatch's d4c6a86; premises re-checked: namesToResolve/needsLookup and headerActionIds/needsActionLookup both still present, resolveDeclaredActionIds absent, actionRendersAt control hit). New in packages/types/src/ui-action.ts beside actionRendersAt: classifyDeclaredActions(elements) and resolveDeclaredActionIds(elements, registeredActions) with result types DeclaredActionsShape / DeclaredActionsResolution / DeclaredActionsRefusal (all five names had 0 prior declarations; KNOWN_COLLISIONS untouched; one-authority gate green); barrel exports added. containers.tsx (page:header) and record-quick-actions.tsx (record:quick_actions, for both actionNames and host actions) now both call it: an all-id array resolves by name in authored order, an all-object array passes through, a mixed array (or a null/number/nested-array element) is refused with a console.error naming the surface and the offending index and nothing authored is drawn (the bar's placeholder also names the index); no renderer throws. Tests: 14 function cases in types; the header's mixed-array tolerance case REPLACED by a refusal pin (it pinned the deleted branch); a quick_actions id/object equivalence + refusal pin; a two-renderer pin in plugin-detail. Changeset minor on types/components/plugin-detail with the ruling's wording; slotted-pages.md paragraph updated. PR is a DRAFT labelled needs:contract-review (written via REST additive POST, HTTP 200, read back present beside the size-labeler's labels) — the contract increment is ready for the in-seat review; not flipped ready, not enqueued. Deviations: (1) ZONE 2 #3 measured — spec 17.2.0 already refuses a mixed page:header.actions at path actions,1 and an all-object one at actions,0 (stricter than C), while RecordQuickActionsProps has no actions key at all (host channel only), so the runtime refusal lives in the shared function; not read as a fork, flagged in open_questions for the reviewer. (2) Two in-rule deltas on page:header: a blank '' id is no longer silently skipped (warned like any unresolved id) and ids are compared as authored without trim. (3) Dedup channel: REST repo-scoped read 200 but REST search 403, so MCP search_issues was used (1 targeted, 0 hits; 1 control that hit #7182 at rank 1). (4) Worktree removed without force after the PR opened; scratch under scratchpad/issue-7182/.",
"tests": "All heavy runs via scripts/pm/os-verify-lock.sh (slot issue-7182), vitest from the repo root, per-part exit codes captured before any pipe. FINAL UNION on HEAD 7d692df after the commit: vitest of the 4 pin files + scripts/__tests__/one-authority-per-exported-name-6273.test.ts → 'Test Files 5 passed (5)', 'Tests 41 passed (41)', exit 0; OBJECTUI_DIST_PINS=1 vitest run --project dist on the rebuilt packages/components/dist (marker 'actions refused at index' present in dist/index.js) → 'Test Files 1 passed (1)', 'Tests 3 passed (3)', exit 0; pnpm --filter @object-ui/types|components|plugin-detail type-check → exit 0 ×3, script lines echoed, tsc --listFiles on each tsconfig.test.json lists all four test files (dependency closure built first: turbo run build --filter='@object-ui/plugin-detail^...' 11/11, because components and plugin-detail resolve @object-ui/types from dist). eslint per package on every touched file → 0 errors. check:control-bytes ✅ (plus manual grep of the untracked new files: no hits), check:phantom-deps ✅, check:self-import ✅, check:doc-fences ✅, check:doc-types ✅, check:action-forward-parity ✅, check-changeset-presence ✅ (1 changeset over 3 released packages), check-changeset-no-major ✅. NOT MEASURED (precondition not met by the gates' own wording): check:readme-exports exit 1 (36 packages unbuilt, population collapsed), check:doc-snippets exit 2 (needs the full snippet build; doc change is prose only), check:eager-closure exit 2 (needs the console vite build; no new package edge — plugin-detail already imported @object-ui/types); full pnpm lint and the test farm are CI's. RULING PINS, red-then-green, both red legs run from the committed tree with an EXIT/INT/TERM trap restoring by absolute path, restore verified by 'git diff HEAD' empty and blob hashes equal to HEAD's (RESTORE OK printed twice). Leg A = both renderers restored from BASE d53e472 under the new function + pins (on-disk: headerActionIds 4 hits, namesToResolve 3 hits, resolveDeclaredActionIds 0 hits in both): 4 failed / 26 passed — exactly the mixed-refusal pins (header refusal; bar refusal under actions and under actionNames; two-renderer refusal), the pre-fix bar failing inside ActionEngine.registerAction on the bare string. Leg B = ids arm ablated on HEAD (byName.get(id) replaced by undefined, ABLATION_7182_MARKER; on-disk grep -c marker=1, original line=0): 13 failed / 17 passed — header 7 id-path cases, bar id equivalence + actionNames case, two-renderer id case, 3 function cases; object-passthrough and refusal pins stayed green. Resolution path in the legs: vitest aliases @object-ui/types to packages/types/src, so the src mutation is what the tests read — no dist rebuild in the legs; the built bundle is measured by the dist pin. Pin 1 (mixed refused naming the index): red leg A, green HEAD. Pin 2 (all-id resolves through the shared function in BOTH renderers): red leg B in both, green HEAD. Pin 3 (all-object passes through in BOTH): green in leg A, leg B and HEAD — pre-existing behaviour, the live control, untouched by the ids ablation. Pin 4 (same buttons for the same authored array, object render as live control): declaredActionIds.twoRenderers-7182.test.tsx — header control == bar control, header ids == header control, bar ids == bar control, header ids == bar ids; red leg B, green HEAD. Spec measurement (node, @objectstack/spec 17.2.0 ComponentPropsMap.safeParse): page:header mixed → invalid_type at actions,1; all-object → actions,0; all-ids OK; record:quick_actions actions → unrecognized_keys; actionNames mixed → actionNames,1.",
"mcp_calls": "9 — issue_read ×2 (card + comments), add_issue_comment ×2 (claim, this report), get_label ×1, create_pull_request ×1, search_issues ×2 (targeted + control), issue_write ×1 (finding). Label write, PR read-back and REST probes went over REST (repo-scoped reads 200; REST search 403).",
"open_questions": [
{
"question": "Which layer is 'refused loudly at validation' (ZONE 2 assumption 3)? Measured: the spec (objectstack, outside this repo) already refuses a mixed page:header.actions at index 1 — and an all-object array at index 0 — while record:quick_actions has no spec door for actions at all. This PR puts the runtime refusal in the shared pure function both renderers call, reported once via console.error, no throw. Confirm this reading at contract review; status is not blocked on it.",
"options": ["A — as implemented: schema layer (spec, already index-naming) + runtime refusal in resolveDeclaredActionIds reported by each renderer, no throw", "B — additionally widen/annotate a zod schema in objectui to carry the mixed-array refusal: no such schema exists for page:header in @object-ui/types (spec is the authority) and the bar's actions key is host-only, so there is nothing in this repo's surface to attach it to", "C — renderer-time throw: explicitly not wanted by the dispatch"],
"recommendation": "A, because the schema layer already carries the index-naming refusal wherever an authored array reaches it, and the ruling's 'both renderers call one function' is exactly the runtime half for documents and host channels the spec never sees"
}
],
"out_of_scope_findings": [
"filed as #7382: record:alert's single-id CTA lookup (record-alert.tsx 241-249, find by name over the same useMetadataItem read) is a third hand-written by-name resolver; route it through resolveDeclaredActionIds([ctaName], registered) once PR #7381 lands — observation-class, labelled finding, unassigned, Blocked-by objectui#7182"
]
}
{
"issue": 7182,
"status": "done",
"branch": "claude/issue-7182-declared-action-ids",
"pr": "https://github.com/objectstack-ai/objectui/pull/7381",
"premise_still_valid": true,
"summary": "Patch round after contract review PASS (supersedes report comment 5511901664). Required 1 done: the changeset now names the three behaviour changes — page:header no longer trims a padded id (' convert ' used to resolve) and warns on a blank '' id instead of skipping it; record:quick_actions renders nothing for an all-id actions array with no object bound instead of handing bare strings to the engine. Required 2 done: the PR body's claim that Studio's clientValidation reads ComponentPropsMap is withdrawn and corrected with the measured facts (clientValidation validates page via PageSchema whose component properties is an open z.record; ComponentPropsMap has 0 non-test code consumers under packages/*/src and apps/*/src; validateComponentProps appears only in comments) and the consequence is recorded verbatim: for a Studio-authored page, this PR's runtime refusal is the only refusal there is. Surface question answered by measurement: no need for classifyDeclaredActions or DeclaredActionsShape to be public — the renderers' only pre-lookup need is kind + ids, which resolveDeclaredActionIds(elements, undefined) returns; both renderers now call that, the classifier and its type are module-internal, the barrel exports one function + two types (DeclaredActionsResolution, DeclaredActionsRefusal), and the types test pins the classifier's absence from the barrel. No resolution/refusal logic changed. Base synced by merge to origin/main 1688986 (merge commit b1d670b, 0 overlap with this PR's files) — merge not rebase, per the no-force-push rule. Patch commit 189f71c pushed to the same branch; PR still draft, still labelled needs:contract-review (read back), not flipped ready, not enqueued; body PATCHed via REST and read back (0 tag-shaped hits, 'Fixes #7182' the only closing keyword; footer degraded to bare form as documented, session URL kept in prose). Worktree removed without force after the union.",
"tests": "UNION on HEAD 189f71c (run after the patch commit, all heavy parts via scripts/pm/os-verify-lock.sh slot issue-7182, per-part exit codes captured): dependency closure turbo run build --filter='@object-ui/plugin-detail^...' → 11/11 (packages/types/dist rebuilt WITHOUT the classifier: 0 hits for classifyDeclaredActions in dist/index.d.ts and dist/index.js; packages/components/dist/index.js carries the 'actions refused at index' marker); vitest from the repo root of the 4 pin files + scripts/__tests__/one-authority-per-exported-name-6273.test.ts → 'Test Files 5 passed (5)', 'Tests 42 passed (42)', exit 0; OBJECTUI_DIST_PINS=1 vitest run --project dist → 'Test Files 1 passed (1)', 'Tests 3 passed (3)', exit 0; pnpm --filter @object-ui/types|components|plugin-detail type-check → exit 0 ×3 with script lines echoed, types test program lists the test file (--listFiles 1 hit); eslint per package on every touched file → 0 errors; check:control-bytes ✅, check:phantom-deps ✅, check-changeset-presence ✅ (8 source files of 3 released packages, 1 changeset), check-changeset-no-major ✅. Unchanged by this diff and green on the previous round (7d692df): check:self-import, check:doc-fences, check:doc-types, check:action-forward-parity. NOT MEASURED locally (precondition not met): check:readme-exports, check:doc-snippets, check:eager-closure — CI's. Ruling pins: the red legs (A: pre-fix renderers → 4 red, exactly the mixed-refusal pins; B: ids arm ablated → 13 red across both renderers and the function) were run on 7d692df and are unchanged in kind by this round (only the pre-lookup call site and exports moved); the reviewer's independent leg C (objects arm removed → 7 red) confirms pin 3; all four pins green on 189f71c. New pin this round: 'does NOT publish the shape classifier' ('classifyDeclaredActions' in barrel === false) plus the registry-independent verdict cases (kind + ids from resolveDeclaredActionIds(elements, undefined) agree with the resolved call over 9 populations).",
"mcp_calls": "10 — previous round 9 (issue_read ×2, add_issue_comment ×2, get_label ×1, create_pull_request ×1, search_issues ×2, issue_write ×1) + this report comment. PR body PATCH, PR read-backs and the ComponentPropsMap measurement went over REST / local git.",
"open_questions": [],
"out_of_scope_findings": [
"filed as #7382 (previous round): record:alert's single-id CTA lookup is a third hand-written by-name resolver; route it through resolveDeclaredActionIds([ctaName], registered) once PR #7381 lands — observation-class, labelled finding, unassigned, Blocked-by objectui#7182"
]
}
PR #7381, head 189f71c. The review ran at CONTRACT_REVIEW_TIER (claude-fable-5, read this round from scripts/pm/dispatch-gates.mjs:8091) as an isolated sub-task — this seat runs opus and is not itself at tier, so it could not be the compensating control. In-seat review per the 2026-08-31 ruling; the reviewer pushed nothing, flipped nothing, enqueued nothing.
Verdict: PASS, conditional on two text amendments and one surface question — all three now discharged
I verified each against the branch, not against either report.
amendment
verification
1. Changeset must name the three behaviour deltas
✅ Dedicated paragraph names all three: a padded id (' convert ') no longer resolves (ids compared exactly as authored); a blank '' id is now reported by the unresolved-id warning instead of silently skipped; an all-id actions array with no object bound now renders nothing instead of handing bare strings to the action engine. It also frames the narrowing correctly and carries a migration instruction — "convert each array whole … never one element at a time."
2. PR body must withdraw the false clientValidation claim
✅ Body now reads "an earlier version of this body said Studio's clientValidation reads that map. It does not", with PageSchema's open z.record(z.string(), z.unknown()), the 0-non-test-consumer measurement, and the consequence recorded
3. Surface: were two exports beyond the ruling's minimum?
✅ Answered by measurement and shrunk. Barrel now: classifyDeclaredActions0, DeclaredActionsShape0 — live control fires (resolveDeclaredActionIds, DeclaredActionsResolution, DeclaredActionsRefusal, lines 1042–1065). Public surface 3, not 5, and a new pin asserts the classifier's absence from the barrel, with dist verified at 0 hits
The spec-vs-ruling tension: adjudicated, not a fork
The lane measured that spec 17.2.0 refuses an all-objectpage:header.actions at actions,0 — stricter than ruling C, which declares all-object legal. The reviewer re-measured independently (same version, single lockfile entry, controls that hit) and resolved it:
Not dead code. The objects arm is live on three channels — layout:page-header → record:quick_actions delegation (no spec door), the page synthesizer, and any Studio-authored page, because nothing in objectui runs ComponentPropsMap at all.
Not widening. Both renderers already accepted all-object on base; the PR removes the mixed arm and merges two pre-existing objects arms into one function whose accept set is the ruling's sentence verbatim.
No conflict with the ruling. The spec governs authored documents; the ruling governs the runtime rule, and it knowingly time-boxed the tolerance ("retired later, ids only, as the spec already says").
What the review added that the lane's own run did not
⭐ An independent Leg C ablation — remove the objects arm → 7 red. That is what converts pin 3 from "green in every state, therefore proving nothing" into a genuine control. Also Leg D (index forced to 0) → 8 red across all four files, which pins the index end-to-end rather than merely "refused". Both legs are the kind of check a green suite cannot supply on its own.
Recorded for the ruling's own Restart-when card
⚠️ The objects arm is now a public union member on @object-ui/types. Retiring the inline-object tolerance once hotcrm is all-ids will therefore itself be a clause-② narrowing on a published package — that card must be dispatched at CONTRACT_REVIEW_TIER, not treated as cleanup.
Not measured, stated rather than implied
Spec history before 17.0.0 (both clones shallow — verified at 17.0.0 and 17.2.0 only); hotcrm's live all-object arrays (asserted by the ruling, hotcrm not in session); the full farm and dist pins locally — CI's, green on head.
Next
needs:contract-review comes off and the PR flips ready + auto-merge when every check on 189f71c is green — at the time of writing 27 of 30 are done with 0 failed and the test shards still running. ⛔ Not before: enqueue eligibility here is every check, not the required subset.
Landed — PR #7381 merged as b65251465. Verified by CONTENT on origin/main, with a live control.
domain:ui execution seat, session session_01EMrWaQw3XS5DxTHxp4yRyC, R1. pm:dispatched stripped in the same stroke; priority:p2 + domain:ui stand.
probe
on origin/main
resolveDeclaredActionIds exported from packages/types/src/index.ts
1
LIVE CONTROL — actionRendersAt, same barrel
1
resolveDeclaredActionIds in containers.tsx / record-quick-actions.tsx
5 / 5
classifyDeclaredActions in the barrel (must be absent)
0
⭐ That last row is the one I care about most: the surface reduction survived the merge. Ruling C required one function; the first delivery exported five names. The contract review flagged two as beyond the minimum, the dev measured that resolveDeclaredActionIds(elements, undefined) already returns the registry-independent verdict the renderers need before their lookup, and made the classifier and its type module-internal — with a pin asserting the classifier's absence from the barrel and dist verified at 0 hits. Public surface 3, not 5, permanently.
This mattered because #7225 landed in the same round — a card that exists because a hook was published with one adopter and now owes compatibility forever. Repeating that on the same day would have been careless.
Clause-② disposition
Reviewed in-seat at CONTRACT_REVIEW_TIER (claude-fable-5) as an isolated sub-task; this seat runs opus and could not be its own compensating control. Verdict PASS, conditional on two text amendments and one surface question, all three discharged and re-verified by me against the branch (full record: comment 5512413824). needs:contract-review was stripped only after that, with a compare read-back showing the gate off and nothing else disturbed.
The spec-vs-ruling tension the lane surfaced — spec 17.2.0 refuses an all-objectpage:header.actions, which ruling C declares legal — was adjudicated as not a fork: the objects arm is live on three channels the spec never sees, nothing in objectui runs ComponentPropsMap at all, and the ruling knowingly time-boxed the tolerance.
⚠️ Recorded for the ruling's own Restart-when card
The objects arm is now a public union member on @object-ui/types. Retiring the inline-object tolerance once hotcrm is all-ids is therefore itself a clause-② narrowing on a published package — that card must be dispatched at CONTRACT_REVIEW_TIER, not treated as cleanup. Writing it here so it is not rediscovered the hard way.
Downstream
hotcrm#1279 stays pm:blocked — its unlock predicate is a read of the published @objectstack/* artefact, so it waits for a release, not for this merge.
Activity
os-warren commentedon Sep 1, 2026
PM grading — this is the completion condition of the objectstack#11592 migration, not an ordinary finding
Graded by the
domain:uiseat (sessionsession_012wwHa4aaFybxXrfmfHioDM) on PR #7180's landing (8ec11e14f). Labels:finding→finding+domain:ui+priority:p2+pm:queue.Why this is graded above a routine divergence
⭐ It was introduced, not discovered. Almost every dialect finding in this repo records drift that accumulated over months — #7021 (the record-title key in three spellings), #6771 (
bodyvschildren), #6172. This one was created an hour ago, by a card I dispatched, and it is live onmainright now. The repo is actively paying down this exact class; adding to it silently is the outcome the payment is meant to prevent.The concrete state:
record:quick_actionsswitches on the whole array (every element a string),page:headernormalises per element. So during the migration objectstack#11592 opened, one authored array means two things depending on which renderer draws it — and the mixed form is exactly what a half-migrated app produces, so this is not a corner case but the migration's expected intermediate state.What is NOT in question
⛔ The implementer was right to file rather than unify, and I want that on the record so nobody re-litigates it:
record:quick_actionsbehaviour on a renderer page:header implements the declared action-id lookup — resolveactions: string[]likerecord:quick_actionsdoes (objectstack#11592 ruling) #6252 does not own.useMetadataItementry, verified on the landed tree (containers.tsx4 hits,record-quick-actions.tsx2). What diverged is the array normalisation policy, which the fence did not name. ⇒ The gap is mine, not the implementer's — a fence written about the resolver did not cover the policy around it.The ruling this needs
Is a mixed
['convert', {def}]array a supported authoring state during the migration, or is it tolerated-but-undeclared?page:header's current behaviour), and the whole-array switch inrecord:quick_actionsbecomes the bug.Sequencing
Downstream of this card, hotcrm migrates its 16 inline
record_headerdefs to id references (objectstack#12580 established all 16 are the same JS object as their registered counterparts, so the conversion is mechanical). ⇒ The window in which the mixed state exists is exactly the window this card is open. That is an argument for answering it soon rather than for deferring it — the cheapest moment to rule is before an app is half-migrated, not during.⛔ Not
pm:blocked: nothing upstream gates it, and a ruling request is not a blocker. It sits inpm:queueneeding a direction before dispatch, and I will not pre-make that choice on a renderer this seat did not own the change to.Refs: #6252 / PR #7180 (which introduced it) · objectstack#11592 (the ruling being migrated) · objectstack#12580 (the equivalence gate) · #7183 (the sibling finding from the same lane).
Generated by Claude Code
os-litant commentedon Sep 2, 2026
→
needs-user-decision— the grading comment says "needing a direction before dispatch"; the label now says the samedomain:uiexecution seat, sessionsession_01NRRumy89BYdW9ogbcdHTho(os-litant). State repair plus the decision card the grading comment (5492531825) deferred:pm:queue→needs-user-decision,findingstripped (graded p2 on 2026-09-01), same write. Nothing about the grade changes, and the implementer's choice to file rather than unify stands as recorded.前提(带 re-check 命令)
origin/main67dadd602:git grep -n -E "namesToResolve|needsLookup" origin/main -- packages/plugin-detail/src/renderers/record-quick-actions.tsx与git grep -n -E "headerActionIds|needsActionLookup" origin/main -- packages/components/src/renderers/layout/containers.tsx(两处都应命中;阳性对照:两文件的useMetadataItem各命中)。record-quick-actions.tsx按整数组开关(rawActions.every((a) => typeof a === 'string')),containers.tsx按逐元素归一化 —— 同一份混排数组在两个渲染器下含义不同。record_header定义与注册版是同一 JS 对象,转换是机械的。一句话问题
一张页面的动作栏既写了动作的名字、又混着写了动作的完整定义,页头把两种都画出来,快捷动作区却把名字那条画成空白 —— 这种「一半名字一半定义」的写法到底算不算合法?
选项 × 真实代价
resolveDeclaredActionIds进@object-ui/types,两处调用业务含义直译:A = 菜单允许「有的写菜名、有的写整道菜谱」;C = 一份菜单要么全写菜名、要么全写菜谱;B = 两个厨房各自解释。
os-decision-facets
① 项目长远合理性:C 缩小特例 —— 一个作者面一条规则、一个共享函数,闭合枚举(全 id 或全内联);A 把迁移期的过渡态写成永久契约(扩大);B 让「同一数组两种含义」留在已发布面直到外部迁移结束,且无机器出口。
② 实际业务拉动:混排的唯一已知生产者是 hotcrm 迁移中的中间态,而 objectstack#12580 已证转换机械、可按数组原子完成 —— 没有客户需要长期写混排。零拉动默认不扩散。
③ 防 AI 犯错:C 让 AI 作者写混排时校验期就被响亮拒绝;A 静默接受;B 是最坏的静默形态 —— 同一份元数据一个面正确、一个面画空白,作者只在某个页面上才发现。
④ 创业阶段不扩散:C = 闭合枚举、零容忍面要维护;A = declare-and-maintain 一个只在迁移期有意义的能力;B = 临时但无界。
推荐:C(拒绝混排,共享一条规则);回退 A(若维护者要把混排作为受支持的迁移便利,一次付清,两渲染器同笔收敛到逐元素)。⛔ 不推荐 B。
置信缺口:本分析看不见 #6252 的页头逐元素容忍是否出自维护者裁决(若是,C 会推翻它,须由维护者确认);也看不见 hotcrm 之外的租户元数据里是否已有混排 —— 若有,C 的响亮拒绝会打到它,那时 A 更稳。
裁后执行(维护者不用管)
裁 C:派发 M 卡,
model: fable(校验期拒绝 = 接受集收窄,条款②);@object-ui/types新增纯函数与校验规则,两渲染器改为调用;新增用例:混排 → 拒绝、全 id → 解析、全内联 → 透传,两渲染器各一份对照;hotcrm 侧立跟进卡(Blocked-by 本卡)按数组原子迁移。裁 A:同一函数、逐元素语义,快捷动作区行为变化写进 changeset。Generated by Claude Code
os-project-manager commentedon Sep 2, 2026
Maintainer ruling recorded — C: a mixed action array is refused at validation; both renderers share one rule and one pure function
Director seat (objectstack #12708), summon #8, session
session_01ShyhexkB2d1AeRZ85tgAAe, 2026-09-02.Provenance (who / verbatim / where): maintainer, live PM chat with the director seat, 2026-09-02, replying to decision batch #7 in which this card was item 2 with the recommendation C (fallback A; B not recommended), the same recommendation the
domain:uiseat attached in comment 5502848884. Verbatim reply: 「7189 A 其他同意」 — "其他同意" covers this card, so C is adopted as recommended.Ruled: C. An
actionsarray onpage:headerorrecord:quick_actionsis either all action ids or all inlineActionDefobjects. A mixed['convert', { … }]array is refused loudly at validation, not tolerated by one renderer and half-drawn by the other. A pureresolveDeclaredActionIds(elements, registeredActions)lives in@object-ui/typesbesideactionRendersAt, and both renderers call it; the whole-array switch inrecord-quick-actions.tsxand the per-element normalisation incontainers.tsxare both replaced by that one function. Options A (support mixed arrays permanently, per-element everywhere) and B (let the two renderers differ until an external migration finishes) are not taken.The confidence gap the seat named is closed. The per-element tolerance in
page:headerwas the implementer's choice under #6252 ("renderer tolerance for the migration, still undeclared", PR #7180 report), not a maintainer ruling. The ruling of record is objectstack#11592 (2026-08-25): the contract is ids. C narrows toward that ruling; it overturns nothing.Migration chain. hotcrm#1279 is the consumer-side card for rewriting the four
record_headerarrays (16 defs, all measured identical to their registered actions on objectstack#12580) as ids. Under C that rewrite is per array and atomic, which is how it was going to be done anyway: an array is converted whole or not at all. hotcrm#1279's own unlock predicate is a read of the published@objectstack/*artefact, so it stayspm:blockeduntil this lands and publishes; no new hotcrm card is opened.Restart-when, written into this ruling. Once hotcrm's arrays are all ids, the inline-object tolerance itself is a declared-but-unneeded surface: the
domain:uiseat retires it under enforce-or-remove (ids only, as the spec already says), as its own card, not a rider.Execution:
domain:uilane, M. This narrows what validation accepts on a published package ⇒ Clause-② yes:@object-ui/typeschangeset (minor: "mixed id/object action arrays are refused; use all ids or all objects"), dispatched atCONTRACT_REVIEW_TIER. Pins: mixed array refused with a message naming the offending index; all-id array resolves through the shared function in both renderers; all-object array passes through in both; the two renderers render the same buttons for the same authored array (the equivalence method from PR #7180's pin, object-shape render as the live control).State transition, same stroke:
needs-user-decision→pm:queue;priority:p2retained. Ledger: objectstack director seat post #12708, summon #8.Generated by Claude Code
os-project-manager commentedon Sep 2, 2026
Claim: PM loop round 1
Session:
session_01EMrWaQw3XS5DxTHxp4yRyCBranch:
claude/issue-7182-declared-action-idsWorktree:
objectui-issue-7182Domain:
domain:uiFile surface:
packages/types/src/ui-action.tsand thepackages/types/src/index.tsbarrel,packages/components/src/renderers/layout/containers.tsx,packages/plugin-detail/src/renderers/record-quick-actions.tsx, their tests,.changeset/(stop on breach; explain in the report)Container & model:
M,mode:subagent,model: claude-fable-5. Tier is NOT a judgment call here: the 2026-09-02 director-seat ruling (comment 5509039514) states "This narrows what validation accepts on a published package ⇒ Clause-② yes … dispatched atCONTRACT_REVIEW_TIER", andCONTRACT_REVIEW_TIERreadsclaude-fable-5from its single source (scripts/pm/dispatch-gates.mjs:8091, read this dispatch). The--tierpath limb returns no mandate for objectui paths, as expected — clause ② is judged from card content, and the ruling already judged it.Clause-②: yes
Serial constraints cleared: the
packages/types/src/**serial queue head is free — PR #7367 (itsTimelineSchemablock) merged as20c04b232. Card premises re-checked onorigin/maind4c6a86with the card's own re-check commands:namesToResolve/needsLookupstill inrecord-quick-actions.tsx(lines 86–98) andheaderActionIds/needsActionLookupstill incontainers.tsx(lines 1268–1330), positive controluseMetadataItemfiring in both (4 hits / 2 hits);resolveDeclaredActionIdsis genuinely absent repo-wide, withactionRendersAtinpackages/types/src/ui-action.tsas the control that must hit and does. No open objectui PR touches these three files. Same-round siblings disjoint by package: #7349 ownspackages/core; #7210 + #7225 own the four non-grid plugin views +packages/react+ the i18n gantt/view block — and both are fenced OFF this card'spackages/typesbarrel.Generated by Claude Code
os-project-manager commentedon Sep 2, 2026
Claim (dev seat): implementing under the 2026-09-02 ruling (comment 5509039514, C).
Session:
session_01EMrWaQw3XS5DxTHxp4yRyCBranch:
claude/issue-7182-declared-action-ids(pushed empty as the write-route probe, exit 0)Worktree:
objectui-issue-7182, baseorigin/maind53e472(moved past the dispatch'sd4c6a86; premises re-checked on this base:namesToResolve/needsLookupstill inrecord-quick-actions.tsxlines 86-98,headerActionIds/needsActionLookupstill incontainers.tsxlines 1268-1330,resolveDeclaredActionIdsabsent repo-wide,actionRendersAtcontrol hits inpackages/types/src/ui-action.ts).File surface as dispatched:
packages/types/src/ui-action.ts+ barrel,packages/components/src/renderers/layout/containers.tsx,packages/plugin-detail/src/renderers/record-quick-actions.tsx, their tests,.changeset/, plus the docs paragraph incontent/docs/guide/slotted-pages.mdthat currently teaches the mixed-array tolerance.Assignee untouched (PM's).
Generated by Claude Code
Generated by Claude Code
os-project-manager commentedon Sep 2, 2026
os-dev-report
{ "issue": 7182, "status": "done", "branch": "claude/issue-7182-declared-action-ids", "pr": "https://github.com/objectstack-ai/objectui/pull/7381", "premise_still_valid": true, "summary": "Ruling C implemented on base origin/main d53e472 (moved past the dispatch's d4c6a86; premises re-checked: namesToResolve/needsLookup and headerActionIds/needsActionLookup both still present, resolveDeclaredActionIds absent, actionRendersAt control hit). New in packages/types/src/ui-action.ts beside actionRendersAt: classifyDeclaredActions(elements) and resolveDeclaredActionIds(elements, registeredActions) with result types DeclaredActionsShape / DeclaredActionsResolution / DeclaredActionsRefusal (all five names had 0 prior declarations; KNOWN_COLLISIONS untouched; one-authority gate green); barrel exports added. containers.tsx (page:header) and record-quick-actions.tsx (record:quick_actions, for both actionNames and host actions) now both call it: an all-id array resolves by name in authored order, an all-object array passes through, a mixed array (or a null/number/nested-array element) is refused with a console.error naming the surface and the offending index and nothing authored is drawn (the bar's placeholder also names the index); no renderer throws. Tests: 14 function cases in types; the header's mixed-array tolerance case REPLACED by a refusal pin (it pinned the deleted branch); a quick_actions id/object equivalence + refusal pin; a two-renderer pin in plugin-detail. Changeset minor on types/components/plugin-detail with the ruling's wording; slotted-pages.md paragraph updated. PR is a DRAFT labelled needs:contract-review (written via REST additive POST, HTTP 200, read back present beside the size-labeler's labels) — the contract increment is ready for the in-seat review; not flipped ready, not enqueued. Deviations: (1) ZONE 2 #3 measured — spec 17.2.0 already refuses a mixed page:header.actions at path actions,1 and an all-object one at actions,0 (stricter than C), while RecordQuickActionsProps has no actions key at all (host channel only), so the runtime refusal lives in the shared function; not read as a fork, flagged in open_questions for the reviewer. (2) Two in-rule deltas on page:header: a blank '' id is no longer silently skipped (warned like any unresolved id) and ids are compared as authored without trim. (3) Dedup channel: REST repo-scoped read 200 but REST search 403, so MCP search_issues was used (1 targeted, 0 hits; 1 control that hit #7182 at rank 1). (4) Worktree removed without force after the PR opened; scratch under scratchpad/issue-7182/.", "tests": "All heavy runs via scripts/pm/os-verify-lock.sh (slot issue-7182), vitest from the repo root, per-part exit codes captured before any pipe. FINAL UNION on HEAD 7d692df after the commit: vitest of the 4 pin files + scripts/__tests__/one-authority-per-exported-name-6273.test.ts → 'Test Files 5 passed (5)', 'Tests 41 passed (41)', exit 0; OBJECTUI_DIST_PINS=1 vitest run --project dist on the rebuilt packages/components/dist (marker 'actions refused at index' present in dist/index.js) → 'Test Files 1 passed (1)', 'Tests 3 passed (3)', exit 0; pnpm --filter @object-ui/types|components|plugin-detail type-check → exit 0 ×3, script lines echoed, tsc --listFiles on each tsconfig.test.json lists all four test files (dependency closure built first: turbo run build --filter='@object-ui/plugin-detail^...' 11/11, because components and plugin-detail resolve @object-ui/types from dist). eslint per package on every touched file → 0 errors. check:control-bytes ✅ (plus manual grep of the untracked new files: no hits), check:phantom-deps ✅, check:self-import ✅, check:doc-fences ✅, check:doc-types ✅, check:action-forward-parity ✅, check-changeset-presence ✅ (1 changeset over 3 released packages), check-changeset-no-major ✅. NOT MEASURED (precondition not met by the gates' own wording): check:readme-exports exit 1 (36 packages unbuilt, population collapsed), check:doc-snippets exit 2 (needs the full snippet build; doc change is prose only), check:eager-closure exit 2 (needs the console vite build; no new package edge — plugin-detail already imported @object-ui/types); full pnpm lint and the test farm are CI's. RULING PINS, red-then-green, both red legs run from the committed tree with an EXIT/INT/TERM trap restoring by absolute path, restore verified by 'git diff HEAD' empty and blob hashes equal to HEAD's (RESTORE OK printed twice). Leg A = both renderers restored from BASE d53e472 under the new function + pins (on-disk: headerActionIds 4 hits, namesToResolve 3 hits, resolveDeclaredActionIds 0 hits in both): 4 failed / 26 passed — exactly the mixed-refusal pins (header refusal; bar refusal under actions and under actionNames; two-renderer refusal), the pre-fix bar failing inside ActionEngine.registerAction on the bare string. Leg B = ids arm ablated on HEAD (byName.get(id) replaced by undefined, ABLATION_7182_MARKER; on-disk grep -c marker=1, original line=0): 13 failed / 17 passed — header 7 id-path cases, bar id equivalence + actionNames case, two-renderer id case, 3 function cases; object-passthrough and refusal pins stayed green. Resolution path in the legs: vitest aliases @object-ui/types to packages/types/src, so the src mutation is what the tests read — no dist rebuild in the legs; the built bundle is measured by the dist pin. Pin 1 (mixed refused naming the index): red leg A, green HEAD. Pin 2 (all-id resolves through the shared function in BOTH renderers): red leg B in both, green HEAD. Pin 3 (all-object passes through in BOTH): green in leg A, leg B and HEAD — pre-existing behaviour, the live control, untouched by the ids ablation. Pin 4 (same buttons for the same authored array, object render as live control): declaredActionIds.twoRenderers-7182.test.tsx — header control == bar control, header ids == header control, bar ids == bar control, header ids == bar ids; red leg B, green HEAD. Spec measurement (node, @objectstack/spec 17.2.0 ComponentPropsMap.safeParse): page:header mixed → invalid_type at actions,1; all-object → actions,0; all-ids OK; record:quick_actions actions → unrecognized_keys; actionNames mixed → actionNames,1.", "mcp_calls": "9 — issue_read ×2 (card + comments), add_issue_comment ×2 (claim, this report), get_label ×1, create_pull_request ×1, search_issues ×2 (targeted + control), issue_write ×1 (finding). Label write, PR read-back and REST probes went over REST (repo-scoped reads 200; REST search 403).", "open_questions": [ { "question": "Which layer is 'refused loudly at validation' (ZONE 2 assumption 3)? Measured: the spec (objectstack, outside this repo) already refuses a mixed page:header.actions at index 1 — and an all-object array at index 0 — while record:quick_actions has no spec door for actions at all. This PR puts the runtime refusal in the shared pure function both renderers call, reported once via console.error, no throw. Confirm this reading at contract review; status is not blocked on it.", "options": ["A — as implemented: schema layer (spec, already index-naming) + runtime refusal in resolveDeclaredActionIds reported by each renderer, no throw", "B — additionally widen/annotate a zod schema in objectui to carry the mixed-array refusal: no such schema exists for page:header in @object-ui/types (spec is the authority) and the bar's actions key is host-only, so there is nothing in this repo's surface to attach it to", "C — renderer-time throw: explicitly not wanted by the dispatch"], "recommendation": "A, because the schema layer already carries the index-naming refusal wherever an authored array reaches it, and the ruling's 'both renderers call one function' is exactly the runtime half for documents and host channels the spec never sees" } ], "out_of_scope_findings": [ "filed as #7382: record:alert's single-id CTA lookup (record-alert.tsx 241-249, find by name over the same useMetadataItem read) is a third hand-written by-name resolver; route it through resolveDeclaredActionIds([ctaName], registered) once PR #7381 lands — observation-class, labelled finding, unassigned, Blocked-by objectui#7182" ] }Generated by Claude Code
Generated by Claude Code
os-project-manager commentedon Sep 2, 2026
os-dev-report
{ "issue": 7182, "status": "done", "branch": "claude/issue-7182-declared-action-ids", "pr": "https://github.com/objectstack-ai/objectui/pull/7381", "premise_still_valid": true, "summary": "Patch round after contract review PASS (supersedes report comment 5511901664). Required 1 done: the changeset now names the three behaviour changes — page:header no longer trims a padded id (' convert ' used to resolve) and warns on a blank '' id instead of skipping it; record:quick_actions renders nothing for an all-id actions array with no object bound instead of handing bare strings to the engine. Required 2 done: the PR body's claim that Studio's clientValidation reads ComponentPropsMap is withdrawn and corrected with the measured facts (clientValidation validates page via PageSchema whose component properties is an open z.record; ComponentPropsMap has 0 non-test code consumers under packages/*/src and apps/*/src; validateComponentProps appears only in comments) and the consequence is recorded verbatim: for a Studio-authored page, this PR's runtime refusal is the only refusal there is. Surface question answered by measurement: no need for classifyDeclaredActions or DeclaredActionsShape to be public — the renderers' only pre-lookup need is kind + ids, which resolveDeclaredActionIds(elements, undefined) returns; both renderers now call that, the classifier and its type are module-internal, the barrel exports one function + two types (DeclaredActionsResolution, DeclaredActionsRefusal), and the types test pins the classifier's absence from the barrel. No resolution/refusal logic changed. Base synced by merge to origin/main 1688986 (merge commit b1d670b, 0 overlap with this PR's files) — merge not rebase, per the no-force-push rule. Patch commit 189f71c pushed to the same branch; PR still draft, still labelled needs:contract-review (read back), not flipped ready, not enqueued; body PATCHed via REST and read back (0 tag-shaped hits, 'Fixes #7182' the only closing keyword; footer degraded to bare form as documented, session URL kept in prose). Worktree removed without force after the union.", "tests": "UNION on HEAD 189f71c (run after the patch commit, all heavy parts via scripts/pm/os-verify-lock.sh slot issue-7182, per-part exit codes captured): dependency closure turbo run build --filter='@object-ui/plugin-detail^...' → 11/11 (packages/types/dist rebuilt WITHOUT the classifier: 0 hits for classifyDeclaredActions in dist/index.d.ts and dist/index.js; packages/components/dist/index.js carries the 'actions refused at index' marker); vitest from the repo root of the 4 pin files + scripts/__tests__/one-authority-per-exported-name-6273.test.ts → 'Test Files 5 passed (5)', 'Tests 42 passed (42)', exit 0; OBJECTUI_DIST_PINS=1 vitest run --project dist → 'Test Files 1 passed (1)', 'Tests 3 passed (3)', exit 0; pnpm --filter @object-ui/types|components|plugin-detail type-check → exit 0 ×3 with script lines echoed, types test program lists the test file (--listFiles 1 hit); eslint per package on every touched file → 0 errors; check:control-bytes ✅, check:phantom-deps ✅, check-changeset-presence ✅ (8 source files of 3 released packages, 1 changeset), check-changeset-no-major ✅. Unchanged by this diff and green on the previous round (7d692df): check:self-import, check:doc-fences, check:doc-types, check:action-forward-parity. NOT MEASURED locally (precondition not met): check:readme-exports, check:doc-snippets, check:eager-closure — CI's. Ruling pins: the red legs (A: pre-fix renderers → 4 red, exactly the mixed-refusal pins; B: ids arm ablated → 13 red across both renderers and the function) were run on 7d692df and are unchanged in kind by this round (only the pre-lookup call site and exports moved); the reviewer's independent leg C (objects arm removed → 7 red) confirms pin 3; all four pins green on 189f71c. New pin this round: 'does NOT publish the shape classifier' ('classifyDeclaredActions' in barrel === false) plus the registry-independent verdict cases (kind + ids from resolveDeclaredActionIds(elements, undefined) agree with the resolved call over 9 populations).", "mcp_calls": "10 — previous round 9 (issue_read ×2, add_issue_comment ×2, get_label ×1, create_pull_request ×1, search_issues ×2, issue_write ×1) + this report comment. PR body PATCH, PR read-backs and the ComponentPropsMap measurement went over REST / local git.", "open_questions": [], "out_of_scope_findings": [ "filed as #7382 (previous round): record:alert's single-id CTA lookup is a third hand-written by-name resolver; route it through resolveDeclaredActionIds([ctaName], registered) once PR #7381 lands — observation-class, labelled finding, unassigned, Blocked-by objectui#7182" ] }Generated by Claude Code
Generated by Claude Code
os-project-manager commentedon Sep 2, 2026
ACCEPT — clause-② contract review PASS, amendments verified. Reviewer of record:
domain:uiseat, sessionsession_01EMrWaQw3XS5DxTHxp4yRyC, R1PR #7381, head
189f71c. The review ran atCONTRACT_REVIEW_TIER(claude-fable-5, read this round fromscripts/pm/dispatch-gates.mjs:8091) as an isolated sub-task — this seat runs opus and is not itself at tier, so it could not be the compensating control. In-seat review per the 2026-08-31 ruling; the reviewer pushed nothing, flipped nothing, enqueued nothing.Verdict: PASS, conditional on two text amendments and one surface question — all three now discharged
I verified each against the branch, not against either report.
' convert ') no longer resolves (ids compared exactly as authored); a blank''id is now reported by the unresolved-id warning instead of silently skipped; an all-idactionsarray with no object bound now renders nothing instead of handing bare strings to the action engine. It also frames the narrowing correctly and carries a migration instruction — "convert each array whole … never one element at a time."clientValidationclaimclientValidationreads that map. It does not", withPageSchema's openz.record(z.string(), z.unknown()), the 0-non-test-consumer measurement, and the consequence recordedclassifyDeclaredActions0,DeclaredActionsShape0 — live control fires (resolveDeclaredActionIds,DeclaredActionsResolution,DeclaredActionsRefusal, lines 1042–1065). Public surface 3, not 5, and a new pin asserts the classifier's absence from the barrel, withdistverified at 0 hitsThe spec-vs-ruling tension: adjudicated, not a fork
The lane measured that spec
17.2.0refuses an all-objectpage:header.actionsatactions,0— stricter than ruling C, which declares all-object legal. The reviewer re-measured independently (same version, single lockfile entry, controls that hit) and resolved it:layout:page-header→record:quick_actionsdelegation (no spec door), the page synthesizer, and any Studio-authored page, because nothing in objectui runsComponentPropsMapat all.What the review added that the lane's own run did not
⭐ An independent Leg C ablation — remove the objects arm → 7 red. That is what converts pin 3 from "green in every state, therefore proving nothing" into a genuine control. Also Leg D (index forced to 0) → 8 red across all four files, which pins the index end-to-end rather than merely "refused". Both legs are the kind of check a green suite cannot supply on its own.
Recorded for the ruling's own
Restart-whencardobjectsarm is now a public union member on@object-ui/types. Retiring the inline-object tolerance once hotcrm is all-ids will therefore itself be a clause-② narrowing on a published package — that card must be dispatched atCONTRACT_REVIEW_TIER, not treated as cleanup.Not measured, stated rather than implied
Spec history before 17.0.0 (both clones shallow — verified at 17.0.0 and 17.2.0 only); hotcrm's live all-object arrays (asserted by the ruling, hotcrm not in session); the full farm and dist pins locally — CI's, green on head.
Next
needs:contract-reviewcomes off and the PR flips ready + auto-merge when every check on189f71cis green — at the time of writing 27 of 30 are done with 0 failed and the test shards still running. ⛔ Not before: enqueue eligibility here is every check, not the required subset.Generated by Claude Code
os-project-manager commentedon Sep 2, 2026
Landed — PR #7381 merged as
b65251465. Verified by CONTENT onorigin/main, with a live control.domain:uiexecution seat, sessionsession_01EMrWaQw3XS5DxTHxp4yRyC, R1.pm:dispatchedstripped in the same stroke;priority:p2+domain:uistand.origin/mainresolveDeclaredActionIdsexported frompackages/types/src/index.tsactionRendersAt, same barrelresolveDeclaredActionIdsincontainers.tsx/record-quick-actions.tsxclassifyDeclaredActionsin the barrel (must be absent)⭐ That last row is the one I care about most: the surface reduction survived the merge. Ruling C required one function; the first delivery exported five names. The contract review flagged two as beyond the minimum, the dev measured that
resolveDeclaredActionIds(elements, undefined)already returns the registry-independent verdict the renderers need before their lookup, and made the classifier and its type module-internal — with a pin asserting the classifier's absence from the barrel anddistverified at 0 hits. Public surface 3, not 5, permanently.This mattered because #7225 landed in the same round — a card that exists because a hook was published with one adopter and now owes compatibility forever. Repeating that on the same day would have been careless.
Clause-② disposition
Reviewed in-seat at
CONTRACT_REVIEW_TIER(claude-fable-5) as an isolated sub-task; this seat runs opus and could not be its own compensating control. Verdict PASS, conditional on two text amendments and one surface question, all three discharged and re-verified by me against the branch (full record: comment 5512413824).needs:contract-reviewwas stripped only after that, with a compare read-back showing the gate off and nothing else disturbed.The spec-vs-ruling tension the lane surfaced — spec
17.2.0refuses an all-objectpage:header.actions, which ruling C declares legal — was adjudicated as not a fork: the objects arm is live on three channels the spec never sees, nothing in objectui runsComponentPropsMapat all, and the ruling knowingly time-boxed the tolerance.Restart-whencardThe
objectsarm is now a public union member on@object-ui/types. Retiring the inline-object tolerance once hotcrm is all-ids is therefore itself a clause-② narrowing on a published package — that card must be dispatched atCONTRACT_REVIEW_TIER, not treated as cleanup. Writing it here so it is not rediscovered the hard way.Downstream
pm:blocked— its unlock predicate is a read of the published@objectstack/*artefact, so it waits for a release, not for this merge.record:alert's CTA lookup is a third hand-written by-name resolver beside the one objectui#7182 unified #7382 (record:alert's single-id CTA lookup, the third hand-written by-name resolver) wasBlocked-bythis card and is now unblocked.Generated by Claude Code
fix(plugin-detail): record:alert resolves its CTA through resolveDecl…