Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
42 changes: 42 additions & 0 deletions .changeset/shared-tenancy-posture-hints.md
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
---
"@objectstack/cli": patch
---

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

`serve.ts` and `doctor.ts` each declared their own `TENANCY_POSTURE_FIX_HINTS`,
and the two tables were **byte-identical** — sha256 `97497ea8…` on both, modulo
the expression spelling the package name — under no cross-check of any kind.

The `isolated` entry at least carried a package literal the spec-owned
`PLATFORM_PLUGIN_WIRED_RUNTIMES` 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. A reword of one
command's copy left the other describing the same posture differently to the
same operator, with every gate green.

Both tables now come from one CLI-internal module,
`packages/cli/src/utils/tenancy-posture-hints.ts`, which also holds the single
`ORGANIZATIONS_RUNTIME_PKG` declaration. `Serve.ORGANIZATIONS_RUNTIME_PKG`
becomes a **re-export** of it rather than a second declaration, keeping the
stable handle `serve`'s boot path and the roster pin already address; the
module-local const #12464 added to `doctor.ts` is **deleted**, which is the
deletion condition that const's own docblock recorded against this card. The
literal is now declared twice (the roster key and this module) instead of three
times.

Each command keeps its **own bullet assembly** — doctor renders
`• OS_TENANCY_POSTURE=<p> — <hint>` inside a health-check fix list, serve renders
`• set OS_TENANCY_POSTURE=<p> — <hint>` inside a FATAL refusal, at different
indents. Only the table was ever duplicated.

The two sibling spelling pins are **retargeted, not dropped** — including the
roster leg, which is the load-bearing one (a rename of the package value leaves
the rendered leg green; only the roster leg catches it). Each gains one new leg
asserting that every posture bullet, `single` and `group` included, renders the
**shared** table's entry verbatim — so a command that re-grows a local copy goes
red instead of drifting in silence.

