Skip to content

refactor(cli): single-source the tenancy posture hint table that os serve and os doctor both print (#12492) - #12532

Merged
os-litant merged 2 commits into
mainfrom
claude/issue-12492-shared-tenancy-hints
Aug 26, 2026
Merged

refactor(cli): single-source the tenancy posture hint table that os serve and os doctor both print (#12492)#12532
os-litant merged 2 commits into
mainfrom
claude/issue-12492-shared-tenancy-hints

Conversation

@os-litant

Copy link
Copy Markdown
Collaborator

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.ts and packages/cli/src/commands/doctor.ts
each declared their own TENANCY_POSTURE_FIX_HINTS. Measured on origin/main
5c41cd675, the two tables were byte-identical modulo the expression
spelling the package name — sha256 97497ea8… on both:

$ sed -n '4273,4279p' serve.ts | sed 's/Serve\.ORGANIZATIONS_RUNTIME_PKG/ORGANIZATIONS_RUNTIME_PKG/' | sha256sum
97497ea8aa24f4e0137a16940b353b9c8c4311dc2db6e1f5769351e86a6a7342
$ sed -n '579,585p' doctor.ts | sha256sum
97497ea8aa24f4e0137a16940b353b9c8c4311dc2db6e1f5769351e86a6a7342

isolated at least carried a package literal the spec-owned roster could be
pinned against (#12464, PR #12496). single and group were 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 isolated would have
closed 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 — a
CLI-internal module, notpackages/spec. A new published export from the
spec would widen public surface to export operator prose to every consumer of the
protocol contract.

The named falsifiable premise — no reader outside packages/cli consumes any of
the three hint strings
— was measured, not assumed, repo-wide over each of
the three strings with git grep -F, excluding generated CHANGELOG.md:

hit outside packages/cliwhat it actually is
packages/plugins/plugin-auth/src/auth-plugin.ts:536a code comment that happens to phrase group similarly — "…group is enforced by the open engine and" — not the hint string
docs/adr/0105-…md:14ADR prose, a different sentence: "one shared database, organization as the membership structures"
.changeset/doctor-organizations-hint-pinned.mdPR #12496's own changeset naming the const

Neither table was ever exported, so no symbol-level reader outside the module
was even possible. The premise holds; the CLI-internal home stands. (Scope
note: measured in this repo. objectui and cloud are not checked out in this
container, 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:

BASE 5c41cd675 render sha256: f72d317027441e1f33d9934a49f933607ae4dee71fcc10e0ff458c1870f8b371
this branch render sha256: f72d317027441e1f33d9934a49f933607ae4dee71fcc10e0ff458c1870f8b371
diff -u before.txt after.txt -> empty

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 isolated sentence's prose, package
name untouched — reddens a pin at both commands:

ON DISK: injected text hits=1 (want 1) · original text hits=0 (want 0, was 1)
── ablated — vitest exit 1 Tests 3 failed | 16 passed (19)
× doctor …#12464 > leg (i) — the `isolated` fix-list bullet names it, with the spacing intact
× doctor …#12464 CONTROL > a lost space around the interpolation fails the rendered comparison
× serve …#12151 > site 6 — the `isolated` fix-list bullet names it, through the real gate
doctor pin failures: 2 serve pin failures: 1

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.ts survive — including
the 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 in
test/serve-capability-vocabulary.test.ts and is untouched.

Added:

  • doctor leg (iii) and serve site 7 — every posture bullet, single and
    group included, renders the shared table's entry verbatim. This is the half
    no 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.
  • serve site 8 — see below.

Each new leg has a matching CONTROL that shows the comparison can say no,
anchored on group and 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's ORGANIZATIONS_RUNTIME_PKG is deleted, as its own docblock's
deletion condition required. But Serve.ORGANIZATIONS_RUNTIME_PKGstays a
string 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.ts refused it, by name:

AssertionError: the sweep no longer sees the @objectstack/organizations load:
expected [ Array(3) ] to include '@objectstack/organizations'
AssertionError: no load site found for @objectstack/organizations: expected 0 to be greater than 0

That sweep proves every app-declarable dynamic import() in serve.ts is
host-anchored. It resolves the organizations load site
(const organizationsPkg = Serve.ORGANIZATIONS_RUNTIME_PKG;) one hop to the
static 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 sibling
module — 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 very
file, "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:

  • serve site 8 asserts serve's literal and the shared module declare the same
    package;
  • each copy is independently pinned as a key of PLATFORM_PLUGIN_WIRED_RUNTIMES
    (serve's via test/serve-capability-vocabulary.test.ts, the shared one via
    doctor'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 clean
tree (git status --porcelain empty).

whatresult
pnpm --filter '@objectstack/cli^...' buildVERDICT command-exit 0
affected CLI suites (8 files, incl. both spelling pins, the roster pin and the host-resolution sweep)Test Files 8 passed (8) · Tests 84 passed (84)
sharing ablation (mutate → both commands red → restore → green)baseline exit=0 · ablated exit=1 · restored exit=0
pnpm --filter @objectstack/cli typecheckexit 0, and tsc --listFiles confirms all 5 touched files are in the program (1 hit each) — not a green over source nothing read
pnpm lint (repo-wide, eslint . --no-inline-config)VERDICT command-exit 0 — the full farm, not a narrowing
15 gate families from scripts/pm/dispatch-gates.mjs --repo objectstack-ai/objectstackall exit 0

Gate 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: 20
package(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-measure needs the whole workspace closure built and
is left to CI, which builds it exactly as lint.yml does.


Generated by Claude Code

@github-actionsgithub-actionsBot added size/m documentation Improvements or additions to documentation tests tooling labels Aug 26, 2026
@github-actions

Copy link
Copy Markdown
Contributor

📓 Docs Drift Check

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

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

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

What this run could not see
  • 4 name(s) were too generic to anchor anything (single lowercase words)
  • a page that states a rule by its inputs shares no identifier with the emitter that implements the rule, so an emitter-only diff cannot list it — not on this run and not on any run. Measured on fix(driver-sql): emit varchar(maxLength) for a text field a declared index keys on #11430: content/docs/protocol/objectql/types.mdx documents the text-family column mapping by the ObjectQL type names it maps FROM (text / textarea / html) while the diff changed createColumn; it went unlisted, and it was the page that diff falsified, in four places. No shared token exists to detect this on, so a rule your change carries has to be re-read by hand in the pages that restate it.

Coarse fallback — 23 page(s) merely mention a changed package (the pre-#9192 predicate, kept for the deliberately-wide backstop): node scripts/docs-audit/affected-docs.mjs --json 33c5fd3d2360ffc2a7ff79df869bf65027197889packageMentionDocs.

Which tree this was computed on

This run read content/docs from 056c1c17e762241776f761d6d9ae8c5ee33b6ec0 — the merge of head bbefdb008bcc8e0fed2acb605473b371f948b166 into base 33c5fd3d2360ffc2a7ff79df869bf65027197889, 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 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

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

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

Labels

documentationImprovements or additions to documentationsize/mteststooling

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants

@os-litant@claude