Skip to content

chore(objectui): bump the console pin to f8a9d0fb0596 (carries objectui#10221 and the objectui#9910 re-cut) with re-measured pin citations and the lockstep re-record - #20036

Merged
objectstack-fleet[bot] merged 9 commits into
mainfrom
claude/issue-20029-objectui-pin-bump
Sep 25, 2026
Merged

objectstack-fleet[bot] merged 9 commits into
mainfrom
claude/issue-20029-objectui-pin-bump

Conversation

@objectstack-fleet

@objectstack-fleet objectstack-fleet Bot commented Sep 24, 2026 •

Copy link
Copy Markdown
Contributor

Fixes #20029

Clause-②: no

Moves the console pin to objectui main f8a9d0fb0596 (the tip at 2026-09-24T20:41Z). That pin carries objectui#10221 (0651e7ab4, a currency amount's decimals come from its currency, never from scale), which ruling 乙 on #19910 item 2 requires before PR #19909 lands. It also carries the objectui#9910 re-cut (5ea623ea, containment is the declared children slot), which #19969 waits on. The PR folds in the #19503 / PR #19832 precedent's two steps: the sdui-parser lockstep re-record, and a re-MEASURE of the packages/spec/src/** citations that check:objectui-pin-citations names. Every spec edit is to comment or migration-rationale prose only, plus ONE .describe() citation re-point (the FormField.span pin, under PM ruling 5822830122, option A). No schema, key, default or export changes. Clause-②: no, because every authored document is accepted and refused exactly as before.

✅ All 47 pin citations are now green. The one .describe() this PR first stopped on (view.zod.ts:3392, FormField.span) was re-pointed under PM ruling 5822830122 (option A) on #20029. See "The describe, resolved" below.

Ancestry reading (fix → new pin: AHEAD)

  • REST compare: NOT MEASURED. This session's proxy refuses GitHub API reads on objectstack-ai/objectui. GET /repos/objectstack-ai/objectui/compare/0651e7ab4...f8a9d0fb0 answered "GitHub access to this repository is not enabled for this session".
  • In its place, measured by git in a FULL clone of objectstack-ai/objectui, where git rev-parse --is-shallow-repository returns false. An exit 0 is self-proving, and an exit 1 there is not a shallow-window artefact.
command exit reading
git merge-base --is-ancestor 0651e7ab4 f8a9d0fb0596 0 objectui#10221 is IN the new pin
git rev-list --count 0651e7ab4..f8a9d0fb0596 14 the new pin is 14 commits AHEAD of the fix (REST would read ahead)
git merge-base --is-ancestor 5ea623ea f8a9d0fb0596 0 the objectui#9910 re-cut is IN the new pin
git merge-base --is-ancestor 5ea623ea 0651e7ab4 0 it is an ancestor of the fix, too
git merge-base --is-ancestor 62597c588072 0651e7ab4 0 the old pin is behind the fix (control leg)
git merge-base --is-ancestor 5ea623ea 62597c588072 1 the re-cut was NOT in the old pin (full clone, so the exit 1 stands)
git rev-list --count 62597c588072..f8a9d0fb0596 92 this hop's size

The bump's own changeset lists both: (objectui 0651e7ab4) and (objectui 5ea623eab).

What moved, and how each artefact was produced

artefact producer
.objectui-sha + .changeset/console-f8a9d0fb0596.md bash scripts/bump-objectui.sh --no-commit f8a9d0fb0596… exit 0. It reads 86 releasing changesets and 9 breaking (all 9 annotated, none declared major); @objectstack/console: minor. The generator's adr-0087: TODO is answered not-required (no-migration-prescription), in the wording of the last bump that carried breaking entries.
the console OBJECTUI_ROOT=../objectui bash scripts/build-console.sh, through the verify lock, VERDICT command-exit 0 (691 s). The import/jobs bundle canary is present. pnpm check:console-sha exit 0: "Console dist matches the objectui pin (objectui@f8a9d0fb0596)".
sdui.manifest.json + scripts/sdui-manifest.record.json node scripts/gen-sdui-manifest-node.mjs exit 0. It wrote 59 components, sha256 12dd4f8fbf45. The diff is +39: the children slot input objectui#9910 declared on alert, badge, box, button, card, container, flex, grid and stack. node scripts/check-sdui-manifest.mjs exit 0.
packages/sdui-parser/objectui-lockstep.json OBJECTUI_ROOT=../objectui pnpm gen:sdui-lockstep exit 0. The diff is 3+/3-, only rev, revDate and recordedAgainstPin. Grammar blob 0131f27cf86d is unchanged (214 lines), and all 26 diagnostic codes agree. pnpm check:sdui-lockstep exit 0: "byte-identical to objectui@f8a9d0fb0596". No parser port is owed by the lockstep gate.
packages/spec/src/migrations/registry.ts pnpm --filter @objectstack/spec gen:migration-registry, from the six edited entries

The #19969 save/render window (measured, not fixed here)

In the NEW sdui.manifest.json, 5 of 59 components disagree between isContainer (what packages/sdui-parser/src/validate.ts still tests) and "declares a { name: 'children', type: 'slot' } input" (what the pinned renderer now decides containment by):

component isContainer children slot direction
badge false true the save gate warns not-a-container on children the renderer renders
alert false true same
button false true same. objectui states it on purpose: "⛔ Not isContainer: objectui#6804 ruled that flag means LAYOUT containment" (button.tsx:97-104)
page:tabs true false the save gate accepts top-level children that the slot rule does not declare (the children live in items[].children)
page:accordion true false same

Non-zero, so #19969 can be sequenced right behind this PR. The predicate is ⛔ not ported here.

Citation re-measure: method

  • Read points. For each cited objectui file, git diff --quiet 62597c588072 f8a9d0fb0596 -- FILE.
    • An unchanged file holds its anchors by identity. The prose says so and keeps the earlier hops' history.
    • For a changed file, each cited span was taken at the old pin and searched for byte-for-byte at the new pin. A moved span is re-pointed. A span not found was re-READ in the new tree, and its content change is stated where it is cited.
  • Corpus counts (six migration entries + registry.ts). git grep -o -F TOKEN SHA | wc -l. The method was calibrated first: at 62597c588 it reproduces every recorded count exactly (8303 files, objectstack 13125, @objectstack/spec 5043, timeout 1086, RuntimeConfig 240, resourceLimits 2, useState 2389, window 3526, period 170, interval 179, metrics 324, TTL 156, tenant 987, Span 486, SpanSchema 53).
    • At f8a9d0fb0, all 100 dark tokens still count 0: every export of the four cited zod files plus each key name.
    • Only the corpus size (8512) and the lit controls move. The two new Span hits are colSpan and the word "Spanish".
  • check:future-spec-major's measurement exemption to the sentence "controls objectstack 12966 and @objectstack/spec 4997 on the same corpus" is kept verbatim. The new figures follow it as "13347 and 5123 … at this pin", and the gate is exit 0.

Every packages/spec/src/** edit, with its before → after anchor

site objectui file(s) reading at f8a9d0fb0
api-methods-batch-conformance.test.ts sys_api_key ObjectGrid.tsx (changed +18/-5: objectui#10083 rowActionsDeclared, objectui#9909 currency locale), useBulkExecutor.ts (identical) selection block 4024-4051 → 4032-4059, still hash c88443302d40. 298-303 is identical (hash 01083348330f)
functional-completeness.ts calendar / gantt / timeline / map rows ListView.tsx (changed: objectui#10275 $select; objectui#10037 / objectui#10250 self-query filter and search on gantt / tree / chart), ObjectGantt.tsx (changed: $search), ObjectCalendar.tsx / ObjectTimeline.tsx / ObjectMap.tsx (identical) no calendar / gantt-date / timeline / map arm touched; prose restated
6 migration entries + their 6 registry.ts copies whole corpus dark tokens 0 at 8512 files; lit controls re-counted (see method)
component.zod.ts / component.test.ts tabs + accordion icon containers.tsx (changed, ba0b61a60: an import line + page:header) 853-859, 912, 1069-1075, 1116 did NOT move, text byte-identical
component.zod.ts / component.test.ts button icon button.tsx (changed, 5ea623eab), resolve-icon.ts / lazy-icon.tsx (identical) ⚠️ registration inputs CONTENT grew: 85-97 → 85-105 (+ children slot input), defaultProps 98-102 → 106-110; still no icon input. :43 / :72 / :74 unmoved
component.zod.ts metric icon ObjectMetricWidget.tsx (changed, 0651e7ab4), index.tsx / MetricWidget.tsx (identical) destructure 174 → 181, forward 483 → 528, text byte-identical
component.zod.ts / component.test.ts kanban limit ×3, quickAdd ×2, navigation ObjectKanban.tsx (changed, objectui#10068 $orderby), plugin-kanban index.tsx (changed), objectql.ts, ElementDataSourceGate.tsx, ListView.tsx, plugin-view ObjectView.tsx (all changed); types.ts, element-data-source.ts, KanbanImpl.tsx, plugin-kanban.mdx (identical) ⚠️ query CONTENT gained $orderby (674-679 → 680-690), mapping gained sort: true (447-450 → 507-511); moved byte-identical: :84→:85, :553→:554, :559→:565, :676→:687, :1210→:1225, :1218-1219→:1233-1234, :1468→:1483, :1484-1497→:1499-1512, :1563→:1578, :1587→:1602, index.tsx:313→:363-364, objectql.ts:3735→:3832, Gate 316-331→373-388, 192-194→200-202, ListView 2979→3067, 2952→3040, ObjectView 1638→1666, 1579→1607. onQuickAdd / quickAdd still 0, onCardClick still 11
component.zod.ts calendar navigation ObjectCalendar.tsx (identical) anchors hold by identity
component.zod.ts object-map header, filter, sort ObjectMap.tsx, plugin-map index.tsx, record-source.ts (identical) anchors hold by identity
component.zod.ts object-gantt header ObjectGantt.tsx (changed +29/-2, objectui#10250) :501-503, :613 unmoved; the rest moved 21 / 29 lines byte-identical (e.g. 844→865, 1615→1644, 2181→2210, 2144→2173). ⚠️ new undeclared reads search / searchableFields (:776-779, :880-885) are recorded, host-generated by ListView's toolbar
component.zod.ts flat TreeConfig guidance + object-tree header + registry-shell count ObjectTree.tsx (changed +69/-3: objectui#9549, objectui#9136), ListView.tsx (changed); index.tsx, record-source.ts (identical) ⚠️ two CONTENT changes, restated: the filter.tree stash read is DELETED (getTreeConfig 235-248 → 246-259, tree :236 → :247), and filter gains a second read on the inline value provider (:837); ObjectTreeSchema now declares filter (objectui#9549). Moved byte-identical: 582→593, 871/922/985→937/988/1051, 813-814→879-880, 742→753, 908→974, 891→957, 775→786, 741-753→752-764, 751→762, 750→761, 197-218→198-219, ListView 3261-3275→3366-3385, 3270→3380. ElementDataSourceGate count re-taken: tree 0, map / gantt / grid / calendar 3 each
component.zod.ts object-timeline header + data read renderer.tsx (changed, 0b6b295a1), ObjectTimeline.tsx / index.tsx (identical) 1215→1240, 1505→1531, 1442-1457→1468-1483, text byte-identical
component.zod.ts ComponentPropsMap comments (map/gantt/tree; timeline) as above restated to match the headers
dataset.zod.ts measure format dataset-format.ts, date-display.ts (both changed, objectui#10026) formatDate 271-306 → 355-390 and 225 → 309 byte-identical; formatMeasureDate 229-264 → 229-263 (comment-only change), call 370 → 369, datetime arm 260-262 → 259-261; the style handling is unchanged
view.zod.ts view-face limit reach (#19228) 9 files instrument 1: probe 0; control 14 lines / 7 files (was 13 / 6; the new one is RelatedList.selectFls-10186.test.tsx:314, executable ⇒ 9 executable, was 8). Instrument 2: the same four flattening spreads and the same exclusions. Every anchor re-pointed byte-identical (e.g. ListView.tsx:3084→3172, 4702→4815, ObjectView.tsx:1725→1753, 2319→2347). Both verdicts stand
view.zod.ts ListMapConfigSchema content-asserted anchors plugin-view ObjectView.tsx, objectql.zod.ts (changed) case 'map': 1764 → 1792 (17-line block identical), ObjectMapConfigSchema 1574 → 1589 (47-line declaration identical), LIST_VIEW_LOCAL_OVERRIDES 734 → 741 (list identical, still no map). --verify-anchors passes all content assertions
view.zod.ts FormField.span DOCBLOCK form.tsx (changed, 5ea623eab: registration inputs only), autoLayout.ts (identical) spanLadderFor :204-231 is byte-identical, so it still emits the ladder

The describe, resolved (PM ruling 5822830122, option A)

The ruling widened the file surface by exactly three items, all landed in one new commit (90f9a30):

file what
packages/spec/src/ui/view.zod.ts the FormField.span .describe() citation, .objectui-sha = 62597c588 → .objectui-sha = f8a9d0fb0596; nothing else in the sentence changes. The claim was re-read true at the new pin: plugin-form's autoLayout.ts is byte-identical, and form.tsx's spanLadderFor :204-231 is byte-identical
content/docs/references/ui/view.mdx regenerated by pnpm --filter @objectstack/spec gen:schema && pnpm --filter @objectstack/spec gen:docs (VERDICT command-exit 0): the two span rows only (+2/-2). No other generated file moved
.changeset/20029-pin-bump-describe-correction.md '@objectstack/spec': patch, stating the describe correction, in the shape of 17429- / 19503-pin-bump-describe-corrections.md

Gates

node scripts/pm/dispatch-gates.mjs --repo objectstack-ai/objectstack --commands on head 90f9a30 derives 124 families (the view.mdx edit adds the docs families). --ran reads: "124 derived famil(ies) accounted for — 121 run, 3 NOT-MEASURED".

  • All run families exit 0, among them: pnpm --filter @objectstack/spec run check:objectui-pin-citations ("47 … match .objectui-sha"), pnpm check:sdui-lockstep, pnpm check:console-sha, node scripts/check-sdui-manifest.mjs, pnpm --filter @objectstack/spec run check:generated, check:docs, check:objectui-changeset, check:future-spec-major and check:migration-registry.
  • Five docs families first read exit 3 (unbuilt @objectstack/lint / formula / client-react). After turbo run build of those closures they read exit 0: check:doc-formula-expressions, check:doc-security-posture, check:skill-examples, check:docs-transcript-drift, check:lean-entry-closure.
  • NOT MEASURED, exit 3 (PREREQUISITE NOT MET), left to CI: check-plugin-teardown-shape --self-test (shallow clone); check:dual-build-cjs-loads and check:type-check-debt (both need a full workspace build). This diff changes no code in any package those read.
  • Tests: @objectstack/spec vitest on component.test.ts, api-methods-batch-conformance.test.ts, src/migrations, functional-completeness, src/ui/view* and src/ui/dataset*: 26 files, 1666 / 1666 pass (head 530fc4d); re-run on src/ui/view* at head 90f9a30: 19 files, 1085 / 1085 pass.

Changed lines: 884 (+600 / -284, 20 files), under the 5000 threshold.

Acceptance notes

  • Stale metric test anchors, out of the gate's population. component.test.ts's metric icon test body still cites index.tsx:204, ObjectMetricWidget.tsx:142 / :474 and MetricWidget.tsx:312-321. It carries no pin citation, so it is outside the gate's population. Those anchors were already stale before this hop: the docblock beside them reads :237, :181 / :528 and :351-360. Left as they are, because the gate does not name them.
  • object-kanban reads sort now (objectui#10068, via ElementDataSourceGate from the binding's dataSource.sort). The node schema declares no sort. The key reaches the board from the binding, not from the authored node, so this is not an authoring gap. Recorded, not acted on.
  • The branch name is claude/issue-20029-objectui-pin-bump, as the claim names. The console changeset is the script's own console-f8a9d0fb0596.md (the precedent's spelling), not a hand-named 20029-*.md.

维护者速读(草稿)

Dev session: session_01BMUTTbeXStLRWxU9zEWDJq (PM seat domain:devx#1, session_01VDtqoecgES7ScQYGbFVDRv).

🤖 Generated with Claude Code

https://claude.ai/code/session_01BMUTTbeXStLRWxU9zEWDJq


Generated by Claude Code

…ui#10221 0651e7ab4 and the objectui#9910 re-cut 5ea623ea)

Claude-Session: https://claude.ai/code/session_01BMUTTbeXStLRWxU9zEWDJq
Co-authored-by: Claude <noreply@anthropic.com>
…migration-entry pin citations at objectui f8a9d0fb0

Claude-Session: https://claude.ai/code/session_01BMUTTbeXStLRWxU9zEWDJq
Co-authored-by: Claude <noreply@anthropic.com>
…n pin citations at objectui f8a9d0fb0

Claude-Session: https://claude.ai/code/session_01BMUTTbeXStLRWxU9zEWDJq
Co-authored-by: Claude <noreply@anthropic.com>
…ns at objectui f8a9d0fb0

Claude-Session: https://claude.ai/code/session_01BMUTTbeXStLRWxU9zEWDJq
Co-authored-by: Claude <noreply@anthropic.com>
…easure-format pin citations at objectui f8a9d0fb0

Claude-Session: https://claude.ai/code/session_01BMUTTbeXStLRWxU9zEWDJq
Co-authored-by: Claude <noreply@anthropic.com>
…and FormField.span docblock at objectui f8a9d0fb0

Claude-Session: https://claude.ai/code/session_01BMUTTbeXStLRWxU9zEWDJq
Co-authored-by: Claude <noreply@anthropic.com>
…parser lockstep at f8a9d0fb0596

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

github-actions Bot commented Sep 24, 2026 •

Copy link
Copy Markdown
Contributor

📓 Docs Drift Check

This PR changes 2 package(s): @objectstack/sdui-parser, @objectstack/spec, touching 12 documentable anchor(s). ⚠️ 2 changed file(s) yielded no anchor (packages/sdui-parser/objectui-lockstep.json, packages/spec/src/kernel/functional-completeness.ts), so the pages documenting them are NOT COVERED by this run — this is not a clean bill of health for those files.

2 hand-written doc(s) NAME something this change touched and may need an implementation-accuracy re-verification:

  • content/docs/data-modeling/analytics.mdx (via DatasetMeasureSchema (symbol, a top-level const))
  • content/docs/protocol/objectui/layout-dsl.mdx (via ComponentPropsMap (symbol, a top-level const object))

⛔ 4 release-owned page(s) also name something this change touched. These are read-only:

  • content/docs/releases/v15.mdx (via PageTabsProps (symbol, a top-level const object))
  • content/docs/releases/v17/17-1.mdx (via ComponentPropsMap (symbol, a top-level const object))
  • content/docs/releases/v17/17-3.mdx (via ComponentPropsMap (symbol, a top-level const object))
  • content/docs/releases/v17/17-4.mdx (via ComponentPropsMap (symbol, a top-level const object))

content/docs/releases/ is RELEASE-OWNED (AGENTS.md "Documentation Guardrails"): release
notes are written centrally at release time, and a code PR that edits them is the exact PR
that guardrail exists to stop. They are still audited — read-only. If one of them is actually
wrong, file an issue or open a dedicated docs-only PR; do not edit it here.

What this run could not see
  • 2 changed file(s) yielded no anchor (packages/sdui-parser/objectui-lockstep.json, packages/spec/src/kernel/functional-completeness.ts) — pages documenting those are invisible to this run
  • 2 name(s) were too generic to anchor anything (single lowercase words)
  • the SDK route bridge reached 60 of 215 client-bound route-ledger rows — the other 155 have no registrar path: tail to select them, so pages documenting THEIR client methods cannot appear above, on this or any run. Of those 155: 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; 100 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 — 137 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 246314dffef9af072b6172d0f1f9678fb7363dec → packageMentionDocs.

Which tree this was computed on

This run read content/docs from 6688d84f1c770438c344d574e5548dde427483e1 — the merge of head b4d386f23c514356f155589a8f7ae20cee640c45 into base 246314dffef9af072b6172d0f1f9678fb7363dec, 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 6688d84f1c770438c344d574e5548dde427483e1 && git checkout 6688d84f1c770438c344d574e5548dde427483e1
# afterwards, rebuild it from the two parents, which stay fetchable
git fetch origin 246314dffef9af072b6172d0f1f9678fb7363dec b4d386f23c514356f155589a8f7ae20cee640c45 && git checkout -B drift-repro 246314dffef9af072b6172d0f1f9678fb7363dec && git merge --no-ff b4d386f23c514356f155589a8f7ae20cee640c45

node scripts/docs-audit/affected-docs.mjs --json 246314dffef9af072b6172d0f1f9678fb7363dec

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

Advisory only, and a precision-first one (#9192): a page is listed because it names a
symbol, wire route or SDK method this diff touched — not because it mentions a changed
package. Each row says which anchor put it there, so a wrong row is reportable rather than
merely annoying. To re-verify, run the docs-accuracy-audit workflow scoped to these files:
node scripts/docs-audit/affected-docs.mjs 246314dffef9af072b6172d0f1f9678fb7363dec → pass the list as
args.docs, on the commit named under Which tree this was computed on.

…i f8a9d0fb0596 and regenerate view.mdx

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

Copy link
Copy Markdown
Contributor Author

Contract review

Served-tier: CONTRACT_REVIEW_TIER
Head-sha: 90f9a303cb0ff4a883cd3c105d69a722379e7825

① Derived judgments

  1. The spec diff is prose plus one describe re-point: CONFIRMED. Merge-base 26550c66 against head, over packages/spec/src/** (692 changed lines). With comment lines filtered out, two kinds of change remain:

    • string-continuation lines inside the reason: field of the six semantic/18.* migration entries and their six generated registry.ts copies (the pin sha, the corpus size 8303 → 8512, and lit-control recounts);
    • the single FormField.span .describe() line in view.zod.ts, where 62597c588 becomes f8a9d0fb0596 and nothing else in the sentence moves.

    component.test.ts and api-methods-batch-conformance.test.ts have zero non-comment changed lines. No z.* call, key, default, enum member, export or behaviour changes.

  2. Citation spot-checks at objectui f8a9d0fb0596: all TRUE. Checked in a full clone (--is-shallow-repository false).

    • (a) FormField.span: plugin-form/src/autoLayout.ts is byte-identical between the pins. components/src/renderers/form/form.tsx changed by +2/-1, adding only a children slot input; spanLadderFor :204-231 is byte-identical.
    • (b) Corpus counts, re-run with git grep -o -F at both pins (new/old): tracked files 8512/8303; objectstack 13347/13125; @objectstack/spec 5123/5043; tenant 1034/987; TTL 156/156; Span 488/486; SpanSchema 53/53; useState 2391/2389; timeout 1096/1086; RuntimeConfig 245/240; window 3581/3526; period 171/170; interval 179/179; metrics 326/324; resourceLimits 2/2. Every number in the six entries matches.
    • (c) button.tsx inputs 85-105 carry the children slot, with the "Not isContainer: objectui#6804" comment at 97-104.
    • (d) containers.tsx 853-859 and 1069-1075 are byte-identical across the hop.
    • (e) ObjectMetricWidget.tsx: the icon destructure moved 174 → 181 and the forward 483 → 528, with identical text.
    • (f) ObjectView.tsx case 'map': moved 1764 → 1792, with the block identical.
  3. Pin and ancestry: CONFIRMED. .objectui-sha = f8a9d0fb0596f4521076628e2bbfe27e6ce67d52. 0651e7ab4, 5ea623ea and the old pin 62597c588 are each ancestors of the new pin (exit 0). Control legs: 5ea623ea and f8a9d0fb0596 are each NOT ancestors of 62597c588 (exit 1). rev-list --count: 0651e7ab4.. = 14, 62597c588.. = 92.

  4. Describe and view.mdx agree: CONFIRMED. The view.zod.ts describe and both regenerated span rows in content/docs/references/ui/view.mdx (+2/-2) read f8a9d0fb0596. No other generated file is in the diff.

  5. Lockstep is a re-record only: CONFIRMED. objectui-lockstep.json changes exactly rev, revDate and recordedAgainstPin; the grammar blob and the files list are untouched.

  6. Manifest: CONFIRMED. sdui.manifest.json is +39 lines: nine children slot inputs, no removals, 59 components before and after. Recomputed from the head manifest, isContainer and a declared children slot disagree on exactly 5 of 59:

    • page:tabs and page:accordion: isContainer true, no slot;
    • badge, alert and button: isContainer false, slot declared.

② Semver level

@objectstack/spec: patch is right. The only published-string changes are one .describe() sentence and migration reason prose. No accept set, key, default, enum or export moves, so Clause-②: no is correct.

Both changesets follow precedent in shape:

  • .changeset/20029-pin-bump-describe-correction.md matches 19503- / 17429-pin-bump-describe-corrections.md.
  • @objectstack/console: minor in console-f8a9d0fb0596.md matches the console-62597c588072.md precedent.

The manifest and lockstep files are tracked artefacts, not published packages, so no further changeset is owed.

③ Boundary flags

Implemented-by: claude/issue-20029-objectui-pin-bump
Reviewed-by: session_01VDtqoecgES7ScQYGbFVDRv

VERDICT: PASS

Land through the queue once it is out of draft; no rework is owed. Conditions:

Isolated at-tier reviewer, adopted by the domain:devx#1 seat.


Generated by Claude Code

@objectstack-fleet

Copy link
Copy Markdown
Contributor Author

Regen-provenance: os-regen-merge.sh · 90f9a30 → b4d386f

Landing step per the seat's comment 5823593976 on #20029. Dev session session_01BMUTTbeXStLRWxU9zEWDJq.

What the hop is: one merge commit b4d386f (git merge origin/main at 246314d; no rebase, no force-push). bash scripts/pm/os-regen-merge.sh steps 1–3 exit 0. Step 2 took main's side of nothing: main moved none of this branch's paths, and the script KEPT the branch's content/docs/references/ui/view.mdx, which the branch changed and main did not. Step 3: "merge left no additional changes to commit".

Step 4, the regeneration: pnpm --filter @objectstack/spec build && gen:migration-registry && gen:schema && gen:docs, through the verify lock, VERDICT command-exit 0. It produced zero drift (git status --porcelain empty afterwards), so no regeneration commit exists and the push carries the merge commit alone.

Gates on b4d386f (exit codes captured before any pipe):

exit gate
0 pnpm --filter @objectstack/spec run check:authorable-surface
0 pnpm --filter @objectstack/spec run check:objectui-pin-citations
0 pnpm --filter @objectstack/spec run check:generated
0 pnpm --filter @objectstack/spec run check:docs
0 pnpm --filter @objectstack/spec run check:migration-registry
0 pnpm check:sdui-lockstep
0 pnpm check:console-sha
0 node scripts/check-sdui-manifest.mjs
0 pnpm check:merge-driver

Files that changed in the hop (git diff --name-only 90f9a303 b4d386f): 31. All 31 are byte-identical to origin/main (the blob at HEAD equals the blob at origin/main for every path), so each one is main's content and none is this PR's:

  • .changeset/19999-sqlite-glob-nul-comparand.md
  • .changeset/20002-explain-sharing-fault.md
  • .changeset/20006-cascade-fk-clear-refusal-text.md
  • .changeset/20010-analytics-where-face-arms.md
  • .changeset/20027-private-credential-objects.md
  • packages/drivers/driver-sql/src/sql-driver-19999-glob-nul-comparand.test.ts
  • packages/drivers/driver-sql/src/sql-driver.ts
  • packages/drivers/driver-sqlite-wasm/src/sqlite-wasm-19999-glob-nul-comparand.test.ts
  • packages/drivers/driver-turso/src/remote-transport.ts
  • packages/drivers/driver-turso/src/turso-19999-glob-nul-comparand.test.ts
  • packages/objectql/src/engine-predicate-relationship.test.ts
  • packages/objectql/src/engine.ts
  • packages/objectql/src/validation/rule-relationship-traversal.test.ts
  • packages/objectql/src/validation/rule-validator.ts
  • packages/plugins/plugin-security/src/explain-dependency-fault.test.ts
  • packages/plugins/plugin-security/src/explain-engine.ts
  • packages/plugins/plugin-security/src/objects/default-permission-sets.ts
  • packages/plugins/plugin-security/src/objects/rbac-objects.test.ts
  • packages/plugins/plugin-security/src/private-credential-row-scope.test.ts
  • packages/rest/src/analytics-filter-refusal-envelope.test.ts
  • packages/services/service-analytics/src/__tests__/comparand-door-single-source.test.ts
  • packages/services/service-analytics/src/__tests__/comparand-shape-refusal.test.ts
  • packages/services/service-analytics/src/__tests__/cross-field-reference-refusal.test.ts
  • packages/services/service-analytics/src/__tests__/filter-normalizer-undefined-comparand.test.ts
  • packages/services/service-analytics/src/__tests__/filter-operator-coverage.test.ts
  • packages/services/service-analytics/src/__tests__/filter-refusal-envelope.test.ts
  • packages/services/service-analytics/src/__tests__/filter-value-type-fidelity.test.ts
  • packages/services/service-analytics/src/__tests__/where-face-arms-refusal.test.ts
  • packages/services/service-analytics/src/preview-evaluator.ts
  • packages/services/service-analytics/src/strategies/filter-normalizer.ts
  • scripts/engine-double-contract.pinned.json

This PR's own delta against origin/main is still the same 20 paths as on the reviewed head.


Generated by Claude Code

akarma-synetal pushed a commit to akarma-synetal/framework that referenced this pull request Sep 28, 2026
… paths (objectstack-ai#20056)

Fixes objectstack-ai#20034
Clause-②: no

## Patch round 1 (head `10b1176328`)

Added on top of the reviewed head `4c216561c6` (at-tier review PASS,
comment 5824559177). It carries the implementer's own two out-of-scope
findings and the reviewer's Clause ② reading:

- **ADR-0087 D3 entry `automation-runs-cursor-retired`**:
`packages/spec/src/migrations/entries/semantic/18.automation-runs-cursor-retired.ts`
`:11`, `:43` and `:72` now name `GET /api/v1/automation/:name/runs`, the
path this PR's contract publishes and the dispatcher mounts.
`packages/spec/src/migrations/registry.ts` was regenerated with `pnpm
--filter @objectstack/spec gen:migration-registry` (not hand-edited; the
diff is the same three lines at `:5943`, `:5975` and `:6004`).
`gen:upgrade-guide` and `gen:spec-changes` were re-run and changed no
bytes, because the entry is in step 18, beyond `PROTOCOL_MAJOR` 17. The
text still ships today as data in `MIGRATIONS_BY_MAJOR[18]`, which is
why it is corrected now. Open PR objectstack-ai#20031 regenerates a different region
of `registry.ts`; whichever of the two lands second regenerates.
- **Two comments**: `packages/runtime/src/query-param.ts:179` and
`packages/services/service-automation/src/run-list-truncation.test.ts:6`
now quote the `/api/v1` path. Both are comments only.
- **Clause ②**: `.changeset/20034-automation-contract-api-v1-paths.md:9`
and this body's line 2 now read `Clause-②: no`, with no arm. This diff
adds no key, widens no accepted input and adds no export
(`scripts/pm/clause2-line.mjs:70`). The level stays `minor`.
- No pending changeset quotes a sentence of the D3 entry.
`.changeset/19365-automation-runs-cursor-hasmore.md:117` carries only
the registration marker naming the entry's id, and the id is unchanged.
So this round needs no further deliberate correction.

## What this changes

`AutomationApiContracts` (`@objectstack/spec/api`) declared its nine
flow endpoints under `/api/automation`. The dispatcher mounts the
automation door at `config.prefix || '/api/v1'` plus `/automation`, and
`objectstack serve` passes no prefix, so every declared path answered
`404 ENDPOINT_NOT_FOUND` on the default composition (measured on a
composed runtime by the objectstack-ai#19966 dev). This PR takes remedy 1: the
contract moves to the served paths. **The runtime and dispatcher are
unchanged.**

- `packages/spec/src/api/automation-api.zod.ts`: the nine `path` values,
the module's `Base path` line and endpoint list, and every other in-file
path quote (section headers, `@example`s, the resume docblock, and the
`cursor` tombstone text `ListRunsRequestSchema` raises) move from
`/api/automation…` to `/api/v1/automation…`. After the edit the file
holds 0 occurrences of `/api/automation` (28 moved, plus the 10 docblock
lines rewritten).
- `packages/spec/src/api/automation-api.zod.test.ts`: the nine path pins
move with the values.
- `content/docs/references/api/automation-api.mdx`: regenerated with
`pnpm --filter @objectstack/spec gen:docs` (not hand-edited).
- `packages/runtime/src/automation-api-contract-mounts.test.ts` (new):
the drift pin, below.
- `.changeset/20034-automation-contract-api-v1-paths.md` (new):
`@objectstack/spec` `minor`.
- `.changeset/19365-automation-runs-cursor-hasmore.md`: a deliberate
correction of a pending note, below.
- Patch round 1:
`migrations/entries/semantic/18.automation-runs-cursor-retired.ts` and
the regenerated `migrations/registry.ts`, plus comments in
`packages/runtime/src/query-param.ts` and
`packages/services/service-automation/src/run-list-truncation.test.ts`.

## Reproduction, at base `adbbc5d01e`

- Spec: `automation-api.zod.ts:14` `Base path: /api/automation`;
`:658`–`:706` nine `path` values under `/api/automation`; the test
pinned all nine to themselves (`automation-api.zod.test.ts:803`–`:811`).
- Runtime: `dispatcher-plugin.ts:909` `const prefix = config.prefix ||
'/api/v1';`; `registerAutomationRoutes(base)` mounts
`${base}/automation…` (`:1465` onwards), called with `prefix` at
`:1746`, and with `${prefix}/environments/:environmentId` at `:1742` /
`:1750` when project scoping is on.
- Route ledger: `route-ledger.ts:429` `POST /automation` (client
`automation.create`) and siblings; the header (`:17`) says to prepend
`/api/v1` for the wire path.
- CLI: `packages/cli/src/commands/serve.ts:4412` calls
`createDispatcherPlugin({ scoping, enforceProjectMembership,
observability, rateLimit })`, no `prefix`; scoping defaults to off
(`:4340`).

## Consumer search: nothing depends on the unversioned form

| candidate | reads the contract's `path`? | verdict |
| --- | --- | --- |
| `packages/adapters/hono/src/hono.test.ts:453` (`GET /api/automation
delegates to dispatch()`) | no | Not a consumer. It drives
`createHonoApp` with the adapter's own default `prefix` (`options.prefix
\|\| '/api'`, `hono/src/index.ts:303`) against a mocked dispatcher and
asserts the dispatcher-internal `/automation`. It never imports the
contract. |
| `packages/client` | no | Builds automation URLs from discovery or its
`/api/v1/automation` convention (`getRoute('automation')`); it never
names `AutomationApiContracts`. Two comments name the spec test file
`automation-api.zod.test.ts`, not the constant. |
| everything else in this repo | no | `AutomationApiContracts` occurs
only in its declaring file, its spec test, `api-surface/api.json` (name
only) and `export-origins/api.json`. No generator reads the path values.
|
| objectui at the pinned `.objectui-sha` `62597c588` | no | `git grep
-F` at that commit: `AutomationApiContracts` 0 files, `/api/automation`
0 files; positive control `/api/v1/automation` 33 files. |
| `objectstack-ai/cloud` and npm consumers | not measured | not checked
out here |

One served surface does use the unversioned form: a host built with
`createHonoApp({ kernel })` and no `prefix` serves the whole dispatcher,
automation included, under `/api`. That is a documented adapter default,
and it applies to every contract family: under that host every other
`*ApiContracts` row (`/api/v1/…`) is off by the same segment. The old
automation paths matched it by coincidence, not by design, and no code
reads the contract under that host, so remedy 2 does not apply. The
changeset says how such a host maps the contract paths.

## Deliberate correction of a pending release note

`.changeset/19365-automation-runs-cursor-hasmore.md` (pending, not yet
released) quotes the `cursor` tombstone text in its FROM/TO block. That
text is one of the path quotes this PR moves, so the note became false.
Its line 32 changes from

-> throws: '`cursor` was removed from GET /api/automation/:name/runs in

to

-> throws: '`cursor` was removed from GET /api/v1/automation/:name/runs
in

Nothing else in that note changes. This is the DELIBERATE CORRECTION
class that `check-empty-changeset.mjs` names. `skip-changeset` is not
applied, and `Check Changeset` stays red **by design**. The same-head
at-tier review (comment 5824559177) names the note and judges the
changed sentence. No other pending changeset quotes an unversioned
automation path. At the base, `git grep -n "/api/automation" --
'.changeset/*.md'` showed only that line. Patch round 1 corrected no
further note: no pending changeset quotes the D3 entry's sentences.

## Changeset level

`minor`, not declared breaking. The `path` type stays `string`, no
accepted input narrows, no method changes, and the old values named
paths that no route served on the default composition, so a caller that
read the constant gets a working URL now without changing code. This
follows the precedent of the `PackageApiContracts.installPackage.path`
rebind (`.changeset/18058-install-door-contract-rebind.md`, `minor`, not
breaking). The declaration is `Clause-②: no` with no arm, in both this
body and the changeset. It answers the reader's question "does this
widen an accepted input or grow the public surface?" and the answer here
is no: no key added, no accepted input widened, no export grown. It is
not `(narrowing)` either, because nothing an author writes is removed.
`minor` is valid under `no`: a published constant's value moves, and
`patch` is a floor, not a ceiling. The first head declared `yes`, copied
from the claim, and the reviewer judged that over-declared.

## The drift pin, and proof that it can fail

`packages/runtime/src/automation-api-contract-mounts.test.ts` has two
legs:
1. **mount**: it starts `createDispatcherPlugin` with **no** `prefix`
(the composition `objectstack serve` builds) on a server that records
registrations, and requires every contract `METHOD path` to be one of
them. The prefix comes from the plugin's own default, not from a
constant in the test.
2. **ledger**: every contract route must be a `route-ledger.ts` row
under the documented `/api/v1` wire prefix. The live-mount parity gate
probes those rows through the real router.

The runtime vitest config aliases `@objectstack/spec/*` to spec
**source**, so the spec side of the pin reads `src/`, not a build. The
ablation was run on the committed tree (`4c216561c6`) with
`scripts/ablation-replace.mjs`, one leg at a time, with restores
anchored on `HEAD`:

| leg | mutation (landed on disk: anchor 1 → 0, blob moved) | pin result
|
| --- | --- | --- |
| contract | `getRun.path` back to `/api/automation/:name/runs/:runId` |
mount red, ledger red (`getRun` named), 1 passed |
| dispatcher | default prefix `'/api/v1'` → `'/api/v2'` | mount red (all
nine named), 2 passed |
| restored | none (blobs equal `HEAD`, `git diff HEAD` empty for both
paths) | 3 passed |

## Environment-scoped mount

The contract does not carry
`/api/v1/environments/:environmentId/automation…`, and this PR does not
add it. No `*ApiContracts` map declares the scoped variants. Scoping is
one mount-time transformation the dispatcher applies to automation,
actions, AI and packages alike, and the client derives scoped URLs from
discovery. If the variants are ever declared, that belongs once in a
contract shared by all the families, not copied into each map. Note that
under `projectResolution: 'required'` none of the nine unscoped paths is
mounted (`dispatcher-plugin.required-scoping-mounts.integration.test.ts`
pins that).

## Verification (head `10b1176328`, patch round 1)

- Tests, each package's full local project, under the verify lock at
this head. The lock's verdict is `batch-last-exit 0`: the last part of
the batch requires all three suites to exit 0, and the batch printed
`SUITES spec=0 service-automation=0 runtime=0`.
  - `@objectstack/spec`: 532 files, 15648 passed, 2 todo.
  - `@objectstack/service-automation`: 144 files, 1725 passed.
- `@objectstack/runtime` (`--project local`): 277 files, 3896 passed, 1
skipped, including the drift pin's 3 cases.
- Build: `turbo run build --filter='./packages/*'
--filter='./packages/*/*'` at this head: 72 of 72 tasks succeeded,
including the tsup and declaration builds of spec, runtime and
service-automation.
- Spec generated artifacts: `check:generated` reports "All 15 generated
artifacts are up to date". `check:migration-registry` reports
"src/migrations/registry.ts is current (242 semantic, 210 retired-key,
183 retired-def)". `check:spec-changes` and `check:upgrade-guide` both
report up to date. `check:api-surface` reports "public API surface +
factory signatures unchanged", and `check:docs` reports "225 generated
files in sync".
- Derived gate set, taken after `git fetch origin main`
(`dispatch-gates.mjs --commands --repo objectstack-ai/objectstack`,
merge base `adbbc5d01`, 10 paths): 113 families, 6 more than round 0
(`check:migration-registry`, `check:spec-changes`,
`check:upgrade-guide`, `check:future-spec-major`, and
`check-tenant-audit-census` with its self-test). All 113 ran, and
`--ran` reports "113 run, 0 NOT-MEASURED" with 0 UNRUN. 112 exited 0.
`node scripts/check-empty-changeset.mjs --base origin/main` exits 1 on
the deliberate correction, as designed. No family answered PREREQUISITE
NOT MET this round.
- The branch is behind `origin/main` (15 commits at the seat's re-read).
Three of those commits regenerated one of this PR's 10 paths,
`packages/spec/src/migrations/registry.ts`: objectstack-ai#20036 (`0bf85eaae6`),
objectstack-ai#19909 (`5b9402d89b`) and objectstack-ai#19818 (`66960564d9`). `git merge-tree
--write-tree` of `origin/main` and this head is clean, and the at-tier
re-review measured that objectstack-ai#20036's and objectstack-ai#19909's hunks do not touch this
PR's region (`:5940`–`:6004`). The merge queue's rebuilt generation
regenerates the file. The derivation's one changed family input across
that range is `scripts/sdui-manifest.record.json`, from objectstack-ai#20036.
(Corrected by the seat after the at-tier review `5825376693` found the
earlier sentence, "None of this PR's 10 paths is touched by those
commits", false.)
- Clause ② and ADR-0087, run offline with this body as the
`pull_request` event:
- `check-changeset-no-major --event` prints "✓ This diff introduces no
`major` bump." and "✓ LEVEL AXIS: this PR declares clause-② `no`, so no
package here is declared to have grown a published surface."
(declaration line `Clause-②: no`, no arm).
- `check-adr-0087-registration` prints "✓ … this PR adds no
declared-breaking changeset (2 non-breaking changeset(s) seen)". This
gate reads the Clause ② arm from the changeset body
(`readClause2Line(parsed.body)`), not from the PR event. Its verdict is
the same with and without `--event`.
- Lint, narrowed and proven: all 7 touched TS files are in the eslint
population (`--print-config` resolves each). `--no-inline-config
--format json` reports 7 files, 0 errors, 0 warnings.
`eslint.config.mjs` enables no type-aware linting: all seven
`parserOptions` blocks are `{ ecmaVersion, sourceType }` only, with no
`project` or `projectService`. So this diff cannot change the verdict on
any untouched file. The repo-wide `pnpm lint` is left to CI.
- Round-0 evidence still stands, and its sources are unchanged in round
1: the ablation above on `4c216561c6`, and the spec and runtime
`typecheck` runs, both exit 0. Round 1 changes only string literals (the
D3 entry and its registry mirror), comments and one changeset line. The
type-check-debt gate re-measured at this head: "4 ledger entr(ies) …
none above its recorded number".

## Acceptance notes (observations, not filed)

- `RouterConfigSchema` (`spec/src/api/router.zod.ts`) defaults
`basePath` to `/api` with `mounts.automation: '/automation'`. That
spec-only declaration has no runtime reader in this repo.
- The contract lists nine of the 17 routes `registerAutomationRoutes`
mounts. The ones not listed are resume, cancel, restore-suspension,
screen, actions, connectors, `_status` and the legacy trigger form.
- **Every remaining `/api/automation` path at head `10b1176328`, and why
it stays** (`git grep -n "/api/automation\b"`, with the `automation-api`
file-name hits filtered out):
- `.changeset/20034-automation-contract-api-v1-paths.md` `:5` and
`:15`-`:20`: the FROM column of this PR's own FROM/TO table.
- `packages/adapters/hono/src/hono.test.ts:453`-`:454`: the Hono
adapter's own default `prefix` (`/api`), driven against a mocked
dispatcher. It does not read the contract.
- `packages/runtime/src/automation-api-contract-mounts.test.ts:9`: the
pin's docblock, describing the drift it guards.
- `packages/spec/scripts/file-description.test.ts:878`-`:937`: synthetic
fixtures for the docblock-description extractor. They do not quote the
contract.
- `packages/spec/src/api/router.test.ts:343`: a custom-mounts fixture
for `RouterConfigSchema`.
- Released `CHANGELOG.md` entries: `packages/client/CHANGELOG.md:3230`,
`packages/runtime/CHANGELOG.md:11589`,
`packages/services/service-automation/CHANGELOG.md:5802` and
`packages/spec/CHANGELOG.md:32723` quote `GET
/api/automation/:name/runs` in the released ExecutionStatus-filter
entry. These are release-owned and never edited in a code PR; an
amendment would be a dedicated docs-only PR.
`packages/spec/CHANGELOG.md:14982` names the file `automation-api.mdx`,
not a path.

Implemented in session `session_019c3Hi6ZMU1p6m6aA6Bz45d` (claim
5823821835; patch round 1 dispatched by the `domain:spec` seat 4).

---------

Co-authored-by: Claude <noreply@anthropic.com>
akarma-synetal pushed a commit to akarma-synetal/framework that referenced this pull request Sep 28, 2026
…isContainer; lockstep pins the predicate (objectstack-ai#20065)

Fixes objectstack-ai#19969

Clause-②: no

## What this does

The save gate's `validateTree` in `packages/sdui-parser` now decides
`not-a-container` by the component's declared `children` input. It no
longer reads `isContainer`, and there is no fallback to the flag. This
is the byte-equal port of objectui `5ea623ea` (objectui#9910 Q1-A,
re-cut as objectui PR objectstack-ai#10270), which is already inside the console pin
`f8a9d0fb0596` (PR objectstack-ai#20036). The objectui side it mirrors is
`packages/sdui-parser/src/validate.ts` at the pin: `CHILD_LIST_KEY` at
`:33`, `acceptsChildren` at `:57-59` and the containment branch at
`:220`.

- **Predicate semantics mirrored exactly.** The test is
`comp.inputs.some((input) => input.name === CHILD_LIST_KEY)`. It matches
on the **name** `children` alone, whatever the input's coarse type. The
dispatch brief said "name `children`, type `slot`". objectui
deliberately matches on the name only, because the five page kinds
publish `children` typed `array`, and objectui pins that case. This port
follows objectui, not the brief.
- **A component the manifest has no entry for** draws
`unknown-component` and gets no containment verdict. This is unchanged
and is the same on both sides.
- **`isContainer`** stays on `ManifestComponent`, re-described as layout
containment in the same words objectui uses. `index.ts` is unchanged: no
new public export. objectui exported `acceptsChildren` and
`CHILD_LIST_KEY` from its entry for its own census tests, and nothing
here needs that.

## The lockstep pin (maintainer-named in the ruling)

`scripts/check-sdui-lockstep.mjs` now also compares the **containment
predicate**. The gate extracts the condition of the one `if` in
`validate.ts` whose body stamps `not-a-container`. It adds the
module-level declarations that condition reaches, transitively
(`acceptsChildren`, then `CHILD_LIST_KEY`), and reduces the result to
its token sequence. Comments and wrapping do not change the reading. A
changed token does. The gate compares this reading against a new
`containmentPredicate` field in the vendored objectui record.

- The record was regenerated with `pnpm gen:sdui-lockstep` against an
objectui checkout parked on the pin `f8a9d0fb0596`. Every other field of
the record is byte-unchanged; the only diff is the new
`containmentPredicate` block.
- If `validate.ts` has zero or two containment branches, the gate
refuses (`[containment-unreachable]`). It never measures only one of
two.
- A new self-test battery with 10 cases is added, and the battery floor
goes from 6 to 7. The first case asserts that the flag predicate and the
slot predicate read differently.

## The five served components (tracked `sdui.manifest.json` at
`0bf85eaa`, 5 of 59 disagree)

| component | `isContainer` | declares `children` | before (flag) |
after (declared input) |
|---|---|---|---|---|
| `badge` | false | yes (`slot`) | warns `not-a-container` (false) | no
warning |
| `alert` | false | yes (`slot`) | warns (false) | no warning |
| `button` | false | yes (`slot`) | warns (false) | no warning |
| `page:tabs` | true | no | silent | warns `not-a-container` |
| `page:accordion` | true | no | silent | warns `not-a-container` |

The new `containment-declared-slot.test.ts` uses these five entries,
copied verbatim from the manifest. Each gets its own case, plus a
control asserting that the two predicates really disagree on every one.
The file also carries objectui's 2×2 (flag × slot) reading.

## Ablation (a trap restores the file, and the restore is proven
byte-identical to HEAD by blob hash and an empty `git diff HEAD`)

- **Leg 1: revert the port.** The branch was changed back to
`!comp.isContainer` through `scripts/ablation-replace.mjs`: anchor 1 to
0, blob `819b7b6a83ac` to `670ce98bdb15`. `node
scripts/check-sdui-lockstep.mjs` exited **1** with
`[containment-drift]`, printing both texts. The file was then restored
to blob `819b7b6a83ac` = HEAD.
- **Leg 2: sneak the fallback into the helper.** The mutation appended
an `isContainer` fallback to the `acceptsChildren` return. The gate
exited **1** with `[containment-drift]`. Restored to `819b7b6a83ac`.
- **Test-suite leg (same mutation as leg 1).** The new test file went
**8 failed / 5 passed** (exit 1). Restored.
- **Restore leg.** `pnpm check:sdui-lockstep` on HEAD `f3ff1e6e9` exits
**0**: the self-test passes, and the gate reports agreement on all 26
codes and on the `not-a-container` predicate (blob `76c18fb95d1f`).

## Verification (HEAD `f3ff1e6e9`; re-run on `bfb082b0b` in the section
below)

- `pnpm --filter @objectstack/sdui-parser exec vitest run`: 11 files,
189 tests passed (VERDICT command-exit 0).
- `pnpm --filter @objectstack/sdui-parser typecheck`: exit 0.
`--listFiles` includes the new test.
- `pnpm check:sdui-lockstep`: exit 0.
- `node scripts/pm/dispatch-gates.mjs --repo objectstack-ai/objectstack
--commands` derived 81 commands. `--ran` reconciles: 77 exit 0, 4 NOT
MEASURED, 0 unrun.
- NOT MEASURED, prerequisite unmet: `check:dual-build-cjs-loads`,
`check:lean-entry-closure` and `check:type-check-debt` each exited 3
because they need a full workspace build.
- NOT MEASURED, over the time cap: `check:pm-dispatch-gates`. Its
self-test ran past the foreground cap, was killed twice, and is declared
to CI.
- `check-plugin-teardown-shape --self-test` first exited 3 because this
is a shallow checkout. It exited 0 after fetching its pinned fixture
commit.
- `eslint --no-inline-config` over the 6 touched source files: 0 errors,
0 warnings. This is a targeted run, not a proven narrowing, so the full
`pnpm lint` is declared to CI.

## packages/lint fixture: resolved by PM ruling `5825043664`

The PM ruling on objectstack-ai#19969 (comment `5825043664`, Q2 = A) widened this card
file surface by exactly one test fixture. Commit `bfb082b0b` gives the
hand-made `flex` entry at
`packages/lint/src/validate-jsx-pages.test.ts:39` the `children` input,
the same triage objectui made on its own fixtures:

```ts
flex: { type: flex, namespace: ui, isContainer: true, inputs: [{ name: children, type: slot }] },
```

No other `packages/lint` file changes; `authoring-rules.ts` and the lint
caller are untouched. Q1 = A, provisionally: the changeset stays `patch`
with `Clause-②: no`, and the contract review judges it on the final
head.

On head `bfb082b0b`, after rebuilding the `@objectstack/lint` dependency
closure:

- `pnpm --filter @objectstack/lint exec vitest run`: 108 of 108 files
passed; 4133 tests passed, 5 skipped, 0 failed (VERDICT command-exit 0).
Before this commit it was 1 failed / 4132 passed.
- `pnpm --filter @objectstack/lint typecheck`: exit 0.
- sdui-parser suite: 189 of 189 passed. `pnpm --filter
@objectstack/sdui-parser typecheck`: exit 0. `pnpm check:sdui-lockstep`:
exit 0.
- `dispatch-gates --commands` on the 9-path set derived 82 commands (one
new: `check:docs-transcript-drift`, exit 0 after building
`@objectstack/lint`). `--ran` reconciles: 78 exit 0, 4 NOT MEASURED, 0
unrun. The four are `check:dual-build-cjs-loads`,
`check:lean-entry-closure` and `check:type-check-debt` (each exit 3,
prerequisite: a full workspace build) and `check:pm-dispatch-gates`
(over the foreground cap). All four are declared to CI.

## Acceptance notes

- `codegen.ts`'s `generateBlockList` still labels its column "container"
from `isContainer`. Under Q2-A that column now means layout container.
It is unchanged here, and objectui leaves it as-is too.

---
_Generated by [Claude
Code](https://claude.ai/code/session_01VDtqoecgES7ScQYGbFVDRv)_

---------

Co-authored-by: Claude <noreply@anthropic.com>
akarma-synetal pushed a commit to akarma-synetal/framework that referenced this pull request Sep 28, 2026
…s / related_list with one true describe shared with record:quick_actions (objectstack-ai#19913)

Fixes objectstack-ai#18159

Clause-②: yes

<sub>Rewritten short by the `domain:spec#5` seat (2026-09-23T19:59Z).
The dev reports are on objectstack-ai#18159 (`5798750532`, `5800008407`, `5800978880`,
`5801656924`); the earlier long body is in the edit history.</sub>

