fyflow-orchestration 0.12.0 → 0.13.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/AGENTS.md +93 -9
- package/README.md +1 -1
- package/dist/browser/chunks/{chunk-4JFQX7DL.js → chunk-53EDI674.js} +66 -22
- package/dist/browser/index.js +12 -4
- package/dist/browser/scheduler/index.js +1 -1
- package/dist/node/chunks/{chunk-4JFQX7DL.js → chunk-53EDI674.js} +66 -22
- package/dist/node/index.js +12 -4
- package/dist/node/scheduler/index.js +1 -1
- package/dist/types/src/definition/condition.d.ts +7 -4
- package/dist/types/src/definition/types.d.ts +31 -7
- package/dist/types/src/definition/validate.d.ts +3 -2
- package/package.json +1 -1
package/AGENTS.md
CHANGED
|
@@ -818,7 +818,8 @@ entries are held to the id charset, a non-string refused — as `validateDefinit
|
|
|
818
818
|
tripped over it, and is refused now. **And the other way, since 0.12.0** (`spec/flow-definition.md` v0.53,
|
|
819
819
|
[ADR 0137](doc/decisions/0137-a-key-path-indexes-an-array-by-a-bracket-suffix-and-a-plain-segment-keeps-its-meaning.md)):
|
|
820
820
|
a gate's key and a comparison's `path` may carry a bracket index — `files[0].series`, `outcome.result.files[0].series` — so both patterns accept it — and
|
|
821
|
-
`KEY_PATH_RE`, the gate key's, is the exported constant whose `source` moved. Only more is accepted.
|
|
821
|
+
`KEY_PATH_RE`, the gate key's, is the exported constant whose `source` moved. Only more is accepted. *(It, the parse and
|
|
822
|
+
the read a host may call are section 4's* Reading a key path the way the library does*.)*
|
|
822
823
|
|
|
823
824
|
| | |
|
|
824
825
|
|---|---|
|
|
@@ -894,14 +895,14 @@ a flow document can say what a branch depends on without any code at all, which
|
|
|
894
895
|
an agent emit a complete flow. A name still works and is what a predicate that COMPUTES uses.
|
|
895
896
|
|
|
896
897
|
```ts run
|
|
897
|
-
import { validateDefinition, type FlowDefinition } from 'fyflow-orchestration';
|
|
898
|
+
import { evaluateWhen, validateDefinition, type FlowDefinition, type When } from 'fyflow-orchestration';
|
|
898
899
|
|
|
899
900
|
// A `when` is EXACTLY ONE of three shapes, told apart by which keys it carries.
|
|
900
901
|
const flow: FlowDefinition = {
|
|
901
902
|
flowId: 'declared', version: 1, entry: 'score',
|
|
902
903
|
nodes: [
|
|
903
904
|
{ id: 'score', kind: 'task', type: 'ScoreWorker', edges: [
|
|
904
|
-
// 1. a COMPARISON — a key path over the context, an operator, a literal.
|
|
905
|
+
// 1. a COMPARISON — a key path over the context, an operator, a literal (or `valueFrom`, below).
|
|
905
906
|
{ when: { path: 'outcome.result.confidence', op: 'lt', value: 0.5 }, to: 'review' },
|
|
906
907
|
// 2. a flat GROUP — `all` or `any`, whose members are comparisons.
|
|
907
908
|
{ when: { any: [
|
|
@@ -940,16 +941,41 @@ if (refuse({ path: 'result.confidence', op: 'lt', value: 0.5 })[0] !== 'FD-V43')
|
|
|
940
941
|
if (refuse({ path: 'outcome.result.name', op: 'matches', value: '^a' })[0] !== 'FD-V44') throw new Error('an unknown operator is FD-V44');
|
|
941
942
|
// FD-V45 — a group is FLAT, and a member naming a registered predicate could be IMPURE.
|
|
942
943
|
if (refuse({ all: [{ any: [{ path: 'outcome.result.a', op: 'eq', value: 1 }] }] })[0] !== 'FD-V45') throw new Error('nesting is FD-V45');
|
|
944
|
+
|
|
945
|
+
// `valueFrom` (since 0.13.0) — compare against ANOTHER key path over the same context, in place of `value`.
|
|
946
|
+
// Exactly one of the two: both, or neither, is FD-V42. It takes the same roots as `path` (FD-V43).
|
|
947
|
+
if (refuse({ path: 'outcome.result.score', op: 'gte', valueFrom: 'outcome.result.threshold' }).length !== 0) throw new Error('valueFrom alone is valid');
|
|
948
|
+
if (refuse({ path: 'outcome.result.score', op: 'gte', value: 50, valueFrom: 'outcome.result.threshold' })[0] !== 'FD-V42') throw new Error('both is FD-V42');
|
|
949
|
+
if (refuse({ path: 'outcome.result.score', op: 'gte', valueFrom: 'result.threshold' })[0] !== 'FD-V43') throw new Error('a rootless valueFrom is FD-V43');
|
|
950
|
+
// A `needs` path — on either side — reads a key THIS EDGE's `needs` declares; `refuse`'s edge declares none, so FD-V43.
|
|
951
|
+
if (refuse({ path: 'outcome.result.size', op: 'lte', valueFrom: 'needs.limit.max' })[0] !== 'FD-V43') throw new Error('an undeclared needs key is FD-V43');
|
|
952
|
+
|
|
953
|
+
// What it resolves to is compared as a literal would be — and an ABSENT one is NO MATCH for every operator,
|
|
954
|
+
// so `x gt y` and `x lte y` are BOTH false where `y` is absent. No `default` stands in for it: that is the left's.
|
|
955
|
+
const ask = (when: When, result: unknown) => evaluateWhen(when, { outcome: { result } as never, needs: {} }, new Map());
|
|
956
|
+
const gt: When = { path: 'outcome.result.x', op: 'gt', valueFrom: 'outcome.result.y' };
|
|
957
|
+
const lte: When = { path: 'outcome.result.x', op: 'lte', valueFrom: 'outcome.result.y' };
|
|
958
|
+
if (!ask(gt, { x: 5, y: 3 }) || ask(lte, { x: 5, y: 3 })) throw new Error('5 gt 3, and not 5 lte 3');
|
|
959
|
+
if (ask(gt, { x: 5 }) || ask(lte, { x: 5 })) throw new Error('y absent: both are false');
|
|
960
|
+
// A resolved object or array is no match too, `ne` included — `!==` would have said true.
|
|
961
|
+
if (ask({ path: 'outcome.result.x', op: 'ne', valueFrom: 'outcome.result.y' }, { x: 5, y: { v: 5 } })) throw new Error('ne against an object is no match');
|
|
943
962
|
```
|
|
944
963
|
|
|
945
964
|
| | |
|
|
946
965
|
|---|---|
|
|
947
|
-
| `path` | a key path over the context, **rooted at `outcome` or `needs`** — `outcome.result.x`, `outcome.taskId`, `needs.earlier.flag`, and an array element by a bracket index after a key, `outcome.result.items[0].score` (never on the root). No wildcard, no expression |
|
|
966
|
+
| `path` | a key path over the context, **rooted at `outcome` or `needs`** — `outcome.result.x`, `outcome.taskId`, `needs.earlier.flag`, and an array element by a bracket index after a key, `outcome.result.items[0].score` (never on the root). No wildcard, no expression. **A `needs` path names a key THIS EDGE's `needs` declares** (since 0.13.0, `FD-V43`): any other would be absent for ever |
|
|
948
967
|
| `op` | one of `eq`, `ne`, `lt`, `lte`, `gt`, `gte`, `startsWith`, and **nothing else** |
|
|
949
|
-
| `value` | a scalar: `null`, a boolean, a finite number or a string |
|
|
968
|
+
| `value` | a scalar: `null`, a boolean, a finite number or a string. **Exactly one of `value` and `valueFrom`** (`FD-V42`) |
|
|
969
|
+
| `valueFrom` | since 0.13.0, in place of `value` — **another key path over the same context**, held to everything `path` is; what it resolves to is compared as a literal would be. **No match where it does not resolve, or resolves to an object or an array** |
|
|
950
970
|
| `default` | optional — what is compared when the path does not resolve **or resolves to `null`**, which is exactly `??`. Without it an absent path is **no match**, and a resolved `null` is compared as the scalar it is |
|
|
951
971
|
| `length` | optional `true` — compare the LENGTH of an array or a string, after `default` |
|
|
952
972
|
|
|
973
|
+
**The type moved with it, in 0.13.0.** `PredicateLeaf` is a two-member union — `{ value, valueFrom?: never }` or
|
|
974
|
+
`{ valueFrom, value?: never }` over the shared `PredicateLeafBase` — so a comparison with both, or neither, fails to
|
|
975
|
+
compile, and code READING `leaf.value` now sees `PredicateScalar | undefined`: tell the two apart by `'valueFrom' in leaf`.
|
|
976
|
+
`PREDICATE_LEAF_KEYS` gained `valueFrom` and `PREDICATE_LEAF_REQUIRED` lost `value`; `predicateProblem(when, edgeNeeds?)`
|
|
977
|
+
takes the keys an edge's `needs` declares, and asks the `needs` rule only when given them.
|
|
978
|
+
|
|
953
979
|
**Three things to know before you write one.**
|
|
954
980
|
**A comparison between kinds is no match, never a coercion** — `{ op: 'lt', value: 50 }` is false
|
|
955
981
|
against the string `"9"` where JavaScript's `<` would say true.
|
|
@@ -960,6 +986,10 @@ holding `null` as well as a missing one.
|
|
|
960
986
|
that holds over NUMBERS, and over an absent path where you declared a `default`. Over a value an
|
|
961
987
|
ordering operator cannot compare, a string or a boolean, **both it and its complement are false**.
|
|
962
988
|
`startsWith` has no complement here at all.
|
|
989
|
+
**An absent `valueFrom` is no match for every operator, and nothing makes it otherwise** — `default` and `length`
|
|
990
|
+
are the LEFT side's, and there is no `valueDefault`. So `x gt y` and `x lte y` are BOTH false where `y` is
|
|
991
|
+
absent: route the absent case by its own edge if it matters. And a `valueFrom` resolving to an object or an
|
|
992
|
+
array is no match even for `ne`, where an object on the LEFT with `ne` is still true — the left side is 0.12.0's.
|
|
963
993
|
**A declared predicate CANNOT be impure.** There is no clock, no random source and no way to name
|
|
964
994
|
anything but the context, so re-evaluating it over the same log gives the same answer. A registered
|
|
965
995
|
name has no such guarantee — which is the trade, and why a regular expression, a date window or a
|
|
@@ -1097,6 +1127,58 @@ the run did not produce stops the run with an `UnresolvedReference` (`FD-R3`, se
|
|
|
1097
1127
|
node's does. **An edge with no `when` may carry a `needs` and nothing will ever read it** — accepted
|
|
1098
1128
|
and inert, which `spec/flow-definition.md` section 3 records rather than refuses.
|
|
1099
1129
|
|
|
1130
|
+
### Reading a key path the way the library does — `readKeyPath`, `keyPathSteps`, `KEY_PATH_RE`, `PREDICATE_ROOTS`
|
|
1131
|
+
|
|
1132
|
+
**A host that suggests, checks or previews a key path need not keep its own copy of the spelling.** Five names, all
|
|
1133
|
+
from the root entry in node, the browser and Deno — `readKeyPath`, `KEY_PATH_RE` and `PREDICATE_ROOTS` since before
|
|
1134
|
+
0.12.0, **`keyPathSteps` and its `KeyPathStep` type since 0.13.0** (P45, fyflow-app's pass 6 #2). They are the ones a
|
|
1135
|
+
gate's `settings.key`, a comparison's `path` and the wiring check read, so a host using them cannot read a path
|
|
1136
|
+
differently from a run.
|
|
1137
|
+
|
|
1138
|
+
| name | what it is |
|
|
1139
|
+
|---|---|
|
|
1140
|
+
| `KEY_PATH_RE` | the GRAMMAR (`spec/flow-definition.md` section 2's KEY PATH): dot-separated object keys, each `[A-Za-z0-9_-]{1,64}` optionally followed by array indexes `[n]` — `n` is `0` or a number with no leading zero, at most 999,999,999 — never an index leading the path; no quoted key, no wildcard, no expression. A gate's `key` is refused `FD-V40` outside it |
|
|
1141
|
+
| `keyPathSteps(path)` | the PARSE: a path's steps in order, each `{ key }` or `{ index }` — that union is `KeyPathStep`. **Total**: any string has steps, and a segment the grammar does not spell is ONE literal key (`files[*]` is a key named `files[*]`), so test the path with `KEY_PATH_RE` first |
|
|
1142
|
+
| `readKeyPath(value, path)` | the READ: a KEY step is an OWN key of a PLAIN object, an INDEX step an own element of a REAL array; anything else — a missing key, `null`, an array under a key step, a class instance, a hole — is `undefined`. **A plain numeric segment is a KEY**: `xs.1` is nothing over an array, `xs[1]` is its element. The value at the end is returned whatever it is |
|
|
1143
|
+
| `PREDICATE_ROOTS` | a task's or a join's edge comparison's two ROOTS, `outcome` and `needs` (`FD-V43`): the first key of its `path` (and of a `valueFrom`), read up to the first `.` or `[`; never indexed, never alone. **An `if` gate's one root, `input`, is not a public name** — section 19 |
|
|
1144
|
+
|
|
1145
|
+
```ts run
|
|
1146
|
+
import { KEY_PATH_RE, keyPathSteps, PREDICATE_ROOTS, readKeyPath, type KeyPathStep } from 'fyflow-orchestration';
|
|
1147
|
+
|
|
1148
|
+
const value = { files: [{ name: 'a.csv' }, { name: 'b.csv' }], xs: ['p', 'q'] };
|
|
1149
|
+
|
|
1150
|
+
// The grammar, the parse and the read of one path.
|
|
1151
|
+
if (!KEY_PATH_RE.test('files[0].name')) throw new Error('files[0].name is a key path');
|
|
1152
|
+
const steps: KeyPathStep[] = keyPathSteps('files[0].name');
|
|
1153
|
+
if (JSON.stringify(steps) !== '[{"key":"files"},{"index":0},{"key":"name"}]') throw new Error(`steps: ${JSON.stringify(steps)}`);
|
|
1154
|
+
if (readKeyPath(value, 'files[0].name') !== 'a.csv') throw new Error('files[0].name reads a.csv');
|
|
1155
|
+
if (readKeyPath(value, 'files[2].name') !== undefined) throw new Error('past the end is nothing');
|
|
1156
|
+
|
|
1157
|
+
// A plain numeric segment is a KEY, and an array has none: xs.1 is nothing where xs[1] is 'q'.
|
|
1158
|
+
if (readKeyPath(value, 'xs[1]') !== 'q' || readKeyPath(value, 'xs.1') !== undefined) throw new Error('xs.1 is a key');
|
|
1159
|
+
|
|
1160
|
+
// A malformed path is refused by the grammar — and the parse, being total, still answers.
|
|
1161
|
+
for (const bad of ['files[*].name', 'xs[01]', '[0].x', 'a..b']) {
|
|
1162
|
+
if (KEY_PATH_RE.test(bad)) throw new Error(`${bad} is not a key path`);
|
|
1163
|
+
}
|
|
1164
|
+
const first = keyPathSteps('files[*].name')[0]!;
|
|
1165
|
+
if (!('key' in first) || first.key !== 'files[*]') throw new Error('an unspelled segment is one literal key');
|
|
1166
|
+
|
|
1167
|
+
// A comparison's path on an edge: a key path whose first key is a root, with a key after it. NECESSARY, not
|
|
1168
|
+
// sufficient: FD-V43 also holds a `needs.X` path to an X the edge's own `needs` declares (since 0.13.0), which
|
|
1169
|
+
// these names cannot see — `validateDefinition` is the authority on a whole flow.
|
|
1170
|
+
const edgePath = (p: string) => KEY_PATH_RE.test(p) && (PREDICATE_ROOTS as readonly string[]).includes(p.split(/[.[]/)[0]!) && /^[A-Za-z0-9_-]+\./.test(p);
|
|
1171
|
+
if (!edgePath('outcome.result.files[0].size') || edgePath('result.size') || edgePath('outcome[0].x') || edgePath('needs')) {
|
|
1172
|
+
throw new Error('the grammar and the roots, as FD-V43 reads them on a path alone');
|
|
1173
|
+
}
|
|
1174
|
+
```
|
|
1175
|
+
|
|
1176
|
+
**What is not exported, and why.** No *is this a valid comparison path* function: the grammar and the roots are
|
|
1177
|
+
these two values, and a second spelling of `FD-V43` would be a second thing to hold in step — `validateDefinition`
|
|
1178
|
+
is the authority on a whole flow. Nor the declared-comparison internals `evalLeaf` and `evaluateDeclared`: a
|
|
1179
|
+
`when` is evaluated over a `DecisionContext` by **`evaluateWhen(when, ctx, predicates)`**, which IS public from the root entry
|
|
1180
|
+
(`predicates` a `Map` of the registered names it may call), and the built-in `if` evaluates its `when` over `{ input }` inside a run.
|
|
1181
|
+
|
|
1100
1182
|
---
|
|
1101
1183
|
|
|
1102
1184
|
## 5. Fan-out, join, and which policy to pass
|
|
@@ -5387,7 +5469,7 @@ registered:
|
|
|
5387
5469
|
|---|---|---|
|
|
5388
5470
|
| **settings** | `{ when }` — a comparison or a flat `all`/`any` group, section 4's grammar, whose paths start at **`input`**: the value that reached the gate | `{ key, cases }` — `key` a key path as `distinct`'s, `cases` a map from a CASE NAME to a scalar or a non-empty array of scalars |
|
|
5389
5471
|
| **routes** | `then`, and `else` — the DEFAULT, so an edge with no `route` is the fallback | every case name, then `default` — the DEFAULT. So a case left unwired is refused at load, NAMING the case (`FD-V41`) |
|
|
5390
|
-
| **matches** | `when` holds over `{ input }`, by the evaluator a task edge's `when` uses — the same operators, absent rule, `default` and `length` | the case one of whose values is `===` the resolved key: `"1"` and `1` are two values, and a value in two cases is refused at load |
|
|
5472
|
+
| **matches** | `when` holds over `{ input }`, by the evaluator a task edge's `when` uses — the same operators, absent rule, `default` and `length`, and since 0.13.0 a `valueFrom` over `input` too (`input.a` `lt` `valueFrom: "input.b"`; `input.b` absent is no match, so `else`) | the case one of whose values is `===` the resolved key: `"1"` and `1` are two values, and a value in two cases is refused at load |
|
|
5391
5473
|
| **refused at load (`FD-V40`)** | a registered predicate NAME (a gate has no outcome to hand it), a root other than `input` — or `input` alone — and every `when` defect section 4 refuses, named | a case named `default` or outside the route charset, a value that is not a scalar, an empty array, a value in two cases or twice in one, an empty `cases` |
|
|
5392
5474
|
|
|
5393
5475
|
- **Neither reads the gate's memory**: two branches with one key take the same route at a `switch`, where
|
|
@@ -5640,8 +5722,9 @@ is underneath it.**
|
|
|
5640
5722
|
library's own sub-path — `import { jsonata } from 'fyflow-orchestration/jsonata'` — so it is one
|
|
5641
5723
|
import and one object entry with no logic in it, but it is not nothing and it is not a domain
|
|
5642
5724
|
worker. **A flow that only SELECTS needs no adapter; a flow that COMPUTES does.**
|
|
5643
|
-
2. **A comparison reads ONE path and compares it to ONE literal
|
|
5644
|
-
|
|
5725
|
+
2. **A comparison reads ONE path and compares it to ONE literal — or, since 0.13.0, to ONE other path
|
|
5726
|
+
(`valueFrom`).** It cannot do arithmetic, so *is the threshold this run carries* is a comparison now
|
|
5727
|
+
(`valueFrom: "needs.cfg.threshold"`, the edge declaring `cfg`) and *is the MEAN below it* is not.
|
|
5645
5728
|
**The route around it is data**: a tier-three `Transform` node computes the answer and the `when`
|
|
5646
5729
|
reads `outcome.result.<field>`. That is what the flow below does, and it is the one place the
|
|
5647
5730
|
three-tier rule's *reshape for … a decision* is load-bearing rather than advisory — a `transform`
|
|
@@ -5651,7 +5734,8 @@ is underneath it.**
|
|
|
5651
5734
|
> **A comparison whose `value` is meant to be a path is NOT refused** — `{ "op": "eq", "value":
|
|
5652
5735
|
> "needs.cfg.threshold" }` is a perfectly good string comparison against the literal text, and it is
|
|
5653
5736
|
> false for ever. The grammar catches a wrong ROOT (`FD-V43`) and nothing catches this
|
|
5654
|
-
> ([Q456](doc/questions/q456.md)). Write the literal
|
|
5737
|
+
> ([Q456](doc/questions/q456.md)). Write the literal — or, since 0.13.0, write the path as `valueFrom`, which is
|
|
5738
|
+
> what reads it.
|
|
5655
5739
|
|
|
5656
5740
|
> **This flow is `examples/flows/ui-emitted.flow.json` and this registry is
|
|
5657
5741
|
> `examples/ui-registry.json`, with the SCHEMAS reduced to what these three verdicts turn on.** The
|
package/README.md
CHANGED
|
@@ -17,7 +17,7 @@ worker returned, routing can **fan out** into parallel branches, and a **join**
|
|
|
17
17
|
The scheduler runs tasks; this library decides **which task runs next**. `fyflow-scheduler` has no
|
|
18
18
|
dependency graph of its own, so sequence, decision, fan-out and join live here.
|
|
19
19
|
|
|
20
|
-
Published on npm as `fyflow-orchestration` and on JSR as `@fyflow/orchestration`. The manifests read `0.
|
|
20
|
+
Published on npm as `fyflow-orchestration` and on JSR as `@fyflow/orchestration`. The manifests read `0.13.0`.
|
|
21
21
|
|
|
22
22
|
## Install
|
|
23
23
|
|
|
@@ -402,7 +402,13 @@ function evalLeaf(scope, p, unknownOp) {
|
|
|
402
402
|
else if (typeof v === "string") v = v.length;
|
|
403
403
|
else return false;
|
|
404
404
|
}
|
|
405
|
-
|
|
405
|
+
let w;
|
|
406
|
+
if (p.valueFrom !== void 0) {
|
|
407
|
+
w = readKeyPath(scope, p.valueFrom);
|
|
408
|
+
if (w === void 0 || typeof w === "object" && w !== null) return false;
|
|
409
|
+
} else {
|
|
410
|
+
w = p.value;
|
|
411
|
+
}
|
|
406
412
|
switch (p.op) {
|
|
407
413
|
// **Type-STRICT, and it is a departure measured rather than assumed.** Under JS comparison
|
|
408
414
|
// `{confidence: "0.4"} < 0.5` is true and every `typeof`-guarded predicate in this tree is false;
|
|
@@ -755,8 +761,9 @@ function gateTypeCollisions(x) {
|
|
|
755
761
|
return names.filter((n) => Object.prototype.hasOwnProperty.call(BUILT_IN_GATE_TYPES, n)).sort();
|
|
756
762
|
}
|
|
757
763
|
var PREDICATE_NAME_KEYS = ["predicate"];
|
|
758
|
-
var PREDICATE_LEAF_KEYS = ["path", "op", "value", "default", "length"];
|
|
759
|
-
var PREDICATE_LEAF_REQUIRED = ["path", "op"
|
|
764
|
+
var PREDICATE_LEAF_KEYS = ["path", "op", "value", "valueFrom", "default", "length"];
|
|
765
|
+
var PREDICATE_LEAF_REQUIRED = ["path", "op"];
|
|
766
|
+
var RIGHT_SIDE_KEYS = ["value", "valueFrom"];
|
|
760
767
|
var PREDICATE_GROUP_KEYS = ["all", "any"];
|
|
761
768
|
function predicateShapeOf(o) {
|
|
762
769
|
const marks = [];
|
|
@@ -783,29 +790,53 @@ var GATE_SCOPE = {
|
|
|
783
790
|
foreignRoot: () => 'which an if gate cannot see \u2014 its one root is "input", the value that reached the gate; a gate has no outcome and no edge needs, and any other first segment would resolve to nothing in silence',
|
|
784
791
|
rootAlone: () => "and names no key \u2014 a gate's path names a key INSIDE the value that reached it, as every gate's key path does; a value that must be compared whole is wrapped by the task before the gate"
|
|
785
792
|
};
|
|
786
|
-
function
|
|
787
|
-
const
|
|
788
|
-
if (extra.length) {
|
|
789
|
-
return { rule: "FD-V42", message: `${where} carries ${extra.map((k) => JSON.stringify(k)).join(", ")}, which a comparison does not declare; it is { ${PREDICATE_LEAF_KEYS.join(", ")} }` };
|
|
790
|
-
}
|
|
791
|
-
for (const k of PREDICATE_LEAF_REQUIRED) {
|
|
792
|
-
if (!(k in o)) return { rule: "FD-V42", message: `${where} is missing required key "${k}"; a comparison is { ${PREDICATE_LEAF_KEYS.join(", ")} } with ${PREDICATE_LEAF_REQUIRED.join(", ")} required` };
|
|
793
|
-
}
|
|
794
|
-
const path = o.path;
|
|
793
|
+
function comparisonPathProblem(o, key, where, scope) {
|
|
794
|
+
const path = o[key];
|
|
795
795
|
if (typeof path !== "string" || !KEY_PATH_RE.test(path)) {
|
|
796
|
-
return { rule: "FD-V43", message: `${where}
|
|
796
|
+
return { rule: "FD-V43", message: `${where} ${key} ${JSON.stringify(path)} is not a key path \u2014 ${KEY_PATH_WORDS}` };
|
|
797
797
|
}
|
|
798
|
-
const
|
|
798
|
+
const segments = path.split(".");
|
|
799
|
+
const first = segments[0];
|
|
799
800
|
const bracket = first.indexOf("[");
|
|
800
801
|
const root = bracket === -1 ? first : first.slice(0, bracket);
|
|
801
802
|
if (!scope.roots.includes(root)) {
|
|
802
|
-
return { rule: "FD-V43", message: `${where}
|
|
803
|
+
return { rule: "FD-V43", message: `${where} ${key} ${JSON.stringify(path)} starts at ${JSON.stringify(root)}, ${scope.foreignRoot(root)}` };
|
|
803
804
|
}
|
|
804
805
|
if (bracket !== -1) {
|
|
805
|
-
return { rule: "FD-V43", message: `${where}
|
|
806
|
+
return { rule: "FD-V43", message: `${where} ${key} ${JSON.stringify(path)} indexes its root ${JSON.stringify(root)} \u2014 a path names a KEY inside its root first, as a gate's key path names a key inside the value that reached it; an index follows a key (${root}.items[0])` };
|
|
807
|
+
}
|
|
808
|
+
if (segments.length < 2) {
|
|
809
|
+
return { rule: "FD-V43", message: `${where} ${key} ${JSON.stringify(path)} is a root alone ${scope.rootAlone(root)}` };
|
|
810
|
+
}
|
|
811
|
+
if (root === "needs" && scope.needsDeclared !== void 0) {
|
|
812
|
+
const seg = segments[1];
|
|
813
|
+
const at = seg.indexOf("[");
|
|
814
|
+
const name = at === -1 ? seg : seg.slice(0, at);
|
|
815
|
+
if (!scope.needsDeclared.includes(name)) {
|
|
816
|
+
const declared = scope.needsDeclared.length === 0 ? "declares no `needs`, so its `ctx.needs` is {}" : `declares needs ${scope.needsDeclared.map((k2) => JSON.stringify(k2)).join(", ")}`;
|
|
817
|
+
return { rule: "FD-V43", message: `${where} ${key} ${JSON.stringify(path)} reads needs key ${JSON.stringify(name)}, and this edge ${declared} \u2014 the key would be absent for ever and the comparison no match for ever; declare it in the edge's needs` };
|
|
818
|
+
}
|
|
819
|
+
}
|
|
820
|
+
return null;
|
|
821
|
+
}
|
|
822
|
+
function leafProblem(o, where, scope = EDGE_SCOPE) {
|
|
823
|
+
const extra = Object.keys(o).filter((k) => !PREDICATE_LEAF_KEYS.includes(k));
|
|
824
|
+
if (extra.length) {
|
|
825
|
+
return { rule: "FD-V42", message: `${where} carries ${extra.map((k) => JSON.stringify(k)).join(", ")}, which a comparison does not declare; it is { ${PREDICATE_LEAF_KEYS.join(", ")} }` };
|
|
806
826
|
}
|
|
807
|
-
|
|
808
|
-
return { rule: "FD-
|
|
827
|
+
for (const k of PREDICATE_LEAF_REQUIRED) {
|
|
828
|
+
if (!(k in o)) return { rule: "FD-V42", message: `${where} is missing required key "${k}"; a comparison is { ${PREDICATE_LEAF_KEYS.join(", ")} } with ${PREDICATE_LEAF_REQUIRED.join(", ")} required and exactly one of ${RIGHT_SIDE_KEYS.map((k2) => JSON.stringify(k2)).join(" or ")}` };
|
|
829
|
+
}
|
|
830
|
+
const sides = RIGHT_SIDE_KEYS.filter((k) => k in o);
|
|
831
|
+
if (sides.length !== 1) {
|
|
832
|
+
return { rule: "FD-V42", message: `${where} carries ${sides.length === 0 ? "neither" : "both"} "value" and "valueFrom"; a comparison compares against exactly one \u2014 a literal "value", or "valueFrom", a key path over the same context` };
|
|
833
|
+
}
|
|
834
|
+
const pathProblem = comparisonPathProblem(o, "path", where, scope);
|
|
835
|
+
if (pathProblem !== null) return pathProblem;
|
|
836
|
+
const fromValue = "valueFrom" in o;
|
|
837
|
+
if (fromValue) {
|
|
838
|
+
const fromProblem = comparisonPathProblem(o, "valueFrom", where, scope);
|
|
839
|
+
if (fromProblem !== null) return fromProblem;
|
|
809
840
|
}
|
|
810
841
|
const op = o.op;
|
|
811
842
|
if (typeof op !== "string" || !PREDICATE_OPS.includes(op)) {
|
|
@@ -815,6 +846,12 @@ function leafProblem(o, where, scope = EDGE_SCOPE) {
|
|
|
815
846
|
return { rule: "FD-V44", message: `${where} length is ${JSON.stringify(o.length)}; it is exactly true or absent \u2014 there is no "compare the value, not its length" to ask for` };
|
|
816
847
|
}
|
|
817
848
|
const takesLength = o.length === true;
|
|
849
|
+
if (fromValue) {
|
|
850
|
+
if (takesLength && STRING_OPS.has(op)) {
|
|
851
|
+
return { rule: "FD-V44", message: `${where} takes a length and uses "${op}"; a length is a number and has no prefix` };
|
|
852
|
+
}
|
|
853
|
+
return defaultProblem(o, where, takesLength);
|
|
854
|
+
}
|
|
818
855
|
if (!isScalar(o.value)) {
|
|
819
856
|
return { rule: "FD-V44", message: `${where} value is ${scalarName(o.value)}; a comparison compares against a SCALAR \u2014 null, a boolean, a finite number or a string` };
|
|
820
857
|
}
|
|
@@ -830,6 +867,9 @@ function leafProblem(o, where, scope = EDGE_SCOPE) {
|
|
|
830
867
|
if (takesLength && STRING_OPS.has(op)) {
|
|
831
868
|
return { rule: "FD-V44", message: `${where} takes a length and uses "${op}"; a length is a number and has no prefix` };
|
|
832
869
|
}
|
|
870
|
+
return defaultProblem(o, where, takesLength);
|
|
871
|
+
}
|
|
872
|
+
function defaultProblem(o, where, takesLength) {
|
|
833
873
|
if ("default" in o) {
|
|
834
874
|
const d = o.default;
|
|
835
875
|
const okDefault = Array.isArray(d) ? d.every(isScalar) : isScalar(d);
|
|
@@ -845,7 +885,8 @@ function leafProblem(o, where, scope = EDGE_SCOPE) {
|
|
|
845
885
|
}
|
|
846
886
|
return null;
|
|
847
887
|
}
|
|
848
|
-
function predicateProblem(v) {
|
|
888
|
+
function predicateProblem(v, edgeNeeds) {
|
|
889
|
+
const scope = edgeNeeds === void 0 ? EDGE_SCOPE : { ...EDGE_SCOPE, needsDeclared: edgeNeeds };
|
|
849
890
|
if (typeof v !== "object" || v === null || Array.isArray(v)) {
|
|
850
891
|
return {
|
|
851
892
|
rule: "FD-V42",
|
|
@@ -866,8 +907,8 @@ function predicateProblem(v) {
|
|
|
866
907
|
if (typeof o.predicate !== "string") return { rule: "FD-V42", message: `when predicate is ${scalarName(o.predicate)}; it is the name of a registered predicate` };
|
|
867
908
|
return null;
|
|
868
909
|
}
|
|
869
|
-
if (shape === "leaf") return leafProblem(o, "when");
|
|
870
|
-
return groupProblem(o, "when",
|
|
910
|
+
if (shape === "leaf") return leafProblem(o, "when", scope);
|
|
911
|
+
return groupProblem(o, "when", scope);
|
|
871
912
|
}
|
|
872
913
|
function groupProblem(o, label, scope) {
|
|
873
914
|
const extra = Object.keys(o).filter((k) => !PREDICATE_GROUP_KEYS.includes(k));
|
|
@@ -1360,7 +1401,9 @@ function validateDefinition(doc, registry) {
|
|
|
1360
1401
|
}
|
|
1361
1402
|
}
|
|
1362
1403
|
if (ed?.when !== void 0) {
|
|
1363
|
-
const
|
|
1404
|
+
const nv = ed.needs;
|
|
1405
|
+
const edgeNeeds = nv === void 0 ? [] : typeof nv === "object" && nv !== null && !Array.isArray(nv) && Object.keys(nv).length > 0 ? Object.keys(nv) : void 0;
|
|
1406
|
+
const problem = predicateProblem(ed.when, edgeNeeds);
|
|
1364
1407
|
if (problem) add(problem.rule, at, problem.message);
|
|
1365
1408
|
else for (const name of predicateNamesIn(ed.when)) {
|
|
1366
1409
|
if (!preds.has(name)) add("FD-V4", at, `when names "${name}", which is not a registered predicate`);
|
|
@@ -7028,6 +7071,7 @@ export {
|
|
|
7028
7071
|
canonical,
|
|
7029
7072
|
findings,
|
|
7030
7073
|
describeFindings,
|
|
7074
|
+
keyPathSteps,
|
|
7031
7075
|
readKeyPath,
|
|
7032
7076
|
ID_RE_COPY,
|
|
7033
7077
|
KINDS_COPY,
|
package/dist/browser/index.js
CHANGED
|
@@ -111,6 +111,7 @@ import {
|
|
|
111
111
|
isTerminal,
|
|
112
112
|
isTerminalEvent,
|
|
113
113
|
keyPathProblem,
|
|
114
|
+
keyPathSteps,
|
|
114
115
|
languagesOf,
|
|
115
116
|
lastRecordFor,
|
|
116
117
|
liveToken,
|
|
@@ -151,7 +152,7 @@ import {
|
|
|
151
152
|
visibilityKey,
|
|
152
153
|
witness,
|
|
153
154
|
workersOf
|
|
154
|
-
} from "./chunks/chunk-
|
|
155
|
+
} from "./chunks/chunk-53EDI674.js";
|
|
155
156
|
|
|
156
157
|
// src/definition/definition-schema.ts
|
|
157
158
|
var DRAFT_2020_12 = "https://json-schema.org/draft/2020-12/schema";
|
|
@@ -237,7 +238,7 @@ var KIND_DESCRIPTIONS = {
|
|
|
237
238
|
var EDGE_DESCRIPTIONS = {
|
|
238
239
|
to: "The id of the node this edge leads to (FD-V2, validateDefinition's).",
|
|
239
240
|
on: 'The terminal states this edge is eligible for: a non-empty list of distinct "done" / "failed" (FD-V18). Absent: ["done"]. "user_action" suspends rather than routes.',
|
|
240
|
-
when: "The decision: a registered predicate NAME { predicate }, a declared COMPARISON { path, op, value, default?, length? }, or a flat GROUP { all } or { any } of comparisons (FD-V42-FD-V45). Absent: the edge is the default for its state.",
|
|
241
|
+
when: "The decision: a registered predicate NAME { predicate }, a declared COMPARISON { path, op, value or valueFrom, default?, length? }, or a flat GROUP { all } or { any } of comparisons (FD-V42-FD-V45). Absent: the edge is the default for its state.",
|
|
241
242
|
needs: "What this edge's predicate sees beyond its node's outcome \u2014 `needs`' grammar minus \".\" (FD-V26).",
|
|
242
243
|
route: "On a gate's edge, which of the gate type's routes it carries; absent: the type's DEFAULT route (FD-V41). Refused on every other kind's edge."
|
|
243
244
|
};
|
|
@@ -251,9 +252,10 @@ var TRANSFORM_DESCRIPTIONS = {
|
|
|
251
252
|
};
|
|
252
253
|
var PREDICATE_DESCRIPTIONS = {
|
|
253
254
|
predicate: "A registered predicate's name (FD-V4, validateDefinition's).",
|
|
254
|
-
path: `A key path over the decision's context, starting at ${PREDICATE_ROOTS.map((r) => `"${r}"`).join(" or ")} and naming a key beyond it; a key past the root may be followed by array indexes [n], the root never (FD-V43).`,
|
|
255
|
+
path: `A key path over the decision's context, starting at ${PREDICATE_ROOTS.map((r) => `"${r}"`).join(" or ")} and naming a key beyond it; a key past the root may be followed by array indexes [n], the root never; a "needs" key the edge declares (FD-V43).`,
|
|
255
256
|
op: `One of ${PREDICATE_OPS.join(", ")} (FD-V44). The ordering operators compare numbers and "startsWith" a string \u2014 a comparison between kinds is no match, never a coercion.`,
|
|
256
|
-
value: "The scalar compared against: null, a boolean, a number or a string (FD-V44).",
|
|
257
|
+
value: "The scalar compared against: null, a boolean, a number or a string (FD-V44). Exactly one of value and valueFrom (FD-V42).",
|
|
258
|
+
valueFrom: `A key path over the same context, compared against as a literal would be \u2014 the same roots as path, ${PREDICATE_ROOTS.map((r) => `"${r}"`).join(" or ")}, and a "needs" key the edge declares (FD-V43). Exactly one of value and valueFrom (FD-V42). Where it does not resolve, or resolves to an object or an array, the comparison is no match for every operator.`,
|
|
257
259
|
default: "What is compared where the path does not resolve or resolves to null: a scalar, or \u2014 with `length` \u2014 an array or a string (FD-V44).",
|
|
258
260
|
length: "Exactly true: compare the LENGTH of the array or string, against a number (FD-V44).",
|
|
259
261
|
all: "A flat group: a non-empty list of comparisons that must all hold (FD-V45).",
|
|
@@ -438,6 +440,9 @@ function leafSchema(id) {
|
|
|
438
440
|
path: { type: "string", pattern: path },
|
|
439
441
|
op: { enum: [...PREDICATE_OPS] },
|
|
440
442
|
value: scalar(),
|
|
443
|
+
// v0.55 (ADR 0140): the RIGHT side as a key path — `path`'s own pattern, so the two keys cannot be held to different
|
|
444
|
+
// grammars; (g1)'s *a `needs` key the edge declares* needs the edge's own `needs` and is the validator's alone.
|
|
445
|
+
valueFrom: { type: "string", pattern: path },
|
|
441
446
|
default: { anyOf: [scalar(), { type: "array", items: scalar() }] },
|
|
442
447
|
length: { const: true }
|
|
443
448
|
};
|
|
@@ -445,6 +450,8 @@ function leafSchema(id) {
|
|
|
445
450
|
description: "A declared comparison over the decision's context.",
|
|
446
451
|
type: "object",
|
|
447
452
|
required: [...PREDICATE_LEAF_REQUIRED],
|
|
453
|
+
// **Exactly one of `value` / `valueFrom`** (`FD-V42`, v0.55): both satisfy both branches and neither satisfies none.
|
|
454
|
+
oneOf: [{ required: ["value"] }, { required: ["valueFrom"] }],
|
|
448
455
|
properties: Object.fromEntries(PREDICATE_LEAF_KEYS.map((k) => {
|
|
449
456
|
const s = keySchemas[k];
|
|
450
457
|
if (s === void 0) throw new Error(`definitionSchema: a comparison declares the key "${k}" and no schema is written for it`);
|
|
@@ -942,6 +949,7 @@ export {
|
|
|
942
949
|
isTerminal,
|
|
943
950
|
isTerminalEvent,
|
|
944
951
|
keyPathProblem,
|
|
952
|
+
keyPathSteps,
|
|
945
953
|
languagesOf,
|
|
946
954
|
lastRecordFor,
|
|
947
955
|
liveToken,
|
|
@@ -402,7 +402,13 @@ function evalLeaf(scope, p, unknownOp) {
|
|
|
402
402
|
else if (typeof v === "string") v = v.length;
|
|
403
403
|
else return false;
|
|
404
404
|
}
|
|
405
|
-
|
|
405
|
+
let w;
|
|
406
|
+
if (p.valueFrom !== void 0) {
|
|
407
|
+
w = readKeyPath(scope, p.valueFrom);
|
|
408
|
+
if (w === void 0 || typeof w === "object" && w !== null) return false;
|
|
409
|
+
} else {
|
|
410
|
+
w = p.value;
|
|
411
|
+
}
|
|
406
412
|
switch (p.op) {
|
|
407
413
|
// **Type-STRICT, and it is a departure measured rather than assumed.** Under JS comparison
|
|
408
414
|
// `{confidence: "0.4"} < 0.5` is true and every `typeof`-guarded predicate in this tree is false;
|
|
@@ -755,8 +761,9 @@ function gateTypeCollisions(x) {
|
|
|
755
761
|
return names.filter((n) => Object.prototype.hasOwnProperty.call(BUILT_IN_GATE_TYPES, n)).sort();
|
|
756
762
|
}
|
|
757
763
|
var PREDICATE_NAME_KEYS = ["predicate"];
|
|
758
|
-
var PREDICATE_LEAF_KEYS = ["path", "op", "value", "default", "length"];
|
|
759
|
-
var PREDICATE_LEAF_REQUIRED = ["path", "op"
|
|
764
|
+
var PREDICATE_LEAF_KEYS = ["path", "op", "value", "valueFrom", "default", "length"];
|
|
765
|
+
var PREDICATE_LEAF_REQUIRED = ["path", "op"];
|
|
766
|
+
var RIGHT_SIDE_KEYS = ["value", "valueFrom"];
|
|
760
767
|
var PREDICATE_GROUP_KEYS = ["all", "any"];
|
|
761
768
|
function predicateShapeOf(o) {
|
|
762
769
|
const marks = [];
|
|
@@ -783,29 +790,53 @@ var GATE_SCOPE = {
|
|
|
783
790
|
foreignRoot: () => 'which an if gate cannot see \u2014 its one root is "input", the value that reached the gate; a gate has no outcome and no edge needs, and any other first segment would resolve to nothing in silence',
|
|
784
791
|
rootAlone: () => "and names no key \u2014 a gate's path names a key INSIDE the value that reached it, as every gate's key path does; a value that must be compared whole is wrapped by the task before the gate"
|
|
785
792
|
};
|
|
786
|
-
function
|
|
787
|
-
const
|
|
788
|
-
if (extra.length) {
|
|
789
|
-
return { rule: "FD-V42", message: `${where} carries ${extra.map((k) => JSON.stringify(k)).join(", ")}, which a comparison does not declare; it is { ${PREDICATE_LEAF_KEYS.join(", ")} }` };
|
|
790
|
-
}
|
|
791
|
-
for (const k of PREDICATE_LEAF_REQUIRED) {
|
|
792
|
-
if (!(k in o)) return { rule: "FD-V42", message: `${where} is missing required key "${k}"; a comparison is { ${PREDICATE_LEAF_KEYS.join(", ")} } with ${PREDICATE_LEAF_REQUIRED.join(", ")} required` };
|
|
793
|
-
}
|
|
794
|
-
const path = o.path;
|
|
793
|
+
function comparisonPathProblem(o, key, where, scope) {
|
|
794
|
+
const path = o[key];
|
|
795
795
|
if (typeof path !== "string" || !KEY_PATH_RE.test(path)) {
|
|
796
|
-
return { rule: "FD-V43", message: `${where}
|
|
796
|
+
return { rule: "FD-V43", message: `${where} ${key} ${JSON.stringify(path)} is not a key path \u2014 ${KEY_PATH_WORDS}` };
|
|
797
797
|
}
|
|
798
|
-
const
|
|
798
|
+
const segments = path.split(".");
|
|
799
|
+
const first = segments[0];
|
|
799
800
|
const bracket = first.indexOf("[");
|
|
800
801
|
const root = bracket === -1 ? first : first.slice(0, bracket);
|
|
801
802
|
if (!scope.roots.includes(root)) {
|
|
802
|
-
return { rule: "FD-V43", message: `${where}
|
|
803
|
+
return { rule: "FD-V43", message: `${where} ${key} ${JSON.stringify(path)} starts at ${JSON.stringify(root)}, ${scope.foreignRoot(root)}` };
|
|
803
804
|
}
|
|
804
805
|
if (bracket !== -1) {
|
|
805
|
-
return { rule: "FD-V43", message: `${where}
|
|
806
|
+
return { rule: "FD-V43", message: `${where} ${key} ${JSON.stringify(path)} indexes its root ${JSON.stringify(root)} \u2014 a path names a KEY inside its root first, as a gate's key path names a key inside the value that reached it; an index follows a key (${root}.items[0])` };
|
|
807
|
+
}
|
|
808
|
+
if (segments.length < 2) {
|
|
809
|
+
return { rule: "FD-V43", message: `${where} ${key} ${JSON.stringify(path)} is a root alone ${scope.rootAlone(root)}` };
|
|
810
|
+
}
|
|
811
|
+
if (root === "needs" && scope.needsDeclared !== void 0) {
|
|
812
|
+
const seg = segments[1];
|
|
813
|
+
const at = seg.indexOf("[");
|
|
814
|
+
const name = at === -1 ? seg : seg.slice(0, at);
|
|
815
|
+
if (!scope.needsDeclared.includes(name)) {
|
|
816
|
+
const declared = scope.needsDeclared.length === 0 ? "declares no `needs`, so its `ctx.needs` is {}" : `declares needs ${scope.needsDeclared.map((k2) => JSON.stringify(k2)).join(", ")}`;
|
|
817
|
+
return { rule: "FD-V43", message: `${where} ${key} ${JSON.stringify(path)} reads needs key ${JSON.stringify(name)}, and this edge ${declared} \u2014 the key would be absent for ever and the comparison no match for ever; declare it in the edge's needs` };
|
|
818
|
+
}
|
|
819
|
+
}
|
|
820
|
+
return null;
|
|
821
|
+
}
|
|
822
|
+
function leafProblem(o, where, scope = EDGE_SCOPE) {
|
|
823
|
+
const extra = Object.keys(o).filter((k) => !PREDICATE_LEAF_KEYS.includes(k));
|
|
824
|
+
if (extra.length) {
|
|
825
|
+
return { rule: "FD-V42", message: `${where} carries ${extra.map((k) => JSON.stringify(k)).join(", ")}, which a comparison does not declare; it is { ${PREDICATE_LEAF_KEYS.join(", ")} }` };
|
|
806
826
|
}
|
|
807
|
-
|
|
808
|
-
return { rule: "FD-
|
|
827
|
+
for (const k of PREDICATE_LEAF_REQUIRED) {
|
|
828
|
+
if (!(k in o)) return { rule: "FD-V42", message: `${where} is missing required key "${k}"; a comparison is { ${PREDICATE_LEAF_KEYS.join(", ")} } with ${PREDICATE_LEAF_REQUIRED.join(", ")} required and exactly one of ${RIGHT_SIDE_KEYS.map((k2) => JSON.stringify(k2)).join(" or ")}` };
|
|
829
|
+
}
|
|
830
|
+
const sides = RIGHT_SIDE_KEYS.filter((k) => k in o);
|
|
831
|
+
if (sides.length !== 1) {
|
|
832
|
+
return { rule: "FD-V42", message: `${where} carries ${sides.length === 0 ? "neither" : "both"} "value" and "valueFrom"; a comparison compares against exactly one \u2014 a literal "value", or "valueFrom", a key path over the same context` };
|
|
833
|
+
}
|
|
834
|
+
const pathProblem = comparisonPathProblem(o, "path", where, scope);
|
|
835
|
+
if (pathProblem !== null) return pathProblem;
|
|
836
|
+
const fromValue = "valueFrom" in o;
|
|
837
|
+
if (fromValue) {
|
|
838
|
+
const fromProblem = comparisonPathProblem(o, "valueFrom", where, scope);
|
|
839
|
+
if (fromProblem !== null) return fromProblem;
|
|
809
840
|
}
|
|
810
841
|
const op = o.op;
|
|
811
842
|
if (typeof op !== "string" || !PREDICATE_OPS.includes(op)) {
|
|
@@ -815,6 +846,12 @@ function leafProblem(o, where, scope = EDGE_SCOPE) {
|
|
|
815
846
|
return { rule: "FD-V44", message: `${where} length is ${JSON.stringify(o.length)}; it is exactly true or absent \u2014 there is no "compare the value, not its length" to ask for` };
|
|
816
847
|
}
|
|
817
848
|
const takesLength = o.length === true;
|
|
849
|
+
if (fromValue) {
|
|
850
|
+
if (takesLength && STRING_OPS.has(op)) {
|
|
851
|
+
return { rule: "FD-V44", message: `${where} takes a length and uses "${op}"; a length is a number and has no prefix` };
|
|
852
|
+
}
|
|
853
|
+
return defaultProblem(o, where, takesLength);
|
|
854
|
+
}
|
|
818
855
|
if (!isScalar(o.value)) {
|
|
819
856
|
return { rule: "FD-V44", message: `${where} value is ${scalarName(o.value)}; a comparison compares against a SCALAR \u2014 null, a boolean, a finite number or a string` };
|
|
820
857
|
}
|
|
@@ -830,6 +867,9 @@ function leafProblem(o, where, scope = EDGE_SCOPE) {
|
|
|
830
867
|
if (takesLength && STRING_OPS.has(op)) {
|
|
831
868
|
return { rule: "FD-V44", message: `${where} takes a length and uses "${op}"; a length is a number and has no prefix` };
|
|
832
869
|
}
|
|
870
|
+
return defaultProblem(o, where, takesLength);
|
|
871
|
+
}
|
|
872
|
+
function defaultProblem(o, where, takesLength) {
|
|
833
873
|
if ("default" in o) {
|
|
834
874
|
const d = o.default;
|
|
835
875
|
const okDefault = Array.isArray(d) ? d.every(isScalar) : isScalar(d);
|
|
@@ -845,7 +885,8 @@ function leafProblem(o, where, scope = EDGE_SCOPE) {
|
|
|
845
885
|
}
|
|
846
886
|
return null;
|
|
847
887
|
}
|
|
848
|
-
function predicateProblem(v) {
|
|
888
|
+
function predicateProblem(v, edgeNeeds) {
|
|
889
|
+
const scope = edgeNeeds === void 0 ? EDGE_SCOPE : { ...EDGE_SCOPE, needsDeclared: edgeNeeds };
|
|
849
890
|
if (typeof v !== "object" || v === null || Array.isArray(v)) {
|
|
850
891
|
return {
|
|
851
892
|
rule: "FD-V42",
|
|
@@ -866,8 +907,8 @@ function predicateProblem(v) {
|
|
|
866
907
|
if (typeof o.predicate !== "string") return { rule: "FD-V42", message: `when predicate is ${scalarName(o.predicate)}; it is the name of a registered predicate` };
|
|
867
908
|
return null;
|
|
868
909
|
}
|
|
869
|
-
if (shape === "leaf") return leafProblem(o, "when");
|
|
870
|
-
return groupProblem(o, "when",
|
|
910
|
+
if (shape === "leaf") return leafProblem(o, "when", scope);
|
|
911
|
+
return groupProblem(o, "when", scope);
|
|
871
912
|
}
|
|
872
913
|
function groupProblem(o, label, scope) {
|
|
873
914
|
const extra = Object.keys(o).filter((k) => !PREDICATE_GROUP_KEYS.includes(k));
|
|
@@ -1360,7 +1401,9 @@ function validateDefinition(doc, registry) {
|
|
|
1360
1401
|
}
|
|
1361
1402
|
}
|
|
1362
1403
|
if (ed?.when !== void 0) {
|
|
1363
|
-
const
|
|
1404
|
+
const nv = ed.needs;
|
|
1405
|
+
const edgeNeeds = nv === void 0 ? [] : typeof nv === "object" && nv !== null && !Array.isArray(nv) && Object.keys(nv).length > 0 ? Object.keys(nv) : void 0;
|
|
1406
|
+
const problem = predicateProblem(ed.when, edgeNeeds);
|
|
1364
1407
|
if (problem) add(problem.rule, at, problem.message);
|
|
1365
1408
|
else for (const name of predicateNamesIn(ed.when)) {
|
|
1366
1409
|
if (!preds.has(name)) add("FD-V4", at, `when names "${name}", which is not a registered predicate`);
|
|
@@ -7028,6 +7071,7 @@ export {
|
|
|
7028
7071
|
canonical,
|
|
7029
7072
|
findings,
|
|
7030
7073
|
describeFindings,
|
|
7074
|
+
keyPathSteps,
|
|
7031
7075
|
readKeyPath,
|
|
7032
7076
|
ID_RE_COPY,
|
|
7033
7077
|
KINDS_COPY,
|
package/dist/node/index.js
CHANGED
|
@@ -111,6 +111,7 @@ import {
|
|
|
111
111
|
isTerminal,
|
|
112
112
|
isTerminalEvent,
|
|
113
113
|
keyPathProblem,
|
|
114
|
+
keyPathSteps,
|
|
114
115
|
languagesOf,
|
|
115
116
|
lastRecordFor,
|
|
116
117
|
liveToken,
|
|
@@ -151,7 +152,7 @@ import {
|
|
|
151
152
|
visibilityKey,
|
|
152
153
|
witness,
|
|
153
154
|
workersOf
|
|
154
|
-
} from "./chunks/chunk-
|
|
155
|
+
} from "./chunks/chunk-53EDI674.js";
|
|
155
156
|
|
|
156
157
|
// src/definition/definition-schema.ts
|
|
157
158
|
var DRAFT_2020_12 = "https://json-schema.org/draft/2020-12/schema";
|
|
@@ -237,7 +238,7 @@ var KIND_DESCRIPTIONS = {
|
|
|
237
238
|
var EDGE_DESCRIPTIONS = {
|
|
238
239
|
to: "The id of the node this edge leads to (FD-V2, validateDefinition's).",
|
|
239
240
|
on: 'The terminal states this edge is eligible for: a non-empty list of distinct "done" / "failed" (FD-V18). Absent: ["done"]. "user_action" suspends rather than routes.',
|
|
240
|
-
when: "The decision: a registered predicate NAME { predicate }, a declared COMPARISON { path, op, value, default?, length? }, or a flat GROUP { all } or { any } of comparisons (FD-V42-FD-V45). Absent: the edge is the default for its state.",
|
|
241
|
+
when: "The decision: a registered predicate NAME { predicate }, a declared COMPARISON { path, op, value or valueFrom, default?, length? }, or a flat GROUP { all } or { any } of comparisons (FD-V42-FD-V45). Absent: the edge is the default for its state.",
|
|
241
242
|
needs: "What this edge's predicate sees beyond its node's outcome \u2014 `needs`' grammar minus \".\" (FD-V26).",
|
|
242
243
|
route: "On a gate's edge, which of the gate type's routes it carries; absent: the type's DEFAULT route (FD-V41). Refused on every other kind's edge."
|
|
243
244
|
};
|
|
@@ -251,9 +252,10 @@ var TRANSFORM_DESCRIPTIONS = {
|
|
|
251
252
|
};
|
|
252
253
|
var PREDICATE_DESCRIPTIONS = {
|
|
253
254
|
predicate: "A registered predicate's name (FD-V4, validateDefinition's).",
|
|
254
|
-
path: `A key path over the decision's context, starting at ${PREDICATE_ROOTS.map((r) => `"${r}"`).join(" or ")} and naming a key beyond it; a key past the root may be followed by array indexes [n], the root never (FD-V43).`,
|
|
255
|
+
path: `A key path over the decision's context, starting at ${PREDICATE_ROOTS.map((r) => `"${r}"`).join(" or ")} and naming a key beyond it; a key past the root may be followed by array indexes [n], the root never; a "needs" key the edge declares (FD-V43).`,
|
|
255
256
|
op: `One of ${PREDICATE_OPS.join(", ")} (FD-V44). The ordering operators compare numbers and "startsWith" a string \u2014 a comparison between kinds is no match, never a coercion.`,
|
|
256
|
-
value: "The scalar compared against: null, a boolean, a number or a string (FD-V44).",
|
|
257
|
+
value: "The scalar compared against: null, a boolean, a number or a string (FD-V44). Exactly one of value and valueFrom (FD-V42).",
|
|
258
|
+
valueFrom: `A key path over the same context, compared against as a literal would be \u2014 the same roots as path, ${PREDICATE_ROOTS.map((r) => `"${r}"`).join(" or ")}, and a "needs" key the edge declares (FD-V43). Exactly one of value and valueFrom (FD-V42). Where it does not resolve, or resolves to an object or an array, the comparison is no match for every operator.`,
|
|
257
259
|
default: "What is compared where the path does not resolve or resolves to null: a scalar, or \u2014 with `length` \u2014 an array or a string (FD-V44).",
|
|
258
260
|
length: "Exactly true: compare the LENGTH of the array or string, against a number (FD-V44).",
|
|
259
261
|
all: "A flat group: a non-empty list of comparisons that must all hold (FD-V45).",
|
|
@@ -438,6 +440,9 @@ function leafSchema(id) {
|
|
|
438
440
|
path: { type: "string", pattern: path },
|
|
439
441
|
op: { enum: [...PREDICATE_OPS] },
|
|
440
442
|
value: scalar(),
|
|
443
|
+
// v0.55 (ADR 0140): the RIGHT side as a key path — `path`'s own pattern, so the two keys cannot be held to different
|
|
444
|
+
// grammars; (g1)'s *a `needs` key the edge declares* needs the edge's own `needs` and is the validator's alone.
|
|
445
|
+
valueFrom: { type: "string", pattern: path },
|
|
441
446
|
default: { anyOf: [scalar(), { type: "array", items: scalar() }] },
|
|
442
447
|
length: { const: true }
|
|
443
448
|
};
|
|
@@ -445,6 +450,8 @@ function leafSchema(id) {
|
|
|
445
450
|
description: "A declared comparison over the decision's context.",
|
|
446
451
|
type: "object",
|
|
447
452
|
required: [...PREDICATE_LEAF_REQUIRED],
|
|
453
|
+
// **Exactly one of `value` / `valueFrom`** (`FD-V42`, v0.55): both satisfy both branches and neither satisfies none.
|
|
454
|
+
oneOf: [{ required: ["value"] }, { required: ["valueFrom"] }],
|
|
448
455
|
properties: Object.fromEntries(PREDICATE_LEAF_KEYS.map((k) => {
|
|
449
456
|
const s = keySchemas[k];
|
|
450
457
|
if (s === void 0) throw new Error(`definitionSchema: a comparison declares the key "${k}" and no schema is written for it`);
|
|
@@ -942,6 +949,7 @@ export {
|
|
|
942
949
|
isTerminal,
|
|
943
950
|
isTerminalEvent,
|
|
944
951
|
keyPathProblem,
|
|
952
|
+
keyPathSteps,
|
|
945
953
|
languagesOf,
|
|
946
954
|
lastRecordFor,
|
|
947
955
|
liveToken,
|
|
@@ -6,9 +6,11 @@
|
|
|
6
6
|
* is*). [W154](../../doc/work-items/w154-the-built-in-if-and-switch.md) exception (p): until it, the `if` reached
|
|
7
7
|
* the evaluator through `routing.ts`, which imports `src/definition` — a module cycle in a published library.
|
|
8
8
|
*
|
|
9
|
-
* **Not re-exported by `index.ts`.**
|
|
10
|
-
* `
|
|
11
|
-
*
|
|
9
|
+
* **Not re-exported WHOLE by `index.ts`.** Three names here are public, each through `validate.ts`' re-export:
|
|
10
|
+
* `readKeyPath` (public since W92, defined in `validate.ts` then; through this re-export since W154), and `keyPathSteps`
|
|
11
|
+
* with its `KeyPathStep` (since W220, P45 (c1) — fyflow-app's pass 6 #2: a host reads a key path with the library's own
|
|
12
|
+
* parse and read rather than a copy of the spelling). `evalLeaf` and `evaluateDeclared` are NOT (P45 decision 1): a
|
|
13
|
+
* host evaluates a `when` by `evaluateWhen`, public through `routing.ts`. Nothing here imports anything but types.
|
|
12
14
|
*/
|
|
13
15
|
import type { PredicateLeaf, When } from './types.js';
|
|
14
16
|
/**
|
|
@@ -52,7 +54,8 @@ export declare function readKeyPath(value: unknown, path: string): unknown;
|
|
|
52
54
|
* closure over anything — so a declared predicate **CANNOT** be impure, which is strictly stronger
|
|
53
55
|
* than [Q9](../../doc/questions/q9.md)'s measured result for a function (evaluating twice caught
|
|
54
56
|
* 1 impurity shape of 4) and is asserted rather than argued. `scope` is a task edge's `DecisionContext`
|
|
55
|
-
* or an `if` gate's `{ input }` (W154) — the path is read off it by `readKeyPath`,
|
|
57
|
+
* or an `if` gate's `{ input }` (W154) — the path, and since v0.55 a `valueFrom`, is read off it by `readKeyPath`,
|
|
58
|
+
* and that is the only read.
|
|
56
59
|
*
|
|
57
60
|
* **Total by construction**, because `FD-V42`-`FD-V45` refuse everything this does not handle at
|
|
58
61
|
* `new Driver`. The `default:` arm below is the assertion that they held; `unknownOp` is the error it throws,
|
|
@@ -86,10 +86,12 @@ export type PredicateOp = typeof PREDICATE_OPS[number];
|
|
|
86
86
|
/** The two roots a predicate's key path may start at — the two members of `DecisionContext`, and no other. */
|
|
87
87
|
export declare const PREDICATE_ROOTS: readonly ["outcome", "needs"];
|
|
88
88
|
/**
|
|
89
|
-
* **What a declared comparison may
|
|
89
|
+
* **What a declared comparison's LITERAL may be** — a scalar, and nothing else (`FD-V44`).
|
|
90
90
|
* Every one of the ten predicate bodies in this tree compares to one (`0.5`, `50`, `true`,
|
|
91
91
|
* `'r1:publish:'`, `0`), and admitting an object would need a deep-equality semantics with no
|
|
92
|
-
* instance and no obvious right answer for `NaN`, key order or a prototype.
|
|
92
|
+
* instance and no obvious right answer for `NaN`, key order or a prototype. *Since v0.55 a comparison
|
|
93
|
+
* may compare against ANOTHER key path instead — `valueFrom` (ADR 0140) — and a resolved object or
|
|
94
|
+
* array there is no match, for the same reason.*
|
|
93
95
|
*/
|
|
94
96
|
export type PredicateScalar = null | boolean | number | string;
|
|
95
97
|
/** A registered predicate NAME, resolved against the registry at instantiation (`FD-V4`). */
|
|
@@ -97,21 +99,20 @@ export interface PredicateName {
|
|
|
97
99
|
predicate: string;
|
|
98
100
|
}
|
|
99
101
|
/**
|
|
100
|
-
* **
|
|
101
|
-
* operator and the
|
|
102
|
+
* **What every comparison carries, whichever right side it names** — `FD-V43` owns the path and `FD-V44` the
|
|
103
|
+
* operator and the modifiers.
|
|
102
104
|
*/
|
|
103
|
-
export interface
|
|
105
|
+
export interface PredicateLeafBase {
|
|
104
106
|
/**
|
|
105
107
|
* A KEY PATH over the `DecisionContext` itself, read by `readKeyPath` — the same resolution a
|
|
106
108
|
* gate's `settings.key` gets, measured unchanged at this scope. **Its first segment is `outcome`
|
|
107
109
|
* or `needs` and nothing else** (`FD-V43`), because a first segment that is neither resolves to
|
|
108
110
|
* `undefined` in silence and a typo would be a wrong key rather than a refusal. A key past the root may
|
|
109
111
|
* carry array indexes — `outcome.result.items[0].score` — and the root never does (v0.53, ADR 0137).
|
|
112
|
+
* **A `needs` path names a key that edge's `needs` DECLARES** (v0.55, P45 (g1)): any other is absent for ever.
|
|
110
113
|
*/
|
|
111
114
|
path: string;
|
|
112
115
|
op: PredicateOp;
|
|
113
|
-
/** The literal compared against. Required — there is no operator here that takes one operand. */
|
|
114
|
-
value: PredicateScalar;
|
|
115
116
|
/**
|
|
116
117
|
* **What is compared when the path does not resolve, or resolves to `null`.** Absent is NO MATCH
|
|
117
118
|
* by default; where this is declared it is substituted instead — which is `?? 0` and `?? []`
|
|
@@ -136,6 +137,29 @@ export interface PredicateLeaf {
|
|
|
136
137
|
*/
|
|
137
138
|
length?: true;
|
|
138
139
|
}
|
|
140
|
+
/**
|
|
141
|
+
* **One comparison over the decision's own context — against a LITERAL, or against ANOTHER KEY PATH** (v0.55,
|
|
142
|
+
* [ADR 0140](../../doc/decisions/0140-a-comparison-may-compare-against-another-key-path-and-an-absent-one-is-no-match.md),
|
|
143
|
+
* P45 (a1)). Exactly one of `value` / `valueFrom` (`FD-V42`), so a TWO-member union rather than one interface with
|
|
144
|
+
* both optional — `PredicateGroup`'s precedent: `{ path, op }` with neither, and both at once, fail at L0. **The type
|
|
145
|
+
* change, stated** (P45 decision 8): until 0.13.0 `value` was required, so code READING `leaf.value` now sees
|
|
146
|
+
* `PredicateScalar | undefined` and tells the two apart by `'valueFrom' in leaf`.
|
|
147
|
+
*/
|
|
148
|
+
export type PredicateLeaf = (PredicateLeafBase & {
|
|
149
|
+
/** The literal compared against. */
|
|
150
|
+
value: PredicateScalar;
|
|
151
|
+
valueFrom?: never;
|
|
152
|
+
}) | (PredicateLeafBase & {
|
|
153
|
+
/**
|
|
154
|
+
* **A key path over the SAME context, compared against as a literal would be** — the same roots as `path` (and
|
|
155
|
+
* the same `needs` rule), held to `FD-V43`; resolved by `readKeyPath` when the comparison is asked. Type-strict, a
|
|
156
|
+
* resolved `null` compared as `null`; **a resolved object or array is NO MATCH for every operator, `ne` included**;
|
|
157
|
+
* and **an ABSENT one is NO MATCH** — the left side's `default` does not stand in for it, so `x gt y` and `x lte y`
|
|
158
|
+
* are both false where `y` is absent. `default` and `length` are the LEFT side's only.
|
|
159
|
+
*/
|
|
160
|
+
valueFrom: string;
|
|
161
|
+
value?: never;
|
|
162
|
+
});
|
|
139
163
|
/**
|
|
140
164
|
* **A FLAT group — `all` or `any`, and its members are LEAVES.** Nesting is refused by name
|
|
141
165
|
* (`FD-V45`), and so is a `{ predicate }` member: a group holding a registered name could be
|
|
@@ -18,7 +18,8 @@
|
|
|
18
18
|
* ADR 0002 rule 3: this runs in full, at instantiation, before any task is queued.
|
|
19
19
|
*/
|
|
20
20
|
import type { Finding, FlowDefinition, GateType, GateTypes, JsonSchema, StaticGateType, LanguageAdapter, Languages, NodeTypeEntry, NodeTypes, Pools, Registry, WorkerEntry, Workers } from './types.js';
|
|
21
|
-
export { readKeyPath } from './condition.js';
|
|
21
|
+
export { keyPathSteps, readKeyPath } from './condition.js';
|
|
22
|
+
export type { KeyPathStep } from './condition.js';
|
|
22
23
|
declare const KINDS: readonly ["task", "fanout", "map", "join", "end", "gate", "ask"];
|
|
23
24
|
/**
|
|
24
25
|
* **`FD-V16`'s charset — a flow's and a node's id** ([spec/task-id.md](../../spec/task-id.md)'s) — v0.50 (W210,
|
|
@@ -269,7 +270,7 @@ export declare const PREDICATE_GROUP_KEYS: readonly string[];
|
|
|
269
270
|
* has owned *an edge's `when` names a predicate absent from the registry* since P1 and whose only
|
|
270
271
|
* change at v0.27 is where the name is written.
|
|
271
272
|
*/
|
|
272
|
-
export declare function predicateProblem(v: unknown): {
|
|
273
|
+
export declare function predicateProblem(v: unknown, edgeNeeds?: readonly string[]): {
|
|
273
274
|
rule: string;
|
|
274
275
|
message: string;
|
|
275
276
|
} | null;
|