@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/LICENSE
ADDED
|
@@ -0,0 +1,201 @@
|
|
|
1
|
+
Apache License
|
|
2
|
+
Version 2.0, January 2004
|
|
3
|
+
http://www.apache.org/licenses/
|
|
4
|
+
|
|
5
|
+
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
|
|
6
|
+
|
|
7
|
+
1. Definitions.
|
|
8
|
+
|
|
9
|
+
"License" shall mean the terms and conditions for use, reproduction,
|
|
10
|
+
and distribution as defined by Sections 1 through 9 of this document.
|
|
11
|
+
|
|
12
|
+
"Licensor" shall mean the copyright owner or entity authorized by
|
|
13
|
+
the copyright owner that is granting the License.
|
|
14
|
+
|
|
15
|
+
"Legal Entity" shall mean the union of the acting entity and all
|
|
16
|
+
other entities that control, are controlled by, or are under common
|
|
17
|
+
control with that entity. For the purposes of this definition,
|
|
18
|
+
"control" means (i) the power, direct or indirect, to cause the
|
|
19
|
+
direction or management of such entity, whether by contract or
|
|
20
|
+
otherwise, or (ii) ownership of fifty percent (50%) or more of the
|
|
21
|
+
outstanding shares, or (iii) beneficial ownership of such entity.
|
|
22
|
+
|
|
23
|
+
"You" (or "Your") shall mean an individual or Legal Entity
|
|
24
|
+
exercising permissions granted by this License.
|
|
25
|
+
|
|
26
|
+
"Source" form shall mean the preferred form for making modifications,
|
|
27
|
+
including but not limited to software source code, documentation
|
|
28
|
+
source, and configuration files.
|
|
29
|
+
|
|
30
|
+
"Object" form shall mean any form resulting from mechanical
|
|
31
|
+
transformation or translation of a Source form, including but
|
|
32
|
+
not limited to compiled object code, generated documentation,
|
|
33
|
+
and conversions to other media types.
|
|
34
|
+
|
|
35
|
+
"Work" shall mean the work of authorship, whether in Source or
|
|
36
|
+
Object form, made available under the License, as indicated by a
|
|
37
|
+
copyright notice that is included in or attached to the work
|
|
38
|
+
(an example is provided in the Appendix below).
|
|
39
|
+
|
|
40
|
+
"Derivative Works" shall mean any work, whether in Source or Object
|
|
41
|
+
form, that is based on (or derived from) the Work and for which the
|
|
42
|
+
editorial revisions, annotations, elaborations, or other modifications
|
|
43
|
+
represent, as a whole, an original work of authorship. For the purposes
|
|
44
|
+
of this License, Derivative Works shall not include works that remain
|
|
45
|
+
separable from, or merely link (or bind by name) to the interfaces of,
|
|
46
|
+
the Work and Derivative Works thereof.
|
|
47
|
+
|
|
48
|
+
"Contribution" shall mean any work of authorship, including
|
|
49
|
+
the original version of the Work and any modifications or additions
|
|
50
|
+
to that Work or Derivative Works thereof, that is intentionally
|
|
51
|
+
submitted to Licensor for inclusion in the Work by the copyright owner
|
|
52
|
+
or by an individual or Legal Entity authorized to submit on behalf of
|
|
53
|
+
the copyright owner. For the purposes of this definition, "submitted"
|
|
54
|
+
means any form of electronic, verbal, or written communication sent
|
|
55
|
+
to the Licensor or its representatives, including but not limited to
|
|
56
|
+
communication on electronic mailing lists, source code control systems,
|
|
57
|
+
and issue tracking systems that are managed by, or on behalf of, the
|
|
58
|
+
Licensor for the purpose of discussing and improving the Work, but
|
|
59
|
+
excluding communication that is conspicuously marked or otherwise
|
|
60
|
+
designated in writing by the copyright owner as "Not a Contribution."
|
|
61
|
+
|
|
62
|
+
"Contributor" shall mean Licensor and any individual or Legal Entity
|
|
63
|
+
on behalf of whom a Contribution has been received by Licensor and
|
|
64
|
+
subsequently incorporated within the Work.
|
|
65
|
+
|
|
66
|
+
2. Grant of Copyright License. Subject to the terms and conditions of
|
|
67
|
+
this License, each Contributor hereby grants to You a perpetual,
|
|
68
|
+
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
|
69
|
+
copyright license to reproduce, prepare Derivative Works of,
|
|
70
|
+
publicly display, publicly perform, sublicense, and distribute the
|
|
71
|
+
Work and such Derivative Works in Source or Object form.
|
|
72
|
+
|
|
73
|
+
3. Grant of Patent License. Subject to the terms and conditions of
|
|
74
|
+
this License, each Contributor hereby grants to You a perpetual,
|
|
75
|
+
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
|
76
|
+
(except as stated in this section) patent license to make, have made,
|
|
77
|
+
use, offer to sell, sell, import, and otherwise transfer the Work,
|
|
78
|
+
where such license applies only to those patent claims licensable
|
|
79
|
+
by such Contributor that are necessarily infringed by their
|
|
80
|
+
Contribution(s) alone or by combination of their Contribution(s)
|
|
81
|
+
with the Work to which such Contribution(s) was submitted. If You
|
|
82
|
+
institute patent litigation against any entity (including a
|
|
83
|
+
cross-claim or counterclaim in a lawsuit) alleging that the Work
|
|
84
|
+
or a Contribution incorporated within the Work constitutes direct
|
|
85
|
+
or contributory patent infringement, then any patent licenses
|
|
86
|
+
granted to You under this License for that Work shall terminate
|
|
87
|
+
as of the date such litigation is filed.
|
|
88
|
+
|
|
89
|
+
4. Redistribution. You may reproduce and distribute copies of the
|
|
90
|
+
Work or Derivative Works thereof in any medium, with or without
|
|
91
|
+
modifications, and in Source or Object form, provided that You
|
|
92
|
+
meet the following conditions:
|
|
93
|
+
|
|
94
|
+
(a) You must give any other recipients of the Work or
|
|
95
|
+
Derivative Works a copy of this License; and
|
|
96
|
+
|
|
97
|
+
(b) You must cause any modified files to carry prominent notices
|
|
98
|
+
stating that You changed the files; and
|
|
99
|
+
|
|
100
|
+
(c) You must retain, in the Source form of any Derivative Works
|
|
101
|
+
that You distribute, all copyright, patent, trademark, and
|
|
102
|
+
attribution notices from the Source form of the Work,
|
|
103
|
+
excluding those notices that do not pertain to any part of
|
|
104
|
+
the Derivative Works; and
|
|
105
|
+
|
|
106
|
+
(d) If the Work includes a "NOTICE" text file as part of its
|
|
107
|
+
distribution, then any Derivative Works that You distribute must
|
|
108
|
+
include a readable copy of the attribution notices contained
|
|
109
|
+
within such NOTICE file, excluding those notices that do not
|
|
110
|
+
pertain to any part of the Derivative Works, in at least one
|
|
111
|
+
of the following places: within a NOTICE text file distributed
|
|
112
|
+
as part of the Derivative Works; within the Source form or
|
|
113
|
+
documentation, if provided along with the Derivative Works; or,
|
|
114
|
+
within a display generated by the Derivative Works, if and
|
|
115
|
+
wherever such third-party notices normally appear. The contents
|
|
116
|
+
of the NOTICE file are for informational purposes only and
|
|
117
|
+
do not modify the License. You may add Your own attribution
|
|
118
|
+
notices within Derivative Works that You distribute, alongside
|
|
119
|
+
or as an addendum to the NOTICE text from the Work, provided
|
|
120
|
+
that such additional attribution notices cannot be construed
|
|
121
|
+
as modifying the License.
|
|
122
|
+
|
|
123
|
+
You may add Your own copyright statement to Your modifications and
|
|
124
|
+
may provide additional or different license terms and conditions
|
|
125
|
+
for use, reproduction, or distribution of Your modifications, or
|
|
126
|
+
for any such Derivative Works as a whole, provided Your use,
|
|
127
|
+
reproduction, and distribution of the Work otherwise complies with
|
|
128
|
+
the conditions stated in this License.
|
|
129
|
+
|
|
130
|
+
5. Submission of Contributions. Unless You explicitly state otherwise,
|
|
131
|
+
any Contribution intentionally submitted for inclusion in the Work
|
|
132
|
+
by You to the Licensor shall be under the terms and conditions of
|
|
133
|
+
this License, without any additional terms or conditions.
|
|
134
|
+
Notwithstanding the above, nothing herein shall supersede or modify
|
|
135
|
+
the terms of any separate license agreement you may have executed
|
|
136
|
+
with Licensor regarding such Contributions.
|
|
137
|
+
|
|
138
|
+
6. Trademarks. This License does not grant permission to use the trade
|
|
139
|
+
names, trademarks, service marks, or product names of the Licensor,
|
|
140
|
+
except as required for reasonable and customary use in describing the
|
|
141
|
+
origin of the Work and reproducing the content of the NOTICE file.
|
|
142
|
+
|
|
143
|
+
7. Disclaimer of Warranty. Unless required by applicable law or
|
|
144
|
+
agreed to in writing, Licensor provides the Work (and each
|
|
145
|
+
Contributor provides its Contributions) on an "AS IS" BASIS,
|
|
146
|
+
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
|
|
147
|
+
implied, including, without limitation, any warranties or conditions
|
|
148
|
+
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
|
|
149
|
+
PARTICULAR PURPOSE. You are solely responsible for determining the
|
|
150
|
+
appropriateness of using or redistributing the Work and assume any
|
|
151
|
+
risks associated with Your exercise of permissions under this License.
|
|
152
|
+
|
|
153
|
+
8. Limitation of Liability. In no event and under no legal theory,
|
|
154
|
+
whether in tort (including negligence), contract, or otherwise,
|
|
155
|
+
unless required by applicable law (such as deliberate and grossly
|
|
156
|
+
negligent acts) or agreed to in writing, shall any Contributor be
|
|
157
|
+
liable to You for damages, including any direct, indirect, special,
|
|
158
|
+
incidental, or consequential damages of any character arising as a
|
|
159
|
+
result of this License or out of the use or inability to use the
|
|
160
|
+
Work (including but not limited to damages for loss of goodwill,
|
|
161
|
+
work stoppage, computer failure or malfunction, or any and all
|
|
162
|
+
other commercial damages or losses), even if such Contributor
|
|
163
|
+
has been advised of the possibility of such damages.
|
|
164
|
+
|
|
165
|
+
9. Accepting Warranty or Additional Liability. While redistributing
|
|
166
|
+
the Work or Derivative Works thereof, You may choose to offer,
|
|
167
|
+
and charge a fee for, acceptance of support, warranty, indemnity,
|
|
168
|
+
or other liability obligations and/or rights consistent with this
|
|
169
|
+
License. However, in accepting such obligations, You may act only
|
|
170
|
+
on Your own behalf and on Your sole responsibility, not on behalf
|
|
171
|
+
of any other Contributor, and only if You agree to indemnify,
|
|
172
|
+
defend, and hold each Contributor harmless for any liability
|
|
173
|
+
incurred by, or claims asserted against, such Contributor by reason
|
|
174
|
+
of your accepting any such warranty or additional liability.
|
|
175
|
+
|
|
176
|
+
END OF TERMS AND CONDITIONS
|
|
177
|
+
|
|
178
|
+
APPENDIX: How to apply the Apache License to your work.
|
|
179
|
+
|
|
180
|
+
To apply the Apache License to your work, attach the following
|
|
181
|
+
boilerplate notice, with the fields enclosed by brackets "[]"
|
|
182
|
+
replaced with your own identifying information. (Don't include
|
|
183
|
+
the brackets!) The text should be enclosed in the appropriate
|
|
184
|
+
comment syntax for the file format. We also recommend that a
|
|
185
|
+
file or class name and description of purpose be included on the
|
|
186
|
+
same "printed page" as the copyright notice for easier
|
|
187
|
+
identification within third-party archives.
|
|
188
|
+
|
|
189
|
+
Copyright [yyyy] [name of copyright owner]
|
|
190
|
+
|
|
191
|
+
Licensed under the Apache License, Version 2.0 (the "License");
|
|
192
|
+
you may not use this file except in compliance with the License.
|
|
193
|
+
You may obtain a copy of the License at
|
|
194
|
+
|
|
195
|
+
http://www.apache.org/licenses/LICENSE-2.0
|
|
196
|
+
|
|
197
|
+
Unless required by applicable law or agreed to in writing, software
|
|
198
|
+
distributed under the License is distributed on an "AS IS" BASIS,
|
|
199
|
+
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
200
|
+
See the License for the specific language governing permissions and
|
|
201
|
+
limitations under the License.
|
package/NOTICE
ADDED
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
CrewHaus Factory
|
|
2
|
+
Copyright 2026 Max Meier
|
|
3
|
+
|
|
4
|
+
This product includes software developed by Max Meier
|
|
5
|
+
(operating as StudioMax — https://studiomax.io) and contributors.
|
|
6
|
+
|
|
7
|
+
The CrewHaus name and logo are trademarks of Max Meier. See
|
|
8
|
+
https://github.com/crewhaus/factory/blob/main/TRADEMARK.md for the
|
|
9
|
+
use-of-marks policy.
|
package/README.md
CHANGED
|
@@ -61,6 +61,10 @@ explanation rather than guessed at, because per ECMAScript an offset-less
|
|
|
61
61
|
date-time string is *local* time while the date-only form is UTC — so the
|
|
62
62
|
same spec would mean different instants on two machines.
|
|
63
63
|
|
|
64
|
+
`RuleScore` adds points as exact decimals, not binary floats, so `0.7 + 0.1`
|
|
65
|
+
is `0.8` and meets a band at `0.8`, and the total does not depend on the
|
|
66
|
+
order the rules are declared in.
|
|
67
|
+
|
|
64
68
|
An epoch outside the ±8.64e15 ms a `Date` can represent is rejected by name
|
|
65
69
|
for the same reason: the field that parsed it is the only place that still
|
|
66
70
|
knows which of a spec's several instants was the bad one, and left to reach a
|
|
@@ -137,6 +141,12 @@ Two details worth knowing:
|
|
|
137
141
|
`capacity`, which is retryable in general — but that particular one
|
|
138
142
|
resolves to `escalate`, and a caller reading the boolean would otherwise
|
|
139
143
|
re-run the identical command and be killed identically.
|
|
144
|
+
- **A `Retry-After` date is read only with its zone written.** The RFC 9110
|
|
145
|
+
HTTP-date forms (GMT by definition), an RFC 5322 date with its zone
|
|
146
|
+
(`+0200`, `UTC`, `-0000`, `PDT`), and an ISO-8601 instant with an offset
|
|
147
|
+
(`T` or a space before the time) are the same instant on every machine; a
|
|
148
|
+
date with no zone is not read at all, rather than read as the host's local
|
|
149
|
+
time. A date that does not exist (30 February, hour 24) is not read either.
|
|
140
150
|
- **Message matching uses substrings, not regular expressions.** That text
|
|
141
151
|
comes from a remote server, and a pattern with nested quantifiers there is
|
|
142
152
|
a denial of service waiting to happen. Caller-supplied rules may use a
|
|
@@ -146,33 +156,32 @@ Exit code `1` is deliberately unclassified. It is the generic "it failed" and
|
|
|
146
156
|
says nothing about why, so claiming a class for it would be inventing
|
|
147
157
|
information.
|
|
148
158
|
|
|
149
|
-
##
|
|
150
|
-
|
|
151
|
-
The `matches` and `notMatches` ops
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
- `
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
- `
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
do.
|
|
159
|
+
## Caller patterns: off the thread, and never a guessed miss
|
|
160
|
+
|
|
161
|
+
The `matches` and `notMatches` ops, and an `ErrorClassify` rule's `matches`,
|
|
162
|
+
run a caller-supplied pattern against caller-supplied text. JavaScript's regex
|
|
163
|
+
engine backtracks, so a synchronous match can hold the whole process
|
|
164
|
+
(`a*a*a*a*b` against 200 letters: 3.7 s; against 400: 108 s), and when the
|
|
165
|
+
engine gives up on a match it answers "no match".
|
|
166
|
+
|
|
167
|
+
So no tool here runs one on the caller's thread. Before evaluating, each tool
|
|
168
|
+
asks every pattern its arms, rows, rules, owners or steps will test, answers
|
|
169
|
+
them all in one `@crewhaus/tool-safety` regex worker under a five-second
|
|
170
|
+
deadline, and then evaluates with the answers (the shared machinery is in
|
|
171
|
+
`@crewhaus/tool-schema`). A pattern the screen refuses (a shape that
|
|
172
|
+
backtracks exponentially, such as `(a+)+`) fails its check as an invalid
|
|
173
|
+
regex does, and an `ErrorClassify` rule with one is an invalid rule. A pattern
|
|
174
|
+
that cannot be run to an answer (the deadline, the engine giving up, which
|
|
175
|
+
includes a no-match slower than 100 ms) decides nothing:
|
|
176
|
+
|
|
177
|
+
- `Branch` takes no arm, and not `otherwise`, and says which arm was undecided;
|
|
178
|
+
- `DecisionTable` gives no outputs when the undecided row could change them;
|
|
179
|
+
- `RuleScore` gives a null score and band, and lists the undecided rules;
|
|
180
|
+
- `LeadAssign` refuses, as it does for an owner missing a load;
|
|
181
|
+
- `SequenceRun` holds the step in `undetermined`, and the flow is `blocked`;
|
|
182
|
+
- `ErrorClassify` answers class `unknown`, action `escalate`, source
|
|
183
|
+
`undetermined`, never a later rule's or a builtin pack's answer.
|
|
184
|
+
|
|
185
|
+
`ErrorClassify` still matches `message` and `code` separately (a pattern
|
|
186
|
+
anchored with `$` against a joined string could never match), and its
|
|
187
|
+
builtin packs still use substrings, never patterns.
|
package/dist/index.d.ts
CHANGED
|
@@ -12,3 +12,9 @@ export declare const sequenceRun: RegisteredTool;
|
|
|
12
12
|
/** Every tool this package registers, in the order a catalog should list them. */
|
|
13
13
|
export declare const FLOW_TOOLS: ReadonlyArray<RegisteredTool>;
|
|
14
14
|
export type { Check };
|
|
15
|
+
/**
|
|
16
|
+
* The strict RFC 9110 HTTP-date reader ErrorClassify uses, for any package
|
|
17
|
+
* that turns a Retry-After or an Expires header into an instant: it never
|
|
18
|
+
* reads a zone-less date as the host's local time.
|
|
19
|
+
*/
|
|
20
|
+
export { parseHttpDate } from "./lib/http-date";
|
package/dist/index.js
CHANGED
|
@@ -21,14 +21,14 @@
|
|
|
21
21
|
* operator learns one grammar.
|
|
22
22
|
*/
|
|
23
23
|
import { buildTool } from "@crewhaus/tool-builder";
|
|
24
|
-
import { ASSERT_OPS } from "@crewhaus/tool-schema";
|
|
24
|
+
import { ASSERT_OPS, RegexAnswers, askCheckPatterns, regexRunContext, } from "@crewhaus/tool-schema";
|
|
25
25
|
import { z } from "zod";
|
|
26
26
|
import { ASSIGN_STRATEGIES, assignOwner } from "./lib/assign";
|
|
27
27
|
import { evaluateBranches } from "./lib/branch";
|
|
28
28
|
import { VOTE_MODES, tallyVotes } from "./lib/consensus";
|
|
29
29
|
import { checkDeadline } from "./lib/deadline";
|
|
30
30
|
import { HIT_POLICIES, evaluateTable } from "./lib/decision";
|
|
31
|
-
import { ERROR_CLASSES, NEXT_ACTIONS, classifyError } from "./lib/errors";
|
|
31
|
+
import { ERROR_CLASSES, NEXT_ACTIONS, askErrorPatterns, classifyError, } from "./lib/errors";
|
|
32
32
|
import { scoreValue } from "./lib/score";
|
|
33
33
|
import { planSequence } from "./lib/sequence";
|
|
34
34
|
import { detectStall } from "./lib/stall";
|
|
@@ -102,6 +102,21 @@ function parseInstant(value, field) {
|
|
|
102
102
|
return parsed;
|
|
103
103
|
}
|
|
104
104
|
const instantField = z.union([z.string(), z.number()]);
|
|
105
|
+
/**
|
|
106
|
+
* Answer every `matches` pattern the given check lists will test against
|
|
107
|
+
* `value`, in the regex worker, before any of them is evaluated. The
|
|
108
|
+
* evaluators then read the answers and never run a caller's pattern on this
|
|
109
|
+
* thread; a pattern with no answer leaves its arm, row, rule, owner or step
|
|
110
|
+
* undetermined rather than unmatched.
|
|
111
|
+
*/
|
|
112
|
+
async function answerChecks(value, lists, ctx) {
|
|
113
|
+
const regex = new RegexAnswers();
|
|
114
|
+
for (const list of lists)
|
|
115
|
+
if (list !== undefined)
|
|
116
|
+
askCheckPatterns(value, list, regex);
|
|
117
|
+
await regex.resolve(regexRunContext(ctx));
|
|
118
|
+
return regex;
|
|
119
|
+
}
|
|
105
120
|
// ---------------------------------------------------------------------------
|
|
106
121
|
export const branch = buildTool({
|
|
107
122
|
name: "Branch",
|
|
@@ -128,16 +143,17 @@ export const branch = buildTool({
|
|
|
128
143
|
}),
|
|
129
144
|
readOnly: true,
|
|
130
145
|
concurrencySafe: true,
|
|
131
|
-
execute: async (input) => {
|
|
132
|
-
const
|
|
133
|
-
|
|
134
|
-
});
|
|
146
|
+
execute: async (input, ctx) => {
|
|
147
|
+
const arms = input.arms;
|
|
148
|
+
const regex = await answerChecks(input.value, arms.map((arm) => arm.when), ctx);
|
|
149
|
+
const outcome = evaluateBranches(input.value, arms, { otherwise: input.otherwise, regex });
|
|
135
150
|
const body = {
|
|
136
151
|
matched: outcome.matched,
|
|
137
152
|
name: outcome.name,
|
|
138
153
|
index: outcome.index,
|
|
139
154
|
fallback: outcome.fallback,
|
|
140
155
|
result: outcome.result,
|
|
156
|
+
...(outcome.undetermined === undefined ? {} : { undetermined: outcome.undetermined }),
|
|
141
157
|
...(input.verbose ? { evaluated: outcome.evaluated } : {}),
|
|
142
158
|
};
|
|
143
159
|
return json(body);
|
|
@@ -164,7 +180,11 @@ export const decisionTable = buildTool({
|
|
|
164
180
|
}),
|
|
165
181
|
readOnly: true,
|
|
166
182
|
concurrencySafe: true,
|
|
167
|
-
execute: async (input) =>
|
|
183
|
+
execute: async (input, ctx) => {
|
|
184
|
+
const table = input;
|
|
185
|
+
const regex = await answerChecks(input.value, table.rows.map((row) => row.when), ctx);
|
|
186
|
+
return json(evaluateTable(input.value, table, { regex }));
|
|
187
|
+
},
|
|
168
188
|
});
|
|
169
189
|
export const errorClassify = buildTool({
|
|
170
190
|
name: "ErrorClassify",
|
|
@@ -181,7 +201,7 @@ export const errorClassify = buildTool({
|
|
|
181
201
|
retryAfter: z
|
|
182
202
|
.string()
|
|
183
203
|
.optional()
|
|
184
|
-
.describe("the raw Retry-After header: seconds or an HTTP-date"),
|
|
204
|
+
.describe("the raw Retry-After header: seconds or an HTTP-date (always GMT); a date with no zone is not read"),
|
|
185
205
|
attempt: z.number().int().positive().optional().describe("1-based attempt number"),
|
|
186
206
|
maxAttempts: z.number().int().positive().optional(),
|
|
187
207
|
now: instantField
|
|
@@ -204,9 +224,14 @@ export const errorClassify = buildTool({
|
|
|
204
224
|
}),
|
|
205
225
|
readOnly: true,
|
|
206
226
|
concurrencySafe: true,
|
|
207
|
-
execute: async (input) => {
|
|
227
|
+
execute: async (input, ctx) => {
|
|
208
228
|
const nowMs = input.now === undefined ? Date.now() : parseInstant(input.now, "now");
|
|
209
|
-
|
|
229
|
+
const rules = input.rules;
|
|
230
|
+
// The caller rules' patterns run in the worker, against the error text.
|
|
231
|
+
const regex = new RegexAnswers();
|
|
232
|
+
askErrorPatterns(input, rules ?? [], regex);
|
|
233
|
+
await regex.resolve(regexRunContext(ctx));
|
|
234
|
+
return json(classifyError(input, { rules, nowMs, regex }));
|
|
210
235
|
},
|
|
211
236
|
});
|
|
212
237
|
export const deadlineCheck = buildTool({
|
|
@@ -341,8 +366,10 @@ export const ruleScore = buildTool({
|
|
|
341
366
|
}),
|
|
342
367
|
readOnly: true,
|
|
343
368
|
concurrencySafe: true,
|
|
344
|
-
execute: async (input) => {
|
|
345
|
-
const
|
|
369
|
+
execute: async (input, ctx) => {
|
|
370
|
+
const model = input;
|
|
371
|
+
const regex = await answerChecks(input.value, model.rules.map((rule) => rule.when), ctx);
|
|
372
|
+
const result = scoreValue(input.value, model, { regex });
|
|
346
373
|
if (input.includeMissed)
|
|
347
374
|
return json(result);
|
|
348
375
|
const { missed: _missed, ...rest } = result;
|
|
@@ -391,8 +418,10 @@ export const leadAssign = buildTool({
|
|
|
391
418
|
}),
|
|
392
419
|
readOnly: true,
|
|
393
420
|
concurrencySafe: true,
|
|
394
|
-
execute: async (input) => {
|
|
395
|
-
const
|
|
421
|
+
execute: async (input, ctx) => {
|
|
422
|
+
const roster = input;
|
|
423
|
+
const regex = await answerChecks(input.value, roster.owners.map((owner) => owner.when), ctx);
|
|
424
|
+
const { considered, ...rest } = assignOwner(input.value, roster, { regex });
|
|
396
425
|
return json(input.verbose ? { ...rest, considered } : rest);
|
|
397
426
|
},
|
|
398
427
|
});
|
|
@@ -433,7 +462,7 @@ export const sequenceRun = buildTool({
|
|
|
433
462
|
}),
|
|
434
463
|
readOnly: true,
|
|
435
464
|
concurrencySafe: true,
|
|
436
|
-
execute: async (input) => {
|
|
465
|
+
execute: async (input, ctx) => {
|
|
437
466
|
const nowMs = input.now === undefined ? Date.now() : parseInstant(input.now, "now");
|
|
438
467
|
// Each `after` is parsed here and dropped, so the planner can only read
|
|
439
468
|
// the parsed number: a step can never end up gated on an offset-less
|
|
@@ -444,7 +473,8 @@ export const sequenceRun = buildTool({
|
|
|
444
473
|
afterMs: after === undefined ? undefined : parseInstant(after, `step "${step.id}" after`),
|
|
445
474
|
when: step.when,
|
|
446
475
|
}));
|
|
447
|
-
|
|
476
|
+
const regex = await answerChecks(input.value, steps.map((step) => step.when), ctx);
|
|
477
|
+
return json(planSequence(input.value, { version: input.version, steps }, { nowMs, completed: input.completed, failed: input.failed }, { regex }));
|
|
448
478
|
},
|
|
449
479
|
});
|
|
450
480
|
/** Every tool this package registers, in the order a catalog should list them. */
|
|
@@ -459,3 +489,9 @@ export const FLOW_TOOLS = Object.freeze([
|
|
|
459
489
|
sequenceRun,
|
|
460
490
|
stallDetect,
|
|
461
491
|
]);
|
|
492
|
+
/**
|
|
493
|
+
* The strict RFC 9110 HTTP-date reader ErrorClassify uses, for any package
|
|
494
|
+
* that turns a Retry-After or an Expires header into an instant: it never
|
|
495
|
+
* reads a zone-less date as the host's local time.
|
|
496
|
+
*/
|
|
497
|
+
export { parseHttpDate } from "./lib/http-date";
|
package/dist/lib/assign.d.ts
CHANGED
|
@@ -21,7 +21,7 @@
|
|
|
21
21
|
* "Nobody is eligible" and "I could not work out who is" are different
|
|
22
22
|
* answers, and only the first one uses `fallback`.
|
|
23
23
|
*/
|
|
24
|
-
import { type Check } from "@crewhaus/tool-schema";
|
|
24
|
+
import { type Check, type RegexAnswers } from "@crewhaus/tool-schema";
|
|
25
25
|
import type { MatchMode } from "./branch";
|
|
26
26
|
/** How the winner is picked from the owners that are eligible. */
|
|
27
27
|
export declare const ASSIGN_STRATEGIES: readonly ["first", "specific", "least_loaded", "round_robin"];
|
|
@@ -119,4 +119,6 @@ export type AssignResult = {
|
|
|
119
119
|
* it false and so reads as the lowest load in the roster. A cursor that is
|
|
120
120
|
* not a whole position goes the same way.
|
|
121
121
|
*/
|
|
122
|
-
export declare function assignOwner(value: unknown, roster: Roster
|
|
122
|
+
export declare function assignOwner(value: unknown, roster: Roster, options?: {
|
|
123
|
+
readonly regex?: RegexAnswers;
|
|
124
|
+
}): AssignResult;
|
package/dist/lib/assign.js
CHANGED
|
@@ -21,7 +21,7 @@
|
|
|
21
21
|
* "Nobody is eligible" and "I could not work out who is" are different
|
|
22
22
|
* answers, and only the first one uses `fallback`.
|
|
23
23
|
*/
|
|
24
|
-
import { runChecks } from "@crewhaus/tool-schema";
|
|
24
|
+
import { checksVerdict, runChecks } from "@crewhaus/tool-schema";
|
|
25
25
|
/** How the winner is picked from the owners that are eligible. */
|
|
26
26
|
export const ASSIGN_STRATEGIES = ["first", "specific", "least_loaded", "round_robin"];
|
|
27
27
|
/**
|
|
@@ -53,7 +53,7 @@ const specificityOf = (owner) => (owner.match ?? "all") === "all" ? owner.when.l
|
|
|
53
53
|
* it false and so reads as the lowest load in the roster. A cursor that is
|
|
54
54
|
* not a whole position goes the same way.
|
|
55
55
|
*/
|
|
56
|
-
export function assignOwner(value, roster) {
|
|
56
|
+
export function assignOwner(value, roster, options = {}) {
|
|
57
57
|
if (roster.owners.length === 0)
|
|
58
58
|
throw new Error("the roster has no owners");
|
|
59
59
|
const seen = new Set();
|
|
@@ -84,11 +84,22 @@ export function assignOwner(value, roster) {
|
|
|
84
84
|
/** Facts the strategy needs and the roster did not supply; see below. */
|
|
85
85
|
const undetermined = [];
|
|
86
86
|
for (const [index, owner] of roster.owners.entries()) {
|
|
87
|
-
const report = runChecks(value, owner.when);
|
|
88
|
-
const
|
|
87
|
+
const report = runChecks(value, owner.when, options.regex === undefined ? {} : { regex: options.regex });
|
|
88
|
+
const verdict = checksVerdict(report, owner.match ?? "all");
|
|
89
|
+
const passes = verdict === "pass";
|
|
89
90
|
const out = (reason) => {
|
|
90
91
|
considered.push({ id: owner.id, eligible: false, reason, matched: report.passed });
|
|
91
92
|
};
|
|
93
|
+
// A territory whose pattern had no answer is neither in nor out, like an
|
|
94
|
+
// owner missing a fact the strategy needs, and the answer refuses rather
|
|
95
|
+
// than hand the lead to someone else.
|
|
96
|
+
if (verdict === "undetermined") {
|
|
97
|
+
const why = report.failures.find((f) => f.undetermined === true)?.reason ?? "";
|
|
98
|
+
const reason = `owner "${owner.id}" could not be decided: ${why}`;
|
|
99
|
+
undetermined.push(reason);
|
|
100
|
+
considered.push({ id: owner.id, eligible: null, reason, matched: report.passed });
|
|
101
|
+
continue;
|
|
102
|
+
}
|
|
92
103
|
if (!passes) {
|
|
93
104
|
out(report.failures[0]?.reason ?? "no condition held");
|
|
94
105
|
continue;
|
package/dist/lib/branch.d.ts
CHANGED
|
@@ -7,7 +7,7 @@
|
|
|
7
7
|
* here: an operator who learns `startsWith` from Assert gets the same
|
|
8
8
|
* operator, with the same semantics, in a branch.
|
|
9
9
|
*/
|
|
10
|
-
import { type Check } from "@crewhaus/tool-schema";
|
|
10
|
+
import { type Check, type RegexAnswers } from "@crewhaus/tool-schema";
|
|
11
11
|
/** Whether every check must hold, or any one of them. */
|
|
12
12
|
export type MatchMode = "all" | "any";
|
|
13
13
|
/** One arm of the branch. `when` is never empty — see {@link evaluateBranches}. */
|
|
@@ -27,6 +27,8 @@ export type ArmReport = {
|
|
|
27
27
|
readonly failed: number;
|
|
28
28
|
/** The first failing check's reason; empty when the arm matched. */
|
|
29
29
|
readonly reason: string;
|
|
30
|
+
/** Set when the arm could not be decided: a pattern had no answer. */
|
|
31
|
+
readonly undetermined?: true;
|
|
30
32
|
};
|
|
31
33
|
export type BranchOutcome = {
|
|
32
34
|
readonly matched: boolean;
|
|
@@ -39,6 +41,12 @@ export type BranchOutcome = {
|
|
|
39
41
|
readonly fallback: boolean;
|
|
40
42
|
/** Every arm evaluated, in order, up to and including the winner. */
|
|
41
43
|
readonly evaluated: ReadonlyArray<ArmReport>;
|
|
44
|
+
/**
|
|
45
|
+
* Why no route was taken, when an arm could not be decided: its pattern
|
|
46
|
+
* had no answer. Later arms and `otherwise` are not tried, because the
|
|
47
|
+
* undecided arm might have been the one that matched.
|
|
48
|
+
*/
|
|
49
|
+
readonly undetermined?: string;
|
|
42
50
|
};
|
|
43
51
|
export type BranchOptions = {
|
|
44
52
|
/** Used when no arm matches. Without it, a miss returns `matched: false`. */
|
|
@@ -46,6 +54,8 @@ export type BranchOptions = {
|
|
|
46
54
|
readonly name: string;
|
|
47
55
|
readonly result?: unknown;
|
|
48
56
|
};
|
|
57
|
+
/** Answers for the arms' `matches` patterns, resolved by the caller in the worker. */
|
|
58
|
+
readonly regex?: RegexAnswers;
|
|
49
59
|
};
|
|
50
60
|
/**
|
|
51
61
|
* Evaluate `rules` against `value` in order and return the first match.
|
package/dist/lib/branch.js
CHANGED
|
@@ -7,7 +7,7 @@
|
|
|
7
7
|
* here: an operator who learns `startsWith` from Assert gets the same
|
|
8
8
|
* operator, with the same semantics, in a branch.
|
|
9
9
|
*/
|
|
10
|
-
import { runChecks } from "@crewhaus/tool-schema";
|
|
10
|
+
import { checksVerdict, runChecks } from "@crewhaus/tool-schema";
|
|
11
11
|
/**
|
|
12
12
|
* Evaluate `rules` against `value` in order and return the first match.
|
|
13
13
|
*
|
|
@@ -41,16 +41,36 @@ export function evaluateBranches(value, rules, options = {}) {
|
|
|
41
41
|
const evaluated = [];
|
|
42
42
|
for (const [index, rule] of rules.entries()) {
|
|
43
43
|
const mode = rule.match ?? "all";
|
|
44
|
-
const report = runChecks(value, rule.when);
|
|
45
|
-
const
|
|
44
|
+
const report = runChecks(value, rule.when, options.regex === undefined ? {} : { regex: options.regex });
|
|
45
|
+
const verdict = checksVerdict(report, mode);
|
|
46
|
+
const ok = verdict === "pass";
|
|
47
|
+
const undecided = report.failures.find((f) => f.undetermined === true);
|
|
46
48
|
evaluated.push({
|
|
47
49
|
name: rule.name,
|
|
48
50
|
index,
|
|
49
51
|
ok,
|
|
50
52
|
passed: report.passed,
|
|
51
53
|
failed: report.failed,
|
|
52
|
-
reason: ok
|
|
54
|
+
reason: ok
|
|
55
|
+
? ""
|
|
56
|
+
: ((verdict === "undetermined" ? undecided?.reason : undefined) ??
|
|
57
|
+
report.failures[0]?.reason ??
|
|
58
|
+
""),
|
|
59
|
+
...(verdict === "undetermined" ? { undetermined: true } : {}),
|
|
53
60
|
});
|
|
61
|
+
if (verdict === "undetermined") {
|
|
62
|
+
// A router that fell through here would take a later arm, or the
|
|
63
|
+
// default, on the strength of an answer it never got.
|
|
64
|
+
return {
|
|
65
|
+
matched: false,
|
|
66
|
+
name: null,
|
|
67
|
+
index: null,
|
|
68
|
+
result: undefined,
|
|
69
|
+
fallback: false,
|
|
70
|
+
evaluated,
|
|
71
|
+
undetermined: `arm "${rule.name}" could not be decided: ${undecided?.reason ?? "a check had no answer"}`,
|
|
72
|
+
};
|
|
73
|
+
}
|
|
54
74
|
if (ok) {
|
|
55
75
|
return {
|
|
56
76
|
matched: true,
|