@glubean/sdk 0.3.1 → 0.4.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.
- package/dist/contract-core.d.ts +15 -3
- package/dist/contract-core.d.ts.map +1 -1
- package/dist/contract-core.js +381 -149
- package/dist/contract-core.js.map +1 -1
- package/dist/contract-flow-condition.d.ts +229 -0
- package/dist/contract-flow-condition.d.ts.map +1 -0
- package/dist/contract-flow-condition.js +457 -0
- package/dist/contract-flow-condition.js.map +1 -0
- package/dist/contract-http/adapter.d.ts.map +1 -1
- package/dist/contract-http/adapter.js +19 -0
- package/dist/contract-http/adapter.js.map +1 -1
- package/dist/contract-http/types.d.ts +9 -0
- package/dist/contract-http/types.d.ts.map +1 -1
- package/dist/contract-types.d.ts +213 -21
- package/dist/contract-types.d.ts.map +1 -1
- package/dist/index.d.ts +2 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js.map +1 -1
- package/dist/test/builder.d.ts +26 -1
- package/dist/test/builder.d.ts.map +1 -1
- package/dist/test/builder.js +144 -4
- package/dist/test/builder.js.map +1 -1
- package/dist/types.d.ts +78 -14
- package/dist/types.d.ts.map +1 -1
- package/dist/types.js +4 -0
- package/dist/types.js.map +1 -1
- package/package.json +1 -1
package/dist/contract-core.d.ts
CHANGED
|
@@ -77,9 +77,6 @@ type ContractNamespace = {
|
|
|
77
77
|
*/
|
|
78
78
|
export declare function bootstrap<Needs, Params = void>(ref: ContractCaseRef<Needs, unknown>, spec: Bootstrap<Params, NoInfer<Needs>>): BootstrapAttachment<Needs, Params>;
|
|
79
79
|
export declare const contract: ContractNamespace;
|
|
80
|
-
/**
|
|
81
|
-
* Protocol-agnostic flow builder. See contract-flow.md v9 §4.1.
|
|
82
|
-
*/
|
|
83
80
|
export declare function flow(idOrMeta: string | FlowMeta): FlowBuilder<unknown>;
|
|
84
81
|
/**
|
|
85
82
|
* Core flow execution helper. Implements the Rule 1 / Rule 2 teardown
|
|
@@ -108,6 +105,21 @@ export declare function normalizeFlow<State>(runtime: RuntimeFlowProjection<Stat
|
|
|
108
105
|
* losing projection data.
|
|
109
106
|
*/
|
|
110
107
|
export declare function extractMappings(fn: (state: any) => any): FieldMapping[];
|
|
108
|
+
/**
|
|
109
|
+
* Run a single-selector lens `fn: (state) => state.a.b.c` through the strict
|
|
110
|
+
* tracing Proxy and return the read path (root segment dropped), e.g.
|
|
111
|
+
* `s => s.lookup.status` → `["lookup", "status"]`.
|
|
112
|
+
*
|
|
113
|
+
* Used by the condition/switch predicate builders (contract-flow-condition.ts)
|
|
114
|
+
* to capture a predicate's read path at construction time. The strict Proxy
|
|
115
|
+
* enforces purity (method calls / `new` / arithmetic-via-coercion throw
|
|
116
|
+
* `LensPurityError`); callers add a source-level single-selector gate on top
|
|
117
|
+
* to also reject ternaries / free-variable reads the Proxy can't see.
|
|
118
|
+
*
|
|
119
|
+
* Throws `LensPurityError` if the lens does not resolve to a single traced
|
|
120
|
+
* value (e.g. returns an object literal, a constant, or `undefined`).
|
|
121
|
+
*/
|
|
122
|
+
export declare function extractSelectorPath(fn: (state: any) => any): string[];
|
|
111
123
|
/**
|
|
112
124
|
* Extract FieldMappings from `out: (state, response) => newState`.
|
|
113
125
|
* Same purity contract as `extractMappings`.
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"contract-core.d.ts","sourceRoot":"","sources":["../src/contract-core.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;GAmBG;AAEH,OAAO,KAAK,EAAQ,WAAW,EAAE,MAAM,YAAY,CAAC;AACpD,OAAO,KAAK,EAEV,SAAS,EACT,mBAAmB,EAEnB,eAAe,EACf,uBAAuB,EAGvB,uBAAuB,EAEvB,YAAY,EACZ,WAAW,EACX,YAAY,
|
|
1
|
+
{"version":3,"file":"contract-core.d.ts","sourceRoot":"","sources":["../src/contract-core.ts"],"names":[],"mappings":"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"}
|
package/dist/contract-core.js
CHANGED
|
@@ -19,6 +19,7 @@
|
|
|
19
19
|
* - `internal/40-discovery/proposals/contract-flow.md` v9
|
|
20
20
|
*/
|
|
21
21
|
import { registerTest } from "./internal.js";
|
|
22
|
+
import { selectBranchSteps, extractBranchStep, predicateScope, assertSelectorSource, assertSwitchCaseValues, assertL2Predicate, } from "./contract-flow-condition.js";
|
|
22
23
|
import { getBootstrap, registerBootstrap } from "./bootstrap-registry.js";
|
|
23
24
|
import { getExplicitInput, getBootstrapInput, isForceStandalone, } from "./runner-input-channel.js";
|
|
24
25
|
/**
|
|
@@ -570,6 +571,166 @@ export const contract = {
|
|
|
570
571
|
/**
|
|
571
572
|
* Protocol-agnostic flow builder. See contract-flow.md v9 §4.1.
|
|
572
573
|
*/
|
|
574
|
+
// =============================================================================
|
|
575
|
+
// Branch desugar helpers (condition / switchOn / switchCond → one "branch" step)
|
|
576
|
+
//
|
|
577
|
+
// Shared by both FlowBuilder (top-level steps) and FlowFragmentBuilder (branch
|
|
578
|
+
// sub-steps). Each `then`/`else`/`default`/case callback is run against a fresh
|
|
579
|
+
// step sink whose accumulated RuntimeFlowStep[] becomes that branch's steps.
|
|
580
|
+
// =============================================================================
|
|
581
|
+
/** Build a contract-call runtime step (adapter lookup + flow-safety validation). */
|
|
582
|
+
function buildContractCallStep(flowId, ref, bindings) {
|
|
583
|
+
const adapter = _adapters.get(ref.protocol);
|
|
584
|
+
if (!adapter) {
|
|
585
|
+
throw new Error(`contract.flow(${JSON.stringify(flowId)}).step: unknown protocol "${ref.protocol}". ` +
|
|
586
|
+
`Did you forget to import a contract plugin package (e.g. "@glubean/grpc")?`);
|
|
587
|
+
}
|
|
588
|
+
if (!adapter.executeCaseInFlow) {
|
|
589
|
+
throw new Error(`contract.flow(${JSON.stringify(flowId)}).step: adapter for "${ref.protocol}" ` +
|
|
590
|
+
`does not implement executeCaseInFlow — this protocol cannot appear in a flow.`);
|
|
591
|
+
}
|
|
592
|
+
// v10: flow-safety validation fires at step-declaration time (not .case()).
|
|
593
|
+
const contractRef = ref.contract;
|
|
594
|
+
adapter.validateCaseForFlow?.(contractRef._spec, ref.caseKey, ref.contractId);
|
|
595
|
+
return {
|
|
596
|
+
kind: "contract-call",
|
|
597
|
+
name: bindings?.name,
|
|
598
|
+
ref,
|
|
599
|
+
caseKey: ref.caseKey,
|
|
600
|
+
contract: ref.contract,
|
|
601
|
+
bindings: bindings
|
|
602
|
+
? {
|
|
603
|
+
in: bindings.in,
|
|
604
|
+
out: bindings.out,
|
|
605
|
+
...(bindings.accept ? { accept: bindings.accept } : {}),
|
|
606
|
+
}
|
|
607
|
+
: undefined,
|
|
608
|
+
};
|
|
609
|
+
}
|
|
610
|
+
/** Module-private key holding a fragment sink's accumulated step list. */
|
|
611
|
+
const FRAGMENT_STEPS = Symbol("glubean.fragment-steps");
|
|
612
|
+
/**
|
|
613
|
+
* Run a branch-body callback and return the steps of the fragment it RETURNS
|
|
614
|
+
* (not a closure-shared array). Because the sink is persistent/immutable (each
|
|
615
|
+
* method returns a NEW sink carrying `[...prev, step]`), the executed steps are
|
|
616
|
+
* exactly the chain that produced the returned, type-checked fragment. A block
|
|
617
|
+
* body that mutates then returns an earlier sink — e.g.
|
|
618
|
+
* `(b) => { b.compute(reshape); return b; }` — yields that earlier sink's
|
|
619
|
+
* (shorter) step list, so runtime matches what the type system saw. This keeps
|
|
620
|
+
* the `NoExtraKeys` / invariant convergence guarantees honest.
|
|
621
|
+
*/
|
|
622
|
+
function collectFragmentSteps(flowId, fn) {
|
|
623
|
+
const result = fn(makeStepSink(flowId, []));
|
|
624
|
+
const out = result?.[FRAGMENT_STEPS];
|
|
625
|
+
if (!Array.isArray(out)) {
|
|
626
|
+
throw new Error(`flow "${flowId}": a branch body must return the fragment builder ` +
|
|
627
|
+
`(e.g. \`(b) => b.compute(...)\` or \`(b) => b\`); it returned a non-builder value`);
|
|
628
|
+
}
|
|
629
|
+
return out;
|
|
630
|
+
}
|
|
631
|
+
/**
|
|
632
|
+
* A persistent (immutable) FlowFragmentBuilder over a steps array. Every method
|
|
633
|
+
* returns a NEW sink carrying `[...steps, newStep]` — it never mutates `steps`.
|
|
634
|
+
* The accumulated list is read back (by `collectFragmentSteps`) via the
|
|
635
|
+
* module-private `FRAGMENT_STEPS` key, so only the RETURNED fragment's steps
|
|
636
|
+
* run. The phantom invariant brand exists only in the type.
|
|
637
|
+
*/
|
|
638
|
+
function makeStepSink(flowId, steps) {
|
|
639
|
+
const extend = (step) => makeStepSink(flowId, [...steps, step]);
|
|
640
|
+
// Typed `any` so methods can return sinks; the public type is the cast below.
|
|
641
|
+
const sink = {
|
|
642
|
+
step(ref, bindings) {
|
|
643
|
+
return extend(buildContractCallStep(flowId, ref, bindings));
|
|
644
|
+
},
|
|
645
|
+
compute(fn) {
|
|
646
|
+
return extend({ kind: "compute", fn });
|
|
647
|
+
},
|
|
648
|
+
condition(spec, thenB, elseB) {
|
|
649
|
+
return extend(buildConditionStep(flowId, "L2", spec, thenB, elseB));
|
|
650
|
+
},
|
|
651
|
+
conditionFn(spec, thenB, elseB) {
|
|
652
|
+
return extend(buildConditionStep(flowId, "L1", spec, thenB, elseB));
|
|
653
|
+
},
|
|
654
|
+
conditionAsync(spec, thenB, elseB) {
|
|
655
|
+
return extend(buildConditionStep(flowId, "L0", spec, thenB, elseB));
|
|
656
|
+
},
|
|
657
|
+
switchOn(lens) {
|
|
658
|
+
return (cases, deflt) => extend(buildSwitchOnStep(flowId, lens, cases, deflt));
|
|
659
|
+
},
|
|
660
|
+
switchCond(cases, deflt) {
|
|
661
|
+
return extend(buildSwitchCondStep(flowId, cases, deflt));
|
|
662
|
+
},
|
|
663
|
+
};
|
|
664
|
+
// Non-enumerable, frozen step list snapshot for this sink.
|
|
665
|
+
Object.defineProperty(sink, FRAGMENT_STEPS, {
|
|
666
|
+
value: Object.freeze(steps),
|
|
667
|
+
enumerable: false,
|
|
668
|
+
});
|
|
669
|
+
return sink;
|
|
670
|
+
}
|
|
671
|
+
/**
|
|
672
|
+
* condition / conditionFn / conditionAsync → predicate-mode branch (1 case +
|
|
673
|
+
* default). L2 builds a declarative `BranchPredicate` via `predicateScope`;
|
|
674
|
+
* L1/L0 wrap the opaque fn and REQUIRE a message (type-level + this runtime
|
|
675
|
+
* guard, so `as any` / JS callers can't produce an unlabeled opaque gate).
|
|
676
|
+
*/
|
|
677
|
+
function buildConditionStep(flowId, tier, spec, thenB, elseB) {
|
|
678
|
+
const thenSteps = collectFragmentSteps(flowId, thenB);
|
|
679
|
+
const elseSteps = elseB ? collectFragmentSteps(flowId, elseB) : [];
|
|
680
|
+
let predicate;
|
|
681
|
+
let message;
|
|
682
|
+
if (tier === "L2") {
|
|
683
|
+
predicate = spec.predicate(predicateScope());
|
|
684
|
+
assertL2Predicate(predicate, "condition"); // runtime brand: reject forged/opaque/non-JSON trees
|
|
685
|
+
message = spec.message;
|
|
686
|
+
}
|
|
687
|
+
else {
|
|
688
|
+
if (typeof spec.message !== "string" || spec.message.length === 0) {
|
|
689
|
+
throw new LensPurityError(tier === "L1" ? "conditionFn" : "conditionAsync", `opaque (${tier}) predicate requires a non-empty \`message\` — the projection marks ` +
|
|
690
|
+
`this branch as an opaque/dynamic gate and the message is its only human-facing label`);
|
|
691
|
+
}
|
|
692
|
+
message = spec.message;
|
|
693
|
+
predicate = { kind: "opaque", sync: tier === "L1", fn: spec.predicate };
|
|
694
|
+
}
|
|
695
|
+
return {
|
|
696
|
+
kind: "branch",
|
|
697
|
+
mode: "predicate",
|
|
698
|
+
cases: [{ ...(message ? { message } : {}), predicate, steps: thenSteps }],
|
|
699
|
+
default: elseSteps,
|
|
700
|
+
};
|
|
701
|
+
}
|
|
702
|
+
/** switchCond → predicate-mode branch (N cases + default), each case a declarative predicate. */
|
|
703
|
+
function buildSwitchCondStep(flowId, cases, deflt) {
|
|
704
|
+
return {
|
|
705
|
+
kind: "branch",
|
|
706
|
+
mode: "predicate",
|
|
707
|
+
cases: cases.map((c) => {
|
|
708
|
+
const predicate = c.when(predicateScope());
|
|
709
|
+
assertL2Predicate(predicate, "switchCond"); // switch is always L2 — reject forged/opaque trees
|
|
710
|
+
return { predicate, steps: collectFragmentSteps(flowId, c.then) };
|
|
711
|
+
}),
|
|
712
|
+
default: collectFragmentSteps(flowId, deflt),
|
|
713
|
+
};
|
|
714
|
+
}
|
|
715
|
+
/**
|
|
716
|
+
* switchOn → value-mode branch. The subject lens runs the same P0 source gate +
|
|
717
|
+
* strict-Proxy path extraction as `w.when` (so the decision-table projection
|
|
718
|
+
* can't lie), values must be unique + finite, and the lens is normalized to
|
|
719
|
+
* `(ctx, s)` for uniform runtime evaluation (it is evaluated exactly once).
|
|
720
|
+
*/
|
|
721
|
+
function buildSwitchOnStep(flowId, lens, cases, deflt) {
|
|
722
|
+
void flowId;
|
|
723
|
+
assertSelectorSource(lens);
|
|
724
|
+
const path = extractSelectorPath(lens);
|
|
725
|
+
assertSwitchCaseValues(cases.map((c) => c.value));
|
|
726
|
+
return {
|
|
727
|
+
kind: "branch",
|
|
728
|
+
mode: "value",
|
|
729
|
+
subject: { lens: (_ctx, s) => lens(s), path },
|
|
730
|
+
cases: cases.map((c) => ({ value: c.value, steps: collectFragmentSteps(flowId, c.then) })),
|
|
731
|
+
default: collectFragmentSteps(flowId, deflt),
|
|
732
|
+
};
|
|
733
|
+
}
|
|
573
734
|
export function flow(idOrMeta) {
|
|
574
735
|
const meta = typeof idOrMeta === "string"
|
|
575
736
|
? { id: idOrMeta }
|
|
@@ -594,34 +755,7 @@ export function flow(idOrMeta) {
|
|
|
594
755
|
return builder;
|
|
595
756
|
},
|
|
596
757
|
step(ref, bindings) {
|
|
597
|
-
|
|
598
|
-
if (!adapter) {
|
|
599
|
-
throw new Error(`contract.flow(${JSON.stringify(meta.id)}).step: unknown protocol "${ref.protocol}". ` +
|
|
600
|
-
`Did you forget to import a contract plugin package (e.g. "@glubean/grpc")?`);
|
|
601
|
-
}
|
|
602
|
-
if (!adapter.executeCaseInFlow) {
|
|
603
|
-
throw new Error(`contract.flow(${JSON.stringify(meta.id)}).step: adapter for "${ref.protocol}" ` +
|
|
604
|
-
`does not implement executeCaseInFlow — this protocol cannot appear in a flow.`);
|
|
605
|
-
}
|
|
606
|
-
// v10: flow-safety validation fires here (at step-declaration time),
|
|
607
|
-
// not at .case() time. The ref itself is pure; only when it enters a
|
|
608
|
-
// flow do we enforce adapter-specific rules like HTTP's "function-
|
|
609
|
-
// valued body/params/headers can't resolve in flow mode". This lets
|
|
610
|
-
// contract.bootstrap(ref) attach to the same ref (where function-
|
|
611
|
-
// valued fields legitimately receive resolvedInput).
|
|
612
|
-
const contractRef = ref.contract;
|
|
613
|
-
adapter.validateCaseForFlow?.(contractRef._spec, ref.caseKey, ref.contractId);
|
|
614
|
-
const step = {
|
|
615
|
-
kind: "contract-call",
|
|
616
|
-
name: bindings?.name,
|
|
617
|
-
ref,
|
|
618
|
-
caseKey: ref.caseKey,
|
|
619
|
-
contract: ref.contract,
|
|
620
|
-
bindings: bindings
|
|
621
|
-
? { in: bindings.in, out: bindings.out }
|
|
622
|
-
: undefined,
|
|
623
|
-
};
|
|
624
|
-
steps.push(step);
|
|
758
|
+
steps.push(buildContractCallStep(meta.id, ref, bindings));
|
|
625
759
|
return builder;
|
|
626
760
|
},
|
|
627
761
|
compute(fn) {
|
|
@@ -632,6 +766,28 @@ export function flow(idOrMeta) {
|
|
|
632
766
|
steps.push(step);
|
|
633
767
|
return builder;
|
|
634
768
|
},
|
|
769
|
+
condition(spec, thenB, elseB) {
|
|
770
|
+
steps.push(buildConditionStep(meta.id, "L2", spec, thenB, elseB));
|
|
771
|
+
return builder;
|
|
772
|
+
},
|
|
773
|
+
conditionFn(spec, thenB, elseB) {
|
|
774
|
+
steps.push(buildConditionStep(meta.id, "L1", spec, thenB, elseB));
|
|
775
|
+
return builder;
|
|
776
|
+
},
|
|
777
|
+
conditionAsync(spec, thenB, elseB) {
|
|
778
|
+
steps.push(buildConditionStep(meta.id, "L0", spec, thenB, elseB));
|
|
779
|
+
return builder;
|
|
780
|
+
},
|
|
781
|
+
switchOn(lens) {
|
|
782
|
+
return (cases, deflt) => {
|
|
783
|
+
steps.push(buildSwitchOnStep(meta.id, lens, cases, deflt));
|
|
784
|
+
return builder;
|
|
785
|
+
};
|
|
786
|
+
},
|
|
787
|
+
switchCond(cases, deflt) {
|
|
788
|
+
steps.push(buildSwitchCondStep(meta.id, cases, deflt));
|
|
789
|
+
return builder;
|
|
790
|
+
},
|
|
635
791
|
build() {
|
|
636
792
|
return finalize();
|
|
637
793
|
},
|
|
@@ -720,6 +876,32 @@ function stepProjectionToRegistry(step) {
|
|
|
720
876
|
writes: step.writes,
|
|
721
877
|
};
|
|
722
878
|
}
|
|
879
|
+
if (step.kind === "branch") {
|
|
880
|
+
if (step.mode === "value") {
|
|
881
|
+
return {
|
|
882
|
+
kind: "branch",
|
|
883
|
+
mode: "value",
|
|
884
|
+
name: step.name,
|
|
885
|
+
subjectPath: step.subjectPath,
|
|
886
|
+
cases: step.cases.map((c) => ({
|
|
887
|
+
value: c.value,
|
|
888
|
+
steps: c.steps.map(stepProjectionToRegistry),
|
|
889
|
+
})),
|
|
890
|
+
default: step.default.map(stepProjectionToRegistry),
|
|
891
|
+
};
|
|
892
|
+
}
|
|
893
|
+
return {
|
|
894
|
+
kind: "branch",
|
|
895
|
+
mode: "predicate",
|
|
896
|
+
name: step.name,
|
|
897
|
+
cases: step.cases.map((c) => ({
|
|
898
|
+
message: c.message,
|
|
899
|
+
predicate: c.predicate,
|
|
900
|
+
steps: c.steps.map(stepProjectionToRegistry),
|
|
901
|
+
})),
|
|
902
|
+
default: step.default.map(stepProjectionToRegistry),
|
|
903
|
+
};
|
|
904
|
+
}
|
|
723
905
|
return {
|
|
724
906
|
kind: "contract-call",
|
|
725
907
|
name: step.name,
|
|
@@ -729,6 +911,7 @@ function stepProjectionToRegistry(step) {
|
|
|
729
911
|
target: step.target,
|
|
730
912
|
inputs: step.inputs,
|
|
731
913
|
outputs: step.outputs,
|
|
914
|
+
...(step.accept ? { accept: step.accept } : {}),
|
|
732
915
|
};
|
|
733
916
|
}
|
|
734
917
|
/**
|
|
@@ -742,81 +925,96 @@ export async function runFlow(flowContract, ctx) {
|
|
|
742
925
|
? await runtime.setup(ctx)
|
|
743
926
|
: undefined;
|
|
744
927
|
try {
|
|
745
|
-
|
|
746
|
-
|
|
747
|
-
|
|
748
|
-
|
|
749
|
-
|
|
750
|
-
|
|
751
|
-
|
|
928
|
+
// Recursive step executor. `state` is the closure-level committed cell —
|
|
929
|
+
// branch sub-steps read and write the same `state`, so a taken branch can
|
|
930
|
+
// observe and mutate flow state exactly like a top-level step. Non-taken
|
|
931
|
+
// branches never run (and so never touch state). Branches nest via recursion.
|
|
932
|
+
const runSteps = async (steps) => {
|
|
933
|
+
for (const step of steps) {
|
|
934
|
+
if (step.kind === "branch") {
|
|
935
|
+
// Evaluate selector/predicates against the current committed state,
|
|
936
|
+
// then execute only the matched case's sub-steps (or default).
|
|
937
|
+
const taken = await selectBranchSteps(step, state, ctx);
|
|
938
|
+
await runSteps(taken);
|
|
939
|
+
continue;
|
|
752
940
|
}
|
|
753
|
-
|
|
754
|
-
|
|
755
|
-
|
|
756
|
-
|
|
757
|
-
|
|
941
|
+
if (step.kind === "compute") {
|
|
942
|
+
// Compute: synchronous pure function. Enforce both syntactic and
|
|
943
|
+
// value-level async rejection.
|
|
944
|
+
if (step.fn.constructor?.name === "AsyncFunction") {
|
|
945
|
+
throw new Error(`flow "${runtime.id}" compute step "${step.name ?? "<unnamed>"}": ` +
|
|
946
|
+
`async functions are not allowed — compute must be synchronous and I/O-free`);
|
|
947
|
+
}
|
|
948
|
+
const result = step.fn(state);
|
|
949
|
+
if (result && typeof result.then === "function") {
|
|
950
|
+
throw new Error(`flow "${runtime.id}" compute step "${step.name ?? "<unnamed>"}": ` +
|
|
951
|
+
`returned a thenable (Promise or Promise-like) — compute must not return async values. ` +
|
|
952
|
+
`If you need async initialization, use flow.setup() instead.`);
|
|
953
|
+
}
|
|
954
|
+
state = result;
|
|
955
|
+
continue;
|
|
758
956
|
}
|
|
759
|
-
|
|
760
|
-
|
|
761
|
-
|
|
762
|
-
|
|
763
|
-
|
|
764
|
-
|
|
765
|
-
|
|
766
|
-
|
|
767
|
-
`
|
|
768
|
-
|
|
769
|
-
|
|
770
|
-
|
|
771
|
-
|
|
772
|
-
|
|
773
|
-
|
|
774
|
-
|
|
775
|
-
|
|
776
|
-
|
|
777
|
-
|
|
778
|
-
|
|
779
|
-
|
|
780
|
-
|
|
781
|
-
|
|
782
|
-
|
|
783
|
-
|
|
784
|
-
|
|
785
|
-
|
|
786
|
-
|
|
787
|
-
|
|
788
|
-
|
|
789
|
-
|
|
790
|
-
|
|
791
|
-
|
|
792
|
-
|
|
793
|
-
|
|
794
|
-
|
|
795
|
-
|
|
796
|
-
|
|
797
|
-
|
|
798
|
-
|
|
799
|
-
|
|
800
|
-
|
|
801
|
-
|
|
802
|
-
|
|
803
|
-
|
|
804
|
-
|
|
805
|
-
|
|
806
|
-
|
|
807
|
-
|
|
957
|
+
// contract-call branch
|
|
958
|
+
const adapter = _adapters.get(step.ref.protocol);
|
|
959
|
+
if (!adapter) {
|
|
960
|
+
throw new Error(`flow "${runtime.id}" step "${step.name ?? step.caseKey}": ` +
|
|
961
|
+
`no registered adapter for protocol "${step.ref.protocol}". ` +
|
|
962
|
+
`Did you forget to import a contract plugin package?`);
|
|
963
|
+
}
|
|
964
|
+
if (!adapter.executeCaseInFlow) {
|
|
965
|
+
throw new Error(`flow "${runtime.id}" step "${step.name ?? step.caseKey}": ` +
|
|
966
|
+
`adapter for "${step.ref.protocol}" does not implement executeCaseInFlow`);
|
|
967
|
+
}
|
|
968
|
+
// v10 Phase 2d Step 2+3 follow-up (RFR v2 / v2.1 P1): enforce `needs`
|
|
969
|
+
// schema at flow boundary, not just standalone. The conditional-tuple
|
|
970
|
+
// `step()` signature catches TS authoring shape mismatches, but runtime
|
|
971
|
+
// validation is the only line of defense against:
|
|
972
|
+
// - `as any` / JS callers that bypass the TS check
|
|
973
|
+
// - Zod parse / coerce / default semantics (schema can transform
|
|
974
|
+
// the value, not just validate it)
|
|
975
|
+
// - State drift producing invalid values at runtime despite valid
|
|
976
|
+
// authoring types
|
|
977
|
+
// Standalone overlay path already validates via the same helper in
|
|
978
|
+
// `dispatchContract`'s test.fn closure; flow mirrors that to keep
|
|
979
|
+
// `needs` a true contract semantic rather than a TS-only boundary.
|
|
980
|
+
//
|
|
981
|
+
// Two-branch guard:
|
|
982
|
+
// needs present + bindings.in missing → throw (contract requires input)
|
|
983
|
+
// needs present + bindings.in present → validate unconditionally
|
|
984
|
+
// needs absent → no guard
|
|
985
|
+
const contractSpec = step.contract._spec;
|
|
986
|
+
const caseSpec = contractSpec?.cases?.[step.caseKey];
|
|
987
|
+
const needsSchema = caseSpec?.needs;
|
|
988
|
+
const hasIn = typeof step.bindings?.in === "function";
|
|
989
|
+
if (needsSchema && !hasIn) {
|
|
990
|
+
throw new Error(`flow "${runtime.id}" step "${step.name ?? step.caseKey}": ` +
|
|
991
|
+
`case "${step.ref.contractId}.${step.caseKey}" declares \`needs\` ` +
|
|
992
|
+
`but the step has no \`bindings.in\`. The TypeScript conditional ` +
|
|
993
|
+
`tuple on FlowBuilder.step() usually prevents this at compile time; ` +
|
|
994
|
+
`this runtime check catches \`as any\` / JS bypass. Provide an ` +
|
|
995
|
+
`\`in: (state) => <logical input>\` binding or remove the \`needs\` ` +
|
|
996
|
+
`schema from the case.`);
|
|
997
|
+
}
|
|
998
|
+
let resolvedInputs = step.bindings?.in?.(state);
|
|
999
|
+
if (needsSchema) {
|
|
1000
|
+
resolvedInputs = validateNeedsOutput(needsSchema, resolvedInputs, {
|
|
1001
|
+
testId: `${step.ref.contractId}.${step.caseKey}`,
|
|
1002
|
+
source: "flow",
|
|
1003
|
+
});
|
|
1004
|
+
}
|
|
1005
|
+
const response = await adapter.executeCaseInFlow({
|
|
1006
|
+
ctx,
|
|
1007
|
+
contract: step.contract,
|
|
1008
|
+
caseKey: step.caseKey,
|
|
1009
|
+
resolvedInputs,
|
|
1010
|
+
...(step.bindings?.accept ? { accept: step.bindings.accept } : {}),
|
|
808
1011
|
});
|
|
1012
|
+
if (step.bindings?.out) {
|
|
1013
|
+
state = step.bindings.out(state, response);
|
|
1014
|
+
}
|
|
809
1015
|
}
|
|
810
|
-
|
|
811
|
-
|
|
812
|
-
contract: step.contract,
|
|
813
|
-
caseKey: step.caseKey,
|
|
814
|
-
resolvedInputs,
|
|
815
|
-
});
|
|
816
|
-
if (step.bindings?.out) {
|
|
817
|
-
state = step.bindings.out(state, response);
|
|
818
|
-
}
|
|
819
|
-
}
|
|
1016
|
+
};
|
|
1017
|
+
await runSteps(runtime.steps);
|
|
820
1018
|
}
|
|
821
1019
|
finally {
|
|
822
1020
|
if (runtime.teardown) {
|
|
@@ -843,6 +1041,67 @@ export async function runFlow(flowContract, ctx) {
|
|
|
843
1041
|
* with step context so the author sees which step needs fixing.
|
|
844
1042
|
*/
|
|
845
1043
|
export function normalizeFlow(runtime) {
|
|
1044
|
+
// Recursive single-step normalizer. Branch sub-steps (cases + default) are
|
|
1045
|
+
// normalized through the same helper, so nested branches and any lens-purity
|
|
1046
|
+
// diagnostics inside them are handled identically at every depth.
|
|
1047
|
+
const normalizeStep = (s, idx) => {
|
|
1048
|
+
const stepLabel = s.name ??
|
|
1049
|
+
(s.kind === "contract-call" ? `${s.contract._projection.id}#${s.caseKey}` : `step-${idx + 1}`);
|
|
1050
|
+
if (s.kind === "branch") {
|
|
1051
|
+
// Recurse over each case/default sub-step list. We re-index from 0 within
|
|
1052
|
+
// the branch for labeling; the branch name (if any) carries outer context.
|
|
1053
|
+
return extractBranchStep(s, (steps) => steps.map((sub, i) => normalizeStep(sub, i)));
|
|
1054
|
+
}
|
|
1055
|
+
if (s.kind === "compute") {
|
|
1056
|
+
try {
|
|
1057
|
+
const { reads, writes } = traceComputeFn(s.fn);
|
|
1058
|
+
return { kind: "compute", name: s.name, reads, writes };
|
|
1059
|
+
}
|
|
1060
|
+
catch (err) {
|
|
1061
|
+
// Wrap compute-tracer failures with the same step-context format
|
|
1062
|
+
// used for lens errors, so authoring mistakes are equally easy
|
|
1063
|
+
// to localize regardless of which tracer caught them.
|
|
1064
|
+
throw new Error(`flow "${runtime.id}" step ${idx + 1} "${stepLabel}" (compute): ${err instanceof Error ? err.message : String(err)}`);
|
|
1065
|
+
}
|
|
1066
|
+
}
|
|
1067
|
+
let inputs;
|
|
1068
|
+
if (s.bindings?.in) {
|
|
1069
|
+
try {
|
|
1070
|
+
inputs = extractMappings(s.bindings.in);
|
|
1071
|
+
}
|
|
1072
|
+
catch (err) {
|
|
1073
|
+
if (err instanceof LensPurityError) {
|
|
1074
|
+
throw new Error(`flow "${runtime.id}" step ${idx + 1} "${stepLabel}" (in lens): ${err.message}`);
|
|
1075
|
+
}
|
|
1076
|
+
throw err;
|
|
1077
|
+
}
|
|
1078
|
+
}
|
|
1079
|
+
let outputs;
|
|
1080
|
+
if (s.bindings?.out) {
|
|
1081
|
+
try {
|
|
1082
|
+
outputs = extractMappingsOut(s.bindings.out);
|
|
1083
|
+
}
|
|
1084
|
+
catch (err) {
|
|
1085
|
+
if (err instanceof LensPurityError) {
|
|
1086
|
+
throw new Error(`flow "${runtime.id}" step ${idx + 1} "${stepLabel}" (out lens): ${err.message}`);
|
|
1087
|
+
}
|
|
1088
|
+
throw err;
|
|
1089
|
+
}
|
|
1090
|
+
}
|
|
1091
|
+
return {
|
|
1092
|
+
kind: "contract-call",
|
|
1093
|
+
name: s.name,
|
|
1094
|
+
contractId: s.contract._projection.id,
|
|
1095
|
+
caseKey: s.caseKey,
|
|
1096
|
+
protocol: s.ref.protocol,
|
|
1097
|
+
target: s.ref.target,
|
|
1098
|
+
inputs,
|
|
1099
|
+
outputs,
|
|
1100
|
+
...(s.bindings?.accept
|
|
1101
|
+
? { accept: [...s.bindings.accept] }
|
|
1102
|
+
: {}),
|
|
1103
|
+
};
|
|
1104
|
+
};
|
|
846
1105
|
return {
|
|
847
1106
|
id: runtime.id,
|
|
848
1107
|
protocol: "flow",
|
|
@@ -852,56 +1111,7 @@ export function normalizeFlow(runtime) {
|
|
|
852
1111
|
...(runtime.skip !== undefined ? { skip: runtime.skip } : {}),
|
|
853
1112
|
...(runtime.only !== undefined ? { only: runtime.only } : {}),
|
|
854
1113
|
setupDynamic: runtime.setup ? true : undefined,
|
|
855
|
-
steps: runtime.steps.map(
|
|
856
|
-
const stepLabel = s.name ??
|
|
857
|
-
(s.kind === "contract-call" ? `${s.contract._projection.id}#${s.caseKey}` : `step-${idx + 1}`);
|
|
858
|
-
if (s.kind === "compute") {
|
|
859
|
-
try {
|
|
860
|
-
const { reads, writes } = traceComputeFn(s.fn);
|
|
861
|
-
return { kind: "compute", name: s.name, reads, writes };
|
|
862
|
-
}
|
|
863
|
-
catch (err) {
|
|
864
|
-
// Wrap compute-tracer failures with the same step-context format
|
|
865
|
-
// used for lens errors, so authoring mistakes are equally easy
|
|
866
|
-
// to localize regardless of which tracer caught them.
|
|
867
|
-
throw new Error(`flow "${runtime.id}" step ${idx + 1} "${stepLabel}" (compute): ${err instanceof Error ? err.message : String(err)}`);
|
|
868
|
-
}
|
|
869
|
-
}
|
|
870
|
-
let inputs;
|
|
871
|
-
if (s.bindings?.in) {
|
|
872
|
-
try {
|
|
873
|
-
inputs = extractMappings(s.bindings.in);
|
|
874
|
-
}
|
|
875
|
-
catch (err) {
|
|
876
|
-
if (err instanceof LensPurityError) {
|
|
877
|
-
throw new Error(`flow "${runtime.id}" step ${idx + 1} "${stepLabel}" (in lens): ${err.message}`);
|
|
878
|
-
}
|
|
879
|
-
throw err;
|
|
880
|
-
}
|
|
881
|
-
}
|
|
882
|
-
let outputs;
|
|
883
|
-
if (s.bindings?.out) {
|
|
884
|
-
try {
|
|
885
|
-
outputs = extractMappingsOut(s.bindings.out);
|
|
886
|
-
}
|
|
887
|
-
catch (err) {
|
|
888
|
-
if (err instanceof LensPurityError) {
|
|
889
|
-
throw new Error(`flow "${runtime.id}" step ${idx + 1} "${stepLabel}" (out lens): ${err.message}`);
|
|
890
|
-
}
|
|
891
|
-
throw err;
|
|
892
|
-
}
|
|
893
|
-
}
|
|
894
|
-
return {
|
|
895
|
-
kind: "contract-call",
|
|
896
|
-
name: s.name,
|
|
897
|
-
contractId: s.contract._projection.id,
|
|
898
|
-
caseKey: s.caseKey,
|
|
899
|
-
protocol: s.ref.protocol,
|
|
900
|
-
target: s.ref.target,
|
|
901
|
-
inputs,
|
|
902
|
-
outputs,
|
|
903
|
-
};
|
|
904
|
-
}),
|
|
1114
|
+
steps: runtime.steps.map(normalizeStep),
|
|
905
1115
|
};
|
|
906
1116
|
}
|
|
907
1117
|
/**
|
|
@@ -919,6 +1129,28 @@ export function extractMappings(fn) {
|
|
|
919
1129
|
const result = fn(proxy);
|
|
920
1130
|
return collectMappings(result, []);
|
|
921
1131
|
}
|
|
1132
|
+
/**
|
|
1133
|
+
* Run a single-selector lens `fn: (state) => state.a.b.c` through the strict
|
|
1134
|
+
* tracing Proxy and return the read path (root segment dropped), e.g.
|
|
1135
|
+
* `s => s.lookup.status` → `["lookup", "status"]`.
|
|
1136
|
+
*
|
|
1137
|
+
* Used by the condition/switch predicate builders (contract-flow-condition.ts)
|
|
1138
|
+
* to capture a predicate's read path at construction time. The strict Proxy
|
|
1139
|
+
* enforces purity (method calls / `new` / arithmetic-via-coercion throw
|
|
1140
|
+
* `LensPurityError`); callers add a source-level single-selector gate on top
|
|
1141
|
+
* to also reject ternaries / free-variable reads the Proxy can't see.
|
|
1142
|
+
*
|
|
1143
|
+
* Throws `LensPurityError` if the lens does not resolve to a single traced
|
|
1144
|
+
* value (e.g. returns an object literal, a constant, or `undefined`).
|
|
1145
|
+
*/
|
|
1146
|
+
export function extractSelectorPath(fn) {
|
|
1147
|
+
const result = fn(makeLensProxy("state"));
|
|
1148
|
+
if (!isTracedValue(result)) {
|
|
1149
|
+
throw new LensPurityError("state", "selector must read a single state path (e.g. s => s.a.b), not compute or repack a value");
|
|
1150
|
+
}
|
|
1151
|
+
const full = result[TRACE_MARKER]; // "state.lookup.status"
|
|
1152
|
+
return full.split(".").slice(1); // drop "state" root
|
|
1153
|
+
}
|
|
922
1154
|
/**
|
|
923
1155
|
* Extract FieldMappings from `out: (state, response) => newState`.
|
|
924
1156
|
* Same purity contract as `extractMappings`.
|