fix(cli): os lint surfaces ADR-0087 conversion notices — console notice + a conversions key in --json - #13741
fix(cli): os lint surfaces ADR-0087 conversion notices — console notice + a conversions key in --json#13741claude[bot] wants to merge 4 commits into
Conversation
… faces `os lint` called `normalizeStackInput` with no options object, so no `onConversionNotice` sink existed and the ADR-0087 D2 deprecation notices its own load path raised were never PRODUCED — not printed for a human, not published in `--json`. This is the #3782 parity class, not the "computed then dropped" family: nothing was discarded, the producer was never wired. `os lint` is the third of the three authoring commands the #4409 registry holds to one bar, and it was the only one telling an author nothing about a conversion it had just applied. A notice carries an expiry (`retiresIn`), so an author whose only authoring gate is `os lint` got no signal at all until the conversion retired and their metadata stopped loading. - Human face: print each notice after normalize, in `compile.ts`'s verbatim wording, so two commands over one tree say the same thing in the same words. - Machine face: publish the same `conversions` key `os validate --json` and `os build --json` publish, unconditionally present, `[]` when nothing converted — including on the catch-all exit, per the 2026-08-25 ruling that every failure exit carries what the run has already computed. - The notices are NOT folded into `issues`; `total`/`errors`/`warnings` keep counting exactly what they counted before. The folding question raised on #12125 is unsettled and this change had no authority to settle it. The source-level parity guard enumerated `compile.ts` and `validate.ts` only — a guard naming a subset of the class it describes reports green for the members it forgot, which is why this gap survived the `os build` repair. Its list is the class now. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01UngCYXF98BVpYA9hfz6NYk
…controls Positive controls, not just "a notice appears": every positive fixture drives the live `page-kind-jsx-to-html` conversion (ADR-0087 D2, protocol 11) and the notice is asserted by identity over the whole array, so "and no more" is asserted too. The negative control runs the identical stack with the canonical `kind: 'html'` and requires the field present and empty in both faces — without it, a hard-coded notice would satisfy every other assertion here. The `--json` key is checked on `Object.keys`, on the serialized bytes, and by value, because `JSON.stringify` drops an `undefined` value silently: a payload that spelled the key but never filled it would still read as "present" to a test that only inspected the parsed object. The catch-all-at-load pin holds the carrying-not-computing line: an exit above the normalize step must report `[]` honestly, so hoisting the call to make that exit look fuller goes red. The fold guard is named a REGRESSION GUARD, not red-before evidence — it is green in both states and takes no side on the open #12125 fold question. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01UngCYXF98BVpYA9hfz6NYk
…ning Grade proposed rather than chosen silently: minor, matching the two nearest precedents on this lane (#13347's code/httpStatus on the CLI --format json envelope, and the sibling conversions change on os validate / os build) rather than the bug/feature framing. Additive member on a published machine-readable surface; no existing key changes meaning and no count moves. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01UngCYXF98BVpYA9hfz6NYk
…-lint-conversion-notices
📓 Docs Drift CheckThis PR changes 1 package(s): 20 hand-written doc(s) name something this change touched — list omitted above 15 rows. Re-derive on the tree named below: ⛔ 4 release-owned page(s) also affected — read-only, see AGENTS.md Documentation Guardrails. What this run could not see
Coarse fallback — 23 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 65ed2efc5a430799cc49eeccb011150d8e0101a0 && git checkout 65ed2efc5a430799cc49eeccb011150d8e0101a0
# afterwards, rebuild it from the two parents, which stay fetchable
git fetch origin 787d757405db4f3ebbc6ca811948af4438a3fe7a b41c3b24fffb93fe62a38c4d32be0b7658c8d443 && git checkout -B drift-repro 787d757405db4f3ebbc6ca811948af4438a3fe7a && git merge --no-ff b41c3b24fffb93fe62a38c4d32be0b7658c8d443
node scripts/docs-audit/affected-docs.mjs --json 787d757405db4f3ebbc6ca811948af4438a3fe7a
|
Fixes #12297
os linttold an author nothing about an ADR-0087 D2 conversion its own load path had just applied — not in the console, not in--json. This wires the producer and publishes both faces.⭐ ZONE 1 — was the #12125 folding question RULED?
No. It was explicitly left unsettled, and I took triage's stated default. Read #12125's body and all four comments before the first edit. Three independent statements, all on that card:
should the two fields be folded?) that the ruling didn't address — route todomain:cliexecution seat as Clause-② yes rather than closing that sub-question here."conversionsthe same way', and nothing more. Whetherwarningsandconversionsshould become one field is unsettled, and this card does not have authority to settle it."recommendation: "WITHHELD by design — this seat was fenced from settling it and no measurement forced it, so picking a side here would be exactly the silent settlement the dispatch prohibited."#12125 closed
completedvia PR #12285, which shipped the unfolded shape —conversionsas its own key besidewarnings. So the question is open, and the shape that actually landed is the separate one.⇒ Branch taken: triage's default — the consistent shape. The same
conversionskeyos validateandos buildpublish, plus the human-face notice through anonConversionNoticesink, mirroring the #11772 repair incompile.ts. What is settled and does govern is the maintainer's 2026-08-25 ruling on #11772/#12047 ("every failure exit carries the lists the run has already computed"), which is why the caught-error exit carries the field too.What was wrong
The load path always converted; nothing ever said so.
Re-derived on
origin/main, with a positive control so the zero is not a blind instrument: the same pattern returns 14 oncompile.tsand 10 onvalidate.ts.normalizeStackInputlocated by symbol, not by the card's line number — it sits atlint.ts:506, not:505.⭐ This is the #3782 parity class, not the "computed then dropped" family. Nothing was computed and discarded: the call passed no options object at all, so no sink existed and notices were never produced. That is the exact gap
os buildwas in before #11772 / PR #12079 — I was wiring a producer, not recovering a value.Why it matters: a conversion notice is the one advisory class carrying an expiry.
retiresInnames the protocol major where the source stops loading, and five conversions are live (protocol 11 and 15). An author whose only authoring gate isos lintgot no signal at all until the conversion retired and their metadata stopped loading.A second finding, from re-deriving rather than trusting the card: the source-level parity guard in
validate-build-gate-parity.test.tslooped over['compile.ts', 'validate.ts']only — while its own docblock claims the bar "coversos linttoo". That is why the gap survived theos buildrepair: a guard enumerating a subset of the class it describes reports green for the members it forgot. Its list is the class now.What changed
os lint)⚠line per notice — path, from → to, conversion id, retiring protocol majoros lint --jsonconversions, unconditionally present,[]when nothing convertedos lint --json, caught error[]for a throw at loadConsole wording is
compile.ts's verbatim, so an author running two of the three commands over one tree is told the same thing in the same words. The sink is declared above thetryso the caught-error exit can read it, while thenormalizeStackInputcall stays exactly where it was — carrying, not computing.⛔ Not folded into
issues.total/errors/warnings/suggestionscount exactly what they counted before, measured on two runs differing only in the page kind. Folding would have been the very question #12125 left open.⛔
os validateandos buildare untouched — read-only references for the shape.Tests
Both controls, because "a notice appears" is worthless against a fixture that converts nothing.
page-kind-jsx-to-htmlconversion (ADR-0087 D2, protocol 11), asserted by identity (conversionId+ converted path) over the whole array, so "and no more" is asserted too. Measured payload carriesretiresIn: 12.kind: 'html'— field present and empty, in both faces. The console negative is its own test: nothing is printed when nothing converted.--jsonkey is asserted three ways — onObject.keys, on the serialized bytes, and by value — becauseJSON.stringifydrops anundefinedvalue silently, so a payload that spelled the key but never filled it would still read as "present" to a test inspecting only the parsed object.[]honestly, so hoisting the call to make it look fuller goes red.Fixed tree:
Test Files 2 passed (2) · Tests 14 passed (14),VERDICT command-exit 0.Ablation — direction predicted before running, and matched exactly
Predicted: reverting only
lint.tsto the merge base turns 5 red — 4 in the new pin file (key absent on the converting run, absent on the canonical run, console silent, caught-error key absent) and 1 in the parity guard — leaving 2 green in both states, the two the file pre-declares as regression guards. Predicted5 failed | 9 passed (14).Measured:
Tests 5 failed | 9 passed (14)— the exact 5 named, and the 2 green-in-both are exactly the two pre-declared.Mutation confirmed on disk before any result was read, anchored, both directions: sink
1 → 0, publish sites2 → 0, bare no-options call0 → 1, plus hash evidence (disk blob equals the base blob and differs from HEAD's; an empty hash was treated as failure). Restore ran undertrap … EXIT INT TERMwith an absolute path andgit checkout HEAD --, then was verified by observing state, not by an exit code: counts back to 1/2/0, disk blob equals the HEAD blob,git diff HEADempty,git statusclean.No rebuild sits on the ablation path:
bin/run-dev.jsstates in its own header that it is the SOURCE entry point runningsrc/through tsx, solint.tsis measured from source. Its dependency@objectstack/spec, which owns the conversion, resolves todist/and is untouched — and was built green.Gates
Union derived after merging
origin/mainand after the last edit, withnode scripts/pm/dispatch-gates.mjs --repo objectstack-ai/objectstack; stderr read first and confirms commitb41c3b24f, 4 paths, three-dot semantics. Harvested with--commands(not grepped — the two spellings would have dropped 15 of 34). All runs on the final commit.36 commands run, all green, including the convention-triggered section — the paragraph path derivation cannot name — which fired on "adds or edits a test file":
check:query-options-erasure,check:type-check-coverage,check:type-check-debt,check:engine-double-contract,check:cross-package-test-inputs,check:where-matcher.Quoted verdict lines, from each gate's own output:
check-type-check-coverage --re-measure: OK — 29 ledger entr(ies) re-measured in 248.4s, 1542 raw tsc error(s) total, none above its recorded number. surplus: none✓ where-matcher conformance holds: 319 matcher(s) discovered, 319 answer the combinator battery correctly or refuse it loudlycheck-i18n-coverage: OK (12 config(s), 602 baselined untranslated string(s), none new)— ⭐ this gate drives the builtos lint, so it independently confirms the change does not disturb lint's output contractcheck-i18n-bundles: OK (9 package(s) — all bundles in sync, no undeclared authoring keys)✓ check:dual-build-cjs-loads — 102 published require entry point(s) across 66 package(s) loadESLint, repo-wide and NOT narrowed:
pnpm lint=eslint . --no-inline-config,VERDICT command-exit 0. No narrowing is claimed anywhere in this PR.Refusals — reported separately, ⛔ never folded into the green list
Three gates first returned PREREQUISITE NOT MET, which is NOT MEASURED, not a red:
check:i18ncheck:i18n-coveragecheck:dual-build-cjs-loadsNote the first two exit 1, the code a finding uses — they are refusals only because the gate's own text says so, which is why the verdict line is read and never a bare status.
One gate remains NOT MEASURED and cannot be measured locally:
check-test-completeness— exit 3, "PREREQUISITE NOT MET … grades a savedturbo run testlog, and no log was named". Its own text says: "There is no local log to hand it, so the local reading for this gate is NOT MEASURED. ⛔ It is not a red, and there is nothing here to fix." CI supplies the log.Exit codes were captured before any pipe throughout (redirect, then
EXIT=$?), per these gates' own warnings that| headturns evenPIPESTATUS/pipefailgreen.One honest NOT MEASURED in the typecheck
pnpm --filter @objectstack/cli typecheckexits 0 — but--listFilesshows the program reads zero of my two test files (src/commands/lint.ts: 1 hit; both test files: 0).packages/cli/tsconfig.jsonscopesincludetosrc, sotest/sits outside every tsc program. That is a pre-existing ledgered condition — the ratchet itself reports "17 package(s) still hide their own tests from tsc" andcheck:type-check-coveragepasses — and the siblingvalidate-json-failure-conversions.e2e.test.tssits in the same position, so this adds no new gap. Stated rather than left to read as coverage.Changeset — grade proposed, not chosen silently
minor, matching the two nearest precedents on this lane rather than the bug/feature framing: #13347 (addingcode/httpStatusto the CLI's--format jsonenvelope) and the siblingconversionschange onos validate/os build(#12125) were both gradedminoras additive members on a published machine-readable surface. Triage graded this card aBugon declared-not-enforced grounds; the two readings are not in conflict — restoring a parity contract can still widen a wire surface, and the grade follows the surface. Nothing is breaking, so no ADR-0087 disposition marker applies (check-adr-0087-registrationgreen). Happy to be re-graded.Publishing a
conversionskey inos lint --jsonadds a member to a published machine-readable surface, so the PM claim declared Clause-② yes and this needs an at-tier contract reviewer.Generated by Claude Code