Skip to content

fix(cli)!: os generate binds declared metadata (--object, --flow), never the item name, and every scaffold passes validate, build and lint - #21369

Merged
objectstack-fleet[bot] merged 10 commits into
mainfrom
claude/issue-21325-generate-scaffolds-pass-gates
Oct 2, 2026
Merged

objectstack-fleet[bot] merged 10 commits into
mainfrom
claude/issue-21325-generate-scaffolds-pass-gates

Conversation

@objectstack-fleet

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

Copy link
Copy Markdown
Contributor

Fixes #21325
Clause-②: yes (narrowing)

The Clause-② arm was measured on the real diff, and it is (narrowing), not the (widening) the claim expected. --object and --flow widen the command. The diff also narrows it: an invocation that used to write a scaffold bound by name is now refused. os g flow customer in a project that declares several objects is one example, the cli.mdx example os g action customer another. The changeset carries the BREAKING banner and its ADR-0087 disposition (not-required (no-migration-prescription)). check-adr-0087-registration and check-changeset-no-major both pass on it.

What changed

Triage's direction (5945870485): flow, action and app take their target object from an argument or from the stack's existing objects, never from the new item's name. With nothing named and nothing to choose, the command refuses. view writes the shape os lint requires. Every generator kind, on a fresh scaffold, passes os validate, os build and os lint with zero findings.

packages/cli/src/commands/generate.ts

  • Bindings are resolved, not derived. Each generator declares binds (ScaffoldBinds). view is named after its object. flow, action and app take the object from --object, or the stack's only object. action takes its flow from --flow, or the stack's only flow. resolveScaffoldBindings resolves every reference against the loaded stack before anything is rendered. It runs below the namespace gates and above the render, the parse check and --dry-run. It is a pure function: it returns a verdict, and the command prints the verdict and exits. generate receives the resolved ScaffoldBindings, and a binding scaffold rendered without them throws. Nothing falls back to the item's name.
  • Refusals, each listing what the stack declares and naming the next command, with nothing written: no object or several and no --object; an --object or --flow that names nothing declared; no flow or several and no --flow; a view whose name is not a declared object; a binding scaffold outside a project (no config, so no stack to check against); --object or --flow on a type that takes neither (unusedBindingFlagRefusal), including the types, client and migration routes, which now sit in one SUB_COMMANDS table.
  • Scaffold content. Each change below removes a finding that was measured.
    • object: no description field (field-no-consumers on every generated object once the stack had any consumer root).
    • view: no container name or label (two liveness-dead-property warnings). It writes list.label (os lint required/label error), and its columns are every field the bound object declares (a fixed name column was a list-view-field-unknown error on any object without one). It is sorted by the title field.
    • flow: status: 'active' (flow-draft-status-ambiguous; draft already fires, so the runtime behaviour is unchanged).
    • action: locations: ['record_header'] (action-no-placement).
    • app: the nav entry is labelled with the bound object's plural label.
  • --object and --flow flags. Their help text names the types that read them, read off the roster.

packages/cli/src/utils/project-namespace.ts: a loaded result also carries the evaluated config, so the bindings are read off the same load that supplies the namespace.

packages/cli/src/utils/scaffold-wiring.ts: registeredItemName returns the key an item is registered under. For an aggregated views container that key is deriveViewContainerObject, the boot registrar's own derivation. stackCarries (the reach report) reads it, so a container with no name is still found.

Argument shape, and why

  • --object OBJECT and --flow FLOW are long flags with a value, the way every other option on os generate is spelled (--dir, --output, --format). The object is accepted as declared (tasks_app_task), or without the namespace prefix (task). In the second case it is looked up through the same prefix rule os g object applies, never re-spelled.
  • No flag means the stack's only object (or flow), said out loud in the header: Object: tasks_app_note (the only object this stack declares; --object binds another). This is the non-interactive reading of the direction's "or from the stack's existing objects". With several declared, the command lists them and asks for the flag instead of choosing. No os generate code path prompts, and this change adds no prompt (os migrate prompts only for a yes/no confirmation).
  • A view is named after its object (os g view task), unchanged. A views container is registered under the object it binds, so the name and the object are one fact. --object on view is refused with that explanation.
  • --flow goes beyond the single object argument the claim anticipated. An action's flow was derived from its name in exactly the same way (complete_task_flow). defineStack refused it whenever flows existed. With no flows declared, the action loaded with a dangling flow reference: measured on 97239c3c8a, os g action approve then exits 0 and os validate exits 0, target: 'approve_flow'. The same rule (argument, or the stack's only one) is applied to the second reference.

The view container's name and label: decided from the code that reads them

The scaffold's comment said the server refuses a container whose name disagrees. os validate said both keys are dead. Read from their readers, both statements are true and they do not conflict:

  • name: the boot registrar files a container under deriveViewContainerObject (packages/metadata/src/view-container.ts): its own object, else list.data.object / form.data.object, and only then name. With object set, name is never the key. Its one reader is viewContainerNameRefusal (packages/objectql/src/view-container-name-refusal.ts). That function refuses a name that is set AND differs from the key, and passes a container that has no name. So name can only restate the key or contradict it, which is the liveness ledger's dead (packages/spec/liveness/view.json). The refusal's own remedy is "drop name".
  • label: expandViewContainerWithDiagnostics (packages/spec/src/ui/view.zod.ts) gives every expanded ViewItem the label of its list or form entry, never the container's. The ledger records no Studio reader either.

So both keys are dropped, and the label a person sees is list.label, which os lint requires.

Measured: the card's scenario

npm create objectstack (the in-repo on-ramp bin/, --skip-install, namespace tasks_app), then os g object project and os g object task, run with the in-repo CLI build.

step before (97239c3c8a) after (de6107c8e1)
os g flow task_done exit 0, bound tasks_app_task_done exit 1, lists the three objects, asks for --object
os g flow task_done --object task (no such flag) exit 0, bound tasks_app_task
os g view task exit 0 exit 0
os g action complete_task exit 1 (defineStack: object tasks_app_complete_task, flow complete_task_flow) exit 1, asks for --object
os g action complete_task --object task (no such flag) exit 0, runs task_done_flow (the only flow)
os g app tasks exit 1 (defineStack: object tasks_app_tasks) exit 1, asks for --object
os g app tasks --object task (no such flag) exit 0
os validate exit 0, 7 warnings exit 0, 1 warning
os build exit 0, 7 warnings exit 0, 1 warning
os lint exit 1: required/label at views[0].list.label, plus 7 warnings exit 0, 1 warning

The remaining warning in the "after" column is field-no-consumers at objects[0].fields.body. It sits on the STARTER's own note object and is not written by os g; see Acceptance notes.

Measured: every generator kind, three gates

