Uh oh!
There was an error while loading. Please reload this page.
refactor(cli): single-source the tenancy posture hint table that os serve and os doctor both print (#12492) - #12532
Conversation
📓 Docs Drift CheckThis PR changes 1 package(s): 20 hand-written doc(s) name something this change touched — list omitted above 15 rows. Re-derive on the tree named below: ⛔ 4 release-owned page(s) also affected — read-only, see AGENTS.md Documentation Guardrails. What this run could not see
Coarse fallback — 23 page(s) merely mention a changed package (the pre-#9192 predicate, kept for the deliberately-wide backstop): Which tree this was computed onThis run read A worktree cut from an older # while this PR is open — GitHub drops the merge commit once it closes
git fetch origin 056c1c17e762241776f761d6d9ae8c5ee33b6ec0 && git checkout 056c1c17e762241776f761d6d9ae8c5ee33b6ec0
# afterwards, rebuild it from the two parents, which stay fetchable
git fetch origin 33c5fd3d2360ffc2a7ff79df869bf65027197889 bbefdb008bcc8e0fed2acb605473b371f948b166 && git checkout -B drift-repro 33c5fd3d2360ffc2a7ff79df869bf65027197889 && git merge --no-ff bbefdb008bcc8e0fed2acb605473b371f948b166
node scripts/docs-audit/affected-docs.mjs --json 33c5fd3d2360ffc2a7ff79df869bf65027197889
|
Uh oh!
There was an error while loading. Please reload this page.
Part of #12492
The hint table is fully single-sourced and proven shared. One thing the card also
hoped for — single-sourcing the package name at the same time — is left open
deliberately, because reaching it requires editing a file another card is holding
right now. That fork is written up under "What is left, and why" below; it is the
PM's call, not mine, so this PR does not close the card.
What was duplicated
packages/cli/src/commands/serve.tsandpackages/cli/src/commands/doctor.tseach declared their own
TENANCY_POSTURE_FIX_HINTS. Measured onorigin/main5c41cd675, the two tables were byte-identical modulo the expressionspelling the package name — sha256
97497ea8…on both:isolatedat least carried a package literal the spec-owned roster could bepinned against (#12464, PR #12496).
singleandgroupwere the worse half:they touch no roster, so nothing anywhere could ever have noticed them drift
apart. All three move together here — single-sourcing only
isolatedwould haveclosed the half that was already covered.
The home: CLI-internal, and the premise behind it was measured
The table now lives in
packages/cli/src/utils/tenancy-posture-hints.ts— aCLI-internal module, not
packages/spec. A new published export from thespec would widen public surface to export operator prose to every consumer of the
protocol contract.
The named falsifiable premise — no reader outside
packages/cliconsumes any ofthe three hint strings — was measured, not assumed, repo-wide over each of
the three strings with
git grep -F, excluding generatedCHANGELOG.md:packages/clipackages/plugins/plugin-auth/src/auth-plugin.ts:536groupsimilarly — "…groupis enforced by the open engine and" — not the hint stringdocs/adr/0105-…md:14.changeset/doctor-organizations-hint-pinned.mdNeither table was ever
exported, so no symbol-level reader outside the modulewas even possible. The premise holds; the CLI-internal home stands. (Scope
note: measured in this repo.
objectuiandcloudare not checked out in thiscontainer, and the strings are non-exported CLI operator prose, so a cross-repo
reader would have to be a textual copy — of which there are none here.)
No behaviour change — proven, not asserted
Both commands' full rendered posture advice was captured end to end through
the real gates (
resolveTenancyPostureOrFinding,resolveTenancyPostureOrRefusal),on the base tree and on this branch, and compared byte for byte. Identical:
That covers all three postures at both commands (6 bullets — doctor's
• OS_TENANCY_POSTURE={posture}fix list and serve's• set OS_TENANCY_POSTURE={posture}FATAL refusal). The base leg restored with
git checkout HEAD -- …under a trap,proven clean by an empty
git diff HEAD.Each command deliberately keeps its own bullet assembly — the two renderings
sit at different indents inside different messages. Only the table was ever
duplicated; hoisting the assembly too would have changed one of the two messages.
The sharing is real, not textual
One edit to the shared declaration — the
isolatedsentence's prose, packagename untouched — reddens a pin at both commands:
Baseline before and restored after: 84 passed (84) both times. Had the table
been shared in name only, one of the two counts would have been zero.
Pins: retargeted, none dropped, two added
Both legs of
doctor-organizations-message-spelling.test.tssurvive — includingthe roster leg, the load-bearing one that PR #12496's M1 mutation showed is
the only one that catches a rename of the package value. Same for
serve-organizations-message-spelling.test.ts, whose roster half lives intest/serve-capability-vocabulary.test.tsand is untouched.Added:
singleandgroupincluded, renders the shared table's entry verbatim. This is the halfno roster leg can reach: it goes red if either command re-grows a local copy,
which is exactly the state this card found. Each carries a vacuity guard
naming the three postures, so an emptied vocabulary cannot pass it silently.
Each new leg has a matching CONTROL that shows the comparison can say no,
anchored on
groupand on a term that is not a substring of the one under test.What is left, and why — a fork for the PM
doctor.ts'sORGANIZATIONS_RUNTIME_PKGis deleted, as its own docblock'sdeletion condition required. But
Serve.ORGANIZATIONS_RUNTIME_PKGstays astring literal, so the spelling is still declared twice inside
packages/cli.That is not an oversight — the first attempt made it a re-export of the shared
module, and
serve-cluster-host-resolution.test.tsrefused it, by name:That sweep proves every app-declarable dynamic
import()inserve.tsishost-anchored. It resolves the organizations load site
(
const organizationsPkg = Serve.ORGANIZATIONS_RUNTIME_PKG;) one hop to thestatic and then reads the literal in that file. Written as a re-export, the
specifier stops resolving and that load drops out of the swept population
rather than failing inside it — the silent-vacuity mode #11614 already paid for
once. The guard caught it, which is the guard working.
Single-sourcing the name properly is a small extension to that sweep's
resolveIdentifier()— one more hop, following an import alias into a siblingmodule — in a resolver whose own docblock records that going one hop further
"strictly WIDENS what the sweep judges; it can never excuse a load". That file
is outside this card's declared surface and is being rewritten right now by
#12162 (
claude/issue-12162-host-resolution-sweep-loud, +406/−57 to that veryfile, "the serve host-resolution sweep reports what it cannot resolve"). Two
agents editing it in the same round is the collision the surface rule exists to
prevent, so this PR leaves it alone and reports it instead.
Meanwhile the residual duplication is checked, not silent — the difference
the #12464 docblock drew between a duplicate that can drift unnoticed and one
that cannot:
package;
PLATFORM_PLUGIN_WIRED_RUNTIMES(serve's via
test/serve-capability-vocabulary.test.ts, the shared one viadoctor's leg (ii)).
Both constants' docblocks carry this reasoning at the site, so the next author is
told why the literal must stay before they "finish the job".
Verification
All of the below ran on
bbefdb008, this branch's final commit, against a cleantree (
git status --porcelainempty).pnpm --filter '@objectstack/cli^...' buildVERDICT command-exit 0Test Files 8 passed (8) · Tests 84 passed (84)baseline exit=0 · ablated exit=1 · restored exit=0pnpm --filter @objectstack/cli typechecktsc --listFilesconfirms all 5 touched files are in the program (1 hit each) — not a green over source nothing readpnpm lint(repo-wide,eslint . --no-inline-config)VERDICT command-exit 0— the full farm, not a narrowingscripts/pm/dispatch-gates.mjs --repo objectstack-ai/objectstackGate families run locally, each derived from the real change set (the script reads
its own merge-base diff):
check:nul-bytes— "OK (scanned 6916 text file(s) …no raw ASCII control bytes)" ·
check:cross-package-test-inputs— "OK: 20package(s) read outside themselves, all declared" ·
check:test-source-alias—"OK — 72 packages with tests scanned" ·
check:engine-double-contract·check:where-matcher·check:objectql-double-limit·check:query-options-erasure·check:route-envelope·check:comment-mask-adoption·check:type-check-coverage·check:ci-filter-parity·check:changeset-gate-self-tests·check:empty-changeset·check-changeset-no-major·check-adr-0087-registration.check:type-check-debt --re-measureneeds the whole workspace closure built andis left to CI, which builds it exactly as
lint.ymldoes.Generated by Claude Code