@glubean/scanner 0.2.2 → 0.2.4

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.
@@ -23,6 +23,18 @@ export type CaseSeverity = "critical" | "warning" | "info";
23
23
  export type CaseRequires = "headless" | "browser" | "out-of-band";
24
24
  /** Case default run policy. */
25
25
  export type CaseDefaultRun = "always" | "opt-in";
26
+ /** Projectable description of opaque verify() semantics. */
27
+ export type VerifyRule = string | {
28
+ id?: string;
29
+ description: string;
30
+ severity?: CaseSeverity;
31
+ extensions?: Record<string, unknown>;
32
+ };
33
+ /** Runnability gates mirrored from the SDK's CaseRunnability. */
34
+ export interface NormalizedRunnability {
35
+ requireAttachment?: boolean;
36
+ requireSession?: boolean;
37
+ }
26
38
  /** A named example entry for OpenAPI docs. */
27
39
  export interface NormalizedExample {
28
40
  value: unknown;
@@ -56,6 +68,35 @@ export interface NormalizedCaseMeta {
56
68
  schemas?: unknown;
57
69
  /** Adapter-defined free-form meta (opaque). */
58
70
  meta?: unknown;
71
+ /**
72
+ * World-state precondition — attachment-model §0.9. Adapter threads
73
+ * through from `BaseCaseSpec.given`. Not a contract semantic input,
74
+ * but projected because it changes what `expect` means.
75
+ */
76
+ given?: string;
77
+ /** True when executable semantics also live in an adapter verify() callback. */
78
+ hasVerify?: boolean;
79
+ /** Projectable companion rules for opaque verify() callbacks. */
80
+ verifyRules?: VerifyRule[];
81
+ /**
82
+ * Runnability metadata — attachment-model §7.2. `requireAttachment`
83
+ * blocks raw execution (case MUST run via a bootstrap overlay). Lives
84
+ * as a first-class field, not in `extensions`, per proposal.
85
+ */
86
+ runnability?: NormalizedRunnability;
87
+ /**
88
+ * True iff the case declared `needs`. Authoritative trigger for
89
+ * `rawBypass` in the attachment inventory. Decoupled from `needsSchema`:
90
+ * a case can have needs whose schema isn't projectable.
91
+ */
92
+ hasNeeds?: boolean;
93
+ /**
94
+ * v10 attachment-model — JSON-safe `needs` schema. May be undefined
95
+ * even when `hasNeeds` is true (e.g. opaque custom validator). Use
96
+ * `hasNeeds` for inventory/rawBypass decisions; this field is a
97
+ * decoration for consumers that want the schema shape when available.
98
+ */
99
+ needsSchema?: unknown;
59
100
  }
60
101
  /**
61
102
  * Protocol-agnostic contract metadata. Mirrors `ExtractedContractProjection`.
@@ -123,15 +164,98 @@ export interface NormalizedFlowMeta {
123
164
  setupDynamic?: true;
124
165
  steps: NormalizedFlowStep[];
125
166
  }
126
- /** Result of extracting contracts/flows from one or more files. */
167
+ /**
168
+ * Default execution mode for a contract case — runs the case directly with
169
+ * an explicit input (or void when no needs declared). Lives in
170
+ * `attachments[]` until a bootstrap overlay replaces it.
171
+ */
172
+ export interface NormalizedRawAttachment {
173
+ kind: "raw";
174
+ /** Contract case test id (`${contractId}.${caseKey}`). */
175
+ testId: string;
176
+ contractId: string;
177
+ caseKey: string;
178
+ /** Export name of the contract this case belongs to. */
179
+ exportName: string;
180
+ /** Runnability gates declared by the case. */
181
+ runnability?: NormalizedRunnability;
182
+ }
183
+ /**
184
+ * Bootstrap overlay attachment — `contract.bootstrap(ref, spec)` registers
185
+ * a setup-and-cleanup that wraps the case. Replaces the raw entry for the
186
+ * target testId. Body of `run`/`spec` is opaque per attachment-model §4.2.
187
+ *
188
+ * `paramsSchema` is undefined in v0 — structured-form param schema
189
+ * extraction needs the bootstrap registry, deferred to Spike 3.
190
+ *
191
+ * `rawBypass` exposes the explicit-input execution path (§5.1) so the
192
+ * inventory is the single authoritative source for both modes per testId.
193
+ */
194
+ export interface NormalizedBootstrapOverlayAttachment {
195
+ kind: "bootstrap-overlay";
196
+ testId: string;
197
+ /** The overlay's own export name (NOT the contract's). */
198
+ exportName: string;
199
+ targetRef: {
200
+ contractId: string;
201
+ caseKey: string;
202
+ };
203
+ bootstrap: {
204
+ /** v0: undefined; populated when structured-form `params` schema lands. */
205
+ paramsSchema?: unknown;
206
+ };
207
+ /** Present iff the target case has `needs` (explicit-input bypass available). */
208
+ rawBypass?: {
209
+ available: true;
210
+ /** JSON-safe form of the case's `needs` schema (may be undefined when SDK can't convert). */
211
+ needsSchema: unknown;
212
+ };
213
+ }
214
+ /**
215
+ * Flow attachment — orchestrates a sequence of contract calls under a
216
+ * single test id. Distinct from raw / overlay (no underlying case).
217
+ */
218
+ export interface NormalizedFlowAttachment {
219
+ kind: "flow";
220
+ /** Flow test id (`flowId`). */
221
+ testId: string;
222
+ exportName: string;
223
+ flow: NormalizedFlowMeta;
224
+ }
225
+ /** Discriminated attachment entry. One per testId in the inventory. */
226
+ export type NormalizedAttachmentMeta = NormalizedRawAttachment | NormalizedBootstrapOverlayAttachment | NormalizedFlowAttachment;
227
+ /** Result of extracting contracts/attachments from one or more files. */
127
228
  export interface ExtractionResult {
128
229
  contracts: NormalizedContractMeta[];
129
- flows?: NormalizedFlowMeta[];
230
+ /**
231
+ * Attachment inventory per attachment-model §7.3. Always present
232
+ * (may be empty). Replaces the previous `flows?: NormalizedFlowMeta[]`
233
+ * top-level field — flows now appear here as `kind: "flow"` entries.
234
+ */
235
+ attachments: NormalizedAttachmentMeta[];
130
236
  errors: Array<{
131
237
  file: string;
132
238
  error: string;
133
239
  }>;
134
240
  }
241
+ /**
242
+ * Check if a value looks like a BootstrapAttachment (v10 attachment model).
243
+ *
244
+ * BootstrapAttachment is the runtime marker returned by `contract.bootstrap()`.
245
+ * Carries `__glubean_type: "bootstrap-attachment"` and `testId`
246
+ * (= `${contractId}.${caseKey}`). Scanner identifies attachments via this
247
+ * marker without importing @glubean/sdk types.
248
+ *
249
+ * v0 attachment projection only surfaces metadata (kind, testId,
250
+ * contractId, caseKey). Bootstrap params schema (structured-form `params`)
251
+ * is not yet exposed on the export object — extracting it requires reading
252
+ * the bootstrap registry, deferred to Spike 3 when the runner input
253
+ * channel ships.
254
+ */
255
+ export declare function isBootstrapAttachment(val: unknown): val is {
256
+ __glubean_type: "bootstrap-attachment";
257
+ testId: string;
258
+ };
135
259
  /**
136
260
  * Check if a value looks like a ProtocolContract.
137
261
  * ProtocolContract extends Array<Test> and has `_projection` with protocol + target.
@@ -204,14 +328,87 @@ export declare function protocolContractToNormalized(value: {
204
328
  _extracted: any;
205
329
  }, exportName: string): NormalizedContractMeta;
206
330
  /**
207
- * Extract contracts + flows from a single file by dynamic import.
331
+ * Bootstrap overlay marker collected during file walk pre-synthesis raw
332
+ * material. Not yet a final attachment entry; `synthesizeAttachments`
333
+ * decides whether to keep it (replaces a raw entry) or report duplicate.
334
+ */
335
+ export interface BootstrapOverlayMarker {
336
+ exportName: string;
337
+ testId: string;
338
+ contractId: string;
339
+ caseKey: string;
340
+ }
341
+ /**
342
+ * Convert a BootstrapAttachment runtime export to a marker. Splits
343
+ * `testId` into contractId + caseKey at the LAST dot (contractId can have
344
+ * dots, e.g. `v2.orders.create.success`). Returns null for malformed shapes
345
+ * (no dot, leading dot, trailing dot) — caller skips them.
346
+ */
347
+ export declare function bootstrapAttachmentToNormalized(attachment: {
348
+ testId: string;
349
+ }, exportName: string): BootstrapOverlayMarker | null;
350
+ /**
351
+ * Synthesize the §7.3 attachment inventory from raw materials.
352
+ *
353
+ * Algorithm (per attachment-model §7.3):
354
+ * 1. Seed: each contract case → `kind: "raw"` entry (testId = `${id}.${key}`).
355
+ * 2. Apply overlays: each marker → REPLACE the raw entry with the same
356
+ * testId by a `kind: "bootstrap-overlay"` entry. Carry `rawBypass`
357
+ * when the target case has `needsSchema` (explicit-input bypass
358
+ * available per §5.1).
359
+ * 3. Duplicate detection: two markers with the same testId → push a
360
+ * load-time error. Keep the first; subsequent overlays ignored.
361
+ * 4. Append flows as `kind: "flow"` entries.
362
+ *
363
+ * Overlays whose testId doesn't match any contract case stand alone —
364
+ * we still emit them as `bootstrap-overlay` (no `rawBypass`, no targetRef
365
+ * verification). This keeps cross-file overlay registration discoverable
366
+ * even when the contract module hasn't been scanned yet.
367
+ */
368
+ export declare function synthesizeAttachments(contracts: NormalizedContractMeta[], flows: NormalizedFlowMeta[], markers: BootstrapOverlayMarker[]): {
369
+ attachments: NormalizedAttachmentMeta[];
370
+ errors: ExtractionResult["errors"];
371
+ };
372
+ /**
373
+ * Extract contracts + attachments from a single file by dynamic import.
208
374
  * One file's import failure does not block others.
375
+ *
376
+ * Per-file synthesis: only this file's contracts/flows/markers are seen.
377
+ * Cross-file overlay replacement (overlay in fileA targets case in fileB)
378
+ * only resolves at project level via `extractContractsFromProject`.
209
379
  */
210
380
  export declare function extractContractFromFile(filePath: string): Promise<ExtractionResult>;
211
381
  /**
212
- * Extract contracts + flows from all recognized files in a project.
382
+ * Extract contracts + attachments from all recognized files in a project.
383
+ *
384
+ * Project-level synthesis: collects raw materials (contracts, flows,
385
+ * overlay markers) across all `.contract.` / `.flow.` / `.bootstrap.`
386
+ * files, then runs `synthesizeAttachments` once. This is what enables
387
+ * cross-file overlay replacement (overlay in `signup.bootstrap.ts`
388
+ * replacing the raw entry for a case in `signup.contract.ts`) and
389
+ * project-wide duplicate-overlay detection per §7.3.
213
390
  */
214
391
  export declare function extractContractsFromProject(dir: string): Promise<ExtractionResult>;
392
+ /**
393
+ * Eagerly import every `*.bootstrap.{ts,js,mjs}` file under `dir` so
394
+ * `contract.bootstrap()` calls execute during module evaluation and
395
+ * register their overlays in the SDK's bootstrap registry.
396
+ *
397
+ * Idempotent: subsequent calls re-import the same module URLs; Node's
398
+ * ESM module cache short-circuits unless the file's mtime changed (the
399
+ * same cache-busting strategy `collectRawMaterials` uses).
400
+ *
401
+ * Errors: per-file failures are returned; the caller decides whether
402
+ * to abort or continue. We return errors instead of throwing because a
403
+ * single broken bootstrap file shouldn't kill an otherwise-working run.
404
+ */
405
+ export declare function loadProjectOverlays(dir: string): Promise<{
406
+ loaded: string[];
407
+ errors: Array<{
408
+ file: string;
409
+ error: string;
410
+ }>;
411
+ }>;
215
412
  /** @deprecated Removed in v0.2 — HTTP now goes through isProtocolContract. */
216
413
  export declare function isHttpContract(_val: unknown): never;
217
414
  //# sourceMappingURL=contract-extraction.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"contract-extraction.d.ts","sourceRoot":"","sources":["../src/contract-extraction.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"contract-extraction.d.ts","sourceRoot":"","sources":["../src/contract-extraction.ts"],"names":[],"mappings":"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"}
@@ -21,6 +21,26 @@ import { readdirSync, statSync } from "node:fs";
21
21
  // =============================================================================
22
22
  // Duck typing
23
23
  // =============================================================================
24
+ /**
25
+ * Check if a value looks like a BootstrapAttachment (v10 attachment model).
26
+ *
27
+ * BootstrapAttachment is the runtime marker returned by `contract.bootstrap()`.
28
+ * Carries `__glubean_type: "bootstrap-attachment"` and `testId`
29
+ * (= `${contractId}.${caseKey}`). Scanner identifies attachments via this
30
+ * marker without importing @glubean/sdk types.
31
+ *
32
+ * v0 attachment projection only surfaces metadata (kind, testId,
33
+ * contractId, caseKey). Bootstrap params schema (structured-form `params`)
34
+ * is not yet exposed on the export object — extracting it requires reading
35
+ * the bootstrap registry, deferred to Spike 3 when the runner input
36
+ * channel ships.
37
+ */
38
+ export function isBootstrapAttachment(val) {
39
+ return (typeof val === "object" &&
40
+ val !== null &&
41
+ val.__glubean_type === "bootstrap-attachment" &&
42
+ typeof val.testId === "string");
43
+ }
24
44
  /**
25
45
  * Check if a value looks like a ProtocolContract.
26
46
  * ProtocolContract extends Array<Test> and has `_projection` with protocol + target.
@@ -90,6 +110,12 @@ export function protocolContractToNormalized(value, exportName) {
90
110
  extensions: c.extensions,
91
111
  schemas: c.schemas,
92
112
  meta: c.meta,
113
+ given: c.given,
114
+ hasVerify: c.hasVerify,
115
+ verifyRules: c.verifyRules,
116
+ runnability: c.runnability,
117
+ hasNeeds: c.hasNeeds,
118
+ needsSchema: c.needsSchema,
93
119
  })),
94
120
  };
95
121
  }
@@ -180,20 +206,142 @@ function autoBuildFlowBuilder(val) {
180
206
  }
181
207
  return val;
182
208
  }
183
- // =============================================================================
184
- // File-level extraction
185
- // =============================================================================
186
- // Per-path mtime cache used by extractContractFromFile() to decide when
187
- // to bust Node's ESM module cache on re-import. See the comment inside
188
- // that function for the full reasoning.
209
+ /**
210
+ * Convert a BootstrapAttachment runtime export to a marker. Splits
211
+ * `testId` into contractId + caseKey at the LAST dot (contractId can have
212
+ * dots, e.g. `v2.orders.create.success`). Returns null for malformed shapes
213
+ * (no dot, leading dot, trailing dot) caller skips them.
214
+ */
215
+ export function bootstrapAttachmentToNormalized(attachment, exportName) {
216
+ const dotIndex = attachment.testId.lastIndexOf(".");
217
+ if (dotIndex <= 0 || dotIndex === attachment.testId.length - 1) {
218
+ return null;
219
+ }
220
+ const contractId = attachment.testId.slice(0, dotIndex);
221
+ const caseKey = attachment.testId.slice(dotIndex + 1);
222
+ return {
223
+ exportName,
224
+ testId: attachment.testId,
225
+ contractId,
226
+ caseKey,
227
+ };
228
+ }
229
+ function normalizeRunnability(runnability) {
230
+ if (!runnability)
231
+ return undefined;
232
+ const normalized = {};
233
+ if (runnability.requireAttachment !== undefined) {
234
+ normalized.requireAttachment = runnability.requireAttachment;
235
+ }
236
+ if (runnability.requireSession !== undefined) {
237
+ normalized.requireSession = runnability.requireSession;
238
+ }
239
+ return Object.keys(normalized).length > 0 ? normalized : undefined;
240
+ }
241
+ /**
242
+ * Synthesize the §7.3 attachment inventory from raw materials.
243
+ *
244
+ * Algorithm (per attachment-model §7.3):
245
+ * 1. Seed: each contract case → `kind: "raw"` entry (testId = `${id}.${key}`).
246
+ * 2. Apply overlays: each marker → REPLACE the raw entry with the same
247
+ * testId by a `kind: "bootstrap-overlay"` entry. Carry `rawBypass`
248
+ * when the target case has `needsSchema` (explicit-input bypass
249
+ * available per §5.1).
250
+ * 3. Duplicate detection: two markers with the same testId → push a
251
+ * load-time error. Keep the first; subsequent overlays ignored.
252
+ * 4. Append flows as `kind: "flow"` entries.
253
+ *
254
+ * Overlays whose testId doesn't match any contract case stand alone —
255
+ * we still emit them as `bootstrap-overlay` (no `rawBypass`, no targetRef
256
+ * verification). This keeps cross-file overlay registration discoverable
257
+ * even when the contract module hasn't been scanned yet.
258
+ */
259
+ export function synthesizeAttachments(contracts, flows, markers) {
260
+ const errors = [];
261
+ // Index cases by testId for O(1) lookup during overlay application.
262
+ const caseByTestId = new Map();
263
+ for (const contract of contracts) {
264
+ for (const c of contract.cases) {
265
+ caseByTestId.set(`${contract.id}.${c.key}`, { contract, case: c });
266
+ }
267
+ }
268
+ // Step 1: seed raw entries. Reads `runnability` directly from the case
269
+ // projection (post-Phase-2f-fix: the adapter now threads it as a
270
+ // first-class field, no longer hidden under `extensions`).
271
+ const byTestId = new Map();
272
+ for (const [testId, { contract, case: c }] of caseByTestId) {
273
+ const runnability = normalizeRunnability(c.runnability);
274
+ byTestId.set(testId, {
275
+ kind: "raw",
276
+ testId,
277
+ contractId: contract.id,
278
+ caseKey: c.key,
279
+ exportName: contract.exportName,
280
+ ...(runnability ? { runnability } : {}),
281
+ });
282
+ }
283
+ // Step 2 + 3: apply overlays, detect duplicates.
284
+ const seenOverlayIds = new Set();
285
+ for (const marker of markers) {
286
+ if (seenOverlayIds.has(marker.testId)) {
287
+ errors.push({
288
+ file: marker.exportName,
289
+ error: `Duplicate bootstrap overlay for testId "${marker.testId}" (export "${marker.exportName}"). Per attachment-model §7.3 testId uniqueness is enforced.`,
290
+ });
291
+ continue;
292
+ }
293
+ seenOverlayIds.add(marker.testId);
294
+ // rawBypass is available iff the target case declared `needs`
295
+ // (hasNeeds === true). This is decoupled from needsSchema projection:
296
+ // a case with a custom safeParse-only validator still satisfies
297
+ // "explicit input can run the raw case" even when the JSON Schema
298
+ // projection is null. If the schema projected, we decorate the
299
+ // bypass slot with it; otherwise bypass is still advertised.
300
+ const target = caseByTestId.get(marker.testId);
301
+ const overlay = {
302
+ kind: "bootstrap-overlay",
303
+ testId: marker.testId,
304
+ exportName: marker.exportName,
305
+ targetRef: { contractId: marker.contractId, caseKey: marker.caseKey },
306
+ bootstrap: {},
307
+ ...(target?.case.hasNeeds
308
+ ? {
309
+ rawBypass: {
310
+ available: true,
311
+ needsSchema: target.case.needsSchema,
312
+ },
313
+ }
314
+ : {}),
315
+ };
316
+ byTestId.set(marker.testId, overlay);
317
+ }
318
+ // Step 4: append flows.
319
+ const attachments = Array.from(byTestId.values());
320
+ for (const flow of flows) {
321
+ attachments.push({
322
+ kind: "flow",
323
+ testId: flow.id,
324
+ exportName: flow.exportName,
325
+ flow,
326
+ });
327
+ }
328
+ return { attachments, errors };
329
+ }
330
+ // Per-path mtime cache used by collectRawMaterials() to decide when to
331
+ // bust Node's ESM module cache on re-import. See the comment inside that
332
+ // function for the full reasoning.
189
333
  const _importMtimeCache = new Map();
190
334
  /**
191
- * Extract contracts + flows from a single file by dynamic import.
192
- * One file's import failure does not block others.
335
+ * Internal: dynamically import a file and collect raw materials
336
+ * (contracts / flows / overlay markers / errors) without synthesis.
337
+ * Both `extractContractFromFile` and `extractContractsFromProject` use
338
+ * this — the former synthesizes per-file, the latter synthesizes
339
+ * project-wide so cross-file overlay replacement and dedup work.
193
340
  */
194
- export async function extractContractFromFile(filePath) {
341
+ async function collectRawMaterials(filePath) {
195
342
  const contracts = [];
196
343
  const flows = [];
344
+ const markers = [];
197
345
  const errors = [];
198
346
  const absolutePath = resolve(filePath);
199
347
  try {
@@ -242,19 +390,47 @@ export async function extractContractFromFile(filePath) {
242
390
  else if (isProtocolContract(value)) {
243
391
  contracts.push(protocolContractToNormalized(value, exportName));
244
392
  }
393
+ else if (isBootstrapAttachment(value)) {
394
+ const marker = bootstrapAttachmentToNormalized(value, exportName);
395
+ if (marker)
396
+ markers.push(marker);
397
+ }
245
398
  }
246
399
  }
247
400
  catch (err) {
248
401
  const message = err instanceof Error ? err.message : String(err);
249
402
  errors.push({ file: absolutePath, error: message });
250
403
  }
251
- return { contracts, flows: flows.length > 0 ? flows : undefined, errors };
404
+ return { contracts, flows, markers, errors };
405
+ }
406
+ /**
407
+ * Extract contracts + attachments from a single file by dynamic import.
408
+ * One file's import failure does not block others.
409
+ *
410
+ * Per-file synthesis: only this file's contracts/flows/markers are seen.
411
+ * Cross-file overlay replacement (overlay in fileA targets case in fileB)
412
+ * only resolves at project level via `extractContractsFromProject`.
413
+ */
414
+ export async function extractContractFromFile(filePath) {
415
+ const raw = await collectRawMaterials(filePath);
416
+ const synth = synthesizeAttachments(raw.contracts, raw.flows, raw.markers);
417
+ return {
418
+ contracts: raw.contracts,
419
+ attachments: synth.attachments,
420
+ errors: [...raw.errors, ...synth.errors],
421
+ };
252
422
  }
253
423
  // =============================================================================
254
424
  // Project-level extraction
255
425
  // =============================================================================
256
426
  /**
257
- * Find all .contract.{ts,js,mjs} and .flow.{ts,js,mjs} files in a directory tree.
427
+ * Find all .contract.{ts,js,mjs}, .flow.{ts,js,mjs}, and
428
+ * .bootstrap.{ts,js,mjs} files in a directory tree.
429
+ *
430
+ * v10 attachment model §7.4 mandates eager loading of `.bootstrap.` files
431
+ * so overlay registrations fire during module evaluation. Without this,
432
+ * filtered runs (CLI / MCP) could miss overlay registrations and silently
433
+ * fall through to the no-overlay path.
258
434
  */
259
435
  function findContractAndFlowFiles(dir) {
260
436
  const files = [];
@@ -267,7 +443,9 @@ function findContractAndFlowFiles(dir) {
267
443
  walk(full);
268
444
  else {
269
445
  const base = basename(entry);
270
- if (base.includes(".contract.") || base.includes(".flow.")) {
446
+ if (base.includes(".contract.") ||
447
+ base.includes(".flow.") ||
448
+ base.includes(".bootstrap.")) {
271
449
  files.push(full);
272
450
  }
273
451
  }
@@ -277,29 +455,95 @@ function findContractAndFlowFiles(dir) {
277
455
  return files;
278
456
  }
279
457
  /**
280
- * Extract contracts + flows from all recognized files in a project.
458
+ * Extract contracts + attachments from all recognized files in a project.
459
+ *
460
+ * Project-level synthesis: collects raw materials (contracts, flows,
461
+ * overlay markers) across all `.contract.` / `.flow.` / `.bootstrap.`
462
+ * files, then runs `synthesizeAttachments` once. This is what enables
463
+ * cross-file overlay replacement (overlay in `signup.bootstrap.ts`
464
+ * replacing the raw entry for a case in `signup.contract.ts`) and
465
+ * project-wide duplicate-overlay detection per §7.3.
281
466
  */
282
467
  export async function extractContractsFromProject(dir) {
283
468
  const files = findContractAndFlowFiles(dir);
284
- if (files.length === 0)
285
- return { contracts: [], errors: [] };
469
+ if (files.length === 0) {
470
+ return { contracts: [], attachments: [], errors: [] };
471
+ }
286
472
  const allContracts = [];
287
473
  const allFlows = [];
474
+ const allMarkers = [];
288
475
  const allErrors = [];
289
476
  for (const filePath of files) {
290
- const { contracts, flows, errors } = await extractContractFromFile(filePath);
291
- allContracts.push(...contracts);
292
- if (flows)
293
- allFlows.push(...flows);
294
- allErrors.push(...errors);
477
+ const raw = await collectRawMaterials(filePath);
478
+ allContracts.push(...raw.contracts);
479
+ allFlows.push(...raw.flows);
480
+ allMarkers.push(...raw.markers);
481
+ allErrors.push(...raw.errors);
295
482
  }
483
+ const synth = synthesizeAttachments(allContracts, allFlows, allMarkers);
296
484
  return {
297
485
  contracts: allContracts,
298
- flows: allFlows.length > 0 ? allFlows : undefined,
299
- errors: allErrors,
486
+ attachments: synth.attachments,
487
+ errors: [...allErrors, ...synth.errors],
300
488
  };
301
489
  }
302
490
  // =============================================================================
491
+ // Eager overlay loading — attachment-model §7.4
492
+ //
493
+ // "Runner MUST eagerly load all *.contract.ts and *.bootstrap.ts files in
494
+ // the contracts root before resolving attachments. Overlay registration is
495
+ // a semantic input to runnable inventory; making it depend on import path
496
+ // leads to nondeterministic runs."
497
+ //
498
+ // CLI/MCP/ProjectRunner call this before any test runs so a filtered run
499
+ // (e.g. `glubean run path/to/single.contract.ts`) still picks up sibling
500
+ // `*.bootstrap.ts` overlay registrations.
501
+ // =============================================================================
502
+ /**
503
+ * Eagerly import every `*.bootstrap.{ts,js,mjs}` file under `dir` so
504
+ * `contract.bootstrap()` calls execute during module evaluation and
505
+ * register their overlays in the SDK's bootstrap registry.
506
+ *
507
+ * Idempotent: subsequent calls re-import the same module URLs; Node's
508
+ * ESM module cache short-circuits unless the file's mtime changed (the
509
+ * same cache-busting strategy `collectRawMaterials` uses).
510
+ *
511
+ * Errors: per-file failures are returned; the caller decides whether
512
+ * to abort or continue. We return errors instead of throwing because a
513
+ * single broken bootstrap file shouldn't kill an otherwise-working run.
514
+ */
515
+ export async function loadProjectOverlays(dir) {
516
+ const allFiles = findContractAndFlowFiles(dir);
517
+ const bootstrapFiles = allFiles.filter((f) => basename(f).includes(".bootstrap."));
518
+ const loaded = [];
519
+ const errors = [];
520
+ for (const filePath of bootstrapFiles) {
521
+ const absolutePath = resolve(filePath);
522
+ try {
523
+ let mtimeKey = 0;
524
+ try {
525
+ mtimeKey = statSync(absolutePath).mtimeMs;
526
+ }
527
+ catch {
528
+ // fall through with mtimeKey=0
529
+ }
530
+ const baseUrl = pathToFileURL(absolutePath).href;
531
+ const lastSeen = _importMtimeCache.get(absolutePath);
532
+ const importUrl = lastSeen === undefined || lastSeen === mtimeKey
533
+ ? baseUrl
534
+ : `${baseUrl}?t=${mtimeKey}`;
535
+ _importMtimeCache.set(absolutePath, mtimeKey);
536
+ await import(importUrl);
537
+ loaded.push(absolutePath);
538
+ }
539
+ catch (err) {
540
+ const message = err instanceof Error ? err.message : String(err);
541
+ errors.push({ file: absolutePath, error: message });
542
+ }
543
+ }
544
+ return { loaded, errors };
545
+ }
546
+ // =============================================================================
303
547
  // Backward-compat: isHttpContract — removed permanently in v0.2.
304
548
  // =============================================================================
305
549
  /** @deprecated Removed in v0.2 — HTTP now goes through isProtocolContract. */