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,walkNativeBindingsare 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 zfrom"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
moduleRefis absent from the result
Scenario: missing required field rejected
- WHEN an entry missing
manifestHashis 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_HELDis thrown ANDdetails.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"