Skip to content

fix(cli): a narrowed os migrate --apply records no deployment flag, and an unknown --object is refused - #21662

Merged
objectstack-fleet[bot] merged 4 commits into
mainfrom
claude/issue-21644-narrowed-apply-flag
Oct 4, 2026
Merged

objectstack-fleet[bot] merged 4 commits into
mainfrom
claude/issue-21644-narrowed-apply-flag

Conversation

@objectstack-fleet

Copy link
Copy Markdown
Contributor

Fixes #21644

Clause-②: no

A deployment-level flag is now written only by a full-scope run. os migrate value-shapes and os migrate files-to-references narrowed by --object apply their fixes, record no deployment flag, and say so. A full-scope --apply records the flag exactly as before. Across the family (value-shapes, files-to-references, summary-nulls, duplicates), an --object name the deployment does not declare is refused with OBJECT_NOT_FOUND before anything is read or written. The refusal names the unknown name and the declared objects. This follows triage ruling 5974774596. --apply --object is not refused.

Measured first (base 759dbe9ed3)

A1. The reach, at the public door (hypothesis confirmed)

A throwaway SQLite project held three objects, one of them os21644_site with a location field. A served-shape boot seeded one clean row per object. Then one off-shape value was written past the write path: the site's geo stored as {latitude, longitude}. The fresh-datastore attestation had recorded both ADR-0104 flags as verified at birth, so the flag table was emptied first.

  • os migrate value-shapes --json exited 1, with gatePassed: false and blocking: 1.
  • os migrate value-shapes --object os21644_sitee --apply --yes --json (misspelled) exited 0. It answered gatePassed: true with scannedObjects: [], and the adr-0104-value-shapes row read verified (verified_at set, blocking: 0).

A2. The census, one row per command

command --object --apply records a deployment flag where it is written (base) unknown --object on base (measured)
value-shapes repeatable adr-0104-value-shapes the CLI: recordDataMigrationRun at value-shapes.ts:221 exit 0, scannedObjects: []; with --apply, the flag is recorded verified
files-to-references repeatable adr-0104-file-references, then the column step's columns_moved_at the producer: runFilesToReferencesMigration at files-to-references-migration.ts:120; the column stamp is recordFileColumnMove in the CLI (files-to-references.ts:472) exit 0, both scans' scannedObjects: []; with --apply, the flag is recorded verified, and the column step moved os21644_product.image and stamped columns_moved_at
summary-nulls repeatable none (its header: "No deployment flag, deliberately") none exit 0, fields: [], on a dry run and on --apply
duplicates single none: no --apply, and it writes nothing none exit 0, scanned: [], filter: { object: 'os21644_sitee' }

A correctly spelled narrowed files-to-references --apply on base also recorded the flag verified and moved the column. Every scan draws its default candidates from the same registry: options.objects ?? Object.keys(engine.getConfigs()) in scanValueShapes, backfillFileReferences, verifyFileReferences and backfillSummaryNulls, and stack.allObjects() for collectScanTargets. Each keeps only the candidates it covers, which is where an undeclared name was dropped.

A3. The narrowed run

  • CLI-recorded flag (value-shapes). The flag write is skipped on a narrowed run, whether the run passes or fails. --json carries flag: null and filter: { objects } (null on a full-scope run, the shape duplicates already keeps). Both faces print one sentence: the run was narrowed, no deployment flag was recorded, and the command that records one is the same command without --object.
  • Producer-recorded flag (files-to-references). runFilesToReferencesMigration skips the write when it is given objects. This is the declared service-storage path only. Its flag result is null on a narrowed run. The CLI prints the same sentence and carries filter.
  • The column step (files-to-references) does not run on a narrowed run. The census row above is why. The step retypes every single-value media column in the database on the authority of the gate, and a narrowed gate vouches only for the named objects. Its stamp also requires a verified flag, which a narrowed run no longer records. Left running, a narrowed --apply would move columns and then fail to record the move. It now returns a stated skip, narrowed_run, and the human face says why.
  • What "narrowed" means. Any --object narrows, even a list that names every declared object. The flag is earned by the one spelling that means "every object", which is a run without --object. Treating a full list as full scope would need a second definition of "the whole deployment", checked against the registry of the moment, and that registry changes with the composition between two runs. The operator also gets one unambiguous prescription.
  • Deviation from the dispatch wording ("skips it when objects is non-empty"). The producer treats any objects as narrowed, [] included. A scan handed [] walks nothing ([] ?? … is []). A non-empty test would therefore record a verified flag over an empty scan, the card's own defect at the producer's API. A unit pin holds this.
  • The prompts and closing lines that promised a flag on a narrowed run now say it records none. ⛔ --apply --object is not refused, and the full-scope write is unchanged.

