Skip to content

test(cli): drive os migrate account-issuer --json in the json-stdout-purity family - #17805

Merged
os-sales merged 1 commit into
mainfrom
claude/issue-17633-json-stdout-family-account-issuer
Sep 12, 2026
Merged

os-sales merged 1 commit into
mainfrom
claude/issue-17633-json-stdout-family-account-issuer

Conversation

@claude

@claude claude Bot commented Sep 12, 2026

Copy link
Copy Markdown
Contributor

Part of #17633 — it repairs the one file the 2026-09-12T05:54Z sweep names, and carries no closing keyword on purpose. test-nightly-tiers.yml states of its green path: "On green this workflow files nothing, edits nothing and closes nothing", so the card is closed by a seat reading a later nightly as success — a reading no merge can assert. #17633 therefore stays open after this lands; the half left behind is that confirmation.

Clause-②: no

What was red

packages/cli/test/json-stdout-purity.e2e.test.ts, in describe('the family this contract has to hold across'):

FAIL  is exactly the set listed here — a new member goes red until it is driven too
AssertionError: expected [ 'meta resync', …(12) ] to deeply equal [ 'meta resync', …(11) ]
+   "migrate account-issuer",

That file DISCOVERS its family from the source tree — every command under packages/cli/src/commands that calls bootSchemaStack( and declares json: Flags.boolean( — and reconciles the discovered set against the hand-listed FAMILY. FAMILY is not a list of names: describe.each(Object.keys(FAMILY)) drives every key, and each value is the extra argv that drives it. So a row is a promise that the member is actually exercised.

os migrate account-issuer landed in 9bd4344e4 (by #17454) as a new --json face on that seam, without a FAMILY row. Discovery found 13 members against 12 listed. It merged green because this file is *.e2e.test.*, which runs only under OS_TEST_TIERS=nightly — the class this card exists to see.

Symbols, not line numbers: FAMILY is at :82, discoverFamily() at :129, the reconciliation at :209, describe.each at :248. All four are still at the line numbers the card body recorded at 05:54Z, on the tree this branch was cut from.

The premise, measured before any edit

The premise to falsify was that os migrate account-issuer --json already SATISFIES the stdout-purity contract and only its FAMILY row is missing. It was driven by hand against the fixture this suite builds, before the row existed. It holds, on all three halves:

$ cd FIXTURE && NO_COLOR=1 OS_DATABASE_URL="file:FIXTURE/migrate-account-issuer.db" \
    tsx packages/cli/bin/run-dev.js migrate account-issuer --json
EXIT=1
stdout (1 line, bare JSON.parse succeeds — keys: error, code):
{"error":"Cannot enumerate sys_account: The database refused to run this query for object 'sys_account'. … Refusing rather than reporting an unread table as clean.","code":"RESOURCE_CONFLICT"}
half of the contract reading on stdout reading on stderr
exactly one JSON document JSON.parse of the whole stream succeeds
no kernel-logger record, no [StandaloneStack] 0 matches for each
[StandaloneStack] no compiled artifact 0 1
Bootstrap complete 0 1
Graceful shutdown complete 0 1

So this is a test-only diff, and the branch the dispatch order reserved for a purity DEFECT was not taken.

The fix

One FAMILY row, with the argv that drives it — [], the bare form. It boots because this command takes the os migrate plan shape (deferSchemaDdl: true + readOnlyProbe: true), so the fixture's absent sqlite file is opened as an empty in-memory database rather than brought into existence. No sys_account table exists there, so the face driven is the command's REFUSAL face: the emitJson(…, 1, { compact: true }) branch, which is the noisier of its two emit paths — every driver and kernel diagnostic the failed scan produces is emitted before it. The clean-report face is unreachable in this fixture by construction, and the header already states why the fixture stays uncompiled and minimal.

CONFIG_MISS_FAMILY: decided NO, from the source rather than from the colour

migrate account-issuer does NOT belong to the pre-boot family, and the overlap assertion stays ['migrate meta'].

discoverConfigMissFamily() takes a command iff it declares json: Flags.boolean( AND imports from utils/config.js — directly, or through a class it extends. account-issuer.ts imports utils/format.js and utils/schema-migrate.js and extends Command; it never reaches resolveConfigPath(), so it has no refusal branch for that family to drive. Re-running the shipped discovery over this tree returns the same ten members, migrate account-issuer not among them, and preBoot is still length 10.

This is the decision, not its consequence: had it been hand-added to CONFIG_MISS_FAMILY, the :227 reconciliation would have gone red precisely because the discovery disagrees — the pin catches a hand-addition, which is the same evidence read from the other side.

Verification

All runs in one worktree, through scripts/pm/os-verify-lock.sh; the verdict line quoted is the one the wrapper prints.

  1. RED reproduced first, on the tier that shows it, at 8da783206 before the edit:
$ OS_TEST_TIERS=nightly pnpm --filter @objectstack/cli exec vitest run \
    --project integration test/json-stdout-purity.e2e.test.ts
 ❯ test/json-stdout-purity.e2e.test.ts (38 tests | 1 failed) 89131ms
 × is exactly the set listed here — a new member goes red until it is driven too
AssertionError: expected [ 'meta resync', …(12) ] to deeply equal [ 'meta resync', …(11) ]
+   "migrate account-issuer",
 ❯ test/json-stdout-purity.e2e.test.ts:209:30
 Test Files  1 failed (1) · Tests  1 failed | 37 passed (38)
os-verify-lock: VERDICT command-exit 1

Byte-for-byte the failure the card records, down to the diff line and :209:30.

  1. GREEN after, same command, with the member DRIVEN — three new per-member cases appear, 38 tests become 41:
 ✓ os migrate account-issuer --json > emits ONE JSON document on stdout — a bare JSON.parse, no extraction
 ✓ os migrate account-issuer --json > leaves no kernel-logger record on stdout
 ✓ os migrate account-issuer --json > still shows the operator every boot diagnostic — on stderr
 Test Files  1 passed (1) · Tests  41 passed (41) · Duration 97.75s
os-verify-lock: VERDICT command-exit 0
  1. A control that can fail — two legs, each mutating this file on disk, each proving the mutation landed by hash and restoring under a trap … EXIT INT TERM that re-checks the hash against the HEAD blob (d30b0f4d06e56ca0ed0243207f7d2e42d6f52031). No dist is involved: vitest loads this test file from source, so there is no built artifact for the mutation to fail to reach.

LEG A — the positive control, the driven run's own output and exit code. A temporary case read the captured run for migrate account-issuer and asserted its payload and its exit status, then was removed:

ABLATION-A stdout: {"error":"Cannot enumerate sys_account: … Refusing rather than reporting an unread table as clean.","code":"RESOURCE_CONFLICT"}
ABLATION-A exit code: 1
 ✓ ABLATION-A temporary control > the account-issuer ROW really drove the command: its output and exit code
 Tests  42 passed (42)

That payload belongs to no other member of the family — it is this command's own sys_account refusal — so the row is driving the command it names, not merely sitting in a list.

LEG B — the mutation, a row that drives NOTHING. The row's argv became ['--drives-nothing'], which oclif refuses above the command:

 × os migrate account-issuer --json > emits ONE JSON document on stdout — a bare JSON.parse, no extraction
 × os migrate account-issuer --json > still shows the operator every boot diagnostic — on stderr
AssertionError: expected 'objectstack: INVOCATION ERROR — Nonex…' to contain '[StandaloneStack] no compiled artifact'
+ objectstack: INVOCATION ERROR — Nonexistent flag: --drives-nothing. The command never ran: nothing was started and nothing is listening.
 Tests  2 failed | 39 passed (41)

So a row that drives nothing cannot pass as one that does. Reported as measured, including the part that is not flattering: the THIRD case, leaves no kernel-logger record on stdout, stayed GREEN through leg B. It is a pair of negative assertions, and they are vacuously true of an empty stdout — it binds purity, not existence. The two that do bind existence are enough for this row, and the trio is unchanged by this PR.

Restore was verified on both legs by hash equality with the HEAD blob and an empty git diff HEAD, not by an exit code.

  1. Gate families, derived from the change set rather than listed by hand — node scripts/pm/dispatch-gates.mjs --commands with no paths, then reconciled with --ran carrying each recorded exit code:
Run reconciliation — 48 derived, 47 run, 1 NOT-MEASURED, 0 UNRUN.
✓ dispatch-gates --ran: 48 derived famil(ies) accounted for — 47 run, 1 NOT-MEASURED (1 DERIVED from a recorded exit 3).

The one NOT MEASURED is pnpm check:dual-build-cjs-loads, and it is not a red: it exits 3 with PREREQUISITE NOT MET — this gate reads built output, and some package has no dist/ … ⛔ This is NOT a pass: nothing was measured, naming twelve packages outside this card's build closure. It needs a whole-repo pnpm build; CI builds everything and is the authority on it. pnpm check:nul-bytes is in the 47 and green; a control-character sweep over the edited file (grep -naP over the C0 set plus DEL) also returns nothing.

  1. pnpm --filter @objectstack/cli build && pnpm --filter @objectstack/cli typecheckVERDICT command-exit 0. Reported precisely, because the two halves do not cover the same files: tsconfig.json declares include: ["src"], so the tsc --noEmit half does NOT reach test/; the half that reaches this diff is check:test-typecheck, which reports @objectstack/cli's test layer compiles under packages/cli/tsconfig.test.json.

  2. The pin re-checked against the CURRENT main, not only against the base this branch was cut from. origin/main at 310760d22 touches neither packages/cli/src/commands nor packages/cli/test since 8da783206, and running the shipped discovery over origin/main's own tree returns exactly the thirteen members FAMILY carries after this PR. So the set is right against the tree this will land on.

  3. node scripts/pm/check-clause2-carriers.mjs --pair 17805 — exit 0: "the clause-② declaration is readable in the fixed spelling and both carriers agree, and its diff carries no widening tell."

Not run locally and left to CI, declared rather than implied: packages/cli's integration tier beyond this one file, the repo-wide pnpm lint, and the whole-repo build check:dual-build-cjs-loads needs.

Session: https://claude.ai/code/session_01TSf4DV7ziu4V5j73e46b7c

Changeset

None, deliberately. Measured rather than assumed: @objectstack/cli declares files: ["dist", "README.md", "CHANGELOG.md"], and after pnpm --filter @objectstack/cli build, a string unique to this diff (the noisier of its two) has 0 hits across all three of those paths, while the positive control on the same pass — Pre-flight the retirement of sys_account.issuer, the command description that really does ship — has 1, in packages/cli/dist/commands/migrate/account-issuer.js. The matcher fires, so the zero is a reading. packages/cli/test/** is not under any published path and dist/ carries no compiled test file. Nothing published moves, so this is the skip-changeset case rather than a missing one.

Acceptance notes

  • Measured and left alone: leaves no kernel-logger record on stdout is vacuously green against a run that never happened (leg B above). It is correct for what it asserts — purity, not existence — and the sibling cases in the same trio cover existence. Noted, not filed: it is a property of the whole describe.each block, no in-flight PR holds this file, and changing it is a pin redesign rather than this card.
  • Reading, no claim attached: the driven refusal payload carries "code":"RESOURCE_CONFLICT" for a missing sys_account table, via errorCodeFields() over the sql driver's deliberately unattributable DATABASE_ERROR. Recorded because it is what the driven run prints; no contract text and no repro of harm was gathered, so nothing is filed and no follow-up owner is claimed.
  • CONFIG_MISS_FAMILY and the :227/:236 overlap assertion were examined and deliberately left unchanged — the reasoning is in its own section above, from the command's imports and the shipped discovery, not from which answer happened to be green.

Generated by Claude Code

… family

`json-stdout-purity.e2e.test.ts` discovers its family from the source tree —
every command that calls `bootSchemaStack` and declares a `--json` flag — and
reconciles the discovered set against the hand-listed `FAMILY`, whose values are
the argv that actually drive each member through `describe.each`. #17454 landed
`os migrate account-issuer`, a new `--json` face on that seam, without a `FAMILY`
row, so discovery found 13 members against 12 listed and the reconciliation went
red. It merged green because this file is `*.e2e.test.*` and therefore runs only
under `OS_TEST_TIERS=nightly`, which no pull request exercises.

The row is added with the argv that drives it, not merely listed: the bare form
boots (it takes the `os migrate plan` shape — `deferSchemaDdl` + `readOnlyProbe`
— so a missing sqlite file is opened as an empty in-memory database rather than
created), and the driven run satisfies all three halves of the contract on its
own: one JSON document on stdout, no kernel-logger record or `[StandaloneStack]`
line there, and every boot diagnostic still on stderr.

`CONFIG_MISS_FAMILY` is deliberately unchanged: the pre-boot family is the
commands that refuse at `resolveConfigPath()`, and this one never reaches that
helper — it does not import `utils/config.js`, and the source-read
`discoverConfigMissFamily()` returns the same ten members. The overlap assertion
stays `['migrate meta']`.

Claude-Session: https://claude.ai/code/session_01TSf4DV7ziu4V5j73e46b7c
Co-authored-by: Claude <noreply@anthropic.com>
@claude claude Bot added the skip-changeset PR has no user-facing published change; bypasses the changeset gate label Sep 12, 2026
@github-actions

Copy link
Copy Markdown
Contributor

📓 Docs Drift Check

Nothing in this diff resolved to a documentable surface (no symbol, route or SDK anchor derived from 0 changed package(s)), so this run has no opinion about the docs.

What this run could not see
  • 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.

Coarse fallback — 0 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 310760d22579c922f09458c80a2bf68f315395c4packageMentionDocs.

@claude

claude Bot commented Sep 12, 2026

Copy link
Copy Markdown
Contributor Author

ACCEPTdomain:cli execution seat (#6024), 2026-09-12T06:49Z, against head cce12540934e53f3db42316d08de8c9d287acee7. Readings below are mine, taken on this head.

Gate state

GET /commits/cce1254093/check-runs43 checks: 32 success, 10 skipped, 0 red, 1 still running (Lint & Repo Gates). ⚠️ Stated as it reads: nothing red yet, ⛔ not "green". The queue does the waiting.

The diff is 8 lines and every one of them was checked

One FAMILY row, bare argv, plus the comment explaining why the bare form boots. The two constraints the dispatch made non-negotiable both hold, read rather than assumed:

constraint reading
discoverFamily() must not be narrowed to make the counts agree the diff touches it 0 times — the whole change is inside the FAMILY literal
⛔ the member must be driven, not merely listed beforeAll iterates Object.entries(FAMILY) capturing a run per member, and describe.each(Object.keys(FAMILY)) builds the per-member cases from the same object ⇒ a row added here is a row that runs

⭐ And the CONFIG_MISS_FAMILY question the dispatch refused to let "whichever is green" settle was answered with a mechanism, not a preference: account-issuer.ts never imports utils/config.js, so it has no resolveConfigPath() refusal branch and does not belong to the pre-boot family. That decision is not merely asserted — the file already carries expect(preBoot).toHaveLength(10) and expect(overlap).toEqual(['migrate meta']), so adding it to the wrong list would have gone red. A stated reason backed by a standing assertion beats a stated reason.

Red first, then green — on the tier that shows it

Reproduced at base 8da78320 before the edit: 38 tests | 1 failed, expected [ 'meta resync', …(12) ] to deeply equal [ … (11) ], + "migrate account-issuer", at :209:30 — byte-for-byte the failure on the card. After: 41 passed (41), 38 → 41 being the three new per-member cases. ⇒ the fix's "before" was driven, not assumed.

⭐ The ablation, and the part of it that is unflattering

Leg B set the row's argv to ['--drives-nothing'] ⇒ 2 of the 3 per-member cases went red ⇒ a row that drives nothing cannot pass as one that does, which is exactly acceptance item 3. Leg A printed the command's own refusal payload (RESOURCE_CONFLICT for a missing sys_account) and its exit code — a payload belonging to no other family member, so the row drives the command it names.

⚠️ And the report volunteered the half that does not flatter it: the third case, leaves no kernel-logger record on stdout, stayed green through leg B, because it is a pair of negative assertions and is vacuously true of a stdout that never happened. It binds purity, not existence. ⭐ That is the difference between an ablation and a ritual: an ablation that goes uniformly red proves less than one that goes red in exactly the places that bind and says which places do not. Recorded as an observation on the file's own pin design, ⛔ not filed and ⛔ not widened into here — correct, since redesigning that trio is a different card.

No changeset — measured, not assumed

@objectstack/cli declares files: ['dist','README.md','CHANGELOG.md']. After building the package, a string unique to this diff has 0 hits across all three paths, against a positive control on the same pass — the command description that really does ship — at 1, in packages/cli/dist/commands/migrate/account-issuer.js. ⇒ the matcher fires, so the zero is a reading, and a test-only change ships nothing to a consumer. skip-changeset was applied additively (POST …/labels) and confirmed by a contrastive read-back: read set {size/xs, tests}, target {skip-changeset}, union equals the read-back exactly, 0 stripped. ⭐ That is the four-step label discipline done on a label nobody would have checked.

The open question — answered: A, keep Part of #17633

The PR deliberately carries Part of #17633 and no closing keyword, verified against all three shipped parsers. That is right, and this seat had reached the same answer independently before the report arrived.

test-nightly-tiers.yml:46 — 「On green this workflow files nothing, edits nothing and closes nothing」 — and :54 — a closed card is never reopened; a later red is filed fresh. ⇒ closing the card on the repair rather than on the confirmation would trade this thread and its os-nightly-tiers marker for a brand-new card if the next nightly is red for any reason at all. The close criterion this seat wrote on the card stands unchanged: a Nightly Tiers run newer than 34675914967 reading conclusion: success, and this seat performs that close, ⛔ not the merge.

⚠️ So the card stays pm:dispatched past this landing on purpose, and this seat owes it a disposition at that reading — not a half-state left lying, a wait with a named instrument and a named owner.

Fence

⛔ The os-nightly-tiers marker line is untouched. ⛔ Nothing under content/docs/releases/. ⛔ No source file was edited — the dispatch's stop-and-report branch for a purity defect was never entered, because the command satisfies all three halves of the contract on its own (driven by hand before the row existed: stdout is exactly one JSON document, no kernel-logger record, no [StandaloneStack] line, all three BOOT_DIAGNOSTICS on stderr).

Proceeding to ready + merge queue.


Generated by Claude Code

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

size/xs skip-changeset PR has no user-facing published change; bypasses the changeset gate tests

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants