@glubean/sdk 0.5.0 → 0.7.0

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 (100) hide show
  1. package/dist/contract-artifacts.d.ts +39 -21
  2. package/dist/contract-artifacts.d.ts.map +1 -1
  3. package/dist/contract-artifacts.js +80 -24
  4. package/dist/contract-artifacts.js.map +1 -1
  5. package/dist/contract-core.d.ts +10 -21
  6. package/dist/contract-core.d.ts.map +1 -1
  7. package/dist/contract-core.js +26 -812
  8. package/dist/contract-core.js.map +1 -1
  9. package/dist/contract-http/adapter.d.ts +13 -0
  10. package/dist/contract-http/adapter.d.ts.map +1 -1
  11. package/dist/contract-http/adapter.js +182 -23
  12. package/dist/contract-http/adapter.js.map +1 -1
  13. package/dist/contract-http/factory.d.ts +2 -2
  14. package/dist/contract-http/factory.d.ts.map +1 -1
  15. package/dist/contract-http/factory.js +5 -0
  16. package/dist/contract-http/factory.js.map +1 -1
  17. package/dist/contract-http/inbound-match.d.ts +47 -0
  18. package/dist/contract-http/inbound-match.d.ts.map +1 -0
  19. package/dist/contract-http/inbound-match.js +136 -0
  20. package/dist/contract-http/inbound-match.js.map +1 -0
  21. package/dist/contract-http/inbound-verify.d.ts +46 -0
  22. package/dist/contract-http/inbound-verify.d.ts.map +1 -0
  23. package/dist/contract-http/inbound-verify.js +101 -0
  24. package/dist/contract-http/inbound-verify.js.map +1 -0
  25. package/dist/contract-http/inbound.d.ts +45 -0
  26. package/dist/contract-http/inbound.d.ts.map +1 -0
  27. package/dist/contract-http/inbound.js +89 -0
  28. package/dist/contract-http/inbound.js.map +1 -0
  29. package/dist/contract-http/index.d.ts +5 -0
  30. package/dist/contract-http/index.d.ts.map +1 -1
  31. package/dist/contract-http/index.js +3 -0
  32. package/dist/contract-http/index.js.map +1 -1
  33. package/dist/contract-http/openapi.d.ts +0 -7
  34. package/dist/contract-http/openapi.d.ts.map +1 -1
  35. package/dist/contract-http/openapi.js +20 -14
  36. package/dist/contract-http/openapi.js.map +1 -1
  37. package/dist/contract-http/types.d.ts +131 -8
  38. package/dist/contract-http/types.d.ts.map +1 -1
  39. package/dist/contract-http/types.js +44 -0
  40. package/dist/contract-http/types.js.map +1 -1
  41. package/dist/contract-types.d.ts +175 -446
  42. package/dist/contract-types.d.ts.map +1 -1
  43. package/dist/index.d.ts +10 -6
  44. package/dist/index.d.ts.map +1 -1
  45. package/dist/index.js +10 -1
  46. package/dist/index.js.map +1 -1
  47. package/dist/internal.d.ts +7 -0
  48. package/dist/internal.d.ts.map +1 -1
  49. package/dist/internal.js +14 -0
  50. package/dist/internal.js.map +1 -1
  51. package/dist/{contract-flow-poll.d.ts → poll-primitives.d.ts} +8 -81
  52. package/dist/poll-primitives.d.ts.map +1 -0
  53. package/dist/{contract-flow-poll.js → poll-primitives.js} +10 -64
  54. package/dist/poll-primitives.js.map +1 -0
  55. package/dist/{contract-flow-condition.d.ts → predicates.d.ts} +34 -71
  56. package/dist/predicates.d.ts.map +1 -0
  57. package/dist/{contract-flow-condition.js → predicates.js} +86 -80
  58. package/dist/predicates.js.map +1 -0
  59. package/dist/test/builder.js +2 -2
  60. package/dist/test/builder.js.map +1 -1
  61. package/dist/test/utils.d.ts +7 -0
  62. package/dist/test/utils.d.ts.map +1 -1
  63. package/dist/test/utils.js +22 -17
  64. package/dist/test/utils.js.map +1 -1
  65. package/dist/types.d.ts +26 -14
  66. package/dist/types.d.ts.map +1 -1
  67. package/dist/types.js.map +1 -1
  68. package/dist/workflow/builder.d.ts +386 -0
  69. package/dist/workflow/builder.d.ts.map +1 -0
  70. package/dist/workflow/builder.js +1150 -0
  71. package/dist/workflow/builder.js.map +1 -0
  72. package/dist/workflow/execute.d.ts +277 -0
  73. package/dist/workflow/execute.d.ts.map +1 -0
  74. package/dist/workflow/execute.js +1489 -0
  75. package/dist/workflow/execute.js.map +1 -0
  76. package/dist/workflow/index.d.ts +11 -0
  77. package/dist/workflow/index.d.ts.map +1 -0
  78. package/dist/workflow/index.js +15 -0
  79. package/dist/workflow/index.js.map +1 -0
  80. package/dist/workflow/project.d.ts +18 -0
  81. package/dist/workflow/project.d.ts.map +1 -0
  82. package/dist/workflow/project.js +321 -0
  83. package/dist/workflow/project.js.map +1 -0
  84. package/dist/workflow/retry.d.ts +13 -0
  85. package/dist/workflow/retry.d.ts.map +1 -0
  86. package/dist/workflow/retry.js +15 -0
  87. package/dist/workflow/retry.js.map +1 -0
  88. package/dist/workflow/types.d.ts +512 -0
  89. package/dist/workflow/types.d.ts.map +1 -0
  90. package/dist/workflow/types.js +2 -0
  91. package/dist/workflow/types.js.map +1 -0
  92. package/package.json +5 -2
  93. package/dist/contract-flow-condition.d.ts.map +0 -1
  94. package/dist/contract-flow-condition.js.map +0 -1
  95. package/dist/contract-flow-poll.d.ts.map +0 -1
  96. package/dist/contract-flow-poll.js.map +0 -1
  97. package/dist/contract-http/flow-helpers.d.ts +0 -12
  98. package/dist/contract-http/flow-helpers.d.ts.map +0 -1
  99. package/dist/contract-http/flow-helpers.js +0 -34
  100. package/dist/contract-http/flow-helpers.js.map +0 -1