A fresh starter plus one os g invocation per leg. "Before" binds by name (os g KIND probe_thing, and the view on the starter's note). "After" runs the pin's own chain: the prerequisites binds declares, then the kind. Findings are counted from the --json output of each gate, at every severity.

kind before: validate / build / lint after: findings beyond the starter ledger
object exit 0 / 0 / 0, 0 findings 0
view exit 1 / 1 / 1: list-view-field-unknown, sort-field-unknown and (lint) required/label errors; liveness-dead-property on name and label 0 (os g view note also leaves zero, the ledger finding included)
action os g exit 1 (object derived from its name) 0
flow exit 0, 3 warnings at each gate (flow-trigger-unknown-object, flow-draft-status-ambiguous, starter note.body) 0
dashboard exit 0, 1 warning at each gate (starter note.body) 0
app os g exit 1 (object derived from its name) 0
skill exit 0, 1 warning at each gate (starter note.body) 0
picklist exit 0, 0 findings 0

The pin, family close-out

  • Per-PR, in-process: packages/cli/test/generate-scaffold-validates.test.ts (extended). For every kind in GENERATOR_SCAFFOLD_TARGETS, each prerequisite named by binds (an object, a flow) is generated first. The bindings are resolved through stackBindingCandidates, the reader the command uses. The target is deliberately not named like the item. The test then runs what the three commands run: defineStack; normalizeStackInput with the unknown-key lints, the parse and runAuthoringRules('validate'); runAuthoringRules('build'); and lintConfig. It asserts zero findings at every severity. Before this change it judged only the error half of os validate, beside an object named like the item, which is why none of the card's cases showed up there. A kind added later is measured with no edit to the file. Also added: binding scaffolds write exactly the binding they are handed, and throw without one.
  • Nightly, real commands: packages/cli/test/generate-scaffold-gates.e2e.test.ts (new). One fresh starter per kind (the on-ramp bin/), the prerequisite chain derived from binds, then os validate --json, os build --json and os lint --json. Each gate must exit 0 with no finding beyond a one-entry starter ledger. A CONTROL leg (the bare starter) must report zero findings at all three gates. The ledger is shrink-only. Its entry must sit on an object the bare starter declares, read from the compiled artifact, and must still fire in at least one leg. It is in e2e because the run is about forty oclif + tsx cold starts, roughly 4 minutes on this box. That is the same reason generate-scaffolds-reach-stack.e2e.test.ts is nightly.
  • Per-PR: packages/cli/test/generate-binds-from-stack.test.ts (new). It covers every branch of resolveScaffoldBindings and unusedBindingFlagRefusal, asserting on the named subjects, not on the prose.

Existing tests the behaviour change required updating. Each needed a binding handed over or a flag added; none had an assertion weakened:

  • generate-object-namespace-prefix.test.ts: the set is composed object first. A binding scaffold must write the object it is handed, whatever the namespace.
  • generate-scaffold-wiring.test.ts: the registered key, and the container writes no name/label.
  • generate-emission-parses.test.ts, scaffold-emission-typechecks.test.ts and generate-refuses-name-outside-charset.test.ts: the probe bindings from the new test/helpers/scaffold-bindings.ts.
  • generate-refuses-unimportable-alias.test.ts: dashboard and skill carry the alias-layer cases that view and flow carried. In config-less directories a binding scaffold is now refused first. dashboard suffixes its binding the same way, so the layer it pins is unchanged.
  • generate-stack-reach.test.ts: an action whose --object names nothing; a view outside a project; the action control generating once an object and a flow exist; the no-config "Not wired" case carried by dashboard.
  • create-objectstack-stack-reach.test.ts and generate-scaffolds-reach-stack.e2e.test.ts: --object. The order is derived from binds.

Ablations

The fix was committed first. Each leg went through scripts/ablation-replace.mjs (wrap mode: the anchor must hit, the counts are checked on disk, and the file is restored to blob == HEAD with an empty git diff HEAD), inside a script with an EXIT/INT/TERM trap. Every leg below ended with the file restored (blob 2c8b969c1f07 for generate.ts, 0d902b75dff7 for scaffold-wiring.ts).

leg mutation red
A1 flow start node binds the object derived from its own name 4 (flow and action legs, the bound-object pin, the start-node pin)
A2 flow status: 'draft' 2 (flow and action legs)
A3 view container writes name and label again 1 (view leg)
A4 view list.label removed 1 (view leg)
A5 action locations removed 1 (action leg)
A6 action object derived from its name 2
A7 app nav object derived from its name 2
A8 object scaffold declares description again 3 (flow, action, app legs)
A9 resolver picks the first object when several are declared 1
A10 resolver drops the prefixed --object lookup 1
A11 --object/--flow on a non-taking type ignored 8
A12 reach reader identifies a container by name only 2
E1 e2e: flow status: 'draft', through dist 6 (flow and action legs × three gates)

Two first attempts were void and were redone with new anchors: the tool refused them because the counts did not move. In A3 the replacement re-contained the anchor. In A10 the replacement ; was already everywhere. E1's first run was also void, and in a way worth knowing: the mutation landed in src/ and the e2e stayed green, 38 of 38. The spawned bin/run-dev.js serves packages/cli/dist when dist is built. Measured: with src mutated to status: 'draft', tsx bin/run-dev.js g flow … --dry-run printed status: 'active'. E1 was then rerun as a dist ablation: mutate, rebuild the CLI, check with ablation-dist-preflight (marker present in dist/commands/generate.js), run the e2e (6 red), restore, rebuild, check with --absent (clean). See Acceptance notes.

Verification (on de6107c8e1, which merges origin/main at f9bcd08bef)

  • The CLI dependency closure was rebuilt after the merge: turbo run build --filter=@objectstack/cli..., 59 of 59 tasks.
  • pnpm --filter @objectstack/cli exec vitest run --project unit: 245 files, 3485 tests passed.
  • --project integration on test/generate- and test/create-objectstack: 7 files, 67 tests passed, run against a dist built from this head.
  • Nightly e2e (OS_TEST_TIERS=nightly): generate-scaffold-gates, generate-scaffolds-reach-stack, generate-object-namespace-prefix and generate-skill. 4 files, 69 tests passed.
  • pnpm --filter @objectstack/cli typecheck: exit 0. That includes check:test-typecheck; the debt ledger did not change.
  • node scripts/pm/dispatch-gates.mjs --repo objectstack-ai/objectstack --commands: 95 commands, every one exit 0. Three first exited 3 (PREREQUISITE NOT MET: check:skill-examples, check:dual-build-cjs-loads, check:i18n-coverage) and were rerun after building their prerequisites. --ran reconciliation: 95 derived, 95 run, 0 NOT MEASURED, a derived zero.
  • pnpm lint (repo-wide eslint, not narrowed): exit 0.

Docs and changeset

  • content/docs/deployment/cli.mdx, the #### os generate entry under ### Scaffolding: where bindings come from, the refusals, the new options, the examples, and the view no longer writing name.
  • .changeset/21325-generate-binds-from-stack.md: @objectstack/cli minor, BREAKING banner, Clause-②: yes (narrowing), ADR-0087 not-required (no-migration-prescription).

Acceptance notes

  • Docs edits outside the claimed region. This change made two examples in cli.mdx false, so they were edited too. The Quick Start "Add more metadata" block, os generate flow customer / os generate action customer, is refused on the starter, which declares two objects once customer exists. The "Typical Workflow" step os g flow opportunity comes after four objects. Each now passes --object. Neither block is in ### Quality, the region of sibling card objectstack verify reports "✓ verify passed" (exit 0) on a stack that os validate, os build and os lint refuse — and the published CLI docs do not document verify #21323.
  • packages/cli/README.md is fixed in this PR. The claim fenced it to cli README: documents -h / -v short flags that exit 2, and says there is no os plugin command group while os plugin build|sign|publish is registered #21310, which has since closed (landed), so the seat lifted the fence under the same claim, 5946394138. Its Quick Start line os generate flow task would be refused after this change, because the app template's object plus task makes two objects. It now reads os generate flow task_changed --object task, measured on a real os init my-app project (exit 0, then os validate / os build / os lint exit 0). The Typical Workflow line os generate view customer still holds and is unchanged.
  • Starter latent finding (the one e2e ledger entry). The npm create objectstack starter's note object declares body, which nothing reads. field-no-consumers stays silent while a stack holds no consumer root. The first view, flow, action, app, dashboard or skill, generated or hand-written, wakes it on the starter's object. os init -t app's item object has the same shape (description, status). os g cannot give someone else's field a consumer, so the literal "zero findings on a fresh starter" holds only for kinds that add no consumer root. Each kind's own output is measured at zero, and the pin records the remainder in a shrink-only ledger that turns red when the starter is fixed. The template fix is outside this card's surface. It is reported to the seat for a separate card.
  • bin/run-dev.js serves dist/ when it exists. It is documented in several CLI test headers as the source entry, and the suite says it does not depend on dist. Measured above (E1), it serves the built tree. So a spawned CLI pin reads build state. A test written against an unbuilt change reads stale behaviour, and an ablation of src/ alone stays green. This PR's spawned tests were rerun against a dist built from this head. No carrier is named for the test infrastructure.
  • Two new internal-tool edits are within the claim's "generate.ts and the CLI internals it directly references": project-namespace.ts and scaffold-wiring.ts. Also new: one test helper, test/helpers/scaffold-bindings.ts, and two test files.

Generated by Claude Code

claude added 8 commits October 2, 2026 06:26
…/action/object scaffolds pass the gates

Claude-Session: https://claude.ai/code/session_01VvcEokUG1tvVxkceYfR5XB
Co-authored-by: Claude <noreply@anthropic.com>
…ss the three doors

Claude-Session: https://claude.ai/code/session_01VvcEokUG1tvVxkceYfR5XB
Co-authored-by: Claude <noreply@anthropic.com>
…d gate pin on the fresh starter

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

github-actions Bot commented Oct 2, 2026 •

Copy link
Copy Markdown
Contributor

📓 Docs Drift Check

This PR changes 1 package(s): @objectstack/cli, touching 31 documentable anchor(s). ⚠️ 1 changed file(s) yielded no anchor (packages/cli/README.md), so the pages documenting them are NOT COVERED by this run — this is not a clean bill of health for those files.

21 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 9360df4138615a646630c0727c3b975711fb2eb1.

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

What this run could not see
  • 1 changed file(s) yielded no anchor (packages/cli/README.md) — pages documenting those are invisible to this run
  • 1 anchor(s) matched too much of the corpus to be a work list: objectName (symbol, 37 pages)
  • 13 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 — 26 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 9360df4138615a646630c0727c3b975711fb2eb1 → packageMentionDocs.

Which tree this was computed on

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

node scripts/docs-audit/affected-docs.mjs --json 9360df4138615a646630c0727c3b975711fb2eb1

⚠️ 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 9360df4138615a646630c0727c3b975711fb2eb1 → 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

Contract review

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

Inputs read: card #21325 body and comments 5945870485 (direction), 5946394138 (claim), 5947877777 (os-dev-report); PR #21369 body and its 19-file list; git diff 5fd4855a9ae7c24f31070227131f2eb42037aabb refs/review/pr-21369; the readers by sha at the head (packages/cli/src/commands/generate.ts at base and head, packages/metadata/src/view-container.ts, packages/objectql/src/view-container-name-refusal.ts, packages/spec/liveness/view.json, packages/spec/src/ui/view.zod.ts at expandViewContainerWithDiagnostics, packages/cli/src/commands/lint.ts lintConfig, packages/lint/src/authoring-rules.ts, validate-list-view-field-refs.ts, services/service-automation/src/engine.ts flowStatusDisabled, scripts/check-changeset-no-major.mjs, scripts/check-adr-0087-registration.mjs); check-runs on the head collapsed latest-per-name: 35 runs, 33 success, 2 skipped (Console Pin Gate, Packed-tarball smoke (opt-in)), 0 failure, 0 pending — verified, matches dispatch. The state of #21310 (closed 2026-10-02T07:46Z) and #21370 (open) was read from the API to settle two boundary flags.

① Derived judgments

(a) Bindings never derived from the item's name — right, for every reference each scaffold writes. At the head each generator declares binds and runMetadataGeneration calls the pure resolveScaffoldBindings after the config load, the namespace read and the residual-prefix gate, and before dir, the render, the parse check and the dry-run branch. The references enumerated from the templates: view writes object, every columns[].field and the sort field — all from the binding read off the loaded stack (stackBindingCandidates), the view being named after a declared object (binds.object: 'name') and refused when the stack does not declare it; flow writes the start node config.objectName from --object or the stack's only object; action writes objectName from --object or the only object and target from --flow or the only flow; app writes navigation[0].objectName and its label from the bound object. No template reads objectNameFor(name, …) for a binding any more; requireBinding throws when a binding scaffold is rendered without one, and the per-PR pin asserts both that the bound object lands and that the namespace-prefixed item name appears nowhere in the artifact. dashboard, skill, object, picklist bind nothing (verified in their templates). The non-interactive reading of "or from the stack's existing objects" — exactly one declared binds, several refuses and lists them — is right: no os generate path prompts and the card's own Expected says "asks / takes --object". --flow is within the direction. The direction's prohibition is on any reference derived from the new item's name; the action's flow (NAME_flow) was one, and the alternative (a script action with an invented body, or an action bound to a flow nobody declared, which defineStack passed when no flows existed) would have kept the defect on the second reference. It is spelled exactly like --object (long flag, value, default = the stack's only one), the dev flagged it in the report's argument_shape and in the PR body, and the changeset and cli.mdx carry it. A second public flag of the same shape, applied to the same rule, did not need a separate ruling; the judgment is recorded here.

(b) Refusals — right: loud, remedy named, nothing written. All five routes land in refuseGeneration (printError, the body, process.exit(1)): no --object with zero or several objects, --object or --flow naming nothing declared, no --flow with zero or several flows, a view named after an undeclared object, and a binding scaffold outside a project (project.kind !== 'loaded'). Each prints the declared names where there are any and the next command to run. unusedBindingFlagRefusal runs in Generate.run before the SUB_COMMANDS routing, so --object or --flow on object, dashboard, skill, picklist, view (names its object), flow (--flow), app (--flow), types, client and migration is refused rather than silently honoured; an unknown type still gets the roster answer. Every refusal sits above the first write, and the spawned pin asserts a byte-identical tree. One nit, not a defect: the "declares none" branches do not restate the empty list in the body — the headline says it.

(c) View container name and label both dropped, list.label written — right, checked against the readers. deriveViewContainerObject (view-container.ts) returns own object ?? list.data.object ?? form.data.object ?? name, so with object set name is never the registry key. viewContainerNameRefusal returns undefined when name is absent or empty and refuses only a name that is set and differs from that key; its remedy text is "drop name, or set it to the key". The liveness ledger grades name dead ("a copy nobody reads") and label dead with the exact evidence the dev cites: expandViewContainerWithDiagnostics stamps each expanded item with v.label / defaultList.label / defaultForm.label and never container.label, and Studio drops containers from every view list. object is live. So the old template's comment ("the server refuses a container whose name disagrees") and os validate's "dead" were both true and compatible, exactly as the dev wrote. lintConfig reads the view label through getViewLabel and requires it (required/label), so list.label is the surviving label. Columns are every declared field (ListColumnSchema.width is optional; list-view-field-unknown / -dotted cannot fire on a declared, undotted name), sort uses resolveDisplayField only when it names a declared field. scaffold-wiring.ts registeredItemName keeps the reach report finding a nameless container under deriveViewContainerObject; the @objectstack/metadata/view-container subpath is exported and @objectstack/metadata is a declared CLI dependency.

(d) Pin family close-out — right. generate-scaffold-validates.test.ts iterates GENERATOR_SCAFFOLD_TARGETS (derived from GENERATORS), builds each kind's prerequisites from its own binds (object stem probe_target, flow stem probe_target_changed, deliberately not the item name), resolves them through stackBindingCandidates, and findings() collects: the defineStack load error, lintUnknownStackKeys + lintUnknownAuthoringKeys, safeParse issues, runAuthoringRules('validate') and ('build') at every severity, and lintConfig (the command's own checks plus its runAuthoringRules('lint') pass at line 680) at every severity — then toEqual([]). The ledger KNOWN_UNVALIDATED_SCAFFOLDS is empty. A kind added later with binds of object/flow or none is measured with no edit; a kind binding a key outside ScaffoldBinds fails the roster test loudly naming the stem to add. The nightly generate-scaffold-gates.e2e.test.ts runs one fresh on-ramp starter per kind plus a CONTROL leg, chains prerequisites from binds, runs os validate|build|lint --json (shapes verified: errors/warnings with rule/path; lint issues; os build is an alias of compile, which writes dist/objectstack.json), requires exit 0 and zero findings beyond STARTER_LATENT_FINDINGS, which has one entry (field-no-consumers@objects[0].fields.body). It goes red when the starter is fixed (the "still fires somewhere" test) and cannot cover a scaffold's own object (the finding's object must be one the bare starter declares, read from the compiled artifact). Evidence of the nightly's green is the dev's run (69 tests); it is not in per-PR CI by its tier, as its sibling generate-scaffolds-reach-stack.e2e.test.ts is not. generate-binds-from-stack.test.ts pins every resolver and unused-flag branch on named subjects.

