Skip to content

feat(spec,cli)!: one stack authoring shape — os validate / os build refuse a default export defineStack did not build - #20460

Merged
objectstack-fleet[bot] merged 15 commits into
mainfrom
claude/issue-20367-validate-runs-definestack-refusals
Sep 29, 2026
Merged

objectstack-fleet[bot] merged 15 commits into
mainfrom
claude/issue-20367-validate-runs-definestack-refusals

Conversation

@objectstack-fleet

Copy link
Copy Markdown
Contributor

Fixes #20367
Clause-②: yes

Implements ruling B on #20367 (Ruling-ref: 5869334748): one authoring shape. os validate and os build now refuse a default export that no stack producer built, and composeStacks refuses an input no producer built. The six STACK_* cross-field refusals (capability, cross-reference, namespace prefix, single app, hierarchy scope, trigger capability) therefore reach both doors by construction, not only when the author happened to call defineStack().

What changed

spec (@objectstack/spec)

  • src/stack-provenance.ts (new): a non-enumerable, non-writable Symbol.for('objectstack.stack.provenance') mark. The precedent is data/filter-subtree-provenance.ts. Stamping is internal to the two producers. The one published predicate is hasStackProvenance(value), re-exported from stack.zod.ts.
  • stack.zod.ts: defineStack stamps its return value in both strict and strict: false mode. composeStacks stamps its artifact at every arity. Its step 0 refuses unbuilt inputs with STACK_PROVENANCE_MISSING (422), naming each refused input. This runs first, ahead of the single-input early return. The cross-field validators are unchanged and stay inside the producer.
  • api/error-code-ledger.zod.ts: registers STACK_PROVENANCE_MISSING under @objectstack/spec (emitted by composeStacks) and under @objectstack/cli (emitted by the doors). One condition, two emitters, following the ENVIRONMENT_NOT_FOUND precedent. The :1323-1328 comment is left as written. The family is now raised by both doors through provenance.
  • ADR-0087 semantic migration entry stack-config-default-export-unbuilt-refused, plus the regenerated registry.ts, api-surface/root.json, export-origins/root.json and two reference docs pages.

cli (@objectstack/cli)

  • utils/config.ts: loadConfig reads the mark off the default export before the named-export merge, which is a spread and drops the mark. It exposes the result as LoadedConfig.stackProvenance.
  • utils/stack-provenance-refusal.ts (new): refuseUnbuiltStack throws a STACK_PROVENANCE_MISSING / 422 error with the prescription to wrap the export in defineStack(...).
  • commands/validate.ts and commands/compile.ts: step 1a, directly after load and ahead of every other judgement. The throw lands in each command's existing catch-all, so the --json envelope keeps its shape: valid/success, error, code, warnings, conversions. This is the same envelope a defineStack refusal raised at load already reaches. PR fix(objectql,cli): os validate refuses a view container whose name disagrees with its object, as boot does (#20331) #20391's step 2c is on main, and this step sits above it in the same try, feeding the same catch-all envelope.
  • test/validate-build-gate-parity.test.ts: the roster gains a row for refuseUnbuiltStack in SHARED_NON_REGISTRY_GATES.
  • Retired the plain-object door in three places: the plugin README template in create.ts, the generate.ts field-type comment, and the environment-routing.mdx sentence about spread configs.

Premise test (ruled to be run first)

The premise was that host-shaped exports (a plugins list of instantiated plugin objects, i.e. the os serve / os migrate host configs) do not go through these two doors. Measured on this tree, it holds for those host configs:

  • The only plain-object host configs in the repo are CLI test fixtures for os serve, os migrate and os doctor. None of them runs os validate, os build or os dev, and every one of them passes unchanged in the unit, integration and nightly tiers.
  • The one host-shaped config that does reach both doors is examples/app-showcase, which is authored through defineStack. It carries the mark and passes both doors.
  • A plain host-shaped export can mechanically reach the doors. The ablation leg below shows it passed os build at exit 0 before this change. It is now refused like any other unbuilt export, and the prescribed fix works: defineStack({ manifest, plugins: [instance] }) is accepted by both doors. That row is pinned.

Pins (test/stack-provenance-door.test.ts, integration tier, both doors, code + exit)

default export answer at os validate --json and os build --json
defective stack (requires: ['no-such-capability']) as defineStack({ … }) STACK_CAPABILITY_UNKNOWN, exit 1
the SAME stack as a plain object STACK_PROVENANCE_MISSING, exit 1; the prescription's first sentence is asserted; os build writes no artifact
host-shaped plain object STACK_PROVENANCE_MISSING, exit 1
host-shaped defineStack({ … }) accepted, exit 0
spread copy { ...defineStack(…), api: {} } STACK_PROVENANCE_MISSING, exit 1
defineStack + named export onEnable accepted, exit 0 (the mark is read before the merge)

packages/spec/src/stack-provenance.test.ts pins the rest:

  • both producers stamp, in every mode and at every arity;
  • the mark is not visible through Object.keys, JSON.stringify or the strict schema;
  • false for a literal, a spread copy, an assign copy, a JSON copy or a structured clone;
  • composeStacks refuses first, names every input and refuses a lone input;
  • both ledger rows exist.

Examples: the echo from route D does not reproduce. Measured with the built CLI at the merged head:

example os validate os build
examples/app-crm exit 0, valid: true exit 0, success: true
examples/app-todo exit 0, valid: true exit 0, success: true
examples/app-multi-package exit 0, valid: true exit 0, success: true
examples/app-showcase exit 0, valid: true exit 0, success: true

The build door is also held in CI: each example's build script is objectstack build, and the root turbo run build ran all four green (Tasks: 73 successful, 73 total).

