Files
suaveplan/openspec/archive/2026-05-25-api-loader/specs/api-loader/spec.md
2026-05-27 23:29:53 +01:00

6.4 KiB

Capability Spec Delta: api-loader (api-loader)

Package: @suaveplan/api-loader Status: PROPOSED

Every requirement carries a **Verified by:** line. Types-only files are noted where coverage is N/A.

ADDED Requirements

Requirement: ModuleResolver contract

The package SHALL export ModuleResolver interface: resolve({ specifier, version }): Promise<ModuleHandle> (imports the module under the cache-busted specifier); dispose(handle): Promise<void> (releases the host's strong ref so a WeakRef may collect); walkNativeBindings(specifier): Promise<readonly string[]> (returns paths of .node files reachable from the entry). Types-only — coverage N/A for this file. Concrete impl lives in api-host.

Scenario: every documented method present

  • WHEN expectTypeOf<ModuleResolver>() is checked
  • THEN resolve, dispose, walkNativeBindings are declared with the documented signatures

Verified by: src/resolver/resolver.spec-conformance.test.ts::"ModuleResolver declares resolve dispose walkNativeBindings"


Requirement: import z from "zod" everywhere

Every Zod import MUST use the default form.

Scenario: no named import of z

  • WHEN the test greps import z from "zod"
  • THEN zero matches

Verified by: tests/zod-import-form.test.ts::"every src file uses default-import z"


Requirement: bustSpecifier algorithm

The package SHALL export bustSpecifier(url: string, version: number): string. The function MUST append ?v=${version} to a clean URL AND REPLACE an existing ?v=N suffix when called on an already-busted URL. The result MUST be a valid file URL (host MUST be able to import() it).

Scenario: clean URL gets ?v=N appended

  • WHEN bustSpecifier("file:///abs/x.js", 1) is called
  • THEN the result is "file:///abs/x.js?v=1"

Scenario: re-busting replaces the version

  • WHEN bustSpecifier("file:///abs/x.js?v=1", 2) is called
  • THEN the result is "file:///abs/x.js?v=2" (NOT "...?v=1?v=2")

Verified by: src/cache-bust/cache-bust.test.ts::"clean URL gets ?v appended" AND src/cache-bust/cache-bust.test.ts::"re-busting replaces version"


Requirement: RollbackJournal ring buffer

The package SHALL export RollbackJournal interface: push(entry), peek(n): readonly RollbackJournalEntry[] (newest first), latest(): RollbackJournalEntry | null, length: number, clear(): void. Default depth 3 (constructor parameter). push beyond capacity MUST drop the oldest entry. Types-only — coverage N/A. Concrete impl in api-host.

Scenario: peek returns newest first

  • WHEN entries A, B, C are pushed in order
  • THEN peek(3) returns [C, B, A]

Scenario: push beyond capacity drops oldest

  • WHEN four entries are pushed to a depth-3 journal
  • THEN the first entry is dropped AND length === 3

Scenario: latest returns the most recent or null

  • WHEN the journal is empty AND latest() is called
  • THEN it returns null

Verified by: src/rollback-journal/rollback-journal.test.ts::"peek returns newest first" AND src/rollback-journal/rollback-journal.test.ts::"push beyond capacity drops oldest" AND src/rollback-journal/rollback-journal.test.ts::"latest returns null on empty"


Requirement: Persisted-entry Zod schema

The package SHALL export rollbackJournalEntryPersistedSchema: z.ZodSchema<PersistedRollbackEntry> covering only the fields that survive host restart: { specifier: string, manifestHash: string, migrationCursor: number, capturedAt: string, version: string }. The in-memory moduleRef: WeakRef is type-only and NOT in this schema.

Scenario: persisted shape parses

  • WHEN a valid persisted entry is parsed
  • THEN parsing succeeds AND moduleRef is absent from the result

Scenario: missing required field rejected

  • WHEN an entry missing manifestHash is parsed
  • THEN Zod reports an issue at manifestHash

Verified by: src/journal-entry/journal-entry.test.ts::"persisted shape parses without moduleRef" AND src/journal-entry/journal-entry.test.ts::"missing required field rejected"


Requirement: Native-binding detector contract

The package SHALL document that walkNativeBindings(specifier) MUST return the paths of every .node file reachable from the entry's import subgraph. When the returned array is non-empty, the host's reload path MUST throw LOADER.NATIVE_BINDING_HELD and refuse the reload — pure-JS reload is the only mode supported on natives.

Scenario: native binding in subgraph triggers refusal

  • WHEN a stub detector returns ["/x/y.node"] AND a host reload check runs
  • THEN LOADER.NATIVE_BINDING_HELD is thrown AND details.bindings === ["/x/y.node"]

Scenario: pure-JS subgraph passes through

  • WHEN the detector returns []
  • THEN the reload check returns without throwing

Verified by: src/native-binding-detector/native-binding-detector.test.ts::"native binding in subgraph triggers refusal" AND src/native-binding-detector/native-binding-detector.test.ts::"pure-JS subgraph passes through"


Requirement: Error codes registry frozen

The package SHALL export LOADER_CODES containing exactly: NATIVE_BINDING_HELD, MODULE_IMPORT_FAILED, JOURNAL_FULL, JOURNAL_ENTRY_INVALID.

Scenario: every code reachable

  • WHEN the test enumerates LOADER_CODES
  • THEN every code has at least one production-path reference

Verified by: src/error-codes/error-codes.spec-conformance.test.ts::"every LOADER_CODES entry thrown by production path"


Requirement: Conformance harnesses for loader + journal

The package SHALL export runLoaderConformance({ factory, label }) and runRollbackJournalConformance({ factory, label }). api-host's concrete impls MUST pass both.

Scenario: loader harness covers resolve, dispose, native binding refusal

  • WHEN the harness runs against a stub loader
  • THEN test categories exist for each documented behavior

Scenario: journal harness covers push, peek, capacity, latest, clear

  • WHEN the harness runs against a stub journal
  • THEN test categories exist for each documented behavior

Verified by: src/testing/loader-conformance.test.ts::"loader harness covers resolve, dispose, native binding refusal" AND src/testing/journal-conformance.test.ts::"journal harness covers push peek capacity latest clear"