@@ -17,10 +17,8 @@
17
17
  * See `internal/40-discovery/proposals/contract-generics-complete.md` v5
18
18
  * and `internal/40-discovery/proposals/contract-flow.md` v9 for design.
19
19
  */
20
- import type { SchemaLike, Test, TestContext } from "./types.js";
20
+ import type { SchemaLike, SecretsAccessor, Test, TestContext } from "./types.js";
21
21
  import type { KnownArtifacts, KnownArtifactParts, KnownArtifactOptions } from "./index.js";
22
- import type { RuntimeBranchStep, ExtractedBranchStep, ExtractedPredicate, JsonScalar, BranchPredicate, PredicateScope } from "./contract-flow-condition.js";
23
- import type { RuntimePollStep, ExtractedPollStep } from "./contract-flow-poll.js";
24
22
  /**
25
23
  * Case lifecycle.
26
24
  *
@@ -261,6 +259,20 @@ export interface CaseMeta<PayloadSchemas = unknown, Meta = unknown> {
261
259
  * the `rawBypass` execution path should appear on an overlay.
262
260
  */
263
261
  hasNeeds?: boolean;
262
+ /**
263
+ * Direction of the case relative to the system under test. Absent =
264
+ * outbound (we call them — the default since contracts began). "inbound"
265
+ * = the counterparty calls us (inbound-contract-design §9.2); such cases
266
+ * are awaited via a workflow inbound poll, never executed.
267
+ */
268
+ direction?: "inbound";
269
+ /**
270
+ * Set to `false` by adapters whose case cannot be executed as a Test
271
+ * (inbound-contract-design §9.5). The dispatcher then registers NO Test
272
+ * and NO runnable-inventory entry for it — the case exists only in
273
+ * projection/metadata. Absent = runnable (every pre-existing case).
274
+ */
275
+ runnable?: boolean;
264
276
  }
265
277
  /**
266
278
  * Runtime contract projection — adapter.project() output.
@@ -305,6 +317,20 @@ export interface ExtractedContractProjection<SafeSchemas = unknown, SafeMeta = u
305
317
  cases: Array<ExtractedCaseMeta<SafeSchemas, SafeMeta>>;
306
318
  schemas?: SafeSchemas;
307
319
  meta?: SafeMeta;
320
+ /**
321
+ * Fully-qualified logical paths of schemas that were DECLARED but could not
322
+ * be projected to JSON Schema (the live `toJSONSchema()` threw, or the value
323
+ * was an unrecognized non-plain object). Distinct from "no schema declared":
324
+ * an absent schema does not appear here. Paths are dotted, e.g.
325
+ * `"request.body"`, `"cases.login.response.body"`, `"cases.login.needsSchema"`.
326
+ *
327
+ * The schema VALUE stays `undefined` in the projection (so OpenAPI / MCP /
328
+ * descriptors degrade exactly as before); this list is the explicit record
329
+ * a snapshot consumer uses to compute `projectionComplete` (false when
330
+ * non-empty) and warn about the specific holes. Undefined/omitted when every
331
+ * declared schema projected cleanly.
332
+ */
333
+ unprojectableSchemas?: string[];
308
334
  }
309
335
  /**
310
336
  * Protocol adapter interface. Implement to add support for a new protocol
@@ -423,7 +449,7 @@ export interface ContractProtocolAdapter<Spec = unknown, RuntimeSchemas = unknow
423
449
  * protocol-specific artifacts (OpenAPI is HTTP-only).
424
450
  */
425
451
  artifacts?: {
426
- [K in keyof KnownArtifacts]?: (projection: ExtractedContractProjection<SafeSchemas, SafeMeta>, options?: KnownArtifactOptions[K]) => KnownArtifactParts[K];
452
+ [K in keyof KnownArtifacts]?: (projection: ExtractedContractProjection<SafeSchemas, SafeMeta>, options?: KnownArtifactOptions[K]) => KnownArtifactParts[K] | null;
427
453
  };
428
454
  /**
429
455
  * Optional: render the `target` string for display. HTTP: "POST /users"
@@ -443,7 +469,7 @@ export interface ContractProtocolAdapter<Spec = unknown, RuntimeSchemas = unknow
443
469
  * 1. Computed `resolvedInputs` via `step.bindings.in(state)`
444
470
  * 2. Prepared current flow state
445
471
  * 3. **v10 (for migrated protocols)**: validated `resolvedInputs` against
446
- * the case's `needs` schema via `runFlow` → `validateNeedsOutput`, so
472
+ * the case's `needs` schema via `validateNeedsOutput`, so
447
473
  * the adapter receives already-validated logical input.
448
474
  * 4. Passed the live contract instance (access merged scoped-factory state
449
475
  * via `contract._spec`)
@@ -505,6 +531,51 @@ export interface ContractProtocolAdapter<Spec = unknown, RuntimeSchemas = unknow
505
531
  * Throws on invalid case; returns undefined on success.
506
532
  */
507
533
  validateCaseForFlow?: (spec: Spec, caseKey: string, contractId: string) => void;
534
+ /**
535
+ * Optional: classify ONE delivery against an inbound case
536
+ * (inbound-contract-design §9.4/§9.4a). Called by the workflow inbound
537
+ * poll once per unclaimed delivery, in receiver order; the FIRST terminal
538
+ * classification wins (consult #4). Authentication decides failure;
539
+ * content decides attribution: fail-class results come only from the
540
+ * channel (signature / staleness / non-JSON), mismatches are probes.
541
+ *
542
+ * Preflight errors (unknown verifier scheme, missing secret) MUST throw —
543
+ * bad config must never hide as a probe (§9.4 preflight row).
544
+ *
545
+ * The poll resolves the state side itself (it owns the state) and hands
546
+ * the matcher the concrete value + the event's EXTRACTED selector path —
547
+ * a path, not the lens, because the matcher must walk unrelated webhook
548
+ * shapes safely: `e => e.data.id` THROWS on a body without `data`, while
549
+ * a safe path walk yields `undefined` → `type-mismatch` probe (codex I3
550
+ * R1 P2).
551
+ *
552
+ * The staleness clock for timestamped signature schemes is the delivery's
553
+ * own `receivedAt` — verification must reflect RECEIPT time, not whenever
554
+ * the poll happened to scan the inbox, or a pre-existing valid delivery
555
+ * (allowed evidence, §9.4a #3) would misclassify as stale after a slow
556
+ * setup (codex I3 R2 P2).
557
+ */
558
+ matchInboundCase?: (input: {
559
+ caseSpec: unknown;
560
+ delivery: InboundDelivery;
561
+ secrets: SecretsAccessor;
562
+ correlate?: {
563
+ eventPath: readonly string[];
564
+ stateValue: unknown;
565
+ };
566
+ }) => InboundMatchResult;
567
+ /**
568
+ * Optional: validate inbound-case CONFIG without a delivery — throw on an
569
+ * unknown verifier scheme / missing secret (§9.4 preflight row). The poll
570
+ * calls this every attempt BEFORE reading the receiver: an empty inbox
571
+ * must not let bad config exhaust as a timeout (codex I3 R3 P2 — the
572
+ * matcher's own preflight only runs when a delivery exists). Adapters
573
+ * implementing matchInboundCase should implement this too.
574
+ */
575
+ preflightInboundCase?: (input: {
576
+ caseSpec: unknown;
577
+ secrets: SecretsAccessor;
578
+ }) => void;
508
579
  }