(e) Published text — every factual sentence true at the head. cli.mdx Quick Start runs on npm create objectstack (blank starter: one object note, no flows): after os generate object customer two objects exist, so --object customer is needed; customer_changed yields customer_changed_flow, the only flow, so os generate action approve --object customer "runs the stack's only flow" — true. The #### os generate region: the binding sources, the four refusals, "outside a project … refused", "--object and --flow on a type that takes neither are refused rather than ignored", the options, the examples (os g app sales --object customer, os g action escalate --object customer --flow customer_changed_flow), step 1 of "What it does", and the rewritten "Refused" bullet all match the resolver at the head. Typical Workflow os g flow opportunity_changed --object opportunity after four objects — true. packages/cli/README.md Quick Start: os init my-app declares my_app_item, then os generate object task makes two objects, so os generate view task (named after declared my_app_task) and os generate flow task_changed --object task — true; the Typical Workflow line os generate view customer after os generate object customer still holds. The changeset's statements (prefix lookup, only-object default, the refusal list including types/client/migration, object drops description, view drops name/label and gains list.label plus all fields, flow status: 'active' — flowStatusDisabled is exactly obsolete || invalid — action locations: ['record_header'] in the schema vocabulary, app nav label) are each true. Sweep for os g|generate flow|action|app|view across content/docs, packages/cli/README.md, packages/create-objectstack/src/templates/**, skills/**: no other invocation; getting-started/your-first-project.mdx:143 is a generic mention; the blank starter's README.md:43/:74, objectstack.config.ts:49/:70 and the src/*/index.ts barrel comments use a NAME placeholder and stay true for a declared name (an undeclared one is now refused with the remedy named), so nothing published is left false.