A4. Unknown --object

  • Checked against the registry the command's own boot resolved, before the scan. For value-shapes, files-to-references and summary-nulls that registry is Object.keys(engine.getConfigs()). For duplicates it is the names of stack.allObjects(). These are the same sets the scans draw from, so the refusal and the scan judge one population. There is no packages/objectql edit and no scanner edit.
  • The refusal is feat(cli): os migrate unmapped-columns reads a retired field's columns, keyed by record id, for conversion before the destructive drop #21643's. It is objectNotFoundError from @objectstack/core: code: 'OBJECT_NOT_FOUND', status: 404, and object naming the first unknown name. Its message names every unknown name and the declared objects, sorted. There is no new error code. value-shapes, files-to-references and summary-nulls answer { error, code }, as unmapped-columns does. duplicates keeps its own error shape, { error: 'report_failed', detail, code }: its catch now passes errorCodeFields through.
  • The list is the declared set, not the covered subset. Computing the covered subset for value-shapes needs isScannableValueShapeField, which @objectstack/objectql does not export, and that package is fenced. The declared set is also exactly the accept set. A declared object the command has nothing to check on is accepted, because an empty answer about a real object is true. On the fixture boot the list is 12 names, platform objects included.
  • Clause-②: no stands as the claim declared it. A misspelled name moves from exit 0 to exit 1, which is the ruled correction of a wrong answer. Every declared name and --apply --object are still accepted.

Changes

  • packages/cli/src/utils/migrate-object-scope.ts (new): refuseUndeclaredObjects, isNarrowedRun and narrowedFlagNote, shared by the four commands.
  • packages/cli/src/commands/migrate/value-shapes.ts: refuses an unknown name, skips the flag on a narrowed run, adds filter, and adjusts the narrowed prompt and closing lines.
  • packages/cli/src/commands/migrate/files-to-references.ts: refuses an unknown name, adds the narrowed_run column-step skip, adds filter, and adjusts the narrowed prompt and closing lines.
  • packages/cli/src/commands/migrate/summary-nulls.ts and duplicates.ts: refuse an unknown name. duplicates' error document carries the error's code.
  • packages/services/service-storage/src/files-to-references-migration.ts: skips the flag write when given objects.
  • content/docs/deployment/cli.mdx: one paragraph under "Data migrations" (--object narrows, an unknown name is refused, only a full-scope run records a flag), and the two --object example comments.
  • .changeset/21644-narrowed-apply-flag.md: @objectstack/cli patch and @objectstack/service-storage patch, Clause-②: no.

packages/objectql, packages/platform-objects, packages/spec, every other service-storage path, and content/docs/releases/ are untouched.

Pins

  • object-scope.integration.test.ts spawns the CLI against SQLite, one database copy per run, and is one enumeration over the census (FAMILY).
    • A narrowed --apply (value-shapes, files-to-references): exit 0, flag: null, filter: { objects }, no flag row, and the note on stderr naming the full-scope command.
    • A full-scope --apply: the flag recorded verified, in the document and in the row.
    • summary-nulls and duplicates: no flag row, narrowed or not, as before.
    • A narrowed --apply after an earned flag leaves that row byte-equal.
    • files-to-references narrowed: columnMove: null and columnsMovedAt: null. Its full-scope control moves os21644_product.image and stamps it.
    • Unknown --object, on all four: exit 1 and OBJECT_NOT_FOUND, naming the name and the declared objects. The one document is the refusal and no report, no flag row is written, and the app rows are unchanged. The human face exits 1 and names it.
    • The measured repro. Control: the full-scope scan sees blocking: 1 and exits 1. The misspelled --object --apply exits 1 with OBJECT_NOT_FOUND, and the flag stays unrecorded. Spelled right, the narrowed run finds the value, exits 1, and still records no flag.
  • migrate-object-scope.test.ts (unit): the envelope (code, status, object), every unknown name named once, the declared list sorted, the empty-registry message, the accepted cases, and what isNarrowedRun treats as narrowed (an empty list and a full list both narrow).
  • files-to-references-migration.test.ts (service-storage, beside the producer):
    • a narrowed apply converts and records no flag;
    • a narrowed failing apply records nothing;
    • a narrowed apply leaves an earned flag row equal;
    • objects: [] records nothing.

Reverse verification (implementation committed first; all three legs re-run at the final head fe988c20f0)

