@glubean/sdk 0.1.39 → 0.2.1

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 (78) hide show
  1. package/dist/configure.d.ts +4 -21
  2. package/dist/configure.d.ts.map +1 -1
  3. package/dist/configure.js +19 -145
  4. package/dist/configure.js.map +1 -1
  5. package/dist/contract-core.d.ts +98 -0
  6. package/dist/contract-core.d.ts.map +1 -0
  7. package/dist/contract-core.js +749 -0
  8. package/dist/contract-core.js.map +1 -0
  9. package/dist/contract-http/adapter.d.ts +52 -0
  10. package/dist/contract-http/adapter.d.ts.map +1 -0
  11. package/dist/contract-http/adapter.js +650 -0
  12. package/dist/contract-http/adapter.js.map +1 -0
  13. package/dist/contract-http/factory.d.ts +32 -0
  14. package/dist/contract-http/factory.d.ts.map +1 -0
  15. package/dist/contract-http/factory.js +83 -0
  16. package/dist/contract-http/factory.js.map +1 -0
  17. package/dist/contract-http/flow-helpers.d.ts +12 -0
  18. package/dist/contract-http/flow-helpers.d.ts.map +1 -0
  19. package/dist/contract-http/flow-helpers.js +34 -0
  20. package/dist/contract-http/flow-helpers.js.map +1 -0
  21. package/dist/contract-http/index.d.ts +16 -0
  22. package/dist/contract-http/index.d.ts.map +1 -0
  23. package/dist/contract-http/index.js +15 -0
  24. package/dist/contract-http/index.js.map +1 -0
  25. package/dist/contract-http/markdown.d.ts +10 -0
  26. package/dist/contract-http/markdown.d.ts.map +1 -0
  27. package/dist/contract-http/markdown.js +21 -0
  28. package/dist/contract-http/markdown.js.map +1 -0
  29. package/dist/contract-http/openapi.d.ts +15 -0
  30. package/dist/contract-http/openapi.d.ts.map +1 -0
  31. package/dist/contract-http/openapi.js +38 -0
  32. package/dist/contract-http/openapi.js.map +1 -0
  33. package/dist/contract-http/types.d.ts +252 -0
  34. package/dist/contract-http/types.d.ts.map +1 -0
  35. package/dist/contract-http/types.js +13 -0
  36. package/dist/contract-http/types.js.map +1 -0
  37. package/dist/contract-types.d.ts +420 -467
  38. package/dist/contract-types.d.ts.map +1 -1
  39. package/dist/contract-types.js +16 -4
  40. package/dist/contract-types.js.map +1 -1
  41. package/dist/each-builder.d.ts +244 -0
  42. package/dist/each-builder.d.ts.map +1 -0
  43. package/dist/each-builder.js +268 -0
  44. package/dist/each-builder.js.map +1 -0
  45. package/dist/index.d.ts +30 -513
  46. package/dist/index.d.ts.map +1 -1
  47. package/dist/index.js +33 -826
  48. package/dist/index.js.map +1 -1
  49. package/dist/internal.d.ts +1 -0
  50. package/dist/internal.d.ts.map +1 -1
  51. package/dist/internal.js +1 -0
  52. package/dist/internal.js.map +1 -1
  53. package/dist/runtime-carrier.d.ts +142 -0
  54. package/dist/runtime-carrier.d.ts.map +1 -0
  55. package/dist/runtime-carrier.js +148 -0
  56. package/dist/runtime-carrier.js.map +1 -0
  57. package/dist/session.d.ts.map +1 -1
  58. package/dist/session.js +2 -1
  59. package/dist/session.js.map +1 -1
  60. package/dist/test-builder.d.ts +249 -0
  61. package/dist/test-builder.d.ts.map +1 -0
  62. package/dist/test-builder.js +265 -0
  63. package/dist/test-builder.js.map +1 -0
  64. package/dist/test-extend.d.ts +59 -0
  65. package/dist/test-extend.d.ts.map +1 -0
  66. package/dist/test-extend.js +111 -0
  67. package/dist/test-extend.js.map +1 -0
  68. package/dist/test-utils.d.ts +39 -0
  69. package/dist/test-utils.d.ts.map +1 -0
  70. package/dist/test-utils.js +91 -0
  71. package/dist/test-utils.js.map +1 -0
  72. package/dist/types.d.ts +41 -122
  73. package/dist/types.d.ts.map +1 -1
  74. package/package.json +1 -1
  75. package/dist/contract.d.ts +0 -64
  76. package/dist/contract.d.ts.map +0 -1
  77. package/dist/contract.js +0 -793
  78. package/dist/contract.js.map +0 -1