509
580
  /**
510
581
  * Protocol-agnostic payload summary. Adapter-provided via describePayload.
@@ -554,14 +625,20 @@ export interface ProtocolContract<Spec = unknown, PayloadSchemas = unknown, Meta
554
625
  */
555
626
  readonly _spec: Spec;
556
627
  /**
557
- * Return a ContractCaseRef for use in `contract.flow(...).step(...)`.
628
+ * Return a ContractCaseRef for use in `workflow().call/.poll(...)`.
558
629
  *
559
630
  * Runtime validation: adapter's `.case(key)` implementation MUST fail-fast
560
631
  * if the case contains function-valued input fields (body/params/query/
561
632
  * headers as functions). Function fields reference case-local setup state
562
633
  * which is not available in flow mode. See contract-flow §5.1.1.
563
634
  */
564
- case<K extends keyof Cases & string>(key: K): ContractCaseRef<InferCaseInput<Cases[K]>, InferOutput<PayloadSchemas>, InferAcceptKey<PayloadSchemas>, InferRawOutcome<PayloadSchemas, InferOutput<PayloadSchemas>>>;
635
+ case<K extends keyof Cases & string>(key: K): ContractCaseRef<InferCaseInput<Cases[K]>, ApplyCaseOutput<PayloadSchemas, Cases[K]>, InferAcceptKey<PayloadSchemas>, InferRawOutcome<PayloadSchemas, InferOutput<PayloadSchemas>>> & (Cases[K] extends {
636
+ direction: "inbound";
637
+ } ? {
638
+ readonly direction: "inbound";
639
+ } : {
640
+ readonly direction?: undefined;
641
+ });
565
642
  }
566
643
  /**
567
644
  * Adapter-defined helper: extract the "case inputs" shape from PayloadSchemas.
@@ -594,6 +671,33 @@ export type InferCaseInput<C> = C extends {
594
671
  } ? unknown extends N ? void : N : void;
595
672
  /** Adapter-defined helper: extract the "case output" shape from PayloadSchemas. */
596
673
  export type InferOutput<_PayloadSchemas> = unknown;
674
+ /**
675
+ * Per-case flow `CaseOutput` (the type of `res` in a `.step`/`.poll` `out`/`until`
676
+ * lens, sans `accept`). Adapters that carry a `__caseOutputShape` marker on their
677
+ * `PayloadSchemas` (HTTP = `HttpFlowCaseOutput`) get that shape with its `body`
678
+ * replaced by THIS case's validated response (`ExtractCaseResponse`); adapters
679
+ * without the marker fall back to `InferOutput`. Computed INSIDE the base `.case()`
680
+ * (not a subtype override) so HTTP contracts stay assignable to the base
681
+ * `ProtocolContract` while still typing `res.body` per case.
682
+ */
683
+ export type ApplyCaseOutput<PayloadSchemas, Case> = PayloadSchemas extends {
684
+ readonly __caseOutputShape?: infer Shape;
685
+ } ? [unknown] extends [Shape] ? InferOutput<PayloadSchemas> : Shape extends {
686
+ body: unknown;
687
+ } ? Omit<Shape, "body"> & {
688
+ body: ExtractCaseResponse<Case>;
689
+ } : InferOutput<PayloadSchemas> : InferOutput<PayloadSchemas>;
690
+ /**
691
+ * A case's validated response body — ONLY from a PRESENT `expect.schema` (matched
692
+ * as required, not `schema?:`). A docs-only `expect.example`, an explicit response
693
+ * generic with no schema value, or any schema-less case stays `unknown`, so a flow
694
+ * lens never types a body the runtime didn't validate.
695
+ */
696
+ export type ExtractCaseResponse<Case> = Case extends {
697
+ expect: {
698
+ schema: SchemaLike<infer S>;
699
+ };
700
+ } ? [unknown] extends [S] ? unknown : S : unknown;
597
701
  /**
598
702
  * Adapter-defined helper: the element type of a step's `accept` list (the
599
703
  * "accepted alternate outcome key"). HTTP = `number` (status); other protocols
@@ -615,6 +719,66 @@ export type InferAcceptKey<P> = P extends {
615
719
  export type InferRawOutcome<P, Fallback = unknown> = P extends {
616
720
  readonly __rawOutcome?: infer R;
617
721
  } ? [unknown] extends [R] ? Fallback : NonNullable<R> : Fallback;
722
+ /**
723
+ * One delivery as received — RAW. `bodyBytes` is the exact byte sequence the
724
+ * counterparty sent: HMAC-style signatures are computed over raw bytes, so
725
+ * any parsing or decoding before verification would destroy the evidence
726
+ * (design §9.1, blind spot 3). Protocol-agnostic: core's inbound-poll
727
+ * machinery and adapter `matchInboundCase` hooks both consume it (the
728
+ * zero-dependency local implementation lives in `contract-http/inbound.ts`).
729
+ */
730
+ export interface InboundDelivery {
731
+ /** Receiver-assigned, unique per delivery (claim key). */
732
+ id: string;
733
+ /** EXACT body bytes as received — THE signature input. */
734
+ bodyBytes: Uint8Array;
735
+ /**
736
+ * UTF-8 decoded view of `bodyBytes`, for JSON parsing and display. LOSSY
737
+ * for non-UTF-8 payloads — never feed this to a signature verifier.
738
+ */
739
+ rawBody: string;
740
+ /** Header names lowercased. */
741
+ headers: Record<string, string>;
742
+ method: string;
743
+ path: string;
744
+ /** Epoch ms at receipt — the measured side of `within` evidence. */
745
+ receivedAt: number;
746
+ }
747
+ /**
748
+ * The receiver protocol an inbound poll (`.poll(ref, { via })`) consumes
749
+ * (design §9.1). NON-DESTRUCTIVE by construction: matching inspects
750
+ * `deliveries()` snapshots and `claim()`s only the matched delivery — other
751
+ * consumers' events are never swallowed by a failed match.
752
+ *
753
+ * ONE handle corresponds to ONE endpoint/secret domain (design §9.4):
754
+ * the matcher never checks method/path — full grade means "received through
755
+ * THIS handle" (consult #7), so multi-source channels must be split into
756
+ * per-domain handles at the receiver layer.
757
+ */
758
+ export interface ReceiverHandle {
759
+ /** Snapshot of UNCLAIMED deliveries, oldest first. */
760
+ deliveries(): readonly InboundDelivery[];
761
+ /** Mark one delivery consumed (idempotent; unknown ids are a no-op). */
762
+ claim(id: string): void;
763
+ /** The URL the counterparty should be pointed at. */
764
+ url: string;
765
+ close(): Promise<void>;
766
+ }
767
+ /**
768
+ * Result of an adapter's `matchInboundCase` over ONE delivery (§9.4/§9.4a).
769
+ * Fail-class results (signature-invalid / stale / unparseable) FAIL the poll
770
+ * node; mismatch results are probes whose labels exist for diagnosis;
771
+ * `matched` claims the delivery and satisfies the poll.
772
+ */
773
+ export type InboundMatchResult = {
774
+ kind: "matched";
775
+ parsed: unknown;
776
+ } | {
777
+ kind: "type-mismatch" | "correlation-mismatch" | "schema-mismatch";
778
+ } | {
779
+ kind: "signature-invalid" | "stale" | "unparseable";
780
+ detail?: string;
781
+ };
618
782
  /**
619
783
  * Opaque reference to a single case of a contract. Produced by
620
784
  * `ProtocolContract.case(key)`. Used as input to `FlowBuilder.step(...)`.
@@ -638,28 +802,6 @@ export interface ContractCaseRef<CaseInputs = unknown, CaseOutput = unknown, Acc
638
802
  /** Phantom — `out`'s `res` type when `accept` is present (adapter raw outcome). */
