Skip to content

fix(cli): os lint surfaces ADR-0087 conversion notices — console notice + a conversions key in --json - #13741

Draft
claude[bot] wants to merge 4 commits into
mainfrom
claude/issue-12297-os-lint-conversion-notices
Draft

fix(cli): os lint surfaces ADR-0087 conversion notices — console notice + a conversions key in --json#13741
claude[bot] wants to merge 4 commits into
mainfrom
claude/issue-12297-os-lint-conversion-notices

Conversation

@claude

@claude claude Bot commented Aug 31, 2026

Copy link
Copy Markdown
Contributor

Fixes #12297

os lint told 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:

  1. Triage (2026-08-25): "the filer flags a live sub-question (should the two fields be folded?) that the ruling didn't address — route to domain:cli execution seat as Clause-② yes rather than closing that sub-question here."
  2. The dispatch's Zone 2: "The ruling covers 'carry conversions the same way', and nothing more. Whether warnings and conversions should become one field is unsettled, and this card does not have authority to settle it."
  3. The implementer's report, 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 completed via PR #12285, which shipped the unfolded shape — conversions as its own key beside warnings. 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 conversions key os validate and os build publish, plus the human-face notice through an onConversionNotice sink, mirroring the #11772 repair in compile.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.

grep -cE 'onConversionNotice|conversions' packages/cli/src/commands/lint.ts
0

Re-derived on origin/main, with a positive control so the zero is not a blind instrument: the same pattern returns 14 on compile.ts and 10 on validate.ts. normalizeStackInput located by symbol, not by the card's line number — it sits at lint.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 build was 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. retiresIn names the protocol major where the source stops loading, and five conversions are live (protocol 11 and 15). An author whose only authoring gate is os lint got 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.ts looped over ['compile.ts', 'validate.ts'] only — while its own docblock claims the bar "covers os lint too". That is why the gap survived the os build repair: 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

face before after
console (os lint) nothing one line per notice — path, from → to, conversion id, retiring protocol major
os lint --json no such key conversions, unconditionally present, [] when nothing converted
os lint --json, caught error no such key what the run had computed — [] for a throw at load

Console 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 the try so the caught-error exit can read it, while the normalizeStackInput call stays exactly where it was — carrying, not computing.

Not folded into issues. total / errors / warnings / suggestions count 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 validate and os build are untouched — read-only references for the shape.

Tests

Both controls, because "a notice appears" is worthless against a fixture that converts nothing.

  • Positive control: every positive fixture drives the live page-kind-jsx-to-html conversion (ADR-0087 D2, protocol 11), asserted by identity (conversionId + converted path) over the whole array, so "and no more" is asserted too. Measured payload carries retiresIn: 12.
  • Negative control: the identical stack with the canonical kind: 'html' — field present and empty, in both faces. The console negative is its own test: nothing is printed when nothing converted.
  • The --json key is asserted three ways — on Object.keys, on the serialized bytes, and by value — because JSON.stringify drops an undefined value 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.
  • Caught-error-at-load pin holds the carrying-not-computing line: an exit above the normalize step reports [] honestly, so hoisting the call to make it look fuller goes red.
  • The fold pin is named a REGRESSION GUARD — green in both states, ⛔ never red-before evidence — and takes no side on the open question.

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.ts to 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. Predicted 5 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 sites 2 → 0, bare no-options call 0 → 1, plus hash evidence (disk blob equals the base blob and differs from HEAD's; an empty hash was treated as failure). Restore ran under trap … EXIT INT TERM with an absolute path and git 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 HEAD empty, git status clean.

No rebuild sits on the ablation path: bin/run-dev.js states in its own header that it is the SOURCE entry point running src/ through tsx, so lint.ts is measured from source. Its dependency @objectstack/spec, which owns the conversion, resolves to dist/ and is untouched — and was built green.

Gates

Union derived after merging origin/main and after the last edit, with node scripts/pm/dispatch-gates.mjs --repo objectstack-ai/objectstack; stderr read first and confirms commit b41c3b24f, 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 loudly
  • check-i18n-coverage: OK (12 config(s), 602 baselined untranslated string(s), none new) — ⭐ this gate drives the built os lint, so it independently confirms the change does not disturb lint's output contract
  • check-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) load