`requiredPermissions` is now declared on `record:details`,
`record:highlights` and `record:related_list`, with the shape
`record:quick_actions` already has (`z.array(z.string()).optional()`).
Ruling: batch objectstack-ai#197 item 2, letter A (`5749268463`), after objectui#10058
became installable at `.objectui-sha` `62597c588072`.

## One describe, four blocks (seat ruling `5798783314`)

The ruling asks for the same describe as `record:quick_actions`. That
published describe said "…every named permission **on this object**",
which is false for the renderer at the pin: the check is
`hasCapabilities`, and the renderer's own comment says the capability
"is not object-scoped". So all four blocks now share ONE describe, from
one constant (`RECORD_BLOCK_REQUIRED_PERMISSIONS_DESCRIPTION`). **The
published `record:quick_actions` describe changes; its shape does not.**
The describe states:
- the names are ADR-0066 capabilities, not object actions;
- the user must hold all of them;
- when one is missing, the block shows an insufficient-permissions
notice instead of its content;
- it is presentation only, and the data API still serves the data;
- a client that cannot resolve the user's capabilities renders the block
(fails open).

The renderer lines behind each clause are in the dev report
`5800008407`.

## Other changes

- The texts that said the key is "deliberately NOT declared" are
rewritten: the `RecordDetailsProps` family header, the related-list and
highlights pointers, and the test header.
- The absence pins are now accept pins: a parse probe and `.shape`
enumeration per block, with `aria` and `fields` as lit controls, plus a
pin that the four JSON Schemas of the key are identical. Removing the
three declarations turns 11 of 25 pins red.
- `authorable-surface/ui.json` and `component.mdx` are regenerated.

