@crewhaus/tool-flow 0.7.0 → 0.8.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/LICENSE +201 -0
- package/NOTICE +9 -0
- package/README.md +39 -30
- package/dist/index.d.ts +6 -0
- package/dist/index.js +52 -16
- package/dist/lib/assign.d.ts +4 -2
- package/dist/lib/assign.js +15 -4
- package/dist/lib/branch.d.ts +11 -1
- package/dist/lib/branch.js +24 -4
- package/dist/lib/consensus.js +86 -9
- package/dist/lib/decision.d.ts +9 -2
- package/dist/lib/decision.js +26 -5
- package/dist/lib/errors.d.ts +25 -4
- package/dist/lib/errors.js +70 -16
- package/dist/lib/http-date.d.ts +30 -0
- package/dist/lib/http-date.js +175 -0
- package/dist/lib/score.d.ts +16 -14
- package/dist/lib/score.js +55 -11
- package/dist/lib/sequence.d.ts +10 -2
- package/dist/lib/sequence.js +29 -12
- package/package.json +12 -7
package/dist/lib/score.js
CHANGED
|
@@ -8,8 +8,23 @@
|
|
|
8
8
|
* be re-run identically next quarter. This returns the contributors, and the
|
|
9
9
|
* version of the rules that produced them.
|
|
10
10
|
*/
|
|
11
|
-
import {
|
|
12
|
-
|
|
11
|
+
import { decimalKernel } from "@crewhaus/tool-math";
|
|
12
|
+
import { checksVerdict, runChecks } from "@crewhaus/tool-schema";
|
|
13
|
+
const { align, decimalAdd, decimalToNumber, parseDecimal } = decimalKernel;
|
|
14
|
+
/**
|
|
15
|
+
* `a` compared with `b`, exactly: -1, 0 or 1. A bound of -Infinity or
|
|
16
|
+
* Infinity (a library caller's, since JSON cannot spell one) compares as
|
|
17
|
+
* what it is rather than failing the parse.
|
|
18
|
+
*/
|
|
19
|
+
function compareExact(a, b) {
|
|
20
|
+
if (b === Number.NEGATIVE_INFINITY)
|
|
21
|
+
return 1;
|
|
22
|
+
if (b === Number.POSITIVE_INFINITY)
|
|
23
|
+
return -1;
|
|
24
|
+
const aligned = align(a, parseDecimal(b));
|
|
25
|
+
return aligned.a < aligned.b ? -1 : aligned.a > aligned.b ? 1 : 0;
|
|
26
|
+
}
|
|
27
|
+
export function scoreValue(value, model, options = {}) {
|
|
13
28
|
if (model.rules.length === 0)
|
|
14
29
|
throw new Error("the scoring model has no rules");
|
|
15
30
|
const seen = new Set();
|
|
@@ -29,28 +44,57 @@ export function scoreValue(value, model) {
|
|
|
29
44
|
}
|
|
30
45
|
const contributors = [];
|
|
31
46
|
const missed = [];
|
|
32
|
-
|
|
47
|
+
// Summed as exact decimals, not doubles. Points are decimal literals, and
|
|
48
|
+
// String(number) gives that literal back, so the sum is the one a person
|
|
49
|
+
// adds up by hand: 0.7 + 0.1 is 0.8 and lands in a band at 0.8, where the
|
|
50
|
+
// float sum 0.7999999999999999 missed it. Exact addition is also
|
|
51
|
+
// associative, so the band no longer depends on the order the rules were
|
|
52
|
+
// declared in (0.1 + 0.1 + 0.6 and 0.6 + 0.1 + 0.1 used to band apart).
|
|
53
|
+
let exact = { unscaled: 0n, scale: 0 };
|
|
54
|
+
const undecided = [];
|
|
33
55
|
for (const rule of model.rules) {
|
|
34
|
-
const report = runChecks(value, rule.when);
|
|
35
|
-
|
|
36
|
-
|
|
56
|
+
const report = runChecks(value, rule.when, options.regex === undefined ? {} : { regex: options.regex });
|
|
57
|
+
const verdict = checksVerdict(report, "all");
|
|
58
|
+
if (verdict === "pass") {
|
|
59
|
+
exact = decimalAdd(exact, parseDecimal(rule.points));
|
|
37
60
|
contributors.push({ id: rule.id, label: rule.label ?? rule.id, points: rule.points });
|
|
38
61
|
}
|
|
62
|
+
else if (verdict === "undetermined") {
|
|
63
|
+
const why = report.failures.find((f) => f.undetermined === true)?.reason ?? "";
|
|
64
|
+
undecided.push({ id: rule.id, reason: why });
|
|
65
|
+
}
|
|
39
66
|
else {
|
|
40
67
|
missed.push({ id: rule.id, reason: report.failures[0]?.reason ?? "" });
|
|
41
68
|
}
|
|
42
69
|
}
|
|
70
|
+
if (undecided.length > 0) {
|
|
71
|
+
return {
|
|
72
|
+
score: null,
|
|
73
|
+
rawScore: null,
|
|
74
|
+
band: null,
|
|
75
|
+
version: model.version ?? null,
|
|
76
|
+
contributors,
|
|
77
|
+
missed,
|
|
78
|
+
undetermined: undecided,
|
|
79
|
+
};
|
|
80
|
+
}
|
|
81
|
+
const rawScore = decimalToNumber(exact);
|
|
43
82
|
let score = rawScore;
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
83
|
+
let clamped = exact;
|
|
84
|
+
if (model.min !== undefined && compareExact(clamped, model.min) < 0) {
|
|
85
|
+
score = model.min;
|
|
86
|
+
clamped = parseDecimal(model.min);
|
|
87
|
+
}
|
|
88
|
+
if (model.max !== undefined && compareExact(clamped, model.max) > 0) {
|
|
89
|
+
score = model.max;
|
|
90
|
+
clamped = parseDecimal(model.max);
|
|
91
|
+
}
|
|
48
92
|
let band = null;
|
|
49
93
|
if (model.bands && model.bands.length > 0) {
|
|
50
94
|
// Highest qualifying band wins, whatever order they were declared in, so
|
|
51
95
|
// a table written low-to-high and one written high-to-low agree.
|
|
52
96
|
const sorted = [...model.bands].sort((a, b) => b.min - a.min);
|
|
53
|
-
band = sorted.find((b) =>
|
|
97
|
+
band = sorted.find((b) => compareExact(clamped, b.min) >= 0)?.name ?? null;
|
|
54
98
|
}
|
|
55
99
|
return { score, rawScore, band, version: model.version ?? null, contributors, missed };
|
|
56
100
|
}
|
package/dist/lib/sequence.d.ts
CHANGED
|
@@ -27,7 +27,7 @@
|
|
|
27
27
|
* a step that died three turns ago.
|
|
28
28
|
* - **skipped** — the step's own conditions did not hold. A settled no.
|
|
29
29
|
*/
|
|
30
|
-
import { type Check } from "@crewhaus/tool-schema";
|
|
30
|
+
import { type Check, type RegexAnswers } from "@crewhaus/tool-schema";
|
|
31
31
|
import type { MatchMode } from "./branch";
|
|
32
32
|
/**
|
|
33
33
|
* Where a flow stands.
|
|
@@ -90,6 +90,12 @@ export type SequencePlan = {
|
|
|
90
90
|
readonly waiting: ReadonlyArray<HeldStep>;
|
|
91
91
|
readonly unreachable: ReadonlyArray<HeldStep>;
|
|
92
92
|
readonly skipped: ReadonlyArray<HeldStep>;
|
|
93
|
+
/**
|
|
94
|
+
* Steps whose conditions could not be decided (a pattern had no answer):
|
|
95
|
+
* neither ready nor skipped, so the flow is `blocked` on them rather than
|
|
96
|
+
* running or ruling out a step on a guess. Present only when there are any.
|
|
97
|
+
*/
|
|
98
|
+
readonly undetermined?: ReadonlyArray<HeldStep>;
|
|
93
99
|
readonly completed: ReadonlyArray<string>;
|
|
94
100
|
readonly failed: ReadonlyArray<string>;
|
|
95
101
|
readonly version: string | null;
|
|
@@ -106,4 +112,6 @@ export type SequencePlan = {
|
|
|
106
112
|
* a leftover from an older revision of the flow — would silently re-run a
|
|
107
113
|
* step the caller believes is finished.
|
|
108
114
|
*/
|
|
109
|
-
export declare function planSequence(value: unknown, spec: SequenceSpec, state: SequenceState
|
|
115
|
+
export declare function planSequence(value: unknown, spec: SequenceSpec, state: SequenceState, options?: {
|
|
116
|
+
readonly regex?: RegexAnswers;
|
|
117
|
+
}): SequencePlan;
|
package/dist/lib/sequence.js
CHANGED
|
@@ -27,7 +27,7 @@
|
|
|
27
27
|
* a step that died three turns ago.
|
|
28
28
|
* - **skipped** — the step's own conditions did not hold. A settled no.
|
|
29
29
|
*/
|
|
30
|
-
import { runChecks } from "@crewhaus/tool-schema";
|
|
30
|
+
import { checksVerdict, runChecks } from "@crewhaus/tool-schema";
|
|
31
31
|
/**
|
|
32
32
|
* Where a flow stands.
|
|
33
33
|
*
|
|
@@ -47,7 +47,7 @@ export const PLAN_STATES = ["ready", "blocked", "finished", "halted"];
|
|
|
47
47
|
* a leftover from an older revision of the flow — would silently re-run a
|
|
48
48
|
* step the caller believes is finished.
|
|
49
49
|
*/
|
|
50
|
-
export function planSequence(value, spec, state) {
|
|
50
|
+
export function planSequence(value, spec, state, options = {}) {
|
|
51
51
|
const steps = spec.steps;
|
|
52
52
|
if (steps.length === 0)
|
|
53
53
|
throw new Error("the sequence has no steps");
|
|
@@ -132,16 +132,28 @@ export function planSequence(value, spec, state) {
|
|
|
132
132
|
});
|
|
133
133
|
continue;
|
|
134
134
|
}
|
|
135
|
-
// The shared evaluator reports a check
|
|
136
|
-
//
|
|
137
|
-
//
|
|
138
|
-
//
|
|
139
|
-
//
|
|
140
|
-
//
|
|
141
|
-
//
|
|
135
|
+
// The shared evaluator reports a malformed check (a regex that will not
|
|
136
|
+
// compile, a bad path) as a check that did not hold, with the reason as
|
|
137
|
+
// its text. That is `Branch` and `RuleScore`'s behaviour too, and
|
|
138
|
+
// inventing a different one here would mean two answers to the same
|
|
139
|
+
// question. It bites harder in a sequence, because the skip cascades
|
|
140
|
+
// into `unreachable` dependents, so the reason travels with every one of
|
|
141
|
+
// them. A pattern that could not be run to an answer is different: it
|
|
142
|
+
// is not a verdict, so the step is held as undetermined instead.
|
|
142
143
|
if (step.when && step.when.length > 0) {
|
|
143
|
-
const report = runChecks(value, step.when);
|
|
144
|
-
const
|
|
144
|
+
const report = runChecks(value, step.when, options.regex === undefined ? {} : { regex: options.regex });
|
|
145
|
+
const verdict = checksVerdict(report, step.match ?? "all");
|
|
146
|
+
if (verdict === "undetermined") {
|
|
147
|
+
// Neither due nor ruled out: skipping it would cascade `unreachable`
|
|
148
|
+
// through its dependents on an answer nobody got.
|
|
149
|
+
states.set(step.id, "undetermined");
|
|
150
|
+
reasons.set(step.id, {
|
|
151
|
+
id: step.id,
|
|
152
|
+
reason: report.failures.find((f) => f.undetermined === true)?.reason ?? "",
|
|
153
|
+
});
|
|
154
|
+
continue;
|
|
155
|
+
}
|
|
156
|
+
const holds = verdict === "pass";
|
|
145
157
|
if (!holds) {
|
|
146
158
|
states.set(step.id, "skipped");
|
|
147
159
|
reasons.set(step.id, {
|
|
@@ -160,6 +172,7 @@ export function planSequence(value, spec, state) {
|
|
|
160
172
|
const waiting = [];
|
|
161
173
|
const unreachable = [];
|
|
162
174
|
const skipped = [];
|
|
175
|
+
const undecided = [];
|
|
163
176
|
const doneIds = [];
|
|
164
177
|
const failedIds = [];
|
|
165
178
|
for (const [index, step] of steps.entries()) {
|
|
@@ -179,6 +192,9 @@ export function planSequence(value, spec, state) {
|
|
|
179
192
|
case "skipped":
|
|
180
193
|
skipped.push(held);
|
|
181
194
|
break;
|
|
195
|
+
case "undetermined":
|
|
196
|
+
undecided.push(held);
|
|
197
|
+
break;
|
|
182
198
|
case "done":
|
|
183
199
|
doneIds.push(step.id);
|
|
184
200
|
break;
|
|
@@ -189,7 +205,7 @@ export function planSequence(value, spec, state) {
|
|
|
189
205
|
}
|
|
190
206
|
const planState = ready.length > 0
|
|
191
207
|
? "ready"
|
|
192
|
-
: waiting.length > 0
|
|
208
|
+
: waiting.length > 0 || undecided.length > 0
|
|
193
209
|
? "blocked"
|
|
194
210
|
: failedIds.length > 0 || unreachable.length > 0
|
|
195
211
|
? "halted"
|
|
@@ -201,6 +217,7 @@ export function planSequence(value, spec, state) {
|
|
|
201
217
|
waiting,
|
|
202
218
|
unreachable,
|
|
203
219
|
skipped,
|
|
220
|
+
...(undecided.length > 0 ? { undetermined: undecided } : {}),
|
|
204
221
|
completed: doneIds,
|
|
205
222
|
failed: failedIds,
|
|
206
223
|
version: spec.version ?? null,
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@crewhaus/tool-flow",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.8.0",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"description": "Deterministic control-flow tools: branching, decision tables, error classification, deadlines, consensus, stall detection, rule scoring, record routing and sequence planning",
|
|
6
6
|
"main": "dist/index.js",
|
|
@@ -8,17 +8,19 @@
|
|
|
8
8
|
"exports": {
|
|
9
9
|
".": {
|
|
10
10
|
"types": "./dist/index.d.ts",
|
|
11
|
-
"import": "./dist/index.js"
|
|
11
|
+
"import": "./dist/index.js",
|
|
12
|
+
"default": "./dist/index.js"
|
|
12
13
|
}
|
|
13
14
|
},
|
|
14
15
|
"scripts": {
|
|
15
16
|
"test": "bun test src"
|
|
16
17
|
},
|
|
17
18
|
"dependencies": {
|
|
18
|
-
"@crewhaus/errors": "0.
|
|
19
|
-
"@crewhaus/tool-builder": "0.
|
|
20
|
-
"@crewhaus/tool-catalog": "0.
|
|
21
|
-
"@crewhaus/tool-
|
|
19
|
+
"@crewhaus/errors": "0.8.0",
|
|
20
|
+
"@crewhaus/tool-builder": "0.8.0",
|
|
21
|
+
"@crewhaus/tool-catalog": "0.8.0",
|
|
22
|
+
"@crewhaus/tool-math": "0.8.0",
|
|
23
|
+
"@crewhaus/tool-schema": "0.8.0",
|
|
22
24
|
"zod": "^3.23.8"
|
|
23
25
|
},
|
|
24
26
|
"devDependencies": {
|
|
@@ -42,5 +44,8 @@
|
|
|
42
44
|
"publishConfig": {
|
|
43
45
|
"access": "public"
|
|
44
46
|
},
|
|
45
|
-
"files": ["dist", "README.md", "LICENSE", "NOTICE"]
|
|
47
|
+
"files": ["dist", "README.md", "LICENSE", "NOTICE"],
|
|
48
|
+
"engines": {
|
|
49
|
+
"bun": ">=1.3.14"
|
|
50
|
+
}
|
|
46
51
|
}
|