fix(cli,verify,dogfood): branch the multi-org remedy on the ABSENCE, not on one failure kind - #15158

Merged
os-trump merged 4 commits into
mainfrom
claude/issue-14270-remedy-formatter-third-kind
Sep 4, 2026
Merged

fix(cli,verify,dogfood): branch the multi-org remedy on the ABSENCE, not on one failure kind#15158
os-trump merged 4 commits into
mainfrom
claude/issue-14270-remedy-formatter-third-kind

Conversation

@os-trump

Copy link
Copy Markdown
Collaborator

Fixes#14270

What was wrong

HostImportFailureKind got a third member in #14041declared-no-loadable-entry: the app declares the package, the install delivered it, and the package's own exports names no runtime entry Node can load (a types-only or browser-only publish, or an unexported subpath). Three consumers pick their remedy line with a two-way branch written when the type had exactly two members, so the third kind fell into the else leg and printed the declare remedy:

sitesymbolwhat the third kind rendered
packages/cli/src/commands/serve.tsformatOrganizationsInstallRemedy"add the package to THIS APP — declare it in the app's package.json and install"
packages/verify/src/harness.tsthe remedy in bootStack's multi-tenant fatal"Install/link it in THIS APP — and DECLARE it in that app's package.json"
packages/qa/dogfood/test/enterprise-organizations.tsprobeOrganizations's refusal / skip reason"declare it in the app's own package.json and install it"

Each of those three also interpolates the importer's own message, which words the case correctly ("the package publishes no entry that Node can load … the remedy lives in the package"). So the bullet an operator reads first contradicted the diagnosis printed underneath it, and prescribed two actions that were already done and could not have helped. That is the confidently-wrong-verdict class #14041 removed one layer down, reintroduced by its callers.

The shape

Each branch now asks "is the declaration the problem?" rather than testing one kind:

  • undeclared ⇒ declare remedy — unchanged
  • declared-unresolvable ⇒ install remedy — unchanged
  • declared-no-loadable-entryprescribes nothing. It names the two things that are not the problem and defers to the importer's message, which every one of the three sites already prints.

No fourth remedy sentence is minted. The importer words the package-shape case once, in the place that measured it; a local re-wording in three consumers would be three copies to drift. The deferral follows an idiom already in serve.tsformatOrganizationsMountFatal's "its message is the authority on the remedy; this CLI has no further detail to add" — and the same reasoning formatI18nLoadDiagnostic's docblock gives for interpolating only the kind TOKEN.

An error carrying no kind never came from the host importer at all and keeps the declare-and-install arm it has always had. Narrowing that one is a different question about a different fact, and is not this branch's.

⛔ The seam with #14278 / PR #15047

packages/types/src/node.ts is not touched — verified on the diff:

git diff --name-only origin/main...HEAD | grep -E '^packages/types/' # no match

The strings this card defers to (/publishes no entry/, /INSTALL problem/) are pinned by #14278's tests in packages/types/src/node.test.ts. This PR changes the three callers' remedy bullets so they stop contradicting that message.

Proof

Red-first, per formatter, naming which arm fired. Tests landed in 063d976b7a, implementation in 78a0a7587f. Against the tests-only tree, all three sites reproduced the defect with a fixture carrying the third kind:

serve.ts expected " • this app DECLARES @objectstack/organizations (dependencies: "^1.2.3") and it IS"
received " • add @objectstack/organizations (the enterprise multi-org runtime) to THIS APP"
harness.ts received "… Install/link it in THIS APP (/tmp/os-verify-org-host-no-entry-AFcXGm) — and DECLARE it
in that app's package.json …" followed, in the same string, by the importer's
"the package publishes no entry that Node can load"
dogfood received "To enable it, declare @objectstack/organizations in /tmp/os-dogfood-org-no-entry-gd5Vpw's
own package.json and install it …" same contradiction, same string

The verify and dogfood fixtures are real host roots on disk — a real node_modules, a real installed package whose manifest publishes exports: { ".": { types: "./index.d.ts" } } and nothing else, nothing mocked. Neither condition set resolves it (ESM_IMPORT_CONDITIONS / CJS_REQUIRE_CONDITIONS), so the importer classifies it declared-no-loadable-entry for real rather than by injection.

Reverse verification — the three formatters restored to their pre-implementation bytes, same suites re-run, then restored:

mutation ON DISK (blob hash + marker counts)
packages/cli/src/commands/serve.ts now=bd070ffe == pre? YES != head? YES kind-marker-count=1
packages/verify/src/harness.ts now=479de177 == pre? YES != head? YES kind-marker-count=0
packages/qa/dogfood/test/enterprise-organizations.ts now=cc6284e9 == pre? YES != head? YES kind-marker-count=0
× site 2b — the `declared-no-loadable-entry` remedy DEFERS … Tests 1 failed | 11 passed (12)
× DEFERS to the importer for a declared, installed package … Tests 1 failed | 6 passed (7)
× DEFERS to the importer when the package is declared, installed …
× THROWS with that same deferral when the run declares the package Tests 2 failed | 7 passed (9)

Restore proven by blob-hash equality against the HEAD blob for all three files, with git status --porcelain empty and git diff HEAD clean — never by an editor exit code. The mutation ran under trap … EXIT INT TERM restoring absolute paths, and the restore leg uses git checkout HEAD -- PATH rather than a bare git checkout -- PATH, since the mutation leg wrote the index too. No rebuild is involved on either leg: all three suites import their subject by relative source path (./serve.js, ./harness, ./enterprise-organizations.js), not through a package exports map into dist.

Controls, byte-unchanged, in the same runs. The undeclared and declared-unresolvable arms are pinned at all three sites and pass identically either side of the change — they are green on the mutated tree above and on the implemented tree. Two of those controls are new here: neither the verify suite nor the dogfood suite had a declared-unresolvable fixture, so this PR adds one to each (declared, not installed) purely as a control.

Green, at dd611c3856:

pnpm --filter @objectstack/cli exec vitest run src/commands/serve-organizations-message-spelling.test.ts Tests 12 passed (12)
pnpm --filter @objectstack/verify exec vitest run src/harness.host-resolution.test.ts Tests 7 passed (7)
pnpm --filter @objectstack/dogfood exec vitest run test/enterprise-organizations.test.ts Tests 9 passed (9)
pnpm --filter @objectstack/cli typecheck && pnpm --filter @objectstack/verify typecheck
&& pnpm --filter @objectstack/dogfood typecheck os-verify-lock: VERDICT command-exit 0
pnpm lint (eslint . --no-inline-config, whole repo, not narrowed) os-verify-lock: VERDICT command-exit 0

Gates

The union was derived at the final HEAD dd611c3856 on a clean tree with node scripts/pm/dispatch-gates.mjs --repo objectstack-ai/objectstack --commands — 50 families (39 by path + 7 by change kind + 6 declared whole-tree, 2 reached both ways) — never from a hand-written path list. All 50 were run locally; each gate's exit code was captured before any pipe. 49 green first time.

One was a real red, and it was mine.pnpm check:type-check-debt reported:

check-type-check-coverage --re-measure: 1 ledger entr(ies) drifted upward
• @objectstack/verify: TEST_DEBT records 3 raw tsc error(s), `tsc --noEmit` now reports 5 (+2).

harness.host-resolution.test.ts imports ./harness without the .js extension, so under NodeNext the specifier does not resolve and every symbol it names is any — which makes a .then(onFulfilled, onRejected) callback parameter implicitly any. My two new cases used that shape and added 2 × TS7006. TEST_DEBT is shrink-only, so the fix is the errors, not the ceiling: dd611c3856 rewrites both as try/catch (a const initialised from an any call is not a diagnostic; only a parameter is). ⛔ The one-line .js fix that would graduate the entry is deliberately not taken here — different card, filed as #15145.

Re-run at the final HEAD after that fix, each exit captured per gate:

RATCHET check:nul-bytes exit=0 RATCHET check:engine-double-contract exit=0
RATCHET check:test-source-alias exit=0 RATCHET check:query-options-erasure exit=0
RATCHET check:cross-package-test-inputs exit=0 RATCHET check:type-check-coverage exit=0
RATCHET check:where-matcher exit=0 RATCHET check:type-check-debt exit=0
check-type-check-coverage --re-measure: OK — 17 ledger entr(ies) re-measured in 166.5s,
217 raw tsc error(s) total, none above its recorded number.

Re-deriving the union at dd611c3856 returns the same 50 families — the ratchet fix implicated nothing new.

Typecheck coverage — measured, not assumed

Measured with tsc --noEmit --listFiles, because "typecheck is clean" says nothing about a file the program never read:

