@glubean/sdk 0.2.2 → 0.2.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (39) hide show
  1. package/dist/bootstrap-registry.d.ts +38 -0
  2. package/dist/bootstrap-registry.d.ts.map +1 -0
  3. package/dist/bootstrap-registry.js +54 -0
  4. package/dist/bootstrap-registry.js.map +1 -0
  5. package/dist/contract-core.d.ts +13 -1
  6. package/dist/contract-core.d.ts.map +1 -1
  7. package/dist/contract-core.js +329 -5
  8. package/dist/contract-core.js.map +1 -1
  9. package/dist/contract-http/adapter.d.ts +1 -7
  10. package/dist/contract-http/adapter.d.ts.map +1 -1
  11. package/dist/contract-http/adapter.js +199 -192
  12. package/dist/contract-http/adapter.js.map +1 -1
  13. package/dist/contract-http/index.d.ts +1 -0
  14. package/dist/contract-http/index.d.ts.map +1 -1
  15. package/dist/contract-http/index.js +1 -0
  16. package/dist/contract-http/index.js.map +1 -1
  17. package/dist/contract-http/types.d.ts +71 -12
  18. package/dist/contract-http/types.d.ts.map +1 -1
  19. package/dist/contract-http/types.js +45 -1
  20. package/dist/contract-http/types.js.map +1 -1
  21. package/dist/contract-types.d.ts +211 -20
  22. package/dist/contract-types.d.ts.map +1 -1
  23. package/dist/index.d.ts +3 -1
  24. package/dist/index.d.ts.map +1 -1
  25. package/dist/index.js +1 -0
  26. package/dist/index.js.map +1 -1
  27. package/dist/internal.d.ts +1 -0
  28. package/dist/internal.d.ts.map +1 -1
  29. package/dist/internal.js +3 -0
  30. package/dist/internal.js.map +1 -1
  31. package/dist/runner-input-channel.d.ts +95 -0
  32. package/dist/runner-input-channel.d.ts.map +1 -0
  33. package/dist/runner-input-channel.js +110 -0
  34. package/dist/runner-input-channel.js.map +1 -0
  35. package/package.json +1 -1
  36. package/dist/contract-http/flow-helpers.d.ts +0 -12
  37. package/dist/contract-http/flow-helpers.d.ts.map +0 -1
  38. package/dist/contract-http/flow-helpers.js +0 -34
  39. package/dist/contract-http/flow-helpers.js.map +0 -1