**No behaviour change.** The rendered text is byte-identical before and after
for all three postures at both commands, verified by capturing both commands'
full rendered fix lists on `origin/main` and on this branch and diffing them.
Original file line numberDiff line numberDiff line change
Expand Up@@ -16,25 +16,50 @@
* declaration, and nothing read this hint table's text.
*
* The defect being closed is that SILENT DRIFT, not the duplication as such.
* The literal is still declared three times (the roster key, serve's static,
* doctor's const) and this file does not change that — see the const's own
* docblock for why the roster cannot supply the name, and for the deletion
* condition that ends the duplication properly.
*
* ── Two legs, and the second is the point ────────────────────────────────
* ── Retargeted by #12492, not rewritten ──────────────────────────────────
*
* (i) RENDERED — the `isolated` bullet is rendered through the real gate
* and compared, whitespace included, against text built from the
* declaration.
* (ii) ROSTER — that declaration IS a key of the spec-owned
* `PLATFORM_PLUGIN_WIRED_RUNTIMES`.
* That deletion condition has since been met. `doctor.ts` no longer declares
* `ORGANIZATIONS_RUNTIME_PKG` or its own hint table: both moved to
* `../utils/tenancy-posture-hints.ts`, which `os serve` reads too. These pins
* moved with the declaration — ⛔ none of them was dropped, because what they
* measure did not change: what `os doctor` RENDERS. Leg (ii) especially, which
* is the load-bearing one (see below).
*
* ⚠️ The literal is still declared three times, not two: the roster key, the
* shared module this file now reads, and `Serve.ORGANIZATIONS_RUNTIME_PKG`,
* which must stay a string LITERAL in `serve.ts` or the host-anchoring sweep in
* `serve-cluster-host-resolution.test.ts` can no longer resolve which package
* that command's `import()` names. What changed is that no copy can drift in
* silence any more: the serve↔shared pair is pinned equal by site 8 of
* `serve-organizations-message-spelling.test.ts`, and each copy is separately
* pinned as a roster key. Ending the duplication needs that sweep's resolver to
* follow one more hop — a file this card does not own.
*
* ── Three legs, and the second is the point ──────────────────────────────
*
* (i) RENDERED — the `isolated` bullet is rendered through the real gate
* and compared, whitespace included, against text built from the
* declaration.
* (ii) ROSTER — that declaration IS a key of the spec-owned
* `PLATFORM_PLUGIN_WIRED_RUNTIMES`.
* (iii) SHARED TABLE (#12492) — every posture bullet, `single` and `group`
* included, renders the shared table's entry verbatim.
*
* ⭐ (ii) is the entire difference between a CHECKED duplicate and a third
* SILENT copy. Leg (i) on its own pins doctor against itself: rename the
* roster key and the hint and the expectation move together, so (i) stays
* green forever while `os doctor` names a package that no longer exists.
* (ii) is what makes that rename loud. Neither leg is optional.
*
* (iii) covers the half neither of the other two can reach. `single` and
* `group` carry no package literal, so no roster leg is possible for them and
* nothing ever watched them — that is the defect #12492 filed. Leg (iii) does
* not check the PROSE (a reword moves the shared table and this expectation
* together, and leg (i)'s hard-coded text is what reddens then); it checks that
* doctor renders THE SHARED TABLE. Re-grow a module-local copy in `doctor.ts` —
* exactly the state this card found — and it goes red on every posture at once.
*
* ── Why it reads the RENDERED text, not the source ───────────────────────
*
* "No bare literal outside the declaration" is the tempting stronger form and
Expand All@@ -51,9 +76,16 @@ import fs from 'node:fs';
import os from 'node:os';
import path from 'node:path';
import { PLATFORM_PLUGIN_WIRED_RUNTIMES } from '@objectstack/spec/kernel';
import { TENANCY_POSTURES } from '@objectstack/spec/security';

// The declaration and the table both live here since #12492 — `os serve` reads
// the same module, which is what makes leg (iii) below a reading of the SHARING
// rather than of one command talking to itself.
import {
ORGANIZATIONS_RUNTIME_PKG,
TENANCY_POSTURE_FIX_HINTS,
} from '../utils/tenancy-posture-hints.js';
import {
resolveTenancyPostureOrFinding,
readDotenvFiles,
type DotenvReading,
Expand DownExpand Up@@ -108,17 +140,20 @@ afterEach(() => {
}
});

/** The `isolated` fix-list bullet, rendered through the real gate. */
const renderIsolatedBullet = (): string => {
/** One posture's fix-list bullet, rendered through the real gate. */
const renderPostureBullet = (posture: string): string => {
process.env.OS_TENANCY_POSTURE = 'not-a-posture';
const reading = resolveTenancyPostureOrFinding(shellOnly);
expect(reading.ok, 'the gate accepted a value that is not a posture').toBe(false);
if (reading.ok) throw new Error('unreachable — guarded above');
const bullet = lines(reading.result.fix ?? '').find((l) => l.includes('OS_TENANCY_POSTURE=isolated'));
expect(bullet, "the fix list no longer offers an `isolated` bullet at all").toBeDefined();
const bullet = lines(reading.result.fix ?? '').find((l) => l.includes(`OS_TENANCY_POSTURE=${posture}`));
expect(bullet, `the fix list no longer offers a '${posture}' bullet at all`).toBeDefined();
return bullet as string;
};

/** The `isolated` fix-list bullet, rendered through the real gate. */
const renderIsolatedBullet = (): string => renderPostureBullet('isolated');

describe('doctor — the posture description an operator reads names the declaration (#12464)', () => {
// LEG (i). Rendered through `resolveTenancyPostureOrFinding` rather than by
// reading the hint table: the bullet's assembly (indent, `• OS_TENANCY_POSTURE=`,
Expand DownExpand Up@@ -148,6 +183,28 @@ describe('doctor — the posture description an operator reads names the declara
expect(row.edition, `edition drift for the runtime doctor names ('${PKG}')`).toBe('enterprise');
});

// LEG (iii) — the half legs (i) and (ii) cannot reach (#12492). `single` and
// `group` carry no package literal, so no roster leg is possible for them;
// before this card nothing anywhere read their text at either command, and a
// reword of one command's copy drifted from the other in total silence. What
// closes that is not a pin on the PROSE — it is this: the bullets an operator
// reads here are assembled from the SHARED table, the same one `os serve`
// renders. A module-local hint table re-grown in `doctor.ts` reddens this.
it('leg (iii) — every posture bullet renders the SHARED hint table verbatim, `single` and `group` included', () => {
for (const posture of TENANCY_POSTURES) {
const hint = TENANCY_POSTURE_FIX_HINTS[posture];
expect(renderPostureBullet(posture)).toBe(
` • OS_TENANCY_POSTURE=${posture}${hint ? ` — ${hint}` : ''}`,
);
}
// …and the sweep above actually swept. A posture vocabulary that went empty
// would satisfy every assertion inside the loop without reading anything —
// the two entries this card is ABOUT are named explicitly for that reason.
expect(TENANCY_POSTURES).toContain('single');
expect(TENANCY_POSTURES).toContain('group');
expect(TENANCY_POSTURES).toContain('isolated');
});

it('no posture bullet an operator reads names any OTHER scoped package', () => {
// The sweep the excluded source-scan form was reaching for, done over the
// rendering instead — where comments cannot reach and no exclusion list is
Expand DownExpand Up@@ -185,6 +242,21 @@ describe('#12464 CONTROL — these pins can say no', () => {
expect(renderIsolatedBullet()).toBe(expected);
});

it('the shared-table comparison rejects a bullet whose hint was reworded (#12492)', () => {
// If leg (iii) could not tell a reworded hint from the shared one it would
// be decorative. Anchored on `group` — one of the two entries that had
// nothing watching them at all before this card, and deliberately NOT a
// substring game: 'closed engine' is a different claim, not a truncation.
const real = ` • OS_TENANCY_POSTURE=group — ${TENANCY_POSTURE_FIX_HINTS.group}`;
expect(
' • OS_TENANCY_POSTURE=group — organization wall enforced by the closed engine, one shared database',
).not.toBe(real);
expect(' • OS_TENANCY_POSTURE=group').not.toBe(real);
// …and says yes to the real thing, so the two `not.toBe`s are a reading
// rather than a pair of vacuous truths.
expect(renderPostureBullet('group')).toBe(real);
});

it('the roster key check rejects a name the roster does not declare', () => {
// Anchored on a term that is NOT a substring of the one under test: a
// membership assertion is not a reading until the same instrument answers no.
Expand Down
74 changes: 15 additions & 59 deletions packages/cli/src/commands/doctor.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -15,6 +15,7 @@ import { checkSpecVersionGap } from '../utils/spec-version.js';
// them apart. That classification lives in one place, with the measurements
// behind it written down there.
import { loadOptionalPackage } from '../utils/optional-package.js';
import { TENANCY_POSTURE_FIX_HINTS } from '../utils/tenancy-posture-hints.js';
import { validateWidgetBindings } from '@objectstack/lint';
import {
resolveTenancyPosture,
Expand DownExpand Up@@ -523,66 +524,21 @@ export function environmentSourcesCheck(
// ─── Tenancy Posture ────────────────────────────────────────────────

/**
* The `plugins[]`-wired multi-org runtime this command NAMES in its posture
* advice, spelled once so the sentence below cannot drift in silence (#12464).
*
* ⚠️ This is the THIRD declaration of this literal, and that is an accepted
* cost rather than an oversight. The roster KEY in `PLATFORM_PLUGIN_WIRED_RUNTIMES`
* is one; `Serve.ORGANIZATIONS_RUNTIME_PKG` (`serve.ts`, #11614) is another.
* What this const buys is NOT fewer copies — it is that this copy can no longer
* drift unnoticed: `doctor-organizations-message-spelling.test.ts` pins the
* RENDERED bullet against this value and pins this value as a roster key, so a
* roster rename turns a test red instead of leaving `os doctor` printing a
* package name that boot no longer resolves with every gate green. Three
* declarations is structurally worse than two; a duplicate that can drift
* silently and one that cannot are different things.
*
* ── Why this does not read the name from the spec roster ─────────────────
*
* Because the roster cannot supply it. `PLATFORM_PLUGIN_WIRED_RUNTIMES` is
* keyed BY package name, and its row type `PlatformPluginWiredRuntime` carries
* no `package` field — deliberately: *"here the package name is the KEY, so it
* cannot be `null` and cannot drift from a duplicate field"*. Its own header
* settles the rest: *"What this roster deliberately is NOT: a resolution
* registry … the rows record that fact as prose provenance, they do not encode
* it as a lookup."* Both rows are `edition: 'enterprise'`, so nothing
* machine-readable selects this one. The roster VALIDATES a name you already
* hold; it does not hand you one — which is why the pin reads it as a KEY
* CHECK, the only first-class read it actually offers.
*
* ⛔ Do NOT replace this with an import of `Serve.ORGANIZATIONS_RUNTIME_PKG`: a
* diagnostic command taking a dependency on a `serve` command's export, in
* order to spell a package name, is a worse coupling than the duplication it
* removes.
*
* ── Deletion condition ───────────────────────────────────────────────────
*
* This const goes away the day a shared tenancy-hint table lands (tracked at
* #12492). The whole `TENANCY_POSTURE_FIX_HINTS` table below is duplicated
* between here and `serve.ts` — `single` and `group` byte-identical too, and
* those two touch no roster, so nothing could ever notice them drift. One
* shared table single-sources all three sentences AND this package name at
* once. This is a step toward that, not the end state.
*
* Exported for the same reason `serve` exposes its own as a static: one
* declaration, two readers — the sentence and the pin. A pin that read a copy
* of this value instead of this value would be pinning the test against itself.
* The posture prose below comes from `../utils/tenancy-posture-hints.ts`, which
* `os serve` reads too (#12492).
*
* #12464 added a module-local `ORGANIZATIONS_RUNTIME_PKG` here and a
* `TENANCY_POSTURE_FIX_HINTS` table that repeated `serve`'s, byte for byte. That
* const's docblock named THIS card as its deletion condition, so it is gone: the
* declaration moved to the shared module and doctor reads it from there. ⛔ Not
* from `serve.ts` — a diagnostic command depending on a `serve` command's export
* in order to spell a package name is the coupling that docblock ruled out, and
* a neutral utility both commands sit above is not that.
*
* Doctor keeps its OWN bullet assembly (indent, `• OS_TENANCY_POSTURE=`, the
* ` — ` separator): serve renders the same hints at a different indent inside a
* FATAL refusal, and only the TABLE was ever duplicated.
*/
export const ORGANIZATIONS_RUNTIME_PKG = '@objectstack/organizations';

/**
* One-line descriptions of the accepted postures, keyed by the vocabulary
* `@objectstack/spec/security` owns. A posture declared there but not described
* here is still listed by the fix list (bare, without prose) rather than
* silently dropped — the advice can go terse, never stale.
*/
const TENANCY_POSTURE_FIX_HINTS: Readonly<Record<string, string>> = {
single: 'one organization, no organization wall — the default',
group: 'organization wall enforced by the open engine, one shared database',
isolated:
`organization wall + the enterprise ${ORGANIZATIONS_RUNTIME_PKG} runtime `
+ "(the legacy spelling 'multi' is accepted and normalizes to this)",
};

/**
* What doctor's tenancy-posture read decided (#5382).
Expand Down
Loading
Loading