## `Check Changeset` is red on purpose

The pending note `.changeset/18159-record-block-field-security-pair.md`
(from PR objectstack-ai#19185) said the third key "is deliberately NOT declared". This
PR makes that false, so the PR corrects it. That is a DELIBERATE
CORRECTION under `check-empty-changeset.mjs`: the check stays red, and
the correction awaits the maintainer's written confirmation on this PR.
The line to confirm is quoted verbatim in `5801679846`. `Check
Changeset` is not a required context.

## Fix round 2026-09-25: `origin/main` merged, objectui pin re-measured

Fix round for `domain:spec#4` (session
`session_019c3Hi6ZMU1p6m6aA6Bz45d`), new head `b19ac16c77`. The merge
queue dequeued the PR. `Type Check · source gates` failed at
`check:objectui-pin-citations`: objectstack-ai#20036 (`0bf85eaae6`) moved
`.objectui-sha` from `62597c588072` to `f8a9d0fb0596`, and the shared
`requiredPermissions` docblock in `component.zod.ts` still asserted the
old pin. This round adds three commits and changes nothing else:

- `62decd77dd` merges `origin/main` at `66960564d9` with
`scripts/pm/os-regen-merge.sh` (merge commit; no rebase or force-push).
It had no text conflict.
- `a3075b6ace` regenerates `packages/spec/authorable-surface/ui.json` on
the merged tree. In that file the merge driver kept this branch's side
and dropped main's 13 `DocNavItem` keys. The regenerated file has main's
keys plus this PR's three `requiredPermissions` keys, and nothing else.
- `b19ac16c77` re-measures the docblock at the new pin. It updates the
sha and the anchors together (10 lines replaced, 0 added).

### What was measured

objectui at `f8a9d0fb0596f4521076628e2bbfe27e6ce67d52` was read from a
scratch fetch of both pins. The seven cited files match the codeload
tarball of the same commit byte for byte. All 21 anchors in the docblock
were re-read. **At the new pin, each cited range holds the same text it
held at `62597c588072`.** No read point changed meaning or disappeared.

- `record-details.tsx` shifted +1 (one new import line).
`record-related-list.tsx` shifted +34 (an import block and the
props-type docblock). Nine anchors get new numbers.
- `record-highlights.tsx`, `record-quick-actions.tsx`,
`MePermissionsProvider.tsx`, `PermissionProvider.tsx` and
`usePermissions.ts` did not change between the two pins, so their twelve
anchors keep their numbers.

| Anchor, old → new | First line of the range at `f8a9d0fb0596` |
|---|---|
| `record-details.tsx:234` → `:235` | `if (required.length > 0 &&
!perms.hasCapabilities(required)) {` |
| `record-details.tsx:223` → `:224` | `* capability is not
object-scoped, and the old && objectName conjunct was` |
| `record-details.tsx:234-242` → `:235-243` | `if (required.length > 0
&& !perms.hasCapabilities(required)) {` |
| `record-details.tsx:186` → `:187` | `if (!ctx) {` |
| `record-related-list.tsx:242` → `:276` | `if (required.length > 0 &&
!perms.hasCapabilities(required)) {` |
| `record-related-list.tsx:229` → `:263` | `* capability is not
object-scoped. This site never carried the` |
| `record-related-list.tsx:242-250` → `:276-284` | `if (required.length
> 0 && !perms.hasCapabilities(required)) {` |
| `record-related-list.tsx:184` → `:218` | `if (!objectName) {` |
| `record-related-list.tsx:202` → `:236` | `if (perms.isLoaded &&
!perms.can(objectName, 'read')) {` |
| `record-highlights.tsx:93` (unchanged) | `const highlightsAllowed =
required.length === 0 \|\| perms.hasCapabilities(required);` |
| `record-highlights.tsx:77` (unchanged) | `* capability is not
object-scoped, and the old && objectName conjunct was` |
| `record-highlights.tsx:151-164` (unchanged) | `if (!highlightsAllowed)
{` |
| `record-highlights.tsx:146-149` (unchanged) |
`useRegisterHighlightFields(` |
| `record-quick-actions.tsx:263` (unchanged) | `if (required.length > 0
&& !perms.hasCapabilities(required)) {` |
| `record-quick-actions.tsx:252` (unchanged) | `* capability is not
object-scoped, and the old && objectName guard was a` |
| `record-quick-actions.tsx:263-271` (unchanged) | `if (required.length
> 0 && !perms.hasCapabilities(required)) {` |
| `MePermissionsProvider.tsx:416`, and the later `:416` (unchanged) |
`return required.every((p) => held.has(p));` |
| `MePermissionsProvider.tsx:414` (unchanged) | `if
(!Array.isArray(perms)) return true;` |
| `PermissionProvider.tsx:77` (unchanged) | `const ALL_CAPABILITIES:
PermissionContextValue['hasCapabilities'] = () => true;` |
| `usePermissions.ts:45` (unchanged) | `hasCapabilities: () => true,` |

(The inner backticks around `&& objectName` in the three docblock lines
are left out of the table.)

One observation from the re-read, which changes no anchor. The
props-type docblock that `record-related-list.tsx` gained (`:89-92`)
says no block the contract maps onto this tag declares
`requiredPermissions`, and says not to reopen the type to admit it. This
PR makes that sentence stale. That is the objectui half ruled **A** on
objectstack-ai/objectui#10281, and it moves to the `domain:ui` seat. The
renderer's read of the key (`:240-242`, through a cast) and its gate
(`:276`) are unchanged.

### Gates, on head `b19ac16c77`

- `check:objectui-pin-citations` exits 0 on `--self-test`, on the
ordinary run and on `--verify-anchors`, with objectui at the new pin
supplied through `OBJECTUI_ROOT`. The ordinary run prints: "48 asserting
objectui pin citation(s) match .objectui-sha (f8a9d0fb0), 40 historical
citation(s) recorded and not checked, across 1555 spec source(s). 7
anchor content assertion(s) verified against objectui at f8a9d0fb0". CI
has no objectui checkout, so it verifies the sha label only.
- `pnpm --filter @objectstack/spec build`, then `check:generated`: all
15 generated artifacts are up to date. `typecheck` exits 0. The full
spec suite: 535 files, 15727 passed, 2 todo.
- `node scripts/pm/dispatch-gates.mjs --commands` derived 107 commands
for this change set. All 107 ran, and `--ran` reconciles 107 of 107 with
recorded exit codes and 0 NOT MEASURED. Seven of them first exited 3 (no
build to read). They were re-run green after building the lint closure,
the client-react closure and then every package. 106 exit 0. The one
exit 1 is `check-empty-changeset.mjs`. That is the deliberate correction
described above, which the maintainer confirmed at head `aeb6a57456`
(`5823706830`).
- The changesets are byte-identical to that head: `git diff --exit-code
aeb6a57 b19ac16 --
.changeset/18159-record-block-field-security-pair.md
.changeset/18159-record-block-required-permissions.md` exits 0. The
blobs are `d502a9a04c6a` and `80546d851168` on both heads. The test file
and `component.mdx` are byte-identical to that head as well.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

https://claude.ai/code/session_01Sfe5YjBLwB9J3y8fvm2xq1

---------

Co-authored-by: Claude <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

1 participant