@@ -0,0 +1,38 @@
1
+ /**
2
+ * Bootstrap attachment registry.
3
+ *
4
+ * Per contract-attachment-model.md v1.3:
5
+ * - `contract.bootstrap(ref, spec)` registers an overlay targeting a
6
+ * specific contract case (identified by testId = `${contractId}.${caseKey}`).
7
+ * - Each test id may have at most one bootstrap overlay. Duplicate
8
+ * registration throws.
9
+ * - Registry is consulted by the runner at runnable resolution time
10
+ * (single-case-execution-api §5.1 algorithm).
11
+ *
12
+ * The registry is a process-global side-effect map. Modules that export
13
+ * `contract.bootstrap(...)` register on evaluation. Scanner / runner are
14
+ * responsible for eager module loading per attachment model §7.4.
15
+ */
16
+ import type { BootstrapAttachment, Bootstrap, ContractCaseRef } from "./contract-types.js";
17
+ interface BootstrapRegistration {
18
+ testId: string;
19
+ contractId: string;
20
+ caseKey: string;
21
+ protocol: string;
22
+ spec: Bootstrap<unknown, unknown>;
23
+ }
24
+ /**
25
+ * Register a bootstrap overlay for a specific case. Called by
26
+ * `contract.bootstrap(ref, spec)`.
27
+ *
28
+ * Throws if another overlay is already registered for the same testId.
29
+ */
30
+ export declare function registerBootstrap<Needs, Params = void>(ref: ContractCaseRef<Needs, unknown>, spec: Bootstrap<Params, Needs>): BootstrapAttachment<Needs, Params>;
31
+ /** Look up a bootstrap registration by testId. Returns undefined if none. */
32
+ export declare function getBootstrap(testId: string): BootstrapRegistration | undefined;
33
+ /** Enumerate all registered bootstrap overlays. Used by scanner / CLI list. */
34
+ export declare function listBootstraps(): BootstrapRegistration[];
35
+ /** Test-only: clear the registry between test runs. Not part of public API. */
36
+ export declare function clearBootstrapRegistry(): void;
37
+ export {};
38
+ //# sourceMappingURL=bootstrap-registry.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"bootstrap-registry.d.ts","sourceRoot":"","sources":["../src/bootstrap-registry.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;GAcG;AAEH,OAAO,KAAK,EACV,mBAAmB,EACnB,SAAS,EACT,eAAe,EAChB,MAAM,qBAAqB,CAAC;AAE7B,UAAU,qBAAqB;IAC7B,MAAM,EAAE,MAAM,CAAC;IACf,UAAU,EAAE,MAAM,CAAC;IACnB,OAAO,EAAE,MAAM,CAAC;IAChB,QAAQ,EAAE,MAAM,CAAC;IACjB,IAAI,EAAE,SAAS,CAAC,OAAO,EAAE,OAAO,CAAC,CAAC;CACnC;AAID;;;;;GAKG;AACH,wBAAgB,iBAAiB,CAAC,KAAK,EAAE,MAAM,GAAG,IAAI,EACpD,GAAG,EAAE,eAAe,CAAC,KAAK,EAAE,OAAO,CAAC,EACpC,IAAI,EAAE,SAAS,CAAC,MAAM,EAAE,KAAK,CAAC,GAC7B,mBAAmB,CAAC,KAAK,EAAE,MAAM,CAAC,CAuBpC;AAED,6EAA6E;AAC7E,wBAAgB,YAAY,CAC1B,MAAM,EAAE,MAAM,GACb,qBAAqB,GAAG,SAAS,CAEnC;AAED,+EAA+E;AAC/E,wBAAgB,cAAc,IAAI,qBAAqB,EAAE,CAExD;AAED,+EAA+E;AAC/E,wBAAgB,sBAAsB,IAAI,IAAI,CAE7C"}
@@ -0,0 +1,54 @@
1
+ /**
2
+ * Bootstrap attachment registry.
3
+ *
4
+ * Per contract-attachment-model.md v1.3:
5
+ * - `contract.bootstrap(ref, spec)` registers an overlay targeting a
6
+ * specific contract case (identified by testId = `${contractId}.${caseKey}`).
7
+ * - Each test id may have at most one bootstrap overlay. Duplicate
8
+ * registration throws.
9
+ * - Registry is consulted by the runner at runnable resolution time
10
+ * (single-case-execution-api §5.1 algorithm).
11
+ *
12
+ * The registry is a process-global side-effect map. Modules that export
13
+ * `contract.bootstrap(...)` register on evaluation. Scanner / runner are
14
+ * responsible for eager module loading per attachment model §7.4.
15
+ */
16
+ const _bootstrapRegistry = new Map();
17
+ /**
18
+ * Register a bootstrap overlay for a specific case. Called by
19
+ * `contract.bootstrap(ref, spec)`.
20
+ *
21
+ * Throws if another overlay is already registered for the same testId.
22
+ */
23
+ export function registerBootstrap(ref, spec) {
24
+ const testId = `${ref.contractId}.${ref.caseKey}`;
25
+ if (_bootstrapRegistry.has(testId)) {
26
+ throw new Error(`contract.bootstrap: duplicate overlay for case "${testId}". ` +
27
+ `Only one bootstrap overlay per case is allowed. ` +
28
+ `If you need multiple variants, use the \`bootstrap.params\` schema.`);
29
+ }
30
+ _bootstrapRegistry.set(testId, {
31
+ testId,
32
+ contractId: ref.contractId,
33
+ caseKey: ref.caseKey,
34
+ protocol: ref.protocol,
35
+ spec: spec,
36
+ });
37
+ return {
38
+ __glubean_type: "bootstrap-attachment",
39
+ testId,
40
+ };
41
+ }
42
+ /** Look up a bootstrap registration by testId. Returns undefined if none. */
43
+ export function getBootstrap(testId) {
44
+ return _bootstrapRegistry.get(testId);
45
+ }
46
+ /** Enumerate all registered bootstrap overlays. Used by scanner / CLI list. */
47
+ export function listBootstraps() {
48
+ return [..._bootstrapRegistry.values()];
49
+ }
50
+ /** Test-only: clear the registry between test runs. Not part of public API. */
51
+ export function clearBootstrapRegistry() {
52
+ _bootstrapRegistry.clear();
53
+ }
54
+ //# sourceMappingURL=bootstrap-registry.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"bootstrap-registry.js","sourceRoot":"","sources":["../src/bootstrap-registry.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;GAcG;AAgBH,MAAM,kBAAkB,GAAG,IAAI,GAAG,EAAiC,CAAC;AAEpE;;;;;GAKG;AACH,MAAM,UAAU,iBAAiB,CAC/B,GAAoC,EACpC,IAA8B;IAE9B,MAAM,MAAM,GAAG,GAAG,GAAG,CAAC,UAAU,IAAI,GAAG,CAAC,OAAO,EAAE,CAAC;IAElD,IAAI,kBAAkB,CAAC,GAAG,CAAC,MAAM,CAAC,EAAE,CAAC;QACnC,MAAM,IAAI,KAAK,CACb,mDAAmD,MAAM,KAAK;YAC5D,kDAAkD;YAClD,qEAAqE,CACxE,CAAC;IACJ,CAAC;IAED,kBAAkB,CAAC,GAAG,CAAC,MAAM,EAAE;QAC7B,MAAM;QACN,UAAU,EAAE,GAAG,CAAC,UAAU;QAC1B,OAAO,EAAE,GAAG,CAAC,OAAO;QACpB,QAAQ,EAAE,GAAG,CAAC,QAAQ;QACtB,IAAI,EAAE,IAAmC;KAC1C,CAAC,CAAC;IAEH,OAAO;QACL,cAAc,EAAE,sBAAsB;QACtC,MAAM;KAC+B,CAAC;AAC1C,CAAC;AAED,6EAA6E;AAC7E,MAAM,UAAU,YAAY,CAC1B,MAAc;IAEd,OAAO,kBAAkB,CAAC,GAAG,CAAC,MAAM,CAAC,CAAC;AACxC,CAAC;AAED,+EAA+E;AAC/E,MAAM,UAAU,cAAc;IAC5B,OAAO,CAAC,GAAG,kBAAkB,CAAC,MAAM,EAAE,CAAC,CAAC;AAC1C,CAAC;AAED,+EAA+E;AAC/E,MAAM,UAAU,sBAAsB;IACpC,kBAAkB,CAAC,KAAK,EAAE,CAAC;AAC7B,CAAC"}
@@ -19,7 +19,7 @@
19
19
  * - `internal/40-discovery/proposals/contract-flow.md` v9
