refactor(cli): render the ADR-0087 conversion notice from one source, and gate it there - #15096

Merged
os-trump merged 3 commits into
mainfrom
claude/issue-13743-conversion-notice-wording-parity
Sep 3, 2026
Merged

refactor(cli): render the ADR-0087 conversion notice from one source, and gate it there#15096
os-trump merged 3 commits into
mainfrom
claude/issue-13743-conversion-notice-wording-parity

Conversation

@os-trump

Copy link
Copy Markdown
Collaborator

Fixes#13743

The ADR-0087 D2 conversion notice now has one source in packages/cli, and the parity
guard holds it there. Output is byte-for-byte unchanged.

Which of triage's three options, and why

Option 1 — extract a shared formatter — carrying the smallest useful half of option 2.
Triage required this to be stated rather than defaulted, and the deciding input was a
measurement rather than a preference.

Option 3's supporting argument is falsified. Its case was that validate.ts folds
its copy into a warnings list --strict judges while the other two print directly, so
"the three call sites are not fully interchangeable, and forcing an extraction may need a
parameter for the difference." The first clause is true and I verified it; the second does
not follow. The asymmetry is entirely in the disposition of the string, never in the
string:

commandwhat it does with the sentencethe sentence itself
os build (compile.ts)printWarning(...) behind !flags.jsonidentical
os lint (lint.ts)printWarning(...) behind !flags.jsonidentical
os validate (validate.ts)warnings.push(...), which --strict then judgesidentical

Measured at the branch point d17f352b1b: extracting the template literal from each of the
three lines and hashing it gives one distinct value, 124 bytes, sha256 prefix
7197b905060762c3. A pure formatConversionNotice(notice) therefore serves all three and
costs no parameter for the difference — the extraction option's stated price does not
materialise.

Why not option 2 alone. A comparator locks today's three copies together and detects a
reword only after someone has written it; one source makes the divergence impossible to
write. It is also the disposition this file already reached for the same problem:
printTruncationNotice in the same module carries the note "ONE implementation of that
sentence, deliberately … nine copies of the wording would be nine chances for them to drift
apart." House precedent, not a new idea.

What option 2 still buys, and is kept. Extraction does not stop a copy coming back.
The rule added to validate-build-gate-parity.test.ts is therefore structural rather than
comparative: every authoring command renders through the one formatter, and none spells the
sentence out inline.

Re-derived the count — and the probe returned more than three

The release comment required a bound with a control that could have returned a fourth. It
did. A whole-repo git grep (no pathspec) for the prose fragment converted at load
tuned to the sentence, not to the three known sites — finds a rendering the card does not
mention, in packages/spec/src/stack.zod.ts:

packages/cli/src/commands/compile.ts:238 the three the card counts
packages/cli/src/commands/lint.ts:580
packages/cli/src/commands/validate.ts:409
packages/spec/src/stack.zod.ts:2310 NOT in the card — defineStack's own face

and reading that seam turns up a fifth prose form on ConversionNotice.message
(packages/spec/src/conversions/apply.ts), which is what the --json payloads publish.

The card's "three" is correct as a count of the three authoring commands, and wrong as
a bound on the wording.
Four renderings, three distinct sentences. The two in
packages/spec are fenced read-only on this card and cannot import the CLI's formatter
anyway (@objectstack/cli depends on @objectstack/spec, not the reverse), so they are
filed separately as #15095 and pinned here as a recorded fact rather than left for the next
reader to discover and "fix" in one command only.

The change

  • packages/cli/src/utils/format.tsformatConversionNotice(notice), a formatter and
    deliberately not a printer.
  • compile.ts / lint.ts / validate.ts — render through it; the three inline literals
    are gone (grep -c for the prose in each command source: 0, 0, 0).
  • packages/cli/test/validate-build-gate-parity.test.ts — the rule the old guard could not
    see, with a positive control that fails if the formatter stops carrying the sentence.
  • packages/cli/src/utils/format.conversion-notice.test.ts — the value pin on the rendered
    sentence, plus the recorded form-1 / form-3 split.
  • .changeset/cli-conversion-notice-one-source.mdpatch. Measured from this package's
    own files field: @objectstack/cli publishes dist, compiled from the edited src, so
    the diff changes the published package even though it changes nothing an author can
    observe. skip-changeset would have been wrong.

Red-first — four arms, each naming a different assertion

Every mutation was committed-first, proved on disk by blob hash, and restored by blob-hash
equality against the HEAD blob under trap ... EXIT INT TERM on absolute paths. The guard
is a source-text scanner, so no build or dist step participates in any arm.

armmutationexpectedmeasured
Areword lint.ts's copy, dropping the shared formatterred on the "renders through ONE formatter" assertionvitest exit 1lint.ts must render its ADR-0087 D2 conversion notices with formatConversionNotice(); 1 failed / 8 passed
Bkeep the call, add a verbatim inline copy beside itred on the "no inline copy" assertionvitest exit 1lint.ts spells the ADR-0087 D2 conversion notice out inline; 1 failed / 8 passed
Creword the ONE source in format.tsred on the positive control AND on the value pinvitest exit 1 — control fires, and the pin reports converted while loading where converted at load was expected; 2 failed / 10 passed
Dunmutated treegreenvitest exit 0 — 2 files, 12 tests passed

Blob evidence, arm by arm: lint.ts HEAD blob 43ace89d…; mutated to 82ef231d… (A) and
a25e93f7… (B); restored to 43ace89d… with git diff HEAD empty each time. format.ts
HEAD blob 53d51fe1…; mutated to 80839a7e… (C); restored to 53d51fe1…. Final
git status --porcelain -- packages/cli was empty.

Arm A is precisely the check the dispatch asked for — reword one copy and show the guard
redden — and arms B and C exist because A alone would not have told you which assertion
fired.

Verification