② Semver level

Right. @objectstack/cli: minor with a BREAKING banner, Clause-②: yes (narrowing), and exactly one ADR-0087 marker not-required (no-migration-prescription). The arm: the flags widen the public surface, but the same diff narrows the accept set — os g flow X in a multi-object stack, os g action X with no flow, any binding scaffold outside a project, and the pre-change cli.mdx examples are now refused — and the declaration takes at most one arm, so the breaking arm is the honest one; the claim's expected (widening) was the pre-diff guess and the dev re-measured as the claim required. Level: (narrowing) is BREAKING; check-changeset-no-major forbids major during the launch window, so minor is the only correct grade. ADR-0087 disposition: no authorable key, export or stored shape moves and files generated by earlier releases load unchanged, so there is nothing for objectstack migrate meta to rewrite; the body's "What to write instead" is command usage, not a consumer-code rewrite, and the gate (Check Changeset, Lint & Repo Gates) accepted it. The migration-sentence requirement is met by that section.

③ Boundary flags

  • cli.mdx edited outside the #### os generate region (Quick Start, Typical Workflow) — accepted. Both blocks were falsified by this diff (each is refused on the starter it documents); the os-dev rule requires fixing text a change falsifies; neither block is in ### Quality (objectstack verify reports "✓ verify passed" (exit 0) on a stack that os validate, os build and os lint refuse — and the published CLI docs do not document verify #21323's region).
  • Per-kind literal zero held on the scaffold's own output; the starter's latent warning ledgered — accepted. The per-PR pin asserts literal zero with no ledger; the nightly's one-entry ledger is shrink-only, must sit on a starter-declared object, and must still fire, so it cannot absorb a scaffold finding and turns red when the template is fixed. The residual is the starter's note.body, not something os g writes; filed as [finding] fresh starters carry a latent field-no-consumers warning that the first os generate wakes (create-objectstack blank note.body; os init -t app item description / status) #21370 (verified open, names both templates).
  • Two CLI internals (project-namespace.ts carries config on the loaded result; scaffold-wiring.ts registeredItemName) — accepted: internals generate.ts directly references, inside the claim's wording.
  • Nine existing tests updated — accepted after reading each diff: every change hands a binding or a flag; unimportable-alias moves its cases to dashboard/skill (still two generators, same layer); stack-reach gains a flow control and asserts the written objectName/target; namespace-prefix adds a pin that a binding template writes exactly what it is handed; nothing weakened.
  • README fixed in the patch round — accepted. cli README: documents -h / -v short flags that exit 2, and says there is no os plugin command group while os plugin build|sign|publish is registered #21310 closed at 07:46Z; the README commit c0190c97 is 08:15Z; No other open PR may claim the same single-writer path is green. One correction for the dispatching seat: the PR body's Acceptance notes still say packages/cli/README.md "was not edited" (body updated 08:16Z) — stale against the file list; fix the sentence, not the diff.
  • run-dev.js serving dist — accepted as noted: test infrastructure with no public door, recorded in the PR's Acceptance notes; the dev reran spawned suites against a dist built from the head and redid E1 as a dist ablation.
  • Blank starter README line 43 — accepted as noted, not filed: the sentence stays true for a declared NAME and an undeclared one is refused with the remedy named, so it is a precision gap on a template outside the surface, not a trap. It appears only in the os-dev-report's out_of_scope_findings, not in the PR's Acceptance notes; minor.
  • --flow — see ①(a): within the direction, flagged by the dev, no ruling owed.
  • Fixes #21325 — right to close on this PR. All three cases on the card are fixed and pinned (per-PR and nightly), the Expected ("passes os validate AND os lint … targets real metadata (or asks / takes --object)") is met for every kind's output, and the one remaining warning is the starter's own defect with its own card. The card this PR closes must claim this branch and Part-of PR must not also close its card are both green.