639
803
  readonly __phantom_raw_outcome?: RawOutcome;
640
804
  }
641
- /** Contract-level metadata for a flow (set via `.meta()`). */
642
- export interface FlowMeta {
643
- id: string;
644
- name?: string;
645
- description?: string;
646
- tags?: string[];
647
- extensions?: Extensions;
648
- /**
649
- * Mark this flow as skipped at run time. Value is the skip reason
650
- * displayed in reports. Useful for illustrative examples that should
651
- * be discoverable (for scanner extraction / docs rendering) but must
652
- * not attempt live HTTP calls.
653
- *
654
- * Mirrors `TestMeta.skip`.
655
- */
656
- skip?: string;
657
- /**
658
- * Mark this flow as focused. When any flows/tests in a run are `only`,
659
- * non-focused ones may be excluded. Mirrors `TestMeta.only`.
660
- */
661
- only?: boolean;
662
- }
663
805
  /**
664
806
  * Field dependency / mapping produced by Proxy dry-run of a lens function.
665
807
  * Consumed by downstream (MCP/CLI/Cloud) to render flow data-flow diagrams.
@@ -679,371 +821,14 @@ export interface FieldMapping {
679
821
  };
680
822
  }
681
823
  /**
682
- * Runtime flow stepdiscriminated union.
683
- * kind "contract-call" = a ContractCaseRef with bindings (Rule 1 teardown applies).
684
- * kind "compute" = a pure sync data-transform function (no adapter, no teardown).
685
- */
686
- export type RuntimeFlowStep = RuntimeContractCallStep | RuntimeComputeStep | RuntimeBranchStep | RuntimePollStep;
687
- export interface RuntimeContractCallStep {
688
- kind: "contract-call";
689
- name?: string;
690
- ref: ContractCaseRef<any, any, any, any>;
691
- caseKey: string;
692
- /** Live contract instance (mirrors ref.contract, kept for direct access). */
693
- contract: ProtocolContract<any, any, any>;
694
- bindings?: {
695
- in?: (state: any) => any;
696
- out?: (state: any, response: any) => any;
697
- /**
698
- * Accepted alternate outcome keys (HTTP: status numbers). When the actual
699
- * outcome key is in this list AND differs from the case's primary expected
700
- * outcome, the adapter treats it as a legal outcome: it skips the case's
701
- * primary result validation (schema / headers / verify) and hands the RAW
702
- * outcome to `out`, so a condition can branch on it. Protocol-agnostic in
703
- * core; the adapter interprets the element type.
704
- */
705
- accept?: readonly unknown[];
706
- };
707
- }
708
- export interface RuntimeComputeStep {
709
- kind: "compute";
710
- name?: string;
711
- /**
712
- * Synchronous pure function. NOT subject to lens Proxy purity — may use
713
- * template literals / method calls / .map(). MUST be synchronous and
714
- * MUST NOT return a thenable (enforced at runtime).
715
- */
716
- fn: (state: any) => any;
717
- }
718
- /**
719
- * Runtime flow projection. Carries live callbacks + live contract refs.
720
- * Never crosses serialization boundaries. `normalizeFlow()` converts to
721
- * ExtractedFlowProjection for downstream consumers.
722
- */
723
- export interface RuntimeFlowProjection<State = unknown> {
724
- protocol: "flow";
725
- description?: string;
726
- tags?: string[];
727
- extensions?: Extensions;
728
- /** Mirrors `FlowMeta.skip` — skip reason for discoverable-but-not-runnable flows. */
729
- skip?: string;
730
- /** Mirrors `FlowMeta.only` — focus filter. */
731
- only?: boolean;
732
- /** Live flow-level setup callback (only I/O-capable callback in flow). */
733
- setup?: (ctx: TestContext) => Promise<State>;
734
- /** Live flow-level teardown callback. Rule 2: outer finally. */
735
- teardown?: (ctx: TestContext, state: State) => Promise<void>;
736
- steps: RuntimeFlowStep[];
737
- }
738
- /**
739
- * Extracted flow step — discriminated union, JSON-safe.
740
- */
741
- export type ExtractedFlowStep = ExtractedContractCallStep | ExtractedComputeStep | ExtractedBranchStep | ExtractedPollStep;
742
- export interface ExtractedContractCallStep {
743
- kind: "contract-call";
744
- name?: string;
745
- contractId: string;
746
- caseKey: string;
747
- protocol: string;
748
- target: string;
749
- /** Proxy dry-run output — input mappings. */
750
- inputs?: FieldMapping[];
751
- /** Proxy dry-run output — state update mappings. */
752
- outputs?: FieldMapping[];
753
- /**
754
- * Accepted alternate outcome keys (HTTP: status numbers) — present when the
755
- * step declared `accept`. Surfaces the multi-status contract to downstream
756
- * consumers (scanner / MCP / CLI / Cloud) so a status-gated step is not
757
- * indistinguishable from a single-primary-status one.
758
- */
759
- accept?: ReadonlyArray<string | number>;
760
- }
761
- export interface ExtractedComputeStep {
762
- kind: "compute";
763
- name?: string;
764
- /** Top-level state paths read (from Proxy dry-run). */
765
- reads: string[];
766
- /** Top-level state keys written (keys of returned object). */
767
- writes: string[];
768
- }
769
- /**
770
- * JSON-safe flow projection. Downstream (scanner / MCP / CLI / Cloud) consume
771
- * this. Produced by `normalizeFlow(runtime)`.
772
- */
773
- export interface ExtractedFlowProjection {
774
- id: string;
775
- protocol: "flow";
776
- description?: string;
777
- tags?: string[];
778
- extensions?: Extensions;
779
- /** Mirrors `FlowMeta.skip`. */
780
- skip?: string;
781
- /** Mirrors `FlowMeta.only`. */
782
- only?: boolean;
783
- /** Present when flow has a setup callback (state source is dynamic). */
784
- setupDynamic?: true;
785
- steps: ExtractedFlowStep[];
786
- }
787
- /**
788
- * Base options shared by `poll` / `pollFn` / `pollAsync`. `Res` switches on
789
- * `accept` exactly like `step` (no accept → CaseOutput; accept → RawOutcome).
790
- * `accept?: Accept` is what drives the `Accept` inference. Bound semantics +
791
- * the build-time rules live in `validatePollBounds`. (Spike P.)
792
- */
793
- export interface PollOptsBase<State, Res, NewState, Accept extends readonly unknown[]> {
794
- /** The exit (satisfying) response written into state. */
795
- out?: (state: State, response: Res) => NewState;
796
- /** Accepted alternate outcome keys (HTTP: status numbers) — drives `Accept` inference. */
797
- accept?: Accept;
798
- name?: string;
799
- /** Total wall-clock bound (ms). */
800
- timeout?: number;
801
- /** Max attempts (incl. the first). */
802
- maxAttempts?: number;
803
- /** Per-attempt budget (ms) — required when `timeout` is absent. */
804
- perAttemptTimeout?: number;
805
- /** Interval between attempts (ms, default 1000). */
806
- every?: number;
807
- /** Backoff multiplier applied to `every` after each retry (default 1, capped). */
808
- backoff?: number;
809
- }
810
- /**
811
- * Poll argument tuple. Mirrors `step`'s conditional tuple: `in` is REQUIRED for
812
- * a needs-input case and FORBIDDEN for a void-input case. `Exit` is each tier's
813
- * `{ until, message }` fragment (L2 declarative vs opaque). (Spike P.)
814
- */
815
- export type PollArgs<State, CaseInputs, Res, NewState, Accept extends readonly unknown[], Exit> = [
816
- CaseInputs
817
- ] extends [void] ? [opts: PollOptsBase<State, Res, NewState, Accept> & Exit] : [opts: PollOptsBase<State, Res, NewState, Accept> & Exit & {
818
- in: (state: State) => CaseInputs;
819
- }];
820
- /**
821
- * Builder for `contract.flow(id)`. State chain threads through `.step()` /
822
- * `.compute()` via TypeScript generics.
823
- */
824
- export interface FlowBuilder<State = unknown> {
825
- readonly __glubean_type: "flow-builder";
826
- meta(m: Omit<FlowMeta, "id">): FlowBuilder<State>;
827
- /**
828
- * Flow-level setup — the ONLY I/O-capable callback in a flow. May be async,
829
- * may read ctx, may call external services. Returns the initial state.
830
- */
831
- setup<NewState>(fn: (ctx: TestContext) => Promise<NewState>): FlowBuilder<NewState>;
832
- /**
833
- * Add a contract-call step. `bindings.in` and `bindings.out` MUST be pure
834
- * lens functions (select / repack only; no I/O, no method calls, no
835
- * branching). Lens purity is enforced at Proxy dry-run time during
836
- * projection extraction.
837
- *
838
- * Single signature with rest-parameter conditional tuple (Spike 0 Finding 3).
839
- * Two-overload form (historically tried) has a subtle hole: overload 1
840
- * (void-input, no `in`) excess-property-fails when `in` is passed → TS
841
- * falls through to overload 2, which accepts because `() => X` is
842
- * bivariant-assignable to `() => void`. Conditional tuple is a single
843
- * signature so TS doesn't get a second chance:
844
- * - void-input case → `bindings` optional; `in` not present in the
845
- * allowed shape
846
- * - typed-input case → `bindings.in` REQUIRED
847
- *
848
- * In v10, `in` returns LOGICAL case input (matches the case's `needs`),
849
- * NOT an adapter patch. HTTP adapter's `executeCaseInFlow` calls
850
- * function-valued body/headers/params/query with this logical input
851
- * (same mechanism as standalone `executeStandaloneCase`).
852
- */
853
- step<CaseInputs, CaseOutput, AcceptKey, RawOutcome, Accept extends readonly AcceptKey[] = never, NewState = State>(ref: ContractCaseRef<CaseInputs, CaseOutput, AcceptKey, RawOutcome>, ...args: [CaseInputs] extends [void] ? [
854
- bindings?: {
855
- accept?: Accept;
856
- out?: (state: State, response: [Accept] extends [never] ? CaseOutput : RawOutcome) => NewState;
857
- name?: string;
858
- }
859
- ] : [
860
- bindings: {
861
- in: (state: State) => CaseInputs;
862
- accept?: Accept;
863
- out?: (state: State, response: [Accept] extends [never] ? CaseOutput : RawOutcome) => NewState;
864
- name?: string;
865
- }
866
- ]): FlowBuilder<NewState>;
867
- /**
868
- * Add a pure synchronous data-transform step. Accepts any synchronous TS
869
- * expression (template literals, method calls, .map()). Projection records
870
- * only read/write dependencies, NOT the formula.
871
- *
872
- * Runtime enforcement: throws if `fn` is async or returns a thenable.
873
- */
874
- compute<NewState>(fn: (state: State) => NewState): FlowBuilder<NewState>;
875
- /**
876
- * Flow-level teardown — runs in Rule 2 outer-finally, receives last-
877
- * committed state. If flow.setup threw, teardown does NOT run.
878
- */
879
- teardown(fn: (ctx: TestContext, state: State) => Promise<void>): FlowBuilder<State>;
880
- /** L2 — declarative predicate (precisely projectable). No else → then keeps State's shape (values may narrow). */
881
- condition<R extends State = State>(spec: {
882
- predicate: (w: PredicateScope<State>) => BranchPredicate<State>;
883
- message?: string;
884
- }, thenBranch: (b: FlowFragmentBuilder<State>) => FlowFragmentBuilder<R & NoExtraKeys<R, State>>): FlowBuilder<State>;
885
- /** L2 with else — `T` is inferred from `then`; `else` is checked against it (`NoInfer`). */
886
- condition<T>(spec: {
887
- predicate: (w: PredicateScope<State>) => BranchPredicate<State>;
888
- message?: string;
889
- }, thenBranch: (b: FlowFragmentBuilder<State>) => FlowFragmentBuilder<T>, elseBranch: (b: FlowFragmentBuilder<State>) => FlowFragmentBuilder<NoInfer<T>>): FlowBuilder<T>;
890
- /** L1 — opaque sync predicate. `message` is required (projection marks ⚠ opaque-gate). */
891
- conditionFn<R extends State = State>(spec: {
892
- predicate: (ctx: TestContext, s: State) => boolean;
893
- message: string;
894
- }, thenBranch: (b: FlowFragmentBuilder<State>) => FlowFragmentBuilder<R & NoExtraKeys<R, State>>): FlowBuilder<State>;
895
- conditionFn<T>(spec: {
896
- predicate: (ctx: TestContext, s: State) => boolean;
897
- message: string;
898
- }, thenBranch: (b: FlowFragmentBuilder<State>) => FlowFragmentBuilder<T>, elseBranch: (b: FlowFragmentBuilder<State>) => FlowFragmentBuilder<NoInfer<T>>): FlowBuilder<T>;
899
- /** L0 — opaque async predicate (may do out-of-contract I/O). `message` required (⚠ dynamic-gate). */
900
- conditionAsync<R extends State = State>(spec: {
901
- predicate: (ctx: TestContext, s: State) => Promise<boolean>;
902
- message: string;
903
- }, thenBranch: (b: FlowFragmentBuilder<State>) => FlowFragmentBuilder<R & NoExtraKeys<R, State>>): FlowBuilder<State>;
904
- conditionAsync<T>(spec: {
905
- predicate: (ctx: TestContext, s: State) => Promise<boolean>;
906
- message: string;
907
- }, thenBranch: (b: FlowFragmentBuilder<State>) => FlowFragmentBuilder<T>, elseBranch: (b: FlowFragmentBuilder<State>) => FlowFragmentBuilder<NoInfer<T>>): FlowBuilder<T>;
908
- /**
909
- * value-switch (single lens × N scalar values). Curried so `V` infers from
910
- * the lens alone (Spike 0): first call fixes `V`, second checks each `value`
911
- * against it. `default` is required (anchors `T`; the no-match path). switch
912
- * is always L2 — a clean decision table.
913
- */
914
- switchOn<V>(lens: (s: State) => V): <T>(cases: ReadonlyArray<{
915
- value: [Exclude<V, undefined>] extends [JsonScalar] ? Exclude<V, undefined> : never;
916
- then: (b: FlowFragmentBuilder<State>) => FlowFragmentBuilder<NoInfer<T>>;
917
- }>, deflt: (b: FlowFragmentBuilder<State>) => FlowFragmentBuilder<T>) => FlowBuilder<T>;
918
- /** cond-switch (ordered, possibly-overlapping declarative predicates; first-match). */
919
- switchCond<T>(cases: ReadonlyArray<{
920
- when: (w: PredicateScope<State>) => BranchPredicate<State>;
921
- then: (b: FlowFragmentBuilder<State>) => FlowFragmentBuilder<NoInfer<T>>;
922
- }>, deflt: (b: FlowFragmentBuilder<State>) => FlowFragmentBuilder<T>): FlowBuilder<T>;
923
- /** L2 — declarative exit predicate over the response (precisely projectable). poll-on-status needs `accept`. */
924
- poll<CaseInputs, CaseOutput, AcceptKey, RawOutcome, Accept extends readonly AcceptKey[] = never, NewState = State>(ref: ContractCaseRef<CaseInputs, CaseOutput, AcceptKey, RawOutcome>, ...args: PollArgs<State, CaseInputs, [Accept] extends [never] ? CaseOutput : RawOutcome, NewState, Accept, {
925
- until: (w: PredicateScope<[Accept] extends [never] ? CaseOutput : RawOutcome>) => BranchPredicate<[Accept] extends [never] ? CaseOutput : RawOutcome>;
926
- message?: string;
927
- }>): FlowBuilder<NewState>;
928
- /** L1 — opaque sync exit predicate (gets ctx, res, state). `message` required (marked gate). */
929
- pollFn<CaseInputs, CaseOutput, AcceptKey, RawOutcome, Accept extends readonly AcceptKey[] = never, NewState = State>(ref: ContractCaseRef<CaseInputs, CaseOutput, AcceptKey, RawOutcome>, ...args: PollArgs<State, CaseInputs, [Accept] extends [never] ? CaseOutput : RawOutcome, NewState, Accept, {
930
- until: (ctx: TestContext, res: [Accept] extends [never] ? CaseOutput : RawOutcome, state: State) => boolean;
931
- message: string;
932
- }>): FlowBuilder<NewState>;
933
- /** L0 — opaque async exit predicate (may do contract-external I/O). `message` required (marked gate). */
934
- pollAsync<CaseInputs, CaseOutput, AcceptKey, RawOutcome, Accept extends readonly AcceptKey[] = never, NewState = State>(ref: ContractCaseRef<CaseInputs, CaseOutput, AcceptKey, RawOutcome>, ...args: PollArgs<State, CaseInputs, [Accept] extends [never] ? CaseOutput : RawOutcome, NewState, Accept, {
935
- until: (ctx: TestContext, res: [Accept] extends [never] ? CaseOutput : RawOutcome, state: State) => Promise<boolean>;
936
- message: string;
937
- }>): FlowBuilder<NewState>;
938
- build(): FlowContract<State>;
939
- }
940
- /**
941
- * Restricted builder for a branch body (condition then/else, switch case/default).
942
- * Accumulates step / compute / nested branch into the branch node's
943
- * `cases[].steps` / `default`; it deliberately does NOT expose flow-level
944
- * setup / teardown / meta / build.
945
- *
946
- * INVARIANT BRAND (Spike 0 / §8.1): `State` appears in both covariant (return)
947
- * and contravariant (parameter) position via the phantom `[FRAGMENT_STATE]`
948
- * field, making `FlowFragmentBuilder` *invariant* in `State`. Without this,
949
- * TS structural width-subtyping would let `FlowFragmentBuilder<{...State, x}>`
950
- * be assignable to `FlowFragmentBuilder<State>`, so a no-else branch could
951
- * silently add fields (or an else could return a `T` supertype) and bypass
952
- * convergence. The phantom is type-only — never read at runtime.
953
- */
954
- export interface FlowFragmentBuilder<State = unknown> {
955
- readonly [FRAGMENT_STATE]: (s: State) => State;
956
- step<CaseInputs, CaseOutput, AcceptKey, RawOutcome, Accept extends readonly AcceptKey[] = never, NewState = State>(ref: ContractCaseRef<CaseInputs, CaseOutput, AcceptKey, RawOutcome>, ...args: [CaseInputs] extends [void] ? [
957
- bindings?: {
958
- accept?: Accept;
959
- out?: (state: State, response: [Accept] extends [never] ? CaseOutput : RawOutcome) => NewState;
960
- name?: string;
961
- }
962
- ] : [
963
- bindings: {
964
- in: (state: State) => CaseInputs;
965
- accept?: Accept;
966
- out?: (state: State, response: [Accept] extends [never] ? CaseOutput : RawOutcome) => NewState;
967
- name?: string;
968
- }
969
- ]): FlowFragmentBuilder<NewState>;
970
- compute<NewState>(fn: (state: State) => NewState): FlowFragmentBuilder<NewState>;
971
- condition<R extends State = State>(spec: {
972
- predicate: (w: PredicateScope<State>) => BranchPredicate<State>;
973
- message?: string;
974
- }, thenBranch: (b: FlowFragmentBuilder<State>) => FlowFragmentBuilder<R & NoExtraKeys<R, State>>): FlowFragmentBuilder<State>;
975
- condition<T>(spec: {
976
- predicate: (w: PredicateScope<State>) => BranchPredicate<State>;
977
- message?: string;
978
- }, thenBranch: (b: FlowFragmentBuilder<State>) => FlowFragmentBuilder<T>, elseBranch: (b: FlowFragmentBuilder<State>) => FlowFragmentBuilder<NoInfer<T>>): FlowFragmentBuilder<T>;
979
- conditionFn<R extends State = State>(spec: {
980
- predicate: (ctx: TestContext, s: State) => boolean;
981
- message: string;
982
- }, thenBranch: (b: FlowFragmentBuilder<State>) => FlowFragmentBuilder<R & NoExtraKeys<R, State>>): FlowFragmentBuilder<State>;
983
- conditionFn<T>(spec: {
984
- predicate: (ctx: TestContext, s: State) => boolean;
985
- message: string;
986
- }, thenBranch: (b: FlowFragmentBuilder<State>) => FlowFragmentBuilder<T>, elseBranch: (b: FlowFragmentBuilder<State>) => FlowFragmentBuilder<NoInfer<T>>): FlowFragmentBuilder<T>;
987
- conditionAsync<R extends State = State>(spec: {
988
- predicate: (ctx: TestContext, s: State) => Promise<boolean>;
989
- message: string;
990
- }, thenBranch: (b: FlowFragmentBuilder<State>) => FlowFragmentBuilder<R & NoExtraKeys<R, State>>): FlowFragmentBuilder<State>;
991
- conditionAsync<T>(spec: {
992
- predicate: (ctx: TestContext, s: State) => Promise<boolean>;
993
- message: string;
994
- }, thenBranch: (b: FlowFragmentBuilder<State>) => FlowFragmentBuilder<T>, elseBranch: (b: FlowFragmentBuilder<State>) => FlowFragmentBuilder<NoInfer<T>>): FlowFragmentBuilder<T>;
995
- switchOn<V>(lens: (s: State) => V): <T>(cases: ReadonlyArray<{
996
- value: [Exclude<V, undefined>] extends [JsonScalar] ? Exclude<V, undefined> : never;
997
- then: (b: FlowFragmentBuilder<State>) => FlowFragmentBuilder<NoInfer<T>>;
998
- }>, deflt: (b: FlowFragmentBuilder<State>) => FlowFragmentBuilder<T>) => FlowFragmentBuilder<T>;
999
- switchCond<T>(cases: ReadonlyArray<{
1000
- when: (w: PredicateScope<State>) => BranchPredicate<State>;
1001
- then: (b: FlowFragmentBuilder<State>) => FlowFragmentBuilder<NoInfer<T>>;
1002
- }>, deflt: (b: FlowFragmentBuilder<State>) => FlowFragmentBuilder<T>): FlowFragmentBuilder<T>;
1003
- poll<CaseInputs, CaseOutput, AcceptKey, RawOutcome, Accept extends readonly AcceptKey[] = never, NewState = State>(ref: ContractCaseRef<CaseInputs, CaseOutput, AcceptKey, RawOutcome>, ...args: PollArgs<State, CaseInputs, [Accept] extends [never] ? CaseOutput : RawOutcome, NewState, Accept, {
1004
- until: (w: PredicateScope<[Accept] extends [never] ? CaseOutput : RawOutcome>) => BranchPredicate<[Accept] extends [never] ? CaseOutput : RawOutcome>;
1005
- message?: string;
1006
- }>): FlowFragmentBuilder<NewState>;
1007
- pollFn<CaseInputs, CaseOutput, AcceptKey, RawOutcome, Accept extends readonly AcceptKey[] = never, NewState = State>(ref: ContractCaseRef<CaseInputs, CaseOutput, AcceptKey, RawOutcome>, ...args: PollArgs<State, CaseInputs, [Accept] extends [never] ? CaseOutput : RawOutcome, NewState, Accept, {
1008
- until: (ctx: TestContext, res: [Accept] extends [never] ? CaseOutput : RawOutcome, state: State) => boolean;
1009
- message: string;
1010
- }>): FlowFragmentBuilder<NewState>;
1011
- pollAsync<CaseInputs, CaseOutput, AcceptKey, RawOutcome, Accept extends readonly AcceptKey[] = never, NewState = State>(ref: ContractCaseRef<CaseInputs, CaseOutput, AcceptKey, RawOutcome>, ...args: PollArgs<State, CaseInputs, [Accept] extends [never] ? CaseOutput : RawOutcome, NewState, Accept, {
1012
- until: (ctx: TestContext, res: [Accept] extends [never] ? CaseOutput : RawOutcome, state: State) => Promise<boolean>;
1013
- message: string;
1014
- }>): FlowFragmentBuilder<NewState>;
1015
- }
1016
- /** Module-private phantom brand making FlowFragmentBuilder invariant in State. */
1017
- declare const FRAGMENT_STATE: unique symbol;
1018
- /**
1019
- * No-else branch shape guard. A no-else `then` must NOT add/remove state keys
1020
- * (when the predicate is false the branch is skipped, so downstream state stays
1021
- * `State`), but it MAY narrow an existing key's value (e.g. flip `ok: boolean`
1022
- * to `ok: true`). Encoded as `R extends State` (no removed keys, values only
1023
- * narrow) intersected with `NoExtraKeys<R, State>` (any extra key collapses to
1024
- * `never`, so adding a field fails to type-check). This is the ergonomic
1025
- * counterpart to the invariant brand: it blocks shape drift without rejecting
1026
- * safe literal narrowing.
824
+ * "No extra keys" structural guard maps any key of R that is NOT in State to
825
+ * `never`, so a branch/fragment returning extra keys fails to type-check.
826
+ * (Shared type utility; survived the legacy-flow deletion because the vNext
827
+ * builder's strict-S sides use the same trick.)
1027
828
  */