edited filein a tsc program?
packages/cli/src/commands/serve.tspackages/cli/tsconfig.json
packages/cli/src/commands/serve-organizations-message-spelling.test.tspackages/cli/tsconfig.json — its include: ['src'] reaches this file, since the pin lives under src/commands/, not under test/. It is 0 files in tsconfig.test.json, whose project is test/.
packages/qa/dogfood/test/enterprise-organizations.ts and its testpackages/qa/dogfood/tsconfig.json (include: ['test/**/*'])
packages/verify/src/harness.tspackages/verify/tsconfig.json
packages/verify/src/harness.host-resolution.test.tsnot in that program — exclude lists **/*.test.ts, and the package has no check:test-typecheck wiring. Its types are read only by check:type-check-debt --re-measure, which is what caught the +2 above. Filed as #15145.

So: pnpm --filter @objectstack/verify typecheck being green is not a statement about the test file this PR edits. The statement that covers it is the ratchet line quoted above.

Changeset

Measured from each of the three packages' own files field, because the answers differ:

  • @objectstack/clifiles: ["dist", …], publishConfig.access: public ⇒ published, src/commands/serve.ts ships ⇒ in the changeset.
  • @objectstack/verifyfiles: ["dist", …], published ⇒ src/harness.ts ships ⇒ in the changeset.
  • @objectstack/dogfoodprivate: true, no files field at all ⇒ publishes nothing ⇒ not in the changeset, and cannot be.

Patch, not minor: operator-facing wording on a failure path, no API change. Nothing here removes or renames anything an author can write, so no ADR-0087 disposition marker is owed.

Out of scope, filed

Fences

packages/types/src/node.ts, packages/rest/**, packages/spec/** and content/docs/releases/** are absent from the diff (asserted above). serve.ts:133's "keep the two in sync" comment and every apps/objectos / apps/cloud comment are untouched — 0 diff hits for that string. packages/verify/src/harness.ts moved one remedy branch and nothing else in the ADR-0105 bootStack harness.

Draft on purpose: ⛔ not flipped ready, ⛔ auto-merge not armed — the PM does that.

🤖 Generated with Claude Code

https://claude.ai/code/session_016yfqQh2dBgPAymYd7xipza


Generated by Claude Code

…three sites
Red-first for #14270. #14041 added a third `HostImportFailureKind`,
`declared-no-loadable-entry` — the app declares the package, the install
delivered it, and the package's own `exports` names no runtime entry Node
can load. Three consumers pick their remedy with a two-way branch written
when only two kinds existed, so the third falls into the else leg and
renders the DECLARE remedy at an operator who has already declared AND
installed it.
Each site gets a fixture carrying that kind and an assertion naming which
arm fired. The verify and dogfood fixtures are real host roots on disk
whose installed package publishes a `types`-only `exports` map; the CLI
formatter takes the kind directly. Controls for `undeclared` and
`declared-unresolvable` are untouched.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016yfqQh2dBgPAymYd7xipza
…not on one kind
`declared-no-loadable-entry` (#14041) means the app declares the package,
the install delivered it, and the package's own `exports` names no runtime
entry Node can load. All three consumers picked their remedy with a
two-way branch written when only two kinds existed, so that kind fell into
the else leg and printed "declare it in the app's package.json and
install" — at an operator who has already done both, and directly
contradicting the importer's own message printed beside it.
Each branch now asks "is the declaration the problem?": undeclared keeps
the declare remedy, `declared-unresolvable` keeps the install remedy, and
the third kind prescribes nothing and defers to the importer's message,
which every one of the three sites already interpolates. No fourth remedy
sentence is minted — the importer words the package-shape case once, and a
local re-wording would be a second copy to drift.
`packages/types` is untouched: the strings deferred to are #14278's.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016yfqQh2dBgPAymYd7xipza
…tion the app already has
Patch for @objectstack/cli and @objectstack/verify — the two published
packages whose `files` field ships the changed code. @objectstack/dogfood
is `private: true` with no `files`, so it publishes nothing and takes no
changeset entry.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016yfqQh2dBgPAymYd7xipza
…EST_DEBT
`harness.host-resolution.test.ts` imports `./harness` without the `.js`
extension, so under NodeNext the specifier does not resolve and every
symbol it names is `any` — which makes a `.then` callback PARAMETER
implicitly any. The two new cases used `.then(onFulfilled, onRejected)`
and added 2 x TS7006, taking the package's TEST_DEBT entry from its
recorded 3 to 5. That ledger is shrink-only, so the fix is the errors,
not the ceiling.
try/catch instead: a `const` initialised from an `any` call is not a
diagnostic, only a parameter is. `check:type-check-debt --re-measure`
now reports 17 entries re-measured, 217 raw errors, none above its
recorded number.
⛔ Deliberately NOT the one-line `.js` fix that graduates the entry — that
is a different card's, filed separately.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016yfqQh2dBgPAymYd7xipza
@github-actions

Copy link
Copy Markdown
Contributor

📓 Docs Drift Check

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

17 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 4283b72a15f9c5ced0755a1b9c8aa53e5f8e16fa.

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

What this run could not see
  • 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 — 23 page(s) merely mention a changed package (the pre-#9192 predicate, kept for the deliberately-wide backstop): node scripts/docs-audit/affected-docs.mjs --json 4283b72a15f9c5ced0755a1b9c8aa53e5f8e16fapackageMentionDocs.

Which tree this was computed on

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

⚠️ 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 4283b72a15f9c5ced0755a1b9c8aa53e5f8e16fa → 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 4, 2026
@os-trump
os-trump marked this pull request as ready for review September 4, 2026 04:20
@os-trump
os-trump added this pull request to the merge queueSep 4, 2026
Merged via the queue into main with commit 5cf1c88Sep 4, 2026
36 checks passed
@os-trump
os-trump deleted the claude/issue-14270-remedy-formatter-third-kind branch September 4, 2026 04:48
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

2 participants

@os-trump@claude
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Add copy buttons to all \u003cpre\u003e\u003ccode\u003e 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

fix(cli,verify,dogfood): branch the multi-org remedy on the ABSENCE, not on one failure kind - #15158

Merged
os-trump merged 4 commits into
mainfrom
claude/issue-14270-remedy-formatter-third-kind
Sep 4, 2026
Merged

fix(cli,verify,dogfood): branch the multi-org remedy on the ABSENCE, not on one failure kind#15158
os-trump merged 4 commits into
mainfrom
claude/issue-14270-remedy-formatter-third-kind

Conversation

@os-trump

Copy link
Copy Markdown
Collaborator

Fixes#14270

What was wrong

HostImportFailureKind got a third member in #14041declared-no-loadable-entry: the app declares the package, the install delivered it, and the package's own exports names no runtime entry Node can load (a types-only or browser-only publish, or an unexported subpath). Three consumers pick their remedy line with a two-way branch written when the type had exactly two members, so the third kind fell into the else leg and printed the declare remedy:

sitesymbolwhat the third kind rendered
packages/cli/src/commands/serve.tsformatOrganizationsInstallRemedy"add the package to THIS APP — declare it in the app's package.json and install"
packages/verify/src/harness.tsthe remedy in bootStack's multi-tenant fatal"Install/link it in THIS APP — and DECLARE it in that app's package.json"
packages/qa/dogfood/test/enterprise-organizations.tsprobeOrganizations's refusal / skip reason"declare it in the app's own package.json and install it"

Each of those three also interpolates the importer's own message, which words the case correctly ("the package publishes no entry that Node can load … the remedy lives in the package"). So the bullet an operator reads first contradicted the diagnosis printed underneath it, and prescribed two actions that were already done and could not have helped. That is the confidently-wrong-verdict class #14041 removed one layer down, reintroduced by its callers.

The shape

Each branch now asks "is the declaration the problem?" rather than testing one kind:

  • undeclared ⇒ declare remedy — unchanged
  • declared-unresolvable ⇒ install remedy — unchanged
  • declared-no-loadable-entryprescribes nothing. It names the two things that are not the problem and defers to the importer's message, which every one of the three sites already prints.

No fourth remedy sentence is minted. The importer words the package-shape case once, in the place that measured it; a local re-wording in three consumers would be three copies to drift. The deferral follows an idiom already in serve.tsformatOrganizationsMountFatal's "its message is the authority on the remedy; this CLI has no further detail to add" — and the same reasoning formatI18nLoadDiagnostic's docblock gives for interpolating only the kind TOKEN.

An error carrying no kind never came from the host importer at all and keeps the declare-and-install arm it has always had. Narrowing that one is a different question about a different fact, and is not this branch's.

⛔ The seam with #14278 / PR #15047

packages/types/src/node.ts is not touched — verified on the diff:

git diff --name-only origin/main...HEAD | grep -E '^packages/types/' # no match

The strings this card defers to (/publishes no entry/, /INSTALL problem/) are pinned by #14278's tests in packages/types/src/node.test.ts. This PR changes the three callers' remedy bullets so they stop contradicting that message.

Proof

Red-first, per formatter, naming which arm fired. Tests landed in 063d976b7a, implementation in 78a0a7587f. Against the tests-only tree, all three sites reproduced the defect with a fixture carrying the third kind:

serve.ts expected " • this app DECLARES @objectstack/organizations (dependencies: "^1.2.3") and it IS"
received " • add @objectstack/organizations (the enterprise multi-org runtime) to THIS APP"
harness.ts received "… Install/link it in THIS APP (/tmp/os-verify-org-host-no-entry-AFcXGm) — and DECLARE it
in that app's package.json …" followed, in the same string, by the importer's
"the package publishes no entry that Node can load"
dogfood received "To enable it, declare @objectstack/organizations in /tmp/os-dogfood-org-no-entry-gd5Vpw's
own package.json and install it …" same contradiction, same string

The verify and dogfood fixtures are real host roots on disk — a real node_modules, a real installed package whose manifest publishes exports: { ".": { types: "./index.d.ts" } } and nothing else, nothing mocked. Neither condition set resolves it (ESM_IMPORT_CONDITIONS / CJS_REQUIRE_CONDITIONS), so the importer classifies it declared-no-loadable-entry for real rather than by injection.

Reverse verification — the three formatters restored to their pre-implementation bytes, same suites re-run, then restored:

mutation ON DISK (blob hash + marker counts)
packages/cli/src/commands/serve.ts now=bd070ffe == pre? YES != head? YES kind-marker-count=1
packages/verify/src/harness.ts now=479de177 == pre? YES != head? YES kind-marker-count=0
packages/qa/dogfood/test/enterprise-organizations.ts now=cc6284e9 == pre? YES != head? YES kind-marker-count=0
× site 2b — the `declared-no-loadable-entry` remedy DEFERS … Tests 1 failed | 11 passed (12)
× DEFERS to the importer for a declared, installed package … Tests 1 failed | 6 passed (7)
× DEFERS to the importer when the package is declared, installed …
× THROWS with that same deferral when the run declares the package Tests 2 failed | 7 passed (9)

Restore proven by blob-hash equality against the HEAD blob for all three files, with git status --porcelain empty and git diff HEAD clean — never by an editor exit code. The mutation ran under trap … EXIT INT TERM restoring absolute paths, and the restore leg uses git checkout HEAD -- PATH rather than a bare git checkout -- PATH, since the mutation leg wrote the index too. No rebuild is involved on either leg: all three suites import their subject by relative source path (./serve.js, ./harness, ./enterprise-organizations.js), not through a package exports map into dist.

Controls, byte-unchanged, in the same runs. The undeclared and declared-unresolvable arms are pinned at all three sites and pass identically either side of the change — they are green on the mutated tree above and on the implemented tree. Two of those controls are new here: neither the verify suite nor the dogfood suite had a declared-unresolvable fixture, so this PR adds one to each (declared, not installed) purely as a control.

Green, at dd611c3856:

pnpm --filter @objectstack/cli exec vitest run src/commands/serve-organizations-message-spelling.test.ts Tests 12 passed (12)
pnpm --filter @objectstack/verify exec vitest run src/harness.host-resolution.test.ts Tests 7 passed (7)
pnpm --filter @objectstack/dogfood exec vitest run test/enterprise-organizations.test.ts Tests 9 passed (9)
pnpm --filter @objectstack/cli typecheck && pnpm --filter @objectstack/verify typecheck
&& pnpm --filter @objectstack/dogfood typecheck os-verify-lock: VERDICT command-exit 0
pnpm lint (eslint . --no-inline-config, whole repo, not narrowed) os-verify-lock: VERDICT command-exit 0

Gates

The union was derived at the final HEAD dd611c3856 on a clean tree with node scripts/pm/dispatch-gates.mjs --repo objectstack-ai/objectstack --commands — 50 families (39 by path + 7 by change kind + 6 declared whole-tree, 2 reached both ways) — never from a hand-written path list. All 50 were run locally; each gate's exit code was captured before any pipe. 49 green first time.

One was a real red, and it was mine.pnpm check:type-check-debt reported:

check-type-check-coverage --re-measure: 1 ledger entr(ies) drifted upward
• @objectstack/verify: TEST_DEBT records 3 raw tsc error(s), `tsc --noEmit` now reports 5 (+2).

harness.host-resolution.test.ts imports ./harness without the .js extension, so under NodeNext the specifier does not resolve and every symbol it names is any — which makes a .then(onFulfilled, onRejected) callback parameter implicitly any. My two new cases used that shape and added 2 × TS7006. TEST_DEBT is shrink-only, so the fix is the errors, not the ceiling: dd611c3856 rewrites both as try/catch (a const initialised from an any call is not a diagnostic; only a parameter is). ⛔ The one-line .js fix that would graduate the entry is deliberately not taken here — different card, filed as #15145.

Re-run at the final HEAD after that fix, each exit captured per gate:

RATCHET check:nul-bytes exit=0 RATCHET check:engine-double-contract exit=0
RATCHET check:test-source-alias exit=0 RATCHET check:query-options-erasure exit=0
RATCHET check:cross-package-test-inputs exit=0 RATCHET check:type-check-coverage exit=0
RATCHET check:where-matcher exit=0 RATCHET check:type-check-debt exit=0
check-type-check-coverage --re-measure: OK — 17 ledger entr(ies) re-measured in 166.5s,
217 raw tsc error(s) total, none above its recorded number.

Re-deriving the union at dd611c3856 returns the same 50 families — the ratchet fix implicated nothing new.

Typecheck coverage — measured, not assumed

Measured with tsc --noEmit --listFiles, because "typecheck is clean" says nothing about a file the program never read:

edited filein a tsc program?
packages/cli/src/commands/serve.tspackages/cli/tsconfig.json
packages/cli/src/commands/serve-organizations-message-spelling.test.tspackages/cli/tsconfig.json — its include: ['src'] reaches this file, since the pin lives under src/commands/, not under test/. It is 0 files in tsconfig.test.json, whose project is test/.
packages/qa/dogfood/test/enterprise-organizations.ts and its testpackages/qa/dogfood/tsconfig.json (include: ['test/**/*'])
packages/verify/src/harness.tspackages/verify/tsconfig.json
packages/verify/src/harness.host-resolution.test.tsnot in that program — exclude lists **/*.test.ts, and the package has no check:test-typecheck wiring. Its types are read only by check:type-check-debt --re-measure, which is what caught the +2 above. Filed as #15145.

So: pnpm --filter @objectstack/verify typecheck being green is not a statement about the test file this PR edits. The statement that covers it is the ratchet line quoted above.

Changeset

Measured from each of the three packages' own files field, because the answers differ:

  • @objectstack/clifiles: ["dist", …], publishConfig.access: public ⇒ published, src/commands/serve.ts ships ⇒ in the changeset.
  • @objectstack/verifyfiles: ["dist", …], published ⇒ src/harness.ts ships ⇒ in the changeset.
  • @objectstack/dogfoodprivate: true, no files field at all ⇒ publishes nothing ⇒ not in the changeset, and cannot be.

Patch, not minor: operator-facing wording on a failure path, no API change. Nothing here removes or renames anything an author can write, so no ADR-0087 disposition marker is owed.

Out of scope, filed

Fences

packages/types/src/node.ts, packages/rest/**, packages/spec/** and content/docs/releases/** are absent from the diff (asserted above). serve.ts:133's "keep the two in sync" comment and every apps/objectos / apps/cloud comment are untouched — 0 diff hits for that string. packages/verify/src/harness.ts moved one remedy branch and nothing else in the ADR-0105 bootStack harness.

Draft on purpose: ⛔ not flipped ready, ⛔ auto-merge not armed — the PM does that.

🤖 Generated with Claude Code

https://claude.ai/code/session_016yfqQh2dBgPAymYd7xipza


Generated by Claude Code

…three sites
Red-first for #14270. #14041 added a third `HostImportFailureKind`,
`declared-no-loadable-entry` — the app declares the package, the install
delivered it, and the package's own `exports` names no runtime entry Node
can load. Three consumers pick their remedy with a two-way branch written
when only two kinds existed, so the third falls into the else leg and
renders the DECLARE remedy at an operator who has already declared AND
installed it.
Each site gets a fixture carrying that kind and an assertion naming which
arm fired. The verify and dogfood fixtures are real host roots on disk
whose installed package publishes a `types`-only `exports` map; the CLI
formatter takes the kind directly. Controls for `undeclared` and
`declared-unresolvable` are untouched.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016yfqQh2dBgPAymYd7xipza
…not on one kind
`declared-no-loadable-entry` (#14041) means the app declares the package,
the install delivered it, and the package's own `exports` names no runtime
entry Node can load. All three consumers picked their remedy with a
two-way branch written when only two kinds existed, so that kind fell into
the else leg and printed "declare it in the app's package.json and
install" — at an operator who has already done both, and directly
contradicting the importer's own message printed beside it.
Each branch now asks "is the declaration the problem?": undeclared keeps
the declare remedy, `declared-unresolvable` keeps the install remedy, and
the third kind prescribes nothing and defers to the importer's message,
which every one of the three sites already interpolates. No fourth remedy
sentence is minted — the importer words the package-shape case once, and a
local re-wording would be a second copy to drift.
`packages/types` is untouched: the strings deferred to are #14278's.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016yfqQh2dBgPAymYd7xipza
…tion the app already has
Patch for @objectstack/cli and @objectstack/verify — the two published
packages whose `files` field ships the changed code. @objectstack/dogfood
is `private: true` with no `files`, so it publishes nothing and takes no
changeset entry.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016yfqQh2dBgPAymYd7xipza
…EST_DEBT
`harness.host-resolution.test.ts` imports `./harness` without the `.js`
extension, so under NodeNext the specifier does not resolve and every
symbol it names is `any` — which makes a `.then` callback PARAMETER
implicitly any. The two new cases used `.then(onFulfilled, onRejected)`
and added 2 x TS7006, taking the package's TEST_DEBT entry from its
recorded 3 to 5. That ledger is shrink-only, so the fix is the errors,
not the ceiling.
try/catch instead: a `const` initialised from an `any` call is not a
diagnostic, only a parameter is. `check:type-check-debt --re-measure`
now reports 17 entries re-measured, 217 raw errors, none above its
recorded number.
⛔ Deliberately NOT the one-line `.js` fix that graduates the entry — that
is a different card's, filed separately.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016yfqQh2dBgPAymYd7xipza
@github-actions

Copy link
Copy Markdown
Contributor

📓 Docs Drift Check

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

17 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 4283b72a15f9c5ced0755a1b9c8aa53e5f8e16fa.

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

What this run could not see
  • 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 — 23 page(s) merely mention a changed package (the pre-#9192 predicate, kept for the deliberately-wide backstop): node scripts/docs-audit/affected-docs.mjs --json 4283b72a15f9c5ced0755a1b9c8aa53e5f8e16fapackageMentionDocs.

Which tree this was computed on

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

⚠️ 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 4283b72a15f9c5ced0755a1b9c8aa53e5f8e16fa → 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 4, 2026
@os-trump
os-trump marked this pull request as ready for review September 4, 2026 04:20
@os-trump
os-trump added this pull request to the merge queueSep 4, 2026
Merged via the queue into main with commit 5cf1c88Sep 4, 2026
36 checks passed
@os-trump
os-trump deleted the claude/issue-14270-remedy-formatter-third-kind branch September 4, 2026 04:48
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

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

fix(cli,verify,dogfood): branch the multi-org remedy on the ABSENCE, not on one failure kind - #15158

Merged
os-trump merged 4 commits into
mainfrom
claude/issue-14270-remedy-formatter-third-kind
Sep 4, 2026
Merged

fix(cli,verify,dogfood): branch the multi-org remedy on the ABSENCE, not on one failure kind#15158
os-trump merged 4 commits into
mainfrom
claude/issue-14270-remedy-formatter-third-kind

Conversation

@os-trump

Copy link
Copy Markdown
Collaborator

Fixes#14270

What was wrong

HostImportFailureKind got a third member in #14041declared-no-loadable-entry: the app declares the package, the install delivered it, and the package's own exports names no runtime entry Node can load (a types-only or browser-only publish, or an unexported subpath). Three consumers pick their remedy line with a two-way branch written when the type had exactly two members, so the third kind fell into the else leg and printed the declare remedy:

sitesymbolwhat the third kind rendered
packages/cli/src/commands/serve.tsformatOrganizationsInstallRemedy"add the package to THIS APP — declare it in the app's package.json and install"
packages/verify/src/harness.tsthe remedy in bootStack's multi-tenant fatal"Install/link it in THIS APP — and DECLARE it in that app's package.json"
packages/qa/dogfood/test/enterprise-organizations.tsprobeOrganizations's refusal / skip reason"declare it in the app's own package.json and install it"

Each of those three also interpolates the importer's own message, which words the case correctly ("the package publishes no entry that Node can load … the remedy lives in the package"). So the bullet an operator reads first contradicted the diagnosis printed underneath it, and prescribed two actions that were already done and could not have helped. That is the confidently-wrong-verdict class #14041 removed one layer down, reintroduced by its callers.

The shape

Each branch now asks "is the declaration the problem?" rather than testing one kind:

  • undeclared ⇒ declare remedy — unchanged
  • declared-unresolvable ⇒ install remedy — unchanged
  • declared-no-loadable-entryprescribes nothing. It names the two things that are not the problem and defers to the importer's message, which every one of the three sites already prints.

No fourth remedy sentence is minted. The importer words the package-shape case once, in the place that measured it; a local re-wording in three consumers would be three copies to drift. The deferral follows an idiom already in serve.tsformatOrganizationsMountFatal's "its message is the authority on the remedy; this CLI has no further detail to add" — and the same reasoning formatI18nLoadDiagnostic's docblock gives for interpolating only the kind TOKEN.

An error carrying no kind never came from the host importer at all and keeps the declare-and-install arm it has always had. Narrowing that one is a different question about a different fact, and is not this branch's.

⛔ The seam with #14278 / PR #15047

packages/types/src/node.ts is not touched — verified on the diff:

git diff --name-only origin/main...HEAD | grep -E '^packages/types/' # no match

The strings this card defers to (/publishes no entry/, /INSTALL problem/) are pinned by #14278's tests in packages/types/src/node.test.ts. This PR changes the three callers' remedy bullets so they stop contradicting that message.

Proof

Red-first, per formatter, naming which arm fired. Tests landed in 063d976b7a, implementation in 78a0a7587f. Against the tests-only tree, all three sites reproduced the defect with a fixture carrying the third kind:

serve.ts expected " • this app DECLARES @objectstack/organizations (dependencies: "^1.2.3") and it IS"
received " • add @objectstack/organizations (the enterprise multi-org runtime) to THIS APP"
harness.ts received "… Install/link it in THIS APP (/tmp/os-verify-org-host-no-entry-AFcXGm) — and DECLARE it
in that app's package.json …" followed, in the same string, by the importer's
"the package publishes no entry that Node can load"
dogfood received "To enable it, declare @objectstack/organizations in /tmp/os-dogfood-org-no-entry-gd5Vpw's
own package.json and install it …" same contradiction, same string

The verify and dogfood fixtures are real host roots on disk — a real node_modules, a real installed package whose manifest publishes exports: { ".": { types: "./index.d.ts" } } and nothing else, nothing mocked. Neither condition set resolves it (ESM_IMPORT_CONDITIONS / CJS_REQUIRE_CONDITIONS), so the importer classifies it declared-no-loadable-entry for real rather than by injection.

Reverse verification — the three formatters restored to their pre-implementation bytes, same suites re-run, then restored:

mutation ON DISK (blob hash + marker counts)
packages/cli/src/commands/serve.ts now=bd070ffe == pre? YES != head? YES kind-marker-count=1
packages/verify/src/harness.ts now=479de177 == pre? YES != head? YES kind-marker-count=0
packages/qa/dogfood/test/enterprise-organizations.ts now=cc6284e9 == pre? YES != head? YES kind-marker-count=0
× site 2b — the `declared-no-loadable-entry` remedy DEFERS … Tests 1 failed | 11 passed (12)
× DEFERS to the importer for a declared, installed package … Tests 1 failed | 6 passed (7)
× DEFERS to the importer when the package is declared, installed …
× THROWS with that same deferral when the run declares the package Tests 2 failed | 7 passed (9)

Restore proven by blob-hash equality against the HEAD blob for all three files, with git status --porcelain empty and git diff HEAD clean — never by an editor exit code. The mutation ran under trap … EXIT INT TERM restoring absolute paths, and the restore leg uses git checkout HEAD -- PATH rather than a bare git checkout -- PATH, since the mutation leg wrote the index too. No rebuild is involved on either leg: all three suites import their subject by relative source path (./serve.js, ./harness, ./enterprise-organizations.js), not through a package exports map into dist.

Controls, byte-unchanged, in the same runs. The undeclared and declared-unresolvable arms are pinned at all three sites and pass identically either side of the change — they are green on the mutated tree above and on the implemented tree. Two of those controls are new here: neither the verify suite nor the dogfood suite had a declared-unresolvable fixture, so this PR adds one to each (declared, not installed) purely as a control.

Green, at dd611c3856:

pnpm --filter @objectstack/cli exec vitest run src/commands/serve-organizations-message-spelling.test.ts Tests 12 passed (12)
pnpm --filter @objectstack/verify exec vitest run src/harness.host-resolution.test.ts Tests 7 passed (7)
pnpm --filter @objectstack/dogfood exec vitest run test/enterprise-organizations.test.ts Tests 9 passed (9)
pnpm --filter @objectstack/cli typecheck && pnpm --filter @objectstack/verify typecheck
&& pnpm --filter @objectstack/dogfood typecheck os-verify-lock: VERDICT command-exit 0
pnpm lint (eslint . --no-inline-config, whole repo, not narrowed) os-verify-lock: VERDICT command-exit 0

Gates

The union was derived at the final HEAD dd611c3856 on a clean tree with node scripts/pm/dispatch-gates.mjs --repo objectstack-ai/objectstack --commands — 50 families (39 by path + 7 by change kind + 6 declared whole-tree, 2 reached both ways) — never from a hand-written path list. All 50 were run locally; each gate's exit code was captured before any pipe. 49 green first time.

One was a real red, and it was mine.pnpm check:type-check-debt reported:

check-type-check-coverage --re-measure: 1 ledger entr(ies) drifted upward
• @objectstack/verify: TEST_DEBT records 3 raw tsc error(s), `tsc --noEmit` now reports 5 (+2).

harness.host-resolution.test.ts imports ./harness without the .js extension, so under NodeNext the specifier does not resolve and every symbol it names is any — which makes a .then(onFulfilled, onRejected) callback parameter implicitly any. My two new cases used that shape and added 2 × TS7006. TEST_DEBT is shrink-only, so the fix is the errors, not the ceiling: dd611c3856 rewrites both as try/catch (a const initialised from an any call is not a diagnostic; only a parameter is). ⛔ The one-line .js fix that would graduate the entry is deliberately not taken here — different card, filed as #15145.

Re-run at the final HEAD after that fix, each exit captured per gate:

RATCHET check:nul-bytes exit=0 RATCHET check:engine-double-contract exit=0
RATCHET check:test-source-alias exit=0 RATCHET check:query-options-erasure exit=0
RATCHET check:cross-package-test-inputs exit=0 RATCHET check:type-check-coverage exit=0
RATCHET check:where-matcher exit=0 RATCHET check:type-check-debt exit=0
check-type-check-coverage --re-measure: OK — 17 ledger entr(ies) re-measured in 166.5s,
217 raw tsc error(s) total, none above its recorded number.

Re-deriving the union at dd611c3856 returns the same 50 families — the ratchet fix implicated nothing new.

Typecheck coverage — measured, not assumed

Measured with tsc --noEmit --listFiles, because "typecheck is clean" says nothing about a file the program never read:

edited filein a tsc program?
packages/cli/src/commands/serve.tspackages/cli/tsconfig.json
packages/cli/src/commands/serve-organizations-message-spelling.test.tspackages/cli/tsconfig.json — its include: ['src'] reaches this file, since the pin lives under src/commands/, not under test/. It is 0 files in tsconfig.test.json, whose project is test/.
packages/qa/dogfood/test/enterprise-organizations.ts and its testpackages/qa/dogfood/tsconfig.json (include: ['test/**/*'])
packages/verify/src/harness.tspackages/verify/tsconfig.json
packages/verify/src/harness.host-resolution.test.tsnot in that program — exclude lists **/*.test.ts, and the package has no check:test-typecheck wiring. Its types are read only by check:type-check-debt --re-measure, which is what caught the +2 above. Filed as #15145.

So: pnpm --filter @objectstack/verify typecheck being green is not a statement about the test file this PR edits. The statement that covers it is the ratchet line quoted above.

Changeset

Measured from each of the three packages' own files field, because the answers differ:

  • @objectstack/clifiles: ["dist", …], publishConfig.access: public ⇒ published, src/commands/serve.ts ships ⇒ in the changeset.
  • @objectstack/verifyfiles: ["dist", …], published ⇒ src/harness.ts ships ⇒ in the changeset.
  • @objectstack/dogfoodprivate: true, no files field at all ⇒ publishes nothing ⇒ not in the changeset, and cannot be.

Patch, not minor: operator-facing wording on a failure path, no API change. Nothing here removes or renames anything an author can write, so no ADR-0087 disposition marker is owed.

Out of scope, filed

Fences

packages/types/src/node.ts, packages/rest/**, packages/spec/** and content/docs/releases/** are absent from the diff (asserted above). serve.ts:133's "keep the two in sync" comment and every apps/objectos / apps/cloud comment are untouched — 0 diff hits for that string. packages/verify/src/harness.ts moved one remedy branch and nothing else in the ADR-0105 bootStack harness.

Draft on purpose: ⛔ not flipped ready, ⛔ auto-merge not armed — the PM does that.

🤖 Generated with Claude Code

https://claude.ai/code/session_016yfqQh2dBgPAymYd7xipza


Generated by Claude Code

…three sites
Red-first for #14270. #14041 added a third `HostImportFailureKind`,
`declared-no-loadable-entry` — the app declares the package, the install
delivered it, and the package's own `exports` names no runtime entry Node
can load. Three consumers pick their remedy with a two-way branch written
when only two kinds existed, so the third falls into the else leg and
renders the DECLARE remedy at an operator who has already declared AND
installed it.
Each site gets a fixture carrying that kind and an assertion naming which
arm fired. The verify and dogfood fixtures are real host roots on disk
whose installed package publishes a `types`-only `exports` map; the CLI
formatter takes the kind directly. Controls for `undeclared` and
`declared-unresolvable` are untouched.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016yfqQh2dBgPAymYd7xipza
…not on one kind
`declared-no-loadable-entry` (#14041) means the app declares the package,
the install delivered it, and the package's own `exports` names no runtime
entry Node can load. All three consumers picked their remedy with a
two-way branch written when only two kinds existed, so that kind fell into
the else leg and printed "declare it in the app's package.json and
install" — at an operator who has already done both, and directly
contradicting the importer's own message printed beside it.
Each branch now asks "is the declaration the problem?": undeclared keeps
the declare remedy, `declared-unresolvable` keeps the install remedy, and
the third kind prescribes nothing and defers to the importer's message,
which every one of the three sites already interpolates. No fourth remedy
sentence is minted — the importer words the package-shape case once, and a
local re-wording would be a second copy to drift.
`packages/types` is untouched: the strings deferred to are #14278's.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016yfqQh2dBgPAymYd7xipza
…tion the app already has
Patch for @objectstack/cli and @objectstack/verify — the two published
packages whose `files` field ships the changed code. @objectstack/dogfood
is `private: true` with no `files`, so it publishes nothing and takes no
changeset entry.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016yfqQh2dBgPAymYd7xipza
…EST_DEBT
`harness.host-resolution.test.ts` imports `./harness` without the `.js`
extension, so under NodeNext the specifier does not resolve and every
symbol it names is `any` — which makes a `.then` callback PARAMETER
implicitly any. The two new cases used `.then(onFulfilled, onRejected)`
and added 2 x TS7006, taking the package's TEST_DEBT entry from its
recorded 3 to 5. That ledger is shrink-only, so the fix is the errors,
not the ceiling.
try/catch instead: a `const` initialised from an `any` call is not a
diagnostic, only a parameter is. `check:type-check-debt --re-measure`
now reports 17 entries re-measured, 217 raw errors, none above its
recorded number.
⛔ Deliberately NOT the one-line `.js` fix that graduates the entry — that
is a different card's, filed separately.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016yfqQh2dBgPAymYd7xipza
@github-actions

Copy link
Copy Markdown
Contributor

📓 Docs Drift Check

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

17 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 4283b72a15f9c5ced0755a1b9c8aa53e5f8e16fa.

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

What this run could not see
  • 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 — 23 page(s) merely mention a changed package (the pre-#9192 predicate, kept for the deliberately-wide backstop): node scripts/docs-audit/affected-docs.mjs --json 4283b72a15f9c5ced0755a1b9c8aa53e5f8e16fapackageMentionDocs.

Which tree this was computed on

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

⚠️ 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 4283b72a15f9c5ced0755a1b9c8aa53e5f8e16fa → 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 4, 2026
@os-trump
os-trump marked this pull request as ready for review September 4, 2026 04:20
@os-trump
os-trump added this pull request to the merge queueSep 4, 2026
Merged via the queue into main with commit 5cf1c88Sep 4, 2026
36 checks passed
@os-trump
os-trump deleted the claude/issue-14270-remedy-formatter-third-kind branch September 4, 2026 04:48
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

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 \u003e 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

fix(cli,verify,dogfood): branch the multi-org remedy on the ABSENCE, not on one failure kind - #15158

Merged
os-trump merged 4 commits into
mainfrom
claude/issue-14270-remedy-formatter-third-kind
Sep 4, 2026
Merged

fix(cli,verify,dogfood): branch the multi-org remedy on the ABSENCE, not on one failure kind#15158
os-trump merged 4 commits into
mainfrom
claude/issue-14270-remedy-formatter-third-kind

Conversation

@os-trump

Copy link
Copy Markdown
Collaborator

Fixes#14270

What was wrong

HostImportFailureKind got a third member in #14041declared-no-loadable-entry: the app declares the package, the install delivered it, and the package's own exports names no runtime entry Node can load (a types-only or browser-only publish, or an unexported subpath). Three consumers pick their remedy line with a two-way branch written when the type had exactly two members, so the third kind fell into the else leg and printed the declare remedy:

sitesymbolwhat the third kind rendered
packages/cli/src/commands/serve.tsformatOrganizationsInstallRemedy"add the package to THIS APP — declare it in the app's package.json and install"
packages/verify/src/harness.tsthe remedy in bootStack's multi-tenant fatal"Install/link it in THIS APP — and DECLARE it in that app's package.json"
packages/qa/dogfood/test/enterprise-organizations.tsprobeOrganizations's refusal / skip reason"declare it in the app's own package.json and install it"

Each of those three also interpolates the importer's own message, which words the case correctly ("the package publishes no entry that Node can load … the remedy lives in the package"). So the bullet an operator reads first contradicted the diagnosis printed underneath it, and prescribed two actions that were already done and could not have helped. That is the confidently-wrong-verdict class #14041 removed one layer down, reintroduced by its callers.

The shape

Each branch now asks "is the declaration the problem?" rather than testing one kind:

  • undeclared ⇒ declare remedy — unchanged
  • declared-unresolvable ⇒ install remedy — unchanged
  • declared-no-loadable-entryprescribes nothing. It names the two things that are not the problem and defers to the importer's message, which every one of the three sites already prints.

No fourth remedy sentence is minted. The importer words the package-shape case once, in the place that measured it; a local re-wording in three consumers would be three copies to drift. The deferral follows an idiom already in serve.tsformatOrganizationsMountFatal's "its message is the authority on the remedy; this CLI has no further detail to add" — and the same reasoning formatI18nLoadDiagnostic's docblock gives for interpolating only the kind TOKEN.

An error carrying no kind never came from the host importer at all and keeps the declare-and-install arm it has always had. Narrowing that one is a different question about a different fact, and is not this branch's.

⛔ The seam with #14278 / PR #15047

packages/types/src/node.ts is not touched — verified on the diff:

git diff --name-only origin/main...HEAD | grep -E '^packages/types/' # no match

The strings this card defers to (/publishes no entry/, /INSTALL problem/) are pinned by #14278's tests in packages/types/src/node.test.ts. This PR changes the three callers' remedy bullets so they stop contradicting that message.

Proof

Red-first, per formatter, naming which arm fired. Tests landed in 063d976b7a, implementation in 78a0a7587f. Against the tests-only tree, all three sites reproduced the defect with a fixture carrying the third kind:

serve.ts expected " • this app DECLARES @objectstack/organizations (dependencies: "^1.2.3") and it IS"
received " • add @objectstack/organizations (the enterprise multi-org runtime) to THIS APP"
harness.ts received "… Install/link it in THIS APP (/tmp/os-verify-org-host-no-entry-AFcXGm) — and DECLARE it
in that app's package.json …" followed, in the same string, by the importer's
"the package publishes no entry that Node can load"
dogfood received "To enable it, declare @objectstack/organizations in /tmp/os-dogfood-org-no-entry-gd5Vpw's
own package.json and install it …" same contradiction, same string

The verify and dogfood fixtures are real host roots on disk — a real node_modules, a real installed package whose manifest publishes exports: { ".": { types: "./index.d.ts" } } and nothing else, nothing mocked. Neither condition set resolves it (ESM_IMPORT_CONDITIONS / CJS_REQUIRE_CONDITIONS), so the importer classifies it declared-no-loadable-entry for real rather than by injection.

Reverse verification — the three formatters restored to their pre-implementation bytes, same suites re-run, then restored:

mutation ON DISK (blob hash + marker counts)
packages/cli/src/commands/serve.ts now=bd070ffe == pre? YES != head? YES kind-marker-count=1
packages/verify/src/harness.ts now=479de177 == pre? YES != head? YES kind-marker-count=0
packages/qa/dogfood/test/enterprise-organizations.ts now=cc6284e9 == pre? YES != head? YES kind-marker-count=0
× site 2b — the `declared-no-loadable-entry` remedy DEFERS … Tests 1 failed | 11 passed (12)
× DEFERS to the importer for a declared, installed package … Tests 1 failed | 6 passed (7)
× DEFERS to the importer when the package is declared, installed …
× THROWS with that same deferral when the run declares the package Tests 2 failed | 7 passed (9)

Restore proven by blob-hash equality against the HEAD blob for all three files, with git status --porcelain empty and git diff HEAD clean — never by an editor exit code. The mutation ran under trap … EXIT INT TERM restoring absolute paths, and the restore leg uses git checkout HEAD -- PATH rather than a bare git checkout -- PATH, since the mutation leg wrote the index too. No rebuild is involved on either leg: all three suites import their subject by relative source path (./serve.js, ./harness, ./enterprise-organizations.js), not through a package exports map into dist.

Controls, byte-unchanged, in the same runs. The undeclared and declared-unresolvable arms are pinned at all three sites and pass identically either side of the change — they are green on the mutated tree above and on the implemented tree. Two of those controls are new here: neither the verify suite nor the dogfood suite had a declared-unresolvable fixture, so this PR adds one to each (declared, not installed) purely as a control.

Green, at dd611c3856:

pnpm --filter @objectstack/cli exec vitest run src/commands/serve-organizations-message-spelling.test.ts Tests 12 passed (12)
pnpm --filter @objectstack/verify exec vitest run src/harness.host-resolution.test.ts Tests 7 passed (7)
pnpm --filter @objectstack/dogfood exec vitest run test/enterprise-organizations.test.ts Tests 9 passed (9)
pnpm --filter @objectstack/cli typecheck && pnpm --filter @objectstack/verify typecheck
&& pnpm --filter @objectstack/dogfood typecheck os-verify-lock: VERDICT command-exit 0
pnpm lint (eslint . --no-inline-config, whole repo, not narrowed) os-verify-lock: VERDICT command-exit 0

Gates

The union was derived at the final HEAD dd611c3856 on a clean tree with node scripts/pm/dispatch-gates.mjs --repo objectstack-ai/objectstack --commands — 50 families (39 by path + 7 by change kind + 6 declared whole-tree, 2 reached both ways) — never from a hand-written path list. All 50 were run locally; each gate's exit code was captured before any pipe. 49 green first time.

One was a real red, and it was mine.pnpm check:type-check-debt reported:

check-type-check-coverage --re-measure: 1 ledger entr(ies) drifted upward
• @objectstack/verify: TEST_DEBT records 3 raw tsc error(s), `tsc --noEmit` now reports 5 (+2).

harness.host-resolution.test.ts imports ./harness without the .js extension, so under NodeNext the specifier does not resolve and every symbol it names is any — which makes a .then(onFulfilled, onRejected) callback parameter implicitly any. My two new cases used that shape and added 2 × TS7006. TEST_DEBT is shrink-only, so the fix is the errors, not the ceiling: dd611c3856 rewrites both as try/catch (a const initialised from an any call is not a diagnostic; only a parameter is). ⛔ The one-line .js fix that would graduate the entry is deliberately not taken here — different card, filed as #15145.

Re-run at the final HEAD after that fix, each exit captured per gate:

RATCHET check:nul-bytes exit=0 RATCHET check:engine-double-contract exit=0
RATCHET check:test-source-alias exit=0 RATCHET check:query-options-erasure exit=0
RATCHET check:cross-package-test-inputs exit=0 RATCHET check:type-check-coverage exit=0
RATCHET check:where-matcher exit=0 RATCHET check:type-check-debt exit=0
check-type-check-coverage --re-measure: OK — 17 ledger entr(ies) re-measured in 166.5s,
217 raw tsc error(s) total, none above its recorded number.

Re-deriving the union at dd611c3856 returns the same 50 families — the ratchet fix implicated nothing new.

Typecheck coverage — measured, not assumed

Measured with tsc --noEmit --listFiles, because "typecheck is clean" says nothing about a file the program never read:

edited filein a tsc program?
packages/cli/src/commands/serve.tspackages/cli/tsconfig.json
packages/cli/src/commands/serve-organizations-message-spelling.test.tspackages/cli/tsconfig.json — its include: ['src'] reaches this file, since the pin lives under src/commands/, not under test/. It is 0 files in tsconfig.test.json, whose project is test/.
packages/qa/dogfood/test/enterprise-organizations.ts and its testpackages/qa/dogfood/tsconfig.json (include: ['test/**/*'])
packages/verify/src/harness.tspackages/verify/tsconfig.json
packages/verify/src/harness.host-resolution.test.tsnot in that program — exclude lists **/*.test.ts, and the package has no check:test-typecheck wiring. Its types are read only by check:type-check-debt --re-measure, which is what caught the +2 above. Filed as #15145.

So: pnpm --filter @objectstack/verify typecheck being green is not a statement about the test file this PR edits. The statement that covers it is the ratchet line quoted above.

Changeset

Measured from each of the three packages' own files field, because the answers differ:

  • @objectstack/clifiles: ["dist", …], publishConfig.access: public ⇒ published, src/commands/serve.ts ships ⇒ in the changeset.
  • @objectstack/verifyfiles: ["dist", …], published ⇒ src/harness.ts ships ⇒ in the changeset.
  • @objectstack/dogfoodprivate: true, no files field at all ⇒ publishes nothing ⇒ not in the changeset, and cannot be.

Patch, not minor: operator-facing wording on a failure path, no API change. Nothing here removes or renames anything an author can write, so no ADR-0087 disposition marker is owed.

Out of scope, filed

Fences

packages/types/src/node.ts, packages/rest/**, packages/spec/** and content/docs/releases/** are absent from the diff (asserted above). serve.ts:133's "keep the two in sync" comment and every apps/objectos / apps/cloud comment are untouched — 0 diff hits for that string. packages/verify/src/harness.ts moved one remedy branch and nothing else in the ADR-0105 bootStack harness.

Draft on purpose: ⛔ not flipped ready, ⛔ auto-merge not armed — the PM does that.

🤖 Generated with Claude Code

https://claude.ai/code/session_016yfqQh2dBgPAymYd7xipza


Generated by Claude Code

…three sites
Red-first for #14270. #14041 added a third `HostImportFailureKind`,
`declared-no-loadable-entry` — the app declares the package, the install
delivered it, and the package's own `exports` names no runtime entry Node
can load. Three consumers pick their remedy with a two-way branch written
when only two kinds existed, so the third falls into the else leg and
renders the DECLARE remedy at an operator who has already declared AND
installed it.
Each site gets a fixture carrying that kind and an assertion naming which
arm fired. The verify and dogfood fixtures are real host roots on disk
whose installed package publishes a `types`-only `exports` map; the CLI
formatter takes the kind directly. Controls for `undeclared` and
`declared-unresolvable` are untouched.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016yfqQh2dBgPAymYd7xipza
…not on one kind
`declared-no-loadable-entry` (#14041) means the app declares the package,
the install delivered it, and the package's own `exports` names no runtime
entry Node can load. All three consumers picked their remedy with a
two-way branch written when only two kinds existed, so that kind fell into
the else leg and printed "declare it in the app's package.json and
install" — at an operator who has already done both, and directly
contradicting the importer's own message printed beside it.
Each branch now asks "is the declaration the problem?": undeclared keeps
the declare remedy, `declared-unresolvable` keeps the install remedy, and
the third kind prescribes nothing and defers to the importer's message,
which every one of the three sites already interpolates. No fourth remedy
sentence is minted — the importer words the package-shape case once, and a
local re-wording would be a second copy to drift.
`packages/types` is untouched: the strings deferred to are #14278's.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016yfqQh2dBgPAymYd7xipza
…tion the app already has
Patch for @objectstack/cli and @objectstack/verify — the two published
packages whose `files` field ships the changed code. @objectstack/dogfood
is `private: true` with no `files`, so it publishes nothing and takes no
changeset entry.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016yfqQh2dBgPAymYd7xipza
…EST_DEBT
`harness.host-resolution.test.ts` imports `./harness` without the `.js`
extension, so under NodeNext the specifier does not resolve and every
symbol it names is `any` — which makes a `.then` callback PARAMETER
implicitly any. The two new cases used `.then(onFulfilled, onRejected)`
and added 2 x TS7006, taking the package's TEST_DEBT entry from its
recorded 3 to 5. That ledger is shrink-only, so the fix is the errors,
not the ceiling.
try/catch instead: a `const` initialised from an `any` call is not a
diagnostic, only a parameter is. `check:type-check-debt --re-measure`
now reports 17 entries re-measured, 217 raw errors, none above its
recorded number.
⛔ Deliberately NOT the one-line `.js` fix that graduates the entry — that
is a different card's, filed separately.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016yfqQh2dBgPAymYd7xipza
@github-actions

Copy link
Copy Markdown
Contributor

📓 Docs Drift Check

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

17 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 4283b72a15f9c5ced0755a1b9c8aa53e5f8e16fa.

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

What this run could not see
  • 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 — 23 page(s) merely mention a changed package (the pre-#9192 predicate, kept for the deliberately-wide backstop): node scripts/docs-audit/affected-docs.mjs --json 4283b72a15f9c5ced0755a1b9c8aa53e5f8e16fapackageMentionDocs.

Which tree this was computed on

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

⚠️ 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 4283b72a15f9c5ced0755a1b9c8aa53e5f8e16fa → 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 4, 2026
@os-trump
os-trump marked this pull request as ready for review September 4, 2026 04:20
@os-trump
os-trump added this pull request to the merge queueSep 4, 2026
Merged via the queue into main with commit 5cf1c88Sep 4, 2026
36 checks passed
@os-trump
os-trump deleted the claude/issue-14270-remedy-formatter-third-kind branch September 4, 2026 04:48
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

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

fix(cli,verify,dogfood): branch the multi-org remedy on the ABSENCE, not on one failure kind - #15158

Merged
os-trump merged 4 commits into
mainfrom
claude/issue-14270-remedy-formatter-third-kind
Sep 4, 2026
Merged

fix(cli,verify,dogfood): branch the multi-org remedy on the ABSENCE, not on one failure kind#15158
os-trump merged 4 commits into
mainfrom
claude/issue-14270-remedy-formatter-third-kind

Conversation

@os-trump

Copy link
Copy Markdown
Collaborator

Fixes#14270

What was wrong

HostImportFailureKind got a third member in #14041declared-no-loadable-entry: the app declares the package, the install delivered it, and the package's own exports names no runtime entry Node can load (a types-only or browser-only publish, or an unexported subpath). Three consumers pick their remedy line with a two-way branch written when the type had exactly two members, so the third kind fell into the else leg and printed the declare remedy:

sitesymbolwhat the third kind rendered
packages/cli/src/commands/serve.tsformatOrganizationsInstallRemedy"add the package to THIS APP — declare it in the app's package.json and install"
packages/verify/src/harness.tsthe remedy in bootStack's multi-tenant fatal"Install/link it in THIS APP — and DECLARE it in that app's package.json"
packages/qa/dogfood/test/enterprise-organizations.tsprobeOrganizations's refusal / skip reason"declare it in the app's own package.json and install it"

Each of those three also interpolates the importer's own message, which words the case correctly ("the package publishes no entry that Node can load … the remedy lives in the package"). So the bullet an operator reads first contradicted the diagnosis printed underneath it, and prescribed two actions that were already done and could not have helped. That is the confidently-wrong-verdict class #14041 removed one layer down, reintroduced by its callers.

The shape

Each branch now asks "is the declaration the problem?" rather than testing one kind:

  • undeclared ⇒ declare remedy — unchanged
  • declared-unresolvable ⇒ install remedy — unchanged
  • declared-no-loadable-entryprescribes nothing. It names the two things that are not the problem and defers to the importer's message, which every one of the three sites already prints.

No fourth remedy sentence is minted. The importer words the package-shape case once, in the place that measured it; a local re-wording in three consumers would be three copies to drift. The deferral follows an idiom already in serve.tsformatOrganizationsMountFatal's "its message is the authority on the remedy; this CLI has no further detail to add" — and the same reasoning formatI18nLoadDiagnostic's docblock gives for interpolating only the kind TOKEN.

An error carrying no kind never came from the host importer at all and keeps the declare-and-install arm it has always had. Narrowing that one is a different question about a different fact, and is not this branch's.

⛔ The seam with #14278 / PR #15047

packages/types/src/node.ts is not touched — verified on the diff:

git diff --name-only origin/main...HEAD | grep -E '^packages/types/' # no match

The strings this card defers to (/publishes no entry/, /INSTALL problem/) are pinned by #14278's tests in packages/types/src/node.test.ts. This PR changes the three callers' remedy bullets so they stop contradicting that message.

Proof

Red-first, per formatter, naming which arm fired. Tests landed in 063d976b7a, implementation in 78a0a7587f. Against the tests-only tree, all three sites reproduced the defect with a fixture carrying the third kind:

serve.ts expected " • this app DECLARES @objectstack/organizations (dependencies: "^1.2.3") and it IS"
received " • add @objectstack/organizations (the enterprise multi-org runtime) to THIS APP"
harness.ts received "… Install/link it in THIS APP (/tmp/os-verify-org-host-no-entry-AFcXGm) — and DECLARE it
in that app's package.json …" followed, in the same string, by the importer's
"the package publishes no entry that Node can load"
dogfood received "To enable it, declare @objectstack/organizations in /tmp/os-dogfood-org-no-entry-gd5Vpw's
own package.json and install it …" same contradiction, same string

The verify and dogfood fixtures are real host roots on disk — a real node_modules, a real installed package whose manifest publishes exports: { ".": { types: "./index.d.ts" } } and nothing else, nothing mocked. Neither condition set resolves it (ESM_IMPORT_CONDITIONS / CJS_REQUIRE_CONDITIONS), so the importer classifies it declared-no-loadable-entry for real rather than by injection.

Reverse verification — the three formatters restored to their pre-implementation bytes, same suites re-run, then restored:

mutation ON DISK (blob hash + marker counts)
packages/cli/src/commands/serve.ts now=bd070ffe == pre? YES != head? YES kind-marker-count=1
packages/verify/src/harness.ts now=479de177 == pre? YES != head? YES kind-marker-count=0
packages/qa/dogfood/test/enterprise-organizations.ts now=cc6284e9 == pre? YES != head? YES kind-marker-count=0
× site 2b — the `declared-no-loadable-entry` remedy DEFERS … Tests 1 failed | 11 passed (12)
× DEFERS to the importer for a declared, installed package … Tests 1 failed | 6 passed (7)
× DEFERS to the importer when the package is declared, installed …
× THROWS with that same deferral when the run declares the package Tests 2 failed | 7 passed (9)

Restore proven by blob-hash equality against the HEAD blob for all three files, with git status --porcelain empty and git diff HEAD clean — never by an editor exit code. The mutation ran under trap … EXIT INT TERM restoring absolute paths, and the restore leg uses git checkout HEAD -- PATH rather than a bare git checkout -- PATH, since the mutation leg wrote the index too. No rebuild is involved on either leg: all three suites import their subject by relative source path (./serve.js, ./harness, ./enterprise-organizations.js), not through a package exports map into dist.

Controls, byte-unchanged, in the same runs. The undeclared and declared-unresolvable arms are pinned at all three sites and pass identically either side of the change — they are green on the mutated tree above and on the implemented tree. Two of those controls are new here: neither the verify suite nor the dogfood suite had a declared-unresolvable fixture, so this PR adds one to each (declared, not installed) purely as a control.

Green, at dd611c3856:

pnpm --filter @objectstack/cli exec vitest run src/commands/serve-organizations-message-spelling.test.ts Tests 12 passed (12)
pnpm --filter @objectstack/verify exec vitest run src/harness.host-resolution.test.ts Tests 7 passed (7)
pnpm --filter @objectstack/dogfood exec vitest run test/enterprise-organizations.test.ts Tests 9 passed (9)
pnpm --filter @objectstack/cli typecheck && pnpm --filter @objectstack/verify typecheck
&& pnpm --filter @objectstack/dogfood typecheck os-verify-lock: VERDICT command-exit 0
pnpm lint (eslint . --no-inline-config, whole repo, not narrowed) os-verify-lock: VERDICT command-exit 0

Gates

The union was derived at the final HEAD dd611c3856 on a clean tree with node scripts/pm/dispatch-gates.mjs --repo objectstack-ai/objectstack --commands — 50 families (39 by path + 7 by change kind + 6 declared whole-tree, 2 reached both ways) — never from a hand-written path list. All 50 were run locally; each gate's exit code was captured before any pipe. 49 green first time.

One was a real red, and it was mine.pnpm check:type-check-debt reported:

check-type-check-coverage --re-measure: 1 ledger entr(ies) drifted upward
• @objectstack/verify: TEST_DEBT records 3 raw tsc error(s), `tsc --noEmit` now reports 5 (+2).

harness.host-resolution.test.ts imports ./harness without the .js extension, so under NodeNext the specifier does not resolve and every symbol it names is any — which makes a .then(onFulfilled, onRejected) callback parameter implicitly any. My two new cases used that shape and added 2 × TS7006. TEST_DEBT is shrink-only, so the fix is the errors, not the ceiling: dd611c3856 rewrites both as try/catch (a const initialised from an any call is not a diagnostic; only a parameter is). ⛔ The one-line .js fix that would graduate the entry is deliberately not taken here — different card, filed as #15145.

Re-run at the final HEAD after that fix, each exit captured per gate:

RATCHET check:nul-bytes exit=0 RATCHET check:engine-double-contract exit=0
RATCHET check:test-source-alias exit=0 RATCHET check:query-options-erasure exit=0
RATCHET check:cross-package-test-inputs exit=0 RATCHET check:type-check-coverage exit=0
RATCHET check:where-matcher exit=0 RATCHET check:type-check-debt exit=0
check-type-check-coverage --re-measure: OK — 17 ledger entr(ies) re-measured in 166.5s,
217 raw tsc error(s) total, none above its recorded number.

Re-deriving the union at dd611c3856 returns the same 50 families — the ratchet fix implicated nothing new.

Typecheck coverage — measured, not assumed

Measured with tsc --noEmit --listFiles, because "typecheck is clean" says nothing about a file the program never read:

edited filein a tsc program?
packages/cli/src/commands/serve.tspackages/cli/tsconfig.json
packages/cli/src/commands/serve-organizations-message-spelling.test.tspackages/cli/tsconfig.json — its include: ['src'] reaches this file, since the pin lives under src/commands/, not under test/. It is 0 files in tsconfig.test.json, whose project is test/.
packages/qa/dogfood/test/enterprise-organizations.ts and its testpackages/qa/dogfood/tsconfig.json (include: ['test/**/*'])
packages/verify/src/harness.tspackages/verify/tsconfig.json
packages/verify/src/harness.host-resolution.test.tsnot in that program — exclude lists **/*.test.ts, and the package has no check:test-typecheck wiring. Its types are read only by check:type-check-debt --re-measure, which is what caught the +2 above. Filed as #15145.

So: pnpm --filter @objectstack/verify typecheck being green is not a statement about the test file this PR edits. The statement that covers it is the ratchet line quoted above.

Changeset

Measured from each of the three packages' own files field, because the answers differ:

  • @objectstack/clifiles: ["dist", …], publishConfig.access: public ⇒ published, src/commands/serve.ts ships ⇒ in the changeset.
  • @objectstack/verifyfiles: ["dist", …], published ⇒ src/harness.ts ships ⇒ in the changeset.
  • @objectstack/dogfoodprivate: true, no files field at all ⇒ publishes nothing ⇒ not in the changeset, and cannot be.

Patch, not minor: operator-facing wording on a failure path, no API change. Nothing here removes or renames anything an author can write, so no ADR-0087 disposition marker is owed.

Out of scope, filed

Fences

packages/types/src/node.ts, packages/rest/**, packages/spec/** and content/docs/releases/** are absent from the diff (asserted above). serve.ts:133's "keep the two in sync" comment and every apps/objectos / apps/cloud comment are untouched — 0 diff hits for that string. packages/verify/src/harness.ts moved one remedy branch and nothing else in the ADR-0105 bootStack harness.

Draft on purpose: ⛔ not flipped ready, ⛔ auto-merge not armed — the PM does that.

🤖 Generated with Claude Code

https://claude.ai/code/session_016yfqQh2dBgPAymYd7xipza


Generated by Claude Code

…three sites
Red-first for #14270. #14041 added a third `HostImportFailureKind`,
`declared-no-loadable-entry` — the app declares the package, the install
delivered it, and the package's own `exports` names no runtime entry Node
can load. Three consumers pick their remedy with a two-way branch written
when only two kinds existed, so the third falls into the else leg and
renders the DECLARE remedy at an operator who has already declared AND
installed it.
Each site gets a fixture carrying that kind and an assertion naming which
arm fired. The verify and dogfood fixtures are real host roots on disk
whose installed package publishes a `types`-only `exports` map; the CLI
formatter takes the kind directly. Controls for `undeclared` and
`declared-unresolvable` are untouched.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016yfqQh2dBgPAymYd7xipza
…not on one kind
`declared-no-loadable-entry` (#14041) means the app declares the package,
the install delivered it, and the package's own `exports` names no runtime
entry Node can load. All three consumers picked their remedy with a
two-way branch written when only two kinds existed, so that kind fell into
the else leg and printed "declare it in the app's package.json and
install" — at an operator who has already done both, and directly
contradicting the importer's own message printed beside it.
Each branch now asks "is the declaration the problem?": undeclared keeps
the declare remedy, `declared-unresolvable` keeps the install remedy, and
the third kind prescribes nothing and defers to the importer's message,
which every one of the three sites already interpolates. No fourth remedy
sentence is minted — the importer words the package-shape case once, and a
local re-wording would be a second copy to drift.
`packages/types` is untouched: the strings deferred to are #14278's.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016yfqQh2dBgPAymYd7xipza
…tion the app already has
Patch for @objectstack/cli and @objectstack/verify — the two published
packages whose `files` field ships the changed code. @objectstack/dogfood
is `private: true` with no `files`, so it publishes nothing and takes no
changeset entry.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016yfqQh2dBgPAymYd7xipza
…EST_DEBT
`harness.host-resolution.test.ts` imports `./harness` without the `.js`
extension, so under NodeNext the specifier does not resolve and every
symbol it names is `any` — which makes a `.then` callback PARAMETER
implicitly any. The two new cases used `.then(onFulfilled, onRejected)`
and added 2 x TS7006, taking the package's TEST_DEBT entry from its
recorded 3 to 5. That ledger is shrink-only, so the fix is the errors,
not the ceiling.
try/catch instead: a `const` initialised from an `any` call is not a
diagnostic, only a parameter is. `check:type-check-debt --re-measure`
now reports 17 entries re-measured, 217 raw errors, none above its
recorded number.
⛔ Deliberately NOT the one-line `.js` fix that graduates the entry — that
is a different card's, filed separately.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016yfqQh2dBgPAymYd7xipza
@github-actions

Copy link
Copy Markdown
Contributor

📓 Docs Drift Check

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

17 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 4283b72a15f9c5ced0755a1b9c8aa53e5f8e16fa.

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

What this run could not see
  • 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 — 23 page(s) merely mention a changed package (the pre-#9192 predicate, kept for the deliberately-wide backstop): node scripts/docs-audit/affected-docs.mjs --json 4283b72a15f9c5ced0755a1b9c8aa53e5f8e16fapackageMentionDocs.

Which tree this was computed on

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

⚠️ 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 4283b72a15f9c5ced0755a1b9c8aa53e5f8e16fa → 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 4, 2026
@os-trump
os-trump marked this pull request as ready for review September 4, 2026 04:20
@os-trump
os-trump added this pull request to the merge queueSep 4, 2026
Merged via the queue into main with commit 5cf1c88Sep 4, 2026
36 checks passed
@os-trump
os-trump deleted the claude/issue-14270-remedy-formatter-third-kind branch September 4, 2026 04:48
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

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

fix(cli,verify,dogfood): branch the multi-org remedy on the ABSENCE, not on one failure kind - #15158

Merged
os-trump merged 4 commits into
mainfrom
claude/issue-14270-remedy-formatter-third-kind
Sep 4, 2026
Merged

fix(cli,verify,dogfood): branch the multi-org remedy on the ABSENCE, not on one failure kind#15158
os-trump merged 4 commits into
mainfrom
claude/issue-14270-remedy-formatter-third-kind

Conversation

@os-trump

Copy link
Copy Markdown
Collaborator

Fixes#14270

What was wrong

HostImportFailureKind got a third member in #14041declared-no-loadable-entry: the app declares the package, the install delivered it, and the package's own exports names no runtime entry Node can load (a types-only or browser-only publish, or an unexported subpath). Three consumers pick their remedy line with a two-way branch written when the type had exactly two members, so the third kind fell into the else leg and printed the declare remedy:

sitesymbolwhat the third kind rendered
packages/cli/src/commands/serve.tsformatOrganizationsInstallRemedy"add the package to THIS APP — declare it in the app's package.json and install"
packages/verify/src/harness.tsthe remedy in bootStack's multi-tenant fatal"Install/link it in THIS APP — and DECLARE it in that app's package.json"
packages/qa/dogfood/test/enterprise-organizations.tsprobeOrganizations's refusal / skip reason"declare it in the app's own package.json and install it"

Each of those three also interpolates the importer's own message, which words the case correctly ("the package publishes no entry that Node can load … the remedy lives in the package"). So the bullet an operator reads first contradicted the diagnosis printed underneath it, and prescribed two actions that were already done and could not have helped. That is the confidently-wrong-verdict class #14041 removed one layer down, reintroduced by its callers.

The shape

Each branch now asks "is the declaration the problem?" rather than testing one kind:

  • undeclared ⇒ declare remedy — unchanged
  • declared-unresolvable ⇒ install remedy — unchanged
  • declared-no-loadable-entryprescribes nothing. It names the two things that are not the problem and defers to the importer's message, which every one of the three sites already prints.

No fourth remedy sentence is minted. The importer words the package-shape case once, in the place that measured it; a local re-wording in three consumers would be three copies to drift. The deferral follows an idiom already in serve.tsformatOrganizationsMountFatal's "its message is the authority on the remedy; this CLI has no further detail to add" — and the same reasoning formatI18nLoadDiagnostic's docblock gives for interpolating only the kind TOKEN.

An error carrying no kind never came from the host importer at all and keeps the declare-and-install arm it has always had. Narrowing that one is a different question about a different fact, and is not this branch's.

⛔ The seam with #14278 / PR #15047

packages/types/src/node.ts is not touched — verified on the diff:

git diff --name-only origin/main...HEAD | grep -E '^packages/types/' # no match

The strings this card defers to (/publishes no entry/, /INSTALL problem/) are pinned by #14278's tests in packages/types/src/node.test.ts. This PR changes the three callers' remedy bullets so they stop contradicting that message.

Proof

Red-first, per formatter, naming which arm fired. Tests landed in 063d976b7a, implementation in 78a0a7587f. Against the tests-only tree, all three sites reproduced the defect with a fixture carrying the third kind:

serve.ts expected " • this app DECLARES @objectstack/organizations (dependencies: "^1.2.3") and it IS"
received " • add @objectstack/organizations (the enterprise multi-org runtime) to THIS APP"
harness.ts received "… Install/link it in THIS APP (/tmp/os-verify-org-host-no-entry-AFcXGm) — and DECLARE it
in that app's package.json …" followed, in the same string, by the importer's
"the package publishes no entry that Node can load"
dogfood received "To enable it, declare @objectstack/organizations in /tmp/os-dogfood-org-no-entry-gd5Vpw's
own package.json and install it …" same contradiction, same string

The verify and dogfood fixtures are real host roots on disk — a real node_modules, a real installed package whose manifest publishes exports: { ".": { types: "./index.d.ts" } } and nothing else, nothing mocked. Neither condition set resolves it (ESM_IMPORT_CONDITIONS / CJS_REQUIRE_CONDITIONS), so the importer classifies it declared-no-loadable-entry for real rather than by injection.

Reverse verification — the three formatters restored to their pre-implementation bytes, same suites re-run, then restored:

mutation ON DISK (blob hash + marker counts)
packages/cli/src/commands/serve.ts now=bd070ffe == pre? YES != head? YES kind-marker-count=1
packages/verify/src/harness.ts now=479de177 == pre? YES != head? YES kind-marker-count=0
packages/qa/dogfood/test/enterprise-organizations.ts now=cc6284e9 == pre? YES != head? YES kind-marker-count=0
× site 2b — the `declared-no-loadable-entry` remedy DEFERS … Tests 1 failed | 11 passed (12)
× DEFERS to the importer for a declared, installed package … Tests 1 failed | 6 passed (7)
× DEFERS to the importer when the package is declared, installed …
× THROWS with that same deferral when the run declares the package Tests 2 failed | 7 passed (9)

Restore proven by blob-hash equality against the HEAD blob for all three files, with git status --porcelain empty and git diff HEAD clean — never by an editor exit code. The mutation ran under trap … EXIT INT TERM restoring absolute paths, and the restore leg uses git checkout HEAD -- PATH rather than a bare git checkout -- PATH, since the mutation leg wrote the index too. No rebuild is involved on either leg: all three suites import their subject by relative source path (./serve.js, ./harness, ./enterprise-organizations.js), not through a package exports map into dist.

Controls, byte-unchanged, in the same runs. The undeclared and declared-unresolvable arms are pinned at all three sites and pass identically either side of the change — they are green on the mutated tree above and on the implemented tree. Two of those controls are new here: neither the verify suite nor the dogfood suite had a declared-unresolvable fixture, so this PR adds one to each (declared, not installed) purely as a control.

Green, at dd611c3856:

pnpm --filter @objectstack/cli exec vitest run src/commands/serve-organizations-message-spelling.test.ts Tests 12 passed (12)
pnpm --filter @objectstack/verify exec vitest run src/harness.host-resolution.test.ts Tests 7 passed (7)
pnpm --filter @objectstack/dogfood exec vitest run test/enterprise-organizations.test.ts Tests 9 passed (9)
pnpm --filter @objectstack/cli typecheck && pnpm --filter @objectstack/verify typecheck
&& pnpm --filter @objectstack/dogfood typecheck os-verify-lock: VERDICT command-exit 0
pnpm lint (eslint . --no-inline-config, whole repo, not narrowed) os-verify-lock: VERDICT command-exit 0

Gates

The union was derived at the final HEAD dd611c3856 on a clean tree with node scripts/pm/dispatch-gates.mjs --repo objectstack-ai/objectstack --commands — 50 families (39 by path + 7 by change kind + 6 declared whole-tree, 2 reached both ways) — never from a hand-written path list. All 50 were run locally; each gate's exit code was captured before any pipe. 49 green first time.

One was a real red, and it was mine.pnpm check:type-check-debt reported:

check-type-check-coverage --re-measure: 1 ledger entr(ies) drifted upward
• @objectstack/verify: TEST_DEBT records 3 raw tsc error(s), `tsc --noEmit` now reports 5 (+2).

harness.host-resolution.test.ts imports ./harness without the .js extension, so under NodeNext the specifier does not resolve and every symbol it names is any — which makes a .then(onFulfilled, onRejected) callback parameter implicitly any. My two new cases used that shape and added 2 × TS7006. TEST_DEBT is shrink-only, so the fix is the errors, not the ceiling: dd611c3856 rewrites both as try/catch (a const initialised from an any call is not a diagnostic; only a parameter is). ⛔ The one-line .js fix that would graduate the entry is deliberately not taken here — different card, filed as #15145.

Re-run at the final HEAD after that fix, each exit captured per gate:

RATCHET check:nul-bytes exit=0 RATCHET check:engine-double-contract exit=0
RATCHET check:test-source-alias exit=0 RATCHET check:query-options-erasure exit=0
RATCHET check:cross-package-test-inputs exit=0 RATCHET check:type-check-coverage exit=0
RATCHET check:where-matcher exit=0 RATCHET check:type-check-debt exit=0
check-type-check-coverage --re-measure: OK — 17 ledger entr(ies) re-measured in 166.5s,
217 raw tsc error(s) total, none above its recorded number.

Re-deriving the union at dd611c3856 returns the same 50 families — the ratchet fix implicated nothing new.

Typecheck coverage — measured, not assumed

Measured with tsc --noEmit --listFiles, because "typecheck is clean" says nothing about a file the program never read:

edited filein a tsc program?
packages/cli/src/commands/serve.tspackages/cli/tsconfig.json
packages/cli/src/commands/serve-organizations-message-spelling.test.tspackages/cli/tsconfig.json — its include: ['src'] reaches this file, since the pin lives under src/commands/, not under test/. It is 0 files in tsconfig.test.json, whose project is test/.
packages/qa/dogfood/test/enterprise-organizations.ts and its testpackages/qa/dogfood/tsconfig.json (include: ['test/**/*'])
packages/verify/src/harness.tspackages/verify/tsconfig.json
packages/verify/src/harness.host-resolution.test.tsnot in that program — exclude lists **/*.test.ts, and the package has no check:test-typecheck wiring. Its types are read only by check:type-check-debt --re-measure, which is what caught the +2 above. Filed as #15145.

So: pnpm --filter @objectstack/verify typecheck being green is not a statement about the test file this PR edits. The statement that covers it is the ratchet line quoted above.

Changeset

Measured from each of the three packages' own files field, because the answers differ:

  • @objectstack/clifiles: ["dist", …], publishConfig.access: public ⇒ published, src/commands/serve.ts ships ⇒ in the changeset.
  • @objectstack/verifyfiles: ["dist", …], published ⇒ src/harness.ts ships ⇒ in the changeset.
  • @objectstack/dogfoodprivate: true, no files field at all ⇒ publishes nothing ⇒ not in the changeset, and cannot be.

Patch, not minor: operator-facing wording on a failure path, no API change. Nothing here removes or renames anything an author can write, so no ADR-0087 disposition marker is owed.

Out of scope, filed

Fences

packages/types/src/node.ts, packages/rest/**, packages/spec/** and content/docs/releases/** are absent from the diff (asserted above). serve.ts:133's "keep the two in sync" comment and every apps/objectos / apps/cloud comment are untouched — 0 diff hits for that string. packages/verify/src/harness.ts moved one remedy branch and nothing else in the ADR-0105 bootStack harness.

Draft on purpose: ⛔ not flipped ready, ⛔ auto-merge not armed — the PM does that.

🤖 Generated with Claude Code

https://claude.ai/code/session_016yfqQh2dBgPAymYd7xipza


Generated by Claude Code

…three sites
Red-first for #14270. #14041 added a third `HostImportFailureKind`,
`declared-no-loadable-entry` — the app declares the package, the install
delivered it, and the package's own `exports` names no runtime entry Node
can load. Three consumers pick their remedy with a two-way branch written
when only two kinds existed, so the third falls into the else leg and
renders the DECLARE remedy at an operator who has already declared AND
installed it.
Each site gets a fixture carrying that kind and an assertion naming which
arm fired. The verify and dogfood fixtures are real host roots on disk
whose installed package publishes a `types`-only `exports` map; the CLI
formatter takes the kind directly. Controls for `undeclared` and
`declared-unresolvable` are untouched.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016yfqQh2dBgPAymYd7xipza
…not on one kind
`declared-no-loadable-entry` (#14041) means the app declares the package,
the install delivered it, and the package's own `exports` names no runtime
entry Node can load. All three consumers picked their remedy with a
two-way branch written when only two kinds existed, so that kind fell into
the else leg and printed "declare it in the app's package.json and
install" — at an operator who has already done both, and directly
contradicting the importer's own message printed beside it.
Each branch now asks "is the declaration the problem?": undeclared keeps
the declare remedy, `declared-unresolvable` keeps the install remedy, and
the third kind prescribes nothing and defers to the importer's message,
which every one of the three sites already interpolates. No fourth remedy
sentence is minted — the importer words the package-shape case once, and a
local re-wording would be a second copy to drift.
`packages/types` is untouched: the strings deferred to are #14278's.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016yfqQh2dBgPAymYd7xipza
…tion the app already has
Patch for @objectstack/cli and @objectstack/verify — the two published
packages whose `files` field ships the changed code. @objectstack/dogfood
is `private: true` with no `files`, so it publishes nothing and takes no
changeset entry.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016yfqQh2dBgPAymYd7xipza
…EST_DEBT
`harness.host-resolution.test.ts` imports `./harness` without the `.js`
extension, so under NodeNext the specifier does not resolve and every
symbol it names is `any` — which makes a `.then` callback PARAMETER
implicitly any. The two new cases used `.then(onFulfilled, onRejected)`
and added 2 x TS7006, taking the package's TEST_DEBT entry from its
recorded 3 to 5. That ledger is shrink-only, so the fix is the errors,
not the ceiling.
try/catch instead: a `const` initialised from an `any` call is not a
diagnostic, only a parameter is. `check:type-check-debt --re-measure`
now reports 17 entries re-measured, 217 raw errors, none above its
recorded number.
⛔ Deliberately NOT the one-line `.js` fix that graduates the entry — that
is a different card's, filed separately.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016yfqQh2dBgPAymYd7xipza
@github-actions

Copy link
Copy Markdown
Contributor

📓 Docs Drift Check

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

17 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 4283b72a15f9c5ced0755a1b9c8aa53e5f8e16fa.

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

What this run could not see
  • 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 — 23 page(s) merely mention a changed package (the pre-#9192 predicate, kept for the deliberately-wide backstop): node scripts/docs-audit/affected-docs.mjs --json 4283b72a15f9c5ced0755a1b9c8aa53e5f8e16fapackageMentionDocs.

Which tree this was computed on

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

⚠️ 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 4283b72a15f9c5ced0755a1b9c8aa53e5f8e16fa → 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 4, 2026
@os-trump
os-trump marked this pull request as ready for review September 4, 2026 04:20
@os-trump
os-trump added this pull request to the merge queueSep 4, 2026
Merged via the queue into main with commit 5cf1c88Sep 4, 2026
36 checks passed
@os-trump
os-trump deleted the claude/issue-14270-remedy-formatter-third-kind branch September 4, 2026 04:48
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

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

fix(cli,verify,dogfood): branch the multi-org remedy on the ABSENCE, not on one failure kind - #15158

Merged
os-trump merged 4 commits into
mainfrom
claude/issue-14270-remedy-formatter-third-kind
Sep 4, 2026
Merged

fix(cli,verify,dogfood): branch the multi-org remedy on the ABSENCE, not on one failure kind#15158
os-trump merged 4 commits into
mainfrom
claude/issue-14270-remedy-formatter-third-kind

Conversation

@os-trump

Copy link
Copy Markdown
Collaborator

Fixes#14270

What was wrong

HostImportFailureKind got a third member in #14041declared-no-loadable-entry: the app declares the package, the install delivered it, and the package's own exports names no runtime entry Node can load (a types-only or browser-only publish, or an unexported subpath). Three consumers pick their remedy line with a two-way branch written when the type had exactly two members, so the third kind fell into the else leg and printed the declare remedy:

sitesymbolwhat the third kind rendered
packages/cli/src/commands/serve.tsformatOrganizationsInstallRemedy"add the package to THIS APP — declare it in the app's package.json and install"
packages/verify/src/harness.tsthe remedy in bootStack's multi-tenant fatal"Install/link it in THIS APP — and DECLARE it in that app's package.json"
packages/qa/dogfood/test/enterprise-organizations.tsprobeOrganizations's refusal / skip reason"declare it in the app's own package.json and install it"

Each of those three also interpolates the importer's own message, which words the case correctly ("the package publishes no entry that Node can load … the remedy lives in the package"). So the bullet an operator reads first contradicted the diagnosis printed underneath it, and prescribed two actions that were already done and could not have helped. That is the confidently-wrong-verdict class #14041 removed one layer down, reintroduced by its callers.

The shape

Each branch now asks "is the declaration the problem?" rather than testing one kind:

  • undeclared ⇒ declare remedy — unchanged
  • declared-unresolvable ⇒ install remedy — unchanged
  • declared-no-loadable-entryprescribes nothing. It names the two things that are not the problem and defers to the importer's message, which every one of the three sites already prints.

No fourth remedy sentence is minted. The importer words the package-shape case once, in the place that measured it; a local re-wording in three consumers would be three copies to drift. The deferral follows an idiom already in serve.tsformatOrganizationsMountFatal's "its message is the authority on the remedy; this CLI has no further detail to add" — and the same reasoning formatI18nLoadDiagnostic's docblock gives for interpolating only the kind TOKEN.

An error carrying no kind never came from the host importer at all and keeps the declare-and-install arm it has always had. Narrowing that one is a different question about a different fact, and is not this branch's.

⛔ The seam with #14278 / PR #15047

packages/types/src/node.ts is not touched — verified on the diff:

git diff --name-only origin/main...HEAD | grep -E '^packages/types/' # no match

The strings this card defers to (/publishes no entry/, /INSTALL problem/) are pinned by #14278's tests in packages/types/src/node.test.ts. This PR changes the three callers' remedy bullets so they stop contradicting that message.

Proof

Red-first, per formatter, naming which arm fired. Tests landed in 063d976b7a, implementation in 78a0a7587f. Against the tests-only tree, all three sites reproduced the defect with a fixture carrying the third kind:

serve.ts expected " • this app DECLARES @objectstack/organizations (dependencies: "^1.2.3") and it IS"
received " • add @objectstack/organizations (the enterprise multi-org runtime) to THIS APP"
harness.ts received "… Install/link it in THIS APP (/tmp/os-verify-org-host-no-entry-AFcXGm) — and DECLARE it
in that app's package.json …" followed, in the same string, by the importer's
"the package publishes no entry that Node can load"
dogfood received "To enable it, declare @objectstack/organizations in /tmp/os-dogfood-org-no-entry-gd5Vpw's
own package.json and install it …" same contradiction, same string

The verify and dogfood fixtures are real host roots on disk — a real node_modules, a real installed package whose manifest publishes exports: { ".": { types: "./index.d.ts" } } and nothing else, nothing mocked. Neither condition set resolves it (ESM_IMPORT_CONDITIONS / CJS_REQUIRE_CONDITIONS), so the importer classifies it declared-no-loadable-entry for real rather than by injection.

Reverse verification — the three formatters restored to their pre-implementation bytes, same suites re-run, then restored:

mutation ON DISK (blob hash + marker counts)
packages/cli/src/commands/serve.ts now=bd070ffe == pre? YES != head? YES kind-marker-count=1
packages/verify/src/harness.ts now=479de177 == pre? YES != head? YES kind-marker-count=0
packages/qa/dogfood/test/enterprise-organizations.ts now=cc6284e9 == pre? YES != head? YES kind-marker-count=0
× site 2b — the `declared-no-loadable-entry` remedy DEFERS … Tests 1 failed | 11 passed (12)
× DEFERS to the importer for a declared, installed package … Tests 1 failed | 6 passed (7)
× DEFERS to the importer when the package is declared, installed …
× THROWS with that same deferral when the run declares the package Tests 2 failed | 7 passed (9)

Restore proven by blob-hash equality against the HEAD blob for all three files, with git status --porcelain empty and git diff HEAD clean — never by an editor exit code. The mutation ran under trap … EXIT INT TERM restoring absolute paths, and the restore leg uses git checkout HEAD -- PATH rather than a bare git checkout -- PATH, since the mutation leg wrote the index too. No rebuild is involved on either leg: all three suites import their subject by relative source path (./serve.js, ./harness, ./enterprise-organizations.js), not through a package exports map into dist.

Controls, byte-unchanged, in the same runs. The undeclared and declared-unresolvable arms are pinned at all three sites and pass identically either side of the change — they are green on the mutated tree above and on the implemented tree. Two of those controls are new here: neither the verify suite nor the dogfood suite had a declared-unresolvable fixture, so this PR adds one to each (declared, not installed) purely as a control.

Green, at dd611c3856:

pnpm --filter @objectstack/cli exec vitest run src/commands/serve-organizations-message-spelling.test.ts Tests 12 passed (12)
pnpm --filter @objectstack/verify exec vitest run src/harness.host-resolution.test.ts Tests 7 passed (7)
pnpm --filter @objectstack/dogfood exec vitest run test/enterprise-organizations.test.ts Tests 9 passed (9)
pnpm --filter @objectstack/cli typecheck && pnpm --filter @objectstack/verify typecheck
&& pnpm --filter @objectstack/dogfood typecheck os-verify-lock: VERDICT command-exit 0
pnpm lint (eslint . --no-inline-config, whole repo, not narrowed) os-verify-lock: VERDICT command-exit 0

Gates

The union was derived at the final HEAD dd611c3856 on a clean tree with node scripts/pm/dispatch-gates.mjs --repo objectstack-ai/objectstack --commands — 50 families (39 by path + 7 by change kind + 6 declared whole-tree, 2 reached both ways) — never from a hand-written path list. All 50 were run locally; each gate's exit code was captured before any pipe. 49 green first time.

One was a real red, and it was mine.pnpm check:type-check-debt reported:

check-type-check-coverage --re-measure: 1 ledger entr(ies) drifted upward
• @objectstack/verify: TEST_DEBT records 3 raw tsc error(s), `tsc --noEmit` now reports 5 (+2).

harness.host-resolution.test.ts imports ./harness without the .js extension, so under NodeNext the specifier does not resolve and every symbol it names is any — which makes a .then(onFulfilled, onRejected) callback parameter implicitly any. My two new cases used that shape and added 2 × TS7006. TEST_DEBT is shrink-only, so the fix is the errors, not the ceiling: dd611c3856 rewrites both as try/catch (a const initialised from an any call is not a diagnostic; only a parameter is). ⛔ The one-line .js fix that would graduate the entry is deliberately not taken here — different card, filed as #15145.

Re-run at the final HEAD after that fix, each exit captured per gate:

RATCHET check:nul-bytes exit=0 RATCHET check:engine-double-contract exit=0
RATCHET check:test-source-alias exit=0 RATCHET check:query-options-erasure exit=0
RATCHET check:cross-package-test-inputs exit=0 RATCHET check:type-check-coverage exit=0
RATCHET check:where-matcher exit=0 RATCHET check:type-check-debt exit=0
check-type-check-coverage --re-measure: OK — 17 ledger entr(ies) re-measured in 166.5s,
217 raw tsc error(s) total, none above its recorded number.

Re-deriving the union at dd611c3856 returns the same 50 families — the ratchet fix implicated nothing new.

Typecheck coverage — measured, not assumed

Measured with tsc --noEmit --listFiles, because "typecheck is clean" says nothing about a file the program never read:

edited filein a tsc program?
packages/cli/src/commands/serve.tspackages/cli/tsconfig.json
packages/cli/src/commands/serve-organizations-message-spelling.test.tspackages/cli/tsconfig.json — its include: ['src'] reaches this file, since the pin lives under src/commands/, not under test/. It is 0 files in tsconfig.test.json, whose project is test/.
packages/qa/dogfood/test/enterprise-organizations.ts and its testpackages/qa/dogfood/tsconfig.json (include: ['test/**/*'])
packages/verify/src/harness.tspackages/verify/tsconfig.json
packages/verify/src/harness.host-resolution.test.tsnot in that program — exclude lists **/*.test.ts, and the package has no check:test-typecheck wiring. Its types are read only by check:type-check-debt --re-measure, which is what caught the +2 above. Filed as #15145.

So: pnpm --filter @objectstack/verify typecheck being green is not a statement about the test file this PR edits. The statement that covers it is the ratchet line quoted above.

Changeset

Measured from each of the three packages' own files field, because the answers differ:

  • @objectstack/clifiles: ["dist", …], publishConfig.access: public ⇒ published, src/commands/serve.ts ships ⇒ in the changeset.
  • @objectstack/verifyfiles: ["dist", …], published ⇒ src/harness.ts ships ⇒ in the changeset.
  • @objectstack/dogfoodprivate: true, no files field at all ⇒ publishes nothing ⇒ not in the changeset, and cannot be.

Patch, not minor: operator-facing wording on a failure path, no API change. Nothing here removes or renames anything an author can write, so no ADR-0087 disposition marker is owed.

Out of scope, filed

Fences

packages/types/src/node.ts, packages/rest/**, packages/spec/** and content/docs/releases/** are absent from the diff (asserted above). serve.ts:133's "keep the two in sync" comment and every apps/objectos / apps/cloud comment are untouched — 0 diff hits for that string. packages/verify/src/harness.ts moved one remedy branch and nothing else in the ADR-0105 bootStack harness.

Draft on purpose: ⛔ not flipped ready, ⛔ auto-merge not armed — the PM does that.

🤖 Generated with Claude Code

https://claude.ai/code/session_016yfqQh2dBgPAymYd7xipza


Generated by Claude Code

…three sites
Red-first for #14270. #14041 added a third `HostImportFailureKind`,
`declared-no-loadable-entry` — the app declares the package, the install
delivered it, and the package's own `exports` names no runtime entry Node
can load. Three consumers pick their remedy with a two-way branch written
when only two kinds existed, so the third falls into the else leg and
renders the DECLARE remedy at an operator who has already declared AND
installed it.
Each site gets a fixture carrying that kind and an assertion naming which
arm fired. The verify and dogfood fixtures are real host roots on disk
whose installed package publishes a `types`-only `exports` map; the CLI
formatter takes the kind directly. Controls for `undeclared` and
`declared-unresolvable` are untouched.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016yfqQh2dBgPAymYd7xipza
…not on one kind
`declared-no-loadable-entry` (#14041) means the app declares the package,
the install delivered it, and the package's own `exports` names no runtime
entry Node can load. All three consumers picked their remedy with a
two-way branch written when only two kinds existed, so that kind fell into
the else leg and printed "declare it in the app's package.json and
install" — at an operator who has already done both, and directly
contradicting the importer's own message printed beside it.
Each branch now asks "is the declaration the problem?": undeclared keeps
the declare remedy, `declared-unresolvable` keeps the install remedy, and
the third kind prescribes nothing and defers to the importer's message,
which every one of the three sites already interpolates. No fourth remedy
sentence is minted — the importer words the package-shape case once, and a
local re-wording would be a second copy to drift.
`packages/types` is untouched: the strings deferred to are #14278's.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016yfqQh2dBgPAymYd7xipza
…tion the app already has
Patch for @objectstack/cli and @objectstack/verify — the two published
packages whose `files` field ships the changed code. @objectstack/dogfood
is `private: true` with no `files`, so it publishes nothing and takes no
changeset entry.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016yfqQh2dBgPAymYd7xipza
…EST_DEBT
`harness.host-resolution.test.ts` imports `./harness` without the `.js`
extension, so under NodeNext the specifier does not resolve and every
symbol it names is `any` — which makes a `.then` callback PARAMETER
implicitly any. The two new cases used `.then(onFulfilled, onRejected)`
and added 2 x TS7006, taking the package's TEST_DEBT entry from its
recorded 3 to 5. That ledger is shrink-only, so the fix is the errors,
not the ceiling.
try/catch instead: a `const` initialised from an `any` call is not a
diagnostic, only a parameter is. `check:type-check-debt --re-measure`
now reports 17 entries re-measured, 217 raw errors, none above its
recorded number.
⛔ Deliberately NOT the one-line `.js` fix that graduates the entry — that
is a different card's, filed separately.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016yfqQh2dBgPAymYd7xipza
@github-actions

Copy link
Copy Markdown
Contributor

📓 Docs Drift Check

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

17 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 4283b72a15f9c5ced0755a1b9c8aa53e5f8e16fa.

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

What this run could not see
  • 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 — 23 page(s) merely mention a changed package (the pre-#9192 predicate, kept for the deliberately-wide backstop): node scripts/docs-audit/affected-docs.mjs --json 4283b72a15f9c5ced0755a1b9c8aa53e5f8e16fapackageMentionDocs.

Which tree this was computed on

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

⚠️ 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 4283b72a15f9c5ced0755a1b9c8aa53e5f8e16fa → 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 4, 2026
@os-trump
os-trump marked this pull request as ready for review September 4, 2026 04:20
@os-trump
os-trump added this pull request to the merge queueSep 4, 2026
Merged via the queue into main with commit 5cf1c88Sep 4, 2026
36 checks passed
@os-trump
os-trump deleted the claude/issue-14270-remedy-formatter-third-kind branch September 4, 2026 04:48
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

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

fix(cli,verify,dogfood): branch the multi-org remedy on the ABSENCE, not on one failure kind - #15158

Merged
os-trump merged 4 commits into
mainfrom
claude/issue-14270-remedy-formatter-third-kind
Sep 4, 2026
Merged

fix(cli,verify,dogfood): branch the multi-org remedy on the ABSENCE, not on one failure kind#15158
os-trump merged 4 commits into
mainfrom
claude/issue-14270-remedy-formatter-third-kind

Conversation

@os-trump

Copy link
Copy Markdown
Collaborator

Fixes#14270

What was wrong

HostImportFailureKind got a third member in #14041declared-no-loadable-entry: the app declares the package, the install delivered it, and the package's own exports names no runtime entry Node can load (a types-only or browser-only publish, or an unexported subpath). Three consumers pick their remedy line with a two-way branch written when the type had exactly two members, so the third kind fell into the else leg and printed the declare remedy:

sitesymbolwhat the third kind rendered
packages/cli/src/commands/serve.tsformatOrganizationsInstallRemedy"add the package to THIS APP — declare it in the app's package.json and install"
packages/verify/src/harness.tsthe remedy in bootStack's multi-tenant fatal"Install/link it in THIS APP — and DECLARE it in that app's package.json"
packages/qa/dogfood/test/enterprise-organizations.tsprobeOrganizations's refusal / skip reason"declare it in the app's own package.json and install it"

Each of those three also interpolates the importer's own message, which words the case correctly ("the package publishes no entry that Node can load … the remedy lives in the package"). So the bullet an operator reads first contradicted the diagnosis printed underneath it, and prescribed two actions that were already done and could not have helped. That is the confidently-wrong-verdict class #14041 removed one layer down, reintroduced by its callers.

The shape

Each branch now asks "is the declaration the problem?" rather than testing one kind:

  • undeclared ⇒ declare remedy — unchanged
  • declared-unresolvable ⇒ install remedy — unchanged
  • declared-no-loadable-entryprescribes nothing. It names the two things that are not the problem and defers to the importer's message, which every one of the three sites already prints.

No fourth remedy sentence is minted. The importer words the package-shape case once, in the place that measured it; a local re-wording in three consumers would be three copies to drift. The deferral follows an idiom already in serve.tsformatOrganizationsMountFatal's "its message is the authority on the remedy; this CLI has no further detail to add" — and the same reasoning formatI18nLoadDiagnostic's docblock gives for interpolating only the kind TOKEN.

An error carrying no kind never came from the host importer at all and keeps the declare-and-install arm it has always had. Narrowing that one is a different question about a different fact, and is not this branch's.

⛔ The seam with #14278 / PR #15047

packages/types/src/node.ts is not touched — verified on the diff:

git diff --name-only origin/main...HEAD | grep -E '^packages/types/' # no match

The strings this card defers to (/publishes no entry/, /INSTALL problem/) are pinned by #14278's tests in packages/types/src/node.test.ts. This PR changes the three callers' remedy bullets so they stop contradicting that message.

Proof

Red-first, per formatter, naming which arm fired. Tests landed in 063d976b7a, implementation in 78a0a7587f. Against the tests-only tree, all three sites reproduced the defect with a fixture carrying the third kind:

serve.ts expected " • this app DECLARES @objectstack/organizations (dependencies: "^1.2.3") and it IS"
received " • add @objectstack/organizations (the enterprise multi-org runtime) to THIS APP"
harness.ts received "… Install/link it in THIS APP (/tmp/os-verify-org-host-no-entry-AFcXGm) — and DECLARE it
in that app's package.json …" followed, in the same string, by the importer's
"the package publishes no entry that Node can load"
dogfood received "To enable it, declare @objectstack/organizations in /tmp/os-dogfood-org-no-entry-gd5Vpw's
own package.json and install it …" same contradiction, same string

The verify and dogfood fixtures are real host roots on disk — a real node_modules, a real installed package whose manifest publishes exports: { ".": { types: "./index.d.ts" } } and nothing else, nothing mocked. Neither condition set resolves it (ESM_IMPORT_CONDITIONS / CJS_REQUIRE_CONDITIONS), so the importer classifies it declared-no-loadable-entry for real rather than by injection.

Reverse verification — the three formatters restored to their pre-implementation bytes, same suites re-run, then restored:

mutation ON DISK (blob hash + marker counts)
packages/cli/src/commands/serve.ts now=bd070ffe == pre? YES != head? YES kind-marker-count=1
packages/verify/src/harness.ts now=479de177 == pre? YES != head? YES kind-marker-count=0
packages/qa/dogfood/test/enterprise-organizations.ts now=cc6284e9 == pre? YES != head? YES kind-marker-count=0
× site 2b — the `declared-no-loadable-entry` remedy DEFERS … Tests 1 failed | 11 passed (12)
× DEFERS to the importer for a declared, installed package … Tests 1 failed | 6 passed (7)
× DEFERS to the importer when the package is declared, installed …
× THROWS with that same deferral when the run declares the package Tests 2 failed | 7 passed (9)

Restore proven by blob-hash equality against the HEAD blob for all three files, with git status --porcelain empty and git diff HEAD clean — never by an editor exit code. The mutation ran under trap … EXIT INT TERM restoring absolute paths, and the restore leg uses git checkout HEAD -- PATH rather than a bare git checkout -- PATH, since the mutation leg wrote the index too. No rebuild is involved on either leg: all three suites import their subject by relative source path (./serve.js, ./harness, ./enterprise-organizations.js), not through a package exports map into dist.

Controls, byte-unchanged, in the same runs. The undeclared and declared-unresolvable arms are pinned at all three sites and pass identically either side of the change — they are green on the mutated tree above and on the implemented tree. Two of those controls are new here: neither the verify suite nor the dogfood suite had a declared-unresolvable fixture, so this PR adds one to each (declared, not installed) purely as a control.

Green, at dd611c3856:

pnpm --filter @objectstack/cli exec vitest run src/commands/serve-organizations-message-spelling.test.ts Tests 12 passed (12)
pnpm --filter @objectstack/verify exec vitest run src/harness.host-resolution.test.ts Tests 7 passed (7)
pnpm --filter @objectstack/dogfood exec vitest run test/enterprise-organizations.test.ts Tests 9 passed (9)
pnpm --filter @objectstack/cli typecheck && pnpm --filter @objectstack/verify typecheck
&& pnpm --filter @objectstack/dogfood typecheck os-verify-lock: VERDICT command-exit 0
pnpm lint (eslint . --no-inline-config, whole repo, not narrowed) os-verify-lock: VERDICT command-exit 0

Gates

The union was derived at the final HEAD dd611c3856 on a clean tree with node scripts/pm/dispatch-gates.mjs --repo objectstack-ai/objectstack --commands — 50 families (39 by path + 7 by change kind + 6 declared whole-tree, 2 reached both ways) — never from a hand-written path list. All 50 were run locally; each gate's exit code was captured before any pipe. 49 green first time.

One was a real red, and it was mine.pnpm check:type-check-debt reported:

check-type-check-coverage --re-measure: 1 ledger entr(ies) drifted upward
• @objectstack/verify: TEST_DEBT records 3 raw tsc error(s), `tsc --noEmit` now reports 5 (+2).

harness.host-resolution.test.ts imports ./harness without the .js extension, so under NodeNext the specifier does not resolve and every symbol it names is any — which makes a .then(onFulfilled, onRejected) callback parameter implicitly any. My two new cases used that shape and added 2 × TS7006. TEST_DEBT is shrink-only, so the fix is the errors, not the ceiling: dd611c3856 rewrites both as try/catch (a const initialised from an any call is not a diagnostic; only a parameter is). ⛔ The one-line .js fix that would graduate the entry is deliberately not taken here — different card, filed as #15145.

Re-run at the final HEAD after that fix, each exit captured per gate:

RATCHET check:nul-bytes exit=0 RATCHET check:engine-double-contract exit=0
RATCHET check:test-source-alias exit=0 RATCHET check:query-options-erasure exit=0
RATCHET check:cross-package-test-inputs exit=0 RATCHET check:type-check-coverage exit=0
RATCHET check:where-matcher exit=0 RATCHET check:type-check-debt exit=0
check-type-check-coverage --re-measure: OK — 17 ledger entr(ies) re-measured in 166.5s,
217 raw tsc error(s) total, none above its recorded number.

Re-deriving the union at dd611c3856 returns the same 50 families — the ratchet fix implicated nothing new.

Typecheck coverage — measured, not assumed

Measured with tsc --noEmit --listFiles, because "typecheck is clean" says nothing about a file the program never read:

edited filein a tsc program?
packages/cli/src/commands/serve.tspackages/cli/tsconfig.json
packages/cli/src/commands/serve-organizations-message-spelling.test.tspackages/cli/tsconfig.json — its include: ['src'] reaches this file, since the pin lives under src/commands/, not under test/. It is 0 files in tsconfig.test.json, whose project is test/.
packages/qa/dogfood/test/enterprise-organizations.ts and its testpackages/qa/dogfood/tsconfig.json (include: ['test/**/*'])
packages/verify/src/harness.tspackages/verify/tsconfig.json
packages/verify/src/harness.host-resolution.test.tsnot in that program — exclude lists **/*.test.ts, and the package has no check:test-typecheck wiring. Its types are read only by check:type-check-debt --re-measure, which is what caught the +2 above. Filed as #15145.

So: pnpm --filter @objectstack/verify typecheck being green is not a statement about the test file this PR edits. The statement that covers it is the ratchet line quoted above.

Changeset

Measured from each of the three packages' own files field, because the answers differ:

  • @objectstack/clifiles: ["dist", …], publishConfig.access: public ⇒ published, src/commands/serve.ts ships ⇒ in the changeset.
  • @objectstack/verifyfiles: ["dist", …], published ⇒ src/harness.ts ships ⇒ in the changeset.
  • @objectstack/dogfoodprivate: true, no files field at all ⇒ publishes nothing ⇒ not in the changeset, and cannot be.

Patch, not minor: operator-facing wording on a failure path, no API change. Nothing here removes or renames anything an author can write, so no ADR-0087 disposition marker is owed.

Out of scope, filed

Fences

packages/types/src/node.ts, packages/rest/**, packages/spec/** and content/docs/releases/** are absent from the diff (asserted above). serve.ts:133's "keep the two in sync" comment and every apps/objectos / apps/cloud comment are untouched — 0 diff hits for that string. packages/verify/src/harness.ts moved one remedy branch and nothing else in the ADR-0105 bootStack harness.

Draft on purpose: ⛔ not flipped ready, ⛔ auto-merge not armed — the PM does that.

🤖 Generated with Claude Code

https://claude.ai/code/session_016yfqQh2dBgPAymYd7xipza


Generated by Claude Code

…three sites
Red-first for #14270. #14041 added a third `HostImportFailureKind`,
`declared-no-loadable-entry` — the app declares the package, the install
delivered it, and the package's own `exports` names no runtime entry Node
can load. Three consumers pick their remedy with a two-way branch written
when only two kinds existed, so the third falls into the else leg and
renders the DECLARE remedy at an operator who has already declared AND
installed it.
Each site gets a fixture carrying that kind and an assertion naming which
arm fired. The verify and dogfood fixtures are real host roots on disk
whose installed package publishes a `types`-only `exports` map; the CLI
formatter takes the kind directly. Controls for `undeclared` and
`declared-unresolvable` are untouched.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016yfqQh2dBgPAymYd7xipza
…not on one kind
`declared-no-loadable-entry` (#14041) means the app declares the package,
the install delivered it, and the package's own `exports` names no runtime
entry Node can load. All three consumers picked their remedy with a
two-way branch written when only two kinds existed, so that kind fell into
the else leg and printed "declare it in the app's package.json and
install" — at an operator who has already done both, and directly
contradicting the importer's own message printed beside it.
Each branch now asks "is the declaration the problem?": undeclared keeps
the declare remedy, `declared-unresolvable` keeps the install remedy, and
the third kind prescribes nothing and defers to the importer's message,
which every one of the three sites already interpolates. No fourth remedy
sentence is minted — the importer words the package-shape case once, and a
local re-wording would be a second copy to drift.
`packages/types` is untouched: the strings deferred to are #14278's.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016yfqQh2dBgPAymYd7xipza
…tion the app already has
Patch for @objectstack/cli and @objectstack/verify — the two published
packages whose `files` field ships the changed code. @objectstack/dogfood
is `private: true` with no `files`, so it publishes nothing and takes no
changeset entry.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016yfqQh2dBgPAymYd7xipza
…EST_DEBT
`harness.host-resolution.test.ts` imports `./harness` without the `.js`
extension, so under NodeNext the specifier does not resolve and every
symbol it names is `any` — which makes a `.then` callback PARAMETER
implicitly any. The two new cases used `.then(onFulfilled, onRejected)`
and added 2 x TS7006, taking the package's TEST_DEBT entry from its
recorded 3 to 5. That ledger is shrink-only, so the fix is the errors,
not the ceiling.
try/catch instead: a `const` initialised from an `any` call is not a
diagnostic, only a parameter is. `check:type-check-debt --re-measure`
now reports 17 entries re-measured, 217 raw errors, none above its
recorded number.
⛔ Deliberately NOT the one-line `.js` fix that graduates the entry — that
is a different card's, filed separately.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016yfqQh2dBgPAymYd7xipza
@github-actions

Copy link
Copy Markdown
Contributor

📓 Docs Drift Check

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

17 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 4283b72a15f9c5ced0755a1b9c8aa53e5f8e16fa.

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

What this run could not see
  • 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 — 23 page(s) merely mention a changed package (the pre-#9192 predicate, kept for the deliberately-wide backstop): node scripts/docs-audit/affected-docs.mjs --json 4283b72a15f9c5ced0755a1b9c8aa53e5f8e16fapackageMentionDocs.

Which tree this was computed on

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

⚠️ 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 4283b72a15f9c5ced0755a1b9c8aa53e5f8e16fa → 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 4, 2026
@os-trump
os-trump marked this pull request as ready for review September 4, 2026 04:20
@os-trump
os-trump added this pull request to the merge queueSep 4, 2026
Merged via the queue into main with commit 5cf1c88Sep 4, 2026
36 checks passed
@os-trump
os-trump deleted the claude/issue-14270-remedy-formatter-third-kind branch September 4, 2026 04:48
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

2 participants

@os-trump@claude