Implemented-by: claude/issue-21325-generate-scaffolds-pass-gates
Reviewed-by: session_01VvcEokUG1tvVxkceYfR5XB

VERDICT: PASS


Generated by Claude Code

akarma-synetal pushed a commit to akarma-synetal/framework that referenced this pull request Oct 7, 2026
…its that decided them (stage 5 of objectstack-ai#20595) (objectstack-ai#21375)

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

## What changed

Stage 5 of the `domain:engine` lane of the dead-citation sweep:
`packages/drivers/driver-memory/**`, comment and docblock prose only,
per the claim (`5947712781`). Stages 1 to 4 landed as `a7d9768ec`,
`d150c3039`, `4bf4e7e70` and `13a24ece2`; objectstack-ai#20595 stays open for the next
stage.

Every comment or docblock site in the package that cited a tracker
number answering 404 is rewritten in ruling C+D's form C (record
`5749154545` on objectstack-ai#19123): the ADR when one records the decision,
otherwise the commit in this repository's history that made it. That is
**104 sites on 103 lines in 27 files, covering 13 numbers**:

- **59 census sites** (59 lines, 5 files under `src/`): the whole
`allocated-but-absent` population of the gate's own census in this
package at the base;
- **4 sites on 3 lines outside the census glob, inside the claimed
surface**: `objectstack.config.ts:19` (objectstack-ai#11332) and `:26` (objectstack-ai#10724), and
`tsconfig.typecheck.json:11` (objectstack-ai#11332 and objectstack-ai#10724 on one `//` line);
- **41 test-comment sites** (41 lines, 20 test files), which the census
defers. They carry 11 numbers: 6 the census itself reads as dead in this
package's `src`, 2 it reads as dead elsewhere in the repository (objectstack-ai#6075,
objectstack-ai#17590), and 3 it never judges because they stand only in test files
(objectstack-ai#14434, objectstack-ai#16838, objectstack-ai#17348), which the board and a single read each settle.

No comment-id citation in the package is dead (see Census).

**Anchors: 13 numbers by commit, 0 by ADR, 0 by words alone.** 11
numbers reuse the anchor another lane or stage already measured for
them; 2 were measured here (objectstack-ai#14435, objectstack-ai#16838). 13 distinct shas. No ADR or
ruling record names any of the 13 for this subject: `git grep` over
`docs/adr` and `scripts/adr-anchors` finds only ADR-0088's correction
row for objectstack-ai#10724, which is about `contributes.functions`, not the
`contributes.drivers` retirement cited here.

Only comments changed. Every file keeps its line count (103 lines out,
103 in, plus the changeset), so no line citation into any of them moves.
No code token moves (the guard below). **No citation number is added**:
on every changed line, the numbers on the new text are a subset of those
on the old, and the diff-scoped gate judged the 7 citations left on
changed lines: all 7 resolve.

**A `patch` changeset**: 20 of the 62 rewritten non-test lines are in
the published `dist` (the `.d.ts` keeps JSDoc on exported members, and
esbuild keeps some comments in the JS), and `dist` is not byte-identical
with the base text (see Changeset).

## H0: the largest remaining `domain:engine` package

The gate's own `node scripts/check-issue-citations.mjs --census --json`
at base `5fd4855a9` (the before run below), `allocated-but-absent` per
package over every `domain:engine` family in
`.claude/skills/pm-dispatch/SKILL.md`'s lane table:

| package | sites |
|---|---|
| `drivers/driver-memory` | **59** |
| `metadata` | 41 |
| `core` | 40 |
| `platform-objects` | 27 |
| `metadata-core` | 19 |
| `drivers/driver-turso` | 14 |
| `drivers/driver-mongodb` | 9 |
| `formula` | 4 |
| `metadata-fs` | 2 |
| `drivers/driver-sqlite-wasm`, `plugins/plugin-pinyin-search`,
`objectql`, `metadata-protocol`, `drivers/driver-sql` | 0 each |

`driver-memory` is still the largest (lane total 215), so this stage
went ahead.

## Census: `driver-memory`, before and after

**Instrument (A1).** The gate's own `node
scripts/check-issue-citations.mjs --census --json`, read-only and
unchanged. The count is its `allocated-but-absent` findings under
`packages/drivers/driver-memory/`.

| reading | tree | board | whole-repo `allocated-but-absent` | sites |
lines | files | numbers |
|---|---|---|---|---|---|---|---|
| before | base `5fd4855a9`, run 07:54:54Z to 07:58:32Z | enumerated,
192 pages, frontier objectstack-ai#21366, 19,187 records (newest number read before
and after the run: objectstack-ai#21366) | 330 | **59** | 59 | 5 | 6 |
| after | `17f667f86`, run 08:13:08Z to 08:17:18Z | enumerated, 192
pages, frontier objectstack-ai#21370, 19,191 records (newest before and after: objectstack-ai#21370)
| 271 | **0** | 0 | 0 | 0 |

The whole-repo drop is 59, and the two finding sets differ by exactly
the 59 rows of this package, removed; none was added. `resolves`
(34,880), `resolves-as-pull-request` (2,378) and `cross-repo-unjudged`
(1,165) did not move. The card's 62 was taken at `f11b5f20a2` with the
older extractor; the base here reads 59. The head's later commits are
the changeset (outside the census surface) and one merge of `main` that
touches no file under `packages/drivers/driver-memory`.

**Supplementary instrument, the whole package.** The census reads
neither test files nor strings nor files outside `src`. A second reading
runs the gate's own exported `extractCitations` (whole-file and
comment-prose projections) over every tracked file in the package (89:
82 `.ts`, `package.json`, `tsconfig.json`, `tsconfig.typecheck.json`,
`README.md`, `CHANGELOG.md`, `LICENSE`, `objectstack.config.ts` among
the `.ts`), and classifies each citation with the gate's
`classifyCitation` against one board enumerated by the gate's
`enumerateBoard` (192 pages, frontier objectstack-ai#21369, 19,190 records, 07:59:39Z
to 08:03:13Z). Every one of the 13 numbers in the population was then
read on its own over the issues endpoint (08:04Z): **all 13 answer
404**, and the lit controls `objectstack-ai#5286` and `objectstack-ai#12624` answer 200.

| reading | citations | dead | src comment | test comment | other
(`tsconfig.typecheck.json`) | test string | changelog |
|---|---|---|---|---|---|---|---|
| before, `5fd4855a9` | 1,720 | **143** | 61 | 41 | 2 | 21 | 18 |
| after, `17f667f86` | 1,616 | **39** | 0 | 0 | 0 | 21 | 18 |

`src comment` here is the 59 census sites plus the 2 in
`objectstack.config.ts`. The drop of 104 citations is exactly the
rewritten sites, and the live counts did not move (src comment: 494
resolve, 13 as pull requests, 1 cross-repo; test comment: 504, 20 and
4). A third, raw reading (every `#` followed by 2 to 6 digits, whatever
surrounds it) counts 1,736 before and 1,632 after: a drop of 104.

**Comment ids.** Three distinct comment-id citations stand on 6 sites in
this package: `5234868334` (1 site), `5861435168` (2) and `5865693155`
(3). Each was read over the issue-comments endpoint and answers 200
(control `5947712781`, 200). So no comment id is in the population.

## Per-number table

`src` counts the census sites plus the four outside the census glob;
`test` counts test-comment sites. Every sha below matches exactly one
commit (`git rev-parse --disambiguate`, count 1) and is an ancestor of
the base `5fd4855a9` (`git merge-base --is-ancestor`, exit 0 for all 13
shas; the clone is not shallow). The `+` lines introduce exactly these
13 nine-hex spans and no other. The message or the diff of each one
names the number it replaces: 6 in the message and the diff
(`56c093c4d`, `0f38ab084`, `9dac1ae01`, `be21955ba`, `e04a0aff2`,
`d367f03d6`), 6 in the diff alone (`555a89cbd` 28 added lines,
`20950404c` 7, `dce5cd4f0` 26, `51efbf116` 4, `20032594f` 8, `b90aff81f`
9), and 1 only in the subject's squash suffix (`93940d492`, where objectstack-ai#14434
was that pull request's own number). Each anchor touches
`packages/drivers/driver-memory` itself, except `e04a0aff2`, whose two
sites name the `driver-sql` fixture it created. `git blame` at the base
puts 67 of the 104 sites on their anchor. The other 37 were written by a
different commit that cites the number as another change: for objectstack-ai#13197,
the declared-index commit `b7f645a8a` and the atomicity commits
`34e80993f` and `4642f4c64`; for objectstack-ai#13195, the clobber generalisation
`df1812050`, the reference-matcher retirement `8fec76a2b`, and two
commits from while that card was open (`178325bcb`, `9268aec56`, see
Wordings to check). In each case the anchor is the commit that made the
change the sentence credits to the number. `source` says whether another
lane or stage already used this anchor for this number (`reused`) or it
was measured here (`measured`).

| number | src | test | anchor | kind | source | what it decided |
|---|---|---|---|---|---|---|
| `objectstack-ai#6075` | 0 | 1 | `d367f03d6` | commit | reused (stage 4) | five
drivers' query parameters follow `DriverQuery` |
| `objectstack-ai#10724` | 2 | 0 | `be21955ba` | commit | reused (cli lane, the
eleven-package sweep `aaad682dbc`, spec) | retire the nine dead
`contributes` members, `drivers` among them, with driver-memory's
manifest corrected in the same change |
| `objectstack-ai#11065` | 2 | 3 | `20950404c` | commit | reused (stage 4) | count a
boolean aggregand as 1/0 in avg and sum, on driver-memory's data face
and analytics face |
| `objectstack-ai#11332` | 2 | 0 | `dce5cd4f0` | commit | reused (cli lane, the
eleven-package sweep, spec) | retire the plugin manifest's dead
top-level containers (`capabilities`, `configuration`, `extensions`),
driver-memory's manifest included |
| `objectstack-ai#13195` | 8 | 11 | `9dac1ae01` | commit | reused (spec stage 4) |
`$exists` means HAS A VALUE on the live mingo path, the analytics face
and `translateFilter`, with the lowered `$exists` promoted to its own
`$and` branch when its key is taken; its changeset records the
maintainer's 2026-08-30 ruling |
| `objectstack-ai#13197` | 16 | 11 | `56c093c4d` | commit | reused (stage 3, the
types, rest and spec lanes) | driver-memory enforces field-level
`unique`, refusing a colliding write with the ADR-0112 envelope;
`updateMany` checks every row before mutating any |
| `objectstack-ai#14434` | 0 | 1 | `93940d492` | commit | reused (stage 4) | un-mask
driver-memory's published `update` / `upsert` types (that pull request's
squash) |
| `objectstack-ai#14435` | 3 | 3 | `20032594f` | commit | measured | publish the
declared return types on `find()`, `findOne()` and `create()` |
| `objectstack-ai#16589` | 23 | 3 | `555a89cbd` | commit | reused (the services lane's
pointer `5904005321` on this card) | refuse a tenant-scoped call instead
of returning cross-organization rows: seam 3,
`assertCallNotTenantScoped` |
| `objectstack-ai#16729` | 7 | 1 | `0f38ab084` | commit | reused (stage 4) | an
explicit tenancy opt-out survives a partial `syncSchema`
re-registration, on driver-memory and driver-sql |
| `objectstack-ai#16838` | 0 | 4 | `b90aff81f` | commit | measured | read a stored
ARRAY as its elements in the equality arm, so both filter faces answer
one filter one way |
| `objectstack-ai#17348` | 0 | 1 | `51efbf116` | commit | reused (spec stage 3) |
type-gate a text operator over a declared temporal column; it pinned
driver-memory's divergence by name and recorded the follow-up as filed |
| `objectstack-ai#17590` | 0 | 2 | `e04a0aff2` | commit | reused (stage 4) | compile
`$contains` on a JSON column as a per-dialect MEMBERSHIP test; it
created the `driver-sql` fixture these two sites copy |

## Wordings to check

Most rewrites swap a tag in place (`[#N]` to `[commit SHA]`, `(#N)` to
`(commit SHA)`, `#N's X` to `commit SHA's X`, `since #N` to `since
commit SHA`), the form the landed stages use. These say more than the
tag:

- **`objectstack-ai#17348`** (`memory-driver-filter-logic-conformance.test.ts:598`):
「the repair is in the driver, and it is objectstack-ai#17348's, filed」 became 「the
repair is in the driver, named as owed in commit 51efbf1 and filed」,
with `:599` (「against objectstack-ai#5499's investment freeze.」) untouched.
`51efbf116` wrote this note, and its changeset records 「That divergence
is filed as objectstack-ai#17348 and pinned by name」.
- **`objectstack-ai#13195` where it was written while that card was open**
(`memory-matcher-no-value-negated-operators.test.ts:32`): the sentence
says `$exists` 「is the neighbouring cell」 and 「still read[s] it as
key-presence」, and `:47` already calls that caution history. A bare swap
would credit `9dac1ae01` with a still-open divergence, so it reads 「(the
card commit 9dac1ae closed)」. The other site written while that card
was open, `memory-exists-has-value-faces.test.ts:4`, is the file's title
from the pin `9268aec56` took before the fix; the header under it says
the file 「is no longer a pin of a divergence」 but was inverted in place,
and `9dac1ae01` is the commit that inverted it, so a tag swap reads true
there.
- **A quotation of this note's own earlier text**
(`memory-driver-document-not.test.ts:239`-`:241`, 「It used to say "… the
direction is now objectstack-ai#13166's and objectstack-ai#13195's to settle."」): the dead number
inside the quote became a bracketed editorial substitution, 「[commit
9dac1ae]'s」, so the quote stays recognisable as a quote and says where
the edit is. It is the note quoting itself, not a ruling.
- **Where the number named the defect, not the change**: 「(this is the
objectstack-ai#16589 defect)」 became 「(this is the defect commit 555a89c fixed)」
(`memory-tenancy-guard.ts:271`); 「This is the objectstack-ai#16589 answer」 became
「This is the defect's answer (fixed in commit 555a89c)」
(`memory-tenant-scope-refusal.test.ts:139`); and in that file's reading
table (`:29`) the cell 「the objectstack-ai#16589 defect」 became 「what commit 555a89c
fixed」, which fits the column's 28-character width, so the table stays
aligned and no row is added.
- **Slash and list pairs**: 「objectstack-ai#13197/objectstack-ai#13239」 became 「commit 56c093c and
objectstack-ai#13239」 (two sites); 「objectstack-ai#13197 / objectstack-ai#13239」 became 「commit 56c093c /
objectstack-ai#13239」; 「[objectstack-ai#13197, objectstack-ai#13239]」 became 「[commit 56c093c, objectstack-ai#13239]」 (four
sites); 「(objectstack-ai#11332 / objectstack-ai#10724 / objectstack-ai#4914)」 became 「(commit dce5cd4 / commit
be21955 / objectstack-ai#4914)」, the exact form the eleven-package sweep
(`aaad682dbc`) gave the same sentence in `plugin-hono-server`.
- **`driver-sql`'s fixture**
(`memory-20874-contains-membership.test.ts:63`, `:110`): 「`driver-sql`'s
objectstack-ai#17590 fixture」 / 「assertions」 became 「`driver-sql`'s commit e04a0af
fixture」 / 「assertions」; `e04a0aff2` created
`sql-driver-17590-json-column-membership.test.ts`.
- **Dated rulings keep their date**: the `objectstack-ai#13195` sites that say 「ruled
… 2026-08-30」 keep it (`9dac1ae01`'s changeset and docblocks carry 「the
maintainer ruled on 2026-08-30」).
- **Capitalisation**: where the number opened a sentence, the new text
opens with 「Commit」 (five sites).
- No line was reflowed, so some are longer than their block's wrap
(`eslint.config.mjs` declares no line-length rule, and a reflow would
move neighbouring lines and every line citation into the file).

## Sites left

- **In comments (src, test, the two config files): none.**
- **String literals: 21 test-string sites, 7 numbers, 11 files**
(`describe` and `it` titles, assertion arguments): `objectstack-ai#13195` 7, `objectstack-ai#13197`
7, `objectstack-ai#11065` 2, `objectstack-ai#16838` 2, `objectstack-ai#14435` 1, `objectstack-ai#16589` 1, `objectstack-ai#16729` 1. Every one
of the 7 is in this stage's population, so the table above holds an
anchor for each. Non-test strings carry none. Strings are outside this
stage's surface.
- **Outside `src`:** the release-owned `CHANGELOG.md` names dead numbers
on 18 sites (11 numbers); left. `package.json`, `tsconfig.json`,
`README.md` and `LICENSE` cite no dead number.

## Mechanical guard: no code token moves

The guard (stages 2 to 4's) compares base `5fd4855a9` against the
working tree over all 27 touched files, with TypeScript 6.0.3
(`tsconfig.typecheck.json` through `ts.parseJsonText`, the same two
walks):

- **Reading 1**: the parser's leaf nodes, from a `forEachChild` walk.
Comments are trivia there, and JSDoc is never visited. A leaf that is
not itself a token is re-scanned with trivia skipped.
- **Reading 2**: the full token stream in parser context, from a
`getChildren` walk, JSDoc nodes skipped. String, template and numeric
literals are compared in full on both readings.

Results:

- Real run at the head: 61,808 base tokens, **0 files with a token
change** (exit 0).
- Comment control (「The declaration dies with the table.」 to 「DIES」,
`memory-driver.ts`): 0 files changed (exit 0).
- Positive control, an identifier (`tenantFieldOf` to `tenantFieldOfX`,
`memory-unique-constraint.ts`): DIFFER on both readings (exit 1).
- Positive control, a string literal
(`'MEMORY_MULTI_TENANT_UNSUPPORTED'` to `…X`,
`memory-tenancy-guard.ts`): DIFFER on both readings (exit 1).
- Positive control, a numeric literal (`UNIQUE_VIOLATION_STATUS = 409`
to `410`, `memory-unique-constraint.ts`): DIFFER on both readings (exit
1).
- Positive control, a JSON value (`"rootDir": "."` to `"./"`,
`tsconfig.typecheck.json`): DIFFER on both readings (exit 1).

Each mutation went through `scripts/ablation-replace.mjs` (wrap mode,
anchor hit 1 to 0) under a shell trap that restores by absolute path
from `HEAD`. Each restore was proven equal to its `HEAD` blob
(`ac823bf8fa9b`, `eb0eb11358ea`, `7b008c1f12b7`, `eb0eb11358ea`,
`88035b359542`), with `git diff HEAD` empty and a clean tree afterwards.

## Changeset: `patch` (`dist` measured)

`files[]` is `dist`, `README.md` and `CHANGELOG.md`, and the package is
not private. The dependency closure and the package were built first
(`turbo run build --filter='@objectstack/driver-memory^...'
--filter=@objectstack/driver-memory`, 5 of 5 tasks, VERDICT command-exit
0). Then the package's own `build` (tsup plus `check-dts-emitted`) ran
three times in one script under the shared verify lock (VERDICT
command-exit 0), at `17f667f86`:

- **Leg 1**, the head text: 6 `dist` files hashed. Of the 62 rewritten
non-test lines, 20 appear verbatim in `dist`: 11 from
`memory-driver.ts`, 4 each from `memory-tenancy-guard.ts` and
`memory-unique-constraint.ts`, 1 from `memory-analytics.ts`; in
`index.d.ts` / `index.d.mts` (16) and `index.js` / `index.mjs` (10).
- **Leg 2**, the base text put back in the 7 non-test touched files (7
of 7 proven equal to their base blob): 4 of the 6 files differ from leg
1 (`index.d.ts`, `index.d.mts`, `index.js`, `index.mjs`), and
`scripts/ablation-dist-preflight.mjs` finds the base marker 「[objectstack-ai#16729]
One driver's record」 in 2 built files (exit 0).
- **Leg 3**, after the proven restore (7 of 7 equal to their `HEAD`
blob, `git diff HEAD` empty, porcelain empty): all 6 files are
byte-identical to leg 1, and the preflight's `--absent` reading exits 0,
so the build is deterministic and the difference is the rewrite.

So the rewrite ships, and
`.changeset/20595-driver-memory-provenance-anchors.md` declares a
`patch` for `@objectstack/driver-memory`, comment text only, with the
claim's `Clause-②: no` line. The later changeset commit and the merge of
`main` touch no file under `packages/drivers/driver-memory`.

## Gates (head `381dddc9c`)

- **Derived gates:** `node scripts/pm/dispatch-gates.mjs --repo
objectstack-ai/objectstack --commands` at `381dddc9c` (28 paths against
merge base `fbe2debb7`, 220 changed lines) derived 62 commands. All 62
ran, each exit code captured before any pipe: 62 exit 0. `--ran` reports
「62 derived, 62 run, 0 NOT-MEASURED, 0 UNRUN」 (a derived zero) and exits
0.
- **Roster families the derivation flags under a touched directory**,
also run, each exit 0: `node scripts/check-changeset-fixed.mjs`, `pnpm
check:authz-resolver`, `pnpm check:error-code-casing`, `pnpm
check:filter-alias-parity`, `pnpm check:object-def-param-keys`, `pnpm
check:tenant-chokepoint`.
- **Named readings:** `node scripts/check-issue-citations.mjs` exits 0
(「every citation this change adds resolves」, 7 judged across 5 files);
`pnpm check:issue-citations` exits 0 (self-test, 173 cases, 9
batteries); `pnpm check:doc-authoring` exits 0 (the sibling-package
prose-id baseline holds, no growth); `pnpm check:nul-bytes` exits 0
(9,669 files, no raw control bytes), and a control-byte grep over the 28
changed files finds none (exit 1).
- **Build before the gates:** `turbo run build --filter='./packages/*'
--filter='./packages/*/*' --concurrency=2` at `381dddc9c`, 71 of 71
tasks, VERDICT command-exit 0.
- **Tests and typecheck, under the verify lock, at `381dddc9c`:** `pnpm
--filter @objectstack/driver-memory test`: 70 test files pass (70),
1,718 tests pass (1,718), exit 0; `pnpm --filter
@objectstack/driver-memory typecheck` (both programs, `tsconfig.json`
and `tsconfig.typecheck.json`) exits 0. `tsc --listFiles` puts all 70
tracked test files and the 25 changed `src` files in the `tsconfig.json`
program, and `objectstack.config.ts` in the `tsconfig.typecheck.json`
one.
- **Lint, as a proven narrowing:** eslint with inline config disabled,
over the 26 touched `.ts` files plus `dist/index.js` as the control: 27
results, 0 errors and 1 warning, the control's ignore notice; none of
the 26 is reported ignored. `eslint.config.mjs` never enables type-aware
linting (its lines 327-328 say so), so a comment edit cannot move the
verdict on an untouched file. `tsconfig.typecheck.json` is not an eslint
target (run alone, eslint answers 「File ignored because no matching
configuration was supplied」); the token guard covers it. The repo-wide
`pnpm lint` is CI's run.

## Acceptance notes

- **Base.** The branch was cut at `5fd4855a9` and merged with `main`
once, at `fbe2debb7`, before the gates (merge `381dddc9c`, no conflict,
no file under `packages/drivers/driver-memory`, and neither
`check-issue-citations.mjs` nor `dispatch-gates.mjs`). `45ce12a48` (the
last `driver-memory` change before this stage) is in the base
(`merge-base --is-ancestor` exit 0; control `13a24ece2`, exit 0). The
net diff against `main` is the 27 rewritten files (+103/−103) and the
changeset.
- **One sentence keeps a deictic whose antecedent was the number.**
`memory-exists-has-value-faces.test.ts:264` says 「not in the card」 two
lines under the tag that named that card, now `[commit 9dac1ae]`. It
has no number of its own, so it was not touched.
- **Wording only:** no line without a number was changed.

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

---------

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