Each leg ran through node scripts/ablation-replace.mjs in wrap mode, under a script trap that restores from HEAD. The spawned CLI loads its commands from src/ through bin/run-dev.js. In the first round packages/cli/dist did not exist. In the final round it held a build of the unmutated source, and legs 1a and 2 still went red, which shows the spawned CLI read the mutated src/. The service-storage unit pin imports the producer from src/. Neither needed a rebuild.

  • Leg 1a, the narrowed-run skip in the CLI (value-shapes.ts):
    • The anchor if (apply && !narrowed) { became if (apply) {: anchor 1 to 0, replacement 0 to 1, blob 9f241dc2 to d3a5c236.
    • 3 red, 17 green. Red: the value-shapes narrowed pin, the earned-flag-unchanged pin, and the spelled-right repro. Green: both full-scope controls (the column-step control among them), the files-to-references narrowed pin (its skip is the producer's), and every unknown-name pin.
    • Restored: blob 9f241dc2 equals HEAD, and git diff HEAD is empty.
  • Leg 1b, the narrowed-run skip in the producer (files-to-references-migration.ts):
    • The same anchor and replacement: anchor 1 to 0, blob 1aa9fea2 to d4bb5002.
    • 4 red, 6 green. Red: all four narrowed pins (passing, failing, earned-flag-unchanged, and objects: []). Green: the six original pins, the full-scope apply among them.
    • Restored: blob 1aa9fea2 equals HEAD.
    • A first round is recorded here because one of its readings was vacuous. At 80e5eda6ea this leg read 3 red and 7 green: the earned-flag-unchanged pin stayed green under the mutation, because the fake engine's rewrite landed in the same millisecond as the earned row. The pin now dates the earned row in the past (fe988c20f0), and the re-run is the reading above.
  • Leg 2, the unknown-name refusal (migrate-object-scope.ts):
    • The anchor if (unknown.length === 0) return; became if (unknown.length >= 0) return;: anchor 1 to 0, replacement 0 to 1, blob b78a88b4 to 9286a990.
    • Unit: 3 red, 3 green. Red: the three refusal cases. Green: the accepted cases and the two isNarrowedRun cases.
    • Integration: 10 red, 10 green. Red: all eight unknown-name pins (two per command, on all four), the human face, and the misspelled repro. Green: every narrowed and full-scope pin, and the repro's control.
    • Restored: blob b78a88b4 equals HEAD.
    • In the first round, the duplicates "refused before anything was read" pin stayed green under this mutation: it asserted only on a key that report never carries. It now asserts that the one document is the refusal, which reds on all four commands.

After all legs, git diff HEAD was empty and git status --porcelain was clean.

Local verification (final head fe988c20f0, on base 759dbe9ed3)

origin/main was 759dbe9ed3 for the whole verification. Just before this PR opened, it gained four commits, f40bb3217f to 1a230548cf (#21649, #21632, #21648, #21650). None of them touches this diff's paths (packages/spec, metadata-protocol, service-automation, lint, skills and docs references), so they were not merged in. CI runs on the merge ref.

  • Builds. The CLI's dependency closure (turbo run build --filter=@objectstack/cli^...) gave VERDICT 0. @objectstack/service-storage was rebuilt after the producer change (exit 0), and @objectstack/cli was built (exit 0). A repo build for the gate prerequisites gave VERDICT 0 (turbo: 72 tasks, 71 cached).
  • @objectstack/cli typecheck (tsc --noEmit plus check:test-typecheck): exit 0 at 80e5eda6ea. No CLI file changed after that commit.
  • @objectstack/cli unit project in full at 80e5eda6ea:
    • 255 of 257 files passed, with 3742 tests passed and 29 skipped (the two files below).
    • The other two files, test/published-subpath-{console,hook-body}.pin.test.ts, refused before testing because packages/cli was not built (their own prerequisite message). After the CLI build, both passed: 2 files, 29 tests.
  • @objectstack/service-storage: typecheck exit 0, and the full suite at fe988c20f0 passed 41 files and 633 tests.
  • os migrate integration pins on built packages, at 80e5eda6ea:
    • this PR's pin plus the absent-database roster: 2 files, 59 passed;
    • the one-shot family plus duplicates.integration: 2 files, 79 passed.
  • Gates. node scripts/pm/dispatch-gates.mjs --commands --repo objectstack-ai/objectstack (no paths) derived 97 commands at fe988c20f0.
    • All 97 ran there, and each exited 0.
    • --ran with exit codes: 97 derived, 97 run, 0 NOT-MEASURED, 0 UNRUN.
    • An earlier round at 80e5eda6ea had three gates answer PREREQUISITE NOT MET (exit 3): check:skill-examples, check:dual-build-cjs-loads and check:i18n-coverage. They read packages outside the CLI closure. The repo build cleared them.
  • Full pnpm lint (eslint . --no-inline-config over the whole repo): exit 0 at fe988c20f0, with nothing printed.
  • No exported symbol was renamed or moved, so the liveness-ledger anchor check had nothing to read.

Acceptance notes

  • A narrowed run's counterexample is not recorded. The ruling says a narrowed --apply records no flag, so it records none even when it finds a violation. Such a counterexample is deployment-level evidence, since one off-shape value disproves "every value is on shape". The operator still gets exit 1 and the findings, and the next full-scope run closes the gate. This is an observation, not a filing. Carrier: none.
  • The declared list includes platform objects. It is 12 names on the fixture's lean boot, and a deployment that composes more plugins prints more. A long list in an error message is the price of naming the exact accept set. Carrier: none.
  • A narrowed value-shapes --apply still takes the plain (DDL-performing) boot even though it now writes nothing. That is unchanged, and the boot paragraph in the docs still describes it truthfully. Carrier: none.

Generated by Claude Code

claude added 4 commits October 4, 2026 00:44
…n unknown --object is refused

value-shapes and files-to-references recorded the deployment-level ADR-0104
flag from a run narrowed by --object, and every command in the family dropped
an undeclared --object name silently, so a typo scanned nothing and read as
clean. A run narrowed by --object now applies its fixes and records no flag
(the files-to-references producer skips it, and the deployment-wide column
step does not run); an undeclared name is refused with OBJECT_NOT_FOUND,
naming it and the declared objects, before anything is scanned.

Co-Authored-By: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016GiHYRmLSNWTfbX9gVQkpz
…ecords a flag; changeset

Co-Authored-By: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016GiHYRmLSNWTfbX9gVQkpz
…nt; the column-step control

Co-Authored-By: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016GiHYRmLSNWTfbX9gVQkpz
… rewrite cannot read as unchanged

Co-Authored-By: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016GiHYRmLSNWTfbX9gVQkpz
@github-actions github-actions Bot added size/l documentation Improvements or additions to documentation tests tooling labels Oct 4, 2026
@github-actions

github-actions Bot commented Oct 4, 2026

Copy link
Copy Markdown
Contributor

📓 Docs Drift Check

This PR changes 2 package(s): @objectstack/cli, @objectstack/service-storage, touching 19 documentable anchor(s).

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

  • content/docs/api/error-catalog.mdx (via os migrate summary-nulls (command, read off packages/cli/src/commands/migrate/summary-nulls.ts))
  • content/docs/deployment/cli.mdx (via os migrate duplicates (command, read off packages/cli/src/commands/migrate/duplicates.ts), os migrate files-to-references (command, read off packages/cli/src/commands/migrate/files-to-references.ts), os migrate summary-nulls (command, read off packages/cli/src/commands/migrate/summary-nulls.ts), os migrate value-shapes (command, read off packages/cli/src/commands/migrate/value-shapes.ts))
  • content/docs/deployment/seed-tenancy-repair.mdx (via os migrate duplicates (command, read off packages/cli/src/commands/migrate/duplicates.ts))
  • content/docs/protocol/objectql/types.mdx (via os migrate files-to-references (command, read off packages/cli/src/commands/migrate/files-to-references.ts), os migrate value-shapes (command, read off packages/cli/src/commands/migrate/value-shapes.ts))
  • content/docs/upgrading.mdx (via os migrate files-to-references (command, read off packages/cli/src/commands/migrate/files-to-references.ts), os migrate value-shapes (command, read off packages/cli/src/commands/migrate/value-shapes.ts))

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

  • content/docs/releases/v17/17-0.mdx (via os migrate files-to-references (command, read off packages/cli/src/commands/migrate/files-to-references.ts), os migrate value-shapes (command, read off packages/cli/src/commands/migrate/value-shapes.ts))
  • content/docs/releases/v17/17-1.mdx (via os migrate duplicates (command, read off packages/cli/src/commands/migrate/duplicates.ts))
  • content/docs/releases/v17/17-2.mdx (via os migrate duplicates (command, read off packages/cli/src/commands/migrate/duplicates.ts))
  • content/docs/releases/v17/17-3.mdx (via os migrate duplicates (command, read off packages/cli/src/commands/migrate/duplicates.ts))
  • content/docs/releases/v17/17-5.mdx (via os migrate files-to-references (command, read off packages/cli/src/commands/migrate/files-to-references.ts))
  • content/docs/releases/v17/index.mdx (via os migrate duplicates (command, read off packages/cli/src/commands/migrate/duplicates.ts))

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

What this run could not see
  • 2 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 — 31 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 1a230548cf6d1771489fdaf796cc7a146a05ac1d → packageMentionDocs.

Which tree this was computed on

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

node scripts/docs-audit/affected-docs.mjs --json 1a230548cf6d1771489fdaf796cc7a146a05ac1d

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

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/l tests tooling

Projects

None yet

2 participants