20
20
  */
21
21
  import type { TestContext } from "./types.js";
22
- import type { ContractProtocolAdapter, ExtractedFlowProjection, FieldMapping, FlowBuilder, FlowContract, FlowMeta, RuntimeFlowProjection } from "./contract-types.js";
22
+ import type { Bootstrap, BootstrapAttachment, ContractCaseRef, ContractProtocolAdapter, ExtractedFlowProjection, FieldMapping, FlowBuilder, FlowContract, FlowMeta, RuntimeFlowProjection } from "./contract-types.js";
23
23
  /** Internal accessor for plugins / downstream (scanner, runner). */
24
24
  export declare function getAdapter(protocol: string): ContractProtocolAdapter<any, any, any, any, any> | undefined;
25
25
  /**
@@ -62,8 +62,20 @@ declare function register<Spec, RuntimeSchemas = unknown, RuntimeMeta = unknown,
62
62
  type ContractNamespace = {
63
63
  register: typeof register;
64
64
  flow: typeof flow;
65
+ bootstrap: typeof bootstrap;
65
66
  [protocol: string]: unknown;
66
67
  };
68
+ /**
69
+ * Register a bootstrap overlay for a contract case. Standalone-only
70
+ * execution path; flow NEVER invokes bootstrap (non-negotiable invariant
71
+ * from attachment model §0.4 / §14.0).
72
+ *
73
+ * `NoInfer<Needs>` on the spec parameter prevents TypeScript's multi-site
74
+ * inference from silently accepting a mismatched `run` return type —
75
+ * without it, TS merges inferences from ref + spec and produces a
76
+ * compatible Needs, masking real type errors (Spike 0 Finding 1).
77
+ */
78
+ export declare function bootstrap<Needs, Params = void>(ref: ContractCaseRef<Needs, unknown>, spec: Bootstrap<Params, NoInfer<Needs>>): BootstrapAttachment<Needs, Params>;
67
79
  export declare const contract: ContractNamespace;