ESLint, 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:

gate first result resolution
check:i18n exit 1 — "the workspace CLI is not built" built the closure, re-ran green
check:i18n-coverage exit 1 — "the workspace CLI is not built" built the closure, re-ran green
check:dual-build-cjs-loads exit 3 — "some package has no dist/" built the closure, re-ran green

Note 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 saved turbo run test log, 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 | head turns even PIPESTATUS/pipefail green.

One honest NOT MEASURED in the typecheck

pnpm --filter @objectstack/cli typecheck exits 0 — but --listFiles shows the program reads zero of my two test files (src/commands/lint.ts: 1 hit; both test files: 0). packages/cli/tsconfig.json scopes include to src, so test/ 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" and check:type-check-coverage passes — and the sibling validate-json-failure-conversions.e2e.test.ts sits 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 (adding code/httpStatus to the CLI's --format json envelope) and the sibling conversions change on os validate/os build (#12125) were both graded minor as additive members on a published machine-readable surface. Triage graded this card a Bug on 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-registration green). Happy to be re-graded.

⚠️ CLAUSE ② — this PR is PARKED by design

Publishing a conversions key in os lint --json adds a member to a published machine-readable surface, so the PM claim declared Clause-② yes and this needs an at-tier contract reviewer.

  • ⛔ No contract-review gate self-cleared, ⛔ auto-merge not armed, ⛔ not flipped ready.
  • ⛔ Scope was not narrowed to the console face to duck the gate — that would be routing around it. Both faces are built, as ZONE 1 selects.

Generated by Claude Code

claude added 4 commits August 31, 2026 07:58
… 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
@github-actions

Copy link
Copy Markdown
Contributor

📓 Docs Drift Check

This PR changes 1 package(s): @objectstack/cli, touching 1 documentable anchor(s).

20 hand-written doc(s) name something this change touched — list omitted above 15 rows. Re-derive on the tree named below: node scripts/docs-audit/affected-docs.mjs --json 787d757405db4f3ebbc6ca811948af4438a3fe7a.

4 release-owned page(s) also affected — read-only, see AGENTS.md Documentation Guardrails.

What this run could not see
  • 1 name(s) were too generic to anchor anything (single lowercase words)
  • 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 — 23 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 787d757405db4f3ebbc6ca811948af4438a3fe7apackageMentionDocs.

Which tree this was computed on

This run read content/docs from 65ed2efc5a430799cc49eeccb011150d8e0101a0 — the merge of head b41c3b24fffb93fe62a38c4d32be0b7658c8d443 into base 787d757405db4f3ebbc6ca811948af4438a3fe7a, which is what actions/checkout gives a pull_request run. Not the PR head.

A worktree cut from an older main holds a different content/docs, so re-deriving there can legitimately return a different list — that is a different tree, not a wrong row. To answer on the same tree:

# 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

⚠️ That checkout carried uncommitted changes, so the commit above does not fully identify what was read.

Advisory only, and a precision-first one (#9192): a page is listed because it names a
symbol, wire route or SDK method this diff touched — not because it mentions a changed
package. Each row says which anchor put it there, so a wrong row is reportable rather than
merely annoying. To re-verify, run the docs-accuracy-audit workflow scoped to these files:
node scripts/docs-audit/affected-docs.mjs 787d757405db4f3ebbc6ca811948af4438a3fe7a → pass the list as
args.docs, on the commit named under Which tree this was computed on.

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

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

os lint never surfaces ADR-0087 conversion notices — it normalizes with no onConversionNotice sink, the #3782 parity gap os build was in

2 participants