At final HEAD 9fc66faf on a clean tree, after merging origin/main (which brought in
PR #15062, the armed sibling the dispatch warned about, plus four others):

  • pnpm --filter @objectstack/cli typecheck — exit 0. Both halves: tsc --noEmit over
    src (which is where the new value pin lives, so it is type-checked), and
    check:test-typecheck over tsconfig.test.json, whose verdict line reads
    OK — @objectstack/cli's test layer compiles … 3 file(s) / 28 error(s) / 6 pinned signature(s) held — the pre-existing ledger, unmoved.
  • Targeted vitest, 6 files / 53 tests passed: the two new/edited guards, the three
    conversion-notice e2e suites (lint-conversion-notices,
    validate-json-failure-conversions, build-json-failure-conversions — these drive the
    real CLI over a live conversion via tsx, so they are the behavioural proof the output
    did not move), and i18n-check-help-surface-parity from the merged sibling PR.
  • Gate union derived by node scripts/pm/dispatch-gates.mjs --repo objectstack-ai/objectstack
    at that HEAD: 42 families (33 by path, 6 by change kind, 5 declared whole-tree).
    40 green. Two are NOT MEASURED and say so themselves: check-test-completeness.mjs
    grades a saved turbo run test log this box cannot produce ("the local reading for this
    gate is NOT MEASURED"), and check-half-states.mjs needs repo-scoped GitHub REST, which
    this container is refused (GET /repos/... returns 403 with no rate-limit headers while
    GET /rate_limit reports 15000 left). Five others first exited 3 on an unbuilt closure
    and went green after a full pnpm build: check:dual-build-cjs-loads, check:i18n,
    check:i18n-coverage, check:type-check-debt (which had also hit a local 300s cap), and
    the two named above stayed 3 for the reasons given.

Declared narrowing. The repo-wide pnpm lint sweep was not run locally; CI owns it and
runs it once regardless. Everything else in the derived union was run.

What this deliberately does not do

ConversionNotice.message and defineStack's warnConversionNotice are untouched. Whether
all of the renderings should descend from one source is a real question with a real cost on
either side, and it is #15095, not this card.


🤖 Generated with Claude Code

https://claude.ai/code/session_016yfqQh2dBgPAymYd7xipza

Generated by Claude Code


Generated by Claude Code

The human face of an ADR-0087 D2 conversion notice was written out three
times, verbatim, in os build / os validate / os lint. The three template
literals were byte-identical (measured: one distinct literal across the
three) and held equal by convention alone — the parity guard asserted each
command PASSES an onConversionNotice sink, never that they SAY the same
thing once they have one, so a reword in one command diverged silently with
every gate green.
Hoist the sentence into formatConversionNotice() in src/utils/format.ts and
render all three through it. It is a formatter, not a printer, which is what
makes one implementation possible: the three call sites genuinely differ in
DISPOSITION (build/lint print behind !flags.json, validate pushes into the
--strict warnings list) but not in what they say, so the difference costs the
function no parameter. Output is byte-identical.
Extend packages/cli/test/validate-build-gate-parity.test.ts with the rule the
old guard could not see: every authoring command renders through the one
formatter and none spells the sentence out inline, with a positive control so
it cannot pass vacuously on a CLI that says nothing.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016yfqQh2dBgPAymYd7xipza
…nsolidation
Part of #13743. `@objectstack/cli` publishes `dist`, compiled from the edited
`src`, so this diff changes the published package even though it changes
nothing an author can observe — `patch`, not `skip-changeset`.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016yfqQh2dBgPAymYd7xipza
…arity
Brings in PR #15062 (`os i18n check --help`), which lands in packages/cli
while this branch is open — the dispatch named it as the one to merge and
re-verify against. Also #15065 (rest), #15063/#15067 (docs) and #15055 (spec).
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016yfqQh2dBgPAymYd7xipza
@github-actions

Copy link
Copy Markdown
Contributor

📓 Docs Drift Check

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

30 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 ab47816914fd39709f4e9645370729ce1f4f9826.

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

What this run could not see
  • 1 anchor(s) matched too much of the corpus to be a work list: os validate (command, 43 pages)
  • 3 name(s) were too generic to anchor anything (single lowercase words)
  • the SDK route bridge reached 47 of 219 client-bound route-ledger rows — the other 172 have no registrar path: tail to select them, so pages documenting THEIR client methods cannot appear above, on this or any run. Of those 172: 14 are remediable by widening that discovery convention (an in-repo file declares the path; the convention did not scan it); 56 are structural — on a ledger where NOT ONE row is declared in-repo, so no discovery change reaches them at any price; 102 are undecided (no in-repo declaration, on a ledger that has other in-repo registrars — absence and an unreadable spelling are not distinguishable here). The rows themselves: node scripts/docs-audit/affected-docs.mjs --bridge-coverage
  • 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 — 22 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 ab47816914fd39709f4e9645370729ce1f4f9826packageMentionDocs.

Which tree this was computed on

This run read content/docs from 48aac740974ea994794664b7dce221ed4dfb15b1 — the merge of head 9fc66fafafab958e3ae03b0f43bf8989c9b72699 into base ab47816914fd39709f4e9645370729ce1f4f9826, 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 48aac740974ea994794664b7dce221ed4dfb15b1 && git checkout 48aac740974ea994794664b7dce221ed4dfb15b1
# afterwards, rebuild it from the two parents, which stay fetchable
git fetch origin ab47816914fd39709f4e9645370729ce1f4f9826 9fc66fafafab958e3ae03b0f43bf8989c9b72699 && git checkout -B drift-repro ab47816914fd39709f4e9645370729ce1f4f9826 && git merge --no-ff 9fc66fafafab958e3ae03b0f43bf8989c9b72699
node scripts/docs-audit/affected-docs.mjs --json ab47816914fd39709f4e9645370729ce1f4f9826

⚠️ 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 ab47816914fd39709f4e9645370729ce1f4f9826 → pass the list as
args.docs, on the commit named under Which tree this was computed on.

@github-actionsgithub-actionsBot added documentation Improvements or additions to documentation tests tooling labels Sep 3, 2026
@os-trump
os-trump marked this pull request as ready for review September 3, 2026 21:56
@os-trump
os-trump added this pull request to the merge queueSep 3, 2026
Merged via the queue into main with commit 4428dd5Sep 3, 2026
35 checks passed
@os-trump
os-trump deleted the claude/issue-13743-conversion-notice-wording-parity branch September 3, 2026 22:39
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

documentationImprovements or additions to documentationsize/mteststooling

Projects

None yet

Development

Successfully merging this pull request may close these issues.

The ADR-0087 conversion-notice wording is duplicated verbatim across all three authoring commands, and no gate holds the three equal

2 participants

@os-trump@claude
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Add copy buttons to all
 blocks\n(function() {\n function addCopyButtons() {\n document.querySelectorAll('pre code').forEach(function(codeBlock) {\n if (codeBlock.parentElement.hasAttribute('data-copy-added')) return;\n codeBlock.parentElement.setAttribute('data-copy-added', 'true');\n \n var btn = document.createElement('button');\n btn.textContent = 'Copy';\n btn.style.cssText = 'position:absolute;top:4px;right:4px;padding:2px 8px;font-size:11px;background:#4ecdc4;border:none;border-radius:4px;color:#1a1a2e;cursor:pointer;opacity:0.7;transition:opacity 0.2s;';\n btn.onmouseover = function() { this.style.opacity = '1'; };\n btn.onmouseout = function() { this.style.opacity = '0.7'; };\n btn.onclick = function() {\n navigator.clipboard.writeText(codeBlock.textContent).then(function() {\n btn.textContent = 'Copied!';\n setTimeout(function() { btn.textContent = 'Copy'; }, 1500);\n });\n };\n codeBlock.parentElement.style.position = 'relative';\n codeBlock.parentElement.appendChild(btn);\n });\n }\n \n addCopyButtons();\n \n // Re-run on dynamic content\n var observer = new MutationObserver(addCopyButtons);\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Add Copy Buttons to Code Blocks");
}
} catch(__e) { console.warn('[Userscript:Add Copy Buttons to Code Blocks]', __e); }
})();
(function(){
try {
var __m = "github.com";
var __re = new RegExp('^' + "github\\.com" + '
Skip to content

refactor(cli): render the ADR-0087 conversion notice from one source, and gate it there - #15096

Merged
os-trump merged 3 commits into
mainfrom
claude/issue-13743-conversion-notice-wording-parity
Sep 3, 2026
Merged

refactor(cli): render the ADR-0087 conversion notice from one source, and gate it there#15096
os-trump merged 3 commits into
mainfrom
claude/issue-13743-conversion-notice-wording-parity

Conversation

@os-trump

Copy link
Copy Markdown
Collaborator

Fixes#13743

The ADR-0087 D2 conversion notice now has one source in packages/cli, and the parity
guard holds it there. Output is byte-for-byte unchanged.

Which of triage's three options, and why

Option 1 — extract a shared formatter — carrying the smallest useful half of option 2.
Triage required this to be stated rather than defaulted, and the deciding input was a
measurement rather than a preference.

Option 3's supporting argument is falsified. Its case was that validate.ts folds
its copy into a warnings list --strict judges while the other two print directly, so
"the three call sites are not fully interchangeable, and forcing an extraction may need a
parameter for the difference." The first clause is true and I verified it; the second does
not follow. The asymmetry is entirely in the disposition of the string, never in the
string:

commandwhat it does with the sentencethe sentence itself
os build (compile.ts)printWarning(...) behind !flags.jsonidentical
os lint (lint.ts)printWarning(...) behind !flags.jsonidentical
os validate (validate.ts)warnings.push(...), which --strict then judgesidentical

Measured at the branch point d17f352b1b: extracting the template literal from each of the
three lines and hashing it gives one distinct value, 124 bytes, sha256 prefix
7197b905060762c3. A pure formatConversionNotice(notice) therefore serves all three and
costs no parameter for the difference — the extraction option's stated price does not
materialise.

Why not option 2 alone. A comparator locks today's three copies together and detects a
reword only after someone has written it; one source makes the divergence impossible to
write. It is also the disposition this file already reached for the same problem:
printTruncationNotice in the same module carries the note "ONE implementation of that
sentence, deliberately … nine copies of the wording would be nine chances for them to drift
apart." House precedent, not a new idea.

What option 2 still buys, and is kept. Extraction does not stop a copy coming back.
The rule added to validate-build-gate-parity.test.ts is therefore structural rather than
comparative: every authoring command renders through the one formatter, and none spells the
sentence out inline.

Re-derived the count — and the probe returned more than three

The release comment required a bound with a control that could have returned a fourth. It
did. A whole-repo git grep (no pathspec) for the prose fragment converted at load
tuned to the sentence, not to the three known sites — finds a rendering the card does not
mention, in packages/spec/src/stack.zod.ts:

packages/cli/src/commands/compile.ts:238 the three the card counts
packages/cli/src/commands/lint.ts:580
packages/cli/src/commands/validate.ts:409
packages/spec/src/stack.zod.ts:2310 NOT in the card — defineStack's own face

and reading that seam turns up a fifth prose form on ConversionNotice.message
(packages/spec/src/conversions/apply.ts), which is what the --json payloads publish.

The card's "three" is correct as a count of the three authoring commands, and wrong as
a bound on the wording.
Four renderings, three distinct sentences. The two in
packages/spec are fenced read-only on this card and cannot import the CLI's formatter
anyway (@objectstack/cli depends on @objectstack/spec, not the reverse), so they are
filed separately as #15095 and pinned here as a recorded fact rather than left for the next
reader to discover and "fix" in one command only.

The change

  • packages/cli/src/utils/format.tsformatConversionNotice(notice), a formatter and
    deliberately not a printer.
  • compile.ts / lint.ts / validate.ts — render through it; the three inline literals
    are gone (grep -c for the prose in each command source: 0, 0, 0).
  • packages/cli/test/validate-build-gate-parity.test.ts — the rule the old guard could not
    see, with a positive control that fails if the formatter stops carrying the sentence.
  • packages/cli/src/utils/format.conversion-notice.test.ts — the value pin on the rendered
    sentence, plus the recorded form-1 / form-3 split.
  • .changeset/cli-conversion-notice-one-source.mdpatch. Measured from this package's
    own files field: @objectstack/cli publishes dist, compiled from the edited src, so
    the diff changes the published package even though it changes nothing an author can
    observe. skip-changeset would have been wrong.

Red-first — four arms, each naming a different assertion

Every mutation was committed-first, proved on disk by blob hash, and restored by blob-hash
equality against the HEAD blob under trap ... EXIT INT TERM on absolute paths. The guard
is a source-text scanner, so no build or dist step participates in any arm.

armmutationexpectedmeasured
Areword lint.ts's copy, dropping the shared formatterred on the "renders through ONE formatter" assertionvitest exit 1lint.ts must render its ADR-0087 D2 conversion notices with formatConversionNotice(); 1 failed / 8 passed
Bkeep the call, add a verbatim inline copy beside itred on the "no inline copy" assertionvitest exit 1lint.ts spells the ADR-0087 D2 conversion notice out inline; 1 failed / 8 passed
Creword the ONE source in format.tsred on the positive control AND on the value pinvitest exit 1 — control fires, and the pin reports converted while loading where converted at load was expected; 2 failed / 10 passed
Dunmutated treegreenvitest exit 0 — 2 files, 12 tests passed

Blob evidence, arm by arm: lint.ts HEAD blob 43ace89d…; mutated to 82ef231d… (A) and
a25e93f7… (B); restored to 43ace89d… with git diff HEAD empty each time. format.ts
HEAD blob 53d51fe1…; mutated to 80839a7e… (C); restored to 53d51fe1…. Final
git status --porcelain -- packages/cli was empty.

Arm A is precisely the check the dispatch asked for — reword one copy and show the guard
redden — and arms B and C exist because A alone would not have told you which assertion
fired.

Verification

At final HEAD 9fc66faf on a clean tree, after merging origin/main (which brought in
PR #15062, the armed sibling the dispatch warned about, plus four others):

  • pnpm --filter @objectstack/cli typecheck — exit 0. Both halves: tsc --noEmit over
    src (which is where the new value pin lives, so it is type-checked), and
    check:test-typecheck over tsconfig.test.json, whose verdict line reads
    OK — @objectstack/cli's test layer compiles … 3 file(s) / 28 error(s) / 6 pinned signature(s) held — the pre-existing ledger, unmoved.
  • Targeted vitest, 6 files / 53 tests passed: the two new/edited guards, the three
    conversion-notice e2e suites (lint-conversion-notices,
    validate-json-failure-conversions, build-json-failure-conversions — these drive the
    real CLI over a live conversion via tsx, so they are the behavioural proof the output
    did not move), and i18n-check-help-surface-parity from the merged sibling PR.
  • Gate union derived by node scripts/pm/dispatch-gates.mjs --repo objectstack-ai/objectstack
    at that HEAD: 42 families (33 by path, 6 by change kind, 5 declared whole-tree).
    40 green. Two are NOT MEASURED and say so themselves: check-test-completeness.mjs
    grades a saved turbo run test log this box cannot produce ("the local reading for this
    gate is NOT MEASURED"), and check-half-states.mjs needs repo-scoped GitHub REST, which
    this container is refused (GET /repos/... returns 403 with no rate-limit headers while
    GET /rate_limit reports 15000 left). Five others first exited 3 on an unbuilt closure
    and went green after a full pnpm build: check:dual-build-cjs-loads, check:i18n,
    check:i18n-coverage, check:type-check-debt (which had also hit a local 300s cap), and
    the two named above stayed 3 for the reasons given.

Declared narrowing. The repo-wide pnpm lint sweep was not run locally; CI owns it and
runs it once regardless. Everything else in the derived union was run.

What this deliberately does not do

ConversionNotice.message and defineStack's warnConversionNotice are untouched. Whether
all of the renderings should descend from one source is a real question with a real cost on
either side, and it is #15095, not this card.


🤖 Generated with Claude Code

https://claude.ai/code/session_016yfqQh2dBgPAymYd7xipza

Generated by Claude Code


Generated by Claude Code

The human face of an ADR-0087 D2 conversion notice was written out three
times, verbatim, in os build / os validate / os lint. The three template
literals were byte-identical (measured: one distinct literal across the
three) and held equal by convention alone — the parity guard asserted each
command PASSES an onConversionNotice sink, never that they SAY the same
thing once they have one, so a reword in one command diverged silently with
every gate green.
Hoist the sentence into formatConversionNotice() in src/utils/format.ts and
render all three through it. It is a formatter, not a printer, which is what
makes one implementation possible: the three call sites genuinely differ in
DISPOSITION (build/lint print behind !flags.json, validate pushes into the
--strict warnings list) but not in what they say, so the difference costs the
function no parameter. Output is byte-identical.
Extend packages/cli/test/validate-build-gate-parity.test.ts with the rule the
old guard could not see: every authoring command renders through the one
formatter and none spells the sentence out inline, with a positive control so
it cannot pass vacuously on a CLI that says nothing.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016yfqQh2dBgPAymYd7xipza
…nsolidation
Part of #13743. `@objectstack/cli` publishes `dist`, compiled from the edited
`src`, so this diff changes the published package even though it changes
nothing an author can observe — `patch`, not `skip-changeset`.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016yfqQh2dBgPAymYd7xipza
…arity
Brings in PR #15062 (`os i18n check --help`), which lands in packages/cli
while this branch is open — the dispatch named it as the one to merge and
re-verify against. Also #15065 (rest), #15063/#15067 (docs) and #15055 (spec).
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016yfqQh2dBgPAymYd7xipza
@github-actions

Copy link
Copy Markdown
Contributor

📓 Docs Drift Check

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

30 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 ab47816914fd39709f4e9645370729ce1f4f9826.

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

What this run could not see
  • 1 anchor(s) matched too much of the corpus to be a work list: os validate (command, 43 pages)
  • 3 name(s) were too generic to anchor anything (single lowercase words)
  • the SDK route bridge reached 47 of 219 client-bound route-ledger rows — the other 172 have no registrar path: tail to select them, so pages documenting THEIR client methods cannot appear above, on this or any run. Of those 172: 14 are remediable by widening that discovery convention (an in-repo file declares the path; the convention did not scan it); 56 are structural — on a ledger where NOT ONE row is declared in-repo, so no discovery change reaches them at any price; 102 are undecided (no in-repo declaration, on a ledger that has other in-repo registrars — absence and an unreadable spelling are not distinguishable here). The rows themselves: node scripts/docs-audit/affected-docs.mjs --bridge-coverage
  • 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 — 22 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 ab47816914fd39709f4e9645370729ce1f4f9826packageMentionDocs.

Which tree this was computed on

This run read content/docs from 48aac740974ea994794664b7dce221ed4dfb15b1 — the merge of head 9fc66fafafab958e3ae03b0f43bf8989c9b72699 into base ab47816914fd39709f4e9645370729ce1f4f9826, 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 48aac740974ea994794664b7dce221ed4dfb15b1 && git checkout 48aac740974ea994794664b7dce221ed4dfb15b1
# afterwards, rebuild it from the two parents, which stay fetchable
git fetch origin ab47816914fd39709f4e9645370729ce1f4f9826 9fc66fafafab958e3ae03b0f43bf8989c9b72699 && git checkout -B drift-repro ab47816914fd39709f4e9645370729ce1f4f9826 && git merge --no-ff 9fc66fafafab958e3ae03b0f43bf8989c9b72699
node scripts/docs-audit/affected-docs.mjs --json ab47816914fd39709f4e9645370729ce1f4f9826

⚠️ 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 ab47816914fd39709f4e9645370729ce1f4f9826 → pass the list as
args.docs, on the commit named under Which tree this was computed on.

@github-actionsgithub-actionsBot added documentation Improvements or additions to documentation tests tooling labels Sep 3, 2026
@os-trump
os-trump marked this pull request as ready for review September 3, 2026 21:56
@os-trump
os-trump added this pull request to the merge queueSep 3, 2026
Merged via the queue into main with commit 4428dd5Sep 3, 2026
35 checks passed
@os-trump
os-trump deleted the claude/issue-13743-conversion-notice-wording-parity branch September 3, 2026 22:39
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

documentationImprovements or additions to documentationsize/mteststooling

Projects

None yet

Development

Successfully merging this pull request may close these issues.

The ADR-0087 conversion-notice wording is duplicated verbatim across all three authoring commands, and no gate holds the three equal

2 participants

@os-trump@claude
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Force GitHub README to respect dark mode\n(function() {\n var style = document.createElement('style');\n style.textContent = '\n .markdown-body {\n color-scheme: dark light;\n }\n .markdown-body pre { background: #161b22 !important; }\n .markdown-body code { background: rgba(110, 118, 129, 0.4) !important; }\n .markdown-body table th, .markdown-body table td { border-color: #30363d !important; }\n .markdown-body img { background: #0d1117; }\n .markdown-body blockquote { border-left-color: #8b949e; }\n .markdown-body hr { border-color: #30363d; }\n ';\n document.head.appendChild(style);\n})();", "GitHub Dark Mode README Fix"); } } catch(__e) { console.warn('[Userscript:GitHub Dark Mode README Fix]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

refactor(cli): render the ADR-0087 conversion notice from one source, and gate it there - #15096

Merged
os-trump merged 3 commits into
mainfrom
claude/issue-13743-conversion-notice-wording-parity
Sep 3, 2026
Merged

refactor(cli): render the ADR-0087 conversion notice from one source, and gate it there#15096
os-trump merged 3 commits into
mainfrom
claude/issue-13743-conversion-notice-wording-parity

Conversation

@os-trump

Copy link
Copy Markdown
Collaborator

Fixes#13743

The ADR-0087 D2 conversion notice now has one source in packages/cli, and the parity
guard holds it there. Output is byte-for-byte unchanged.

Which of triage's three options, and why

Option 1 — extract a shared formatter — carrying the smallest useful half of option 2.
Triage required this to be stated rather than defaulted, and the deciding input was a
measurement rather than a preference.

Option 3's supporting argument is falsified. Its case was that validate.ts folds
its copy into a warnings list --strict judges while the other two print directly, so
"the three call sites are not fully interchangeable, and forcing an extraction may need a
parameter for the difference." The first clause is true and I verified it; the second does
not follow. The asymmetry is entirely in the disposition of the string, never in the
string:

commandwhat it does with the sentencethe sentence itself
os build (compile.ts)printWarning(...) behind !flags.jsonidentical
os lint (lint.ts)printWarning(...) behind !flags.jsonidentical
os validate (validate.ts)warnings.push(...), which --strict then judgesidentical

Measured at the branch point d17f352b1b: extracting the template literal from each of the
three lines and hashing it gives one distinct value, 124 bytes, sha256 prefix
7197b905060762c3. A pure formatConversionNotice(notice) therefore serves all three and
costs no parameter for the difference — the extraction option's stated price does not
materialise.

Why not option 2 alone. A comparator locks today's three copies together and detects a
reword only after someone has written it; one source makes the divergence impossible to
write. It is also the disposition this file already reached for the same problem:
printTruncationNotice in the same module carries the note "ONE implementation of that
sentence, deliberately … nine copies of the wording would be nine chances for them to drift
apart." House precedent, not a new idea.

What option 2 still buys, and is kept. Extraction does not stop a copy coming back.
The rule added to validate-build-gate-parity.test.ts is therefore structural rather than
comparative: every authoring command renders through the one formatter, and none spells the
sentence out inline.

Re-derived the count — and the probe returned more than three

The release comment required a bound with a control that could have returned a fourth. It
did. A whole-repo git grep (no pathspec) for the prose fragment converted at load
tuned to the sentence, not to the three known sites — finds a rendering the card does not
mention, in packages/spec/src/stack.zod.ts:

packages/cli/src/commands/compile.ts:238 the three the card counts
packages/cli/src/commands/lint.ts:580
packages/cli/src/commands/validate.ts:409
packages/spec/src/stack.zod.ts:2310 NOT in the card — defineStack's own face

and reading that seam turns up a fifth prose form on ConversionNotice.message
(packages/spec/src/conversions/apply.ts), which is what the --json payloads publish.

The card's "three" is correct as a count of the three authoring commands, and wrong as
a bound on the wording.
Four renderings, three distinct sentences. The two in
packages/spec are fenced read-only on this card and cannot import the CLI's formatter
anyway (@objectstack/cli depends on @objectstack/spec, not the reverse), so they are
filed separately as #15095 and pinned here as a recorded fact rather than left for the next
reader to discover and "fix" in one command only.

The change

  • packages/cli/src/utils/format.tsformatConversionNotice(notice), a formatter and
    deliberately not a printer.
  • compile.ts / lint.ts / validate.ts — render through it; the three inline literals
    are gone (grep -c for the prose in each command source: 0, 0, 0).
  • packages/cli/test/validate-build-gate-parity.test.ts — the rule the old guard could not
    see, with a positive control that fails if the formatter stops carrying the sentence.
  • packages/cli/src/utils/format.conversion-notice.test.ts — the value pin on the rendered
    sentence, plus the recorded form-1 / form-3 split.
  • .changeset/cli-conversion-notice-one-source.mdpatch. Measured from this package's
    own files field: @objectstack/cli publishes dist, compiled from the edited src, so
    the diff changes the published package even though it changes nothing an author can
    observe. skip-changeset would have been wrong.

Red-first — four arms, each naming a different assertion

Every mutation was committed-first, proved on disk by blob hash, and restored by blob-hash
equality against the HEAD blob under trap ... EXIT INT TERM on absolute paths. The guard
is a source-text scanner, so no build or dist step participates in any arm.

armmutationexpectedmeasured
Areword lint.ts's copy, dropping the shared formatterred on the "renders through ONE formatter" assertionvitest exit 1lint.ts must render its ADR-0087 D2 conversion notices with formatConversionNotice(); 1 failed / 8 passed
Bkeep the call, add a verbatim inline copy beside itred on the "no inline copy" assertionvitest exit 1lint.ts spells the ADR-0087 D2 conversion notice out inline; 1 failed / 8 passed
Creword the ONE source in format.tsred on the positive control AND on the value pinvitest exit 1 — control fires, and the pin reports converted while loading where converted at load was expected; 2 failed / 10 passed
Dunmutated treegreenvitest exit 0 — 2 files, 12 tests passed

Blob evidence, arm by arm: lint.ts HEAD blob 43ace89d…; mutated to 82ef231d… (A) and
a25e93f7… (B); restored to 43ace89d… with git diff HEAD empty each time. format.ts
HEAD blob 53d51fe1…; mutated to 80839a7e… (C); restored to 53d51fe1…. Final
git status --porcelain -- packages/cli was empty.

Arm A is precisely the check the dispatch asked for — reword one copy and show the guard
redden — and arms B and C exist because A alone would not have told you which assertion
fired.

Verification

At final HEAD 9fc66faf on a clean tree, after merging origin/main (which brought in
PR #15062, the armed sibling the dispatch warned about, plus four others):

  • pnpm --filter @objectstack/cli typecheck — exit 0. Both halves: tsc --noEmit over
    src (which is where the new value pin lives, so it is type-checked), and
    check:test-typecheck over tsconfig.test.json, whose verdict line reads
    OK — @objectstack/cli's test layer compiles … 3 file(s) / 28 error(s) / 6 pinned signature(s) held — the pre-existing ledger, unmoved.
  • Targeted vitest, 6 files / 53 tests passed: the two new/edited guards, the three
    conversion-notice e2e suites (lint-conversion-notices,
    validate-json-failure-conversions, build-json-failure-conversions — these drive the
    real CLI over a live conversion via tsx, so they are the behavioural proof the output
    did not move), and i18n-check-help-surface-parity from the merged sibling PR.
  • Gate union derived by node scripts/pm/dispatch-gates.mjs --repo objectstack-ai/objectstack
    at that HEAD: 42 families (33 by path, 6 by change kind, 5 declared whole-tree).
    40 green. Two are NOT MEASURED and say so themselves: check-test-completeness.mjs
    grades a saved turbo run test log this box cannot produce ("the local reading for this
    gate is NOT MEASURED"), and check-half-states.mjs needs repo-scoped GitHub REST, which
    this container is refused (GET /repos/... returns 403 with no rate-limit headers while
    GET /rate_limit reports 15000 left). Five others first exited 3 on an unbuilt closure
    and went green after a full pnpm build: check:dual-build-cjs-loads, check:i18n,
    check:i18n-coverage, check:type-check-debt (which had also hit a local 300s cap), and
    the two named above stayed 3 for the reasons given.

Declared narrowing. The repo-wide pnpm lint sweep was not run locally; CI owns it and
runs it once regardless. Everything else in the derived union was run.

What this deliberately does not do

ConversionNotice.message and defineStack's warnConversionNotice are untouched. Whether
all of the renderings should descend from one source is a real question with a real cost on
either side, and it is #15095, not this card.


🤖 Generated with Claude Code

https://claude.ai/code/session_016yfqQh2dBgPAymYd7xipza

Generated by Claude Code


Generated by Claude Code

The human face of an ADR-0087 D2 conversion notice was written out three
times, verbatim, in os build / os validate / os lint. The three template
literals were byte-identical (measured: one distinct literal across the
three) and held equal by convention alone — the parity guard asserted each
command PASSES an onConversionNotice sink, never that they SAY the same
thing once they have one, so a reword in one command diverged silently with
every gate green.
Hoist the sentence into formatConversionNotice() in src/utils/format.ts and
render all three through it. It is a formatter, not a printer, which is what
makes one implementation possible: the three call sites genuinely differ in
DISPOSITION (build/lint print behind !flags.json, validate pushes into the
--strict warnings list) but not in what they say, so the difference costs the
function no parameter. Output is byte-identical.
Extend packages/cli/test/validate-build-gate-parity.test.ts with the rule the
old guard could not see: every authoring command renders through the one
formatter and none spells the sentence out inline, with a positive control so
it cannot pass vacuously on a CLI that says nothing.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016yfqQh2dBgPAymYd7xipza
…nsolidation
Part of #13743. `@objectstack/cli` publishes `dist`, compiled from the edited
`src`, so this diff changes the published package even though it changes
nothing an author can observe — `patch`, not `skip-changeset`.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016yfqQh2dBgPAymYd7xipza
…arity
Brings in PR #15062 (`os i18n check --help`), which lands in packages/cli
while this branch is open — the dispatch named it as the one to merge and
re-verify against. Also #15065 (rest), #15063/#15067 (docs) and #15055 (spec).
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016yfqQh2dBgPAymYd7xipza
@github-actions

Copy link
Copy Markdown
Contributor

📓 Docs Drift Check

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

30 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 ab47816914fd39709f4e9645370729ce1f4f9826.

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

What this run could not see
  • 1 anchor(s) matched too much of the corpus to be a work list: os validate (command, 43 pages)
  • 3 name(s) were too generic to anchor anything (single lowercase words)
  • the SDK route bridge reached 47 of 219 client-bound route-ledger rows — the other 172 have no registrar path: tail to select them, so pages documenting THEIR client methods cannot appear above, on this or any run. Of those 172: 14 are remediable by widening that discovery convention (an in-repo file declares the path; the convention did not scan it); 56 are structural — on a ledger where NOT ONE row is declared in-repo, so no discovery change reaches them at any price; 102 are undecided (no in-repo declaration, on a ledger that has other in-repo registrars — absence and an unreadable spelling are not distinguishable here). The rows themselves: node scripts/docs-audit/affected-docs.mjs --bridge-coverage
  • 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 — 22 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 ab47816914fd39709f4e9645370729ce1f4f9826packageMentionDocs.

Which tree this was computed on

This run read content/docs from 48aac740974ea994794664b7dce221ed4dfb15b1 — the merge of head 9fc66fafafab958e3ae03b0f43bf8989c9b72699 into base ab47816914fd39709f4e9645370729ce1f4f9826, 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 48aac740974ea994794664b7dce221ed4dfb15b1 && git checkout 48aac740974ea994794664b7dce221ed4dfb15b1
# afterwards, rebuild it from the two parents, which stay fetchable
git fetch origin ab47816914fd39709f4e9645370729ce1f4f9826 9fc66fafafab958e3ae03b0f43bf8989c9b72699 && git checkout -B drift-repro ab47816914fd39709f4e9645370729ce1f4f9826 && git merge --no-ff 9fc66fafafab958e3ae03b0f43bf8989c9b72699
node scripts/docs-audit/affected-docs.mjs --json ab47816914fd39709f4e9645370729ce1f4f9826

⚠️ 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 ab47816914fd39709f4e9645370729ce1f4f9826 → pass the list as
args.docs, on the commit named under Which tree this was computed on.

@github-actionsgithub-actionsBot added documentation Improvements or additions to documentation tests tooling labels Sep 3, 2026
@os-trump
os-trump marked this pull request as ready for review September 3, 2026 21:56
@os-trump
os-trump added this pull request to the merge queueSep 3, 2026
Merged via the queue into main with commit 4428dd5Sep 3, 2026
35 checks passed
@os-trump
os-trump deleted the claude/issue-13743-conversion-notice-wording-parity branch September 3, 2026 22:39
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

documentationImprovements or additions to documentationsize/mteststooling

Projects

None yet

Development

Successfully merging this pull request may close these issues.

The ADR-0087 conversion-notice wording is duplicated verbatim across all three authoring commands, and no gate holds the three equal

2 participants

@os-trump@claude
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Highlight search terms from Google/DuckDuckGo/Bing referrer\n(function() {\n var ref = document.referrer;\n var terms = [];\n \n if (ref.includes('google.com') || ref.includes('duckduckgo.com') || ref.includes('bing.com')) {\n var url = new URL(ref);\n var q = url.searchParams.get('q') || url.searchParams.get('p');\n if (q) {\n terms = q.split(/\\s+/).filter(function(t) { return t.length > 2; });\n }\n }\n \n if (terms.length === 0) return;\n \n var style = document.createElement('style');\n style.textContent = '.userscript-highlight { background: #fbbf24; color: #1a1a2e; padding: 1px 3px; border-radius: 2px; }';\n document.head.appendChild(style);\n \n function highlight(node) {\n if (node.nodeType === 3) { // text node\n var text = node.textContent;\n var found = false;\n terms.forEach(function(term) {\n var regex = new RegExp('(' + term.replace(/[.*+?^${}()|[\\]\\\\]/g, '\\\\') + ')', 'gi');\n if (regex.test(text)) {\n found = true;\n var frag = document.createDocumentFragment();\n var parts = text.split(regex);\n parts.forEach(function(part, i) {\n if (i % 2 === 0) {\n frag.appendChild(document.createTextNode(part));\n } else {\n var span = document.createElement('span');\n span.className = 'userscript-highlight';\n span.textContent = part;\n frag.appendChild(span);\n }\n });\n node.parentNode.replaceChild(frag, node);\n }\n });\n } else if (node.nodeType === 1 && node.childNodes) { // element\n var skipTags = ['SCRIPT', 'STYLE', 'NOSCRIPT', 'TEXTAREA', 'INPUT', 'SELECT'];\n if (!skipTags.includes(node.tagName)) {\n Array.from(node.childNodes).forEach(highlight);\n }\n }\n }\n \n highlight(document.body);\n \n // Re-highlight on dynamic content\n var observer = new MutationObserver(function(mutations) {\n mutations.forEach(function(m) {\n m.addedNodes.forEach(function(node) {\n if (node.nodeType === 1 || node.nodeType === 3) highlight(node);\n });\n });\n });\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Highlight Search Terms"); } } catch(__e) { console.warn('[Userscript:Highlight Search Terms]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

refactor(cli): render the ADR-0087 conversion notice from one source, and gate it there - #15096

Merged
os-trump merged 3 commits into
mainfrom
claude/issue-13743-conversion-notice-wording-parity
Sep 3, 2026
Merged

refactor(cli): render the ADR-0087 conversion notice from one source, and gate it there#15096
os-trump merged 3 commits into
mainfrom
claude/issue-13743-conversion-notice-wording-parity

Conversation

@os-trump

Copy link
Copy Markdown
Collaborator

Fixes#13743

The ADR-0087 D2 conversion notice now has one source in packages/cli, and the parity
guard holds it there. Output is byte-for-byte unchanged.

Which of triage's three options, and why

Option 1 — extract a shared formatter — carrying the smallest useful half of option 2.
Triage required this to be stated rather than defaulted, and the deciding input was a
measurement rather than a preference.

Option 3's supporting argument is falsified. Its case was that validate.ts folds
its copy into a warnings list --strict judges while the other two print directly, so
"the three call sites are not fully interchangeable, and forcing an extraction may need a
parameter for the difference." The first clause is true and I verified it; the second does
not follow. The asymmetry is entirely in the disposition of the string, never in the
string:

commandwhat it does with the sentencethe sentence itself
os build (compile.ts)printWarning(...) behind !flags.jsonidentical
os lint (lint.ts)printWarning(...) behind !flags.jsonidentical
os validate (validate.ts)warnings.push(...), which --strict then judgesidentical

Measured at the branch point d17f352b1b: extracting the template literal from each of the
three lines and hashing it gives one distinct value, 124 bytes, sha256 prefix
7197b905060762c3. A pure formatConversionNotice(notice) therefore serves all three and
costs no parameter for the difference — the extraction option's stated price does not
materialise.

Why not option 2 alone. A comparator locks today's three copies together and detects a
reword only after someone has written it; one source makes the divergence impossible to
write. It is also the disposition this file already reached for the same problem:
printTruncationNotice in the same module carries the note "ONE implementation of that
sentence, deliberately … nine copies of the wording would be nine chances for them to drift
apart." House precedent, not a new idea.

What option 2 still buys, and is kept. Extraction does not stop a copy coming back.
The rule added to validate-build-gate-parity.test.ts is therefore structural rather than
comparative: every authoring command renders through the one formatter, and none spells the
sentence out inline.

Re-derived the count — and the probe returned more than three

The release comment required a bound with a control that could have returned a fourth. It
did. A whole-repo git grep (no pathspec) for the prose fragment converted at load
tuned to the sentence, not to the three known sites — finds a rendering the card does not
mention, in packages/spec/src/stack.zod.ts:

packages/cli/src/commands/compile.ts:238 the three the card counts
packages/cli/src/commands/lint.ts:580
packages/cli/src/commands/validate.ts:409
packages/spec/src/stack.zod.ts:2310 NOT in the card — defineStack's own face

and reading that seam turns up a fifth prose form on ConversionNotice.message
(packages/spec/src/conversions/apply.ts), which is what the --json payloads publish.

The card's "three" is correct as a count of the three authoring commands, and wrong as
a bound on the wording.
Four renderings, three distinct sentences. The two in
packages/spec are fenced read-only on this card and cannot import the CLI's formatter
anyway (@objectstack/cli depends on @objectstack/spec, not the reverse), so they are
filed separately as #15095 and pinned here as a recorded fact rather than left for the next
reader to discover and "fix" in one command only.

The change

  • packages/cli/src/utils/format.tsformatConversionNotice(notice), a formatter and
    deliberately not a printer.
  • compile.ts / lint.ts / validate.ts — render through it; the three inline literals
    are gone (grep -c for the prose in each command source: 0, 0, 0).
  • packages/cli/test/validate-build-gate-parity.test.ts — the rule the old guard could not
    see, with a positive control that fails if the formatter stops carrying the sentence.
  • packages/cli/src/utils/format.conversion-notice.test.ts — the value pin on the rendered
    sentence, plus the recorded form-1 / form-3 split.
  • .changeset/cli-conversion-notice-one-source.mdpatch. Measured from this package's
    own files field: @objectstack/cli publishes dist, compiled from the edited src, so
    the diff changes the published package even though it changes nothing an author can
    observe. skip-changeset would have been wrong.

Red-first — four arms, each naming a different assertion

Every mutation was committed-first, proved on disk by blob hash, and restored by blob-hash
equality against the HEAD blob under trap ... EXIT INT TERM on absolute paths. The guard
is a source-text scanner, so no build or dist step participates in any arm.

armmutationexpectedmeasured
Areword lint.ts's copy, dropping the shared formatterred on the "renders through ONE formatter" assertionvitest exit 1lint.ts must render its ADR-0087 D2 conversion notices with formatConversionNotice(); 1 failed / 8 passed
Bkeep the call, add a verbatim inline copy beside itred on the "no inline copy" assertionvitest exit 1lint.ts spells the ADR-0087 D2 conversion notice out inline; 1 failed / 8 passed
Creword the ONE source in format.tsred on the positive control AND on the value pinvitest exit 1 — control fires, and the pin reports converted while loading where converted at load was expected; 2 failed / 10 passed
Dunmutated treegreenvitest exit 0 — 2 files, 12 tests passed

Blob evidence, arm by arm: lint.ts HEAD blob 43ace89d…; mutated to 82ef231d… (A) and
a25e93f7… (B); restored to 43ace89d… with git diff HEAD empty each time. format.ts
HEAD blob 53d51fe1…; mutated to 80839a7e… (C); restored to 53d51fe1…. Final
git status --porcelain -- packages/cli was empty.

Arm A is precisely the check the dispatch asked for — reword one copy and show the guard
redden — and arms B and C exist because A alone would not have told you which assertion
fired.

Verification

At final HEAD 9fc66faf on a clean tree, after merging origin/main (which brought in
PR #15062, the armed sibling the dispatch warned about, plus four others):

  • pnpm --filter @objectstack/cli typecheck — exit 0. Both halves: tsc --noEmit over
    src (which is where the new value pin lives, so it is type-checked), and
    check:test-typecheck over tsconfig.test.json, whose verdict line reads
    OK — @objectstack/cli's test layer compiles … 3 file(s) / 28 error(s) / 6 pinned signature(s) held — the pre-existing ledger, unmoved.
  • Targeted vitest, 6 files / 53 tests passed: the two new/edited guards, the three
    conversion-notice e2e suites (lint-conversion-notices,
    validate-json-failure-conversions, build-json-failure-conversions — these drive the
    real CLI over a live conversion via tsx, so they are the behavioural proof the output
    did not move), and i18n-check-help-surface-parity from the merged sibling PR.
  • Gate union derived by node scripts/pm/dispatch-gates.mjs --repo objectstack-ai/objectstack
    at that HEAD: 42 families (33 by path, 6 by change kind, 5 declared whole-tree).
    40 green. Two are NOT MEASURED and say so themselves: check-test-completeness.mjs
    grades a saved turbo run test log this box cannot produce ("the local reading for this
    gate is NOT MEASURED"), and check-half-states.mjs needs repo-scoped GitHub REST, which
    this container is refused (GET /repos/... returns 403 with no rate-limit headers while
    GET /rate_limit reports 15000 left). Five others first exited 3 on an unbuilt closure
    and went green after a full pnpm build: check:dual-build-cjs-loads, check:i18n,
    check:i18n-coverage, check:type-check-debt (which had also hit a local 300s cap), and
    the two named above stayed 3 for the reasons given.

Declared narrowing. The repo-wide pnpm lint sweep was not run locally; CI owns it and
runs it once regardless. Everything else in the derived union was run.

What this deliberately does not do

ConversionNotice.message and defineStack's warnConversionNotice are untouched. Whether
all of the renderings should descend from one source is a real question with a real cost on
either side, and it is #15095, not this card.


🤖 Generated with Claude Code

https://claude.ai/code/session_016yfqQh2dBgPAymYd7xipza

Generated by Claude Code


Generated by Claude Code

The human face of an ADR-0087 D2 conversion notice was written out three
times, verbatim, in os build / os validate / os lint. The three template
literals were byte-identical (measured: one distinct literal across the
three) and held equal by convention alone — the parity guard asserted each
command PASSES an onConversionNotice sink, never that they SAY the same
thing once they have one, so a reword in one command diverged silently with
every gate green.
Hoist the sentence into formatConversionNotice() in src/utils/format.ts and
render all three through it. It is a formatter, not a printer, which is what
makes one implementation possible: the three call sites genuinely differ in
DISPOSITION (build/lint print behind !flags.json, validate pushes into the
--strict warnings list) but not in what they say, so the difference costs the
function no parameter. Output is byte-identical.
Extend packages/cli/test/validate-build-gate-parity.test.ts with the rule the
old guard could not see: every authoring command renders through the one
formatter and none spells the sentence out inline, with a positive control so
it cannot pass vacuously on a CLI that says nothing.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016yfqQh2dBgPAymYd7xipza
…nsolidation
Part of #13743. `@objectstack/cli` publishes `dist`, compiled from the edited
`src`, so this diff changes the published package even though it changes
nothing an author can observe — `patch`, not `skip-changeset`.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016yfqQh2dBgPAymYd7xipza
…arity
Brings in PR #15062 (`os i18n check --help`), which lands in packages/cli
while this branch is open — the dispatch named it as the one to merge and
re-verify against. Also #15065 (rest), #15063/#15067 (docs) and #15055 (spec).
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016yfqQh2dBgPAymYd7xipza
@github-actions

Copy link
Copy Markdown
Contributor

📓 Docs Drift Check

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

30 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 ab47816914fd39709f4e9645370729ce1f4f9826.

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

What this run could not see
  • 1 anchor(s) matched too much of the corpus to be a work list: os validate (command, 43 pages)
  • 3 name(s) were too generic to anchor anything (single lowercase words)
  • the SDK route bridge reached 47 of 219 client-bound route-ledger rows — the other 172 have no registrar path: tail to select them, so pages documenting THEIR client methods cannot appear above, on this or any run. Of those 172: 14 are remediable by widening that discovery convention (an in-repo file declares the path; the convention did not scan it); 56 are structural — on a ledger where NOT ONE row is declared in-repo, so no discovery change reaches them at any price; 102 are undecided (no in-repo declaration, on a ledger that has other in-repo registrars — absence and an unreadable spelling are not distinguishable here). The rows themselves: node scripts/docs-audit/affected-docs.mjs --bridge-coverage
  • 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 — 22 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 ab47816914fd39709f4e9645370729ce1f4f9826packageMentionDocs.

Which tree this was computed on

This run read content/docs from 48aac740974ea994794664b7dce221ed4dfb15b1 — the merge of head 9fc66fafafab958e3ae03b0f43bf8989c9b72699 into base ab47816914fd39709f4e9645370729ce1f4f9826, 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 48aac740974ea994794664b7dce221ed4dfb15b1 && git checkout 48aac740974ea994794664b7dce221ed4dfb15b1
# afterwards, rebuild it from the two parents, which stay fetchable
git fetch origin ab47816914fd39709f4e9645370729ce1f4f9826 9fc66fafafab958e3ae03b0f43bf8989c9b72699 && git checkout -B drift-repro ab47816914fd39709f4e9645370729ce1f4f9826 && git merge --no-ff 9fc66fafafab958e3ae03b0f43bf8989c9b72699
node scripts/docs-audit/affected-docs.mjs --json ab47816914fd39709f4e9645370729ce1f4f9826

⚠️ 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 ab47816914fd39709f4e9645370729ce1f4f9826 → pass the list as
args.docs, on the commit named under Which tree this was computed on.

@github-actionsgithub-actionsBot added documentation Improvements or additions to documentation tests tooling labels Sep 3, 2026
@os-trump
os-trump marked this pull request as ready for review September 3, 2026 21:56
@os-trump
os-trump added this pull request to the merge queueSep 3, 2026
Merged via the queue into main with commit 4428dd5Sep 3, 2026
35 checks passed
@os-trump
os-trump deleted the claude/issue-13743-conversion-notice-wording-parity branch September 3, 2026 22:39
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

documentationImprovements or additions to documentationsize/mteststooling

Projects

None yet

Development

Successfully merging this pull request may close these issues.

The ADR-0087 conversion-notice wording is duplicated verbatim across all three authoring commands, and no gate holds the three equal

2 participants

@os-trump@claude
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Strip utm_, fbclid, gclid, etc. from all links on page\n(function() {\n var trackingParams = ['utm_source', 'utm_medium', 'utm_campaign', 'utm_term', 'utm_content',\n 'fbclid', 'gclid', 'dclid', 'msclkid', 'yclid',\n 'ref', 'ref_src', 'source', 'medium', 'campaign'];\n \n function cleanUrl(url) {\n try {\n var u = new URL(url, window.location.origin);\n var changed = false;\n trackingParams.forEach(function(p) {\n if (u.searchParams.has(p)) {\n u.searchParams.delete(p);\n changed = true;\n }\n });\n return changed ? u.toString() : url;\n } catch (e) {\n return url;\n }\n }\n \n function cleanLinks() {\n document.querySelectorAll('a[href]').forEach(function(a) {\n var clean = cleanUrl(a.href);\n if (clean !== a.href) a.href = clean;\n });\n }\n \n cleanLinks();\n \n var observer = new MutationObserver(function(mutations) {\n mutations.forEach(function(m) {\n m.addedNodes.forEach(function(node) {\n if (node.nodeType === 1) {\n if (node.tagName === 'A') cleanLinks();\n node.querySelectorAll('a[href]').forEach(function(a) {\n var clean = cleanUrl(a.href);\n if (clean !== a.href) a.href = clean;\n });\n }\n });\n });\n });\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Remove Tracking Parameters from Links"); } } catch(__e) { console.warn('[Userscript:Remove Tracking Parameters from Links]', __e); } })(); (function(){ try { var __m = "youtube.com"; var __re = new RegExp('^' + "youtube\\.com" + '
Skip to content

refactor(cli): render the ADR-0087 conversion notice from one source, and gate it there - #15096

Merged
os-trump merged 3 commits into
mainfrom
claude/issue-13743-conversion-notice-wording-parity
Sep 3, 2026
Merged

refactor(cli): render the ADR-0087 conversion notice from one source, and gate it there#15096
os-trump merged 3 commits into
mainfrom
claude/issue-13743-conversion-notice-wording-parity

Conversation

@os-trump

Copy link
Copy Markdown
Collaborator

Fixes#13743

The ADR-0087 D2 conversion notice now has one source in packages/cli, and the parity
guard holds it there. Output is byte-for-byte unchanged.

Which of triage's three options, and why

Option 1 — extract a shared formatter — carrying the smallest useful half of option 2.
Triage required this to be stated rather than defaulted, and the deciding input was a
measurement rather than a preference.

Option 3's supporting argument is falsified. Its case was that validate.ts folds
its copy into a warnings list --strict judges while the other two print directly, so
"the three call sites are not fully interchangeable, and forcing an extraction may need a
parameter for the difference." The first clause is true and I verified it; the second does
not follow. The asymmetry is entirely in the disposition of the string, never in the
string:

commandwhat it does with the sentencethe sentence itself
os build (compile.ts)printWarning(...) behind !flags.jsonidentical
os lint (lint.ts)printWarning(...) behind !flags.jsonidentical
os validate (validate.ts)warnings.push(...), which --strict then judgesidentical

Measured at the branch point d17f352b1b: extracting the template literal from each of the
three lines and hashing it gives one distinct value, 124 bytes, sha256 prefix
7197b905060762c3. A pure formatConversionNotice(notice) therefore serves all three and
costs no parameter for the difference — the extraction option's stated price does not
materialise.

Why not option 2 alone. A comparator locks today's three copies together and detects a
reword only after someone has written it; one source makes the divergence impossible to
write. It is also the disposition this file already reached for the same problem:
printTruncationNotice in the same module carries the note "ONE implementation of that
sentence, deliberately … nine copies of the wording would be nine chances for them to drift
apart." House precedent, not a new idea.

What option 2 still buys, and is kept. Extraction does not stop a copy coming back.
The rule added to validate-build-gate-parity.test.ts is therefore structural rather than
comparative: every authoring command renders through the one formatter, and none spells the
sentence out inline.

Re-derived the count — and the probe returned more than three

The release comment required a bound with a control that could have returned a fourth. It
did. A whole-repo git grep (no pathspec) for the prose fragment converted at load
tuned to the sentence, not to the three known sites — finds a rendering the card does not
mention, in packages/spec/src/stack.zod.ts:

packages/cli/src/commands/compile.ts:238 the three the card counts
packages/cli/src/commands/lint.ts:580
packages/cli/src/commands/validate.ts:409
packages/spec/src/stack.zod.ts:2310 NOT in the card — defineStack's own face

and reading that seam turns up a fifth prose form on ConversionNotice.message
(packages/spec/src/conversions/apply.ts), which is what the --json payloads publish.

The card's "three" is correct as a count of the three authoring commands, and wrong as
a bound on the wording.
Four renderings, three distinct sentences. The two in
packages/spec are fenced read-only on this card and cannot import the CLI's formatter
anyway (@objectstack/cli depends on @objectstack/spec, not the reverse), so they are
filed separately as #15095 and pinned here as a recorded fact rather than left for the next
reader to discover and "fix" in one command only.

The change

  • packages/cli/src/utils/format.tsformatConversionNotice(notice), a formatter and
    deliberately not a printer.
  • compile.ts / lint.ts / validate.ts — render through it; the three inline literals
    are gone (grep -c for the prose in each command source: 0, 0, 0).
  • packages/cli/test/validate-build-gate-parity.test.ts — the rule the old guard could not
    see, with a positive control that fails if the formatter stops carrying the sentence.
  • packages/cli/src/utils/format.conversion-notice.test.ts — the value pin on the rendered
    sentence, plus the recorded form-1 / form-3 split.
  • .changeset/cli-conversion-notice-one-source.mdpatch. Measured from this package's
    own files field: @objectstack/cli publishes dist, compiled from the edited src, so
    the diff changes the published package even though it changes nothing an author can
    observe. skip-changeset would have been wrong.

Red-first — four arms, each naming a different assertion

Every mutation was committed-first, proved on disk by blob hash, and restored by blob-hash
equality against the HEAD blob under trap ... EXIT INT TERM on absolute paths. The guard
is a source-text scanner, so no build or dist step participates in any arm.

armmutationexpectedmeasured
Areword lint.ts's copy, dropping the shared formatterred on the "renders through ONE formatter" assertionvitest exit 1lint.ts must render its ADR-0087 D2 conversion notices with formatConversionNotice(); 1 failed / 8 passed
Bkeep the call, add a verbatim inline copy beside itred on the "no inline copy" assertionvitest exit 1lint.ts spells the ADR-0087 D2 conversion notice out inline; 1 failed / 8 passed
Creword the ONE source in format.tsred on the positive control AND on the value pinvitest exit 1 — control fires, and the pin reports converted while loading where converted at load was expected; 2 failed / 10 passed
Dunmutated treegreenvitest exit 0 — 2 files, 12 tests passed

Blob evidence, arm by arm: lint.ts HEAD blob 43ace89d…; mutated to 82ef231d… (A) and
a25e93f7… (B); restored to 43ace89d… with git diff HEAD empty each time. format.ts
HEAD blob 53d51fe1…; mutated to 80839a7e… (C); restored to 53d51fe1…. Final
git status --porcelain -- packages/cli was empty.

Arm A is precisely the check the dispatch asked for — reword one copy and show the guard
redden — and arms B and C exist because A alone would not have told you which assertion
fired.

Verification

At final HEAD 9fc66faf on a clean tree, after merging origin/main (which brought in
PR #15062, the armed sibling the dispatch warned about, plus four others):

  • pnpm --filter @objectstack/cli typecheck — exit 0. Both halves: tsc --noEmit over
    src (which is where the new value pin lives, so it is type-checked), and
    check:test-typecheck over tsconfig.test.json, whose verdict line reads
    OK — @objectstack/cli's test layer compiles … 3 file(s) / 28 error(s) / 6 pinned signature(s) held — the pre-existing ledger, unmoved.
  • Targeted vitest, 6 files / 53 tests passed: the two new/edited guards, the three
    conversion-notice e2e suites (lint-conversion-notices,
    validate-json-failure-conversions, build-json-failure-conversions — these drive the
    real CLI over a live conversion via tsx, so they are the behavioural proof the output
    did not move), and i18n-check-help-surface-parity from the merged sibling PR.
  • Gate union derived by node scripts/pm/dispatch-gates.mjs --repo objectstack-ai/objectstack
    at that HEAD: 42 families (33 by path, 6 by change kind, 5 declared whole-tree).
    40 green. Two are NOT MEASURED and say so themselves: check-test-completeness.mjs
    grades a saved turbo run test log this box cannot produce ("the local reading for this
    gate is NOT MEASURED"), and check-half-states.mjs needs repo-scoped GitHub REST, which
    this container is refused (GET /repos/... returns 403 with no rate-limit headers while
    GET /rate_limit reports 15000 left). Five others first exited 3 on an unbuilt closure
    and went green after a full pnpm build: check:dual-build-cjs-loads, check:i18n,
    check:i18n-coverage, check:type-check-debt (which had also hit a local 300s cap), and
    the two named above stayed 3 for the reasons given.

Declared narrowing. The repo-wide pnpm lint sweep was not run locally; CI owns it and
runs it once regardless. Everything else in the derived union was run.

What this deliberately does not do

ConversionNotice.message and defineStack's warnConversionNotice are untouched. Whether
all of the renderings should descend from one source is a real question with a real cost on
either side, and it is #15095, not this card.


🤖 Generated with Claude Code

https://claude.ai/code/session_016yfqQh2dBgPAymYd7xipza

Generated by Claude Code


Generated by Claude Code

The human face of an ADR-0087 D2 conversion notice was written out three
times, verbatim, in os build / os validate / os lint. The three template
literals were byte-identical (measured: one distinct literal across the
three) and held equal by convention alone — the parity guard asserted each
command PASSES an onConversionNotice sink, never that they SAY the same
thing once they have one, so a reword in one command diverged silently with
every gate green.
Hoist the sentence into formatConversionNotice() in src/utils/format.ts and
render all three through it. It is a formatter, not a printer, which is what
makes one implementation possible: the three call sites genuinely differ in
DISPOSITION (build/lint print behind !flags.json, validate pushes into the
--strict warnings list) but not in what they say, so the difference costs the
function no parameter. Output is byte-identical.
Extend packages/cli/test/validate-build-gate-parity.test.ts with the rule the
old guard could not see: every authoring command renders through the one
formatter and none spells the sentence out inline, with a positive control so
it cannot pass vacuously on a CLI that says nothing.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016yfqQh2dBgPAymYd7xipza
…nsolidation
Part of #13743. `@objectstack/cli` publishes `dist`, compiled from the edited
`src`, so this diff changes the published package even though it changes
nothing an author can observe — `patch`, not `skip-changeset`.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016yfqQh2dBgPAymYd7xipza
…arity
Brings in PR #15062 (`os i18n check --help`), which lands in packages/cli
while this branch is open — the dispatch named it as the one to merge and
re-verify against. Also #15065 (rest), #15063/#15067 (docs) and #15055 (spec).
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016yfqQh2dBgPAymYd7xipza
@github-actions

Copy link
Copy Markdown
Contributor

📓 Docs Drift Check

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

30 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 ab47816914fd39709f4e9645370729ce1f4f9826.

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

What this run could not see
  • 1 anchor(s) matched too much of the corpus to be a work list: os validate (command, 43 pages)
  • 3 name(s) were too generic to anchor anything (single lowercase words)
  • the SDK route bridge reached 47 of 219 client-bound route-ledger rows — the other 172 have no registrar path: tail to select them, so pages documenting THEIR client methods cannot appear above, on this or any run. Of those 172: 14 are remediable by widening that discovery convention (an in-repo file declares the path; the convention did not scan it); 56 are structural — on a ledger where NOT ONE row is declared in-repo, so no discovery change reaches them at any price; 102 are undecided (no in-repo declaration, on a ledger that has other in-repo registrars — absence and an unreadable spelling are not distinguishable here). The rows themselves: node scripts/docs-audit/affected-docs.mjs --bridge-coverage
  • 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 — 22 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 ab47816914fd39709f4e9645370729ce1f4f9826packageMentionDocs.

Which tree this was computed on

This run read content/docs from 48aac740974ea994794664b7dce221ed4dfb15b1 — the merge of head 9fc66fafafab958e3ae03b0f43bf8989c9b72699 into base ab47816914fd39709f4e9645370729ce1f4f9826, 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 48aac740974ea994794664b7dce221ed4dfb15b1 && git checkout 48aac740974ea994794664b7dce221ed4dfb15b1
# afterwards, rebuild it from the two parents, which stay fetchable
git fetch origin ab47816914fd39709f4e9645370729ce1f4f9826 9fc66fafafab958e3ae03b0f43bf8989c9b72699 && git checkout -B drift-repro ab47816914fd39709f4e9645370729ce1f4f9826 && git merge --no-ff 9fc66fafafab958e3ae03b0f43bf8989c9b72699
node scripts/docs-audit/affected-docs.mjs --json ab47816914fd39709f4e9645370729ce1f4f9826

⚠️ 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 ab47816914fd39709f4e9645370729ce1f4f9826 → pass the list as
args.docs, on the commit named under Which tree this was computed on.

@github-actionsgithub-actionsBot added documentation Improvements or additions to documentation tests tooling labels Sep 3, 2026
@os-trump
os-trump marked this pull request as ready for review September 3, 2026 21:56
@os-trump
os-trump added this pull request to the merge queueSep 3, 2026
Merged via the queue into main with commit 4428dd5Sep 3, 2026
35 checks passed
@os-trump
os-trump deleted the claude/issue-13743-conversion-notice-wording-parity branch September 3, 2026 22:39
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

documentationImprovements or additions to documentationsize/mteststooling

Projects

None yet

Development

Successfully merging this pull request may close these issues.

The ADR-0087 conversion-notice wording is duplicated verbatim across all three authoring commands, and no gate holds the three equal

2 participants

@os-trump@claude
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Auto-enable theater mode on YouTube\n(function() {\n function tryTheater() {\n var btn = document.querySelector('button[aria-label=\"Theater mode\"], ytd-player #player button[title=\"Theater mode\"]');\n if (btn && !btn.classList.contains('activated')) {\n btn.click();\n }\n }\n \n // Try immediately\n tryTheater();\n \n // Try after navigation (SPA)\n var lastUrl = location.href;\n setInterval(function() {\n if (location.href !== lastUrl) {\n lastUrl = location.href;\n setTimeout(tryTheater, 500);\n }\n }, 1000);\n \n // Also try on player load\n var observer = new MutationObserver(tryTheater);\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "YouTube Theater Mode Default"); } } catch(__e) { console.warn('[Userscript:YouTube Theater Mode Default]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

refactor(cli): render the ADR-0087 conversion notice from one source, and gate it there - #15096

Merged
os-trump merged 3 commits into
mainfrom
claude/issue-13743-conversion-notice-wording-parity
Sep 3, 2026
Merged

refactor(cli): render the ADR-0087 conversion notice from one source, and gate it there#15096
os-trump merged 3 commits into
mainfrom
claude/issue-13743-conversion-notice-wording-parity

Conversation

@os-trump

Copy link
Copy Markdown
Collaborator

Fixes#13743

The ADR-0087 D2 conversion notice now has one source in packages/cli, and the parity
guard holds it there. Output is byte-for-byte unchanged.

Which of triage's three options, and why

Option 1 — extract a shared formatter — carrying the smallest useful half of option 2.
Triage required this to be stated rather than defaulted, and the deciding input was a
measurement rather than a preference.

Option 3's supporting argument is falsified. Its case was that validate.ts folds
its copy into a warnings list --strict judges while the other two print directly, so
"the three call sites are not fully interchangeable, and forcing an extraction may need a
parameter for the difference." The first clause is true and I verified it; the second does
not follow. The asymmetry is entirely in the disposition of the string, never in the
string:

commandwhat it does with the sentencethe sentence itself
os build (compile.ts)printWarning(...) behind !flags.jsonidentical
os lint (lint.ts)printWarning(...) behind !flags.jsonidentical
os validate (validate.ts)warnings.push(...), which --strict then judgesidentical

Measured at the branch point d17f352b1b: extracting the template literal from each of the
three lines and hashing it gives one distinct value, 124 bytes, sha256 prefix
7197b905060762c3. A pure formatConversionNotice(notice) therefore serves all three and
costs no parameter for the difference — the extraction option's stated price does not
materialise.

Why not option 2 alone. A comparator locks today's three copies together and detects a
reword only after someone has written it; one source makes the divergence impossible to
write. It is also the disposition this file already reached for the same problem:
printTruncationNotice in the same module carries the note "ONE implementation of that
sentence, deliberately … nine copies of the wording would be nine chances for them to drift
apart." House precedent, not a new idea.

What option 2 still buys, and is kept. Extraction does not stop a copy coming back.
The rule added to validate-build-gate-parity.test.ts is therefore structural rather than
comparative: every authoring command renders through the one formatter, and none spells the
sentence out inline.

Re-derived the count — and the probe returned more than three

The release comment required a bound with a control that could have returned a fourth. It
did. A whole-repo git grep (no pathspec) for the prose fragment converted at load
tuned to the sentence, not to the three known sites — finds a rendering the card does not
mention, in packages/spec/src/stack.zod.ts:

packages/cli/src/commands/compile.ts:238 the three the card counts
packages/cli/src/commands/lint.ts:580
packages/cli/src/commands/validate.ts:409
packages/spec/src/stack.zod.ts:2310 NOT in the card — defineStack's own face

and reading that seam turns up a fifth prose form on ConversionNotice.message
(packages/spec/src/conversions/apply.ts), which is what the --json payloads publish.

The card's "three" is correct as a count of the three authoring commands, and wrong as
a bound on the wording.
Four renderings, three distinct sentences. The two in
packages/spec are fenced read-only on this card and cannot import the CLI's formatter
anyway (@objectstack/cli depends on @objectstack/spec, not the reverse), so they are
filed separately as #15095 and pinned here as a recorded fact rather than left for the next
reader to discover and "fix" in one command only.

The change

  • packages/cli/src/utils/format.tsformatConversionNotice(notice), a formatter and
    deliberately not a printer.
  • compile.ts / lint.ts / validate.ts — render through it; the three inline literals
    are gone (grep -c for the prose in each command source: 0, 0, 0).
  • packages/cli/test/validate-build-gate-parity.test.ts — the rule the old guard could not
    see, with a positive control that fails if the formatter stops carrying the sentence.
  • packages/cli/src/utils/format.conversion-notice.test.ts — the value pin on the rendered
    sentence, plus the recorded form-1 / form-3 split.
  • .changeset/cli-conversion-notice-one-source.mdpatch. Measured from this package's
    own files field: @objectstack/cli publishes dist, compiled from the edited src, so
    the diff changes the published package even though it changes nothing an author can
    observe. skip-changeset would have been wrong.

Red-first — four arms, each naming a different assertion

Every mutation was committed-first, proved on disk by blob hash, and restored by blob-hash
equality against the HEAD blob under trap ... EXIT INT TERM on absolute paths. The guard
is a source-text scanner, so no build or dist step participates in any arm.

armmutationexpectedmeasured
Areword lint.ts's copy, dropping the shared formatterred on the "renders through ONE formatter" assertionvitest exit 1lint.ts must render its ADR-0087 D2 conversion notices with formatConversionNotice(); 1 failed / 8 passed
Bkeep the call, add a verbatim inline copy beside itred on the "no inline copy" assertionvitest exit 1lint.ts spells the ADR-0087 D2 conversion notice out inline; 1 failed / 8 passed
Creword the ONE source in format.tsred on the positive control AND on the value pinvitest exit 1 — control fires, and the pin reports converted while loading where converted at load was expected; 2 failed / 10 passed
Dunmutated treegreenvitest exit 0 — 2 files, 12 tests passed

Blob evidence, arm by arm: lint.ts HEAD blob 43ace89d…; mutated to 82ef231d… (A) and
a25e93f7… (B); restored to 43ace89d… with git diff HEAD empty each time. format.ts
HEAD blob 53d51fe1…; mutated to 80839a7e… (C); restored to 53d51fe1…. Final
git status --porcelain -- packages/cli was empty.

Arm A is precisely the check the dispatch asked for — reword one copy and show the guard
redden — and arms B and C exist because A alone would not have told you which assertion
fired.

Verification

At final HEAD 9fc66faf on a clean tree, after merging origin/main (which brought in
PR #15062, the armed sibling the dispatch warned about, plus four others):

  • pnpm --filter @objectstack/cli typecheck — exit 0. Both halves: tsc --noEmit over
    src (which is where the new value pin lives, so it is type-checked), and
    check:test-typecheck over tsconfig.test.json, whose verdict line reads
    OK — @objectstack/cli's test layer compiles … 3 file(s) / 28 error(s) / 6 pinned signature(s) held — the pre-existing ledger, unmoved.
  • Targeted vitest, 6 files / 53 tests passed: the two new/edited guards, the three
    conversion-notice e2e suites (lint-conversion-notices,
    validate-json-failure-conversions, build-json-failure-conversions — these drive the
    real CLI over a live conversion via tsx, so they are the behavioural proof the output
    did not move), and i18n-check-help-surface-parity from the merged sibling PR.
  • Gate union derived by node scripts/pm/dispatch-gates.mjs --repo objectstack-ai/objectstack
    at that HEAD: 42 families (33 by path, 6 by change kind, 5 declared whole-tree).
    40 green. Two are NOT MEASURED and say so themselves: check-test-completeness.mjs
    grades a saved turbo run test log this box cannot produce ("the local reading for this
    gate is NOT MEASURED"), and check-half-states.mjs needs repo-scoped GitHub REST, which
    this container is refused (GET /repos/... returns 403 with no rate-limit headers while
    GET /rate_limit reports 15000 left). Five others first exited 3 on an unbuilt closure
    and went green after a full pnpm build: check:dual-build-cjs-loads, check:i18n,
    check:i18n-coverage, check:type-check-debt (which had also hit a local 300s cap), and
    the two named above stayed 3 for the reasons given.

Declared narrowing. The repo-wide pnpm lint sweep was not run locally; CI owns it and
runs it once regardless. Everything else in the derived union was run.

What this deliberately does not do

ConversionNotice.message and defineStack's warnConversionNotice are untouched. Whether
all of the renderings should descend from one source is a real question with a real cost on
either side, and it is #15095, not this card.


🤖 Generated with Claude Code

https://claude.ai/code/session_016yfqQh2dBgPAymYd7xipza

Generated by Claude Code


Generated by Claude Code

The human face of an ADR-0087 D2 conversion notice was written out three
times, verbatim, in os build / os validate / os lint. The three template
literals were byte-identical (measured: one distinct literal across the
three) and held equal by convention alone — the parity guard asserted each
command PASSES an onConversionNotice sink, never that they SAY the same
thing once they have one, so a reword in one command diverged silently with
every gate green.
Hoist the sentence into formatConversionNotice() in src/utils/format.ts and
render all three through it. It is a formatter, not a printer, which is what
makes one implementation possible: the three call sites genuinely differ in
DISPOSITION (build/lint print behind !flags.json, validate pushes into the
--strict warnings list) but not in what they say, so the difference costs the
function no parameter. Output is byte-identical.
Extend packages/cli/test/validate-build-gate-parity.test.ts with the rule the
old guard could not see: every authoring command renders through the one
formatter and none spells the sentence out inline, with a positive control so
it cannot pass vacuously on a CLI that says nothing.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016yfqQh2dBgPAymYd7xipza
…nsolidation
Part of #13743. `@objectstack/cli` publishes `dist`, compiled from the edited
`src`, so this diff changes the published package even though it changes
nothing an author can observe — `patch`, not `skip-changeset`.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016yfqQh2dBgPAymYd7xipza
…arity
Brings in PR #15062 (`os i18n check --help`), which lands in packages/cli
while this branch is open — the dispatch named it as the one to merge and
re-verify against. Also #15065 (rest), #15063/#15067 (docs) and #15055 (spec).
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016yfqQh2dBgPAymYd7xipza
@github-actions

Copy link
Copy Markdown
Contributor

📓 Docs Drift Check

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

30 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 ab47816914fd39709f4e9645370729ce1f4f9826.

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

What this run could not see
  • 1 anchor(s) matched too much of the corpus to be a work list: os validate (command, 43 pages)
  • 3 name(s) were too generic to anchor anything (single lowercase words)
  • the SDK route bridge reached 47 of 219 client-bound route-ledger rows — the other 172 have no registrar path: tail to select them, so pages documenting THEIR client methods cannot appear above, on this or any run. Of those 172: 14 are remediable by widening that discovery convention (an in-repo file declares the path; the convention did not scan it); 56 are structural — on a ledger where NOT ONE row is declared in-repo, so no discovery change reaches them at any price; 102 are undecided (no in-repo declaration, on a ledger that has other in-repo registrars — absence and an unreadable spelling are not distinguishable here). The rows themselves: node scripts/docs-audit/affected-docs.mjs --bridge-coverage
  • 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 — 22 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 ab47816914fd39709f4e9645370729ce1f4f9826packageMentionDocs.

Which tree this was computed on

This run read content/docs from 48aac740974ea994794664b7dce221ed4dfb15b1 — the merge of head 9fc66fafafab958e3ae03b0f43bf8989c9b72699 into base ab47816914fd39709f4e9645370729ce1f4f9826, 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 48aac740974ea994794664b7dce221ed4dfb15b1 && git checkout 48aac740974ea994794664b7dce221ed4dfb15b1
# afterwards, rebuild it from the two parents, which stay fetchable
git fetch origin ab47816914fd39709f4e9645370729ce1f4f9826 9fc66fafafab958e3ae03b0f43bf8989c9b72699 && git checkout -B drift-repro ab47816914fd39709f4e9645370729ce1f4f9826 && git merge --no-ff 9fc66fafafab958e3ae03b0f43bf8989c9b72699
node scripts/docs-audit/affected-docs.mjs --json ab47816914fd39709f4e9645370729ce1f4f9826

⚠️ 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 ab47816914fd39709f4e9645370729ce1f4f9826 → pass the list as
args.docs, on the commit named under Which tree this was computed on.

@github-actionsgithub-actionsBot added documentation Improvements or additions to documentation tests tooling labels Sep 3, 2026
@os-trump
os-trump marked this pull request as ready for review September 3, 2026 21:56
@os-trump
os-trump added this pull request to the merge queueSep 3, 2026
Merged via the queue into main with commit 4428dd5Sep 3, 2026
35 checks passed
@os-trump
os-trump deleted the claude/issue-13743-conversion-notice-wording-parity branch September 3, 2026 22:39
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

documentationImprovements or additions to documentationsize/mteststooling

Projects

None yet

Development

Successfully merging this pull request may close these issues.

The ADR-0087 conversion-notice wording is duplicated verbatim across all three authoring commands, and no gate holds the three equal

2 participants

@os-trump@claude
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Remove or un-stick sticky/fixed headers that block content\n(function() {\n function unstick() {\n document.querySelectorAll('header, nav, [role=\"banner\"], .header, .navbar, .sticky, .fixed-top, [style*=\"position: fixed\"], [style*=\"position:sticky\"]').forEach(function(el) {\n if (el.style.position === 'fixed' || el.style.position === 'sticky' || \n getComputedStyle(el).position === 'fixed' || getComputedStyle(el).position === 'sticky') {\n el.style.position = 'static';\n el.style.top = 'auto';\n el.style.zIndex = 'auto';\n }\n });\n }\n \n unstick();\n \n var observer = new MutationObserver(unstick);\n observer.observe(document.body, { childList: true, subtree: true, attributes: true, attributeFilter: ['style', 'class'] });\n})();", "Kill Sticky Headers"); } } catch(__e) { console.warn('[Userscript:Kill Sticky Headers]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

refactor(cli): render the ADR-0087 conversion notice from one source, and gate it there - #15096

Merged
os-trump merged 3 commits into
mainfrom
claude/issue-13743-conversion-notice-wording-parity
Sep 3, 2026
Merged

refactor(cli): render the ADR-0087 conversion notice from one source, and gate it there#15096
os-trump merged 3 commits into
mainfrom
claude/issue-13743-conversion-notice-wording-parity

Conversation

@os-trump

Copy link
Copy Markdown
Collaborator

Fixes#13743

The ADR-0087 D2 conversion notice now has one source in packages/cli, and the parity
guard holds it there. Output is byte-for-byte unchanged.

Which of triage's three options, and why

Option 1 — extract a shared formatter — carrying the smallest useful half of option 2.
Triage required this to be stated rather than defaulted, and the deciding input was a
measurement rather than a preference.

Option 3's supporting argument is falsified. Its case was that validate.ts folds
its copy into a warnings list --strict judges while the other two print directly, so
"the three call sites are not fully interchangeable, and forcing an extraction may need a
parameter for the difference." The first clause is true and I verified it; the second does
not follow. The asymmetry is entirely in the disposition of the string, never in the
string:

commandwhat it does with the sentencethe sentence itself
os build (compile.ts)printWarning(...) behind !flags.jsonidentical
os lint (lint.ts)printWarning(...) behind !flags.jsonidentical
os validate (validate.ts)warnings.push(...), which --strict then judgesidentical

Measured at the branch point d17f352b1b: extracting the template literal from each of the
three lines and hashing it gives one distinct value, 124 bytes, sha256 prefix
7197b905060762c3. A pure formatConversionNotice(notice) therefore serves all three and
costs no parameter for the difference — the extraction option's stated price does not
materialise.

Why not option 2 alone. A comparator locks today's three copies together and detects a
reword only after someone has written it; one source makes the divergence impossible to
write. It is also the disposition this file already reached for the same problem:
printTruncationNotice in the same module carries the note "ONE implementation of that
sentence, deliberately … nine copies of the wording would be nine chances for them to drift
apart." House precedent, not a new idea.

What option 2 still buys, and is kept. Extraction does not stop a copy coming back.
The rule added to validate-build-gate-parity.test.ts is therefore structural rather than
comparative: every authoring command renders through the one formatter, and none spells the
sentence out inline.

Re-derived the count — and the probe returned more than three

The release comment required a bound with a control that could have returned a fourth. It
did. A whole-repo git grep (no pathspec) for the prose fragment converted at load
tuned to the sentence, not to the three known sites — finds a rendering the card does not
mention, in packages/spec/src/stack.zod.ts:

packages/cli/src/commands/compile.ts:238 the three the card counts
packages/cli/src/commands/lint.ts:580
packages/cli/src/commands/validate.ts:409
packages/spec/src/stack.zod.ts:2310 NOT in the card — defineStack's own face

and reading that seam turns up a fifth prose form on ConversionNotice.message
(packages/spec/src/conversions/apply.ts), which is what the --json payloads publish.

The card's "three" is correct as a count of the three authoring commands, and wrong as
a bound on the wording.
Four renderings, three distinct sentences. The two in
packages/spec are fenced read-only on this card and cannot import the CLI's formatter
anyway (@objectstack/cli depends on @objectstack/spec, not the reverse), so they are
filed separately as #15095 and pinned here as a recorded fact rather than left for the next
reader to discover and "fix" in one command only.

The change

  • packages/cli/src/utils/format.tsformatConversionNotice(notice), a formatter and
    deliberately not a printer.
  • compile.ts / lint.ts / validate.ts — render through it; the three inline literals
    are gone (grep -c for the prose in each command source: 0, 0, 0).
  • packages/cli/test/validate-build-gate-parity.test.ts — the rule the old guard could not
    see, with a positive control that fails if the formatter stops carrying the sentence.
  • packages/cli/src/utils/format.conversion-notice.test.ts — the value pin on the rendered
    sentence, plus the recorded form-1 / form-3 split.
  • .changeset/cli-conversion-notice-one-source.mdpatch. Measured from this package's
    own files field: @objectstack/cli publishes dist, compiled from the edited src, so
    the diff changes the published package even though it changes nothing an author can
    observe. skip-changeset would have been wrong.

Red-first — four arms, each naming a different assertion

Every mutation was committed-first, proved on disk by blob hash, and restored by blob-hash
equality against the HEAD blob under trap ... EXIT INT TERM on absolute paths. The guard
is a source-text scanner, so no build or dist step participates in any arm.

armmutationexpectedmeasured
Areword lint.ts's copy, dropping the shared formatterred on the "renders through ONE formatter" assertionvitest exit 1lint.ts must render its ADR-0087 D2 conversion notices with formatConversionNotice(); 1 failed / 8 passed
Bkeep the call, add a verbatim inline copy beside itred on the "no inline copy" assertionvitest exit 1lint.ts spells the ADR-0087 D2 conversion notice out inline; 1 failed / 8 passed
Creword the ONE source in format.tsred on the positive control AND on the value pinvitest exit 1 — control fires, and the pin reports converted while loading where converted at load was expected; 2 failed / 10 passed
Dunmutated treegreenvitest exit 0 — 2 files, 12 tests passed

Blob evidence, arm by arm: lint.ts HEAD blob 43ace89d…; mutated to 82ef231d… (A) and
a25e93f7… (B); restored to 43ace89d… with git diff HEAD empty each time. format.ts
HEAD blob 53d51fe1…; mutated to 80839a7e… (C); restored to 53d51fe1…. Final
git status --porcelain -- packages/cli was empty.

Arm A is precisely the check the dispatch asked for — reword one copy and show the guard
redden — and arms B and C exist because A alone would not have told you which assertion
fired.

Verification

At final HEAD 9fc66faf on a clean tree, after merging origin/main (which brought in
PR #15062, the armed sibling the dispatch warned about, plus four others):

  • pnpm --filter @objectstack/cli typecheck — exit 0. Both halves: tsc --noEmit over
    src (which is where the new value pin lives, so it is type-checked), and
    check:test-typecheck over tsconfig.test.json, whose verdict line reads
    OK — @objectstack/cli's test layer compiles … 3 file(s) / 28 error(s) / 6 pinned signature(s) held — the pre-existing ledger, unmoved.
  • Targeted vitest, 6 files / 53 tests passed: the two new/edited guards, the three
    conversion-notice e2e suites (lint-conversion-notices,
    validate-json-failure-conversions, build-json-failure-conversions — these drive the
    real CLI over a live conversion via tsx, so they are the behavioural proof the output
    did not move), and i18n-check-help-surface-parity from the merged sibling PR.
  • Gate union derived by node scripts/pm/dispatch-gates.mjs --repo objectstack-ai/objectstack
    at that HEAD: 42 families (33 by path, 6 by change kind, 5 declared whole-tree).
    40 green. Two are NOT MEASURED and say so themselves: check-test-completeness.mjs
    grades a saved turbo run test log this box cannot produce ("the local reading for this
    gate is NOT MEASURED"), and check-half-states.mjs needs repo-scoped GitHub REST, which
    this container is refused (GET /repos/... returns 403 with no rate-limit headers while
    GET /rate_limit reports 15000 left). Five others first exited 3 on an unbuilt closure
    and went green after a full pnpm build: check:dual-build-cjs-loads, check:i18n,
    check:i18n-coverage, check:type-check-debt (which had also hit a local 300s cap), and
    the two named above stayed 3 for the reasons given.

Declared narrowing. The repo-wide pnpm lint sweep was not run locally; CI owns it and
runs it once regardless. Everything else in the derived union was run.

What this deliberately does not do

ConversionNotice.message and defineStack's warnConversionNotice are untouched. Whether
all of the renderings should descend from one source is a real question with a real cost on
either side, and it is #15095, not this card.


🤖 Generated with Claude Code

https://claude.ai/code/session_016yfqQh2dBgPAymYd7xipza

Generated by Claude Code


Generated by Claude Code

The human face of an ADR-0087 D2 conversion notice was written out three
times, verbatim, in os build / os validate / os lint. The three template
literals were byte-identical (measured: one distinct literal across the
three) and held equal by convention alone — the parity guard asserted each
command PASSES an onConversionNotice sink, never that they SAY the same
thing once they have one, so a reword in one command diverged silently with
every gate green.
Hoist the sentence into formatConversionNotice() in src/utils/format.ts and
render all three through it. It is a formatter, not a printer, which is what
makes one implementation possible: the three call sites genuinely differ in
DISPOSITION (build/lint print behind !flags.json, validate pushes into the
--strict warnings list) but not in what they say, so the difference costs the
function no parameter. Output is byte-identical.
Extend packages/cli/test/validate-build-gate-parity.test.ts with the rule the
old guard could not see: every authoring command renders through the one
formatter and none spells the sentence out inline, with a positive control so
it cannot pass vacuously on a CLI that says nothing.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016yfqQh2dBgPAymYd7xipza
…nsolidation
Part of #13743. `@objectstack/cli` publishes `dist`, compiled from the edited
`src`, so this diff changes the published package even though it changes
nothing an author can observe — `patch`, not `skip-changeset`.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016yfqQh2dBgPAymYd7xipza
…arity
Brings in PR #15062 (`os i18n check --help`), which lands in packages/cli
while this branch is open — the dispatch named it as the one to merge and
re-verify against. Also #15065 (rest), #15063/#15067 (docs) and #15055 (spec).
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016yfqQh2dBgPAymYd7xipza
@github-actions

Copy link
Copy Markdown
Contributor

📓 Docs Drift Check

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

30 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 ab47816914fd39709f4e9645370729ce1f4f9826.

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

What this run could not see
  • 1 anchor(s) matched too much of the corpus to be a work list: os validate (command, 43 pages)
  • 3 name(s) were too generic to anchor anything (single lowercase words)
  • the SDK route bridge reached 47 of 219 client-bound route-ledger rows — the other 172 have no registrar path: tail to select them, so pages documenting THEIR client methods cannot appear above, on this or any run. Of those 172: 14 are remediable by widening that discovery convention (an in-repo file declares the path; the convention did not scan it); 56 are structural — on a ledger where NOT ONE row is declared in-repo, so no discovery change reaches them at any price; 102 are undecided (no in-repo declaration, on a ledger that has other in-repo registrars — absence and an unreadable spelling are not distinguishable here). The rows themselves: node scripts/docs-audit/affected-docs.mjs --bridge-coverage
  • 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 — 22 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 ab47816914fd39709f4e9645370729ce1f4f9826packageMentionDocs.

Which tree this was computed on

This run read content/docs from 48aac740974ea994794664b7dce221ed4dfb15b1 — the merge of head 9fc66fafafab958e3ae03b0f43bf8989c9b72699 into base ab47816914fd39709f4e9645370729ce1f4f9826, 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 48aac740974ea994794664b7dce221ed4dfb15b1 && git checkout 48aac740974ea994794664b7dce221ed4dfb15b1
# afterwards, rebuild it from the two parents, which stay fetchable
git fetch origin ab47816914fd39709f4e9645370729ce1f4f9826 9fc66fafafab958e3ae03b0f43bf8989c9b72699 && git checkout -B drift-repro ab47816914fd39709f4e9645370729ce1f4f9826 && git merge --no-ff 9fc66fafafab958e3ae03b0f43bf8989c9b72699
node scripts/docs-audit/affected-docs.mjs --json ab47816914fd39709f4e9645370729ce1f4f9826

⚠️ 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 ab47816914fd39709f4e9645370729ce1f4f9826 → pass the list as
args.docs, on the commit named under Which tree this was computed on.

@github-actionsgithub-actionsBot added documentation Improvements or additions to documentation tests tooling labels Sep 3, 2026
@os-trump
os-trump marked this pull request as ready for review September 3, 2026 21:56
@os-trump
os-trump added this pull request to the merge queueSep 3, 2026
Merged via the queue into main with commit 4428dd5Sep 3, 2026
35 checks passed
@os-trump
os-trump deleted the claude/issue-13743-conversion-notice-wording-parity branch September 3, 2026 22:39
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

documentationImprovements or additions to documentationsize/mteststooling

Projects

None yet

Development

Successfully merging this pull request may close these issues.

The ADR-0087 conversion-notice wording is duplicated verbatim across all three authoring commands, and no gate holds the three equal

2 participants

@os-trump@claude
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Universal Dark Mode - works on any site\n(function() {\n var enabled = true;\n \n function applyDarkMode() {\n if (!enabled) return;\n \n // Create style element if it doesn't exist\n var style = document.getElementById('universal-dark-mode-style');\n if (!style) {\n style = document.createElement('style');\n style.id = 'universal-dark-mode-style';\n document.head.appendChild(style);\n }\n \n // Dark mode CSS - inverts colors but preserves images/video\n style.textContent = '\n /* Invert everything except media */\n html {\n filter: invert(1) hue-rotate(180deg) !important;\n background: #1a1a2e !important;\n }\n \n /* Restore images, videos, iframes, canvas */\n img, video, iframe, canvas, svg, picture, [style*=\"background-image\"] {\n filter: invert(1) hue-rotate(180deg) !important;\n }\n \n /* Preserve specific elements that should not be inverted */\n .no-dark-mode, .no-dark-mode *,\n [data-theme=\"light\"], [data-theme=\"light\"],\n .ace_editor, .ace_editor *,\n .CodeMirror, .CodeMirror *,\n .monaco-editor, .monaco-editor *,\n .markdown-body pre, .markdown-body pre *,\n .highlight, .highlight *,\n pre code, pre code * {\n filter: none !important;\n }\n \n /* Fix common UI elements */\n .modal, .popup, .dropdown-menu, .tooltip, .popover {\n filter: invert(1) hue-rotate(180deg) !important;\n background: #2d2d44 !important;\n border-color: #444 !important;\n }\n \n /* Scrollbars */\n ::-webkit-scrollbar { background: #1a1a2e !important; }\n ::-webkit-scrollbar-thumb { background: #444 !important; }\n ::-webkit-scrollbar-thumb:hover { background: #555 !important; }\n \n /* Selection */\n ::selection { background: #4ecdc4 !important; color: #1a1a2e !important; }\n ::-moz-selection { background: #4ecdc4 !important; color: #1a1a2e !important; }\n ';\n }\n \n function removeDarkMode() {\n var style = document.getElementById('universal-dark-mode-style');\n if (style) style.remove();\n }\n \n // Toggle with Alt+Shift+D\n document.addEventListener('keydown', function(e) {\n if (e.altKey && e.shiftKey && e.key === 'D') {\n e.preventDefault();\n enabled = !enabled;\n if (enabled) {\n applyDarkMode();\n console.log('[Universal Dark Mode] Enabled');\n } else {\n removeDarkMode();\n console.log('[Universal Dark Mode] Disabled');\n }\n }\n });\n \n // Apply on load\n applyDarkMode();\n \n // Re-apply on dynamic content\n var observer = new MutationObserver(function(mutations) {\n if (enabled && !document.getElementById('universal-dark-mode-style')) {\n applyDarkMode();\n }\n });\n observer.observe(document.head, { childList: true });\n \n console.log('[Universal Dark Mode] Loaded - Press Alt+Shift+D to toggle');\n})();", "Universal Dark Mode"); } } catch(__e) { console.warn('[Userscript:Universal Dark Mode]', __e); } })(); })();
Skip to content

refactor(cli): render the ADR-0087 conversion notice from one source, and gate it there - #15096

Merged
os-trump merged 3 commits into
mainfrom
claude/issue-13743-conversion-notice-wording-parity
Sep 3, 2026
Merged

refactor(cli): render the ADR-0087 conversion notice from one source, and gate it there#15096
os-trump merged 3 commits into
mainfrom
claude/issue-13743-conversion-notice-wording-parity

Conversation

@os-trump

Copy link
Copy Markdown
Collaborator

Fixes#13743

The ADR-0087 D2 conversion notice now has one source in packages/cli, and the parity
guard holds it there. Output is byte-for-byte unchanged.

Which of triage's three options, and why

Option 1 — extract a shared formatter — carrying the smallest useful half of option 2.
Triage required this to be stated rather than defaulted, and the deciding input was a
measurement rather than a preference.

Option 3's supporting argument is falsified. Its case was that validate.ts folds
its copy into a warnings list --strict judges while the other two print directly, so
"the three call sites are not fully interchangeable, and forcing an extraction may need a
parameter for the difference." The first clause is true and I verified it; the second does
not follow. The asymmetry is entirely in the disposition of the string, never in the
string:

commandwhat it does with the sentencethe sentence itself
os build (compile.ts)printWarning(...) behind !flags.jsonidentical
os lint (lint.ts)printWarning(...) behind !flags.jsonidentical
os validate (validate.ts)warnings.push(...), which --strict then judgesidentical

Measured at the branch point d17f352b1b: extracting the template literal from each of the
three lines and hashing it gives one distinct value, 124 bytes, sha256 prefix
7197b905060762c3. A pure formatConversionNotice(notice) therefore serves all three and
costs no parameter for the difference — the extraction option's stated price does not
materialise.

Why not option 2 alone. A comparator locks today's three copies together and detects a
reword only after someone has written it; one source makes the divergence impossible to
write. It is also the disposition this file already reached for the same problem:
printTruncationNotice in the same module carries the note "ONE implementation of that
sentence, deliberately … nine copies of the wording would be nine chances for them to drift
apart." House precedent, not a new idea.

What option 2 still buys, and is kept. Extraction does not stop a copy coming back.
The rule added to validate-build-gate-parity.test.ts is therefore structural rather than
comparative: every authoring command renders through the one formatter, and none spells the
sentence out inline.

Re-derived the count — and the probe returned more than three

The release comment required a bound with a control that could have returned a fourth. It
did. A whole-repo git grep (no pathspec) for the prose fragment converted at load
tuned to the sentence, not to the three known sites — finds a rendering the card does not
mention, in packages/spec/src/stack.zod.ts:

packages/cli/src/commands/compile.ts:238 the three the card counts
packages/cli/src/commands/lint.ts:580
packages/cli/src/commands/validate.ts:409
packages/spec/src/stack.zod.ts:2310 NOT in the card — defineStack's own face

and reading that seam turns up a fifth prose form on ConversionNotice.message
(packages/spec/src/conversions/apply.ts), which is what the --json payloads publish.

The card's "three" is correct as a count of the three authoring commands, and wrong as
a bound on the wording.
Four renderings, three distinct sentences. The two in
packages/spec are fenced read-only on this card and cannot import the CLI's formatter
anyway (@objectstack/cli depends on @objectstack/spec, not the reverse), so they are
filed separately as #15095 and pinned here as a recorded fact rather than left for the next
reader to discover and "fix" in one command only.

The change

  • packages/cli/src/utils/format.tsformatConversionNotice(notice), a formatter and
    deliberately not a printer.
  • compile.ts / lint.ts / validate.ts — render through it; the three inline literals
    are gone (grep -c for the prose in each command source: 0, 0, 0).
  • packages/cli/test/validate-build-gate-parity.test.ts — the rule the old guard could not
    see, with a positive control that fails if the formatter stops carrying the sentence.
  • packages/cli/src/utils/format.conversion-notice.test.ts — the value pin on the rendered
    sentence, plus the recorded form-1 / form-3 split.
  • .changeset/cli-conversion-notice-one-source.mdpatch. Measured from this package's
    own files field: @objectstack/cli publishes dist, compiled from the edited src, so
    the diff changes the published package even though it changes nothing an author can
    observe. skip-changeset would have been wrong.

Red-first — four arms, each naming a different assertion

Every mutation was committed-first, proved on disk by blob hash, and restored by blob-hash
equality against the HEAD blob under trap ... EXIT INT TERM on absolute paths. The guard
is a source-text scanner, so no build or dist step participates in any arm.

armmutationexpectedmeasured
Areword lint.ts's copy, dropping the shared formatterred on the "renders through ONE formatter" assertionvitest exit 1lint.ts must render its ADR-0087 D2 conversion notices with formatConversionNotice(); 1 failed / 8 passed
Bkeep the call, add a verbatim inline copy beside itred on the "no inline copy" assertionvitest exit 1lint.ts spells the ADR-0087 D2 conversion notice out inline; 1 failed / 8 passed
Creword the ONE source in format.tsred on the positive control AND on the value pinvitest exit 1 — control fires, and the pin reports converted while loading where converted at load was expected; 2 failed / 10 passed
Dunmutated treegreenvitest exit 0 — 2 files, 12 tests passed

Blob evidence, arm by arm: lint.ts HEAD blob 43ace89d…; mutated to 82ef231d… (A) and
a25e93f7… (B); restored to 43ace89d… with git diff HEAD empty each time. format.ts
HEAD blob 53d51fe1…; mutated to 80839a7e… (C); restored to 53d51fe1…. Final
git status --porcelain -- packages/cli was empty.

Arm A is precisely the check the dispatch asked for — reword one copy and show the guard
redden — and arms B and C exist because A alone would not have told you which assertion
fired.

Verification

At final HEAD 9fc66faf on a clean tree, after merging origin/main (which brought in
PR #15062, the armed sibling the dispatch warned about, plus four others):

  • pnpm --filter @objectstack/cli typecheck — exit 0. Both halves: tsc --noEmit over
    src (which is where the new value pin lives, so it is type-checked), and
    check:test-typecheck over tsconfig.test.json, whose verdict line reads
    OK — @objectstack/cli's test layer compiles … 3 file(s) / 28 error(s) / 6 pinned signature(s) held — the pre-existing ledger, unmoved.
  • Targeted vitest, 6 files / 53 tests passed: the two new/edited guards, the three
    conversion-notice e2e suites (lint-conversion-notices,
    validate-json-failure-conversions, build-json-failure-conversions — these drive the
    real CLI over a live conversion via tsx, so they are the behavioural proof the output
    did not move), and i18n-check-help-surface-parity from the merged sibling PR.
  • Gate union derived by node scripts/pm/dispatch-gates.mjs --repo objectstack-ai/objectstack
    at that HEAD: 42 families (33 by path, 6 by change kind, 5 declared whole-tree).
    40 green. Two are NOT MEASURED and say so themselves: check-test-completeness.mjs
    grades a saved turbo run test log this box cannot produce ("the local reading for this
    gate is NOT MEASURED"), and check-half-states.mjs needs repo-scoped GitHub REST, which
    this container is refused (GET /repos/... returns 403 with no rate-limit headers while
    GET /rate_limit reports 15000 left). Five others first exited 3 on an unbuilt closure
    and went green after a full pnpm build: check:dual-build-cjs-loads, check:i18n,
    check:i18n-coverage, check:type-check-debt (which had also hit a local 300s cap), and
    the two named above stayed 3 for the reasons given.

Declared narrowing. The repo-wide pnpm lint sweep was not run locally; CI owns it and
runs it once regardless. Everything else in the derived union was run.

What this deliberately does not do

ConversionNotice.message and defineStack's warnConversionNotice are untouched. Whether
all of the renderings should descend from one source is a real question with a real cost on
either side, and it is #15095, not this card.


🤖 Generated with Claude Code

https://claude.ai/code/session_016yfqQh2dBgPAymYd7xipza

Generated by Claude Code


Generated by Claude Code

The human face of an ADR-0087 D2 conversion notice was written out three
times, verbatim, in os build / os validate / os lint. The three template
literals were byte-identical (measured: one distinct literal across the
three) and held equal by convention alone — the parity guard asserted each
command PASSES an onConversionNotice sink, never that they SAY the same
thing once they have one, so a reword in one command diverged silently with
every gate green.
Hoist the sentence into formatConversionNotice() in src/utils/format.ts and
render all three through it. It is a formatter, not a printer, which is what
makes one implementation possible: the three call sites genuinely differ in
DISPOSITION (build/lint print behind !flags.json, validate pushes into the
--strict warnings list) but not in what they say, so the difference costs the
function no parameter. Output is byte-identical.
Extend packages/cli/test/validate-build-gate-parity.test.ts with the rule the
old guard could not see: every authoring command renders through the one
formatter and none spells the sentence out inline, with a positive control so
it cannot pass vacuously on a CLI that says nothing.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016yfqQh2dBgPAymYd7xipza
…nsolidation
Part of #13743. `@objectstack/cli` publishes `dist`, compiled from the edited
`src`, so this diff changes the published package even though it changes
nothing an author can observe — `patch`, not `skip-changeset`.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016yfqQh2dBgPAymYd7xipza
…arity
Brings in PR #15062 (`os i18n check --help`), which lands in packages/cli
while this branch is open — the dispatch named it as the one to merge and
re-verify against. Also #15065 (rest), #15063/#15067 (docs) and #15055 (spec).
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016yfqQh2dBgPAymYd7xipza
@github-actions

Copy link
Copy Markdown
Contributor

📓 Docs Drift Check

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

30 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 ab47816914fd39709f4e9645370729ce1f4f9826.

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

What this run could not see
  • 1 anchor(s) matched too much of the corpus to be a work list: os validate (command, 43 pages)
  • 3 name(s) were too generic to anchor anything (single lowercase words)
  • the SDK route bridge reached 47 of 219 client-bound route-ledger rows — the other 172 have no registrar path: tail to select them, so pages documenting THEIR client methods cannot appear above, on this or any run. Of those 172: 14 are remediable by widening that discovery convention (an in-repo file declares the path; the convention did not scan it); 56 are structural — on a ledger where NOT ONE row is declared in-repo, so no discovery change reaches them at any price; 102 are undecided (no in-repo declaration, on a ledger that has other in-repo registrars — absence and an unreadable spelling are not distinguishable here). The rows themselves: node scripts/docs-audit/affected-docs.mjs --bridge-coverage
  • 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 — 22 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 ab47816914fd39709f4e9645370729ce1f4f9826packageMentionDocs.

Which tree this was computed on

This run read content/docs from 48aac740974ea994794664b7dce221ed4dfb15b1 — the merge of head 9fc66fafafab958e3ae03b0f43bf8989c9b72699 into base ab47816914fd39709f4e9645370729ce1f4f9826, 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 48aac740974ea994794664b7dce221ed4dfb15b1 && git checkout 48aac740974ea994794664b7dce221ed4dfb15b1
# afterwards, rebuild it from the two parents, which stay fetchable
git fetch origin ab47816914fd39709f4e9645370729ce1f4f9826 9fc66fafafab958e3ae03b0f43bf8989c9b72699 && git checkout -B drift-repro ab47816914fd39709f4e9645370729ce1f4f9826 && git merge --no-ff 9fc66fafafab958e3ae03b0f43bf8989c9b72699
node scripts/docs-audit/affected-docs.mjs --json ab47816914fd39709f4e9645370729ce1f4f9826

⚠️ 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 ab47816914fd39709f4e9645370729ce1f4f9826 → pass the list as
args.docs, on the commit named under Which tree this was computed on.

@github-actionsgithub-actionsBot added documentation Improvements or additions to documentation tests tooling labels Sep 3, 2026
@os-trump
os-trump marked this pull request as ready for review September 3, 2026 21:56
@os-trump
os-trump added this pull request to the merge queueSep 3, 2026
Merged via the queue into main with commit 4428dd5Sep 3, 2026
35 checks passed
@os-trump
os-trump deleted the claude/issue-13743-conversion-notice-wording-parity branch September 3, 2026 22:39
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

documentationImprovements or additions to documentationsize/mteststooling

Projects

None yet

Development

Successfully merging this pull request may close these issues.

The ADR-0087 conversion-notice wording is duplicated verbatim across all three authoring commands, and no gate holds the three equal

2 participants

@os-trump@claude