68
80
  /**
69
81
  * Protocol-agnostic flow builder. See contract-flow.md v9 §4.1.
@@ -1 +1 @@
1
- {"version":3,"file":"contract-core.d.ts","sourceRoot":"","sources":["../src/contract-core.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;GAmBG;AAEH,OAAO,KAAK,EAAQ,WAAW,EAAE,MAAM,YAAY,CAAC;AACpD,OAAO,KAAK,EAGV,uBAAuB,EAGvB,uBAAuB,EAEvB,YAAY,EACZ,WAAW,EACX,YAAY,EACZ,QAAQ,EAKR,qBAAqB,EAEtB,MAAM,qBAAqB,CAAC;AAS7B,oEAAoE;AACpE,wBAAgB,UAAU,CACxB,QAAQ,EAAE,MAAM,GACf,uBAAuB,CAAC,GAAG,EAAE,GAAG,EAAE,GAAG,EAAE,GAAG,EAAE,GAAG,CAAC,GAAG,SAAS,CAE9D;AAED;;;;;;;GAOG;AACH,wBAAgB,uBAAuB,IAAI,MAAM,EAAE,CAElD;AAED;;;;;;;;;;GAUG;AACH,wBAAgB,8BAA8B,CAAC,QAAQ,EAAE,MAAM,GAAG,IAAI,CAIrE;AAED;;;;;;;GAOG;AACH,wBAAgB,gCAAgC,IAAI,IAAI,CAIvD;AAaD;;;;;GAKG;AACH,iBAAS,QAAQ,CACf,IAAI,EACJ,cAAc,GAAG,OAAO,EACxB,WAAW,GAAG,OAAO,EACrB,WAAW,GAAG,OAAO,EACrB,QAAQ,GAAG,OAAO,EAElB,QAAQ,EAAE,MAAM,EAChB,OAAO,EAAE,uBAAuB,CAAC,IAAI,EAAE,cAAc,EAAE,WAAW,EAAE,WAAW,EAAE,QAAQ,CAAC,GACzF,IAAI,CAgBN;AAwOD,KAAK,iBAAiB,GAAG;IACvB,QAAQ,EAAE,OAAO,QAAQ,CAAC;IAC1B,IAAI,EAAE,OAAO,IAAI,CAAC;IAClB,CAAC,QAAQ,EAAE,MAAM,GAAG,OAAO,CAAC;CAC7B,CAAC;AAEF,eAAO,MAAM,QAAQ,EAAE,iBAGtB,CAAC;AAMF;;GAEG;AACH,wBAAgB,IAAI,CAAC,QAAQ,EAAE,MAAM,GAAG,QAAQ,GAAG,WAAW,CAAC,OAAO,CAAC,CAqJtE;AAyBD;;;GAGG;AACH,wBAAsB,OAAO,CAAC,KAAK,EACjC,YAAY,EAAE,YAAY,CAAC,KAAK,CAAC,EACjC,GAAG,EAAE,WAAW,GACf,OAAO,CAAC,IAAI,CAAC,CAuEf;AAMD;;;;;;;GAOG;AACH,wBAAgB,aAAa,CAAC,KAAK,EACjC,OAAO,EAAE,qBAAqB,CAAC,KAAK,CAAC,GAAG;IAAE,EAAE,EAAE,MAAM,CAAA;CAAE,GACrD,uBAAuB,CAoEzB;AAED;;;;;;;;;GASG;AACH,wBAAgB,eAAe,CAAC,EAAE,EAAE,CAAC,KAAK,EAAE,GAAG,KAAK,GAAG,GAAG,YAAY,EAAE,CAIvE;AAED;;;GAGG;AACH,wBAAgB,kBAAkB,CAChC,EAAE,EAAE,CAAC,KAAK,EAAE,GAAG,EAAE,QAAQ,EAAE,GAAG,KAAK,GAAG,GACrC,YAAY,EAAE,CAMhB;AAkED;;;;GAIG;AACH,qBAAa,eAAgB,SAAQ,KAAK;IACxC,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;IACtB,QAAQ,CAAC,SAAS,EAAE,MAAM,CAAC;gBACf,IAAI,EAAE,MAAM,EAAE,SAAS,EAAE,MAAM;CAY5C;AAiDD;;;;;;;;GAQG;AACH,wBAAgB,cAAc,CAC5B,EAAE,EAAE,CAAC,KAAK,EAAE,GAAG,KAAK,GAAG,GACtB;IAAE,KAAK,EAAE,MAAM,EAAE,CAAC;IAAC,MAAM,EAAE,MAAM,EAAE,CAAA;CAAE,CAavC"}
1
+ {"version":3,"file":"contract-core.d.ts","sourceRoot":"","sources":["../src/contract-core.ts"],"names":[],"mappings":"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"}
@@ -19,6 +19,64 @@
19
19
  * - `internal/40-discovery/proposals/contract-flow.md` v9
20
20
  */
21
21
  import { registerTest } from "./internal.js";
22
+ import { getBootstrap, registerBootstrap } from "./bootstrap-registry.js";
23
+ import { getExplicitInput, getBootstrapInput, isForceStandalone, } from "./runner-input-channel.js";
24
+ /**
25
+ * Validate a value against a `needs` schema. Used by the v10 attachment model
26
+ * dispatcher after bootstrap overlay produces `resolvedInput` — before passing
27
+ * to `adapter.executeCase`. Keeps the invariant that adapter receives
28
+ * already-validated input.
29
+ *
30
+ * Handles both SchemaLike flavors (safeParse preferred, parse fallback).
31
+ * When neither is present, passes value through — the schema was purely
32
+ * type-level and carries no runtime check.
33
+ */
34
+ /**
35
+ * Run all registered cleanup callbacks in LIFO order. Each cleanup error is
36
+ * reported via console.error and does NOT mask the primary failure (the
37
+ * caller has already thrown or will throw whatever brought them here).
38
+ *
39
+ * Used by the v10 dispatcher's overlay path — same helper in all three
40
+ * failure sites (bootstrap run threw, needs validation failed, executeCase
41
+ * threw / succeeded). Before v3 feedback, the first two paths swallowed
42
+ * cleanup errors silently; this helper unifies reporting.
43
+ *
44
+ * Cleanup error reporting is `console.error` today. Spike 3 routes it
45
+ * through structured events so CI/agent consumers see it in test output.
46
+ */
47
+ async function runCleanupsLifo(cleanups, testId) {
48
+ while (cleanups.length > 0) {
49
+ const cleanup = cleanups.pop();
50
+ try {
51
+ await cleanup();
52
+ }
53
+ catch (err) {
54
+ // eslint-disable-next-line no-console
55
+ console.error(`bootstrap cleanup error for ${testId}:`, err);
56
+ }
57
+ }
58
+ }
59
+ function validateNeedsOutput(needsSchema, value, ctx) {
60
+ const sp = needsSchema.safeParse;
61
+ if (typeof sp === "function") {
62
+ const result = sp(value);
63
+ if (result.success)
64
+ return result.data;
65
+ const lines = result.error.issues.map((i) => ` - ${i.path?.join(".") ?? "<root>"}: ${i.message}`);
66
+ const sourceLabel = ctx.source === "bootstrap" ? "Bootstrap output"
67
+ : ctx.source === "explicit" ? "Explicit input"
68
+ : ctx.source === "bootstrap-params" ? "Bootstrap params"
69
+ : "Flow `in` output";
70
+ const schemaLabel = ctx.source === "bootstrap-params" ? "params schema" : "needs schema";
71
+ throw new Error(`${sourceLabel} for case "${ctx.testId}" does not satisfy ${schemaLabel}:\n${lines.join("\n")}`);
72
+ }
73
+ const p = needsSchema.parse;
74
+ if (typeof p === "function") {
75
+ return p(value);
76
+ }
77
+ // Schema declares neither safeParse nor parse — type-level only, pass through.
78
+ return value;
79
+ }
22
80
  // =============================================================================
