@intutic/gate 2.9.0 → 3.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/actions.d.ts +39 -0
- package/dist/actions.d.ts.map +1 -1
- package/dist/actions.js +139 -0
- package/dist/actions.js.map +1 -1
- package/dist/client.d.ts +10 -0
- package/dist/client.d.ts.map +1 -1
- package/dist/client.js +19 -0
- package/dist/client.js.map +1 -1
- package/dist/decide.d.ts +269 -0
- package/dist/decide.d.ts.map +1 -0
- package/dist/decide.js +757 -0
- package/dist/decide.js.map +1 -0
- package/dist/decideApi.d.ts +24 -0
- package/dist/decideApi.d.ts.map +1 -0
- package/dist/decideApi.js +32 -0
- package/dist/decideApi.js.map +1 -0
- package/dist/decideEngine.d.ts +195 -0
- package/dist/decideEngine.d.ts.map +1 -0
- package/dist/decideEngine.js +707 -0
- package/dist/decideEngine.js.map +1 -0
- package/dist/decideEvaluate.d.ts +250 -0
- package/dist/decideEvaluate.d.ts.map +1 -0
- package/dist/decideEvaluate.js +504 -0
- package/dist/decideEvaluate.js.map +1 -0
- package/dist/decideHostGuard.d.ts +56 -0
- package/dist/decideHostGuard.d.ts.map +1 -0
- package/dist/decideHostGuard.js +194 -0
- package/dist/decideHostGuard.js.map +1 -0
- package/dist/decideLog.d.ts +287 -0
- package/dist/decideLog.d.ts.map +1 -0
- package/dist/decideLog.js +521 -0
- package/dist/decideLog.js.map +1 -0
- package/dist/decideLogRedaction.d.ts +17 -0
- package/dist/decideLogRedaction.d.ts.map +1 -0
- package/dist/decideLogRedaction.js +33 -0
- package/dist/decideLogRedaction.js.map +1 -0
- package/dist/decideRecord.d.ts +128 -0
- package/dist/decideRecord.d.ts.map +1 -0
- package/dist/decideRecord.js +254 -0
- package/dist/decideRecord.js.map +1 -0
- package/dist/decideSdk.d.ts +86 -0
- package/dist/decideSdk.d.ts.map +1 -0
- package/dist/decideSdk.js +146 -0
- package/dist/decideSdk.js.map +1 -0
- package/dist/dsh.d.ts.map +1 -1
- package/dist/dsh.js +6 -1
- package/dist/dsh.js.map +1 -1
- package/dist/errors.d.ts +26 -2
- package/dist/errors.d.ts.map +1 -1
- package/dist/errors.js +38 -1
- package/dist/errors.js.map +1 -1
- package/dist/gate.d.ts +85 -1
- package/dist/gate.d.ts.map +1 -1
- package/dist/gate.js +314 -18
- package/dist/gate.js.map +1 -1
- package/dist/gateRuleRecord.d.ts +244 -0
- package/dist/gateRuleRecord.d.ts.map +1 -0
- package/dist/gateRuleRecord.js +459 -0
- package/dist/gateRuleRecord.js.map +1 -0
- package/dist/harness.d.ts.map +1 -1
- package/dist/harness.js +22 -10
- package/dist/harness.js.map +1 -1
- package/dist/index.d.ts +14 -5
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +5 -3
- package/dist/index.js.map +1 -1
- package/dist/lanes.d.ts +94 -0
- package/dist/lanes.d.ts.map +1 -0
- package/dist/lanes.js +150 -0
- package/dist/lanes.js.map +1 -0
- package/dist/limits.d.ts +6 -0
- package/dist/limits.d.ts.map +1 -1
- package/dist/limits.js +6 -0
- package/dist/limits.js.map +1 -1
- package/dist/refusalDetail.d.ts +58 -0
- package/dist/refusalDetail.d.ts.map +1 -0
- package/dist/refusalDetail.js +121 -0
- package/dist/refusalDetail.js.map +1 -0
- package/dist/scopeMatch.d.ts +156 -0
- package/dist/scopeMatch.d.ts.map +1 -0
- package/dist/scopeMatch.js +438 -0
- package/dist/scopeMatch.js.map +1 -0
- package/dist/snapshot.d.ts +51 -3
- package/dist/snapshot.d.ts.map +1 -1
- package/dist/snapshot.js +86 -13
- package/dist/snapshot.js.map +1 -1
- package/dist/testOutcome.d.ts +164 -0
- package/dist/testOutcome.d.ts.map +1 -0
- package/dist/testOutcome.js +439 -0
- package/dist/testOutcome.js.map +1 -0
- package/dist/wrapTools.d.ts +7 -1
- package/dist/wrapTools.d.ts.map +1 -1
- package/dist/wrapTools.js +30 -2
- package/dist/wrapTools.js.map +1 -1
- package/package.json +2 -2
package/dist/decide.js
ADDED
|
@@ -0,0 +1,757 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Decide rules: the grammar, the validator and the semantics.
|
|
3
|
+
*
|
|
4
|
+
* A decide rule asks a self-hosted model a few narrow `yes_no` or `choice`
|
|
5
|
+
* questions about a tool call or a model response, turns each answer's
|
|
6
|
+
* probability into yes, no or unknown through a band, and combines the
|
|
7
|
+
* answers with the rule's `when` expression in Kleene three-valued logic:
|
|
8
|
+
* true refuses, false allows, unknown escalates. It is written as one SOP
|
|
9
|
+
* front-matter line, `decide: {json}`, one rule per line, the way a
|
|
10
|
+
* `hook_rule:` line is.
|
|
11
|
+
*
|
|
12
|
+
* This module is the pure part every layer shares:
|
|
13
|
+
*
|
|
14
|
+
* - {@link parseDecideLine} and {@link validateDecideRule}: the shape and the
|
|
15
|
+
* eight ordered checks, each failure with one fixed reason;
|
|
16
|
+
* - {@link parseWhen} and {@link canonicalWhen}: the `when` grammar and its
|
|
17
|
+
* fully parenthesised form, `!=` and `IN` rewritten;
|
|
18
|
+
* - {@link decideHashInput}: the canonical JSON the rule hash is taken over,
|
|
19
|
+
* which leaves out `id`, `title`, `escalate` and `rollout`;
|
|
20
|
+
* - {@link bandValue}, {@link choiceOrders}, {@link choiceAtoms}, the Kleene
|
|
21
|
+
* connectives, {@link decideRuleOutcome} and {@link decideCallOutcome}.
|
|
22
|
+
*
|
|
23
|
+
* Self-contained (no imports, no module state) so `@intutic/gate` can carry
|
|
24
|
+
* a byte-identical copy (`packages/gate-js/src/decide.ts`). The proxy's port
|
|
25
|
+
* is `packages/proxy/src/decide/mod.rs` and the Python gate's is
|
|
26
|
+
* `intutic_clawde/gate/decide.py`; all of them run
|
|
27
|
+
* `fixtures/decide-vectors.json` and must give the same answer, reason text
|
|
28
|
+
* included. The SHA-256 is passed in ({@link decideRuleHash}), because this
|
|
29
|
+
* file also runs where `node:crypto` does not.
|
|
30
|
+
*
|
|
31
|
+
* @module
|
|
32
|
+
*/
|
|
33
|
+
// ─── Limits ───────────────────────────────────────────────────────────
|
|
34
|
+
export const DECIDE_MAX_QUESTIONS = 8;
|
|
35
|
+
export const DECIDE_MAX_OPTIONS = 8;
|
|
36
|
+
export const DECIDE_MAX_RULES_PER_CALL = 4;
|
|
37
|
+
export const DECIDE_MAX_WHEN_CHARS = 256;
|
|
38
|
+
export const DECIDE_MIN_ASK_CHARS = 8;
|
|
39
|
+
export const DECIDE_MAX_ASK_CHARS = 300;
|
|
40
|
+
export const DECIDE_DEFAULT_BAND = { yes_at: 0.85, no_below: 0.15 };
|
|
41
|
+
/** Appended to every `choice` question's options when absent, and always asked last. */
|
|
42
|
+
export const NONE_OF_THESE = 'none_of_these';
|
|
43
|
+
// The same values as `TOOL_TOKEN_RE`, `MAX_HOOK_TOOLS` and `MAX_TITLE_CHARS`
|
|
44
|
+
// in `guardrailIr.ts`; a test holds them equal.
|
|
45
|
+
const TOOL_TOKEN = /^[A-Za-z][A-Za-z0-9_.:-]{0,63}$/;
|
|
46
|
+
const MAX_TOOLS = 8;
|
|
47
|
+
const MAX_TITLE = 80;
|
|
48
|
+
const RULE_ID = /^[a-z][a-z0-9_-]{0,47}$/;
|
|
49
|
+
const NAME = /^[a-z][a-z0-9_]{0,31}$/;
|
|
50
|
+
const KEYWORDS = ['AND', 'OR', 'NOT', 'IN'];
|
|
51
|
+
/**
|
|
52
|
+
* A question that asks about approval. Approval comes from the harness's ask
|
|
53
|
+
* or the review queue, never from a model reading text, so the validator
|
|
54
|
+
* refuses such a question (check 8). ASCII word boundaries and case folding,
|
|
55
|
+
* as JavaScript, Python's `re.ASCII` and Rust's `(?-u)` all read it.
|
|
56
|
+
*/
|
|
57
|
+
export const DECIDE_APPROVAL_RE = /\b(approv\w*|authori[sz]\w*|pre-?approv\w*|consent\w*|permi(t|ssion)\w*|sign(ed)?[- ]off)\b/i;
|
|
58
|
+
// ─── Shape ────────────────────────────────────────────────────────────
|
|
59
|
+
const RULE_FIELDS = ['id', 'title', 'on', 'tools', 'questions', 'when', 'band', 'escalate', 'rollout', 'context'];
|
|
60
|
+
const QUESTION_FIELDS = ['type', 'ask', 'options', 'band'];
|
|
61
|
+
const BAND_FIELDS = ['yes_at', 'no_below'];
|
|
62
|
+
function isObject(v) {
|
|
63
|
+
return v !== null && typeof v === 'object' && !Array.isArray(v);
|
|
64
|
+
}
|
|
65
|
+
/** An own, defined property: `in` would also see `Object.prototype`, and an `undefined` field is an absent one. */
|
|
66
|
+
function has(obj, key) {
|
|
67
|
+
return Object.prototype.hasOwnProperty.call(obj, key) && obj[key] !== undefined;
|
|
68
|
+
}
|
|
69
|
+
function own(obj, key) {
|
|
70
|
+
return obj && Object.prototype.hasOwnProperty.call(obj, key) ? obj[key] : undefined;
|
|
71
|
+
}
|
|
72
|
+
/** Length in code points, as Python's `len` and Rust's `chars().count()` measure it. */
|
|
73
|
+
function codePoints(s) {
|
|
74
|
+
return Array.from(s).length;
|
|
75
|
+
}
|
|
76
|
+
function unknownField(obj, allowed) {
|
|
77
|
+
return Object.keys(obj)
|
|
78
|
+
.filter((k) => !allowed.includes(k) && obj[k] !== undefined)
|
|
79
|
+
.sort()[0];
|
|
80
|
+
}
|
|
81
|
+
function bandShape(v, path) {
|
|
82
|
+
if (!isObject(v))
|
|
83
|
+
return `${path} must be an object`;
|
|
84
|
+
const extra = unknownField(v, BAND_FIELDS);
|
|
85
|
+
if (extra !== undefined)
|
|
86
|
+
return `${path}: unknown field ${extra}`;
|
|
87
|
+
if (!has(v, 'yes_at'))
|
|
88
|
+
return `${path}.yes_at is required`;
|
|
89
|
+
if (typeof v.yes_at !== 'number' || !(v.yes_at > 0.5 && v.yes_at < 1))
|
|
90
|
+
return `${path}.yes_at must be above 0.5 and below 1`;
|
|
91
|
+
if (!has(v, 'no_below'))
|
|
92
|
+
return `${path}.no_below is required`;
|
|
93
|
+
if (typeof v.no_below !== 'number' || !(v.no_below > 0))
|
|
94
|
+
return `${path}.no_below must be above 0`;
|
|
95
|
+
return null;
|
|
96
|
+
}
|
|
97
|
+
function questionShape(name, v) {
|
|
98
|
+
const path = `questions.${name}`;
|
|
99
|
+
if (!isObject(v))
|
|
100
|
+
return `${path} must be an object`;
|
|
101
|
+
const extra = unknownField(v, QUESTION_FIELDS);
|
|
102
|
+
if (extra !== undefined)
|
|
103
|
+
return `${path}: unknown field ${extra}`;
|
|
104
|
+
if (!has(v, 'type'))
|
|
105
|
+
return `${path}.type is required`;
|
|
106
|
+
if (v.type !== 'yes_no' && v.type !== 'choice')
|
|
107
|
+
return `${path}.type must be yes_no or choice`;
|
|
108
|
+
if (!has(v, 'ask'))
|
|
109
|
+
return `${path}.ask is required`;
|
|
110
|
+
const ask = v.ask;
|
|
111
|
+
if (typeof ask !== 'string' || codePoints(ask) < DECIDE_MIN_ASK_CHARS || codePoints(ask) > DECIDE_MAX_ASK_CHARS || /[\t\r\n]/.test(ask)) {
|
|
112
|
+
return `${path}.ask must be ${DECIDE_MIN_ASK_CHARS} to ${DECIDE_MAX_ASK_CHARS} characters with no tab or newline`;
|
|
113
|
+
}
|
|
114
|
+
if (v.type === 'yes_no') {
|
|
115
|
+
if (has(v, 'options'))
|
|
116
|
+
return `${path}.options applies to choice questions only`;
|
|
117
|
+
}
|
|
118
|
+
else {
|
|
119
|
+
if (!has(v, 'options'))
|
|
120
|
+
return `${path}.options is required for a choice question`;
|
|
121
|
+
const options = v.options;
|
|
122
|
+
if (!Array.isArray(options))
|
|
123
|
+
return `${path}.options must be a list`;
|
|
124
|
+
if (options.length < 2 || options.length > DECIDE_MAX_OPTIONS)
|
|
125
|
+
return `${path}.options lists 2 to ${DECIDE_MAX_OPTIONS} options`;
|
|
126
|
+
for (let i = 0; i < options.length; i++) {
|
|
127
|
+
const o = options[i];
|
|
128
|
+
if (typeof o !== 'string' || !NAME.test(o))
|
|
129
|
+
return `${path}.options[${i}] is not an option name`;
|
|
130
|
+
if (options.indexOf(o) !== i)
|
|
131
|
+
return `${path}.options[${i}] repeats ${o}`;
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
if (has(v, 'band'))
|
|
135
|
+
return bandShape(v.band, `${path}.band`);
|
|
136
|
+
return null;
|
|
137
|
+
}
|
|
138
|
+
function ruleShape(v) {
|
|
139
|
+
if (!isObject(v))
|
|
140
|
+
return 'a rule is a JSON object';
|
|
141
|
+
const extra = unknownField(v, RULE_FIELDS);
|
|
142
|
+
if (extra !== undefined)
|
|
143
|
+
return `unknown field ${extra}`;
|
|
144
|
+
if (!has(v, 'id'))
|
|
145
|
+
return 'id is required';
|
|
146
|
+
if (typeof v.id !== 'string' || !RULE_ID.test(v.id))
|
|
147
|
+
return 'id must be a lower-case letter, then up to 47 of a-z, 0-9, _ or -';
|
|
148
|
+
if (has(v, 'title')) {
|
|
149
|
+
const t = v.title;
|
|
150
|
+
if (typeof t !== 'string' || codePoints(t) < 1 || codePoints(t) > MAX_TITLE || /[\t\r\n]/.test(t)) {
|
|
151
|
+
return `title must be 1 to ${MAX_TITLE} characters with no tab or newline`;
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
if (!has(v, 'on'))
|
|
155
|
+
return 'on is required';
|
|
156
|
+
if (v.on !== 'tool_call' && v.on !== 'response')
|
|
157
|
+
return 'on must be tool_call or response';
|
|
158
|
+
if (has(v, 'tools')) {
|
|
159
|
+
const tools = v.tools;
|
|
160
|
+
if (!Array.isArray(tools))
|
|
161
|
+
return 'tools must be a list';
|
|
162
|
+
if (tools.length < 1 || tools.length > MAX_TOOLS)
|
|
163
|
+
return `tools names 1 to ${MAX_TOOLS} tools`;
|
|
164
|
+
for (let i = 0; i < tools.length; i++) {
|
|
165
|
+
const t = tools[i];
|
|
166
|
+
if (typeof t !== 'string' || !TOOL_TOKEN.test(t))
|
|
167
|
+
return `tools[${i}] is not a tool name`;
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
if (!has(v, 'questions'))
|
|
171
|
+
return 'questions is required';
|
|
172
|
+
const questions = v.questions;
|
|
173
|
+
if (!isObject(questions))
|
|
174
|
+
return 'questions must be an object';
|
|
175
|
+
for (const name of Object.keys(questions).sort()) {
|
|
176
|
+
if (!NAME.test(name))
|
|
177
|
+
return `questions: ${name} is not a question name`;
|
|
178
|
+
if (KEYWORDS.includes(name.toUpperCase()))
|
|
179
|
+
return `questions: ${name} is a when keyword`;
|
|
180
|
+
const why = questionShape(name, questions[name]);
|
|
181
|
+
if (why)
|
|
182
|
+
return why;
|
|
183
|
+
}
|
|
184
|
+
if (!has(v, 'when'))
|
|
185
|
+
return 'when is required';
|
|
186
|
+
if (typeof v.when !== 'string')
|
|
187
|
+
return 'when must be a string';
|
|
188
|
+
if (codePoints(v.when) < 1 || codePoints(v.when) > DECIDE_MAX_WHEN_CHARS)
|
|
189
|
+
return `when must be 1 to ${DECIDE_MAX_WHEN_CHARS} characters`;
|
|
190
|
+
if (has(v, 'band')) {
|
|
191
|
+
const why = bandShape(v.band, 'band');
|
|
192
|
+
if (why)
|
|
193
|
+
return why;
|
|
194
|
+
}
|
|
195
|
+
if (has(v, 'escalate') && v.escalate !== 'ask' && v.escalate !== 'review' && v.escalate !== 'refuse')
|
|
196
|
+
return 'escalate must be ask, review or refuse';
|
|
197
|
+
if (has(v, 'rollout') && v.rollout !== 'shadow' && v.rollout !== 'enforce')
|
|
198
|
+
return 'rollout must be shadow or enforce';
|
|
199
|
+
if (has(v, 'context')) {
|
|
200
|
+
const c = v.context;
|
|
201
|
+
if (!Array.isArray(c))
|
|
202
|
+
return 'context must be a list';
|
|
203
|
+
if (c.length > 1 || (c.length === 1 && c[0] !== 'last_user_turn'))
|
|
204
|
+
return 'context may list last_user_turn, once';
|
|
205
|
+
}
|
|
206
|
+
return null;
|
|
207
|
+
}
|
|
208
|
+
/** Thrown inside the parser only; {@link parseWhen} turns it into a result. */
|
|
209
|
+
class WhenError {
|
|
210
|
+
message;
|
|
211
|
+
column;
|
|
212
|
+
constructor(message, column) {
|
|
213
|
+
this.message = message;
|
|
214
|
+
this.column = column;
|
|
215
|
+
}
|
|
216
|
+
}
|
|
217
|
+
function isWordChar(c) {
|
|
218
|
+
return (c >= 'a' && c <= 'z') || (c >= 'A' && c <= 'Z') || (c >= '0' && c <= '9') || c === '_';
|
|
219
|
+
}
|
|
220
|
+
function tokenize(src) {
|
|
221
|
+
const cs = Array.from(src);
|
|
222
|
+
const out = [];
|
|
223
|
+
let i = 0;
|
|
224
|
+
while (i < cs.length) {
|
|
225
|
+
const c = cs[i];
|
|
226
|
+
const col = i + 1;
|
|
227
|
+
if (c === ' ' || c === '\t' || c === '\n' || c === '\r') {
|
|
228
|
+
i++;
|
|
229
|
+
}
|
|
230
|
+
else if (c === '(' || c === ')' || c === ',') {
|
|
231
|
+
out.push({ t: c, v: c, col });
|
|
232
|
+
i++;
|
|
233
|
+
}
|
|
234
|
+
else if ((c === '=' || c === '!') && cs[i + 1] === '=') {
|
|
235
|
+
out.push({ t: c === '=' ? '==' : '!=', v: `${c}=`, col });
|
|
236
|
+
i += 2;
|
|
237
|
+
}
|
|
238
|
+
else if (isWordChar(c)) {
|
|
239
|
+
let j = i;
|
|
240
|
+
while (j < cs.length && isWordChar(cs[j]))
|
|
241
|
+
j++;
|
|
242
|
+
const w = cs.slice(i, j).join('');
|
|
243
|
+
if (KEYWORDS.includes(w))
|
|
244
|
+
out.push({ t: 'kw', v: w, col });
|
|
245
|
+
else if (/^[a-z][a-z0-9_]*$/.test(w))
|
|
246
|
+
out.push({ t: 'name', v: w, col });
|
|
247
|
+
else
|
|
248
|
+
throw new WhenError(`unknown word ${w}`, col);
|
|
249
|
+
i = j;
|
|
250
|
+
}
|
|
251
|
+
else {
|
|
252
|
+
throw new WhenError(`unexpected character ${c}`, col);
|
|
253
|
+
}
|
|
254
|
+
}
|
|
255
|
+
out.push({ t: 'end', v: '', col: cs.length + 1 });
|
|
256
|
+
return out;
|
|
257
|
+
}
|
|
258
|
+
/** Parse a `when` expression (§3.3 of the decide LLD). Never throws. Names are not checked here; {@link validateDecideRule} does that. */
|
|
259
|
+
export function parseWhen(src) {
|
|
260
|
+
try {
|
|
261
|
+
const toks = tokenize(src);
|
|
262
|
+
let k = 0;
|
|
263
|
+
const peek = () => toks[k];
|
|
264
|
+
const next = () => toks[k++];
|
|
265
|
+
const isKw = (w) => peek().t === 'kw' && peek().v === w;
|
|
266
|
+
const option = () => {
|
|
267
|
+
const t = next();
|
|
268
|
+
if (t.t !== 'name')
|
|
269
|
+
throw new WhenError('expected an option', t.col);
|
|
270
|
+
return t.v;
|
|
271
|
+
};
|
|
272
|
+
const parseOr = () => {
|
|
273
|
+
let a = parseAnd();
|
|
274
|
+
while (isKw('OR')) {
|
|
275
|
+
next();
|
|
276
|
+
a = { op: 'or', a, b: parseAnd() };
|
|
277
|
+
}
|
|
278
|
+
return a;
|
|
279
|
+
};
|
|
280
|
+
const parseAnd = () => {
|
|
281
|
+
let a = parseUnary();
|
|
282
|
+
while (isKw('AND')) {
|
|
283
|
+
next();
|
|
284
|
+
a = { op: 'and', a, b: parseUnary() };
|
|
285
|
+
}
|
|
286
|
+
return a;
|
|
287
|
+
};
|
|
288
|
+
const parseUnary = () => {
|
|
289
|
+
if (isKw('NOT')) {
|
|
290
|
+
next();
|
|
291
|
+
return { op: 'not', a: parseUnary() };
|
|
292
|
+
}
|
|
293
|
+
return parseAtom();
|
|
294
|
+
};
|
|
295
|
+
const parseAtom = () => {
|
|
296
|
+
const t = next();
|
|
297
|
+
if (t.t === '(') {
|
|
298
|
+
const e = parseOr();
|
|
299
|
+
const close = next();
|
|
300
|
+
if (close.t !== ')')
|
|
301
|
+
throw new WhenError('expected )', close.col);
|
|
302
|
+
return e;
|
|
303
|
+
}
|
|
304
|
+
if (t.t !== 'name')
|
|
305
|
+
throw new WhenError('expected a question name, NOT or (', t.col);
|
|
306
|
+
const q = t.v;
|
|
307
|
+
if (peek().t === '==') {
|
|
308
|
+
next();
|
|
309
|
+
return { op: 'eq', q, o: option() };
|
|
310
|
+
}
|
|
311
|
+
if (peek().t === '!=') {
|
|
312
|
+
next();
|
|
313
|
+
return { op: 'not', a: { op: 'eq', q, o: option() } };
|
|
314
|
+
}
|
|
315
|
+
if (isKw('IN')) {
|
|
316
|
+
next();
|
|
317
|
+
const open = next();
|
|
318
|
+
if (open.t !== '(')
|
|
319
|
+
throw new WhenError('expected (', open.col);
|
|
320
|
+
let e = { op: 'eq', q, o: option() };
|
|
321
|
+
for (;;) {
|
|
322
|
+
const sep = next();
|
|
323
|
+
if (sep.t === ')')
|
|
324
|
+
return e;
|
|
325
|
+
if (sep.t !== ',')
|
|
326
|
+
throw new WhenError('expected , or )', sep.col);
|
|
327
|
+
e = { op: 'or', a: e, b: { op: 'eq', q, o: option() } };
|
|
328
|
+
}
|
|
329
|
+
}
|
|
330
|
+
return { op: 'yes', q };
|
|
331
|
+
};
|
|
332
|
+
const expr = parseOr();
|
|
333
|
+
if (peek().t !== 'end')
|
|
334
|
+
throw new WhenError('expected AND, OR or the end', peek().col);
|
|
335
|
+
return { ok: true, expr };
|
|
336
|
+
}
|
|
337
|
+
catch (err) {
|
|
338
|
+
if (err instanceof WhenError)
|
|
339
|
+
return { ok: false, message: err.message, column: err.column };
|
|
340
|
+
throw err;
|
|
341
|
+
}
|
|
342
|
+
}
|
|
343
|
+
/** The canonical form: fully parenthesised, `!=` and `IN` rewritten. `a AND b != o` → `(a AND (NOT (b == o)))`. */
|
|
344
|
+
export function canonicalWhen(e) {
|
|
345
|
+
switch (e.op) {
|
|
346
|
+
case 'yes':
|
|
347
|
+
return e.q;
|
|
348
|
+
case 'eq':
|
|
349
|
+
return `(${e.q} == ${e.o})`;
|
|
350
|
+
case 'not':
|
|
351
|
+
return `(NOT ${canonicalWhen(e.a)})`;
|
|
352
|
+
case 'and':
|
|
353
|
+
return `(${canonicalWhen(e.a)} AND ${canonicalWhen(e.b)})`;
|
|
354
|
+
case 'or':
|
|
355
|
+
return `(${canonicalWhen(e.a)} OR ${canonicalWhen(e.b)})`;
|
|
356
|
+
}
|
|
357
|
+
}
|
|
358
|
+
/** Every atom, left to right as written. */
|
|
359
|
+
function atomsOf(e, out = []) {
|
|
360
|
+
if (e.op === 'yes' || e.op === 'eq')
|
|
361
|
+
out.push(e);
|
|
362
|
+
else if (e.op === 'not')
|
|
363
|
+
atomsOf(e.a, out);
|
|
364
|
+
else {
|
|
365
|
+
atomsOf(e.a, out);
|
|
366
|
+
atomsOf(e.b, out);
|
|
367
|
+
}
|
|
368
|
+
return out;
|
|
369
|
+
}
|
|
370
|
+
// ─── Validation ───────────────────────────────────────────────────────
|
|
371
|
+
/** A `choice` question's options as asked: `none_of_these` appended when absent and moved last when present. */
|
|
372
|
+
export function decideOptions(q) {
|
|
373
|
+
return [...(q.options ?? []).filter((o) => o !== NONE_OF_THESE), NONE_OF_THESE];
|
|
374
|
+
}
|
|
375
|
+
/** The band a question is judged on: its own, else the rule's, else the default. */
|
|
376
|
+
export function decideQuestionBand(rule, name) {
|
|
377
|
+
return own(rule.questions, name)?.band ?? rule.band ?? DECIDE_DEFAULT_BAND;
|
|
378
|
+
}
|
|
379
|
+
/**
|
|
380
|
+
* Validate an untrusted value as a decide rule (the line's JSON, without
|
|
381
|
+
* `kind`). Never throws. The shape comes first, then the eight checks in
|
|
382
|
+
* their fixed order; the first failure is the reason. Questions are visited
|
|
383
|
+
* in sorted name order and `when` atoms left to right, so every port reports
|
|
384
|
+
* the same first failure.
|
|
385
|
+
*/
|
|
386
|
+
export function validateDecideRule(value) {
|
|
387
|
+
const shape = ruleShape(value);
|
|
388
|
+
if (shape)
|
|
389
|
+
return { ok: false, reason: `decide: ${shape}` };
|
|
390
|
+
const rule = value;
|
|
391
|
+
const names = Object.keys(rule.questions).sort();
|
|
392
|
+
if ((rule.on === 'tool_call') !== (rule.tools !== undefined)) {
|
|
393
|
+
return { ok: false, reason: 'decide: tools is required for on: tool_call and not allowed for on: response' };
|
|
394
|
+
}
|
|
395
|
+
if (rule.on !== 'tool_call' && rule.context !== undefined)
|
|
396
|
+
return { ok: false, reason: 'decide: context applies to on: tool_call only' };
|
|
397
|
+
if (names.length < 1 || names.length > DECIDE_MAX_QUESTIONS)
|
|
398
|
+
return { ok: false, reason: `decide: a rule asks 1 to ${DECIDE_MAX_QUESTIONS} questions` };
|
|
399
|
+
if (rule.band && !(rule.band.no_below < rule.band.yes_at))
|
|
400
|
+
return { ok: false, reason: 'decide: band: no_below must be below yes_at' };
|
|
401
|
+
for (const n of names) {
|
|
402
|
+
const b = rule.questions[n].band;
|
|
403
|
+
if (b && !(b.no_below < b.yes_at))
|
|
404
|
+
return { ok: false, reason: `decide: ${n}: no_below must be below yes_at` };
|
|
405
|
+
}
|
|
406
|
+
const parsed = parseWhen(rule.when);
|
|
407
|
+
if (!parsed.ok)
|
|
408
|
+
return { ok: false, reason: `decide: when: ${parsed.message} at column ${parsed.column}` };
|
|
409
|
+
const used = new Set();
|
|
410
|
+
for (const atom of atomsOf(parsed.expr)) {
|
|
411
|
+
const q = own(rule.questions, atom.q);
|
|
412
|
+
if (!q)
|
|
413
|
+
return { ok: false, reason: `decide: when: unknown question ${atom.q}` };
|
|
414
|
+
if (atom.op === 'yes' && q.type !== 'yes_no')
|
|
415
|
+
return { ok: false, reason: `decide: when: ${atom.q} is not a yes_no question` };
|
|
416
|
+
if (atom.op === 'eq') {
|
|
417
|
+
if (q.type !== 'choice')
|
|
418
|
+
return { ok: false, reason: `decide: when: ${atom.q} is not a choice question` };
|
|
419
|
+
if (!decideOptions(q).includes(atom.o))
|
|
420
|
+
return { ok: false, reason: `decide: when: unknown option ${atom.o} for ${atom.q}` };
|
|
421
|
+
}
|
|
422
|
+
used.add(atom.q);
|
|
423
|
+
}
|
|
424
|
+
for (const n of names) {
|
|
425
|
+
if (!used.has(n))
|
|
426
|
+
return { ok: false, reason: `decide: question ${n} is never used in when` };
|
|
427
|
+
}
|
|
428
|
+
for (const n of names) {
|
|
429
|
+
if (DECIDE_APPROVAL_RE.test(rule.questions[n].ask)) {
|
|
430
|
+
return { ok: false, reason: `decide: ${n} asks about approval; approval comes from the harness ask or the review queue, never from a question` };
|
|
431
|
+
}
|
|
432
|
+
}
|
|
433
|
+
return { ok: true, rule, expr: parsed.expr };
|
|
434
|
+
}
|
|
435
|
+
/** The value of one `decide:` line (the text after the key). */
|
|
436
|
+
export function parseDecideLine(raw) {
|
|
437
|
+
const text = raw.trim();
|
|
438
|
+
if (!text)
|
|
439
|
+
return { kind: 'unreadable', id: null, error: 'decide: the line is empty; write one rule per line as `decide: {json}`' };
|
|
440
|
+
let value;
|
|
441
|
+
try {
|
|
442
|
+
value = JSON.parse(text);
|
|
443
|
+
}
|
|
444
|
+
catch {
|
|
445
|
+
return { kind: 'unreadable', id: null, error: 'decide: the value is not JSON; write one rule per line as `decide: {json}`' };
|
|
446
|
+
}
|
|
447
|
+
const id = isObject(value) && has(value, 'id') && typeof value.id === 'string' && value.id !== '' ? value.id : null;
|
|
448
|
+
const v = validateDecideRule(value);
|
|
449
|
+
return v.ok ? { kind: 'rule', rule: v.rule, expr: v.expr } : { kind: 'unreadable', id, error: v.reason };
|
|
450
|
+
}
|
|
451
|
+
/** Every `decide:` line of one SOP, in order. A line reusing an earlier line's id is unreadable: ids are unique within a SOP. */
|
|
452
|
+
export function parseDecideLines(values) {
|
|
453
|
+
const seen = new Set();
|
|
454
|
+
return values.map((raw) => {
|
|
455
|
+
const d = parseDecideLine(raw);
|
|
456
|
+
const id = d.kind === 'rule' ? d.rule.id : d.id;
|
|
457
|
+
if (id === null)
|
|
458
|
+
return d;
|
|
459
|
+
if (seen.has(id))
|
|
460
|
+
return { kind: 'unreadable', id, error: `decide: id ${id} is used twice in this SOP` };
|
|
461
|
+
seen.add(id);
|
|
462
|
+
return d;
|
|
463
|
+
});
|
|
464
|
+
}
|
|
465
|
+
// ─── Identity ─────────────────────────────────────────────────────────
|
|
466
|
+
/** JSON with keys sorted at every level and no whitespace. Numbers as `JSON.stringify` writes them, which the other ports reproduce. */
|
|
467
|
+
function canonicalJson(v) {
|
|
468
|
+
if (Array.isArray(v))
|
|
469
|
+
return `[${v.map(canonicalJson).join(',')}]`;
|
|
470
|
+
if (isObject(v)) {
|
|
471
|
+
const keys = Object.keys(v).sort();
|
|
472
|
+
return `{${keys.map((k) => `${JSON.stringify(k)}:${canonicalJson(v[k])}`).join(',')}}`;
|
|
473
|
+
}
|
|
474
|
+
return JSON.stringify(v);
|
|
475
|
+
}
|
|
476
|
+
function uniqueSorted(xs) {
|
|
477
|
+
return [...new Set(xs)].sort();
|
|
478
|
+
}
|
|
479
|
+
/**
|
|
480
|
+
* What the rule hash covers: `on`, `tools` (sorted), the questions with
|
|
481
|
+
* their bands made explicit and `none_of_these` placed, `when` in canonical
|
|
482
|
+
* form and `context` (sorted), as canonical JSON. `id`, `title`, `escalate`
|
|
483
|
+
* and `rollout` are left out, so flipping shadow to enforce keeps the rule's
|
|
484
|
+
* evidence, while a changed word, option or band starts it over.
|
|
485
|
+
* `tools` and `context` are `[]` when absent.
|
|
486
|
+
*/
|
|
487
|
+
export function decideHashInput(rule) {
|
|
488
|
+
const parsed = parseWhen(rule.when);
|
|
489
|
+
if (!parsed.ok)
|
|
490
|
+
throw new Error(`decideHashInput: when does not parse (${parsed.message})`);
|
|
491
|
+
const questions = {};
|
|
492
|
+
for (const name of Object.keys(rule.questions)) {
|
|
493
|
+
const q = rule.questions[name];
|
|
494
|
+
const b = decideQuestionBand(rule, name);
|
|
495
|
+
questions[name] = {
|
|
496
|
+
type: q.type,
|
|
497
|
+
ask: q.ask,
|
|
498
|
+
...(q.type === 'choice' ? { options: decideOptions(q) } : {}),
|
|
499
|
+
band: { yes_at: b.yes_at, no_below: b.no_below },
|
|
500
|
+
};
|
|
501
|
+
}
|
|
502
|
+
return canonicalJson({
|
|
503
|
+
on: rule.on,
|
|
504
|
+
tools: uniqueSorted(rule.tools ?? []),
|
|
505
|
+
questions,
|
|
506
|
+
when: canonicalWhen(parsed.expr),
|
|
507
|
+
context: uniqueSorted(rule.context ?? []),
|
|
508
|
+
});
|
|
509
|
+
}
|
|
510
|
+
/** `ruleHash`: lower-case hex SHA-256 of {@link decideHashInput}. `sha256Hex` hashes a string's UTF-8 bytes. */
|
|
511
|
+
export function decideRuleHash(rule, sha256Hex) {
|
|
512
|
+
return sha256Hex(decideHashInput(rule));
|
|
513
|
+
}
|
|
514
|
+
export function kAnd(a, b) {
|
|
515
|
+
return a === 'F' || b === 'F' ? 'F' : a === 'T' && b === 'T' ? 'T' : 'U';
|
|
516
|
+
}
|
|
517
|
+
export function kOr(a, b) {
|
|
518
|
+
return a === 'T' || b === 'T' ? 'T' : a === 'F' && b === 'F' ? 'F' : 'U';
|
|
519
|
+
}
|
|
520
|
+
export function kNot(a) {
|
|
521
|
+
return a === 'T' ? 'F' : a === 'F' ? 'T' : 'U';
|
|
522
|
+
}
|
|
523
|
+
/** A probability through a band: T at or above `yes_at`, F below `no_below`, U between. */
|
|
524
|
+
export function bandValue(p, band) {
|
|
525
|
+
if (p >= band.yes_at)
|
|
526
|
+
return 'T';
|
|
527
|
+
if (p < band.no_below)
|
|
528
|
+
return 'F';
|
|
529
|
+
return 'U';
|
|
530
|
+
}
|
|
531
|
+
/**
|
|
532
|
+
* The orders a `choice` question is asked in, as index lists over its n
|
|
533
|
+
* options: as written and reversed, plus, past three options, the list
|
|
534
|
+
* rotated by half, because reversing leaves the middle in the middle. The
|
|
535
|
+
* order rule of `attributeSops` in the judge cascade.
|
|
536
|
+
*/
|
|
537
|
+
export function choiceOrders(n) {
|
|
538
|
+
const ids = Array.from({ length: n }, (_, i) => i);
|
|
539
|
+
const half = Math.floor(n / 2);
|
|
540
|
+
return [ids, [...ids].reverse(), ...(n > 3 ? [[...ids.slice(half), ...ids.slice(0, half)]] : [])];
|
|
541
|
+
}
|
|
542
|
+
/**
|
|
543
|
+
* Each option's atom `q == o` from the per-order probabilities (option name
|
|
544
|
+
* to probability, one map per order; an option a map leaves out is 0). The
|
|
545
|
+
* mean goes through the band; when the orders disagree by more than the
|
|
546
|
+
* band's width the atom is U whatever the mean. No orders at all is U.
|
|
547
|
+
*/
|
|
548
|
+
export function choiceAtoms(options, perOrder, band) {
|
|
549
|
+
const out = {};
|
|
550
|
+
for (const o of options) {
|
|
551
|
+
const ps = perOrder.map((m) => own(m, o) ?? 0);
|
|
552
|
+
if (ps.length === 0) {
|
|
553
|
+
out[o] = { p: 0, spread: 0, value: 'U' };
|
|
554
|
+
continue;
|
|
555
|
+
}
|
|
556
|
+
let sum = 0;
|
|
557
|
+
for (const p of ps)
|
|
558
|
+
sum += p;
|
|
559
|
+
const p = sum / ps.length;
|
|
560
|
+
const spread = Math.max(...ps) - Math.min(...ps);
|
|
561
|
+
out[o] = { p, spread, value: spread > band.yes_at - band.no_below ? 'U' : bandValue(p, band) };
|
|
562
|
+
}
|
|
563
|
+
return out;
|
|
564
|
+
}
|
|
565
|
+
export function evalWhen(e, atoms) {
|
|
566
|
+
switch (e.op) {
|
|
567
|
+
case 'yes':
|
|
568
|
+
return own(atoms.yes, e.q) ?? 'U';
|
|
569
|
+
case 'eq':
|
|
570
|
+
return own(own(atoms.eq, e.q), e.o) ?? 'U';
|
|
571
|
+
case 'not':
|
|
572
|
+
return kNot(evalWhen(e.a, atoms));
|
|
573
|
+
case 'and':
|
|
574
|
+
return kAnd(evalWhen(e.a, atoms), evalWhen(e.b, atoms));
|
|
575
|
+
case 'or':
|
|
576
|
+
return kOr(evalWhen(e.a, atoms), evalWhen(e.b, atoms));
|
|
577
|
+
}
|
|
578
|
+
}
|
|
579
|
+
function logit(p) {
|
|
580
|
+
const q = Math.min(Math.max(p, 1e-12), 1 - 1e-12);
|
|
581
|
+
return Math.log(q / (1 - q));
|
|
582
|
+
}
|
|
583
|
+
/**
|
|
584
|
+
* s = σ(bias + Σ w_q · logit(p_q)), summed in sorted question order. A
|
|
585
|
+
* `choice` question enters through the averaged probability of its top
|
|
586
|
+
* option. Null when a weighted
|
|
587
|
+
* question has no answer, which the caller reads as U.
|
|
588
|
+
*/
|
|
589
|
+
export function combinerScore(rule, answers, combiner) {
|
|
590
|
+
let x = combiner.bias;
|
|
591
|
+
for (const name of Object.keys(combiner.weights).sort()) {
|
|
592
|
+
const q = own(rule.questions, name);
|
|
593
|
+
const a = own(answers, name);
|
|
594
|
+
if (!q || !a)
|
|
595
|
+
return null;
|
|
596
|
+
let p;
|
|
597
|
+
if (q.type === 'yes_no') {
|
|
598
|
+
if (typeof a.p !== 'number')
|
|
599
|
+
return null;
|
|
600
|
+
p = a.p;
|
|
601
|
+
}
|
|
602
|
+
else {
|
|
603
|
+
if (!a.perOrder || a.perOrder.length === 0)
|
|
604
|
+
return null;
|
|
605
|
+
const atoms = choiceAtoms(decideOptions(q), a.perOrder, decideQuestionBand(rule, name));
|
|
606
|
+
p = Math.max(...Object.values(atoms).map((c) => c.p));
|
|
607
|
+
}
|
|
608
|
+
x += combiner.weights[name] * logit(p);
|
|
609
|
+
}
|
|
610
|
+
return 1 / (1 + Math.exp(-x));
|
|
611
|
+
}
|
|
612
|
+
/** The atoms of a validated rule's answers, each question through its own band. */
|
|
613
|
+
export function decideAtoms(rule, answers) {
|
|
614
|
+
const atoms = { yes: {}, eq: {} };
|
|
615
|
+
for (const name of Object.keys(rule.questions).sort()) {
|
|
616
|
+
const q = rule.questions[name];
|
|
617
|
+
const a = own(answers, name);
|
|
618
|
+
const band = decideQuestionBand(rule, name);
|
|
619
|
+
if (q.type === 'yes_no') {
|
|
620
|
+
atoms.yes[name] = a && typeof a.p === 'number' ? bandValue(a.p, band) : 'U';
|
|
621
|
+
}
|
|
622
|
+
else {
|
|
623
|
+
const c = choiceAtoms(decideOptions(q), a?.perOrder ?? [], band);
|
|
624
|
+
atoms.eq[name] = Object.fromEntries(Object.entries(c).map(([o, v]) => [o, v.value]));
|
|
625
|
+
}
|
|
626
|
+
}
|
|
627
|
+
return atoms;
|
|
628
|
+
}
|
|
629
|
+
/**
|
|
630
|
+
* One rule's outcome: T refuses (`DECIDE_REFUSED`), F allows, U escalates
|
|
631
|
+
* through `escalate` (default `ask`); `escalate: refuse` refuses with
|
|
632
|
+
* `DECIDE_ESCALATED`. `expr` is the rule's parsed `when`
|
|
633
|
+
* ({@link validateDecideRule} returns it).
|
|
634
|
+
*/
|
|
635
|
+
export function decideRuleOutcome(rule, expr, answers, combiner) {
|
|
636
|
+
const atoms = decideAtoms(rule, answers);
|
|
637
|
+
let value;
|
|
638
|
+
let score = null;
|
|
639
|
+
if (combiner) {
|
|
640
|
+
score = combinerScore(rule, answers, combiner);
|
|
641
|
+
value = score === null ? 'U' : bandValue(score, combiner.band);
|
|
642
|
+
}
|
|
643
|
+
else {
|
|
644
|
+
value = evalWhen(expr, atoms);
|
|
645
|
+
}
|
|
646
|
+
if (value === 'T')
|
|
647
|
+
return { value, outcome: 'refuse', code: 'DECIDE_REFUSED', via: null, score, atoms };
|
|
648
|
+
if (value === 'F')
|
|
649
|
+
return { value, outcome: 'allow', code: null, via: null, score, atoms };
|
|
650
|
+
const escalate = rule.escalate ?? 'ask';
|
|
651
|
+
if (escalate === 'refuse')
|
|
652
|
+
return { value, outcome: 'refuse', code: 'DECIDE_ESCALATED', via: null, score, atoms };
|
|
653
|
+
return { value, outcome: 'escalate', code: 'DECIDE_ESCALATED', via: escalate, score, atoms };
|
|
654
|
+
}
|
|
655
|
+
/**
|
|
656
|
+
* The rules a tool call asks. `candidates` are in snapshot order (SOP order,
|
|
657
|
+
* then line order). A rule matches when it is `on: tool_call` and names the
|
|
658
|
+
* tool exactly.
|
|
659
|
+
*/
|
|
660
|
+
export function selectDecideRules(candidates, tool) {
|
|
661
|
+
const matched = candidates.filter((c) => c.on === 'tool_call' && (c.tools ?? []).includes(tool));
|
|
662
|
+
const ordered = [...matched.filter((c) => c.effective === 'enforce'), ...matched.filter((c) => c.effective !== 'enforce')];
|
|
663
|
+
const beyond = ordered.slice(DECIDE_MAX_RULES_PER_CALL);
|
|
664
|
+
return {
|
|
665
|
+
evaluate: ordered.slice(0, DECIDE_MAX_RULES_PER_CALL),
|
|
666
|
+
skipped: beyond.filter((c) => c.effective !== 'enforce'),
|
|
667
|
+
unavailable: beyond.some((c) => c.effective === 'enforce') ? `more than ${DECIDE_MAX_RULES_PER_CALL} decide rules name ${tool}` : null,
|
|
668
|
+
};
|
|
669
|
+
}
|
|
670
|
+
/**
|
|
671
|
+
* The call's outcome from its rules' results, in evaluation order. Only
|
|
672
|
+
* enforce rules count: the first that refuses (or whose engine was
|
|
673
|
+
* unavailable, `GOVERNANCE_UNAVAILABLE`) refuses the call; otherwise the
|
|
674
|
+
* first that escalates escalates it; otherwise the call is allowed. Shadow
|
|
675
|
+
* results never change the call.
|
|
676
|
+
*/
|
|
677
|
+
export function decideCallOutcome(selection, results) {
|
|
678
|
+
if (selection.unavailable)
|
|
679
|
+
return { outcome: 'refuse', code: 'GOVERNANCE_UNAVAILABLE', ruleRef: null, via: null, reason: selection.unavailable };
|
|
680
|
+
const enforcing = results.filter((r) => r.effective === 'enforce');
|
|
681
|
+
const refusal = enforcing.find((r) => r.outcome === 'refuse' || r.outcome === 'unavailable');
|
|
682
|
+
if (refusal) {
|
|
683
|
+
const code = refusal.outcome === 'unavailable' ? 'GOVERNANCE_UNAVAILABLE' : (refusal.code ?? 'DECIDE_REFUSED');
|
|
684
|
+
return { outcome: 'refuse', code, ruleRef: refusal.ref, via: null, reason: null };
|
|
685
|
+
}
|
|
686
|
+
const escalation = enforcing.find((r) => r.outcome === 'escalate');
|
|
687
|
+
if (escalation)
|
|
688
|
+
return { outcome: 'escalate', code: 'DECIDE_ESCALATED', ruleRef: escalation.ref, via: escalation.via ?? 'ask', reason: null };
|
|
689
|
+
return { outcome: 'allow', code: null, ruleRef: null, via: null, reason: null };
|
|
690
|
+
}
|
|
691
|
+
// ─── As source ────────────────────────────────────────────────────────
|
|
692
|
+
/**
|
|
693
|
+
* This module as JavaScript source, for the hook gates to embed: every
|
|
694
|
+
* constant, the parser's error class and every function above, in a scope of
|
|
695
|
+
* the gate's choosing. A test holds the list to the functions this file
|
|
696
|
+
* declares. A CommonJS build names an exported constant `exports.X` inside a
|
|
697
|
+
* function; the source defines each as a plain `X`, so that prefix is dropped.
|
|
698
|
+
*/
|
|
699
|
+
export const DECIDE_JS_SOURCE = [
|
|
700
|
+
`const DECIDE_MAX_QUESTIONS = ${DECIDE_MAX_QUESTIONS};`,
|
|
701
|
+
`const DECIDE_MAX_OPTIONS = ${DECIDE_MAX_OPTIONS};`,
|
|
702
|
+
`const DECIDE_MAX_RULES_PER_CALL = ${DECIDE_MAX_RULES_PER_CALL};`,
|
|
703
|
+
`const DECIDE_MAX_WHEN_CHARS = ${DECIDE_MAX_WHEN_CHARS};`,
|
|
704
|
+
`const DECIDE_MIN_ASK_CHARS = ${DECIDE_MIN_ASK_CHARS};`,
|
|
705
|
+
`const DECIDE_MAX_ASK_CHARS = ${DECIDE_MAX_ASK_CHARS};`,
|
|
706
|
+
`const DECIDE_DEFAULT_BAND = ${JSON.stringify(DECIDE_DEFAULT_BAND)};`,
|
|
707
|
+
`const NONE_OF_THESE = ${JSON.stringify(NONE_OF_THESE)};`,
|
|
708
|
+
`const TOOL_TOKEN = ${String(TOOL_TOKEN)};`,
|
|
709
|
+
`const MAX_TOOLS = ${MAX_TOOLS};`,
|
|
710
|
+
`const MAX_TITLE = ${MAX_TITLE};`,
|
|
711
|
+
`const RULE_ID = ${String(RULE_ID)};`,
|
|
712
|
+
`const NAME = ${String(NAME)};`,
|
|
713
|
+
`const KEYWORDS = ${JSON.stringify(KEYWORDS)};`,
|
|
714
|
+
`const DECIDE_APPROVAL_RE = ${String(DECIDE_APPROVAL_RE)};`,
|
|
715
|
+
`const RULE_FIELDS = ${JSON.stringify(RULE_FIELDS)};`,
|
|
716
|
+
`const QUESTION_FIELDS = ${JSON.stringify(QUESTION_FIELDS)};`,
|
|
717
|
+
`const BAND_FIELDS = ${JSON.stringify(BAND_FIELDS)};`,
|
|
718
|
+
...[
|
|
719
|
+
WhenError,
|
|
720
|
+
isObject,
|
|
721
|
+
has,
|
|
722
|
+
own,
|
|
723
|
+
codePoints,
|
|
724
|
+
unknownField,
|
|
725
|
+
bandShape,
|
|
726
|
+
questionShape,
|
|
727
|
+
ruleShape,
|
|
728
|
+
isWordChar,
|
|
729
|
+
tokenize,
|
|
730
|
+
parseWhen,
|
|
731
|
+
canonicalWhen,
|
|
732
|
+
atomsOf,
|
|
733
|
+
decideOptions,
|
|
734
|
+
decideQuestionBand,
|
|
735
|
+
validateDecideRule,
|
|
736
|
+
parseDecideLine,
|
|
737
|
+
parseDecideLines,
|
|
738
|
+
canonicalJson,
|
|
739
|
+
uniqueSorted,
|
|
740
|
+
decideHashInput,
|
|
741
|
+
decideRuleHash,
|
|
742
|
+
kAnd,
|
|
743
|
+
kOr,
|
|
744
|
+
kNot,
|
|
745
|
+
bandValue,
|
|
746
|
+
choiceOrders,
|
|
747
|
+
choiceAtoms,
|
|
748
|
+
evalWhen,
|
|
749
|
+
logit,
|
|
750
|
+
combinerScore,
|
|
751
|
+
decideAtoms,
|
|
752
|
+
decideRuleOutcome,
|
|
753
|
+
selectDecideRules,
|
|
754
|
+
decideCallOutcome,
|
|
755
|
+
].map((fn) => fn.toString().replace(/\bexports\./g, '')),
|
|
756
|
+
].join('\n');
|
|
757
|
+
//# sourceMappingURL=decide.js.map
|