@@ -0,0 +1,749 @@
1
+ /**
2
+ * Protocol-agnostic contract core.
3
+ *
4
+ * Provides:
5
+ * - Adapter registry (`_adapters`)
6
+ * - `contract.register(protocol, adapter)` — plugin extension point
7
+ * - `contract[protocol](id, spec)` dispatcher — validates 1:1 case keys,
8
+ * invokes adapter.execute per case, registers tests + ContractRegistryMeta
9
+ * - `contract.flow(id)` — protocol-agnostic FlowBuilder
10
+ * - `runFlow(flow, ctx)` — core flow execution helper (Rule 1/2 teardown)
11
+ * - `normalizeFlow(runtime)` — Runtime → ExtractedFlowProjection
12
+ * - Permissive Proxy tracer for `compute` nodes (best-effort reads/writes)
13
+ *
14
+ * HTTP is NOT handled here — it registers itself in `./contract-http/`.
15
+ * This file has zero HTTP-specific code.
16
+ *
17
+ * See:
18
+ * - `internal/40-discovery/proposals/contract-generics-complete.md` v5
19
+ * - `internal/40-discovery/proposals/contract-flow.md` v9
20
+ */
21
+ import { registerTest } from "./internal.js";
22
+ // =============================================================================
23
+ // Adapter registry
24
+ // =============================================================================
25
+ const _adapters = new Map();
26
+ /** Internal accessor for plugins / downstream (scanner, runner). */
27
+ export function getAdapter(protocol) {
28
+ return _adapters.get(protocol);
29
+ }
30
+ // =============================================================================
31
+ // contract.register + dispatcher
32
+ // =============================================================================
33
+ const RESERVED_PROTOCOL_NAMES = new Set([
34
+ "register",
35
+ "flow",
36
+ "getAdapter",
37
+ ]);
38
+ /**
39
+ * Register an adapter. Called by built-in HTTP adapter on SDK load, and by
40
+ * external protocol packages (`@glubean/grpc`, `@glubean/graphql` etc.) on
41
+ * their import — single-package model, each protocol package owns both
42
+ * transport + contract adapter.
43
+ */
44
+ function register(protocol, adapter) {
45
+ if (RESERVED_PROTOCOL_NAMES.has(protocol)) {
46
+ throw new Error(`Cannot register reserved protocol name "${protocol}"`);
47
+ }
48
+ _adapters.set(protocol, adapter);
49
+ // Attach contract[protocol] dispatcher dynamically.
50
+ contract[protocol] = (id, spec) => {
51
+ return dispatchContract(protocol, adapter, id, spec);
52
+ };
53
+ }
54
+ /**
55
+ * Generic contract dispatcher — shared across all protocols.
56
+ * Called by `contract[protocol](id, spec)` above.
57
+ */
58
+ function dispatchContract(protocol, adapter, id, spec) {
59
+ const rawProjection = adapter.project(spec);
60
+ // Registered protocol name is source of truth — adapters may report a
61
+ // canonical name that drifts from the registration name.
62
+ const projection = {
63
+ ...rawProjection,
64
+ protocol,
65
+ };
66
+ // 1:1 key invariant between spec.cases and projection.cases
67
+ validateCaseKeys(protocol, spec.cases ?? {}, projection.cases);
68
+ const cases = spec.cases ?? {};
69
+ const contractTagsRaw = spec.tags;
70
+ const contractTags = Array.isArray(contractTagsRaw)
71
+ ? contractTagsRaw
72
+ : contractTagsRaw
73
+ ? [contractTagsRaw]
74
+ : [];
75
+ const projCaseMap = new Map(projection.cases.map((c) => [c.key, c]));
76
+ const tests = Object.entries(cases).map(([caseKey, caseSpec]) => {
77
+ const testId = `${id}.${caseKey}`;
78
+ const testName = `${id} — ${caseKey}`;
79
+ const projCase = projCaseMap.get(caseKey);
80
+ const caseTags = caseSpec.tags ?? [];
81
+ const allTags = [...contractTags, ...caseTags];
82
+ // Projection lifecycle/severity are authoritative
83
+ const requires = projCase.requires ?? caseSpec.requires ?? "headless";
84
+ const defaultRun = projCase.defaultRun ?? caseSpec.defaultRun ??
85
+ (requires !== "headless" ? "opt-in" : "always");
86
+ const runtimeTags = [];
87
+ if (requires !== "headless")
88
+ runtimeTags.push(`requires:${requires}`);
89
+ if (defaultRun === "opt-in")
90
+ runtimeTags.push("default-run:opt-in");
91
+ const finalTags = [...allTags, ...runtimeTags];
92
+ const skipDeprecated = projCase.lifecycle === "deprecated"
93
+ ? `deprecated: ${projCase.deprecatedReason ?? caseSpec.deprecated ?? "deprecated"}`
94
+ : caseSpec.deprecated
95
+ ? `deprecated: ${caseSpec.deprecated}`
96
+ : undefined;
97
+ const skipDeferred = projCase.lifecycle === "deferred"
98
+ ? (projCase.deferredReason ?? caseSpec.deferred ?? "deferred")
99
+ : caseSpec.deferred;
100
+ const testDef = {
101
+ meta: {
102
+ id: testId,
103
+ name: testName,
104
+ description: projCase.description,
105
+ tags: finalTags.length > 0 ? finalTags : undefined,
106
+ deferred: skipDeferred,
107
+ deprecated: skipDeprecated
108
+ ? (projCase.deprecatedReason ?? caseSpec.deprecated ?? "deprecated")
109
+ : undefined,
110
+ requires,
111
+ defaultRun,
112
+ },
113
+ type: "simple",
114
+ fn: async (ctx) => {
115
+ if (skipDeprecated)
116
+ ctx.skip(skipDeprecated);
117
+ if (skipDeferred)
118
+ ctx.skip(skipDeferred);
119
+ await adapter.execute(ctx, caseSpec, spec);
120
+ },
121
+ };
122
+ // Build ContractRegistryMeta
123
+ const payloadSummary = adapter.describePayload
124
+ ? (() => {
125
+ // describePayload expects SafeSchemas — we don't have safe form yet,
126
+ // so pass projCase.schemas as a best-effort (adapters using this in
127
+ // runtime meta should be tolerant of live objects).
128
+ try {
129
+ return adapter.describePayload((projCase.schemas ?? undefined));
130
+ }
131
+ catch {
132
+ return undefined;
133
+ }
134
+ })()
135
+ : undefined;
136
+ const registryMeta = {
137
+ target: projection.target,
138
+ protocol: projection.protocol,
139
+ caseKey,
140
+ lifecycle: projCase.lifecycle,
141
+ severity: projCase.severity,
142
+ instanceName: projection.instanceName,
143
+ payloadSummary,
144
+ meta: projCase.meta,
145
+ };
146
+ registerTest({
147
+ id: testId,
148
+ name: testName,
149
+ type: "simple",
150
+ tags: finalTags.length > 0 ? finalTags : undefined,
151
+ description: projCase.description,
152
+ groupId: id,
153
+ requires,
154
+ defaultRun,
155
+ contract: registryMeta,
156
+ });
157
+ return testDef;
158
+ });
159
+ // Core injects id into both _projection and _spec carrier
160
+ const enrichedProjection = { ...projection, id };
161
+ const arr = [...tests];
162
+ const contractObj = Object.assign(arr, {
163
+ _projection: enrichedProjection,
164
+ _spec: spec,
165
+ case(key) {
166
+ // Delegate fail-fast validation to adapter (e.g. HTTP rejects
167
+ // function-valued body/params/query/headers).
168
+ adapter.validateCaseForFlow?.(spec, key, id);
169
+ return makeContractCaseRef(protocol, id, projection.target, key, contractObj, spec);
170
+ },
171
+ });
172
+ return contractObj;
173
+ }
174
+ function validateCaseKeys(protocol, specCases, projectionCases) {
175
+ // Duplicate check
176
+ const keys = projectionCases.map((c) => c.key);
177
+ const keySet = new Set(keys);
178
+ if (keySet.size !== keys.length) {
179
+ const dupes = keys.filter((k, i) => keys.indexOf(k) !== i);
180
+ throw new Error(`contract.register("${protocol}"): project() returned duplicate case key(s): ${[...new Set(dupes)].join(", ")}. ` +
181
+ `Each projected case key must be unique.`);
182
+ }
183
+ const specKeys = new Set(Object.keys(specCases));
184
+ for (const key of keySet) {
185
+ if (!specKeys.has(key)) {
186
+ throw new Error(`contract.register("${protocol}"): project() returned case "${key}" not present in spec.cases. ` +
187
+ `Projected cases must 1:1 match spec.cases keys.`);
188
+ }
189
+ }
190
+ for (const key of specKeys) {
191
+ if (!keySet.has(key)) {
192
+ throw new Error(`contract.register("${protocol}"): spec.cases has "${key}" but project() did not return it. ` +
193
+ `Projected cases must 1:1 match spec.cases keys.`);
194
+ }
195
+ }
196
+ }
197
+ /**
198
+ * Construct a ContractCaseRef. Adapters may extend this via module
199
+ * augmentation if they want stricter typing for their specific I/O shapes.
200
+ *
201
+ * This function performs the `function-valued field fail-fast` check
202
+ * required by contract-flow §5.1.1 — but since the check is
203
+ * protocol-specific (what counts as an "input slot" varies), adapters
204
+ * should enforce it in their own `.case()` wrapper if needed. For
205
+ * built-in HTTP adapter, see `./contract-http/adapter.ts`.
206
+ */
207
+ function makeContractCaseRef(protocol, contractId, target, caseKey, contract, _spec) {
208
+ return {
209
+ __glubean_type: "contract-case-ref",
210
+ contractId,
211
+ caseKey,
212
+ protocol,
213
+ target,
214
+ contract,
215
+ };
216
+ }
217
+ export const contract = {
218
+ register,
219
+ flow,
220
+ };
221
+ // =============================================================================
222
+ // Flow: FlowBuilder + runFlow + normalizeFlow + tracePureFn
223
+ // =============================================================================
224
+ /**
225
+ * Protocol-agnostic flow builder. See contract-flow.md v9 §4.1.
226
+ */
227
+ export function flow(idOrMeta) {
228
+ const meta = typeof idOrMeta === "string"
229
+ ? { id: idOrMeta }
230
+ : idOrMeta;
231
+ const steps = [];
232
+ let setupFn;
233
+ let teardownFn;
234
+ let built = false;
235
+ let extraMeta = {};
236
+ const builder = {
237
+ __glubean_type: "flow-builder",
238
+ meta(m) {
239
+ extraMeta = { ...extraMeta, ...m };
240
+ return builder;
241
+ },
242
+ setup(fn) {
243
+ setupFn = fn;
244
+ return builder;
245
+ },
246
+ teardown(fn) {
247
+ teardownFn = fn;
248
+ return builder;
249
+ },
250
+ step(ref, bindings) {
251
+ const adapter = _adapters.get(ref.protocol);
252
+ if (!adapter) {
253
+ throw new Error(`contract.flow(${JSON.stringify(meta.id)}).step: unknown protocol "${ref.protocol}". ` +
254
+ `Did you forget to import a contract plugin package (e.g. "@glubean/grpc")?`);
255
+ }
256
+ if (!adapter.executeCaseInFlow) {
257
+ throw new Error(`contract.flow(${JSON.stringify(meta.id)}).step: adapter for "${ref.protocol}" ` +
258
+ `does not implement executeCaseInFlow — this protocol cannot appear in a flow.`);
259
+ }
260
+ const step = {
261
+ kind: "contract-call",
262
+ name: bindings?.name,
263
+ ref,
264
+ caseKey: ref.caseKey,
265
+ contract: ref.contract,
266
+ bindings: bindings
267
+ ? { in: bindings.in, out: bindings.out }
268
+ : undefined,
269
+ };
270
+ steps.push(step);
271
+ return builder;
272
+ },
273
+ compute(fn) {
274
+ const step = {
275
+ kind: "compute",
276
+ fn: fn,
277
+ };
278
+ steps.push(step);
279
+ return builder;
280
+ },
281
+ build() {
282
+ return finalize();
283
+ },
284
+ };
285
+ // Auto-finalize via microtask (mirrors TestBuilder pattern)
286
+ queueMicrotask(() => {
287
+ if (!built)
288
+ finalize();
289
+ });
290
+ function finalize() {
291
+ if (built)
292
+ return builtResult;
293
+ built = true;
294
+ const runtime = {
295
+ protocol: "flow",
296
+ description: extraMeta.description,
297
+ tags: extraMeta.tags,
298
+ extensions: extraMeta.extensions,
299
+ setup: setupFn,
300
+ teardown: teardownFn,
301
+ steps,
302
+ };
303
+ // Build the single Test that runFlow orchestrates.
304
+ const flowTest = {
305
+ meta: {
306
+ id: meta.id,
307
+ name: extraMeta.name ?? meta.id,
308
+ tags: extraMeta.tags,
309
+ description: extraMeta.description,
310
+ // `FlowMeta.skip: string` (the reason) → `TestMeta.deferred: string`
311
+ // mirrors the ContractCase.deferred convention so downstream
312
+ // reporters render the skip reason consistently.
313
+ ...(extraMeta.skip !== undefined ? { deferred: extraMeta.skip } : {}),
314
+ ...(extraMeta.only !== undefined ? { only: extraMeta.only } : {}),
315
+ },
316
+ type: "simple",
317
+ fn: async (ctx) => {
318
+ // Belt-and-suspenders: runtime ctx.skip in case the runner didn't
319
+ // filter on meta.deferred (e.g. the user ran with an explicit
320
+ // target that bypasses skip filters).
321
+ if (extraMeta.skip)
322
+ ctx.skip(extraMeta.skip);
323
+ await runFlow(resultHandle, ctx);
324
+ },
325
+ };
326
+ const arr = [flowTest];
327
+ const runtimeWithId = { ...runtime, id: meta.id };
328
+ // Pre-compute extracted projection so downstream consumers (scanner,
329
+ // CLI, MCP, Cloud) get full field mappings + compute reads/writes
330
+ // without having to import the SDK to call normalizeFlow themselves.
331
+ const extracted = normalizeFlow(runtimeWithId);
332
+ const resultHandle = Object.assign(arr, {
333
+ _flow: runtimeWithId,
334
+ _extracted: extracted,
335
+ });
336
+ // Register flow in the registry
337
+ const flowRegistryMeta = {
338
+ id: extracted.id,
339
+ description: extracted.description,
340
+ tags: extracted.tags,
341
+ steps: extracted.steps.map(stepProjectionToRegistry),
342
+ setupDynamic: extracted.setupDynamic,
343
+ };
344
+ registerTest({
345
+ id: meta.id,
346
+ name: extraMeta.name ?? meta.id,
347
+ type: "simple",
348
+ tags: extraMeta.tags,
349
+ description: extraMeta.description,
350
+ flow: flowRegistryMeta,
351
+ });
352
+ builtResult = resultHandle;
353
+ return resultHandle;
354
+ }
355
+ let builtResult;
356
+ return builder;
357
+ }
358
+ function stepProjectionToRegistry(step) {
359
+ if (step.kind === "compute") {
360
+ return {
361
+ kind: "compute",
362
+ name: step.name,
363
+ reads: step.reads,
364
+ writes: step.writes,
365
+ };
366
+ }
367
+ return {
368
+ kind: "contract-call",
369
+ name: step.name,
370
+ contractId: step.contractId,
371
+ caseKey: step.caseKey,
372
+ protocol: step.protocol,
373
+ target: step.target,
374
+ inputs: step.inputs,
375
+ outputs: step.outputs,
376
+ };
377
+ }
378
+ /**
379
+ * Core flow execution helper. Implements the Rule 1 / Rule 2 teardown
380
+ * semantics from contract-flow.md §7.
381
+ */
382
+ export async function runFlow(flowContract, ctx) {
383
+ const runtime = flowContract._flow;
384
+ // If flow.setup throws → flow.teardown does NOT run (Rule 2).
385
+ let state = runtime.setup
386
+ ? await runtime.setup(ctx)
387
+ : undefined;
388
+ try {
389
+ for (const step of runtime.steps) {
390
+ if (step.kind === "compute") {
391
+ // Compute: synchronous pure function. Enforce both syntactic and
392
+ // value-level async rejection.
393
+ if (step.fn.constructor?.name === "AsyncFunction") {
394
+ throw new Error(`flow "${runtime.id}" compute step "${step.name ?? "<unnamed>"}": ` +
395
+ `async functions are not allowed — compute must be synchronous and I/O-free`);
396
+ }
397
+ const result = step.fn(state);
398
+ if (result && typeof result.then === "function") {
399
+ throw new Error(`flow "${runtime.id}" compute step "${step.name ?? "<unnamed>"}": ` +
400
+ `returned a thenable (Promise or Promise-like) — compute must not return async values. ` +
401
+ `If you need async initialization, use flow.setup() instead.`);
402
+ }
403
+ state = result;
404
+ continue;
405
+ }
406
+ // contract-call branch
407
+ const adapter = _adapters.get(step.ref.protocol);
408
+ if (!adapter) {
409
+ throw new Error(`flow "${runtime.id}" step "${step.name ?? step.caseKey}": ` +
410
+ `no registered adapter for protocol "${step.ref.protocol}". ` +
411
+ `Did you forget to import a contract plugin package?`);
412
+ }
413
+ if (!adapter.executeCaseInFlow) {
414
+ throw new Error(`flow "${runtime.id}" step "${step.name ?? step.caseKey}": ` +
415
+ `adapter for "${step.ref.protocol}" does not implement executeCaseInFlow`);
416
+ }
417
+ const resolvedInputs = step.bindings?.in?.(state);
418
+ const response = await adapter.executeCaseInFlow({
419
+ ctx,
420
+ contract: step.contract,
421
+ caseKey: step.caseKey,
422
+ resolvedInputs,
423
+ });
424
+ if (step.bindings?.out) {
425
+ state = step.bindings.out(state, response);
426
+ }
427
+ }
428
+ }
429
+ finally {
430
+ if (runtime.teardown) {
431
+ // flow.teardown runs in Rule 2 outer-finally. Its errors are logged
432
+ // but must not mask the primary exception.
433
+ try {
434
+ await runtime.teardown(ctx, state);
435
+ }
436
+ catch (teardownErr) {
437
+ ctx.log?.(`flow.teardown failed: ${String(teardownErr)}`);
438
+ }
439
+ }
440
+ }
441
+ }
442
+ // =============================================================================
443
+ // normalizeFlow + permissive Proxy tracer
444
+ // =============================================================================
445
+ /**
446
+ * Normalize a RuntimeFlowProjection to JSON-safe ExtractedFlowProjection.
447
+ * Runs Proxy dry-run of lens functions to extract FieldMappings.
448
+ *
449
+ * Lens purity is enforced here: if a `step.bindings.in` or `.out` lens
450
+ * violates purity (method call, `new`, etc.), `LensPurityError` is thrown
451
+ * with step context so the author sees which step needs fixing.
452
+ */
453
+ export function normalizeFlow(runtime) {
454
+ return {
455
+ id: runtime.id,
456
+ protocol: "flow",
457
+ description: runtime.description,
458
+ tags: runtime.tags,
459
+ extensions: runtime.extensions,
460
+ setupDynamic: runtime.setup ? true : undefined,
461
+ steps: runtime.steps.map((s, idx) => {
462
+ const stepLabel = s.name ??
463
+ (s.kind === "contract-call" ? `${s.contract._projection.id}#${s.caseKey}` : `step-${idx + 1}`);
464
+ if (s.kind === "compute") {
465
+ try {
466
+ const { reads, writes } = traceComputeFn(s.fn);
467
+ return { kind: "compute", name: s.name, reads, writes };
468
+ }
469
+ catch (err) {
470
+ // Wrap compute-tracer failures with the same step-context format
471
+ // used for lens errors, so authoring mistakes are equally easy
472
+ // to localize regardless of which tracer caught them.
473
+ throw new Error(`flow "${runtime.id}" step ${idx + 1} "${stepLabel}" (compute): ${err instanceof Error ? err.message : String(err)}`);
474
+ }
475
+ }
476
+ let inputs;
477
+ if (s.bindings?.in) {
478
+ try {
479
+ inputs = extractMappings(s.bindings.in);
480
+ }
481
+ catch (err) {
482
+ if (err instanceof LensPurityError) {
483
+ throw new Error(`flow "${runtime.id}" step ${idx + 1} "${stepLabel}" (in lens): ${err.message}`);
484
+ }
485
+ throw err;
486
+ }
487
+ }
488
+ let outputs;
489
+ if (s.bindings?.out) {
490
+ try {
491
+ outputs = extractMappingsOut(s.bindings.out);
492
+ }
493
+ catch (err) {
494
+ if (err instanceof LensPurityError) {
495
+ throw new Error(`flow "${runtime.id}" step ${idx + 1} "${stepLabel}" (out lens): ${err.message}`);
496
+ }
497
+ throw err;
498
+ }
499
+ }
500
+ return {
501
+ kind: "contract-call",
502
+ name: s.name,
503
+ contractId: s.contract._projection.id,
504
+ caseKey: s.caseKey,
505
+ protocol: s.ref.protocol,
506
+ target: s.ref.target,
507
+ inputs,
508
+ outputs,
509
+ };
510
+ }),
511
+ };
512
+ }
513
+ /**
514
+ * Run a pure lens `fn: (state) => output` with a tracing Proxy, extracting
515
+ * FieldMappings from state paths to output paths.
516
+ *
517
+ * Pure-lens enforcement: method calls, `new`, and coercion on the state
518
+ * proxy raise `LensPurityError`. Errors are **not** swallowed — the caller
519
+ * (typically `normalizeFlow`) wraps them with step context and re-throws,
520
+ * so authors see the failure at flow build time rather than silently
521
+ * losing projection data.
522
+ */
523
+ export function extractMappings(fn) {
524
+ const proxy = makeLensProxy("state");
525
+ const result = fn(proxy);
526
+ return collectMappings(result, []);
527
+ }
528
+ /**
529
+ * Extract FieldMappings from `out: (state, response) => newState`.
530
+ * Same purity contract as `extractMappings`.
531
+ */
532
+ export function extractMappingsOut(fn) {
533
+ const stateProxy = makeLensProxy("state");
534
+ const resProxy = makeLensProxy("response");
535
+ const result = fn(stateProxy, resProxy);
536
+ // outputs target is always state.X
537
+ return collectMappings(result, ["state"]);
538
+ }
539
+ /**
540
+ * Recursively descend into an object produced by a traced lens, collecting
541
+ * paths. Lens outputs are plain objects whose leaf values are TracedValue
542
+ * markers (recording their source path).
543
+ */
544
+ function collectMappings(out, targetPath) {
545
+ const mappings = [];
546
+ walk(out, targetPath);
547
+ return mappings;
548
+ function walk(val, path) {
549
+ if (isTracedValue(val)) {
550
+ mappings.push({
551
+ target: path.join("."),
552
+ source: { kind: "path", path: val[TRACE_MARKER] },
553
+ });
554
+ return;
555
+ }
556
+ if (val === undefined || val === null)
557
+ return;
558
+ if (typeof val === "object" && !Array.isArray(val)) {
559
+ for (const [k, v] of Object.entries(val)) {
560
+ walk(v, [...path, k]);
561
+ }
562
+ return;
563
+ }
564
+ if (Array.isArray(val)) {
565
+ val.forEach((v, i) => walk(v, [...path, String(i)]));
566
+ return;
567
+ }
568
+ // Primitive literal
569
+ mappings.push({
570
+ target: path.join("."),
571
+ source: { kind: "literal", value: val },
572
+ });
573
+ }
574
+ }
575
+ // --- Traced values (returned by lens proxy on property access) ---------------
576
+ //
577
+ // The trace marker is a Symbol (not a string key) so it is invisible to
578
+ // `Object.keys` / spread / JSON.stringify. Without this, `{ ...s }` in a lens
579
+ // would copy the tracer sentinel into the result and trip the traced-value
580
+ // check against the entire spread object.
581
+ const TRACE_MARKER = Symbol.for("@glubean/lens-trace");
582
+ function isTracedValue(v) {
583
+ // Lens proxy uses a callable function target (for apply-trap purity
584
+ // enforcement), so the proxy itself reports `typeof === "function"` in
585
+ // V8. Accept either object or function.
586
+ return ((typeof v === "object" || typeof v === "function") &&
587
+ v !== null &&
588
+ v[TRACE_MARKER] !== undefined);
589
+ }
590
+ /**
591
+ * Raised by the strict lens Proxy when a user lens fn attempts an operation
592
+ * that breaks the "pure field access + repack" contract. Caught and re-
593
+ * thrown with step context by `normalizeFlow`.
594
+ */
595
+ export class LensPurityError extends Error {
596
+ path;
597
+ operation;
598
+ constructor(path, operation) {
599
+ super(`Flow step in/out lens must be a pure select/repack function ` +
600
+ `(no method calls, branches, or arithmetic). ` +
601
+ `Illegal operation: "${operation}" on path "${path}". ` +
602
+ `If you need computation, move it to a .compute(s => ...) step between ` +
603
+ `this .step() and the next one.`);
604
+ this.name = "LensPurityError";
605
+ this.path = path;
606
+ this.operation = operation;
607
+ }
608
+ }
609
+ /**
610
+ * Strict lens proxy — records top-level property access path as a TracedValue
611
+ * on read. Used for `.step()` in/out lenses where we want precise field
612
+ * mappings. Trace marker is a Symbol so it doesn't leak through spread.
613
+ *
614
+ * Enforces the lens purity invariant (contract-flow v9 §3.1 + §3.3):
615
+ * method calls / coercion / method dispatch on state or response throw
616
+ * `LensPurityError`. Authors who need computation must move it to a
617
+ * `.compute(s => ...)` step.
618
+ */
619
+ function makeLensProxy(rootPath) {
620
+ function childProxy(path) {
621
+ // Use a callable **arrow** function target so `apply` trap fires on
622
+ // method calls. Arrow functions don't own `prototype`, so ownKeys can
623
+ // return [] without violating Proxy invariants (regular functions own
624
+ // a non-configurable `prototype` that would have to be surfaced).
625
+ const target = (() => { });
626
+ return new Proxy(target, {
627
+ get(_target, prop) {
628
+ if (prop === TRACE_MARKER)
629
+ return path;
630
+ if (typeof prop === "symbol")
631
+ return undefined;
632
+ return childProxy(`${path}.${String(prop)}`);
633
+ },
634
+ apply(_target, _thisArg, _args) {
635
+ // Method call — forbidden. Extract the leaf segment as the offending op.
636
+ const dot = path.lastIndexOf(".");
637
+ const op = dot >= 0 ? path.slice(dot + 1) : path;
638
+ const owner = dot >= 0 ? path.slice(0, dot) : path;
639
+ throw new LensPurityError(owner, `${op}()`);
640
+ },
641
+ construct(_target, _args) {
642
+ throw new LensPurityError(path, "new");
643
+ },
644
+ // ownKeys returns nothing so spread copies no fields.
645
+ ownKeys() {
646
+ return [];
647
+ },
648
+ getOwnPropertyDescriptor() {
649
+ return undefined;
650
+ },
651
+ });
652
+ }
653
+ return childProxy(rootPath);
654
+ }
655
+ // --- Permissive compute proxy (different role, different trap rules) --------
656
+ /**
657
+ * Trace a `compute` fn to extract top-level reads + writes.
658
+ *
659
+ * Compute allows arbitrary sync TS (template literals, method calls, .map()),
660
+ * so the Proxy must be permissive: respond to primitive coercion, swallow
661
+ * method calls, etc. We only record top-level property access paths.
662
+ *
663
+ * See contract-flow.md §4.1.1 for design + known limitations.
664
+ */
665
+ export function traceComputeFn(fn) {
666
+ const reads = new Set();
667
+ const proxy = makeComputeProxy("state", reads);
668
+ // compute fn is permitted to do arbitrary synchronous TS — the proxy is
669
+ // tolerant of method calls / coercion / iteration via its permissive
670
+ // trap rules, so any exception here is genuine (e.g. fn threw explicitly).
671
+ // Don't silently swallow: let the error propagate so callers surface it.
672
+ const result = fn(proxy);
673
+ const writes = result && typeof result === "object" && !Array.isArray(result)
674
+ ? Object.keys(result).filter((k) => typeof k === "string")
675
+ : [];
676
+ return { reads: [...reads].sort(), writes };
677
+ }
678
+ function makeComputeProxy(rootPath, reads) {
679
+ // Arrow-function target: callable (for `apply` trap on method calls) but
680
+ // does NOT own `prototype`, so `ownKeys` returning [] is valid and spread
681
+ // (`{ ...s }`) works without triggering a Proxy invariant violation.
682
+ return new Proxy((() => { }), {
683
+ get(_target, prop) {
684
+ if (prop === Symbol.toPrimitive)
685
+ return () => "";
686
+ if (prop === "toString")
687
+ return () => "";
688
+ if (prop === "valueOf")
689
+ return () => 0;
690
+ if (prop === Symbol.iterator)
691
+ return function* () { };
692
+ if (prop === "length")
693
+ return 0;
694
+ if (prop === Symbol.isConcatSpreadable)
695
+ return false;
696
+ if (typeof prop !== "string")
697
+ return undefined;
698
+ reads.add(`${rootPath}.${prop}`);
699
+ return makePermissiveChildProxy();
700
+ },
701
+ apply() {
702
+ return makePermissiveChildProxy();
703
+ },
704
+ has() {
705
+ return true;
706
+ },
707
+ ownKeys() {
708
+ return [];
709
+ },
710
+ getOwnPropertyDescriptor() {
711
+ return { configurable: true, enumerable: true, value: undefined };
712
+ },
713
+ });
714
+ }
715
+ /**
716
+ * Totally-permissive child proxy — swallows every access / invocation,
717
+ * returns itself. Used for depth-2+ of compute tracing where we don't track.
718
+ */
719
+ function makePermissiveChildProxy() {
720
+ const self = new Proxy((() => { }), {
721
+ get(_target, prop) {
722
+ if (prop === Symbol.toPrimitive)
723
+ return () => "";
724
+ if (prop === "toString")
725
+ return () => "";
726
+ if (prop === "valueOf")
727
+ return () => 0;
728
+ if (prop === Symbol.iterator)
729
+ return function* () { };
730
+ if (prop === "length")
731
+ return 0;
732
+ return self;
733
+ },
734
+ apply() {
735
+ return self;
736
+ },
737
+ has() {
738
+ return true;
739
+ },
740
+ ownKeys() {
741
+ return [];
742
+ },
743
+ getOwnPropertyDescriptor() {
744
+ return { configurable: true, enumerable: true, value: undefined };
745
+ },
746
+ });
747
+ return self;
748
+ }
749
+ //# sourceMappingURL=contract-core.js.map