23
81
  // Adapter registry
24
82
  // =============================================================================
@@ -88,6 +146,10 @@ function register(protocol, adapter) {
88
146
  }
89
147
  _adapters.set(protocol, adapter);
90
148
  // Attach contract[protocol] dispatcher dynamically.
149
+ // Generic `Cases` preserves per-case Needs/Output through ProtocolContract
150
+ // so `.case("key")` returns a properly-typed ContractCaseRef. Without this,
151
+ // `contract.bootstrap(ref, { run })` cannot type-check the run return
152
+ // against the specific case's needs.
91
153
  contract[protocol] = (id, spec) => {
92
154
  return dispatchContract(protocol, adapter, id, spec);
93
155
  };
@@ -106,6 +168,11 @@ function dispatchContract(protocol, adapter, id, spec) {
106
168
  };
107
169
  // 1:1 key invariant between spec.cases and projection.cases
108
170
  validateCaseKeys(protocol, spec.cases ?? {}, projection.cases);
171
+ // Forward-declared carrier reference. Populated below via Object.assign and
172
+ // captured by each test.fn closure so that v10 bootstrap overlay dispatch
173
+ // can pass `contract` to `adapter.executeCase({ ..., contract, ... })` at
174
+ // runtime. Safe because fn closures only execute after contractObj is set.
175
+ let contractObj;
109
176
  const cases = spec.cases ?? {};
110
177
  const contractTagsRaw = spec.tags;
111
178
  const contractTags = Array.isArray(contractTagsRaw)