1028
829
  export type NoExtraKeys<R, State> = {
1029
830
  [K in Exclude<keyof R, keyof State>]: never;
1030
831
  };
1031
- /**
1032
- * Runtime flow contract. Extends Array<Test> so runner iterates directly.
1033
- * The single Test inside orchestrates setup → steps → teardown via runFlow.
1034
- */
1035
- export interface FlowContract<State = unknown> extends Array<Test> {
1036
- readonly _flow: RuntimeFlowProjection<State> & {
1037
- id: string;
1038
- };
1039
- /**
1040
- * Pre-computed JSON-safe extracted projection. Populated by the flow
1041
- * builder via `normalizeFlow(_flow)` so downstream consumers (scanner,
1042
- * CLI, MCP, Cloud) don't need to import the SDK to get field mappings
1043
- * for `.step()` lenses and reads/writes for `.compute()` nodes.
1044
- */
1045
- readonly _extracted: ExtractedFlowProjection;
1046
- }
1047
832
  /**
1048
833
  * Contract registry metadata attached to tests produced by `contract[protocol]()`.
1049
834
  * Mirrored by `RegisteredTestMeta.contract` in types.ts.
@@ -1066,60 +851,4 @@ export interface ContractRegistryMeta {
1066
851
  /** Plugin-defined free-form meta; core does not inspect. */
