Skip to content

feat(spec)!: object-map, object-gantt and object-tree type navigation by reference, and one pin enumerates every z.unknown() member of ComponentPropsMap (#21464, stage 1) - #21502

Merged
objectstack-fleet[bot] merged 7 commits into
mainfrom
claude/issue-21464-component-props-unknown-closeout
Oct 3, 2026
Merged

objectstack-fleet[bot] merged 7 commits into
mainfrom
claude/issue-21464-component-props-unknown-closeout

Conversation

@objectstack-fleet

@objectstack-fleet objectstack-fleet Bot commented Oct 3, 2026 •

Copy link
Copy Markdown
Contributor

Part of #21464
Clause-②: yes (narrowing)

What this does

This is the first stage of the ComponentPropsMap z.unknown() close-out (triage 5961300594, claim 5962381903). It does two things.

  1. navigation is typed on object-map, object-gantt and object-tree. Each row took z.unknown(), although each renderer hands the member to the console's shared navigation hook, which reads navigation.mode and falls back to page. Any value passed the component-props gate, and an off-shape one opened the record page in silence. Each row now takes the list view's NavigationConfigSchema by reference. That is the block object-grid, object-kanban, object-calendar and object-timeline already take.
  2. One enumeration pin holds the whole map. packages/spec/src/ui/component-props-unknown-members.pin.test.ts walks every ComponentPropsMap row, into arrays, record values, union arms, lazy schemas and catchalls. It fails on any z.unknown() member that is neither typed nor listed with a recorded reason. It also fails on a ledger line whose member is no longer z.unknown(), so the ledger can only shrink as later stages type members.

Why Part of, not a closing line: the census found more than one reviewable PR's worth (the dispatch's size rule, A7). There are 31 renderer-read members. The precedent, PR #21463, spent about 1,100 lines on 7 members. The other 28 are recorded in the pin as staged, each with its reader at the pin and a named stage. The staging proposal below is in the report for a ruling. #21464 remains open for those stages.

navigation, measured at the .objectui-sha pin 89cad75d55

row was now read point at the pin
object-map z.unknown() NavigationConfigSchema, by reference plugin-map/src/ObjectMap.tsx:1189 hands it to useNavigationOverlay, which reads navigation?.mode ?? 'page' (react/src/hooks/useNavigationOverlay.ts:364)
object-gantt z.unknown() NavigationConfigSchema, by reference plugin-gantt/src/ObjectGantt.tsx:1960 reads schema.navigation ?? { mode: 'drawer' } and hands it to the hook (:2025-2026)
object-tree z.unknown() NavigationConfigSchema, by reference plugin-tree/src/ObjectTree.tsx:1054 hands it to the hook (:1046)

objectui's own node types and Zod mirrors already declare this member as ViewNavigationConfig or the spec's NavigationConfigSchema (types/src/objectql.ts, types/src/zod/objectql.zod.ts). The spec row now agrees with its sibling.

No measured writer is refused (A3).

  • objectstack, on the merged head: 5 files per row name the type. None authors navigation on one of these blocks. The one co-mention writes app navigation arrays. Control: objectName is found in 4 of the 5 files per row.
  • objectui, at the pin: 88 / 98 / 59 files name object-map / object-gantt / object-tree. Control: objectName is found in 55 / 70 / 40 of them. They contain 77 navigation: lines. Every distinct authored literal parses on all three rows, 36 of 36: { mode } with each of the seven modes, plus size: 'lg' and openNewTab.
  • The values that do not parse are probes that already expect a refusal. navigation: 'anything' is a both-faces parity probe. navigation: 'drawer' sits under a @ts-expect-error. { mode: 'sideways' } is an expected navigation.mode issue.
  • The { mode: 'drawer', width: '600px' } example belongs to object-view (plugin-view/src/ObjectView.tsx:1060), not these rows.

The census (A1), whole

The census covers 107 z.unknown() members across 56 rows, on the merged head. It is unchanged from the base minus the three this PR types. Writer counts are file co-mentions (a file naming the type and authoring KEY:), with a control key. They are an upper bound, not a value census: each later stage runs its own value-level census first.

31 members a renderer reads with a fixed shape. Citations are objectui at 89cad75d55. Writers are "os N/M (control)" for objectstack and "ou N/M (control)" for objectui.

