fix(cli): a narrowed os migrate --apply records no deployment flag, and an unknown --object is refused - #21662
Conversation
…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
📓 Docs Drift CheckThis PR changes 2 package(s): 5 hand-written doc(s) NAME something this change touched and may need an implementation-accuracy re-verification:
⛔ 6 release-owned page(s) also name something this change touched. These are read-only:
What this run could not see
Coarse fallback — 31 page(s) merely mention a changed package (the pre-#9192 predicate, kept for the deliberately-wide backstop): Which tree this was computed onThis run read A worktree cut from an older # 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
|
Fixes #21644
Clause-②: no
A deployment-level flag is now written only by a full-scope run.
os migrate value-shapesandos migrate files-to-referencesnarrowed by--objectapply their fixes, record no deployment flag, and say so. A full-scope--applyrecords the flag exactly as before. Across the family (value-shapes,files-to-references,summary-nulls,duplicates), an--objectname the deployment does not declare is refused withOBJECT_NOT_FOUNDbefore anything is read or written. The refusal names the unknown name and the declared objects. This follows triage ruling5974774596.--apply --objectis 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_sitewith alocationfield. A served-shape boot seeded one clean row per object. Then one off-shape value was written past the write path: the site'sgeostored 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 --jsonexited 1, withgatePassed: falseandblocking: 1.os migrate value-shapes --object os21644_sitee --apply --yes --json(misspelled) exited 0. It answeredgatePassed: truewithscannedObjects: [], and theadr-0104-value-shapesrow read verified (verified_atset,blocking: 0).A2. The census, one row per command
--object--applyrecords a deployment flag--objecton base (measured)value-shapesadr-0104-value-shapesrecordDataMigrationRunatvalue-shapes.ts:221scannedObjects: []; with--apply, the flag is recorded verifiedfiles-to-referencesadr-0104-file-references, then the column step'scolumns_moved_atrunFilesToReferencesMigrationatfiles-to-references-migration.ts:120; the column stamp isrecordFileColumnMovein the CLI (files-to-references.ts:472)scannedObjects: []; with--apply, the flag is recorded verified, and the column step movedos21644_product.imageand stampedcolumns_moved_atsummary-nullsfields: [], on a dry run and on--applyduplicates--apply, and it writes nothingscanned: [],filter: { object: 'os21644_sitee' }A correctly spelled narrowed
files-to-references --applyon 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())inscanValueShapes,backfillFileReferences,verifyFileReferencesandbackfillSummaryNulls, andstack.allObjects()forcollectScanTargets. Each keeps only the candidates it covers, which is where an undeclared name was dropped.A3. The narrowed run
value-shapes). The flag write is skipped on a narrowed run, whether the run passes or fails.--jsoncarriesflag: nullandfilter: { objects }(nullon a full-scope run, the shapeduplicatesalready 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.files-to-references).runFilesToReferencesMigrationskips the write when it is givenobjects. This is the declaredservice-storagepath only. Itsflagresult isnullon a narrowed run. The CLI prints the same sentence and carriesfilter.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--applywould move columns and then fail to record the move. It now returns a stated skip,narrowed_run, and the human face says why.--objectnarrows, 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.objectsis non-empty"). The producer treats anyobjectsas 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.--apply --objectis not refused, and the full-scope write is unchanged.A4. Unknown
--objectvalue-shapes,files-to-referencesandsummary-nullsthat registry isObject.keys(engine.getConfigs()). Forduplicatesit is the names ofstack.allObjects(). These are the same sets the scans draw from, so the refusal and the scan judge one population. There is nopackages/objectqledit and no scanner edit.objectNotFoundErrorfrom@objectstack/core:code: 'OBJECT_NOT_FOUND',status: 404, andobjectnaming 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-referencesandsummary-nullsanswer{ error, code }, asunmapped-columnsdoes.duplicateskeeps its own error shape,{ error: 'report_failed', detail, code }: its catch now passeserrorCodeFieldsthrough.value-shapesneedsisScannableValueShapeField, which@objectstack/objectqldoes 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.--apply --objectare still accepted.Changes
packages/cli/src/utils/migrate-object-scope.ts(new):refuseUndeclaredObjects,isNarrowedRunandnarrowedFlagNote, shared by the four commands.packages/cli/src/commands/migrate/value-shapes.ts: refuses an unknown name, skips the flag on a narrowed run, addsfilter, and adjusts the narrowed prompt and closing lines.packages/cli/src/commands/migrate/files-to-references.ts: refuses an unknown name, adds thenarrowed_runcolumn-step skip, addsfilter, and adjusts the narrowed prompt and closing lines.packages/cli/src/commands/migrate/summary-nulls.tsandduplicates.ts: refuse an unknown name.duplicates' error document carries the error'scode.packages/services/service-storage/src/files-to-references-migration.ts: skips the flag write when givenobjects.content/docs/deployment/cli.mdx: one paragraph under "Data migrations" (--objectnarrows, an unknown name is refused, only a full-scope run records a flag), and the two--objectexample comments..changeset/21644-narrowed-apply-flag.md:@objectstack/clipatch and@objectstack/service-storagepatch,Clause-②: no.packages/objectql,packages/platform-objects,packages/spec, every otherservice-storagepath, andcontent/docs/releases/are untouched.Pins
object-scope.integration.test.tsspawns the CLI against SQLite, one database copy per run, and is one enumeration over the census (FAMILY).--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.--apply: the flag recorded verified, in the document and in the row.summary-nullsandduplicates: no flag row, narrowed or not, as before.--applyafter an earned flag leaves that row byte-equal.files-to-referencesnarrowed:columnMove: nullandcolumnsMovedAt: null. Its full-scope control movesos21644_product.imageand stamps it.--object, on all four: exit 1 andOBJECT_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.blocking: 1and exits 1. The misspelled--object --applyexits 1 withOBJECT_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 whatisNarrowedRuntreats as narrowed (an empty list and a full list both narrow).files-to-references-migration.test.ts(service-storage, beside the producer):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.mjsin wrap mode, under a script trap that restores fromHEAD. The spawned CLI loads its commands fromsrc/throughbin/run-dev.js. In the first roundpackages/cli/distdid 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 mutatedsrc/. Theservice-storageunit pin imports the producer fromsrc/. Neither needed a rebuild.value-shapes.ts):if (apply && !narrowed) {becameif (apply) {: anchor 1 to 0, replacement 0 to 1, blob9f241dc2tod3a5c236.value-shapesnarrowed pin, the earned-flag-unchanged pin, and the spelled-right repro. Green: both full-scope controls (the column-step control among them), thefiles-to-referencesnarrowed pin (its skip is the producer's), and every unknown-name pin.9f241dc2equalsHEAD, andgit diff HEADis empty.files-to-references-migration.ts):1aa9fea2tod4bb5002.objects: []). Green: the six original pins, the full-scope apply among them.1aa9fea2equalsHEAD.80e5eda6eathis 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.migrate-object-scope.ts):if (unknown.length === 0) return;becameif (unknown.length >= 0) return;: anchor 1 to 0, replacement 0 to 1, blobb78a88b4to9286a990.isNarrowedRuncases.b78a88b4equalsHEAD.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 HEADwas empty andgit status --porcelainwas clean.Local verification (final head
fe988c20f0, on base759dbe9ed3)origin/mainwas759dbe9ed3for the whole verification. Just before this PR opened, it gained four commits,f40bb3217fto1a230548cf(#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.turbo run build --filter=@objectstack/cli^...) gave VERDICT 0.@objectstack/service-storagewas rebuilt after the producer change (exit 0), and@objectstack/cliwas built (exit 0). A repo build for the gate prerequisites gave VERDICT 0 (turbo: 72 tasks, 71 cached).@objectstack/clitypecheck (tsc --noEmitpluscheck:test-typecheck): exit 0 at80e5eda6ea. No CLI file changed after that commit.@objectstack/cliunit project in full at80e5eda6ea:test/published-subpath-{console,hook-body}.pin.test.ts, refused before testing becausepackages/cliwas 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 atfe988c20f0passed 41 files and 633 tests.os migrateintegration pins on built packages, at80e5eda6ea:duplicates.integration: 2 files, 79 passed.node scripts/pm/dispatch-gates.mjs --commands --repo objectstack-ai/objectstack(no paths) derived 97 commands atfe988c20f0.--ranwith exit codes: 97 derived, 97 run, 0 NOT-MEASURED, 0 UNRUN.80e5eda6eahad three gates answerPREREQUISITE NOT MET(exit 3):check:skill-examples,check:dual-build-cjs-loadsandcheck:i18n-coverage. They read packages outside the CLI closure. The repo build cleared them.pnpm lint(eslint . --no-inline-configover the whole repo): exit 0 atfe988c20f0, with nothing printed.Acceptance notes
--applyrecords 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.value-shapes --applystill 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