@@ -157,6 +224,201 @@ function dispatchContract(protocol, adapter, id, spec) {
157
224
  ctx.skip(skipDeprecated);
158
225
  if (skipDeferred)
159
226
  ctx.skip(skipDeferred);
227
+ // ── §5.1 runnable resolution algorithm ───────────────────────────
228
+ //
229
+ // Step 1: explicit input wins over everything.
230
+ // - case has needs → validate, run raw with that input
231
+ // - case has no needs → reject (input has no schema target)
232
+ // Step 2: requireAttachment + no overlay → hard error
233
+ // (bypassed by setForceStandalone via internal channel; debug only)
234
+ // Step 3: overlay registered → run overlay path; bootstrap params
235
+ // come from getBootstrapInput; validated against overlay's
236
+ // `params` schema if structured form
237
+ // Step 4: needs declared, no overlay, no input → hard error
238
+ // Step 5: no needs, no overlay → run raw with no input
239
+ //
240
+ // The order below mirrors §5.1 exactly. Tests for each path live
241
+ // in `contract.test.ts` (search "§5.1").
242
+ const needsSchema = caseSpec.needs;
243
+ const requireAttachment = caseSpec.runnability?.requireAttachment;
244
+ // §5.1 Step 1 — explicit input always wins. Bootstrap overlay (even
245
+ // if registered) is NOT invoked. No "run bootstrap for side-effects
246
+ // then use my input" mode (per §5.1 invariants).
247
+ const explicit = getExplicitInput(testId);
248
+ if (explicit.has) {
249
+ if (!needsSchema) {
250
+ const requireHint = requireAttachment
251
+ ? " Use --force-standalone for debug, or attach a bootstrap."
252
+ : "";
253
+ throw new Error(`case "${testId}" has no \`needs\` schema; explicit input is ` +
254
+ `meaningless (no schema to satisfy).${requireHint}`);
255
+ }
256
+ if (!contractObj) {
257
+ throw new Error(`Internal: contract carrier not assembled when running "${testId}".`);
258
+ }
259
+ if (!adapter.executeCase) {
260
+ throw new Error(`Explicit input provided for "${testId}" but the adapter for ` +
261
+ `protocol "${protocol}" has not been migrated to the ` +
262
+ `attachment model (missing executeCase method).`);
263
+ }
264
+ const validatedInput = validateNeedsOutput(needsSchema, explicit.value, { testId, source: "explicit" });
265
+ await adapter.executeCase({
266
+ ctx,
267
+ contract: contractObj,
268
+ caseKey,
269
+ resolvedInput: validatedInput,
270
+ });
271
+ return;
272
+ }
273
+ // §5.1 Step 3 — overlay path. Step 2 (requireAttachment + no
274
+ // overlay) is handled in the no-overlay branch below.
275
+ const overlay = getBootstrap(testId);
276
+ if (overlay) {
277
+ // Hard error if adapter hasn't implemented the attachment-model
278
+ // entry point. Better to fail loudly at dispatch than to run the
279
+ // raw case as if no overlay existed.
280
+ if (!adapter.executeCase) {
281
+ throw new Error(`Bootstrap overlay registered for "${testId}" but the adapter ` +
282
+ `for protocol "${protocol}" has not been migrated to the ` +
283
+ `attachment model (missing executeCase method). The overlay ` +
284
+ `would be silently ignored. Either remove the overlay or ` +
285
+ `migrate the adapter runtime.`);
286
+ }
287
+ if (!contractObj) {
288
+ // Unreachable at fn-call time (contractObj is assigned before
289
+ // any test.fn runs). Defensive check keeps TS narrowing clean.
290
+ throw new Error(`Internal: contract carrier not assembled when running "${testId}".`);
291
+ }
292
+ const cleanups = [];
293
+ const bootstrapCtx = Object.assign(Object.create(ctx), ctx, {
294
+ cleanup(fn) {
295
+ cleanups.push(fn);
296
+ },
297
+ });
298
+ const runFn = typeof overlay.spec === "function"
299
+ ? overlay.spec
300
+ : overlay.spec.run;
301
+ // §5.1 step 3a-3b: structured-form `params` schema validation.
302
+ // Bootstrap input comes from the runner channel (CLI
303
+ // `--bootstrap-json`, MCP `bootstrapInput`, programmatic
304
+ // `bootstrapInput`).
305
+ const paramsSchema = typeof overlay.spec === "object" && overlay.spec !== null
306
+ ? overlay.spec.params
307
+ : undefined;
308
+ const providedBootstrap = getBootstrapInput(testId);
309
+ // Plain-function overlay (or structured form without `params`)
310
+ // does NOT accept bootstrap input. If the runner supplied one,
311
+ // the user almost certainly forgot to declare a `params` schema
312
+ // or mistyped the testId — silently dropping the input would
313
+ // mask the bug. Hard-error before invoking run().
314
+ if (!paramsSchema && providedBootstrap.has) {
315
+ throw new Error(`case "${testId}": runner supplied bootstrap input but the ` +
316
+ `registered overlay does not declare a \`params\` schema. ` +
317
+ `Plain-function overlays (and structured overlays without ` +
318
+ `\`params\`) cannot consume bootstrap input — declare ` +
319
+ `\`params: SchemaLike<...>\` on the overlay spec, or remove ` +
320
+ `the bootstrap input from the runner invocation.`);
321
+ }
322
+ let validatedParams = undefined;
323
+ if (paramsSchema) {
324
+ // §5.1 step 3a: "Take bootstrapInput from runner options
325
+ // (may be empty if no params)". Validation happens whether
326
+ // or not the runner supplied input — schema may have
327
+ // defaults, or all fields may be optional.
328
+ validatedParams = validateNeedsOutput(paramsSchema, providedBootstrap.has ? providedBootstrap.value : undefined, { testId, source: "bootstrap-params" });
329
+ }
330
+ let resolvedInput;
331
+ try {
332
+ resolvedInput = await runFn(bootstrapCtx, validatedParams);
333
+ }
334
+ catch (err) {
335
+ await runCleanupsLifo(cleanups, testId);
336
+ throw err;
337
+ }
338
+ // v10 Phase 2b Step 2: validate bootstrap output against `needs`
339
+ // schema before handing off to adapter. Adapter's contract is
340
+ // "receives already-validated input"; validation at the runner
341
+ // boundary matches single-case-execution-api.md §7.
342
+ if (needsSchema) {
343
+ try {
344
+ resolvedInput = validateNeedsOutput(needsSchema, resolvedInput, { testId, source: "bootstrap" });
345
+ }
346
+ catch (err) {
347
+ await runCleanupsLifo(cleanups, testId);
348
+ throw err;
349
+ }
350
+ }
351
+ try {
352
+ await adapter.executeCase({
353
+ ctx,
354
+ contract: contractObj,
355
+ caseKey,
356
+ resolvedInput,
357
+ });
358
+ }
359
+ finally {
360
+ await runCleanupsLifo(cleanups, testId);
361
+ }
362
+ return;
363
+ }
364
+ // ── No-overlay standalone path ───────────────────────────────────
365
+ //
366
+ // First: bootstrap input supplied but no overlay registered. The
367
+ // user provided `--bootstrap-json` / `bootstrapInput` for a case
368
+ // that has no `contract.bootstrap()`. Almost always a typo or a
369
+ // missing overlay file; silently dropping the input would mask
370
+ // the bug. Hard-error before any other resolution.
371
+ const providedBootstrapNoOverlay = getBootstrapInput(testId);
372
+ if (providedBootstrapNoOverlay.has) {
373
+ throw new Error(`case "${testId}": runner supplied bootstrap input but no ` +
374
+ `bootstrap overlay is registered for this case. Register an ` +
375
+ `overlay via \`contract.bootstrap(ref, { params, run })\`, ` +
376
+ `or remove the bootstrap input from the runner invocation. ` +
377
+ `(If you meant to bypass the overlay and pass case input ` +
378
+ `directly, use --input-json / inputJson instead.)`);
379
+ }
380
+ // §5.1 step 2: requireAttachment + no overlay → hard error
381
+ // (UNLESS isForceStandalone(testId) — debug bypass per §6.3).
382
+ // §5.1 step 4: needs declared, no overlay, no input → hard error.
383
+ // §5.1 step 5: no needs, no overlay → run raw with no input.
384
+ //
385
+ // Order matters: step 2 fires before step 4 because requireAttachment
386
+ // is the stronger declaration ("don't run bare under any input
387
+ // condition"). A case with both needs + requireAttachment + no
388
+ // overlay + no input gets the requireAttachment error message,
389
+ // which carries the more actionable hint (attach a bootstrap or
390
+ // pass --input-json depending on needs presence).
391
+ if (requireAttachment) {
392
+ if (isForceStandalone(testId)) {
393
+ // §6.3 debug escape valve. Author opted out of the guard via
394
+ // --force-standalone; emit a runtime warning so it's visible
395
+ // in test output, then fall through to step 4/5.
396
+ // eslint-disable-next-line no-console
397
+ console.warn(`⚠ case "${testId}": bypassing runnability.requireAttachment ` +
398
+ `via --force-standalone (debug). Do not use this in normal runs.`);
399
+ }
400
+ else {
401
+ throw new Error(`case "${testId}" sets \`runnability.requireAttachment: true\` ` +
402
+ `but no bootstrap overlay is registered. Per attachment model ` +
403
+ `§7.2: register a bootstrap overlay via \`contract.bootstrap(...)\`, ` +
404
+ `attach the case to a flow step, or pass \`--input-json\` (if ` +
405
+ `the case declares \`needs\`). Raw execution is disabled for ` +
406
+ `this case by its own declaration.`);
407
+ }
408
+ }
409
+ // §5.1 step 4: needs case with no overlay + no input → hard error.
410
+ // (Step 1 already returned for cases with explicit input above.)
411
+ if (needsSchema) {
412
+ throw new Error(`case "${testId}" declares \`needs\` but has no bootstrap overlay ` +
413
+ `and no explicit input. Per attachment model §5.1: register a ` +
414
+ `bootstrap overlay via \`contract.bootstrap(...)\`, or pass ` +
415
+ `\`--input-json\` to provide explicit input.`);
416
+ }
417
+ // §5.1 step 5: no needs, no overlay (or requireAttachment+force-
418
+ // standalone) → run case with no input. Static body / headers /
419
+ // params / query work as-is; function-valued action fields receive
420
+ // undefined (author responsibility — typically a no-needs case has
421
+ // no function-valued fields).
160
422
  await adapter.execute(ctx, caseSpec, spec);
161
423
  },