1067
852
  meta?: unknown;
1068
853
  }
1069
- /**
1070
- * Flow registry metadata attached to the single Test generated by
1071
- * `contract.flow()`. Mirrored by `RegisteredTestMeta.flow` in types.ts.
1072
- */
1073
- /** Registry step descriptor — same shape family as ExtractedFlowStep (recursive for branch). */
1074
- export type FlowRegistryStep = {
1075
- kind: "contract-call" | "compute";
1076
- name?: string;
1077
- contractId?: string;
1078
- caseKey?: string;
1079
- protocol?: string;
1080
- target?: string;
1081
- inputs?: FieldMapping[];
1082
- outputs?: FieldMapping[];
1083
- reads?: string[];
1084
- writes?: string[];
1085
- accept?: ReadonlyArray<string | number>;
1086
- } | {
1087
- kind: "branch";
1088
- mode: "value" | "predicate";
1089
- name?: string;
1090
- subjectPath?: string[];
1091
- cases: Array<{
1092
- value?: JsonScalar;
1093
- message?: string;
1094
- predicate?: ExtractedPredicate;
1095
- steps: FlowRegistryStep[];
1096
- }>;
1097
- default: FlowRegistryStep[];
1098
- } | {
1099
- kind: "poll";
1100
- name?: string;
1101
- contractId?: string;
1102
- caseKey?: string;
1103
- protocol?: string;
1104
- target?: string;
1105
- inputs?: FieldMapping[];
1106
- outputs?: FieldMapping[];
1107
- accept?: ReadonlyArray<string | number>;
1108
- until?: ExtractedPredicate;
1109
- message?: string;
1110
- every?: number;
1111
- backoff?: number;
1112
- timeoutMs?: number;
1113
- perAttemptTimeoutMs?: number;
1114
- maxAttempts?: number;
1115
- };
1116
- export interface FlowRegistryMeta {
1117
- id: string;
1118
- description?: string;
1119
- tags?: string[];
1120
- /** Step descriptors (same shape family as ExtractedFlowStep; branch nests recursively). */
1121
- steps: FlowRegistryStep[];
1122
- setupDynamic?: true;
1123
- }
1124
- export {};
1125
854
  //# sourceMappingURL=contract-types.d.ts.map