From 61be8f8dfd1fe5d9a9e138be4de0ab71fd25cebd Mon Sep 17 00:00:00 2001 From: Claude Date: Sun, 23 Aug 2026 16:10:50 +0000 Subject: [PATCH] test(spec): derive the always-on mount boundary instead of pinning slice(0, 6) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The literal prefix assertion bundled the four bind targets (queue/job/cache/ settings) together with two of their readers (email/storage) and stopped one short of the third — sms, at index 6 — whose position was then held by nothing. The comment above it told the next author to grow the slate AFTER those six, which is exactly how sms came to be added outside the pin. State the rule instead: every always-on entry that is not one of the services others bind into at kernel:ready is mounted after all of them. Retire the inviting comment in the test AND its twin above the declaration itself. Part of #11046 Co-Authored-By: Claude Fable 5 Claude-Session: https://claude.ai/code/session_01T9cDbY2NBiVJWYx3BpWfH2 --- .../src/kernel/platform-capabilities.test.ts | 94 +++++++++++++++++-- .../spec/src/kernel/platform-capabilities.ts | 13 ++- 2 files changed, 99 insertions(+), 8 deletions(-) diff --git a/packages/spec/src/kernel/platform-capabilities.test.ts b/packages/spec/src/kernel/platform-capabilities.test.ts index 16c3b71954..6051747626 100644 --- a/packages/spec/src/kernel/platform-capabilities.test.ts +++ b/packages/spec/src/kernel/platform-capabilities.test.ts @@ -126,6 +126,48 @@ describe('classifyRequiredCapability (#3366)', () => { * the single declaration trustworthy for both readers. */ describe('PLATFORM_ALWAYS_ON_CAPABILITIES', () => { + /** + * The always-on entries that OTHER always-on entries bind into during their + * own `kernel:ready` phase. Mount order is what makes those bindings resolve, + * so the boundary is a ROLE — never a count. + * + * A literal `slice(0, 6)` prefix assertion stood here, under a comment telling + * the next author to grow the slate *after those six*. That instruction is + * what produced #10250: the six bundled these four bind targets together with + * two of their READERS (`email`, `storage`) and stopped one short of the third + * — `sms`, at index 6 — which was then added exactly as instructed, outside + * the pin, with its position relative to `settings` held by nothing at all. + * + * Adding a seventh entry to the count would have been the same defect moved + * one position. Derived from what the pin is FOR, the rule covers whatever is + * added tomorrow, wherever it goes. + * + * ⚠️ Scope of this pin: `packages/spec` sits below every plugin package, so + * this file cannot import the plugin classes and therefore cannot verify that + * `BIND_TARGETS` still lists the right services — it pins slate ORDER only. + * `serve-settings-ordering.pin.test.ts` in `@objectstack/cli` is the half that + * keeps the membership honest: it resolves the real plugin classes and proves + * each shipped settings reader declares the ordering edge (ADR-0116/ADR-0049). + * The two are complementary, not duplicates — neither subsumes the other. + */ + const BIND_TARGETS = ['queue', 'job', 'cache', 'settings'] as const; + + const slateIndex = (token: string) => PLATFORM_ALWAYS_ON_CAPABILITIES.indexOf(token); + + /** + * The rule itself, as one predicate: every entry that is not a bind target + * must be mounted after all of them. Stated once so the pin below and its + * positive control cannot drift apart. + */ + const orderingViolations = ( + slate: readonly string[], + targets: readonly string[] = BIND_TARGETS, + ): string[] => { + const at = (t: string) => slate.indexOf(t); + const lastTarget = Math.max(...targets.map(at)); + return slate.filter((c) => !targets.includes(c) && at(c) < lastTarget); + }; + it('is frozen, non-empty and free of duplicates', () => { expect(Object.isFrozen(PLATFORM_ALWAYS_ON_CAPABILITIES)).toBe(true); expect(PLATFORM_ALWAYS_ON_CAPABILITIES.length).toBeGreaterThan(0); @@ -134,12 +176,52 @@ describe('PLATFORM_ALWAYS_ON_CAPABILITIES', () => { ); }); - it('pins the foundational prefix — grow the slate AFTER these six', () => { - // Order matters at mount time: settings/queue/job must precede the services - // that bind to them during their own `kernel:ready` phase. - expect(PLATFORM_ALWAYS_ON_CAPABILITIES.slice(0, 6)).toEqual([ - 'queue', 'job', 'cache', 'settings', 'email', 'storage', - ]); + it('mounts every bind target — the services other entries bind into at `kernel:ready`', () => { + // The floor for the ordering rule below. A missing target would make that + // rule constrain less without failing; with every target gone it would + // constrain nothing at all, and still pass. + for (const target of BIND_TARGETS) { + expect(slateIndex(target), `${target} must be on the slate`).toBeGreaterThanOrEqual(0); + } + }); + + it('mounts every other entry after ALL bind targets — derived, so tomorrow’s entry is covered on arrival', () => { + // THE BOUNDARY, DERIVED. Not "the first six": the rule is that the services + // other entries bind into during their own `kernel:ready` phase are mounted + // first, and everything else grows after them. `sms`, `sharing`, + // `messaging`, `analytics` — and an eleventh entry added tomorrow, wherever + // it goes — are all covered by this the moment they are added. + const tail = PLATFORM_ALWAYS_ON_CAPABILITIES.filter( + (c) => !(BIND_TARGETS as readonly string[]).includes(c), + ); + // Non-vacuity: the tail is what this case is about, so an empty one would + // be a pass over nothing. + expect(tail.length).toBeGreaterThan(0); + expect(orderingViolations(PLATFORM_ALWAYS_ON_CAPABILITIES)).toEqual([]); + }); + + it('…and the rule is falsifiable — a hostile slate is named, not shrugged off', () => { + // The positive control for the derivation above. The SAME predicate over a + // slate with `settings` mounted last must name every entry that now + // precedes it; a predicate that reported nothing here would report nothing + // on a real regression either. + expect( + orderingViolations(['queue', 'job', 'cache', 'email', 'sms', 'storage', 'settings']), + ).toEqual(['email', 'sms', 'storage']); + + // …and the class the retired `slice(0, 6)` pin structurally could not see. + // That pin constrained indices 0-5 and nothing else, so a bind target + // arriving anywhere past the prefix read as correct at a glance — the same + // blind spot that left `sms` at index 6 held by nothing. Model it: today's + // slate, unchanged, plus a NEW bind target appended. `slice(0, 6)` is + // untouched and the old pin passes; this rule names all six readers now + // mounted ahead of it. + expect( + orderingViolations( + [...PLATFORM_ALWAYS_ON_CAPABILITIES, 'secrets'], + [...BIND_TARGETS, 'secrets'], + ), + ).toEqual(['email', 'storage', 'sms', 'sharing', 'messaging', 'analytics']); }); it('every member is a real platform capability token', () => { diff --git a/packages/spec/src/kernel/platform-capabilities.ts b/packages/spec/src/kernel/platform-capabilities.ts index 80e4f4cbce..dee97ad3c2 100644 --- a/packages/spec/src/kernel/platform-capabilities.ts +++ b/packages/spec/src/kernel/platform-capabilities.ts @@ -201,8 +201,17 @@ export const PLATFORM_CAPABILITY_PROVIDERS: Readonly