162
424
  };
@@ -205,14 +467,18 @@ function dispatchContract(protocol, adapter, id, spec) {
205
467
  // implement is a compile error, not a silent runtime hole.
206
468
  const extracted = adapter.normalize(enrichedProjection);
207
469
  const arr = [...tests];
208
- const contractObj = Object.assign(arr, {
470
+ contractObj = Object.assign(arr, {
209
471
  _projection: enrichedProjection,
210
472
  _extracted: extracted,
211
473
  _spec: spec,
212
474
  case(key) {
213
- // Delegate fail-fast validation to adapter (e.g. HTTP rejects
214
- // function-valued body/params/query/headers).
215
- adapter.validateCaseForFlow?.(spec, key, id);
475
+ // v10: .case(key) is pure case-ref creation no flow-only validation
476
+ // here. Flow-safety checks (e.g. HTTP's "function-valued body/params/
477
+ // headers can't be resolved in flow mode") moved to FlowBuilder.step()
478
+ // where the ref's flow usage is actually being declared. The same
479
+ // ref used via contract.bootstrap(overlay) legitimately uses
480
+ // function-valued fields with `resolvedInput` — v9's
481
+ // validateCaseForFlow incorrectly rejected that at .case() time.
216
482
  return makeContractCaseRef(protocol, id, projection.target, key, contractObj, spec);
217
483
  },
218
484
  });
@@ -261,9 +527,23 @@ function makeContractCaseRef(protocol, contractId, target, caseKey, contract, _s
261
527
  contract,
262
528
  };
263
529
  }
530
+ /**
531
+ * Register a bootstrap overlay for a contract case. Standalone-only
532
+ * execution path; flow NEVER invokes bootstrap (non-negotiable invariant
533
+ * from attachment model §0.4 / §14.0).
534
+ *
535
+ * `NoInfer<Needs>` on the spec parameter prevents TypeScript's multi-site
536
+ * inference from silently accepting a mismatched `run` return type —
537
+ * without it, TS merges inferences from ref + spec and produces a
538
+ * compatible Needs, masking real type errors (Spike 0 Finding 1).
539
+ */
540
+ export function bootstrap(ref, spec) {
541
+ return registerBootstrap(ref, spec);
542
+ }
264
543
  export const contract = {
265
544
  register,
266
545
  flow,
546
+ bootstrap,
267
547
  };
268
548
  // =============================================================================
269
549
  // Flow: FlowBuilder + runFlow + normalizeFlow + tracePureFn
@@ -304,6 +584,14 @@ export function flow(idOrMeta) {
304
584
  throw new Error(`contract.flow(${JSON.stringify(meta.id)}).step: adapter for "${ref.protocol}" ` +
305
585
  `does not implement executeCaseInFlow — this protocol cannot appear in a flow.`);
306
586
  }
587
+ // v10: flow-safety validation fires here (at step-declaration time),
588
+ // not at .case() time. The ref itself is pure; only when it enters a
589
+ // flow do we enforce adapter-specific rules like HTTP's "function-
590
+ // valued body/params/headers can't resolve in flow mode". This lets
591
+ // contract.bootstrap(ref) attach to the same ref (where function-
592
+ // valued fields legitimately receive resolvedInput).
593
+ const contractRef = ref.contract;
594
+ adapter.validateCaseForFlow?.(contractRef._spec, ref.caseKey, ref.contractId);
307
595
  const step = {
308
596
  kind: "contract-call",
309
597
  name: bindings?.name,
@@ -461,7 +749,43 @@ export async function runFlow(flowContract, ctx) {
461
749
  throw new Error(`flow "${runtime.id}" step "${step.name ?? step.caseKey}": ` +
462
750
  `adapter for "${step.ref.protocol}" does not implement executeCaseInFlow`);
463
751
  }
464
- const resolvedInputs = step.bindings?.in?.(state);
752
+ // v10 Phase 2d Step 2+3 follow-up (RFR v2 / v2.1 P1): enforce `needs`
753
+ // schema at flow boundary, not just standalone. The conditional-tuple
754
+ // `step()` signature catches TS authoring shape mismatches, but runtime
755
+ // validation is the only line of defense against:
756
+ // - `as any` / JS callers that bypass the TS check
757
+ // - Zod parse / coerce / default semantics (schema can transform
758
+ // the value, not just validate it)
759
+ // - State drift producing invalid values at runtime despite valid
760
+ // authoring types
761
+ // Standalone overlay path already validates via the same helper in
762
+ // `dispatchContract`'s test.fn closure; flow mirrors that to keep
763
+ // `needs` a true contract semantic rather than a TS-only boundary.
764
+ //
765
+ // Two-branch guard:
766
+ // needs present + bindings.in missing → throw (contract requires input)
767
+ // needs present + bindings.in present → validate unconditionally
768
+ // needs absent → no guard
769
+ const contractSpec = step.contract._spec;
770
+ const caseSpec = contractSpec?.cases?.[step.caseKey];
771
+ const needsSchema = caseSpec?.needs;
772
+ const hasIn = typeof step.bindings?.in === "function";
773
+ if (needsSchema && !hasIn) {
774
+ throw new Error(`flow "${runtime.id}" step "${step.name ?? step.caseKey}": ` +
775
+ `case "${step.ref.contractId}.${step.caseKey}" declares \`needs\` ` +
776
+ `but the step has no \`bindings.in\`. The TypeScript conditional ` +
777
+ `tuple on FlowBuilder.step() usually prevents this at compile time; ` +
778
+ `this runtime check catches \`as any\` / JS bypass. Provide an ` +
779
+ `\`in: (state) => <logical input>\` binding or remove the \`needs\` ` +
780
+ `schema from the case.`);
781
+ }
782
+ let resolvedInputs = step.bindings?.in?.(state);
783
+ if (needsSchema) {
784
+ resolvedInputs = validateNeedsOutput(needsSchema, resolvedInputs, {
785
+ testId: `${step.ref.contractId}.${step.caseKey}`,
786
+ source: "flow",
787
+ });
788
+ }
465
789
  const response = await adapter.executeCaseInFlow({
466
790
  ctx,
467
791
  contract: step.contract,