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
35 changes: 35 additions & 0 deletions .changeset/serve-organizations-pkg-roster-key.md
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
---
'@objectstack/cli': patch
---

refactor(cli): `serve` resolves `@objectstack/organizations` through one declaration the spec roster pins (#11614)

`PLATFORM_PLUGIN_WIRED_RUNTIMES` (`packages/spec`) is the provenance roster for
`plugins[]`-wired out-of-repo runtimes, keyed by npm package name — the single
machine-readable answer to "is this `@objectstack/*` package real, and where
does it ship from?". It exists because a **fabricated** `@objectstack/framework`
sat next to the real `@objectstack/organizations` in published docs for months,
indistinguishable by inspection (#10921).

`serve` is the only runtime that prints one of those names AT OPERATORS — the
install remedy, the fatal refusal when a walled tenancy posture cannot load the
multi-org runtime, and the degraded-boot warning. It spelled the package as a
bare literal at the resolution site, under no pin at all, so the roster and the
name `serve` actually resolves could diverge in silence: rename the roster key,
or mistype the literal, and every gate stays green while boot reaches for a
package that does not exist and the fatal message tells the operator to install
it.

The name is now declared once, as `Serve.ORGANIZATIONS_RUNTIME_PKG`, and both
resolution-path uses read it — `importFromHost(…)` and the `readHostDeclaration(…)`
that decides which of the two absence remedies to print. A drift pin in
`serve-capability-vocabulary.test.ts` (the suite that already holds the two
rosters to each other) asserts that value is a roster **key** and that its row is
the `enterprise` edition.

**Load semantics are untouched.** Which postures load the runtime, the two-stage
import/mount failure classification, and what `OS_ALLOW_DEGRADED_TENANCY` does
and does not cover are all exactly as they were; the roster is deliberately not a
resolution registry and is not consulted at boot. This single-sources the
spelling and nothing else, so there is no behaviour or output change — the
declared value is byte-identical to the literal it replaces.
47 changes: 37 additions & 10 deletions packages/cli/src/commands/serve-cluster-host-resolution.test.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -204,16 +204,39 @@ function packageNameOf(specifier: string): string | undefined {
}

/**
* Resolve one level of `const X = '<literal>'` — the idiom `serve.ts` uses
* everywhere to keep `tsc` from statically resolving an optional package
* (`const i18nPkg = '@objectstack/service-i18n'`). Without this the sweep would
* see only an identifier and classify every optional load as unknowable.
* Resolve `const X = '<literal>'` — the idiom `serve.ts` uses everywhere to keep
* `tsc` from statically resolving an optional package
* (`const i18nPkg = '@objectstack/service-i18n'`) — and one further hop,
* `const X = Serve.MEMBER`, where `MEMBER` is a `static readonly` string on the
* command class in this same file. Without this the sweep sees only an
* identifier and classifies the load as unknowable.
*
* The second hop is not a convenience. #11614 single-sourced the
* `@objectstack/organizations` spelling onto `Serve.ORGANIZATIONS_RUNTIME_PKG`
* so the spec-owned provenance roster could pin it, and a resolver that stops
* one hop short turns that load from "app-declarable, host-anchored, checked"
* into "unknowable" — SILENTLY, because an unresolved specifier drops OUT of
* `APP_DECLARABLE_LOADS` rather than into it. The named half of the vacuity
* guard below is what caught that, and is why it names packages instead of only
* counting them. Resolving one hop further strictly WIDENS what the sweep
* judges; it can never excuse a load.
*/
function resolveIdentifier(code: string, name: string): string | undefined {
const m = code.match(
const direct = code.match(
new RegExp(`\\bconst\\s+${name}\\s*(?::\\s*string\\s*)?=\\s*(['"\`])([^'"\`]*)\\1`),
);
return m?.[2];
if (direct) return direct[2];

// `const organizationsPkg = Serve.ORGANIZATIONS_RUNTIME_PKG;` (#11614)
const viaStatic = code.match(
new RegExp(`\\bconst\\s+${name}\\s*(?::\\s*string\\s*)?=\\s*Serve\\.([A-Za-z_$][\\w$]*)\\s*;`),
);
if (!viaStatic) return undefined;

const member = code.match(
new RegExp(`\\bstatic\\s+readonly\\s+${viaStatic[1]}\\s*(?::\\s*string\\s*)?=\\s*(['"\`])([^'"\`]*)\\1`),
);
return member?.[2];
}

/** Every dynamic load in `serve.ts`, bare or host-anchored. */
Expand DownExpand Up@@ -453,14 +476,18 @@ describe('os serve → every app-declarable optional load is host-anchored', ()
"packages/cli's manifest read as empty — every package would look app-declarable",
).toBeGreaterThan(20);

// Named, not just counted: this proves the resolver still handles all three
// spellings serve.ts uses — a `const` binding, a template prefix, and the
// manifest cross-check that decides app-declarable at all.
// Named, not just counted: this proves the resolver still handles every
// spelling serve.ts uses — a `const` binding, a template prefix, a `const`
// bound to a class static, and the manifest cross-check that decides
// app-declarable at all. Naming them is what caught #11614: the
// organizations spelling moved onto a static, the resolver stopped one hop
// short, and that load dropped OUT of the swept population — which the
// count-only floor of >20 absorbed without a word.
const found = new Set(APP_DECLARABLE_LOADS.map((s) => s.packageName));
for (const pkg of [
'@objectstack/service-cluster', // const binding (#10645)
'@objectstack/service-cluster-', // template prefix (#10645, the driver)
'@objectstack/organizations', // const binding (cloud#1013)
'@objectstack/organizations', // const <- static (cloud#1013, #11614)
'@objectstack/service-i18n', // const binding (#10769)
]) {
expect(found, `the sweep no longer sees the ${pkg} load`).toContain(pkg);
Expand Down
33 changes: 31 additions & 2 deletions packages/cli/src/commands/serve.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -491,6 +491,33 @@ export default class Serve extends Command {
*/
static readonly ALWAYS_ON_CAPABILITIES: readonly string[] = PLATFORM_ALWAYS_ON_CAPABILITIES;

/**
* The `plugins[]`-wired multi-org runtime this command loads when the
* resolved tenancy posture is walled (ADR-0105 D12) — declared ONCE, here.
*
* `serve` is the only runtime that prints this package name AT OPERATORS (the
* install remedy, the fatal refusal, the degraded-boot warning), and the
* spec-owned provenance roster `PLATFORM_PLUGIN_WIRED_RUNTIMES` is what makes
* the name answerable as real rather than fabricated — #10921 is the case
* where a fabricated `@objectstack/framework` sat next to the real
* `@objectstack/organizations` in published docs for months, indistinguishable.
* Spelled inline at the resolution site, the name was a second copy under no
* check at all: the roster's own header calls that "a second description
* nobody checks", and a roster cannot pin a spelling it cannot see. Read from
* here, it is one declaration with a drift pin over it —
* `serve-capability-vocabulary.test.ts` asserts this value is a roster KEY
* (#11614), so a rename on either side fails a test instead of silently
* pointing operators at a package that does not exist.
*
* Exposed as a static for the same reason ALWAYS_ON_CAPABILITIES above is:
* one declaration, two readers — the boot path and the pin.
*
* This single-sources the SPELLING and nothing else. Which postures load the
* runtime, and what each of the two failure stages means, stay exactly where
* they are; the roster is deliberately not a resolution registry.
*/
static readonly ORGANIZATIONS_RUNTIME_PKG = '@objectstack/organizations';

/**
* Auto-registered plugin tiers. Plugins explicitly listed in
* `config.plugins` are always loaded — tiers only gate the optional
Expand DownExpand Up@@ -2794,7 +2821,9 @@ export default class Serve extends Command {
// instances: `instanceof` and named `code` checks are both
// fragile here. Stage is the only classifier that needs to know
// nothing about the plugin's internals.
const organizationsPkg = '@objectstack/organizations';
// #11614 — the spelling comes from the class-level declaration the
// spec roster pins, not from a fresh literal at the resolution site.
const organizationsPkg = Serve.ORGANIZATIONS_RUNTIME_PKG;
let orgMod: any;
// ── Stage 1: import. Failure here = the package is ABSENT. ──
try {
Expand DownExpand Up@@ -2830,7 +2859,7 @@ export default class Serve extends Command {
// and whose install was pruned, that sent them to re-read a
// file that was already correct. The importer now says which
// one it is, so this text can too.
const declaration = readHostDeclaration('@objectstack/organizations', hostRoot);
const declaration = readHostDeclaration(organizationsPkg, hostRoot);
const remedy =
hostImportFailureKind(orgErr) === 'declared-unresolvable'
? ' • this app DECLARES @objectstack/organizations ' +
Expand Down
23 changes: 23 additions & 0 deletions packages/cli/test/serve-capability-vocabulary.test.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -92,6 +92,29 @@ describe('PLATFORM_PLUGIN_WIRED_RUNTIMES vs providers + serve resolver (#11263)'
expect(row.edition).toBe('enterprise');
});

// #11614 — the other half of the pin above, and the half that was missing.
// That one reads the roster with a literal spelled HERE, so it proves the
// roster has a row; it says nothing about the name `serve` actually resolves.
// Until this pin, that name was a bare literal in serve.ts under no check at
// all: rename the roster key, or mistype the literal, and both sides stay
// green while boot reaches for a package that does not exist and the fatal
// message tells operators to install it.
it('the package name serve resolves IS a roster key — not a second, unchecked copy of it (#11614)', () => {
expect(
Object.keys(PLATFORM_PLUGIN_WIRED_RUNTIMES),
`serve loads '${Serve.ORGANIZATIONS_RUNTIME_PKG}', which PLATFORM_PLUGIN_WIRED_RUNTIMES does not declare. ` +
'The roster is the single source for whether an out-of-repo @objectstack/* package is real and where ' +
'it ships from (#10921); a runtime that prints a package name at operators must name a row in it.',
).toContain(Serve.ORGANIZATIONS_RUNTIME_PKG);

// Provenance, read through serve's own spelling rather than a literal: the
// row this command's remedy text describes is the enterprise one.
const row = PLATFORM_PLUGIN_WIRED_RUNTIMES[Serve.ORGANIZATIONS_RUNTIME_PKG];
expect(row.edition, `edition drift for the runtime serve loads ('${Serve.ORGANIZATIONS_RUNTIME_PKG}')`).toBe(
'enterprise',
);
});

it('every enterprise-edition provider package has a roster row — enterprise means plugins[]-wired, by definition', () => {
// CapabilityEdition's own definition: `enterprise` = "a separately-licensed
// enterprise package the app installs and wires in via `plugins[]`". So an
Expand Down
Loading