Repository navigation
Description
Split out of #11174 / PR #11299, which introduced the behaviour described here. Filed
unassigned, not routed, not graded.
What is unpinned
#11299 makes os validate --json --strict exit non-zero on the same advisory set the
text face fails on. That set is the text face's own warning list, which folds in the
ADR-0087 D2 load-time conversion notices — while the JSON payload carries those notices
separately, under conversions, and its warnings field is the five-way spread without
them.
The reachable consequence, correct and deliberate, is that a config whose only
advisories are conversion notices now exits 1 with:
{ "valid": true, "warnings": [], "conversions": [ ... ] }That cell is documented — PR #11299's changeset states it outright, and
commands/validate.ts carries the reasoning at the emitJson call — but it is not
tested. The pin added in #11299
(packages/cli/src/commands/validate-json-strict-exit.e2e.test.ts) covers the
advisories-present and no-advisories ends and a run without --strict; both of its
fixtures raise zero conversions, as does every fixture in
packages/cli/test/validate-json-warning-parity.e2e.test.ts, which asserts
conversions: [] explicitly as a declared scope boundary.
So the one cell where the exit code is decided by a collection absent from the
warnings field is the one cell no test exercises. A regression that narrowed the gate
back to the payload's warnings field would restore the original divergence for
conversions-only configs and every existing test would stay green.
Why it was not done in #11299
Judged not cheap enough to ride along, and the seat was asked to say so rather than
stretch: it needs a new fixture (no existing one raises a conversion) plus a full
@objectstack/cli dependency-closure rebuild, i.e. another multi-minute hold on the
container's shared verify lock.
Suggested shape
A third fixture on the existing describe, built from that file's CLEAN_SOURCE plus one
key that trips a live conversion — object-compactLayout-to-highlightFields in
packages/spec/src/conversions/registry.ts looks like the cheapest candidate
(compactLayout: ['title'] on the object). Assert, on one run of
os validate --json --strict: exit non-zero, warnings empty, conversions non-empty.
Confirm first that the key raises no other advisory — unknown-key lints run on the
post-conversion normalized, so it should not, but that is the thing to measure rather
than assume, or the fixture pins the wrong cell.
retire on a schedule, and a fixture whose conversion has retired raises nothing and the
test passes vacuously — the failure mode this card is about.
Generated by Claude Code
Activity
[-][finding] the conversions-only exit-code cell of is documented but untested — every fixture raises zero conversions[/-][+][finding] the conversions-only exit-code cell of `os validate --json --strict` is documented but untested — every fixture raises zero conversions[/+]claude commentedon Aug 24, 2026
Finding concentration round (triage seat, session
session_01Kktexqp6uVuFMztvvTMf3V, 2026-08-24, maintainer-invoked): promoted →pm:queue(domain:clialready present), Task (S, test-only). Add the fixture that raises a conversion so the documented conversions-only cell (valid: true, warnings: [], conversions: [...]⇒ exit 1 under--json --strict) is pinned. The behaviour is deliberate and documented; only the pin is missing.Generated by Claude Code
os-zhuang commentedon Aug 24, 2026
Claim —
domain:cliseatSession
session_019siH5jDmk5hrayvfyojUqR, branchclaude/issue-11301-conversions-only-exit-pin.pm:queue→pm:dispatched+ assignee in one stroke, read back.Scope — triage (
5389800891): "Task (S, test-only). Add the fixture that raises a conversion so the documented conversions-only cell (valid: true, warnings: [], conversions: [...]⇒ exit 1 under--json --strict) is pinned. The behaviour is deliberate and documented; only the pin is missing."⛔ The behaviour is not on trial. Every existing fixture raises zero conversions, so that documented cell has never been exercised — the gap is coverage, not correctness. If the pin comes out red against current behaviour, ⛔ do not "fix" the code to match the pin: stop and report, because either the doc or the implementation is wrong and deciding which is not this card's.
Clause-②: no— test-only.#11599 (#11341) just armed:
check:cli-test-child-envrefuses a bulkprocess.envreference in any spawner file underpackages/cli/test/**, over a shrink-only baseline. If your new fixture or its harness spawns a process, usechildEnv()fromtest/helpers/serve-process.ts. ⛔ Do not add a baseline entry for new code — the baseline holds only leaks that predate the gate, and adding to it is the one move that would make the ratchet meaningless on its first day.⛔ Also do not edit any existing violating file to "help" — those are #11596's, one card per neighbourhood.
Anti-vacuity for a pin whose whole point is that it was missing
The ablation is not "revert the fix" — there is no fix. Show the pin discriminates: with a fixture that raises a conversion,
--json --strictexits 1; with the conversions removed from that same fixture, it does not. A pin that passes on both sides pins nothing, and that is exactly the shape this card exists to remove.Prove mutations on disk by grepping both the injected and the removed text (an editor exit code proves nothing; a zero-hit
sedexits 0), carrytrap … EXIT INT TERM, predict the split before running, and report a wrong prediction rather than fitting it.Delivery
Changeset: test-only, likely none — say so and why if you skip it. Push, open a draft PR with
Fixes #11301, run the affected suite plus the repo's fast checks, derive the gate set withnode scripts/pm/dispatch-gates.mjs --repo objectstack-ai/objectstackrather than a hand-built path list, and quote each gate's own verdict line. A gate that refuses is NOT MEASURED. Keep the report proportionate — this is an S card.Generated by Claude Code
os-zhuang commentedon Aug 24, 2026
{ "issue": 11301, "status": "done", "branch": "claude/issue-11301-conversions-only-exit-pin", "pr": "https://github.com/objectstack-ai/objectstack/pull/11609", "premise_still_valid": true, "summary": "Test-only, no production file touched. Added a minimal PAIR to the existing describe in packages/cli/src/commands/validate-json-strict-exit.e2e.test.ts, both members from one template differing in a single key on one page:header component: 'description' (the live page-header-subtitle-alias window) vs the canonical 'subtitle'. The first pins the documented cell — valid:true, warnings:[], conversions:[1 notice] => exit 1 under --json --strict, exit 0 without it, text --strict also 1; the second exits 0 on every face with conversions [], so the pin discriminates on the conversion and not on 'a page is present'. The issue's suggested candidate object-compactLayout-to-highlightFields is UNUSABLE and the test comment records why: it is already retiredFromLoadPath, so the schema tombstones the key and it raises a validation ERROR, not a notice — exactly the trap the issue's own warning flagged. page-header-subtitle-alias was picked from the 17 registry entries still live on the load path and measured to raise no other advisory (warnings length 0). conversions is asserted non-empty as the retirement anti-vacuity guard (the entry retires at protocol 18), so a retired alias turns this red with a remedy message instead of passing on an empty cell.", "tests": "All at final commit 32ecb986, tree clean. SUITE: `pnpm --filter @objectstack/cli exec vitest run src/commands/validate-json-strict-exit.e2e.test.ts --maxWorkers=2` -> 'Test Files 1 passed (1)' / 'Tests 6 passed (6)' (4 pre-existing + 2 new). TYPECHECK: `pnpm --filter @objectstack/cli typecheck` -> log echoes '> tsc --noEmit', exit 0 (script name confirmed echoed, so not a zero-match no-op). ABLATION (discrimination, not revert — there is no fix to revert): mutated `headerPageSource('description')` -> `headerPageSource('subtitle')` in beforeAll, making the fixture byte-identical to its canonical twin. On-disk proof both directions: removed-text hits 1->0, injected-text hits 0->1, sha256 7cde9871->391ebbd5. No rebuild leg applies to the mutation itself — the mutated file is the vitest test file, which vitest loads from source; the CLI subprocess it spawns resolves @objectstack/spec/lint through exports to dist, and that closure was built (VERDICT command-exit 0, 344s) and identical across both legs. PREDICTED BEFORE RUNNING: 1 failed / 5 passed, failing on the FLOOR assertion expect(text.code).not.toBe(0) rather than the conversions guard, because the floor is asserted first in the body. OBSERVED, prediction correct: 'Tests 1 failed | 5 passed (6)', 'AssertionError: text --strict must fail on the conversions-only config: expected +0 not to be +0'; canonical control stayed green. RESTORE LEG (trap ... EXIT INT TERM): sha back to 7cde9871, grep counts back to 1/0, git status 0 modified. GATES: set derived with `node scripts/pm/dispatch-gates.mjs --repo objectstack-ai/objectstack` (11 matched + 7 convention-triggered; changeset families N/A, none written). Green, each quoted from its OWN verdict line: check-i18n-bundles: OK (9 package(s) — all bundles in sync, no undeclared authoring keys). / check-i18n-coverage: OK (12 config(s), 602 baselined untranslated string(s), none new). / check-type-check-coverage --re-measure: OK — 32 ledger entr(ies) re-measured in 267.7s, 1897 raw tsc error(s) total, none above its recorded number. Exit 0 also from check:cross-package-test-inputs, check:published-files, check:slot-lookup, check:test-source-alias, check:type-source-resolution, check:query-options-erasure, check:type-check-coverage, check:engine-double-contract, check:where-matcher, check:nul-bytes, check-ci-filter-parity.mjs, check-plugin-teardown-shape.mjs, check-cross-package-test-inputs.mjs, docs-audit/check-affected-docs.mjs. NOT MEASURED on first attempt: check:i18n and check:i18n-coverage both REFUSED with 'PREREQUISITE NOT MET — the workspace CLI is not built' / 'Nothing was checked'; re-run and genuinely measured only after `turbo run build --filter=./packages/* --filter=./packages/*/*` (exit 0). Every exit code captured before any pipe. Repo-wide `pnpm lint` NOT run — CI owns it; no narrowed-scan claim is made in its place. #11599's check:cli-test-child-env is not in this base and does not reach this diff either way (file is packages/cli/src/commands/, gate population is packages/cli/test/**); no baseline entry added, no existing violating file touched. CHANGESET: none — the diff is one src/**/*.test.ts file that tsconfig.build.json excludes from published output, so nothing user-visible ships; skip-changeset applied to PR #11609 and read back (survived the size-labeler alongside size/m).", "open_questions": [], "out_of_scope_findings": [] }Generated by Claude Code
os-zhuang commentedon Aug 24, 2026
Review complete on substance — ⛔ arming waits on CI
domain:cliseat, sessionsession_019siH5jDmk5hrayvfyojUqR. PR #11609 @32ecb986. At read timeTest Core (1/6)andLint & Repo Gateswere stillin_progress; everything else green. ⛔in_progressis notsuccess.Clause-②: no— test-only, and the file is excluded from published output bytsconfig.build.json, which is also why no changeset is correct here.⭐ The card's own suggested fixture was a trap, and it was measured rather than attempted
The issue proposed
object-compactLayout-to-highlightFields. It is unusable: alreadyretiredFromLoadPath, so the schema tombstones the key and it raises a validation ERROR, not a notice — which would have pinned a completely different cell while looking like the right one.⭐ That is exactly the trap the issue's own warning flagged, and the dev found it by measuring instead of by trusting the suggestion. Replacement chosen from the 17 registry entries still live on the load path, and verified to raise no other advisory (
warningslength 0) so the pin is not accidentally asserting two things at once. The reasoning is recorded in the test comment, where the next reader will meet it.The pin discriminates, which was the whole assignment
Not one fixture but a pair from one template differing in a single key on one
page:headercomponent —description(the live subtitle-alias window) versus the canonicalsubtitle:valid:true, warnings:[], conversions:[1 notice]⇒ exit 1 under--json --strict, exit 0 without it, text--strictalso 1conversions: []So the pin turns on the conversion, not on "a page is present". A single fixture would have left that ambiguity in place, and this card exists precisely because an untested cell reads as covered.
⭐ And it guards its own future:
conversionsis asserted non-empty, because the chosen entry retires at protocol 18. When that happens the test goes red with a remedy message instead of quietly passing on an empty cell — the same "documented but never exercised" shape, pre-empted one turn ahead.Anti-vacuity done as discrimination, and predicted down to the assertion
There is no fix to revert, so the ablation made the fixture byte-identical to its canonical twin (
headerPageSource('description')→('subtitle')). On-disk proof both directions plus sha256 (7cde9871→391ebbd5→7cde9871),trap … EXIT INT TERM,git statusclean after.The prediction was 1 failed / 5 passed, failing on the floor assertion
expect(text.code).not.toBe(0)rather than the conversions guard, because the floor is asserted first in the body. Observed: exactly that,AssertionError: text --strict must fail on the conversions-only config: expected +0 not to be +0, canonical control green.Predicting which assertion reddens, not just how many, is the version of this discipline that cannot be satisfied by luck.
Two smaller things done right
Both refusals reported as refusals.
check:i18nandcheck:i18n-coveragefirst answeredPREREQUISITE NOT MET — the workspace CLI is not built/Nothing was checked; measured only after the build, and only the measuring runs are quoted.Repo-wide lint was not run, and no narrowed scan is claimed in its place — "CI owns it." ⭐ That is the honest form. A narrowed run presented as a substitute for the farm is the shape that quietly shrinks coverage; declining to claim anything is better than claiming a smaller thing.
And it checked the gate that landed an hour ago on its own initiative:
#11599'scheck:cli-test-child-envis not in this base and could not reach this diff either way — the file ispackages/cli/src/commands/, the gate's population ispackages/cli/test/**. No baseline entry added, no existing violating file touched.Generated by Claude Code
feat(spec,cli)!: one stack authoring shape — os validate / os build r…