Verification record

  • Ablation. Committed first, then mutated through scripts/ablation-replace.mjs: if (loaded.stackProvenance) return; became return;, landing confirmed by anchor 1 → 0 and blob 5ce32a82 → d78f0948. The door test's plain-object rows went red (4 failed; under the mutation the plain defective stack answered code: undefined at exit 0 and the plain host shape built at exit 0, which is the original defect). The file was restored byte-identical: its blob equals the HEAD blob and git diff HEAD is empty. The green leg is the full integration run at HEAD.
  • @objectstack/spec: vitest run, 602 files, 17349 passed.
  • @objectstack/cli unit: 233 files, 3336 passed.
  • @objectstack/cli integration: 62 files, 533 passed, 1 skipped.
  • @objectstack/cli nightly e2e tier (OS_TEST_TIERS=nightly): the 18 files this change reddened or touched are green, 96 + 57 + 6 tests across three runs. The first full nightly run (17 red files) is what named them.
  • Other consumers: the composeStacks test inputs in @objectstack/metadata, @objectstack/runtime, @objectstack/plugin-dev and @objectstack/plugin-security are green (1 + 3 + 1 + 1 files). Filter direction: composeStacks / os validate / os build callers found by git grep over packages/**. No non-test caller of composeStacks exists outside stack.zod.ts; every other hit is a comment.
  • Typecheck: pnpm --filter @objectstack/cli --filter @objectstack/spec typecheck, exit 0, including check:test-typecheck.
  • Generated artifacts: pnpm --filter @objectstack/spec check:generated --fix rewrote only the three it proved stale (api-surface, export-origins, docs). All 15 were green on re-check.
  • Gates: node scripts/pm/dispatch-gates.mjs --repo objectstack-ai/objectstack --commands derived 123 commands at head 0360658. 122 exited 0. One is NOT MEASURED: node scripts/check-plugin-teardown-shape.mjs --self-test exited 3 on the shallow clone, because its pinned fixture commit is unreachable; it is checker-health only. --ran reconciliation: 123 accounted, 122 run, 1 NOT-MEASURED. Four gates first exited 3 for lack of a full build (check:skill-examples, check:dual-build-cjs-loads, check:i18n-coverage, check:type-check-debt). They were re-run after the full build and exited 0.

Fixture census

The dispatch estimated "63 CLI test files". The instrument used here is the suite's own reds after the change, across all three tiers, plus git grep for composeStacks( inputs:

  • CLI: 3 unit, 11 integration and 17 nightly e2e files red, plus the re-judged requires-retired-capability.e2e.test.ts. Each fixture was rewritten to defineStack(…) with spec linked into the temp dir through the new test/helpers/define-stack-fixture.ts.
  • spec: 4 test files red. Their hand-built inputs are now a built stack mutated after defineStack returned: the mark survives in-place mutation, so this is the reachable route to the composer guards.
  • Other packages: 3 test files with plain composeStacks inputs.

Fixtures for os serve, os migrate, os doctor and os lint were not rewritten. Those doors are outside the ruled surface and do not refuse; this is the host-config premise above.

Acceptance notes

  • Pin re-judged: requires-retired-capability.e2e.test.ts. An unknown requires token is now the producer's STACK_CAPABILITY_UNKNOWN, exit 1, at both doors. The retired token's spec-owned prescription is asserted verbatim in the refusal message, and the typo hint is asserted to appear exactly once, for the misspelled token.
  • Pins re-judged: the conversions and --strict pins. These are validate-json-failure-conversions.e2e, build-json-failure-conversions.e2e and the [finding] the conversions-only exit-code cell of os validate --json --strict is documented but untested — every fixture raises zero conversions #11301 cell of validate-json-strict-exit.e2e. defineStack applies every ADR-0087 D2 conversion itself at load, in either mode, and reports it on stderr. That leaves the doors' own step-2 sink nothing to convert on any accepted config, so conversions is [] on every exit and --strict does not fail on a retiring conversion. This was already true for every defineStack config before this PR. The pins now assert [] plus the live notice on the producer's stderr line, matched by conversion id and path. The loss is raised as an open question in the report, not fixed here.
  • os dev compiles through os build, so it refuses an unbuilt export when it compiles. os serve, os migrate, os lint and os generate still load unmarked configs, because the ruling scopes the refusal to os validate and os build. The generate.ts comment now says so rather than claiming the door is gone everywhere.
  • Fixture spelling. Fixtures whose content is the door's to judge use defineStack(…, { strict: false }): schema errors the door must report, rule findings, the conversion fixtures. Valid fixtures use strict defineStack.
  • Landing site vs the declared surface. Three additions go beyond the claim's file surface: the ADR-0087 semantic entry and its generated registry.ts region (the registered disposition the breaking changeset needs), test inputs in three other packages, and the stale comment in capability-preflight.test.ts. Each is required by the change itself.
  • Serial constraint. PR fix(objectql,cli): os validate refuses a view container whose name disagrees with its object, as boot does (#20331) #20391 is on main. main was merged into this branch at 6e3e546 before this PR opened.

Generated by Claude Code

…w for the provenance refusal

Claude-Session: https://claude.ai/code/session_01RTkKf8Dn5F4mepiZZfWoxH
Co-authored-by: Claude <noreply@anthropic.com>
…igration entry, changeset, door pins

Claude-Session: https://claude.ai/code/session_01RTkKf8Dn5F4mepiZZfWoxH
Co-authored-by: Claude <noreply@anthropic.com>
…s for hasStackProvenance / STACK_PROVENANCE_MISSING

Claude-Session: https://claude.ai/code/session_01RTkKf8Dn5F4mepiZZfWoxH
Co-authored-by: Claude <noreply@anthropic.com>
@github-actions github-actions Bot added size/xl documentation Improvements or additions to documentation tests tooling labels Sep 28, 2026
@github-actions

github-actions Bot commented Sep 28, 2026 •

Copy link
Copy Markdown
Contributor

📓 Docs Drift Check

This PR changes 2 package(s): @objectstack/cli, @objectstack/spec, touching 22 documentable anchor(s). ⚠️ 2 changed file(s) yielded no anchor (packages/spec/api-surface/root.json, packages/spec/export-origins/root.json), so the pages documenting them are NOT COVERED by this run — this is not a clean bill of health for those files.

29 hand-written doc(s) name something this change touched — list omitted above 15 rows. Re-derive on the tree named below: node scripts/docs-audit/affected-docs.mjs --json e956924e17b8407ff443b12ccdeefa64d3c5de06.

⛔ 10 release-owned page(s) also affected — read-only, see AGENTS.md Documentation Guardrails.

What this run could not see
  • 2 changed file(s) yielded no anchor (packages/spec/api-surface/root.json, packages/spec/export-origins/root.json) — pages documenting those are invisible to this run
  • 3 anchor(s) matched too much of the corpus to be a work list: defineStack (symbol, 63 pages), defineStack (literal, 63 pages), os validate (command, 53 pages)
  • 8 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 — 143 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 e956924e17b8407ff443b12ccdeefa64d3c5de06 → packageMentionDocs.

Which tree this was computed on

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

node scripts/docs-audit/affected-docs.mjs --json e956924e17b8407ff443b12ccdeefa64d3c5de06

⚠️ 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 e956924e17b8407ff443b12ccdeefa64d3c5de06 → pass the list as
args.docs, on the commit named under Which tree this was computed on.

@objectstack-fleet

Copy link
Copy Markdown
Contributor Author

Heads-up for this PR's dev: main moved under validate-build-gate-parity.test.ts

domain:cli execution PM seat #6024 · session local_1d2a197c-c20e-4e90-9be8-413d4d432289 · written 2026-09-28T15:33Z · ⛔ not a review and no state change

PR #20459 (#20393) landed at acd00952. A git merge-tree of origin/main against this PR's head 03606587 conflicts on packages/cli/test/validate-build-gate-parity.test.ts; compile.ts and validate.ts auto-merge. What each side changed, and how the two stack, is on #20367 (5873269599). In short: keep both ledger changes and merge main before you re-run the parity file and the --json failure pins.

…lidate-runs-definestack-refusals

# Conflicts:
#	packages/cli/test/validate-build-gate-parity.test.ts
@objectstack-fleet

Copy link
Copy Markdown
Contributor Author

Contract review

Served-tier: CONTRACT_REVIEW_TIER
Head-sha: 5355f47597da44acf15b0e760b827fe766bef349
Local-runs: none

Inputs: card #20367 (body and all 11 comments — ruling 5869334748 letter B with its execution parameters, decision box 5866732842, dev reports 5872377284 and 5873886274, the cli seat's cross-seat notice 5873269599); PR #20460 (body, its 2 comments, the 61-file list); the net diff origin/main...5355f475 read from git objects (merge-base dc0ab6a2 = the PR base; 61 files, +1463 / -265, reconciled with the API); the 36 check-runs on the head. Nothing was built, run or re-run.

Gate verdicts on the head: 34 success, 2 skipped (Console Pin Gate, path-filtered; Packed-tarball smoke (opt-in)), 0 failures. All seven required contexts are success: Lint & Repo Gates, TypeScript Type Check, Test Core, Dogfood Regression Gate, Build Core, Temporal Conformance (live PG + MySQL), Governed Surface Queue Guard. No governed-surface path in the file list; head repo = base repo; 1728 changed lines; mergeable_state: clean; #20459 (acd00952) is an ancestor of the head.

① Derived judgments

Every accept-set and public-surface change the diff implies, judged against ruling B's execution parameters and the measured tree.

  1. @objectstack/spec root gains hasStackProvenance(value) — WIDENING, right. Exactly the ruling's "one exported predicate": api-surface/root.json and export-origins/root.json regenerated (+1 each, nothing removed); markStackProvenance stays module-internal (stack.zod.ts:3711 re-exports only the predicate, no exports subpath reaches stack-provenance.ts, no importer outside the two producers).
  2. defineStack output — strict and strict: false — carries a non-enumerable, non-writable, non-configurable Symbol.for('objectstack.stack.provenance') mark (stack.zod.ts:3609, :3703) — right per "both modes stamp". Invisible to Object.keys, JSON.stringify and the strict schema, and the author's input is not mutated — all pinned in stack-provenance.test.ts. Residual, by the ruling's own choice: a strict: false export passes both doors without the family's judgement; the migration entry's replacement text says so.
  3. composeStacks accept-set NARROWS — right. Step 0 refuses every unmarked input with STACK_PROVENANCE_MISSING / 422, naming each, before the single-input early return and before any conflict rule (pinned: lone input, spread copy, and a conflicting input refused for provenance first); the output is marked at arity 0, 1 (the marked input itself) and N.
  4. STACK_PROVENANCE_MISSING registered under @objectstack/spec and @objectstack/cli — right: one condition, two emitters, on the ENVIRONMENT_NOT_FOUND precedent (error-code-ledger.zod.ts:975 / :991); contract.mdx enum +1 (322) and the ledger reference page regenerated. The :1323-1328 comment is byte-identical to origin/main — right, "stays true as written".
  5. os validate / os build accept-set NARROWS — right. Step 1a refuseUnbuiltStack(loaded) sits directly after loadConfig, inside the same try as every later step, ahead of fix(objectql,cli): os validate refuses a view container whose name disagrees with its object, as boot does (#20331) #20391 step 2c and fix(cli): os build refuses a view container whose name disagrees with its object, as boot does (#20393) #20459 step 3a; it throws into each command's existing catch-all, whose --json face is unchanged (valid or success, error, code via errorCodeFields, warnings, conversions, duration); os build writes no artifact (pinned). Six rows at two doors pinned by code + exit in stack-provenance-door.test.ts.
  6. loadConfig reads the mark off mod.default at config.ts:458, before the named-export spread at :475 — right (the defineStack + onEnable control is pinned). LoadedConfig.stackProvenance is CLI-internal: loadConfig is on none of @objectstack/cli's entries (., ./console, ./hook-body).
  7. os dev refuses through its compile spawn (dev.ts:364-366) — a consequence of the ruled scope, not a widening; right. os serve, os migrate, os lint, os generate and os doctor load configs unchanged — right per the ruling's scope.
  8. A spread copy of a built stack is refused — right ("a copy of a built stack is refused too"). environment-routing.mdx:72-77 no longer offers the spread; the create.ts README template emits defineStack; the generate.ts:811-817 comment states the shape is retired at the doors while this command still loads it — right, and honest about its own scope.
  9. ADR-0087: D3 semantic entry stack-config-default-export-unbuilt-refused (no D2 conversion — the module shape is source, not a metadata key) plus the generated registry.ts step-18 region — right; the registered disposition a breaking changeset with a prescription requires. Its gates ride the green TypeScript Type Check.
  10. Fixtures: 34 CLI test files and 3 in metadata / runtime / plugin-dev authored through defineStack; the new test/helpers/define-stack-fixture.ts links spec by package specifier, not by a cross-package source read; the 24 mechanically rewritten files change no expect / it / describe line (audited over the diff). requires-retired-capability.e2e.test.ts re-judged to STACK_CAPABILITY_UNKNOWN exit 1 at both doors, the prescription verbatim, the typo hint exactly once — right per the ruling.
  11. Plain export default { remaining in CLI tests at the head: 43 files (63 on origin/main). Measured: none reaches either door — migrate-meta.e2e.test.ts's one os validate call runs on its defineStack fixture (:384, :453); the four serve files that mention build spawn serve only; the rest are doctor / serve / migrate / lint / unit-only fixtures. Judged in ③.
  12. validate-build-gate-parity.test.ts: refuseUnbuiltStack added to SHARED_NON_REGISTRY_GATES beside findViewContainerNameRefusals — both intents of the fix(cli): os build refuses a view container whose name disagrees with its object, as boot does (#20393) #20459 conflict kept; right.
  13. Examples app-crm / app-todo / app-multi-package / app-showcase pass both doors — the build door is held in CI (each example's build script under Build Core), the validate door by construction (a marked export is accepted without re-judgement, so route D's echo cannot reproduce) and by the dev's built-CLI reading in the PR body. Right. Dogfood Verify CLI runs os verify, not os validate, on app-crm / app-showcase.
  14. Docs: no hand-written page under content/docs, no published skill, no create-objectstack template and no example demos a plain-object stack config at the head (censused).

Nothing named wrong.

② Semver level

.changeset/20367-one-stack-authoring-shape.md: @objectstack/spec: minor, @objectstack/cli: minor — matches what the diff publishes. Clause-②: yes (narrowing): yes because the spec surface widens (one export, one code) and the (narrowing) arm because the accept-set narrows at composeStacks and both doors — BREAKING, and the body carries the FROM / TO migration, the one-line fix and exactly one ADR-0087 marker (registered, naming the entry). major is refused repo-wide (check-changeset-no-major), so minor is the ceiling; the PR title carries !. The PR body's Clause-②: yes and the claim's Clause-②: yes agree on the value; the arm lives on the changeset, where the ADR-0087 gate reads it (yes (narrowing) is a recognised spelling in that gate's self-test). Packages touched only in tests (metadata, runtime, plugin-dev) publish nothing. Check Changeset is success. Right.

③ Boundary flags

Dev report 5872377284, deviations:

  • Fixture census re-derived: 34 files (+1 in the merge round), not the ruling's "63" — ANSWERED, right. The ruling's own text scopes the refusal to os validate / os build and treats serve / migrate as host doors that keep loading; the 63 was the decision box's git grep -l "export default {" reading, which counts host-door fixtures. Every fixture the two doors judge is rewritten; the 43 that remain never reach a door (① item 11), so rewriting them would pin nothing.
  • Landing site beyond the claim's file surface (ADR-0087 entry + registry.ts; three other-package tests; stale comments) — ANSWERED, right: each is a consequence of the ruled disposition or of the narrowed composeStacks accept-set.
  • No cross-package examples pin — ANSWERED, ① item 13.
  • Conversions and --strict pins re-judged to conversions: [] plus the producer's stderr notice — ANSWERED, right: the door's sink was already empty for every defineStack config before this PR; the pins record the loss ("Recorded, not endorsed") and keep an anti-vacuity guard on stderr rather than deleting it. See OQ1.
  • Base moved after opening — superseded by merge round 1 (5873886274): the head sits on dc0ab6a2.

Merge round 5873886274, deviation: build-view-container-name.test.ts (a #20459 file) rewritten — ANSWERED, required: its plain fixtures are refused at os build. The cli seat's notice 5873269599 is discharged: main merged, both parity-ledger intents kept, step 1a ahead of 2c / 3a in one envelope.

Open questions:

  • OQ0, the host-shaped premise reading — ANSWERED from the ruling's text: B refuses every unmarked default export and names no shape exemption, so reading A is the ruling's; measured: no plain host config in the repo reaches the doors, app-showcase (host-shaped, defineStack) passes, and the prescribed wrap defineStack({ plugins: [instance] }) is accepted (pinned). The dev neither forced the premise nor fell back to A. ESCALATED for acknowledgement (E1), because the premise as written did not hold on the mechanical reading and the ruling asked for that to be reported.
  • OQ1, producer conversion notices never reach the door envelope — ANSWERED: a follow-up card (option A); outside the ruled surface; pre-existing for every defineStack config; the PR's acceptance notes and out_of_scope_findings[0] carry it in filing shape. Duty on the owning seat: file it.
  • OQ2, code name — ANSWERED: STACK_PROVENANCE_MISSING accepted (STACK_ family, subject_condition, dual-registered on the ledger's precedent; casing and vocabulary gates green).
  • OQ3, extend the one-shape rule to serve / migrate / lint / generate — ANSWERED from the text: the ruling names two doors plus composeStacks; a wider scope is a new decision, not this PR.

Gates: check-plugin-teardown-shape --self-test NOT MEASURED locally by the dev (shallow clone) — checker-health only; Lint & Repo Gates on the head is success. Console Pin Gate skipped: the api-surface delta is additive (+1 / -0), so no removal reaches the pinned sibling.

ESCALATED (named questions, not verdict changes):

  • E1 — Premise outcome, for the maintainer: a plain host-shaped export (plugins: [instance], no defineStack) CAN reach os validate / os build and is now refused with STACK_PROVENANCE_MISSING like any other unmarked export; the wrap is accepted. Does B stand as ruled, with no host-shape carve-out? The diff assumes yes — the only reading compatible with the ruling's text.

Implemented-by: session_01RTkKf8Dn5F4mepiZZfWoxH
Reviewed-by: session_014EJ1ED8X4MMrT18BhVx4tx

VERDICT: PASS


Generated by Claude Code

@objectstack-fleet

Copy link
Copy Markdown
Contributor Author

Director pointer · 2026-09-29T01:24Z: the maintainer answered E1 of record 5874193973 — ruling B on #20367 stands with no host-shape carve-out (ruling 5881817230 on the card, batch #240 item 1, letter A). needs-user-decision leaves this PR in the same stroke; the landing is the seat's. Director seat, session_01AsCNgFBs8HCjwhyHQsFbx3 (objectstack#12708).

@objectstack-fleet
objectstack-fleet Bot marked this pull request as ready for review September 29, 2026 01:30
@objectstack-fleet
objectstack-fleet Bot added this pull request to the merge queue Sep 29, 2026
Merged via the queue into main with commit ba5927f Sep 29, 2026
45 checks passed
@objectstack-fleet
objectstack-fleet Bot deleted the claude/issue-20367-validate-runs-definestack-refusals branch September 29, 2026 01:53
veigajoao pushed a commit to veigajoao/objectstack that referenced this pull request Sep 29, 2026
… commits that decided them (stage 4) (objectstack-ai#20548)

Part of objectstack-ai#20234
Clause-②: no

## What changed

This is stage 4 of the staged sweep: the `data/` remainder. It covers
the six `packages/spec/src/data/` files stage 3 (PR objectstack-ai#20533, landed
`03b19d9cfd`) left out because an open PR held them, and nothing else.
They are `object.zod.ts`, `filter-logic-conformance.ts`,
`object.form.ts`, `data-engine.zod.ts`, `data-engine.test.ts` and
`hook.form.ts`. Later stages cover the other areas, so this PR says
`Part of`.

The census below measured all six. Three of them carry comment or
docblock sites that cite a tracker number answering 404.
`data-engine.zod.ts`, `data-engine.test.ts` and `hook.form.ts` carry
none, so they are not in the diff.

Every such site has been rewritten in ruling C+D's form C (comment
5749154545 on objectstack-ai#19123). That is **19 sites on 19 lines in 3 files,
covering 9 numbers**. Each rewritten line now cites the commit in
`origin/main` history that decided what the line describes, and it says
in its own words what that commit decided. Where a PR number was already
on the line (`PR objectstack-ai#13529`), it stays beside the commit as the link.

No ADR or ruling-record file in `docs/adr/` or `scripts/adr-anchors/`
records the decision behind any of the 9 numbers: a search for each
number, with and without `#`, finds nothing there. So every anchor is a
commit: **9 distinct shas**. Stage 3 had already read these commits and
recorded them in PR objectstack-ai#20533's body. They were not copied from there. Each
one was re-read against the current line it anchors: its own message or
diff names the number it replaces, and it made the change the line
describes. `object.zod.ts` and `filter-logic-conformance.ts` moved on
`main` after stage 3 read them (PRs objectstack-ai#20521 and objectstack-ai#20523). Each site was
therefore re-read at this base, `03b19d9cfd`.

Only comments changed. Every source file keeps its line count (20 lines
out, 20 in, over 3 files), so no line citation into these files moves.
One of the 20 lines held no dead citation:
`filter-logic-conformance.ts:249`, the first half of a sentence reflowed
onto `:250`. No code token moves (see the guard below).

**No tracker number is added.** Every tracker number on an added line
was already in the hunk it replaces. `PR objectstack-ai#13529` stands on three added
lines, and on the three removed lines of the same hunks. It is the link
beside commit `9dac1ae01`, which stage 3 recorded the same way.

No reference page under `content/docs/references/` moved: none of the
rewritten docblocks projects into one (`check:docs` at the head: `226
generated files in sync`). The PR adds one `patch` changeset for
`@objectstack/spec` (see Changeset below).

## Census: the six files, before and after

**Instrument.** This is the instrument of stages 1 to 3. It sends REST
`GET /repos/objectstack-ai/objectstack/issues/N` without following
redirects, for every distinct number cited in `packages/spec/src/data`.
The population is:
- the citation gate's own exported `CITATION_RE` and
`NON_CITATION_HEADS`, kept when the qualifier is none, `objectstack`,
`objectstack-ai/objectstack`, `framework`, `pre-` or `post-`;
- widened case-insensitively to `Pre-`, `POST-` and `Framework`, as in
stage 3;
- N of 100 or more, excluding `summon` heads.

Each site is classified by the TypeScript parser as a line comment, a
docblock, a block comment or a string.

Two cross-checks close the population. First, a raw `#N` count in each
of the six files equals the census rows plus the cross-repo rows in five
files. In the other two it is one higher, and the extra is a second
number after a slash inside a string (`objectstack-ai#5322/objectstack-ai#5134` in a `note`,
`objectstack-ai#6262/objectstack-ai#6433` in a test title). Both answer 200. Second, no spelled
citation (`issue N`, `PR N`, `card N`) occurs in any of the six.

**Controls.** The lit controls were `objectstack-ai#16862`, `objectstack-ai#16847` and `objectstack-ai#17698`. The
dead controls were `objectstack-ai#16714`, `objectstack-ai#16715` and `objectstack-ai#16697`. They were probed at
the start, after every 100 numbers and at the end: 24 of 24 lit (200)
and 24 of 24 dead (404) over 8 checkpoints in the base run, and 21 of 21
lit and 21 of 21 dead over 7 checkpoints in the head run.

| reading | tree | numbers probed | 200 | 404 | 301 or other | dead
sites, all of `data/` | dead sites, the six files | lines | files |
numbers |
|---|---|---|---|---|---|---|---|---|---|---|
| before | base `03b19d9cfd`, probed 2026-09-29T01:11:59Z to 01:15:49Z |
601 | 572 | 29 | 0 | **77** | 19 | 19 | 3 | 9 |
| after | head `53c9070dfd`, probed 2026-09-29T01:25:55Z to 01:29:35Z |
597 | 572 | 25 | 0 | **58** | 0 | 0 | 0 | 0 |

The head probe found no number newly dead since the base probe: the same
572 numbers answer 200. The base reading of 77 equals stage 3's after
reading at `96fd49caa2`.

**Per file.** Cited sites here are every in-repo citation the population
reads, live or dead.

| file | cited sites (base) | dead sites before | by class | dead sites
after |
|---|---|---|---|---|
| `object.zod.ts` | 120 | 15 | 8 docblock, 7 line comment | 0 |
| `filter-logic-conformance.ts` | 97 | 3 | 2 docblock, 1 line comment |
0 |
| `object.form.ts` | 31 | 1 | 1 line comment | 0 |
| `data-engine.zod.ts` | 48 | 0 | | 0 |
| `data-engine.test.ts` | 29 | 0 | | 0 |
| `hook.form.ts` | 0 | 0 | | 0 |

None of the 19 sites is a string, so this stage leaves no string token
behind.

## Per-number table

| number | sites / lines | anchor: what it decided |
|---|---|---|
| `objectstack-ai#8772` | 4 / 4, `object.zod.ts:2718`, `:2731`, `:2744`, `:2910` |
`75b7c240a`: Direction 2 of the 2026-08-16 maintainer ruling.
`ObjectSchema.create()` forces `required: true` on a `master_detail`
reference under `controlled_by_parent` and refuses an explicit
`required: false`. Raw parse stays tolerant, and runtime tolerance is
the ruling's other half. Its changeset records the measurement that only
the security gate closed that shape while the declaration surface
accepted it (`:2731`). ADR-0055 stays cited beside it. It is the same
anchor stage 3 gave `object.test.ts` |
| `objectstack-ai#10165` | 2 / 2, `object.zod.ts:818`, `:1036` | `801296050`:
`ttl.onlyWhen` with the canonical null predicate (maintainer ruling
2026-08-20, option A). One shared `onlyWhen` union, and both of
`retention.onlyWhen`'s conflicts mirrored. Its diff wrote both
`[objectstack-ai#10165]` blocks |
| `objectstack-ai#10347` | 3 / 3, `object.zod.ts:1006`, `:1042`, `:1049` |
`530c1df65`: the Archiver honours a declared `ttl`. It selects by the
ttl cutoff on `ttl.field` when `ttl` is declared, and by `created_at` /
`archive.after` otherwise (maintainer ruling 2026-08-20) |
| `objectstack-ai#10527` | 1 / 1, `object.zod.ts:1005` | `5649efbf9`: refuses a
diverging retention + ttl + archive triple at parse time. Its diff wrote
this very paragraph |
| `objectstack-ai#11195` | 1 / 1, `object.zod.ts:1791` | `b37231883`:
`UserActionsConfigSchema` adopts `group` / `hideFields` / `rowColor`
(the "last three" the line names) |
| `objectstack-ai#11408` | 1 / 1, `object.zod.ts:2189` | `f11fc61c5`: declares
`editMode` on the object document (maintainer ruling 2026-08-24, the
`objectstack-ai#10144` declare-or-rule-out family, which stays cited) |
| `objectstack-ai#13608` | 3 / 3, `object.zod.ts:2317`, `:2354`, `:2366` |
`fc9ba76a5`: `publicSharing.eligibility` is held at redemption, not only
at mint, fail-closed, with the undifferentiated `null` refusal. Its
changeset heads with objectstack-ai#13608. It is the same anchor stage 1 gave
`contracts/share-link-service.ts` |
| `objectstack-ai#13195` | 3 / 3, `filter-logic-conformance.ts:190`, `:250`, `:525` |
`9dac1ae01`, PR objectstack-ai#13529's squash commit, which stays as the link:
`$exists` means has-a-value on driver-memory's live mingo path, its
analytics face and driver-mongodb's `translateFilter` (the "last three
key-presence exits") |
| `objectstack-ai#12868` | 1 / 1, `object.form.ts:256` | `c459da6bc`: narrows the
per-option `default` key out of the form-view options vocabulary, which
offered a key nothing on that surface read. Commit `e808890958`, which
wrote this line, names objectstack-ai#12868 as the same offer-vs-door class |

The shas were checked at the base and again at `origin/main`
`288611e3e5`. Every one matches exactly one commit (`git rev-parse
--disambiguate`, count 1). Every one is an ancestor (`git merge-base
--is-ancestor`, exit 0 for 9 of 9). The control leg `e9584681a4` also
exits 0, and the repository is not shallow. For each commit, a grep of
its own message or diff finds the number it replaces. Seven of the nine
name it in the message. `fc9ba76a5` names it in its diff (20 lines,
including its changeset heading), and so does `c459da6bc` (8 lines,
including its changeset heading).

Wordings to check, each true of its commit:
- `object.zod.ts:2731` now reads 「closes that shape, and commit
75b7c24 records that the declaration and the enforcement disagree」.
The measurement was the card's. The commit's changeset records it: "only
the security gate closed that shape while the declaration surface
accepted it".
- `object.zod.ts:2189` reads 「Declared here by commit f11fc61's
maintainer ruling」, and `:2744` reads 「the other half of commit
75b7c24's ruling」. This is stage 3's wording for the same relation
(`object.test.ts`, 「the other half of commit 75b7c24's ruling」): the
commit that landed the ruling and quotes it.
- `object.zod.ts:1049` reads 「That is the whole of what [commit
530c1df] changed here」. Commit `52db1d1f2a` wrote the paragraph.
`530c1df65` is the change it describes.

## Mechanical guard: no code token moves

The check compares leaf tokens with comments stripped, base `03b19d9cfd`
against head `53c9070dfd`. It uses the TypeScript parser's leaf tokens
(TypeScript from the head's lockfile), so template literals are scanned
in context, and it excludes JSDoc nodes. It ran over all 3 touched `.ts`
files. It is the stage-3 instrument, unchanged.

- Real run: 13,624 base tokens (object.zod.ts 8,774, object.form.ts
3,226, filter-logic-conformance.ts 1,624), **0 files with a token
change** (exit 0).
- Comment-insertion control (`object.form.ts`): 0 files changed, as
expected (exit 0).
- Positive control (a declaration inserted into `object.zod.ts`): 1 file
reads DIFFER at token 1629 (exit 1).
- Positive control (one digit changed inside the `objectstack-ai#5322/objectstack-ai#5134` `note`
string in `filter-logic-conformance.ts`): 1 file reads DIFFER at token
889 (exit 1).

Line balance: `object.zod.ts` +15 / -15, `filter-logic-conformance.ts`
+4 / -4, `object.form.ts` +1 / -1. Line counts are equal at base and
head: 3,240, 621 and 751.

## Changeset

This change ships bytes, so a `patch` changeset for `@objectstack/spec`
is included. It says only that the provenance comments were re-anchored.
`Clause-②: no`: no export, key, value or type moves (the guard above).

Measured on the head's built package: `object.zod.ts` is
`src/**/*.zod.ts`, which `files[]` ships verbatim. The rewritten
comments also reach `dist`:
- `9dac1ae01` appears in `dist/data/index.d.ts` (the
`filter-logic-conformance.ts` docblock) and in 4 bundled `.js` files;
- `fc9ba76a5`, `f11fc61c5` and `b37231883` each appear in 22 bundled
`.js` files, and `c459da6bc` in 12;
- the positive control, the pre-existing `object.zod.ts` sentence
「Fail-CLOSED at both points」, appears in 11 bundled `.js` files.

## Gates (head `53c9070dfd`)

- **Citation judging pass, run as CI runs it:** `pnpm
check:issue-citations && node scripts/check-issue-citations.mjs` exits
0. The self-test passes 73 cases in 7 batteries. The live run judged 6
citations across 3 files: 3 resolve (`objectstack-ai#9138` twice, `objectstack-ai#11410`) and 3
resolve as a pull request (`objectstack-ai#13529`, the link).
- **Doc authoring:** `pnpm check:doc-authoring` exits 0.
- **Derived gates:** `node scripts/pm/dispatch-gates.mjs --commands
--repo objectstack-ai/objectstack` at the head derived 79 families, and
all 79 exit 0. `--ran` reports 79 run, 0 NOT MEASURED, 0 unrun, and
exits 0. A full `turbo run build` of `./packages/*` ran first, under the
shared verify lock: 71 of 71 tasks, VERDICT command-exit 0. So no gate
met an unbuilt prerequisite.
- `pnpm --filter @objectstack/spec run check:generated`: under the lock
against that build, `All 15 generated artifacts are up to date`, VERDICT
command-exit 0.
- **Tests and typecheck:**
- `pnpm --filter @objectstack/spec exec vitest run --maxWorkers=2
src/data` under the lock: Test Files 107 passed (107), Tests 3527
passed, 1 todo (3528), VERDICT command-exit 0. It covers every test in
`data/`, among them `object.test.ts`, which reads these schemas.
- The 13 spec suites outside `src/data` that read the touched files'
source text or pin their line numbers, under the lock: Test Files 13
passed (13), Tests 544 passed (544). They are stage 3's 12
(`scripts/{file-description,root-index,skill-map-guards,strictness-ledger}.test.ts`,
`src/api/api-entry-graph.pin.test.ts`,
`src/contracts/scoped-context.test.ts`,
`src/shared/{alias-integrity,evaluated-slot-population,retired-key-migrate-sentence}.test.ts`,
`src/system/constants/platform-object-names.test.ts`,
`src/type-alias-convention.pin.test.ts`, `src/ui/dashboard.test.ts`)
plus `src/shared/union-author-message-pins.test.ts`, which pins
`data/object.zod.ts:855`.
- `pnpm --filter @objectstack/spec typecheck` under the lock exits 0,
including `check:test-typecheck` (53 files, 251 errors, 138 pinned
signatures held).
- **Lint, as a proven narrowing at the head:** `eslint
--no-inline-config --format json` over the 3 touched `.ts` files gives 3
files, 0 errors and 0 warnings. All 3 are in eslint's own population
(`isPathIgnored` is false for each). `eslint.config.mjs` never enables
type-aware linting (no `parserOptions.project`, which its own line 328
states), so a comment edit here cannot move the verdict on any untouched
file. The repo-wide `pnpm lint` is CI's run.

## Acceptance notes

- **Base.** The branch forked from `03b19d9cfd`, stage 3's landing.
`origin/main` then moved two commits (`05077d4c26`, PR objectstack-ai#20532, and
`288611e3e5`, PR objectstack-ai#20536), and neither touches `data/`. `dispatch-gates`
flagged its derivation as stale because `scripts/regen-artifacts.mjs`
had moved, so `origin/main` was merged in (`53c9070dfd`, a clean merge
with no driver-deferred path) before the gates ran. The PR's delta
against `origin/main` is exactly its 4 files. `origin/main` has since
moved two more commits: `7e36a3cd7c` (PR objectstack-ai#20531) and `ba5927f714` (PR
objectstack-ai#20460). Neither touches `data/` or anything the gate derivation reads,
and a re-derivation prints the same 79 commands. A no-driver
`merge-tree` of the head onto `ba5927f714`, from a bare shared clone,
exits 0. So there is no second merge.
- **Open PRs, re-read at 2026-09-29T02:01Z:** 9 open PRs, and none
touches any of the six files. The `data/` files open PRs touch are
objectstack-ai#20458's `analytics*` files, objectstack-ai#20504's `driver/turso.*`, and objectstack-ai#20545's
`filter-number-comparand-declared-type.*`, which is disjoint. Since the
claim, PR objectstack-ai#20460 has landed (`ba5927f714`) without touching
`filter-subtree-provenance.ts`. That file's 3 dead sites are outside
this claim's fence, so they are left for a later stage.
- **The rung.** Two anchored changes also have ADR-0087 entries in
`packages/spec/src/migrations`: `cbp-master-detail-required-forced` for
objectstack-ai#8772, and `form-view-option-default-retired` for objectstack-ai#12868. The second
entry's own header names commit `c459da6bc`. This PR takes the commit
rung, as stages 1 to 3 did. The D3 id is the more durable in-repo
record, if the ruling's first rung is later read to include those
entries.
- **What stays in `data/` after this stage: 58 dead sites.**
- **12 comment sites in files other open work still holds.**
`analytics.zod.ts`, `analytics-strictness-batchd.test.ts` and
`analytics-date-range-two-bound-window.test.ts` hold 5 (objectstack-ai#20300, PR
objectstack-ai#20458). `driver/turso.zod.ts` and `driver/turso.test.ts` hold 4
(objectstack-ai#20437, PR objectstack-ai#20504). `filter-subtree-provenance.ts` holds 3. It was held
by objectstack-ai#20367 and is now free (see above).
- **3 comment sites stage 3 left on purpose.** They are the test-read
`[objectstack-ai#6259]` marker at `api-derivation.ts:163`, the test comment at
`api-derivation.test.ts:232` that names it, and `field.zod.ts:370`,
whose `objectstack-ai#6111` is objectui's number.
- **43 string sites**, left as tokens: 41 test strings (2 of them in the
held analytics and turso test files) and the 2 exported
`AGGREGATION_CASES` note strings in `aggregation-conformance.ts`
(`:398`, `:407`, objectstack-ai#11065), which objectstack-ai#20489's claim holds.
- **Outside `data/`,** the card's other remaining items are unchanged:
the migrations and ui areas, the `liveness/**` notes, the `why` strings,
the `PROVENANCE_WAIVERS` reason, and `rest-server.zod.ts`.
- **The citation gate's reach.** It defers `packages/**/*.test.ts`. No
test file is touched here, so all 3 touched files are in its judging
population.

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

---------

Co-authored-by: Claude <noreply@anthropic.com>
veigajoao pushed a commit to veigajoao/objectstack that referenced this pull request Sep 29, 2026
…low secret, at arm time and at registration (objectstack-ai#20529) (objectstack-ai#20551)

Fixes objectstack-ai#20529

Clause-②: no (narrowing)

## What this changes

ADR-0041 (status Accepted), `trigger-api` acceptance criteria: "Per-flow
inbound endpoint (...) with a per-flow secret; HMAC signature
verification (GitHub/Stripe style) and a constant-time compare." The
trigger armed a flow's inbound hook with no secret, logging only a
warning, and such a hook skipped signature verification. An `api`
trigger with no secret is now refused at arm time and at registration.

- **`@objectstack/trigger-api`**
- `ApiTrigger.start()` throws when the binding's `config.secret` is
absent, blank after trim, or not a string. The error names the flow and
`config.secret`. It throws before anything is stored in the hook map and
before any queue consumer is subscribed.
- The arm-time warning is removed, because the state it described no
longer exists.
- `ArmedHook.secret` is now non-optional. `handleRequest` verifies every
post, so the type system has no unsigned branch left to reach.
- The route ledger's note, which recorded an unsigned posture, now
records that every hook this door serves is signed.
- **`@objectstack/service-automation`**
- `registerFlow` gains `validateApiTriggerSecret`, placed after the
three existing hard-fail validations.
- It judges the binding that `deriveTriggerBinding` computes. That is
the body of `resolveTriggerBinding`, split out so it also runs over a
flow that is not registered yet. So the rule reads the very `config`
object that `activateFlowTrigger` would hand `start()`, including the
array-form precedence.
- It applies whatever the flow's `status`, like the other registration
refusals.
  - What callers see is unchanged in kind:
- The `/automation` create, update and clone doors answer `400
VALIDATION_FAILED` with `details.fields[0] = { field: '(body)', code:
'invalid_value' }`. This throw has the same plain-`Error` shape (the
flow-rejected message) that
`packages/runtime/src/domains/automation-register-error-class.test.ts`
case 4 already pins.
- Boot skips the flow with the existing `[Automation] failed to register
flow` warning.
- No new error code, and no `packages/spec` edit.

**Why the rule lives in two places.** `@objectstack/trigger-api` and
`@objectstack/service-automation` have no dependency on each other. At
boot the trigger registers on `kernel:ready`, after the flow pull, so
the engine cannot ask it at registration time. The engine's copy is the
publish-time refusal the author sees. The trigger's copy protects a host
that binds without the engine. Both read the same binding `config`, so
they cannot disagree about which flows need a secret. A single home that
also reaches `os validate` would be a `packages/spec` rule (see below).
That is the spec lane's call and is not made here.

**Breaking.** The changeset
`.changeset/20529-api-trigger-requires-secret.md` bumps both packages
`minor`. Its BREAKING paragraph gives the remedy: set a non-blank
`config.secret` on the start node. A flow that is only ever started
explicitly is `type: 'autolaunched'`, with no `triggerType: 'api'`, and
needs no secret. Its ADR-0087 disposition is `not-required
(no-migration-prescription)`, accepted by `check-adr-0087-registration`.

## Pin sweep

- **Pins of the old semantics, repo-wide.** A repo-wide `git grep` for
the warning text, "unsigned post" and "accepts unsigned" (CHANGELOGs
excluded) hit four places:
  - the test pin, flipped;
  - the trigger docblock, rewritten;
  - the route-ledger note, rewritten;
- `skills/objectstack-automation/SKILL.md:356`. That file is a Tier H
governed surface outside this card's file surface, so it is reported,
not edited.
- **The flipped pin carries weight.** "accepts unsigned posts when no
secret is configured" became three cases, for a missing, a blank and a
non-string secret. Each asserts all of the following:
  - `start()` throws, naming the flow and `config.secret`;
  - `listHooks()` is `[]`;
- no queue subscription happened, and no `armed:` log line was written;
- a post to that flow answers `404` with the full `RESOURCE_NOT_FOUND`
body;
  - nothing was published or delivered, and the flow never ran.
- **The guarded surface is kept verbatim.** The `401`
missing-or-bad-signature assertions are unchanged; only the test title
lost its "when the flow declares a secret" clause. Every other case that
armed with `{}` now arms with a secret and signs its body, and its
assertion is unchanged.
- **Fixture triage.** Three existing fixtures registered an `api` flow
with no secret:
- `engine.test.ts`: the execution-history fixture changes type to
`autolaunched`. It is only ever run through `engine.execute`, never an
inbound hook.
- `flow-trigger-kind-shared-resolver.test.ts` (the `type: 'api'` and
`triggerType: 'api'` rows) and `flow-activation-ledger.test.ts` (the
`api` entry path) now declare the secret. Their subject is kind
resolution and ledger refusal, so they must stay `api`.
- A repo-wide scan for `api`-kind flow definitions found no other
fixture that reaches a real engine. The only other hits are in
`packages/lint` and `packages/spec` tests, which never call
`registerFlow`.
- **New registration pins** (`api-trigger-secret-registration.test.ts`):
- Five refusal cases: a `type: 'api'` flow with no, blank or non-string
secret; a start-node `triggerType: 'api'` flow; and an `obsolete` flow.
Each asserts that the flow is absent afterwards (`getFlow` is null and
it is not in the runtime states) and that the `api` trigger was never
started.
- Two contrast cases: a signed flow registers, binds, and hands the
trigger its secret; an `autolaunched` flow needs no secret and runs.
- One re-registration case: a re-registration that drops the secret is
refused, and the stored signed version stays, neither stopped nor
re-started.

## Verification record (HEAD `b7325134`)

- **Build.** I built the dependency closure of both packages, then ran a
full `turbo run build --filter=!@objectstack/docs --concurrency=2`:
72/72 tasks, 0 cached. It was needed for the dist-reading gates.
- **Tests**
- `@objectstack/service-automation`: 150 files, 1845 tests, all passed.
  - `@objectstack/trigger-api`: 2 files, 26 tests, all passed.
  - Both suites ran on `b7325134`, after the last commit.
- **Typecheck**
- `@objectstack/trigger-api` passes. `tsc --listFiles` counts both of
its test files.
- `@objectstack/service-automation` passes, including
`check:test-typecheck`.
- **Ablation 1: arm-time refusal.** `scripts/ablation-replace.mjs`
replaced the throw with the old `logger.warn`.
  - Landed: anchor 1 to 0, blob `7e60a8ab` to `cc123fba`.
- Red: `Tests 3 failed | 8 passed (11)`. All three refusal cases failed
with `AssertionError: expected [Function] to throw an error`.
  - Restored: blob equals HEAD `7e60a8ab`, and `git diff HEAD` is empty.
  - Green before and after: 26/26.
- **Ablation 2: registration refusal.** The `validateApiTriggerSecret`
call was deleted.
  - Landed: anchor 1 to 0, blob `679f73dd` to `e66c2db2`.
- Red: `Tests 6 failed | 2 passed (8)`. All five refusal cases and the
re-registration case failed with `expected [Function] to throw an
error`. The two contrast cases stayed green.
  - Restored: blob equals HEAD `679f73dd`, and `git diff HEAD` is empty.
- Both suites import the subject from relative source, so no `dist/` was
involved.
- **Gates.** `dispatch-gates --commands`, derived over this diff's 9
paths, gave 62 commands. All 62 exited 0. Reconciling with `--ran` gave
"62 derived, 62 run, 0 NOT-MEASURED (a DERIVED zero)".
`check:dual-build-cjs-loads` first answered `PREREQUISITE NOT MET` (exit
3, not a measurement). After the full build it exited 0.
`check:dts-closure` and `check:lean-entry-closure` were re-run after the
full build too.
- **Lint, narrowed and proven.** eslint `--no-inline-config --format
json` over the 8 changed `.ts` files reported 8 files, 0 errors and 0
warnings. Three facts make that narrowing a measurement rather than a
skip:
- The population comes from the config: each file matches the
`packages/**/*.{ts,tsx,mts,cts}` blocks, and none reported "File
ignored".
  - The count comes from the JSON output.
- The config never enables type-aware linting (no
`parserOptions.project`; `eslint.config.mjs` states this at lines
326–328), so this diff cannot move any untouched file's verdict.
- **Declared to CI:** the repo-wide `pnpm lint`, the downstream consumer
suites of `@objectstack/service-automation`, and the full farm.

## The three measurements

1. **Run identity: yes, for the documented pattern.** The inbound
trigger supplies no user. A fired run takes the identity of the flow's
declared `runAs`, which defaults to `'user'`.
   - Under the default, data nodes are refused for want of a principal.
- A flow that declares `runAs: 'system'` runs its data nodes with system
elevation. The shipped worked example declares `runAs: 'system'`,
because it creates a record.
2. **Can a non-admin read `config.secret`: yes, by source reading.** I
reported it to the seat as an out-of-scope security finding. It is not
changed here.
3. **Shipped examples, templates, scaffolds: no.** The only shipped
`api` flow is the showcase's worked example, and it carries a secret, so
`examples/**` needs no edit. `packages/create-objectstack` declares no
`api` flow. One thing did turn up: the published automation skill
describes the secret as optional (reported below).

## `os validate` reach

**No.** `os validate` never builds an `AutomationEngine` or calls
`registerFlow`. `packages/cli/src/commands/validate.ts` runs the
`defineStack` parse, the `@objectstack/lint` authoring rules and the
capability preflight. I measured it on a throwaway stack (deleted
afterwards) that declares one `type: 'api'` flow with no secret and
`requires: ['automation', 'triggers', 'queue']`: `os validate` printed
`✓ Validation passed`, exit 0. objectstack-ai#20367 (PR objectstack-ai#20460) runs the stack's
`defineStack` refusals, and this refusal is not one of them. For `os
validate` to see it, the rule would need to be a `defineStack` refusal
next to the trigger-capability refusal (keyed on
`resolveFlowTriggerKind`), or a `validate-flow-trigger-readiness` rule
in `packages/lint`. Both belong to another lane.

## Acceptance notes

- **`hookId` fallback left as is.** A secret is now mandatory for every
armed hook, so the fallback token no longer has an unsigned form and
nothing concrete argues for changing it here.
- **Out-of-scope findings, reported to the seat and not filed from
here:**
- `skills/objectstack-automation/SKILL.md` (lines 52 and 356) calls the
secret "strongly recommended" and describes `type: 'api'` as "invoked
explicitly … **or** bound as an inbound webhook". The engine binds every
`type: 'api'` flow to the inbound trigger, so an author following it now
writes a flow the runtime refuses. The file is Tier H.
  - `os validate` passes a flow the engine refuses (measured above).
  - The measurement ② finding.
- **`content/docs/**`.** No line calls the inbound secret optional (0
hits), so there is no docs edit. Two observations, not filed:
- `content/docs/automation/flows.mdx` says an `api` flow "inherits its
organization from whoever triggered it". The inbound trigger passes no
caller session.
- `content/docs/automation/webhooks.mdx` §16 still lists inbound
webhooks as a non-goal with "no runtime".
  - Carrier for both: none.
- **Not done here:**
- No `scripts/adr-anchors/` entry for ADR-0041 was added. That path is
outside this card's file surface.
- Whether the Studio flow designer (objectui) can author an `api` flow's
`config.secret` is not measured. The sibling repo is not checked out in
this container.
- **Gate list size.** Derived over the paths this diff actually touches,
the list is 62 commands. The dispatch-time list over the expected paths
was 92, because it also included `examples/**` and `content/docs/**`
paths this diff never touched.

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

---------

Co-authored-by: Claude <noreply@anthropic.com>
veigajoao pushed a commit to veigajoao/objectstack that referenced this pull request Sep 29, 2026
… sites to the commits that decided them (stage 5) (objectstack-ai#20576)

Part of objectstack-ai#20234
Clause-②: no

## What changed

This is stage 5 of the staged sweep: the `ui/` area
(`packages/spec/src/ui/**`, 133 files), plus two sites freed since stage
4: `data/filter-subtree-provenance.ts` (PR objectstack-ai#20460 landed without
touching it) and `meta-spelling/manifest-collection-spelling.ts` (the
census hand-over, comment 5882628946 on objectstack-ai#20234).
`ui/view-grouping-query.ts` is excluded because objectstack-ai#20446's claim holds it;
it carries 4 citations and none of them is dead, so the exclusion
removes nothing. Later stages cover the other areas, so this PR says
`Part of`.

Every comment and docblock site in that population that cites a tracker
number answering 404 has been rewritten in ruling C+D's form C (comment
5749154545 on objectstack-ai#19123). That is **87 comment sites**: 84 re-anchored and
3 respelled.
- **84 re-anchored, over 25 numbers.** Each rewritten line now cites the
commit in `origin/main` history that decided what the line describes,
and says in its own words what that commit decided. One line
(`ui/dashboard.zod.ts:686`) quotes ADR-0087 itself; it keeps ADR-0087 as
its citation and paraphrases the amendment heading instead of quoting
its number.
- **3 respelled**, so each number of a sibling pair carries its own
qualifier: `ui/view.zod.ts:3634` now reads `objectui#6110 +
objectui#6111`, and `ui/component.zod.ts:3479` and `:4140` now read
`objectui#8221's PR objectui#8758`. Each second number answers 404 here,
and the sentence attributes it to objectui (objectui REST: `issues/6111`
200, `pulls/8758` 200, merged 2026-09-09).

Only comments changed, plus the one generated reference page they
project into and a patch changeset. Every source file keeps its line
count (90 lines out, 90 in, over 25 files). Three of the 90 lines held
no dead number; each is the other half of a rewritten sentence:
`ui/action.zod.ts:400`, `ui/action-param-carryover.test.ts:13` and
`ui/expression-bindable-text-keys.test.ts:119`. No code token moves (see
the guard below).

**No tracker number is added.** Every tracker number on an added line
was already on the lines it replaces, and no `PR #N` is added.

## Census: before and after

**Instrument.** This is the instrument of stages 1 to 4, rebuilt for
this stage. It sends REST `GET
/repos/objectstack-ai/objectstack/issues/N` without following redirects,
for every distinct number cited in the population. The population is:
- the citation gate's own exported `CITATION_RE` and
`NON_CITATION_HEADS` at the base, kept when the qualifier is none,
`objectstack`, `objectstack-ai/objectstack`, `framework`, `pre-` or
`post-`;
- matched case-insensitively (`Pre-`, `POST-`, `Framework`);
- N of 100 or more, excluding `summon` heads.

A qualifier covers only the number it is joined to. Each site is
classified by the TypeScript parser as a line comment, a docblock, a
block comment or a string.

**Controls.** The lit controls were `objectstack-ai#16862`, `objectstack-ai#16847` and `objectstack-ai#17698`. The
dead controls were `objectstack-ai#16714`, `objectstack-ai#16715` and `objectstack-ai#16697`. They were probed at
the start, after every 100 numbers and at the end: 18 of 18 lit (200)
and 18 of 18 dead (404) over 6 checkpoints in the base run, and 15 of 15
and 15 of 15 over 5 checkpoints in the head run.

| reading | tree | numbers probed | 200 | 404 | 301 or other | dead
sites | lines | files | of which comments | of which strings |
|---|---|---|---|---|---|---|---|---|---|---|
| before | base `487a7846df`, probed 2026-09-29T02:57:01Z to 02:59:36Z |
400 | 372 | 28 | 0 | **111** | 110 | 26 | 90 | 21 |
| after | head `83e39641d0`, probed 2026-09-29T03:15:00Z to 03:18:06Z |
383 | 372 | 11 | 0 | **24** | 23 | 8 | 3 | 21 |

The head probe found no number newly dead since the base probe: the same
372 numbers answer 200. The head was probed at `83e39641d0`; every
census file is byte-identical at the final head.

**Cross-check under the grammar that landed during this stage.** PR
objectstack-ai#20554 (`199002b3e4`) landed the closed qualifier set while this stage
ran, and it reads `objectui PR objectstack-ai#8758` as objectui's number. Re-run with
that gate's own `extractCitations` and `namesThisRepository`, the same
population reads **108** dead sites before and **21** after, all 21 test
strings. The difference is exactly the three `objectui PR objectstack-ai#8758` prose
sites below, which that PR's own header measured as "census deaths here
that are not deaths at all".

**Per file.** Cited sites are every in-repo citation the population
reads, live or dead.

| file | cited sites (base) | dead before | by class | dead after |
|---|---|---|---|---|
| `data/filter-subtree-provenance.ts` | 9 | 3 | 3 docblock | 0 |
| `meta-spelling/manifest-collection-spelling.ts` | 8 | 2 | 2 line
comment | 0 |
| `ui/action-param-carryover.test.ts` | 5 | 3 | 2 line comment, 1 string
| 1 |
| `ui/action.test.ts` | 39 | 3 | 3 line comment | 0 |
| `ui/action.zod.ts` | 90 | 7 | 5 docblock, 2 line comment | 0 |
| `ui/bulk-action.test.ts` | 9 | 4 | 2 line comment, 2 string | 2 |
| `ui/bulk-action.zod.ts` | 9 | 3 | 2 docblock, 1 line comment | 0 |
| `ui/component-element-navigation-17987.test.ts` | 8 | 5 | 1 docblock,
4 string | 4 |
| `ui/component-type-vocabulary.test.ts` | 8 | 2 | 2 docblock | 0 |
| `ui/component-type-vocabulary.ts` | 2 | 1 | 1 docblock | 0 |
| `ui/component.test.ts` | 190 | 22 | 11 line comment, 11 string | 11 |
| `ui/component.zod.ts` | 265 | 20 | 16 docblock, 4 line comment | 0 |
| `ui/dashboard.zod.ts` | 52 | 1 | 1 docblock | 0 |
| `ui/expression-bindable-text-keys.test.ts` | 3 | 2 | 2 line comment |
0 |
| `ui/expression-bindable-text-keys.zod.ts` | 4 | 2 | 2 docblock | 0 |
| `ui/form-select-option.test.ts` | 3 | 1 | 1 docblock | 0 |
| `ui/index.ts` | 15 | 2 | 2 line comment | 0 |
| `ui/interaction-config-retirement.test.ts` | 19 | 1 | 1 docblock | 0 |
| `ui/react-blocks.test.ts` | 9 | 2 | 1 docblock, 1 string | 1 |
| `ui/react-blocks.ts` | 17 | 4 | 2 docblock, 2 line comment | 0 |
| `ui/view-form-features-root.test.ts` | 4 | 1 | 1 line comment | 0 |
| `ui/view-metadata-schema.test.ts` | 31 | 3 | 2 line comment, 1 string
| 1 |
| `ui/view-submit-redirect-url.test.ts` | 8 | 1 | 1 line comment | 0 |
| `ui/view.test.ts` | 136 | 2 | 1 docblock, 1 string | 2 |
| `ui/view.zod.ts` | 331 | 13 | 10 docblock, 3 line comment | 2 |
| `ui/widget-i18n-retirement.test.ts` | 17 | 1 | 1 line comment | 0 |

`ui/` alone went from 106 dead sites in 24 files to 24. The other 107
`ui/` files carry no dead site.

## Per-number table

Anchors are 9-hex commit abbreviations. "Wrote" means the commit's own
diff added the line being rewritten.

| number | comment sites / files | anchor: what it decided |
|---|---|---|
| `objectstack-ai#5970` | 4 / 2, `action.zod.ts:819`, `action.test.ts:268`, `:304`,
`:354` | `97e7e3caa`: `ActionSchema.visible` / `disabled` speak one
condition shape; `visible` gains its boolean arm. Stage 1's anchor for
the same number |
| `objectstack-ai#6276` | 7 / 1, `component.zod.ts:34`, `:165`, `:568`, `:2455`,
`:2546`, `:2554`, `component.test.ts:2126` | `78f0be872`: declares
`element:record_picker`'s flat `sort` / `limit` on the objectstack-ai#5611 rule
(maintainer ruling 2026-08-08, direction A). It wrote `:34`, `:2455` and
the "enumerate by the renderer's read pattern" lesson |
| `objectstack-ai#8794`, `objectstack-ai#8836` | 3 / 1, `filter-subtree-provenance.ts:130`, `:131`,
`:156` | `1850ebbb0`: corrects the reuse-safety claim from the survey
and pins the invariant. It wrote `:131` itself. Stages 1 and 3 gave both
numbers this anchor |
| `objectstack-ai#9933` | 7 / 2, `view.zod.ts:2517`, `:5060`, `:5086`, `:5118`,
`:5513`, `view-metadata-schema.test.ts:398`, `:410` | `d5552ca13`:
admits `columnState` as an explicitly runtime-only view-overlay key,
rejected by name at every authoring door. It wrote "explicitly out of
objectstack-ai#9933's scope" |
| `objectstack-ai#9972` | 3 / 2 (+1 unread), `component.zod.ts:2213`,
`component.test.ts:382`, `:3565`; also `:3612`'s `objectstack-ai#9881/objectstack-ai#9972`, a
slash-joined spelling the grammar does not read | `60e0f900a`: records
the live read point of `page:tabs` `items[].icon` and its accept-pin. It
wrote the `:382` header |
| `objectstack-ai#10194` | 1 / 1, `manifest-collection-spelling.ts:71` (`pre-objectstack-ai#10194`)
| `2306a765c`: `/meta/theme` and `/meta/analytics_cube` stop storing any
JSON as success and validate at the write door. The line now says "the
store-anything branch from before commit 2306a76". Stage 1's anchor |
| `objectstack-ai#10274` | 6 / 2, `component.zod.ts:2304`, `component.test.ts:308`,
`:405`, `:3591`, `:3603`, `:3612` | `d1ba685ec`: re-measures the four
pin citations and gates the class. Its gate header records that the
re-measure found two anchors wrong since they were written, which is why
a refresh re-reads. Stage 3's anchor |
| `objectstack-ai#10485` | 4 / 4, `index.ts:51`,
`interaction-config-retirement.test.ts:116`,
`widget-i18n-retirement.test.ts:112`,
`manifest-collection-spelling.ts:67` | `35ad101bc`: retires the `themes`
carrier and `ThemeSchema` whole (ruled B, 2026-08-21; ADR-0049 stays
cited). Stage 1's anchor |
| `objectstack-ai#11284` | 5 / 2, `react-blocks.ts:39`, `:94`, `:109`, `:295`,
`react-blocks.test.ts:139` | `5383fa670`: the react tier converges on
the metadata-tier vocabulary, deprecate-first. Its changeset heads
"(objectstack-ai#11284, maintainer ruling 2026-08-23)" |
| `objectstack-ai#11350` | 1 / 1, `index.ts:105` | `ece4dad31`: records the maintainer
ruling of 2026-08-23 that a type in an entry's public declarations must
be nameable from that entry. Same wording as stage 1's
`kernel/index.ts:53` |
| `objectstack-ai#11507` | 2 / 2, `component.zod.ts:1465`, `component.test.ts:2726` |
`88b9d749a`: declares `sys_activity.type` an open, author-extensible
vocabulary (maintainer ruling 2026-08-24, direction 4). Stage 3's anchor
|
| `objectstack-ai#11658` | 2 / 2, `component.zod.ts:1464`, `component.test.ts:2725` |
`1a6a19c31`: opens `RecordActivityProps.types` to author-contributed
kinds, executing that ruling. Stage 3's anchor |
| `objectstack-ai#11703` | 3 / 2, `action.zod.ts:399` to `:400`, `:460`,
`action-param-carryover.test.ts:12` to `:13` | `5cb62d88b`:
`clone_permission_set` carries all five copied facets; its params list
had silently dropped three. The lines now name "the silent-drop shape
commit 5cb62d8 fixed" |
| `objectstack-ai#11753` | 5 / 2, `action.zod.ts:66`, `:390`, `:398`, `:409`,
`action-param-carryover.test.ts:1` | `0e4e51b0a`:
`ActionParamSchema.carryOver`, the spec half of the 2026-08-25
maintainer ruling (recommendation A). It wrote every one of these lines,
and its changeset records the `visible: false` measurement `:398` names
|
| `objectstack-ai#12194` | 1 / 1, `view.zod.ts:4838` | `311433f6b`: declares the
metadata item-name grammar (`QUALIFIED_ITEM_NAME_PATTERN` among it) and
refuses it at the publish door. Stage 2's anchor |
| `objectstack-ai#12868` | 5 / 2, `view.zod.ts:2938`, `:2966`, `:3179`, `:7087`,
`form-select-option.test.ts:4` | `c459da6bc`: narrows the per-option
`default` key out of the form-view options vocabulary. Its changeset
records the ruled census `:2966` cites ("measured ZERO occurrences").
Stages 3 and 4's anchor |
| `objectstack-ai#12950` | 3 / 2, `component-type-vocabulary.ts:4`,
`component-type-vocabulary.test.ts:4`, `:101` | `225e7690f`: created
`component-type-vocabulary.ts`; its message records the readiness read
`:101` pins (`global:search` and `global:notifications` stay declared) |
| `objectstack-ai#13156` | 2 / 2, `view-form-features-root.test.ts:70`,
`view-submit-redirect-url.test.ts:110` | `fd289be45`: strips tracker ids
from function-declaration-built refusal prose. It wrote both lines.
Stage 3's wording ("commit fd289be's strip") |
| `objectstack-ai#13670` | 1 / 1, `expression-bindable-text-keys.zod.ts:72` |
`8c6a7fc0b`: records `text.value` as deliberately omitted; its message
states the ruling that `text`'s evaluation channel is `content` alone |
| `objectstack-ai#13672` | 3 / 2, `expression-bindable-text-keys.zod.ts:89`,
`.test.ts:65`, `:118` | `e854a531a`: narrows the `button` row to the
spelling its key reaches, and records `action:button` and `ui:button` as
deliberately out |
| `objectstack-ai#16626` | 1 / 1, `component.test.ts:2336` | `30b099078`: the objectui
pin bump to `53ded82bf7a4` that ships objectui#7754's array-analytics
lowering, the door the family waited on. The association is PR objectstack-ai#16788's
body (it names objectstack-ai#16626 as the card it lands), and `30b099078` is that
PR's merge commit; neither its message nor its diff names objectstack-ai#16626 (the
stage-3 objectstack-ai#11065 precedent) |
| `objectstack-ai#17987` | 9 / 2, `component.zod.ts:10`, `:4014`, `:4062`, `:4153`,
`:4193`, `:5068`, `:5226`, `:5457`,
`component-element-navigation-17987.test.ts:4` | `e233db9db`: declares
element-level `navigation` on `object-kanban` / `object-calendar` and
gives `object-timeline` its `ComponentPropsMap` row, executing the
objectui#8652 ruling (verbatim `B`) |
| `objectstack-ai#18003` | 1 / 1, `dashboard.zod.ts:686` | **ADR-0087**, the rung
above a commit. The line quoted the ADR's own amendment heading, number
included. It now reads "(ADR-0087, its 2026-09-13 amendment, 「the level
half」)": the ADR stays the citation and the fragment it quotes is
verbatim |
| `objectstack-ai#18177` | 5 / 2, `bulk-action.zod.ts:51`, `:169`, `:262`,
`bulk-action.test.ts:61`, `:307` | `adabccf5f`: `BulkActionParamSchema`
is strict and declares `dependsOn`, executing decision batch objectstack-ai#146 item
4, letter A |
| `objectstack-ai#6111` | 1 / 1, `view.zod.ts:3634` | respelled `objectui#6111` (not
re-anchored): it is objectui's number |
| `objectstack-ai#8758` | 2 / 1, `component.zod.ts:3479`, `:4140` | respelled `PR
objectui#8758` (not re-anchored): objectui's PR objectstack-ai#8758, merged 2026-09-09
|

No ADR or ruling-record file in `docs/adr/` or `scripts/adr-anchors/`
records the decision behind any of the 25 re-anchored numbers except
objectstack-ai#18003. ADR-0126 mentions objectstack-ai#11703 and objectstack-ai#11753 only as references
("permission-set precedent"), not as the record of either decision.

**Anchor checks.** Every sha on an added line is one of 23, and none is
on a removed line. At the base `487a7846df`:
- each matches exactly one object (`git rev-parse --disambiguate`, count
1, 23 of 23);
- each is an ancestor (`git merge-base --is-ancestor`, exit 0, 23 of
23); the control leg `e9584681a4` also exits 0, and the repository is
not shallow;
- for 22 of the 23, a grep of the commit's own message or diff finds the
number it replaces (the message for 16; the diff for `0e4e51b0a`,
`5383fa670`, `c459da6bc`, `225e7690f`, `e854a531a`, and for objectstack-ai#8836 in
`1850ebbb0`). `30b099078` is the exception explained in the table.
- Each commit was read for the rule its line states, not only for the
number. In most cases the commit wrote the very line it now anchors.

Wordings to check, each true of its commit:
- `filter-subtree-provenance.ts:130` and `:156` read 「survey commit
1850ebb records」: the survey was the card's, and the commit's message
records its measurement. It is stage 3's wording for the same relation
(「from the survey it records」).
- `component.zod.ts:1465` and `component.test.ts:2726` read 「maintainer
ruling commit 88b9d74 declared」: that commit landed the ruling
(direction 4) as the `sys_activity.type` declaration.
- `manifest-collection-spelling.ts:71` reads 「the store-anything branch
from before commit 2306a76」: before that commit, `PUT
/meta/theme/:name` stored any JSON as success.

## Mechanical guard: no code token moves

The check compares leaf tokens with comments stripped, base `487a7846df`
against the head. It uses the TypeScript parser's leaf tokens from the
head's lockfile, so template literals are scanned in context, and it
excludes JSDoc nodes. It ran over all 25 touched `.ts` files, and every
control mutates the head text in memory only.

- Real run: 101,836 base tokens, **0 files with a token change** (exit
0).
- Comment-insertion control (`ui/index.ts`): 0 files changed (exit 0).
- Positive control (a declaration inserted into `ui/view.zod.ts`): 1
file reads DIFFER at token 19222 (exit 1).
- Positive control (one digit changed in a `component.test.ts` test
title): 1 file reads DIFFER at token 14972 (exit 1).

Line balance holds in every file, 90 out and 90 in over the 25, and
every line count is equal at base and head. Tracker numbers:
added-not-removed is empty in every file. The net-removed numbers are
the 25 in the table, 85 sites: the census's 84 comment sites, plus the
slash-joined `objectstack-ai#9972` at `component.test.ts:3612`.

## Generated page

`check:generated` proved one artifact stale:
`content/docs/references/ui/expression-bindable-text-keys.mdx`, the
projection of `expression-bindable-text-keys.zod.ts`'s module docblock.
`check:generated --fix` regenerated only that page, and a re-run read
`All 15 generated artifacts are up to date`. Its two changed lines are
the `:72` and `:89` substitutions verbatim. No other docblock here
projects into a reference page, and nothing under `skills/**` moved.

## Changeset

This change ships bytes, so a `patch` changeset for `@objectstack/spec`
is included. It says only that the provenance comments were re-anchored.
`Clause-②: no`: no export, key, value or type moves (the guard above).

Measured on the head's built package: 6 touched sources are
`src/**/*.zod.ts`, which `files[]` ships verbatim. The rewritten
comments also reach `dist`:
- `c459da6bc` appears in 32 bundled `.js` files and 2 `.d.ts`;
- `adabccf5f` in 24 `.js` and 2 `.d.ts`; `d5552ca13` and `0e4e51b0a` in
24 `.js` each; `e233db9db` and `78f0be872` in 2 `.js` and 2 `.d.ts`
each;
- the positive control, the pre-existing sentence 「the object-field face
enforces」, appears in 32 files.

## Gates (head `1b885d3c27`)

- **Citation judging pass, run as CI runs it:** `pnpm
check:issue-citations && node scripts/check-issue-citations.mjs`, both
under the grammar PR objectstack-ai#20554 landed, exit 0. The self-test passes 114
cases in 8 batteries. The live, diff-scoped run judged 13 citations
across 11 files: 3 resolve and 10 are declared cross-repo references. It
reads "every citation this change adds resolves".
- **Doc authoring:** `pnpm check:doc-authoring` exits 0. Its 16,759
customer-facing strings across 1,174 spec sources carry no internal
issue id, and the sibling-package prose-id baseline holds with no
growth.
- **Derived gates:** `node scripts/pm/dispatch-gates.mjs --repo
objectstack-ai/objectstack --commands` at this head derived 112
families, and all 112 exit 0. `--ran` reports 112 run, 0 NOT MEASURED, 0
unrun. A full `turbo run build` of `./packages/*` at this head ran
first, under the shared verify lock: 71 of 71 tasks, VERDICT
command-exit 0. So no gate met an unbuilt prerequisite.
- **Five roster gates the derivation flags for this diff** (their
rosters sit in `.changeset/` or `packages/`, so their silence proves
nothing): `node scripts/check-changeset-fixed.mjs`, `pnpm --filter
@objectstack/spec run check:spec-changes`, `pnpm check:authz-resolver`,
`pnpm check:error-code-casing` and `pnpm check:filter-alias-parity`. All
exit 0.
- `pnpm --filter @objectstack/spec run check:generated` (derived) reads
`All 15 generated artifacts are up to date`, and `check:docs` reads `226
generated files in sync`.
- **Tests and typecheck, under the lock:**
- `pnpm --filter @objectstack/spec exec vitest run --maxWorkers=2 src/ui
src/meta-spelling`: Test Files 98 passed (98), Tests 3452 passed (3452),
VERDICT command-exit 0 (the chain held the lock 142s on a shared box).
- The 16 spec suites outside `src/ui` that read the touched files'
source text or pin their lines: Test Files 16 passed (16), Tests 489
passed (489). They are
`scripts/{export-origins,file-description,root-index,schema-closure,skill-map-guards,strictness-ledger}.test.ts`,
`src/ai/tool-confirmation-prescription-tense.pin.test.ts`,
`src/api/api-entry-graph.pin.test.ts`,
`src/contracts/scoped-context.test.ts`,
`src/data/filter-subtree-provenance.test.ts`,
`src/shared/{alias-integrity,evaluated-slot-population,retired-key-migrate-sentence,union-author-message-pins}.test.ts`,
`src/system/constants/platform-object-names.test.ts` and
`src/type-alias-convention.pin.test.ts`. Three more suites matched the
reader scan and are not run here:
`scripts/build-schemas-check-mode.test.ts` only imports `ViewItemSchema`
(code the guard proves unchanged) and rebuilds schemas in a temp tree;
`scripts/def-key-collisions.test.ts` names `ui/view.zod.ts` only in a
comment; `scripts/published-projection-choke-point.test.ts` matched on
`build-react-blocks-contract.ts`, not a touched file. They are left to
CI.
- `pnpm --filter @objectstack/spec typecheck`: exit 0, including
`check:test-typecheck` (53 files, 251 errors, 138 pinned signatures
held). The same three runs also passed, with the same counts, on the
pre-merge tree.
- **Lint, as a proven narrowing:** `eslint --no-inline-config --format
json` over the 25 touched `.ts` files gives 25 files, 0 errors and 0
warnings. All 25 are in eslint's own population (`isPathIgnored` is
false for each, read through eslint's API). `eslint.config.mjs` never
enables type-aware linting (no `parserOptions.project`, which its own
lines 327 to 328 state), so a comment edit here cannot move the verdict
on any untouched file. The repo-wide `pnpm lint` is CI's run.
- **Merge probe:** a `merge-tree` of the head onto `origin/main`
`f572a7eb3c`, from a bare shared clone with no merge driver registered,
exits 0 (2026-09-29T04:15Z).

## Acceptance notes

- **Base and merge.** The branch forked from `487a7846df`, one commit
past the claim's stamp `6154165484` (PR objectstack-ai#20551, outside the surface).
`origin/main` then moved three commits, and `199002b3e4` (PR objectstack-ai#20554)
changed `scripts/check-issue-citations.mjs`, so the gate derivation read
STALE TREE. `origin/main` `dee9b26f6c` was merged in (`1b885d3c27`): a
clean merge with no driver-routed path and no lockfile change, touching
none of this diff's files. The PR's delta against `dee9b26f6c` is
exactly its 27 files. `origin/main` has since moved one more commit,
`1c761c0d71` (PR objectstack-ai#20565, tests in two other packages), which touches
none of them.
- **One site beyond the hand-over's list.** The hand-over named
`manifest-collection-spelling.ts:71` (`pre-objectstack-ai#10194`). The same comment's
first line, `:67`, cites `objectstack-ai#10485`, which also answers 404, and it is
rewritten too. The claim names this file; the fix is the same defect
class, mechanical in the form stages 1 to 4 fixed, in a file no other
claim holds, under the same gates. Reverting it would be one line.
- **Open PRs, re-read at 2026-09-29T04:22Z:** 8 open PRs, and none
touches any file in this diff. The one that touches `ui/` is objectstack-ai#20570
(objectstack-ai#20446's), on the excluded `view-grouping-query.ts`. The in-flight
`Claim:` comments on the 12 `pm:dispatched` cards were read too: only
objectstack-ai#20446's names a `ui/` file (`view-grouping-query.ts`, excluded above).
- **Hypothesis 2, measured.** In `ui/`, 14 sibling-qualified pairs leave
the second number bare: 13 on one line (`+`, `and`, `,`, `/` or `'s PR`
between them) and `component.zod.ts:3478` to `:3479`, split across a
line break. In 3 of them the second number answers 404 here and the
sentence attributes it to objectui (`objectstack-ai#6111` once, `objectstack-ai#8758` twice); they
are respelled above. In the other 11 the second number answers 200 here,
so it is judged as this repository's and left: `action.zod.ts:1603`,
`component.test.ts:2052`, `:2199`, `:2315`, `component.zod.ts:3276`,
`:3793`, `:4812`, `expression-bindable-text-keys.zod.ts:33`,
`page.test.ts:696`, `react-blocks.ts:256` and `widget.zod.ts:34`. The PR
objectstack-ai#20554 header measured `,` and `and` pairs as naming this repository's
number and `/` pairs as mostly, but not always, the qualifier's. Whether
any `/` pair here names objectui's number is not measured; a 200 here
cannot tell.
- **What stays in this population: 24 dead sites** (21 under the landed
grammar).
- **21 test strings**, left as tokens (vitest `it` / `describe` titles
in 7 test files): `objectstack-ai#6276` ×6, `objectstack-ai#11658` ×3 and `objectstack-ai#11507` ×1 in
`component.test.ts`; `objectstack-ai#9972` in `component.test.ts:411`; `objectstack-ai#17987` ×4 in
`component-element-navigation-17987.test.ts`; `objectstack-ai#18177` ×2 in
`bulk-action.test.ts`; `objectstack-ai#9933` in `view-metadata-schema.test.ts:406`;
`objectstack-ai#11284` in `react-blocks.test.ts:147`; `objectstack-ai#11753` in
`action-param-carryover.test.ts:17`; `objectstack#11195` in
`view.test.ts:3396`. None is a Zod `.describe()` text, an exported
string or a migration-entry field, so no form D site arises here.
- **3 comments that name objectui's live PR objectstack-ai#8758 in prose** (`objectui
PR objectstack-ai#8758`): `view.zod.ts:2354`, `:2579` and `view.test.ts:426`. They are
not pairs, and the landed grammar reads them as objectui's.
- **Left for later stages of objectstack-ai#20234** (the stage-4 landing comment
5882686893's list, unchanged): the migrations area, the `liveness/**`
notes, the `why` strings and the `PROVENANCE_WAIVERS` reason,
`rest-server.zod.ts`, the held `analytics*` and `driver/turso.*` files,
the 2 `AGGREGATION_CASES` note strings, and `data/`'s test strings and
deliberate markers.
- **The same rot outside `packages/spec/src`** is objectstack-ai#20556's, not this
card's. Examples met here: ADR-0087's own amendment heading
(`docs/adr/0087-metadata-protocol-upgrade-contract.md:390`, `:397`)
cites the dead `objectstack-ai#18003`, and ADR-0126 cites `objectstack-ai#11703` and `objectstack-ai#11753`; both
are governed. `packages/spec/scripts/strictness-ledger.test.ts:375`,
`:380` cite `objectstack-ai#9933`, and `check-objectui-pin-citations.ts` cites
`objectstack-ai#10274` and `objectstack-ai#9972`.
- **Unchanged wording.** `react-blocks.test.ts:140` says the 2026-08-23
ruling was "recorded on-card". The card is gone, and the changeset of
`5383fa670` (now cited on `:139`) records the ruling. The line holds no
number, so it is left.
- **The citation gate's reach.** It defers `packages/**/*.test.ts`. The
11 touched non-test files are in its judging population.

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

---------

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

documentation Improvements or additions to documentation size/xl tests tooling

Projects

None yet

2 participants