row key reader at the pin by-reference candidate writers decision
object-map / object-gantt / object-tree navigation see the table above NavigationConfigSchema os 0 on the row · ou 36/36 literals parse typed here
object-metric aggregate plugin-dashboard/src/ObjectMetricWidget.tsx:250, .field / .function at :404 chart aggregate (chart.zod.ts:644) os 7/8 (7) · ou 17/36 (20) stage object-metric
object-metric trend ObjectMetricWidget.tsx:254 (typed :176) none: to the renderer's read os 0/8 · ou 1/36 stage object-metric
object-metric drillDown ObjectMetricWidget.tsx:265 (ObjectMetricDrillDownConfig :218) chart drillDown (chart.zod.ts:663) os 0/8 · ou 7/36 stage object-metric
object-metric compareTo ObjectMetricWidget.tsx:267 (CompareToConfig :241) dashboard widget compareTo (dashboard.zod.ts:1205) os 1/8 · ou 4/36 stage object-metric
object-form fields[] plugin-form/src/ObjectForm.tsx:961 none os 11/14 (12) · ou 178/210 (188) stage object-form
object-form customFields ObjectForm.tsx:755, :1180 none os 0/14 · ou 21/210 stage object-form
object-form sections[] ObjectForm.tsx:364, :1518 FormViewSchema.sections os 3/14 · ou 78/210 stage object-form
object-form contentLayout plugin-form/src/ModalForm.tsx:854 ('simple' or 'tabbed', :151) none os 0/14 · ou 7/210 stage object-form
object-form submitBehavior ObjectForm.tsx:1312-1313 FormViewSchema.submitBehavior os 2/14 · ou 12/210 stage object-form
object-form navigateOnSuccess ObjectForm.tsx:1373, :1412 none os 0/14 · ou 5/210 stage object-form
object-form mobile ObjectForm.tsx:1857 none os 0/14 · ou 7/210 stage object-form
object-master-detail-form sections[], fields[] plugin-form/src/MasterDetailForm.tsx:1692-1693, into the parent form as object-form os 2/4, 4/4 (4) · ou 9/29, 18/29 (21) stage object-form
object-grid columns[] plugin-grid/src/ObjectGrid.tsx:2158 (normalizeColumns, string or ListColumn) ListViewSchema.columns os 7/21 (16) · ou 198/292 (248) stage list-family
object-grid fields[] ObjectGrid.tsx:1946 none os 7/21 · ou 191/292 stage list-family
object-grid selection ObjectGrid.tsx:4799-4810 (.type) ListViewSchema.selection os 1/21 · ou 19/292 stage list-family
object-grid selectable ObjectGrid.tsx:4813-4815 none os 0/21 · ou 7/292 stage list-family
object-grid rowActions[] ObjectGrid.tsx:1834-1835 (string[]) ListViewSchema.rowActions os 1/21 · ou 19/292 stage list-family
object-grid bulkActions[], batchActions[] ObjectGrid.tsx:4763 (batchActions ?? bulkActions) ListViewSchema.bulkActions os 2/21, 0/21 · ou 27/292, 7/292 stage list-family
object-kanban columns[] plugin-kanban/src/KanbanBoardCore.tsx:95 none os 4/12 (9) · ou 87/152 (99) stage list-family
object-calendar calendar plugin-calendar/src/ObjectCalendar.tsx:296-297 (ObjectCalendarConfig) ListViewSchema.calendar os 1/6 (6) · ou 50/83 (62) stage list-family
object-gantt markers[] plugin-gantt/src/ObjectGantt.tsx:2497 (GanttMarker) none in the spec os 0/5 (4) · ou 6/98 (70) stage objectui-held
object-timeline items[] plugin-timeline/src/ObjectTimeline.tsx:587 none in the spec os 3/3 (3) · ou 12/45 (34) stage objectui-held
object-timeline mapping ObjectTimeline.tsx:551, :576-579 (TimelineMappingSchema) none in the spec os 2/3 · ou 2/45 stage objectui-held
action:group actions[]{} components/src/renderers/action/action-group.tsx:303 (UIActionSchema[]) none in the spec (UIActionSchema is objectui's interface) os 2/3 (label 3) · ou 23/36 (26) stage objectui-held
action:menu actions[]{} components/src/renderers/action/action-menu.tsx:339 (UIActionSchema[]) as above os 2/4 (3) · ou 23/38 (28) stage objectui-held
object-kanban conditionalFormatting KanbanBoardCore.tsx:114, evaluated at KanbanImpl.tsx:179 ListViewSchema.conditionalFormatting os 1/12 · ou 9/152 held for a ruling (A3)

76 members with a recorded reason, grouped:

  • slot (7): child-component lists. Each is a declared slot position (§2 checks this), judged at its own node: page:tabs / page:accordion items[].children[], page:card children[] / footer[], page:footer / page:sidebar / page:section children[].
  • runner (16): action:button and action:icon params, bodyExtra, bodyShape, operation, patch, toast, resultDialog and onSuccess. Each .describe() says "forwarded to the runner" (§2 checks this).
  • records (18): record rows and field values, shaped by the bound object's fields: staticData[], data[], data.items[], initialValues{} / initialData{}, and the grid bulk action's patch{} / params[].default.
  • shared (35): members of a schema another file owns. These are the expression AST beside every source (14), the HTTP data source's params{} / body (16), the inline JSON Schema document (4), and the bulk-action option entry, deliberately open (1).

3 one-off reasons:

  • element:definition-list items[].description is shown as-is (components/src/renderers/basic/data-list.tsx:68, toText).
  • object-grid pagination.* is a deliberately open z.looseObject, whose named members are typed.
  • ai:chat_window context{} has no reader: objectui registers no renderer for the row (components/src/renderers/placeholders.tsx:109-113). Per A2 it is an open question, not a verdict (see the report).

Held for a ruling: object-kanban conditionalFormatting (A3)

The list view's own conditionalFormatting, the shape the grid took in PR #21463, refuses both rule dialects that objectui's own kanban fixtures author:

  • { field, operator, value, backgroundColor } (plugin-kanban/src/__tests__/ObjectKanban.structuredMembersReachTheirSinks-8313.test.tsx:463) is refused with unrecognized_keys and invalid_type@style.
  • { condition, backgroundColor } (:479) is refused with invalid_type@style and unrecognized_keys[backgroundColor].
  • objectui's own types/src/__tests__/kanban-conditional-formatting.test.ts:29-52 asserts that its kanban schema accepts both, and a mix of the two.

So the narrowing is not shipped. It is recorded as staged('held-for-decision', …) and reported with options.

The pin, red then green (A4), and its ablation

  • Green on the final head: vitest run --project local … component-props-unknown-members.pin.test.ts reports Tests 51 passed (51).
  • Ablation. The mutation reverts object-map's navigation to z.unknown(), with no ledger line, through node scripts/ablation-replace.mjs in WRAP mode under the verify lock. It landed: anchor x1 → x0, replace x0 → x1, blob 58b6e27fbc → 7e3b411604.
    • The pin went red: Tests 8 failed | 43 passed (51).
    • §1 received ["object-map navigation"] against an expected [].
    • §1's census read expected 108 to be 107.
    • The six object-map §4 cases failed (unwrap identity, parse equality, four refusals).
  • Restore, proven by blob: blob after restore 58b6e27fbc… == blob at HEAD 58b6e27fbc…, git diff HEAD empty. The rerun was green again: Tests 51 passed (51).
  • The pin imports ./component.zod from source, so no build sits between the mutation and the run.

Gates, on c222ab563b

  • Fix round after review 5963877733: merged origin/main 85e29b8858 and regenerated content/docs/references/ui/component.mdx (the optionality face fd96a8473d renders). On c222ab563b, check:generated, check:migration-registry, spec test, spec typecheck and check:doc-authoring all exit 0. The other five files are byte-unchanged since d3431deed3, where the readings below were taken.

  • node scripts/pm/dispatch-gates.mjs --repo objectstack-ai/objectstack --commands (no paths; merge base 100c394f6f, 6 changed paths) derived 114 commands. Every exit code was written to disk before anything read it. All 114 exit 0, reconciled with --ran.

    • Six spec gates first answered PREREQUISITE NOT MET (exit 3), because dist/ predated the final pin edit. They were rerun after pnpm --filter @objectstack/spec build at the final head, together with the rest of the spec family.
  • pnpm --filter @objectstack/spec test: Test Files 602 passed | 1 skipped (603), Tests 17832 passed.

  • pnpm --filter @objectstack/spec typecheck: exit 0, with check:test-typecheck: OK — … test layer compiles under packages/spec/tsconfig.test.json. The package tsconfig.json excludes **/*.test.ts, so it is the test layer that covers the pin.

  • scripts/step18-rationale-merge.test.ts (--project repo): Tests 9 passed (9).

  • check:generated: ✓ All 15 generated artifacts are up to date.

  • check:migration-registry: ✓ src/migrations/registry.ts is current (353 semantic, 246 retired-key, 217 retired-def).

  • check:liveness, check:strictness-ledger and check:authorable-surface are green. authorable-surface/ui.json and the strictness counts did not move. component.mdx is regenerated.

  • check:doc-authoring: ✓ doc authoring guard: 17287 customer-facing string(s) … clean.

  • check:nul-bytes: check-nul-bytes: OK (… no raw ASCII control bytes).

  • Semver: @objectstack/spec minor, BREAKING banner, Clause-②: yes (narrowing), and the ADR-0087 marker registered ui-object-map-gantt-tree-navigation-typed.

    • check-adr-0087-registration: ✓ 1 declared-breaking changeset(s), each carrying an ADR-0087 disposition.
    • check-changeset-no-major --base origin/main: ✓ This diff introduces no major bump.
    • The level axis, run with this body as the --event payload, is quoted in the report.

The retirement kit

  • D3 entry: ui-object-map-gantt-tree-navigation-typed, a sibling of ui-object-grid-row-members-typed.
  • Step-18 rationale fragment: order 64, sorted by id.
  • Regenerated: migrations/registry.ts and content/docs/references/ui/component.mdx.
  • No conversion is registered: page-component properties is not parsed on the save or load path, and an off-shape value has no rewrite that honours what the author meant.

Acceptance notes

  • The hot-file check (A8): no open PR touches ui/component.zod.ts, component.mdx, migrations/registry.ts, authorable-surface/ui.json or the pin. Seven of the eight open PRs were read in full. The bot Version Packages PR was read only to its first file page, because the proxy refused its pagination.
  • Deployed metadata was not measured.
  • The Console Pin Gate is CI's. objectui at the pin already types these members as NavigationConfig.
  • The ai:chat_window row has zero authored writers in either repository. objectui excludes it from the palette and treats it as a tombstone. Whether it retires is in the report's open questions, not in this PR.

Resume

A container restart stopped the first run after it pushed c7bb62ae6f, 527957de24 and 9a8037abbe, and before it opened a PR. This run made the following changes:

  • confirmed the staging decision against the census;
  • merged origin/main through os-regen-merge.sh (no regeneration was owed);
  • re-measured the navigation writers and the kanban refusal;
  • corrected the pin's two stage reasons to name the real candidates;
  • finished the changeset;
  • ran every gate on the final head.

Generated by Claude Code

claude added 5 commits October 2, 2026 22:35
…by reference, and one pin enumerates every z.unknown() member of ComponentPropsMap (#21464)

Claude-Session: https://claude.ai/code/session_01YDt3PzwfrkuFzUBF89WPmM
Co-authored-by: Claude <noreply@anthropic.com>
…avigation members (#21464)

Claude-Session: https://claude.ai/code/session_01YDt3PzwfrkuFzUBF89WPmM
Co-authored-by: Claude <noreply@anthropic.com>
… stage reasons, re-measured on the merged head (#21464)

Completes the wip commit 527957d. The D3 entry and the step-18
rationale fragment stand as written (order 64 is still one above the
highest after merging origin/main 100c394). The changeset's measured
census is re-anchored to the merged head, with its controls, and its
summary now matches the ledger: 107 listed members, 28 renderer-read,
27 staged and one held for a ruling. The pin's object-metric and
objectui-held stage reasons now name the real by-reference candidates.

Claude-Session: https://claude.ai/code/session_01YDt3PzwfrkuFzUBF89WPmM
Co-authored-by: Claude <noreply@anthropic.com>
@github-actions

github-actions Bot commented Oct 3, 2026 •

Copy link
Copy Markdown
Contributor

📓 Docs Drift Check

5 anchor(s) derived from 1 changed package(s); no hand-written page names any of them, so this run has nothing to list — not a clean bill of health. This check sees only pages that NAME a derived anchor: one that documents this change in prose, or enumerates it in an authoring dialect, names none and stays invisible to it on every run.

What this run could not see
  • 6 name(s) were too generic to anchor anything (single lowercase words)
  • the SDK route bridge reached 54 of 206 client-bound route-ledger rows — the other 152 have no registrar path: tail to select them, so pages documenting THEIR client methods cannot appear above, on this or any run. Of those 152: 0 are remediable by widening that discovery convention (an in-repo file declares the path; the convention did not scan it); 55 are structural — on a ledger where NOT ONE row is declared in-repo, so no discovery change reaches them at any price; 97 are undecided (no in-repo declaration, on a ledger that has other in-repo registrars — absence and an unreadable spelling are not distinguishable here). The rows themselves: node scripts/docs-audit/affected-docs.mjs --bridge-coverage
  • a page that states a rule by its inputs shares no identifier with the emitter that implements the rule, so an emitter-only diff cannot list it — not on this run and not on any run. Measured on fix(driver-sql): emit varchar(maxLength) for a text field a declared index keys on #11430: content/docs/protocol/objectql/types.mdx documents the text-family column mapping by the ObjectQL type names it maps FROM (text / textarea / html) while the diff changed createColumn; it went unlisted, and it was the page that diff falsified, in four places. No shared token exists to detect this on, so a rule your change carries has to be re-read by hand in the pages that restate it.
  • a key NAME is not a key, so the hand re-read the line above prescribes can land on the wrong schema. The same spelling is authorable on one governed type and a [REMOVED] tombstone on another for each of active, aria, joins, objects, template, tools and version (censused on [finding] tools is a key on BOTH AgentSchema (tombstoned, dead) and SkillSchema (live, cloud-attested), so a name-based search attributes skill examples to the agent key — it produced a false stop-the-line alarm on PR #19059 #19093 over the liveness ledger's governed types, top-level keys); nothing in a search result distinguishes the two, so a grep hit on a LIVE example reads as evidence about the DEAD key. Measured on fix(spec): the agent.tools liveness row says dead — it claimed live on a key the schema tombstoned #19059: content/docs/ai/agents.mdx was reported as contradicting the agent.tools tombstone over its tools: example at :161, which is inside the defineSkill({ block opened at :155 — the page was already correct. Settle ownership by PARSING the value against both schemas, never by the name: that literal PASSES SkillSchema, and as an AgentSchema it FAILS at tools with the tombstone prescription. ⛔ These names are not the whole class — a key retired through a .strict() guidance map leaves no tombstone in the walked shape and none of them here (tool.category, live as AIToolDefinition.category).

Coarse fallback — 138 page(s) merely mention a changed package (the pre-#9192 predicate, kept for the deliberately-wide backstop): node scripts/docs-audit/affected-docs.mjs --json 85e29b8858b55a6dab39f8b7f004290743a25da8 → packageMentionDocs.

Which tree this was computed on

This run read content/docs from 0002894704ae3ffeac0bc2aa0dc1e50b77b5f2f9 — the merge of head c222ab563b65319ac713f766ad58b088ba46890a into base 85e29b8858b55a6dab39f8b7f004290743a25da8, which is what actions/checkout gives a pull_request run. Not the PR head.

A worktree cut from an older main holds a different content/docs, so re-deriving there can legitimately return a different list — that is a different tree, not a wrong row. To answer on the same tree:

# while this PR is open — GitHub drops the merge commit once it closes
git fetch origin 0002894704ae3ffeac0bc2aa0dc1e50b77b5f2f9 && git checkout 0002894704ae3ffeac0bc2aa0dc1e50b77b5f2f9
# afterwards, rebuild it from the two parents, which stay fetchable
git fetch origin 85e29b8858b55a6dab39f8b7f004290743a25da8 c222ab563b65319ac713f766ad58b088ba46890a && git checkout -B drift-repro 85e29b8858b55a6dab39f8b7f004290743a25da8 && git merge --no-ff c222ab563b65319ac713f766ad58b088ba46890a

node scripts/docs-audit/affected-docs.mjs --json 85e29b8858b55a6dab39f8b7f004290743a25da8

⚠️ That checkout carried uncommitted changes, so the commit above does not fully identify what was read.

@objectstack-fleet

Copy link
Copy Markdown
Contributor Author

Contract review

Served-tier: CONTRACT_REVIEW_TIER
Head-sha: d3431deed339c1825fa5831af79cd92e0f156e83
Local-runs: none

Inputs read: card #21464 (body; comments 5961300594 triage, 5962381903 claim, 5963737459 dev report, 5963787404 seat answer), PR #21502 (body, the 6-file list, the net diff against main, the docs-drift bot comment), the precedent PR #21463 (body, 14-file list, diff, its D3 entry and changeset at aa4632235b), and the 34 check-runs on this head. Source was read at refs only: objectstack at d3431deed3, 6f17d1d364 and fd96a8473d; objectui at the pin 89cad75d55. Nothing was built, run or re-run.

Gate verdicts on this head, as they stood at this read (2026-10-03, about 00:55Z): 28 success · 2 failure — Type Check · source gates (red at the step "Check generated reference docs are in sync with the spec"; every later step of that job skipped) and its rollup TypeScript Type Check · 2 in progress — Test Core (1/6), Test Core (4/6) · 2 skipped — Console Pin Gate, Packed-tarball smoke (opt-in). The red is the verdict; its cause is established below from the trees, not from a run.

① Derived judgments

1. navigation on object-map / object-gantt / object-tree — RIGHT. Each row now takes NavigationConfigSchema.optional() by reference (component.zod.ts :5683 / :5909 / :6248 at the head), the same def the grid, kanban, calendar and timeline rows carry (:4194 / :4912 / :5080 / :6566). The readers at the pin match that shape: plugin-map/src/ObjectMap.tsx:1188-1192 and plugin-tree/src/ObjectTree.tsx:1046-1054 hand schema.navigation to useNavigationOverlay; plugin-gantt/src/ObjectGantt.tsx:1960 reads schema.navigation ?? { mode: 'drawer' } and its .mode, then hands it on at :2025-2026; the hook (react/src/hooks/useNavigationOverlay.ts:149, :212, :364) types the option as the authored input of the spec's own NavigationConfigSchema and reads navigation?.mode ?? 'page' plus resolveOverlayWidth(navigation). The pin's §4 proves def identity (.unwrap()._zod.def is NavigationConfigSchema._zod.def), parse equality, all seven modes, and four refusals with code AND path. The gantt .describe() records the renderer's { mode: 'drawer' } absent-key default, which matches :1960.

Writer census — SOUND; no measured writer refused. Re-derived read-only at the pin: every distinct authored navigation: literal in a file naming one of the three types is { mode } with one of the seven modes, some with size, openNewTab, preventNavigation or the deprecated width, all of which the schema declares. The non-parsing values are refusal probes ('anything' parity probe, { mode: 'sideways' } / { mode: 'not-a-mode' } expected-issue probes, a bare 'drawer' under @ts-expect-error) or prose in docblocks. My file counts differ from the dev's 88 / 98 / 59 (a broader substring matcher finds 168 / 153 / 91), which changes nothing: the conclusion is on values, and it holds. objectstack at the head: the only navigation: co-mentions outside packages/spec/src are CHANGELOG prose and this PR's own changeset — 0 writers on the three rows, as reported.

2. The enumeration pin — RIGHT, with one recorded blind spot. §1 fails on an unlisted z.unknown() (census() keys not in LEDGER) and on a stale ledger line (LEDGER keys not in census()); the size-equality control makes an empty walk impossible to pass. The dev's ablation (object-map navigation reverted with no ledger line → 8 failed, §1 names exactly that member; blob-proven restore) is consistent with that construction. The walk descends optional / nullable / default / prefault / readonly / catch / nonoptional wrappers, pipes on the input side, lazies, arrays, records, union arms, intersections, object shapes and catchalls, and §3 lights each position. The reasons, each checked against the source: slot (7) — §2 checks them against pageComponentSlotPositions() (component.zod.ts:6941); runner (16) — the action rows' params, bodyExtra, bodyShape, operation, patch, toast, resultDialog, onSuccess on action:button / action:icon; at the pin action-button.tsx:181-298 forwards each into the runner's ActionDef, the row's docblock (:3098-3104) records that posture, each .describe() says "forwarded to the runner" and §2 checks the phrase — exactly the class triage's direction keeps as z.unknown(); records (18), shared (35), any-value (1) (data-list.tsx:68, toText), open-bag (1) (pagination is a z.looseObject), no-reader (1) (placeholders.tsx:109-113 omits ai:chat_window on purpose; no registration anywhere at the pin, and app-shell's palette excludes it) — all honest. Staged (27) + held (1): 16 of the cited readers were resolved at the pin (ObjectMetricWidget.tsx:250 / :254 / :265 / :267, ObjectForm.tsx:961, :1312-1313, ModalForm.tsx:854, ObjectGrid.tsx:2158, :4763, :4799-4815, KanbanBoardCore.tsx:95, :114, ObjectCalendar.tsx:296-297, ObjectGantt.tsx:2497, ObjectTimeline.tsx:551, :587, action-group.tsx:303, action-menu.tsx:339); each is a real read of the named member. No staged or runner line hides a member this stage owed under triage's direction: the claim's size rule says census first, build the first group, then return a staging proposal before building past it, and the first group is the card's own named reach — the three navigation rows typed, kanban held under the caveat. Blind spot, not a defect: the predicate is def.type === 'unknown' only, so a future z.any() member would pass unlisted; component.zod.ts carries 0 z.any() today and the card's direction names z.unknown().

3. Staging under the claim's size rule — SOUND; Part of #21464 — RIGHT. The precedent spent 1,167 added lines on 7 members, about 140 non-generated lines per member; 28 × 140 ≈ 3,900 is a fair projection and more than one reviewable PR. The claim's own rule ("the dev stops after the census and returns a staging proposal before building past the first group") is followed to the letter. The Part-of gate is green and the card stays open for the stages.

4. The held item, object-kanban conditionalFormatting — RIGHT. ListViewSchema.conditionalFormatting (view.zod.ts:2819-2834) is a strict { condition, style } with aliases and a color guidance. objectui at the pin authors both other dialects in its own kanban fixtures (plugin-kanban/src/__tests__/ObjectKanban.structuredMembersReachTheirSinks-8313.test.tsx:459-485: { field, operator, value, backgroundColor } and { condition, backgroundColor }) and asserts both on its ObjectKanbanSchema (types/src/__tests__/kanban-conditional-formatting.test.ts:25-52); KanbanImpl.tsx:179 evaluates them through the shared evaluator. The by-reference narrowing would therefore refuse a measured writer, and triage's caveat says report, not ship silently — which the pin's held-for-decision line and the report do. The precedent could ship the grid's narrowing because its census found no writer of either dialect; the kanban census does. Observation: the kanban row's .describe() ('Card conditional formatting rules') carries no hint of the hold, so the recorded reason lives in the pin alone — acceptable under the card's "listed with its recorded reason".

5. The retirement kit — three of four RIGHT, one STALE.

  • D3 entry 18.ui-object-map-gantt-tree-navigation-typed.ts: a faithful sibling of ui-object-grid-row-members-typed (surface / replacement / reason / acceptanceCriteria; "D3 only", no conversion, with the reason recorded; the bare-mode-string rewrite spelled out). RIGHT.
  • Step-18 rationale fragment, order 64: unique in STEP18_RATIONALE (the duplicated orders 56 / 60 / 62 are pre-existing on main, not this PR's); the tracker citation in its text follows the file's convention (88 such citations), and check:doc-authoring is green. RIGHT.
  • Generated registry region: byte-identical to the entry file, leading comment included, as the generator emits the precedent's. RIGHT.
  • Regenerated references page content/docs/references/ui/component.mdx: STALE against main — the failed gate. Timeline from the trees: the dev merged main at 100c394f6f (merge commit 58b235eba2, 23:32Z) and regenerated; fd96a8473d (fix(spec-docs): one shared def renders one optionality face on every reference page #21478, "one shared def renders one optionality face on every reference page") landed on main at 23:35Z — three minutes later — changing packages/spec/scripts/lib/format-type.ts so the Type-cell shape summary marks every defaulted member with ?; the final commit d3431deed3 (23:41Z) and the PR (00:37Z) followed with no second merge. On main@6f17d1d364 every typed navigation Type cell reads { mode?: …; preventNavigation?: boolean; openNewTab?: boolean; size?: …; … }, and fd96a8473d itself rewrote the grid / calendar / timeline rows from the old face to that one. The PR's three NEW rows for ObjectGanttProps, ObjectMapProps and ObjectTreeProps (head component.mdx :587, :954, :1189) carry the superseded face { mode: …; preventNavigation: boolean; openNewTab: boolean; size: …; … }. On the merge commit the generator renders them with ? and the sync check reds — the one row of AGENTS.md §10 / §11 ("never trust git's textual merge of a generated file") this head trips. The dev's STALE TREE note was honest when written: it named scripts/engine-double-contract.pinned.json against origin/main 5555047117, which predates fd96a8473d. The remedy is mechanical and seat-side: merge origin/main (now 85e29b8858), pnpm --filter @objectstack/spec build and check:generated --fix, commit, push; the new head then needs its own record.

② Semver level

RIGHT, and identical in shape to the precedent. .changeset/21464-component-props-navigation-typed.md: '@objectstack/spec': minor, a feat(spec)!: title, Clause-②: yes (narrowing), the BREAKING banner with the launch-window minor convention sentence, the adr-0087 registered marker naming the one id ui-object-map-gantt-tree-navigation-typed (the precedent named three because it also carried a D2 conversion and a retired-key entry; this PR registers no conversion, with the reason recorded in the D3 entry), a FROM → TO table, and a measured "Who is affected" section. Check Changeset is green on this head, and the dev's check-adr-0087-registration and check-changeset-no-major readings agree with the text. Observations, not defects: the changeset title and third bullet describe the enumeration pin (a test) and promise that "27 of them are typed in later changes" — release-notes prose about internal tests and future work.

③ Boundary flags

  • main merged into the branch (58b235eba2) through os-regen-merge.sh — seat matter; AGENTS.md multi-agent §9-§11 prescribe exactly that before a PR opens. The only contract consequence is that the merge was one commit too early (①5).
  • wip subject kept in history (527957de24) — seat matter; squash landing collapses it, and no force-push is the rule.
  • STALE TREE note — honest at its time (seat matter); overtaken by fd96a8473d, which is the contract matter in ①5.
  • Harness footer / trailer deviation — seat matter; AGENTS.md governs those lines.
  • open_questions[0], staging A / B / C — seat matter (dispatch shape). Seat answer 5963787404 picks A: four serial Part-of stages (S-list, S-form, S-metric, S-objectui-held), each with its own value census first; B rejected because every stage edits the same two files; C fails A7. Consistent with triage's direction and the claim's size rule; the pin's shrink-only ledger is what keeps each stage honest.
  • open_questions[1], kanban conditionalFormatting — contract matter, and a cross-repo ruling. Seat answer: held as the pin records; the objectui side carded as finding(plugin-kanban): kanban conditionalFormatting accepts a native rule dialect the spec's list-view member refuses, blocking the spec from typing it objectui#11522 for that repo's triage; typed here once objectui settles on one dialect, and the card's closing line waits for it. That is the correct reading of triage's caveat. The card the seat names was not read here (outside this review's inputs).
  • open_questions[2], ai:chat_window — contract matter for its own card (ADR-0049 enforce-or-remove), not this PR's. Seat answer: carded as finding(spec): ai:chat_window is a declared page component type with no renderer — the console answers a page that uses it with "Unknown component type", and no app authors it #21504, filed bare; context{} stays no-reader in the pin. Right; that card was not read here.
  • Nothing in the file list touches a governed surface (Governed Surface Queue Guard green); no objectui edit and no renderer change, as the claim required.

Implemented-by: claude/issue-21464-component-props-unknown-closeout
Reviewed-by: session_01YDt3PzwfrkuFzUBF89WPmM

VERDICT: FAIL

Failed item (one): the regenerated content/docs/references/ui/component.mdx is stale against main's generator (fd96a8473d, #21478) on the three new navigation Type cells, so Type Check · source gates reds at "Check generated reference docs are in sync with the spec" and its rollup TypeScript Type Check reds with it. Every other judged item passes; a merge of origin/main plus check:generated --fix on a new head, reviewed again, closes it.


Generated by Claude Code

claude added 2 commits October 3, 2026 01:01
…ree, so the typed navigation cells take main's optionality face (#21464)

The merge of origin/main 85e29b8 brought fd96a84, which renders
every defaulted member of a shared def with `?` in a Type-cell shape
summary. The page was regenerated from the merged sources with
`check:generated --fix` (gen:docs, the only stale artifact): the three
new navigation cells on ObjectGanttProps, ObjectMapProps and
ObjectTreeProps now read `{ mode?: …; preventNavigation?: …;
openNewTab?: …; size?: …; … }`, like the grid, kanban, calendar and
timeline cells. The branch's delta against main is unchanged: the same
six files, +637/-12.

Claude-Session: https://claude.ai/code/session_01YDt3PzwfrkuFzUBF89WPmM
Co-authored-by: Claude <noreply@anthropic.com>
@objectstack-fleet

Copy link
Copy Markdown
Contributor Author

Contract review

Served-tier: CONTRACT_REVIEW_TIER
Head-sha: c222ab563b65319ac713f766ad58b088ba46890a
Local-runs: none

Re-review after record 5963877733 (head d3431deed339c1825fa5831af79cd92e0f156e83, VERDICT FAIL on one item). Inputs read: card #21464 (comments 5961300594 triage, 5962381903 claim, 5963737459 and 5964076083 dev reports, 5963787404 seat answer), PR #21502 (body, the 6-file list, the 7-commit list, the net diff against main, the docs-drift bot comment, record 5963877733), the 42 check-runs on this head and the step list of the Type Check · source gates job. Source was read at refs only: objectstack at c222ab563b, 6702f97ac3, d3431deed3, 85e29b8858, 100c394f6f and fd96a8473d. Nothing was built, run or re-run.

Gate verdicts on this head, as they stood at this read (2026-10-03, 01:38Z): 42 check-runs, all completed — 38 success · 0 failure · 0 in progress · 4 skipped (Console Pin Gate and Packed-tarball smoke (opt-in) by design; Auto Label and Check PR Size skipped on the second workflow run 37086056361, the one the PR-body edit triggered, after both succeeded on the first run 37084840768). The two that were red on d3431deed3 — Type Check · source gates and its rollup TypeScript Type Check — are success here.

① Derived judgments

1. The failed item, content/docs/references/ui/component.mdx — RIGHT now; the page is the generator's output for this merged tree.

  • CI's own verdict: job Type Check · source gates (111092847621) at head c222ab563b reports step 26 "Check generated reference docs are in sync with the spec" — pnpm --filter @objectstack/spec check:docs (lint.yml:5652-5653) — success, and every later step of that job ran and passed. That check is byte-exact: the shared sink (packages/spec/scripts/lib/generated-output.ts, flush) reads each emitted path back from disk and reports ~ changed on readFileSync(file) !== content, plus + added / - unowned for the set itself, so a green check:docs means the committed page equals the emitted page byte for byte.
  • The three Type cells read the current optionality face. At the head the page carries seven navigation rows (:491, :587, :729, :909, :954, :1132, :1189) and ONE distinct Type-cell string across all seven — { mode?: …; preventNavigation?: boolean; openNewTab?: boolean; size?: …; … }, the fd96a8473d face main renders on grid, kanban, calendar and timeline; 0 cells read the superseded { mode: … face and 0 read any. The three new Nested Shape blocks (ObjectGanttProps.navigation :682, ObjectMapProps.navigation :1019, ObjectTreeProps.navigation :1240) are line-identical to main's ObjectGridProps.navigation block (mode optional default "page", the [REMOVED] view tombstone, preventNavigation, openNewTab, size default "auto", the deprecated width), and sit in the same blank-blank-rule shape as main's own ObjectCalendarProps.navigation and ObjectTimelineProps.navigation blocks. The net diff against main is the +36/-3 the first review expected: three row replacements (any becomes the shape; each description now names ListViewSchema.navigation) and the three nested-shape blocks, nothing else on the page.
  • The merge, from the trees. In the window 100c394f6f..85e29b8858 main changed exactly one of the PR's six paths, component.mdx (14/14 lines, fd96a8473d); registry.ts (42a63e54 at both ends) and component.zod.ts (9b6bb1dd at both ends) did not move, so the merge dropped nothing on the PR's other paths. The merge commit 6702f97ac3 (parents d3431deed3, 85e29b8858) carries the BRANCH's component.mdx blob (b8ab062e, the stale one) — what the merge=os-regen driver (.gitattributes:178, content/docs/references/**) keeps when both sides changed a path; os-regen-merge.sh step 2 then restores main's side in the working tree and step 3's commit is refused by pre-commit until the regeneration, which is the design the script's header states and the dev's report describes. c222ab563b is that regeneration: component.mdx alone, 17/17 lines over the merge commit (the 14 main re-rendered + the PR's 3 rows). So the intermediate merge commit is stale ON ITS OWN, by design; the head is current, and a squash landing collapses the intermediate.

2. The delta from d3431deed3 touches nothing else of the PR's change — RIGHT. git diff d3431deed3 c222ab563b restricted to the five hand-written paths is empty; the blobs are identical at d3431deed3, 6702f97ac3 and c222ab563b: .changeset/21464-component-props-navigation-typed.md 1b4b151e, 18.ui-object-map-gantt-tree-navigation-typed.ts e9bddf3d, migrations/registry.ts 3641f885, component-props-unknown-members.pin.test.ts 03b1bf90, ui/component.zod.ts 58b6e27f. The PR's net diff of those five files against its merge base — 100c394f6f at d3431deed3, 85e29b8858 at c222ab563b — is byte-identical at both heads once index lines are stripped (687 lines each). GitHub's served net diff equals the local 85e29b8858..c222ab563b diff: 6 files, +637/-12, as the body states.

3. Step-18 rationale fragment order 64 — still unique. STEP18_RATIONALE at the head holds 68 fragments (67 on main); order 64 is held by ui-object-map-gantt-tree-navigation-typed alone and is the maximum; the id list is strictly sorted and unique under LC_ALL=C; the duplicated orders 56 / 60 / 62 are the same three main@85e29b8858 already carries, none this PR's. scripts/step18-rationale-merge.test.ts pins sortedness, key uniqueness, kebab ids and rendering by order; it does not pin order uniqueness (the pre-existing duplicates would red it) — carried from the first record as an observation on main, not a defect here.

4. The generated registry region — current after the merge. The 37-line region added to migrations/registry.ts (leading comment and entry object) is identical, modulo the generator's indentation, to 18.ui-object-map-gantt-tree-navigation-typed.ts lines 5-41; main's registry.ts did not change in the merge window, so no sibling entry landed to re-sort against; check:migration-registry runs in Lint & Repo Gates (lint.yml:419), success on this head; the dev's local reading agrees (353 semantic, 246 retired-key, 217 retired-def, exit 0).

5. Carried by reference from record 5963877733: its ①1 (the three rows typed by reference and the readers at the pin), ①2 (the enumeration pin, its reasons and the z.any() blind spot), ①3 (the staging and Part of), ①4 (the kanban conditionalFormatting hold), and the first three bullets of its ①5 (the D3 entry, the rationale fragment, the registry region) — every input those judgments read is byte-identical at this head, so they stand as written and were not re-derived.

② Semver level

RIGHT — carried. The changeset blob (1b4b151e) is unchanged since d3431deed3: '@objectstack/spec': minor, feat(spec)!:, Clause-②: yes (narrowing), the BREAKING banner, the adr-0087 registered marker naming ui-object-map-gantt-tree-navigation-typed. Check Changeset is success on both workflow runs at this head. The observations of record 5963877733 ② stand.

③ Boundary flags

  • A second workflow run (37086056361) on this head, triggered by the PR-body edit that retitled the gates heading to c222ab563b — seat matter. Auto Label and Check PR Size are skipped there and success on the first run; a skip on an edited event, not a changed verdict. The body's gates section now names the head and the fix round, as the dev's deviation asked.
  • The dev's report sentence "Step 2 took main side of component.mdx" and the merge commit's branch-side blob are both true of different states (the working tree after step 2; the step-1 commit the driver wrote) — observation, no action; the regeneration commit is what CI judged.
  • Test counts moved (603 files, 17838 tests against the first report's 602 | 1 skipped, 17832) — main's incoming commits, not this PR's; seat matter.
  • Docs Drift Check (5963708231) on the merge 0002894704: "nothing to list — not a clean bill of health" — advisory, unchanged in kind from the first head.
  • Still a draft; auto_merge null; no governed path in the file list (Governed Surface Queue Guard success). The landing is the owning seat's.

Implemented-by: claude/issue-21464-component-props-unknown-closeout
Reviewed-by: session_01YDt3PzwfrkuFzUBF89WPmM

VERDICT: PASS

The one item record 5963877733 failed is closed on this head: component.mdx is the generator's byte-exact output for the merged tree (CI's sync step green; all seven navigation cells on one current face), the five hand-written files are byte-identical in their change against main at both heads, the step-18 order 64 is unique, the registry region is current, and every check on c222ab563b has completed with no failure.


Generated by Claude Code

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

documentation Improvements or additions to documentation protocol:ui size/l tests tooling

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants