@felspar/sdk 0.2.0 → 0.2.1
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/README.md +27 -65
- package/dist/chunks/{chunk-247XZ6IY.js → chunk-B4BWUYQY.js} +4 -5
- package/dist/chunks/{chunk-HACK7D24.js → chunk-XLKRU5RA.js} +54 -10
- package/dist/chunks/{chunk-IY6A2423.js → chunk-Y6KRRIQI.js} +354 -189
- package/dist/draft-action-gate-claude.js +5 -5
- package/dist/draft-action-gate-eve.js +3 -3
- package/dist/draft-action-gate-openai.js +5 -5
- package/dist/draft-action-gate.js +2 -2
- package/dist/draft-hosted-client.js +1 -1
- package/dist/eve.js +3 -3
- package/dist/types/draft-action-gate-cases.d.ts +1 -0
- package/dist/types/draft-action-gate-decisions.d.ts +33 -3
- package/dist/types/draft-action-gate-describe.d.ts +39 -1
- package/dist/types/draft-action-gate-subject.d.ts +33 -0
- package/dist/types/draft-action-gate.d.ts +5 -3
- package/dist/types/draft-hosted-client.d.ts +149 -28
- package/package.json +1 -1
- package/skills/felspar/SKILL.md +5 -4
|
@@ -1,119 +1,6 @@
|
|
|
1
1
|
import {
|
|
2
2
|
createHostedFelsparClient
|
|
3
|
-
} from "./chunk-
|
|
4
|
-
|
|
5
|
-
// src/draft-action-gate-cases.ts
|
|
6
|
-
var emailRecipientFields = ["to", "cc", "bcc", "replyTo", "reply_to"];
|
|
7
|
-
var emailSubjectFields = ["subject"];
|
|
8
|
-
var foldCase = (text2) => text2.replaceAll(/[A-Z]/gu, (letter) => letter.toLowerCase());
|
|
9
|
-
var caseKeyOf = (rule) => rule.entity === void 0 ? foldCase : (id) => id.trim();
|
|
10
|
-
var sameField = (names) => {
|
|
11
|
-
const lower = new Set(names.map((name) => name.toLowerCase()));
|
|
12
|
-
return (field) => lower.has(field.toLowerCase());
|
|
13
|
-
};
|
|
14
|
-
function fieldValues(input, names) {
|
|
15
|
-
const isWanted = sameField(names);
|
|
16
|
-
const values = [];
|
|
17
|
-
const unreadable = [];
|
|
18
|
-
for (const [field, value] of Object.entries(input)) {
|
|
19
|
-
if (!isWanted(field)) {
|
|
20
|
-
continue;
|
|
21
|
-
}
|
|
22
|
-
for (const item of [value].flat()) {
|
|
23
|
-
if (typeof item === "string") {
|
|
24
|
-
values.push(item.trim());
|
|
25
|
-
} else if (item !== void 0 && item !== null) {
|
|
26
|
-
unreadable.push(field);
|
|
27
|
-
}
|
|
28
|
-
}
|
|
29
|
-
}
|
|
30
|
-
return { values, unreadable };
|
|
31
|
-
}
|
|
32
|
-
var isAscii = (text2) => /^[!-~]+$/u.test(text2);
|
|
33
|
-
var plainAddress = /^[a-z0-9._+-]+@([a-z0-9-]+(?:\.[a-z0-9-]+)+)$/u;
|
|
34
|
-
var isUnder = (domain, entity) => domain === entity || domain.endsWith(`.${entity}`);
|
|
35
|
-
function domainsIn(text2, entities) {
|
|
36
|
-
return entities.filter((entity) => {
|
|
37
|
-
const escaped = entity.replaceAll(".", String.raw`\.`);
|
|
38
|
-
const whole = new RegExp(`(?:^|[^a-z0-9.-])${escaped}(?:$|[^a-z0-9-])`, "u");
|
|
39
|
-
return whole.test(text2);
|
|
40
|
-
});
|
|
41
|
-
}
|
|
42
|
-
function offTestList(recipients, rules) {
|
|
43
|
-
const listed = new Set((rules.testRecipients ?? []).map((address) => address.trim().toLowerCase()));
|
|
44
|
-
const outside = recipients.filter((address) => !listed.has(address));
|
|
45
|
-
return outside.length > 0 ? `recipient not on the test list: ${outside.join(", ")}` : null;
|
|
46
|
-
}
|
|
47
|
-
function testCompany(recipients, subject, scope) {
|
|
48
|
-
const outside = offTestList(recipients, scope.rules);
|
|
49
|
-
if (outside !== null) {
|
|
50
|
-
return { reason: outside };
|
|
51
|
-
}
|
|
52
|
-
const named = domainsIn(subject.toLowerCase(), scope.entities);
|
|
53
|
-
return named.length === 1 ? { company: named[0] } : { reason: `the subject names ${named.length} decided companies, not one` };
|
|
54
|
-
}
|
|
55
|
-
function recipientCompany(recipients, entities) {
|
|
56
|
-
const companies = /* @__PURE__ */ new Set();
|
|
57
|
-
for (const address of recipients) {
|
|
58
|
-
const domain = plainAddress.exec(address)?.[1] ?? "";
|
|
59
|
-
const owners = entities.filter((entity) => isUnder(domain, entity));
|
|
60
|
-
if (owners.length === 0) {
|
|
61
|
-
return { reason: `${address} is not at a company Felspar decided on`, missing: true };
|
|
62
|
-
}
|
|
63
|
-
for (const owner of owners) {
|
|
64
|
-
companies.add(owner);
|
|
65
|
-
}
|
|
66
|
-
}
|
|
67
|
-
return companies.size === 1 ? { company: [...companies][0] } : { reason: "the recipients belong to more than one company" };
|
|
68
|
-
}
|
|
69
|
-
function recipientsOf(scope, input) {
|
|
70
|
-
const { rules, rule } = scope;
|
|
71
|
-
const recipientFields = rule.recipientFields ?? rules.recipientFields ?? emailRecipientFields;
|
|
72
|
-
const recipients = fieldValues(input, recipientFields);
|
|
73
|
-
if (recipients.unreadable.length > 0) {
|
|
74
|
-
return { reason: `${recipients.unreadable.join(", ")} holds a value that is not an address` };
|
|
75
|
-
}
|
|
76
|
-
const malformed = recipients.values.find((address) => !isAscii(address) || !plainAddress.test(address.toLowerCase()));
|
|
77
|
-
return malformed === void 0 ? { addresses: recipients.values.map((address) => address.toLowerCase()) } : { reason: `"${malformed}" is not one plain address` };
|
|
78
|
-
}
|
|
79
|
-
function companyFor(scope, input) {
|
|
80
|
-
const { rules, rule } = scope;
|
|
81
|
-
const { addresses, reason } = recipientsOf(scope, input);
|
|
82
|
-
if (addresses === void 0) {
|
|
83
|
-
return { reason };
|
|
84
|
-
}
|
|
85
|
-
if (addresses.length === 0) {
|
|
86
|
-
return { reason: "the call has no recipients" };
|
|
87
|
-
}
|
|
88
|
-
if (rules.testRecipients === void 0) {
|
|
89
|
-
return recipientCompany(addresses, scope.entities);
|
|
90
|
-
}
|
|
91
|
-
const subjectFields = rule.subjectFields ?? rules.subjectFields ?? emailSubjectFields;
|
|
92
|
-
const subject = fieldValues(input, subjectFields).values.join("\n");
|
|
93
|
-
return testCompany(addresses, subject, scope);
|
|
94
|
-
}
|
|
95
|
-
function fieldEntity(scope, field, input) {
|
|
96
|
-
const { values, unreadable } = fieldValues(input, [field]);
|
|
97
|
-
if (unreadable.length > 0 || values.length !== 1) {
|
|
98
|
-
return { reason: `the call must set ${field} to exactly one ID` };
|
|
99
|
-
}
|
|
100
|
-
const [value = ""] = values;
|
|
101
|
-
if (value.length === 0) {
|
|
102
|
-
return { reason: `the call's ${field} is empty` };
|
|
103
|
-
}
|
|
104
|
-
if (scope.rules.testRecipients !== void 0) {
|
|
105
|
-
const { addresses, reason } = recipientsOf(scope, input);
|
|
106
|
-
const outside = addresses === void 0 ? reason : offTestList(addresses, scope.rules);
|
|
107
|
-
if (outside !== null) {
|
|
108
|
-
return { reason: outside };
|
|
109
|
-
}
|
|
110
|
-
}
|
|
111
|
-
return { company: value };
|
|
112
|
-
}
|
|
113
|
-
function caseFor(scope, input) {
|
|
114
|
-
const { entity } = scope.rule;
|
|
115
|
-
return entity === void 0 ? companyFor(scope, input) : fieldEntity(scope, entity.field, input);
|
|
116
|
-
}
|
|
3
|
+
} from "./chunk-XLKRU5RA.js";
|
|
117
4
|
|
|
118
5
|
// src/draft-action-gate-outputs.ts
|
|
119
6
|
var isRecord = (value) => typeof value === "object" && value !== null && !Array.isArray(value);
|
|
@@ -220,6 +107,10 @@ function felsparOutputCheckVerifier(configuration) {
|
|
|
220
107
|
}
|
|
221
108
|
|
|
222
109
|
// src/draft-action-gate-decisions.ts
|
|
110
|
+
var identityModes = ["provided", "fields", "content", "issued"];
|
|
111
|
+
var escalationTriggers = ["outcome", "unresolved", "attempt_limit"];
|
|
112
|
+
var isIdentityMode = (value) => identityModes.includes(value);
|
|
113
|
+
var isEscalationTrigger = (value) => escalationTriggers.includes(value);
|
|
223
114
|
var isRecord2 = (value) => typeof value === "object" && value !== null && !Array.isArray(value);
|
|
224
115
|
function parsedJson2(text2) {
|
|
225
116
|
try {
|
|
@@ -269,20 +160,51 @@ function entityIdOf(input) {
|
|
|
269
160
|
function freshnessOf(explanation) {
|
|
270
161
|
return isRecord2(explanation) && typeof explanation.evidenceFreshUntil === "string" ? explanation.evidenceFreshUntil : null;
|
|
271
162
|
}
|
|
272
|
-
|
|
163
|
+
var isId = (value) => typeof value === "string" && value.length > 0 && value.length <= 200;
|
|
164
|
+
function caseOf(result) {
|
|
165
|
+
if (!Object.hasOwn(result, "case")) {
|
|
166
|
+
return void 0;
|
|
167
|
+
}
|
|
168
|
+
const found = result.case;
|
|
169
|
+
return isRecord2(found) && isId(found.id) && isIdentityMode(found.mode) ? { id: found.id, mode: found.mode } : null;
|
|
170
|
+
}
|
|
171
|
+
function escalationOf(result) {
|
|
172
|
+
const found = result.escalation;
|
|
173
|
+
if (!isRecord2(found) || !isId(found.id) || !isEscalationTrigger(found.trigger)) {
|
|
174
|
+
return void 0;
|
|
175
|
+
}
|
|
176
|
+
const why = isRecord2(found.why) && typeof found.why.text === "string" ? found.why.text : "";
|
|
177
|
+
return { id: found.id, trigger: found.trigger, why };
|
|
178
|
+
}
|
|
179
|
+
var requestOf = (request) => typeof request === "string" ? { entity: request } : request;
|
|
180
|
+
function decidedOf(result) {
|
|
181
|
+
const { receipt, outcome, executionObjectId } = result;
|
|
182
|
+
if (typeof outcome !== "string" || typeof executionObjectId !== "string" || !isRecord2(receipt)) {
|
|
183
|
+
return null;
|
|
184
|
+
}
|
|
185
|
+
const { receiptDigest } = receipt;
|
|
186
|
+
return typeof receiptDigest === "string" ? { outcome, executionObjectId, receipt: { ...receipt, receiptDigest } } : null;
|
|
187
|
+
}
|
|
188
|
+
function decisionFromResult(request, output) {
|
|
273
189
|
const result = jobResponse(output)?.result;
|
|
274
|
-
if (!isRecord2(result)
|
|
190
|
+
if (!isRecord2(result)) {
|
|
275
191
|
return null;
|
|
276
192
|
}
|
|
277
|
-
const {
|
|
278
|
-
|
|
193
|
+
const { entity, job } = requestOf(request);
|
|
194
|
+
const found = caseOf(result);
|
|
195
|
+
const key = found === void 0 ? entity : found?.id;
|
|
196
|
+
const decided = decidedOf(result);
|
|
197
|
+
if (decided === null || found === null || key === void 0 || key.length === 0) {
|
|
279
198
|
return null;
|
|
280
199
|
}
|
|
200
|
+
const escalation = escalationOf(result);
|
|
281
201
|
return {
|
|
282
|
-
entity,
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
202
|
+
entity: key,
|
|
203
|
+
...found !== void 0 && { case: found },
|
|
204
|
+
...job !== void 0 && { job },
|
|
205
|
+
...isId(result.releaseId) && { releaseId: result.releaseId },
|
|
206
|
+
...escalation !== void 0 && { escalation },
|
|
207
|
+
...decided,
|
|
286
208
|
context: result.context ?? null,
|
|
287
209
|
evidenceFreshUntil: freshnessOf(result.explanation)
|
|
288
210
|
};
|
|
@@ -302,8 +224,8 @@ function createDecisionRecorder(limits = {}) {
|
|
|
302
224
|
const requested = /* @__PURE__ */ new Map();
|
|
303
225
|
const decided = /* @__PURE__ */ new Map();
|
|
304
226
|
const checked = /* @__PURE__ */ new Map();
|
|
305
|
-
function record(sessionId,
|
|
306
|
-
const decision = decisionFromResult(
|
|
227
|
+
function record(sessionId, request, output) {
|
|
228
|
+
const decision = decisionFromResult(request, output);
|
|
307
229
|
if (decision === null) {
|
|
308
230
|
return null;
|
|
309
231
|
}
|
|
@@ -315,14 +237,14 @@ function createDecisionRecorder(limits = {}) {
|
|
|
315
237
|
}
|
|
316
238
|
return {
|
|
317
239
|
record,
|
|
318
|
-
noteRequest(callId,
|
|
319
|
-
requested.set(callId,
|
|
240
|
+
noteRequest(callId, request) {
|
|
241
|
+
requested.set(callId, request);
|
|
320
242
|
trim(requested, maxPending);
|
|
321
243
|
},
|
|
322
244
|
noteResult(sessionId, callId, output) {
|
|
323
|
-
const
|
|
245
|
+
const noted = requested.get(callId);
|
|
324
246
|
requested.delete(callId);
|
|
325
|
-
return
|
|
247
|
+
return noted === void 0 ? null : record(sessionId, noted, output);
|
|
326
248
|
},
|
|
327
249
|
decisionsIn: (sessionId) => decided.get(sessionId) ?? [],
|
|
328
250
|
recordOutputCheck(sessionId, jobId, output) {
|
|
@@ -379,28 +301,208 @@ async function writesProblem(describe, decided) {
|
|
|
379
301
|
const outputs = outcomeIn(description, decided.outcome)?.outputs ?? [];
|
|
380
302
|
return outputs.some((row) => row.id === decided.output) ? null : `${decided.outcome} doesn't ask for ${decided.output}`;
|
|
381
303
|
}
|
|
304
|
+
async function subjectOf(describe, releaseId, jobId) {
|
|
305
|
+
const { subject, identity } = await describedFor(describe, releaseId, jobId);
|
|
306
|
+
return subject === void 0 ? null : { type: subject.type, match: subject.match, mode: identity?.mode ?? "provided" };
|
|
307
|
+
}
|
|
382
308
|
var fixOutputCheck = "Fix what check_output flagged, check it again, then retry.";
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
309
|
+
var quotedText = (quoted) => typeof quoted?.text === "string" && quoted.text.length > 0 ? quoted.text : null;
|
|
310
|
+
function escalationIdOf(step, description) {
|
|
311
|
+
if (step.kind === "unresolved") {
|
|
312
|
+
const id2 = description?.escalations?.unresolved ?? null;
|
|
313
|
+
return id2 === null ? null : { id: id2, trigger: "unresolved" };
|
|
314
|
+
}
|
|
315
|
+
if (step.replayed !== void 0) {
|
|
316
|
+
return step.replayed.trigger === "outcome" ? { id: step.replayed.id, trigger: "outcome" } : null;
|
|
386
317
|
}
|
|
387
|
-
const
|
|
388
|
-
|
|
389
|
-
return typeof quoted?.text === "string" && quoted.text.length > 0 ? quoted.text : null;
|
|
318
|
+
const id = description === null ? void 0 : outcomeIn(description, step.outcome)?.escalate;
|
|
319
|
+
return id === void 0 ? null : { id, trigger: "outcome" };
|
|
390
320
|
}
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
321
|
+
function escalationOf2(step, description) {
|
|
322
|
+
const named = escalationIdOf(step, description);
|
|
323
|
+
if (named === null) {
|
|
324
|
+
return null;
|
|
394
325
|
}
|
|
395
|
-
|
|
326
|
+
const route = description?.escalations?.routes.find((row) => row.id === named.id);
|
|
327
|
+
const recorded = step.kind === "outcome" && step.recorded?.id === named.id ? step.recorded.why : null;
|
|
328
|
+
const why = quotedText(route?.quote) ?? (recorded === "" ? null : recorded);
|
|
329
|
+
return { ...named, why };
|
|
330
|
+
}
|
|
331
|
+
function nextStepOf(step, description) {
|
|
332
|
+
return step.kind === "unresolved" ? description.whenUnresolved : outcomeIn(description, step.outcome)?.nextStep;
|
|
333
|
+
}
|
|
334
|
+
async function describedStep(step, describe, jobId) {
|
|
335
|
+
if (describe === void 0 || step.releaseId === void 0) {
|
|
396
336
|
return null;
|
|
397
337
|
}
|
|
398
338
|
try {
|
|
399
|
-
return await
|
|
339
|
+
return await describedFor(describe, step.releaseId, jobId);
|
|
400
340
|
} catch {
|
|
401
341
|
return null;
|
|
402
342
|
}
|
|
403
343
|
}
|
|
344
|
+
async function escalationFor(step, describe, jobId) {
|
|
345
|
+
if (step?.kind === "output-check") {
|
|
346
|
+
return { escalate: fixOutputCheck };
|
|
347
|
+
}
|
|
348
|
+
if (step === void 0) {
|
|
349
|
+
return {};
|
|
350
|
+
}
|
|
351
|
+
const description = await describedStep(step, describe, jobId);
|
|
352
|
+
const escalate = description === null ? null : quotedText(nextStepOf(step, description));
|
|
353
|
+
const escalation = escalationOf2(step, description);
|
|
354
|
+
return {
|
|
355
|
+
...escalate !== null && { escalate },
|
|
356
|
+
...escalation !== null && { escalation }
|
|
357
|
+
};
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
// src/draft-action-gate-cases.ts
|
|
361
|
+
var emailRecipientFields = ["to", "cc", "bcc", "replyTo", "reply_to"];
|
|
362
|
+
var emailSubjectFields = ["subject"];
|
|
363
|
+
var foldCase = (text2) => text2.replaceAll(/[A-Z]/gu, (letter) => letter.toLowerCase());
|
|
364
|
+
var sameField = (names) => {
|
|
365
|
+
const lower = new Set(names.map((name) => name.toLowerCase()));
|
|
366
|
+
return (field) => lower.has(field.toLowerCase());
|
|
367
|
+
};
|
|
368
|
+
function fieldValues(input, names) {
|
|
369
|
+
const isWanted = sameField(names);
|
|
370
|
+
const values = [];
|
|
371
|
+
const unreadable = [];
|
|
372
|
+
for (const [field, value] of Object.entries(input)) {
|
|
373
|
+
if (!isWanted(field)) {
|
|
374
|
+
continue;
|
|
375
|
+
}
|
|
376
|
+
for (const item of [value].flat()) {
|
|
377
|
+
if (typeof item === "string") {
|
|
378
|
+
values.push(item.trim());
|
|
379
|
+
} else if (item !== void 0 && item !== null) {
|
|
380
|
+
unreadable.push(field);
|
|
381
|
+
}
|
|
382
|
+
}
|
|
383
|
+
}
|
|
384
|
+
return { values, unreadable };
|
|
385
|
+
}
|
|
386
|
+
var isAscii = (text2) => /^[!-~]+$/u.test(text2);
|
|
387
|
+
var plainAddress = /^[a-z0-9._+-]+@([a-z0-9-]+(?:\.[a-z0-9-]+)+)$/u;
|
|
388
|
+
var isUnder = (domain, entity) => domain === entity || domain.endsWith(`.${entity}`);
|
|
389
|
+
function domainsIn(text2, entities) {
|
|
390
|
+
return entities.filter((entity) => {
|
|
391
|
+
const escaped = entity.replaceAll(".", String.raw`\.`);
|
|
392
|
+
const whole = new RegExp(`(?:^|[^a-z0-9.-])${escaped}(?:$|[^a-z0-9-])`, "u");
|
|
393
|
+
return whole.test(text2);
|
|
394
|
+
});
|
|
395
|
+
}
|
|
396
|
+
function offTestList(recipients, rules) {
|
|
397
|
+
const listed = new Set((rules.testRecipients ?? []).map((address) => address.trim().toLowerCase()));
|
|
398
|
+
const outside = recipients.filter((address) => !listed.has(address));
|
|
399
|
+
return outside.length > 0 ? `recipient not on the test list: ${outside.join(", ")}` : null;
|
|
400
|
+
}
|
|
401
|
+
function testCompany(recipients, subject, scope) {
|
|
402
|
+
const outside = offTestList(recipients, scope.rules);
|
|
403
|
+
if (outside !== null) {
|
|
404
|
+
return { reason: outside };
|
|
405
|
+
}
|
|
406
|
+
const named = domainsIn(subject.toLowerCase(), scope.entities);
|
|
407
|
+
return named.length === 1 ? { company: named[0] } : { reason: `the subject names ${named.length} decided companies, not one` };
|
|
408
|
+
}
|
|
409
|
+
function recipientCompany(recipients, entities) {
|
|
410
|
+
const companies = /* @__PURE__ */ new Set();
|
|
411
|
+
for (const address of recipients) {
|
|
412
|
+
const domain = plainAddress.exec(address)?.[1] ?? "";
|
|
413
|
+
const owners = entities.filter((entity) => isUnder(domain, entity));
|
|
414
|
+
if (owners.length === 0) {
|
|
415
|
+
return { reason: `${address} is not at a company Felspar decided on`, missing: true };
|
|
416
|
+
}
|
|
417
|
+
for (const owner of owners) {
|
|
418
|
+
companies.add(owner);
|
|
419
|
+
}
|
|
420
|
+
}
|
|
421
|
+
return companies.size === 1 ? { company: [...companies][0] } : { reason: "the recipients belong to more than one company" };
|
|
422
|
+
}
|
|
423
|
+
function recipientsOf(scope, input) {
|
|
424
|
+
const { rules, rule } = scope;
|
|
425
|
+
const recipientFields = rule.recipientFields ?? rules.recipientFields ?? emailRecipientFields;
|
|
426
|
+
const recipients = fieldValues(input, recipientFields);
|
|
427
|
+
if (recipients.unreadable.length > 0) {
|
|
428
|
+
return { reason: `${recipients.unreadable.join(", ")} holds a value that is not an address` };
|
|
429
|
+
}
|
|
430
|
+
const malformed = recipients.values.find((address) => !isAscii(address) || !plainAddress.test(address.toLowerCase()));
|
|
431
|
+
return malformed === void 0 ? { addresses: recipients.values.map((address) => address.toLowerCase()) } : { reason: `"${malformed}" is not one plain address` };
|
|
432
|
+
}
|
|
433
|
+
function companyFor(scope, input) {
|
|
434
|
+
const { rules, rule } = scope;
|
|
435
|
+
const { addresses, reason } = recipientsOf(scope, input);
|
|
436
|
+
if (addresses === void 0) {
|
|
437
|
+
return { reason };
|
|
438
|
+
}
|
|
439
|
+
if (addresses.length === 0) {
|
|
440
|
+
return { reason: "the call has no recipients" };
|
|
441
|
+
}
|
|
442
|
+
if (rules.testRecipients === void 0) {
|
|
443
|
+
return recipientCompany(addresses, scope.entities);
|
|
444
|
+
}
|
|
445
|
+
const subjectFields = rule.subjectFields ?? rules.subjectFields ?? emailSubjectFields;
|
|
446
|
+
const subject = fieldValues(input, subjectFields).values.join("\n");
|
|
447
|
+
return testCompany(addresses, subject, scope);
|
|
448
|
+
}
|
|
449
|
+
function testListProblem(scope, input) {
|
|
450
|
+
if (scope.rules.testRecipients === void 0) {
|
|
451
|
+
return null;
|
|
452
|
+
}
|
|
453
|
+
const { addresses, reason } = recipientsOf({ ...scope, entities: [] }, input);
|
|
454
|
+
return addresses === void 0 ? reason : offTestList(addresses, scope.rules);
|
|
455
|
+
}
|
|
456
|
+
function fieldEntity(scope, field, input) {
|
|
457
|
+
const { values, unreadable } = fieldValues(input, [field]);
|
|
458
|
+
if (unreadable.length > 0 || values.length !== 1) {
|
|
459
|
+
return { reason: `the call must set ${field} to exactly one ID` };
|
|
460
|
+
}
|
|
461
|
+
const [value = ""] = values;
|
|
462
|
+
if (value.length === 0) {
|
|
463
|
+
return { reason: `the call's ${field} is empty` };
|
|
464
|
+
}
|
|
465
|
+
const outside = testListProblem(scope, input);
|
|
466
|
+
return outside === null ? { company: value } : { reason: outside };
|
|
467
|
+
}
|
|
468
|
+
function caseFor(scope, input) {
|
|
469
|
+
const { entity } = scope.rule;
|
|
470
|
+
return entity === void 0 ? companyFor(scope, input) : fieldEntity(scope, entity.field, input);
|
|
471
|
+
}
|
|
472
|
+
|
|
473
|
+
// src/draft-action-gate-rules.ts
|
|
474
|
+
var isText = (value) => typeof value === "string" && value.trim().length > 0;
|
|
475
|
+
function ruleProblem(rule) {
|
|
476
|
+
if (rule.outcomes === void 0 === (rule.writes === void 0)) {
|
|
477
|
+
return "this tool's rule must give exactly one of outcomes or writes";
|
|
478
|
+
}
|
|
479
|
+
const { outcomes, writes, entity } = rule;
|
|
480
|
+
if (outcomes !== void 0 && !(Array.isArray(outcomes) && outcomes.every(isText))) {
|
|
481
|
+
return "this tool's rule has unreadable outcomes";
|
|
482
|
+
}
|
|
483
|
+
if (writes !== void 0 && !isText(writes)) {
|
|
484
|
+
return "this tool's rule has an unreadable writes";
|
|
485
|
+
}
|
|
486
|
+
if (entity !== void 0 && !isText(entity.field)) {
|
|
487
|
+
return "this tool's rule has an unreadable entity field";
|
|
488
|
+
}
|
|
489
|
+
const checked = rule.outputCheck?.output;
|
|
490
|
+
return writes !== void 0 && checked !== void 0 && checked !== writes ? "this tool's output check is for a different output than it writes" : null;
|
|
491
|
+
}
|
|
492
|
+
function fieldProblem(refuse, rule, input) {
|
|
493
|
+
const isAllowed = sameField(rule.fields);
|
|
494
|
+
const isRefused = sameField(refuse);
|
|
495
|
+
const unknown = Object.keys(input).filter((field) => !isAllowed(field));
|
|
496
|
+
if (unknown.length > 0) {
|
|
497
|
+
return `the call sets ${unknown.join(", ")}, which this tool's rule doesn't allow`;
|
|
498
|
+
}
|
|
499
|
+
const refused = Object.keys(input).filter((field) => isRefused(field));
|
|
500
|
+
return refused.length > 0 ? `the call sets ${refused.join(", ")}` : null;
|
|
501
|
+
}
|
|
502
|
+
function checkRuleOf(rule) {
|
|
503
|
+
const { outputCheck, writes } = rule;
|
|
504
|
+
return outputCheck !== void 0 && writes !== void 0 && outputCheck.output === void 0 ? { ...outputCheck, output: writes } : outputCheck;
|
|
505
|
+
}
|
|
404
506
|
|
|
405
507
|
// src/draft-action-gate-domains.ts
|
|
406
508
|
var publicSuffixes = [
|
|
@@ -512,38 +614,72 @@ function isCompanyDomain(entity, extraShared = []) {
|
|
|
512
614
|
return domainShape.test(entity) && !builtIn.has(entity) && extraShared.every((domain) => domain.toLowerCase() !== entity);
|
|
513
615
|
}
|
|
514
616
|
|
|
515
|
-
// src/draft-action-gate-
|
|
516
|
-
var
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
return "this tool's rule has an unreadable writes";
|
|
617
|
+
// src/draft-action-gate-subject.ts
|
|
618
|
+
var trimmed = (id) => id.trim();
|
|
619
|
+
var folded = (id) => foldCase(id.trim());
|
|
620
|
+
function keyingFor(subject, path) {
|
|
621
|
+
const isFolded = path === "recipients" || subject?.match === "case-insensitive";
|
|
622
|
+
const key = isFolded ? folded : trimmed;
|
|
623
|
+
return { key, decisionKey: (decision) => key(decision.case?.id ?? decision.entity) };
|
|
624
|
+
}
|
|
625
|
+
async function subjectFor(scope, decision) {
|
|
626
|
+
if (decision === void 0) {
|
|
627
|
+
return null;
|
|
527
628
|
}
|
|
528
|
-
|
|
529
|
-
|
|
629
|
+
const { describe } = scope;
|
|
630
|
+
const { releaseId } = decision;
|
|
631
|
+
const described = describe === void 0 || releaseId === void 0 ? null : await subjectOf(describe, releaseId, scope.rules.job);
|
|
632
|
+
const mode = decision.case?.mode ?? "provided";
|
|
633
|
+
return described === null && mode !== "provided" ? { type: "case", match: "exact", mode } : described;
|
|
634
|
+
}
|
|
635
|
+
var isRecipientJob = (subject) => subject === null || subject.mode === "provided" && subject.match === "case-insensitive";
|
|
636
|
+
var distinct = (keys) => [...new Set(keys)];
|
|
637
|
+
async function pick(scope, found) {
|
|
638
|
+
const { company, keying, subject } = found;
|
|
639
|
+
const decision = scope.decisions.filter((row) => keying.decisionKey(row) === company).at(-1);
|
|
640
|
+
if (decision === void 0) {
|
|
641
|
+
return { reason: `no decision for ${company}`, missing: true };
|
|
642
|
+
}
|
|
643
|
+
const own = await subjectFor(scope, decision);
|
|
644
|
+
return JSON.stringify(own) === JSON.stringify(subject) ? { company, decision, keying } : { reason: `${company} was decided on a release that identifies cases differently` };
|
|
645
|
+
}
|
|
646
|
+
async function onlyCase(scope, subject, input) {
|
|
647
|
+
const keying = keyingFor(subject, "case");
|
|
648
|
+
const keys = distinct(scope.decisions.map((row) => keying.decisionKey(row)));
|
|
649
|
+
if (keys.length !== 1) {
|
|
650
|
+
return {
|
|
651
|
+
reason: keys.length === 0 ? `no decision for this job's ${subject.type}` : `this session decided ${keys.length} ${subject.type} cases and the call doesn't say which; give the tool's rule an entity field holding the case ID`,
|
|
652
|
+
...keys.length === 0 && { missing: true }
|
|
653
|
+
};
|
|
530
654
|
}
|
|
531
|
-
const
|
|
532
|
-
|
|
655
|
+
const outside = testListProblem(scope, input);
|
|
656
|
+
const [company = ""] = keys;
|
|
657
|
+
return outside === null ? await pick(scope, { company, keying, subject }) : { reason: outside };
|
|
533
658
|
}
|
|
534
|
-
function
|
|
535
|
-
const
|
|
536
|
-
const
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
659
|
+
async function selected(scope, input) {
|
|
660
|
+
const { rules, rule } = scope;
|
|
661
|
+
const subject = await subjectFor(scope, scope.decisions.at(-1));
|
|
662
|
+
if (rule.entity !== void 0) {
|
|
663
|
+
const keying2 = keyingFor(subject, "case");
|
|
664
|
+
const found2 = caseFor({ rules, rule, entities: [] }, input);
|
|
665
|
+
return found2.company === void 0 ? { reason: found2.reason ?? "no case" } : await pick(scope, { company: keying2.key(found2.company), keying: keying2, subject });
|
|
666
|
+
}
|
|
667
|
+
const keying = keyingFor(subject, "recipients");
|
|
668
|
+
const keys = distinct(scope.decisions.map((row) => keying.decisionKey(row)));
|
|
669
|
+
if (subject !== null && !isRecipientJob(subject)) {
|
|
670
|
+
return await onlyCase(scope, subject, input);
|
|
671
|
+
}
|
|
672
|
+
const entities = keys.filter((key) => isCompanyDomain(key, rules.sharedDomains));
|
|
673
|
+
const found = caseFor({ rules, rule, entities }, input);
|
|
674
|
+
return found.company === void 0 ? { reason: found.reason ?? "no case", ...found.missing === true && { missing: true } } : await pick(scope, { company: found.company, keying, subject });
|
|
675
|
+
}
|
|
676
|
+
async function selectCase(scope, input) {
|
|
677
|
+
const decisions = scope.decisions.filter((row) => row.job === void 0 || row.job === scope.rules.job);
|
|
678
|
+
try {
|
|
679
|
+
return await selected({ ...scope, decisions }, input);
|
|
680
|
+
} catch (error) {
|
|
681
|
+
return { reason: `Felspar's job description could not be read: ${String(error)}` };
|
|
540
682
|
}
|
|
541
|
-
const refused = Object.keys(input).filter((field) => isRefused(field));
|
|
542
|
-
return refused.length > 0 ? `the call sets ${refused.join(", ")}` : null;
|
|
543
|
-
}
|
|
544
|
-
function checkRuleOf(rule) {
|
|
545
|
-
const { outputCheck, writes } = rule;
|
|
546
|
-
return outputCheck !== void 0 && writes !== void 0 && outputCheck.output === void 0 ? { ...outputCheck, output: writes } : outputCheck;
|
|
547
683
|
}
|
|
548
684
|
|
|
549
685
|
// src/draft-action-gate-verifier.ts
|
|
@@ -562,20 +698,42 @@ function felsparVerifier(configuration) {
|
|
|
562
698
|
|
|
563
699
|
// src/draft-action-gate.ts
|
|
564
700
|
function denialText(verdict) {
|
|
565
|
-
|
|
701
|
+
const parts = [verdict.reason];
|
|
702
|
+
if (verdict.escalate !== void 0) {
|
|
703
|
+
parts.push(`Next step: ${verdict.escalate}`);
|
|
704
|
+
}
|
|
705
|
+
const { escalation } = verdict;
|
|
706
|
+
if (escalation !== void 0) {
|
|
707
|
+
const why = escalation.why === null ? "" : `: ${escalation.why}`;
|
|
708
|
+
parts.push(`Escalation ${escalation.id} (${escalation.trigger})${why}`);
|
|
709
|
+
}
|
|
710
|
+
return parts.join(". ");
|
|
566
711
|
}
|
|
567
712
|
function gatedRule(rules, toolName) {
|
|
568
713
|
return Object.hasOwn(rules.tools, toolName) ? rules.tools[toolName] : void 0;
|
|
569
714
|
}
|
|
570
715
|
var isInput = (value) => typeof value === "object" && value !== null && !Array.isArray(value);
|
|
571
|
-
function
|
|
572
|
-
const
|
|
716
|
+
function caseProblem(decision, verified) {
|
|
717
|
+
const recorded = decision.case;
|
|
718
|
+
const replayed = verified.case;
|
|
719
|
+
if (recorded === void 0 && replayed === void 0) {
|
|
720
|
+
return null;
|
|
721
|
+
}
|
|
722
|
+
const isSame = recorded?.id === replayed?.id && recorded?.mode === replayed?.mode && replayed?.id === verified.entityId;
|
|
723
|
+
return isSame ? null : `the receipt is for case ${replayed?.id ?? "(none)"}, not ${recorded?.id ?? "(none)"}`;
|
|
724
|
+
}
|
|
725
|
+
function replayProblem(rules, found, verified) {
|
|
726
|
+
const { company, decision, keying } = found;
|
|
573
727
|
if (verified.jobId !== rules.job) {
|
|
574
728
|
return `the receipt is for job ${verified.jobId}, not ${rules.job}`;
|
|
575
729
|
}
|
|
576
|
-
if (
|
|
730
|
+
if (keying.key(verified.entityId) !== company) {
|
|
577
731
|
return `the receipt is for ${verified.entityId}, not ${company}`;
|
|
578
732
|
}
|
|
733
|
+
const mismatch = caseProblem(decision, verified);
|
|
734
|
+
if (mismatch !== null) {
|
|
735
|
+
return mismatch;
|
|
736
|
+
}
|
|
579
737
|
const { releases } = rules;
|
|
580
738
|
const isUnpinned = releases !== void 0 && !releases.includes(verified.releaseId ?? "");
|
|
581
739
|
return isUnpinned ? `the receipt is for release ${verified.releaseId ?? "(none)"}, which is not allowed` : null;
|
|
@@ -587,7 +745,8 @@ function staleness(decision, now) {
|
|
|
587
745
|
}
|
|
588
746
|
return Date.parse(until) > now ? null : `the decision's evidence went stale at ${until}`;
|
|
589
747
|
}
|
|
590
|
-
async function permitProblem(rule,
|
|
748
|
+
async function permitProblem(rule, decided, request) {
|
|
749
|
+
const { verified, decision } = decided;
|
|
591
750
|
const { releaseId, outcome } = verified;
|
|
592
751
|
let problem = null;
|
|
593
752
|
if (rule.writes !== void 0) {
|
|
@@ -600,7 +759,18 @@ async function permitProblem(rule, verified, request) {
|
|
|
600
759
|
} else if (rule.outcomes?.includes(outcome) !== true) {
|
|
601
760
|
problem = `${verified.entityId} is ${outcome}, which doesn't allow this`;
|
|
602
761
|
}
|
|
603
|
-
|
|
762
|
+
if (problem === null) {
|
|
763
|
+
return null;
|
|
764
|
+
}
|
|
765
|
+
const recorded = decision.escalation;
|
|
766
|
+
const step = {
|
|
767
|
+
kind: "outcome",
|
|
768
|
+
releaseId,
|
|
769
|
+
outcome,
|
|
770
|
+
...verified.escalation !== void 0 && { replayed: verified.escalation },
|
|
771
|
+
...recorded?.trigger === "outcome" && { recorded }
|
|
772
|
+
};
|
|
773
|
+
return { allow: false, reason: problem, step };
|
|
604
774
|
}
|
|
605
775
|
async function verifiedFor(rule, found, request) {
|
|
606
776
|
const { company, decision } = found;
|
|
@@ -612,7 +782,7 @@ async function verifiedFor(rule, found, request) {
|
|
|
612
782
|
if (verified === null) {
|
|
613
783
|
return { allow: false, reason: `Felspar did not reproduce ${company}'s receipt` };
|
|
614
784
|
}
|
|
615
|
-
const problem = replayProblem(
|
|
785
|
+
const problem = replayProblem(request.rules, found, verified);
|
|
616
786
|
if (problem !== null) {
|
|
617
787
|
return { allow: false, reason: problem };
|
|
618
788
|
}
|
|
@@ -621,7 +791,7 @@ async function verifiedFor(rule, found, request) {
|
|
|
621
791
|
const reason = `${verified.entityId} is undecided, which doesn't allow this`;
|
|
622
792
|
return { allow: false, reason, step: { kind: "unresolved", releaseId: verified.releaseId } };
|
|
623
793
|
}
|
|
624
|
-
const denied = await permitProblem(rule, { ...verified, outcome }, request);
|
|
794
|
+
const denied = await permitProblem(rule, { verified: { ...verified, outcome }, decision }, request);
|
|
625
795
|
if (denied !== null) {
|
|
626
796
|
return denied;
|
|
627
797
|
}
|
|
@@ -658,20 +828,16 @@ async function decide(request) {
|
|
|
658
828
|
if (problem !== null) {
|
|
659
829
|
return { allow: false, reason: problem };
|
|
660
830
|
}
|
|
661
|
-
const
|
|
662
|
-
|
|
663
|
-
|
|
664
|
-
const unresolved = { kind: "unresolved", releaseId: pinned };
|
|
665
|
-
const { company, reason, missing } = caseFor({ rules, rule, entities }, input);
|
|
666
|
-
if (company === void 0) {
|
|
831
|
+
const selected2 = await selectCase({ rules, rule, decisions, describe: request.describe }, input);
|
|
832
|
+
if (selected2.company === void 0) {
|
|
833
|
+
const pinned = rules.releases?.length === 1 ? rules.releases[0] : void 0;
|
|
667
834
|
return {
|
|
668
835
|
allow: false,
|
|
669
|
-
reason: reason
|
|
670
|
-
...missing === true && { step: unresolved }
|
|
836
|
+
reason: selected2.reason,
|
|
837
|
+
...selected2.missing === true && { step: { kind: "unresolved", releaseId: pinned } }
|
|
671
838
|
};
|
|
672
839
|
}
|
|
673
|
-
|
|
674
|
-
return decision === void 0 ? { allow: false, reason: `no decision for ${company}`, step: unresolved } : await verifiedFor(rule, { company, decision, input }, request);
|
|
840
|
+
return await verifiedFor(rule, { ...selected2, input }, request);
|
|
675
841
|
}
|
|
676
842
|
async function checkAction(request) {
|
|
677
843
|
let decided;
|
|
@@ -684,8 +850,7 @@ async function checkAction(request) {
|
|
|
684
850
|
return decided;
|
|
685
851
|
}
|
|
686
852
|
const { step, ...denied } = decided;
|
|
687
|
-
|
|
688
|
-
return escalate === null ? denied : { ...denied, escalate };
|
|
853
|
+
return { ...denied, ...await escalationFor(step, request.describe, request.rules.job) };
|
|
689
854
|
}
|
|
690
855
|
function report(onVerdict, toolName, verdict) {
|
|
691
856
|
if (onVerdict === void 0) {
|
|
@@ -723,8 +888,6 @@ function createActionGate(config) {
|
|
|
723
888
|
}
|
|
724
889
|
|
|
725
890
|
export {
|
|
726
|
-
emailRecipientFields,
|
|
727
|
-
emailSubjectFields,
|
|
728
891
|
outputCheckFromResult,
|
|
729
892
|
outputTextDigest,
|
|
730
893
|
felsparOutputCheckVerifier,
|
|
@@ -733,6 +896,8 @@ export {
|
|
|
733
896
|
decisionFromResult,
|
|
734
897
|
createDecisionRecorder,
|
|
735
898
|
felsparJobDescriber,
|
|
899
|
+
emailRecipientFields,
|
|
900
|
+
emailSubjectFields,
|
|
736
901
|
sharedDomains,
|
|
737
902
|
isCompanyDomain,
|
|
738
903
|
felsparVerifier,
|