Repository navigation
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
|
…bjectstack-ai#21677) Fixes objectstack-ai#21588 Clause-②: no Three sentences in two published skills describe the date-bucket engine, and each one named something the runtime does not do. This PR rewrites exactly those three sentences, read arm by arm from the driver code on `origin/main` at `55e6f14f8d`, and nothing else in either file. | File | Line | Before | After | |:--|:--|:--|:--| | `skills/objectstack-ui/rules/dashboards.md` | 321 | `'week'` row: "ISO date of the bucket (`YYYY-MM-DD`)" | "ISO week `YYYY-Www`" | | `skills/objectstack-ui/rules/dashboards.md` | 326–328 | "Postgres `date_trunc`, MySQL `date_format`, SQLite `strftime`, MongoDB `$dateTrunc`, in-memory fallback. All emitted by the analytics service, not the client." | "drivers emit the bucket as a label (`2026-01`), not an instant: Postgres `to_char`, MySQL `date_format`, SQLite `strftime`, MongoDB `$dateToString`, in-memory `bucketDateKey`." | | `skills/objectstack-query/rules/aggregation.md` | 82–85 | "pushes bucketing down to the driver (`DATE_TRUNC` etc.)" | "pushes bucketing down to the driver (`to_char` / `date_format` / `strftime` / `$dateToString`, never `date_trunc`)" — the push-down / in-memory-fallback clause and "**including the column keys**" are kept as they were | The ruling on the card (triage comment 5969875145) fixed the engine sentence and said no other sentence in `dashboards.md` moves; the engine seat's carrier addition (5973137901) measured the `'week'` row and the aggregation sentence as the same family, and the dispatching seat folded all three into this one governed PR (claim comment 5975736098). ## Reading 1 — what each arm emits, read from the code Every arm answers a string label, never a truncated instant; the week label is the ISO week `YYYY-Www` on all five. | Arm | Where | Expression emitted | Example keys | |:--|:--|:--|:--| | PostgreSQL | `packages/drivers/driver-sql/src/sql-driver.ts:6161–6169` (`buildDateBucketExpr`) | `to_char((col)::timestamptz AT TIME ZONE 'UTC', FORMAT)` for a datetime column, `to_char((col)::date::timestamp, FORMAT)` for a `Field.date`; formats `YYYY`, `YYYY-MM`, `YYYY-MM-DD`, `YYYY"-Q"Q`, `IYYY"-W"IW` | `2026-01`, `2026-Q1`, `2026-W23` | | MySQL | `sql-driver.ts:6172–6180` | `date_format(convert_tz(col, @@session.time_zone, '+00:00'), FORMAT)` (bare `col` for a `Field.date`); `%Y`, `%Y-%m`, `%Y-%m-%d`, `%x-W%v`; quarter is `concat(date_format(…, '%Y'), '-Q', quarter(…))` | `2026-01`, `2026-W23` | | SQLite | `sql-driver.ts:6183–6208` | `strftime(FORMAT, ARG)` with `%Y`, `%Y-%m`, `%Y-%m-%d`; quarter from `%Y` and `(%m - 1) / 3 + 1`; week by the Thursday rule, `strftime('%Y', ARG, '-3 days', 'weekday 4') \|\| '-W' \|\| printf('%02d', (cast(strftime('%j', ARG, '-3 days', 'weekday 4') as integer) - 1) / 7 + 1)` (PR objectstack-ai#21629, merged, on `origin/main`) | `2026-01`, `2026-W23` | | MongoDB | `packages/drivers/driver-mongodb/src/mongodb-aggregation.ts:246–275` | `{ $dateToString: { format, date: { $convert: { input: '$FIELD', to: 'date', onError: null, onNull: null } } } }` with `%Y`, `%Y-%m`, `%Y-%m-%d`, `%G-W%V`; quarter is `$concat` of `%Y`, `-Q` and a `$switch` over `%m`. The docblock at `:194` is headed "Labels, not instants — and therefore no `$dateTrunc`" | `2026-01`, `2026-W23` | | In-memory | `packages/core/src/utils/datetime.ts:313` `bucketDateKey` (week via `isoWeekLabelFromCalendarDay`, `:389`); the engine's fallback `packages/objectql/src/in-memory-aggregation.ts:375` delegates to it, and `packages/objectql/src/engine.ts:17615` picks push-down vs fallback from `supports.queryDateGranularity` | string keys `YEAR`, `YEAR-MM`, `YEAR-MM-DD`, `YEAR-Qn`, `ISOYEAR-Www` built from the calendar parts in the reference zone — the writer the drivers' expressions are held equal to (`checkDateBucketParity`) | `2026-01`, `2026-W23` | The rendered dashboard label is the key: `packages/services/service-analytics/src/dimension-labels.ts:321–333` `formatDateBucket` returns a key the writer wrote at that granularity as written (`bucketKeyToCalendarRange(value, granularity) !== null`), and `src/__tests__/dataset-granularity-postprocess.test.ts:66` pins `week: '2026-W29'` through `queryDataset`. So the `'week'` row's "ISO date of the bucket (`YYYY-MM-DD`)" was wrong on every face, and the engine sentence named two expressions no arm emits. ## Reading 2 — token ratchet and line count, before / after `node scripts/check-skills-token-ratchet.mjs` (ceil(utf8 bytes / 4)), measured on the worktree before the edit and at `e381bcd9e1` after it: | File | Tokens before | Tokens after | Bytes | Lines | |:--|:--|:--|:--|:--| | `skills/objectstack-ui/rules/dashboards.md` | `6243 tokens (ceiling 6252; headroom 9)` | `6243 tokens (ceiling 6252; headroom 9)` | 24970 → 24969 | 468 → 468 | | `skills/objectstack-query/rules/aggregation.md` | `1845 tokens (ceiling 2357; headroom 512)` | `1860 tokens (ceiling 2357; headroom 497)` | 7378 → 7437 | 241 → 241 | Per sentence: the `'week'` row 52 → 34 bytes; the engine sentence 185 → 202 bytes over the same three lines; the aggregation sentence 244 → 303 bytes over the same four lines. Net for `dashboards.md`: 0 tokens, 0 lines, −1 byte. No ceiling moved. ## Gates All runs at `e381bcd9e1` (the branch's only commit); each runner log records the sha it started at. - `node scripts/pm/dispatch-gates.mjs --commands --repo objectstack-ai/objectstack` derives 23 commands from the change set (2 paths vs merge base `55e6f14f8`). All 23 exit 0. The 8 `node scripts/check-*.mjs` runs plus `check:doc-formula-expressions`, `check:agent-test-spelling` and `check:corpus-claim-drift` ran under `scripts/pm/os-verify-lock.sh` (VERDICT command-exit 0, held 120s). The remaining 12 `pnpm check:*` scanners ran unlocked — a declared narrowing: the lock's own `--status` text places `check:*` gate scripts outside its coverage, and two consecutive lock calls answered queue-timeout (exit 99, 360s each) behind a holder at 13+ minutes. - `check:doc-formula-expressions` first answered exit 3 PREREQUISITE NOT MET (`@objectstack/formula` / `@objectstack/lint` not built — nothing measured). The production closure (`spec`, `types`, `core`, `client`, `client-react`, `formula`, `sdui-parser`, `lint`) was built under the lock (VERDICT command-exit 0, held 133s; `git status` clean afterwards), and the re-run exits 0: "22 record-scoped formula example(s) across 460 files / 1381 TS blocks judged clean by @objectstack/formula". - `pnpm --filter @objectstack/spec run check:skill-docs` exits 0: "✅ Skill docs in sync". - `pnpm --filter @objectstack/spec run check:skill-examples` exits 0 (run unlocked after a third queue-timeout, same declared narrowing): "✅ 260 prose examples type-check across 3 surface(s) — every marked block parsed, so tsc ran the SEMANTIC pass on all of them". The diff sits outside every fence (dashboards.md fences close at 316 and reopen at 354; aggregation.md's close at 72 and reopen at 95), so no example changed. - `node scripts/pm/dispatch-gates.mjs --repo objectstack-ai/objectstack --ran` over the 23 commands with their exit codes: "23 derived famil(ies) accounted for — 23 run, 0 NOT-MEASURED (a DERIVED zero — all 23 recorded an exit code and none of them is 3)". The tool warns the tree is 3 commits behind `origin/main` (`417443eb27`, fetched after the runs); re-deriving against that merge-base yields the same 23 commands, and the three incoming commits (objectstack-ai#21662, objectstack-ai#21653, objectstack-ai#21661) touch no `skills/**` path, so the branch was not merged forward for a two-file documentation change. - `pnpm lint` narrowing, with the three pieces of evidence: ① `eslint.config.mjs` `files:` globs cover only `{ts,tsx,mts,cts,js,jsx,mjs,cjs}` (lines 971–1238), so neither `.md` file is in the population; ② `pnpm exec eslint --no-inline-config --format json` over the two files answers 2 files, 0 errors, 1 warning each — "File ignored because no matching configuration was supplied."; ③ the config enables no `parserOptions.project` or typed rules (line 328), so this diff moves no untouched file's verdict. - No package is touched, so no build closure (①) and no package test / typecheck (②) is owed; `check:nul-bytes` is among the 23. ## 维护者速读(草稿) **改了什么** — 两个已发布技能里描述日期分桶引擎的三句话。dashboards 规则的「Engine support」句原说 Postgres 用 `date_trunc`、MongoDB 用 `$dateTrunc`、由 analytics service 发出;改为按五个臂点名驱动真实发出的表达式(Postgres `to_char`、MySQL `date_format`、SQLite `strftime`、MongoDB `$dateToString`、内存 `bucketDateKey`),并写明桶键是标签(如 `2026-01`)不是时刻。同一张表的 `'week'` 行由「ISO date of the bucket (`YYYY-MM-DD`)」改为 ISO 周标签 `YYYY-Www`。aggregation 规则的下推句把 `DATE_TRUNC` 换成同一表达式族。不改任何代码,不改产品行为。 **为什么改** — 技能是 AI 作者读的权威面。写错引擎会让作者按 `date_trunc` 语义(时间戳形的桶键)去比较或解析桶值,而运行时五个臂实际都返回字符串标签;`'week'` 行与运行时每个臂返回的 `2026-W29` 不符。每个臂都在 `origin/main`(`55e6f14f8d`)上逐条读过代码,见上文 Reading 1。 **风险与代价(含回滚)** — 纯文档改动,8 行替换 8 行。token 棘轮:dashboards.md 净 0(6243/6252 不变,行数不变),aggregation.md +15(1860/2357)。23 条派生门禁加 `check:skill-docs`、`check:skill-examples` 全绿。回滚 = revert 本 PR 的单个 commit。 **席位意见** — (留空) **你要做的** — 对这个 Tier H `skills/**` PR 给一次 APPROVED review;之后由 `domain:skills#1` 席位落地。 ## Acceptance notes Noted, not filed (code comments, no behaviour, no carrier): - `packages/services/service-analytics/src/dimension-labels.ts:302` — the `formatDateBucket` TSDoc example list still reads `week → "2026-04-13" (ISO date of the bucket)`, while the body just below (`:327–:333`) returns a `YYYY-Www` key as written and relabels only a raw non-key value as its own day key. Comment drift only; the behaviour is pinned by the tests cited above. - `packages/objectql/src/in-memory-aggregation.ts:59` — the header comment names `date_trunc(...)` as the SQL path's NULL-propagating expression; the SQL path emits `to_char` / `date_format` / `strftime`, whose NULL propagation is the same point. Comment drift only. --- _Generated by [Claude Code](https://claude.ai/code/session_01CB6W87z22K2yjUCDyVrJRk)_ Co-authored-by: Claude <noreply@anthropic.com>
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