@skill-harness/core 0.7.0 → 0.9.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/adapters/types.d.ts +8 -0
- package/dist/adjudication.d.ts +1 -0
- package/dist/adjudication.js +26 -4
- package/dist/capture-trace-types.d.ts +19 -0
- package/dist/comparison.d.ts +96 -0
- package/dist/comparison.js +183 -0
- package/dist/execution-trace.js +93 -3
- package/dist/index.d.ts +4 -0
- package/dist/index.js +4 -0
- package/dist/lint.d.ts +7 -0
- package/dist/lint.js +14 -2
- package/dist/regate.d.ts +1 -1
- package/dist/regate.js +94 -21
- package/dist/regrade.d.ts +4 -0
- package/dist/regrade.js +29 -5
- package/dist/report.d.ts +7 -0
- package/dist/report.js +6 -0
- package/dist/reps.d.ts +8 -1
- package/dist/reps.js +47 -13
- package/dist/rescore.js +2 -2
- package/dist/restamp.d.ts +68 -0
- package/dist/restamp.js +194 -0
- package/dist/results.d.ts +28 -0
- package/dist/results.js +56 -4
- package/dist/run.js +96 -35
- package/dist/sandbox.d.ts +32 -0
- package/dist/sandbox.js +12 -0
- package/dist/score.d.ts +3 -1
- package/dist/score.js +20 -5
- package/dist/seeded.d.ts +7 -1
- package/dist/seeded.js +29 -8
- package/dist/sources.d.ts +50 -0
- package/dist/sources.js +186 -7
- package/dist/spec.d.ts +15 -0
- package/dist/spec.js +53 -1
- package/dist/stability.js +26 -4
- package/dist/trace-gates.d.ts +2 -1
- package/dist/trace-gates.js +4 -1
- package/dist/trajectory-gates.d.ts +137 -0
- package/dist/trajectory-gates.js +592 -0
- package/package.json +5 -3
- package/schemas/results-v2.schema.json +133 -0
- package/schemas/specification-v1.schema.json +217 -0
- package/schemas/trajectory-event-v1.schema.json +61 -0
|
@@ -0,0 +1,592 @@
|
|
|
1
|
+
import { createHash } from "node:crypto";
|
|
2
|
+
import { parsePredicate, testPredicate } from "./trace-gates.js";
|
|
3
|
+
/** Adapter-neutral event contract used by objective trajectory assertions. */
|
|
4
|
+
export const TRAJECTORY_EVENT_VERSION = "1.0";
|
|
5
|
+
export const TRAJECTORY_ASSERT_VERSION = "1.0";
|
|
6
|
+
export function trajectoryEventsSha256(events) {
|
|
7
|
+
return createHash("sha256").update(stableStringify(events)).digest("hex");
|
|
8
|
+
}
|
|
9
|
+
export function serializeTrajectoryEvents(events) {
|
|
10
|
+
return events.map((event) => JSON.stringify(event)).join("\n") + (events.length ? "\n" : "");
|
|
11
|
+
}
|
|
12
|
+
export function deserializeTrajectoryEvents(text) {
|
|
13
|
+
const out = [];
|
|
14
|
+
try {
|
|
15
|
+
for (const line of text.split("\n").filter((entry) => entry.trim())) {
|
|
16
|
+
const event = JSON.parse(line);
|
|
17
|
+
if (validateEvent(event) !== null)
|
|
18
|
+
return null;
|
|
19
|
+
out.push(event);
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
catch {
|
|
23
|
+
return null;
|
|
24
|
+
}
|
|
25
|
+
if (!out.length)
|
|
26
|
+
return null;
|
|
27
|
+
const sequences = out.map((event) => event.seq).sort((a, b) => a - b);
|
|
28
|
+
if (new Set(sequences).size !== out.length || sequences.some((seq, index) => seq !== index + 1))
|
|
29
|
+
return null;
|
|
30
|
+
return out;
|
|
31
|
+
}
|
|
32
|
+
/** Evaluate a closed, declarative assertion set over saved normalized events. */
|
|
33
|
+
export function evaluateTrajectoryGates(assert, input) {
|
|
34
|
+
const assertions = [];
|
|
35
|
+
const events = [...input].sort((a, b) => a.seq - b.seq);
|
|
36
|
+
const invalid = input.map((event) => ({ event, problem: validateEvent(event) })).find((entry) => entry.problem !== null);
|
|
37
|
+
const sequences = [...input.map((event) => event.seq)].sort((a, b) => a - b);
|
|
38
|
+
const duplicateSeq = new Set(sequences).size !== input.length;
|
|
39
|
+
const nonContiguous = sequences.some((seq, index) => seq !== index + 1);
|
|
40
|
+
if (invalid || duplicateSeq || nonContiguous) {
|
|
41
|
+
assertions.push({
|
|
42
|
+
kind: "evidence",
|
|
43
|
+
status: "ERROR",
|
|
44
|
+
detail: invalid
|
|
45
|
+
? `normalized event evidence is invalid at sequence ${String(invalid.event.seq)}: ${invalid.problem}`
|
|
46
|
+
: duplicateSeq
|
|
47
|
+
? "normalized event evidence has duplicate sequence numbers"
|
|
48
|
+
: "normalized event evidence sequence must be contiguous from 1",
|
|
49
|
+
});
|
|
50
|
+
}
|
|
51
|
+
for (const required of assert.require ?? []) {
|
|
52
|
+
const hits = matching(events, required);
|
|
53
|
+
const min = required.count?.min ?? 1;
|
|
54
|
+
const max = required.count?.max;
|
|
55
|
+
const ok = hits.length >= min && (max === undefined || hits.length <= max);
|
|
56
|
+
assertions.push({
|
|
57
|
+
kind: "require_event",
|
|
58
|
+
status: ok ? "PASS" : "FAIL",
|
|
59
|
+
detail: ok
|
|
60
|
+
? `required event \`${required.event}\` occurred ${hits.length} time(s)`
|
|
61
|
+
: `required event \`${required.event}\` expected ${bounds(min, max)}, saw ${hits.length}`,
|
|
62
|
+
});
|
|
63
|
+
}
|
|
64
|
+
for (const forbidden of assert.forbid ?? []) {
|
|
65
|
+
const hits = matching(events, forbidden);
|
|
66
|
+
assertions.push({
|
|
67
|
+
kind: "forbid_event",
|
|
68
|
+
status: hits.length ? "FAIL" : "PASS",
|
|
69
|
+
detail: hits.length
|
|
70
|
+
? `forbidden event \`${forbidden.event}\` occurred at sequence(s) ${hits.map((event) => event.seq).join(", ")}`
|
|
71
|
+
: `forbidden event \`${forbidden.event}\` did not occur`,
|
|
72
|
+
});
|
|
73
|
+
}
|
|
74
|
+
for (const chain of assert.ordered ?? []) {
|
|
75
|
+
let cursor = -Infinity;
|
|
76
|
+
const found = [];
|
|
77
|
+
for (const selector of chain) {
|
|
78
|
+
const next = matching(events, selector).find((event) => event.seq > cursor);
|
|
79
|
+
if (!next)
|
|
80
|
+
break;
|
|
81
|
+
found.push(next);
|
|
82
|
+
cursor = next.seq;
|
|
83
|
+
}
|
|
84
|
+
const ok = found.length === chain.length;
|
|
85
|
+
assertions.push({
|
|
86
|
+
kind: "ordered_events",
|
|
87
|
+
status: ok ? "PASS" : "FAIL",
|
|
88
|
+
detail: ok
|
|
89
|
+
? `ordered events occurred at sequences ${found.map((event) => event.seq).join(" < ")}`
|
|
90
|
+
: `ordered trajectory broke at \`${chain[found.length]?.event ?? "unknown"}\` after sequence ${cursor === -Infinity ? "start" : cursor}`,
|
|
91
|
+
});
|
|
92
|
+
}
|
|
93
|
+
for (const correlation of assert.correlate ?? []) {
|
|
94
|
+
const left = selected(events, correlation.left);
|
|
95
|
+
const right = selected(events, correlation.right);
|
|
96
|
+
if (!left.length || !right.length) {
|
|
97
|
+
assertions.push({ kind: "correlation", status: "FAIL", detail: `correlation needs \`${correlation.left.event}\` and \`${correlation.right.event}\`` });
|
|
98
|
+
continue;
|
|
99
|
+
}
|
|
100
|
+
for (const l of left)
|
|
101
|
+
for (const r of right) {
|
|
102
|
+
const checked = compareFields(l, r, correlation.same ?? [], correlation.different ?? []);
|
|
103
|
+
if (checked.error) {
|
|
104
|
+
assertions.push({ kind: "correlation", status: "ERROR", detail: checked.error });
|
|
105
|
+
continue;
|
|
106
|
+
}
|
|
107
|
+
const ordered = correlation.order === undefined || (correlation.order === "before" ? l.seq < r.seq : l.seq > r.seq);
|
|
108
|
+
assertions.push({
|
|
109
|
+
kind: "correlation",
|
|
110
|
+
status: checked.ok && ordered ? "PASS" : "FAIL",
|
|
111
|
+
detail: checked.ok && ordered
|
|
112
|
+
? `\`${correlation.left.event}\` and \`${correlation.right.event}\` satisfy identity/order correlation`
|
|
113
|
+
: `\`${correlation.left.event}\` and \`${correlation.right.event}\` violate ${!checked.ok ? "identity" : `${correlation.order} ordering`} correlation`,
|
|
114
|
+
});
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
for (const freshness of assert.freshness ?? []) {
|
|
118
|
+
const subjects = selected(events, freshness.subject);
|
|
119
|
+
if (!subjects.length) {
|
|
120
|
+
assertions.push({ kind: "freshness", status: "FAIL", detail: `freshness subject \`${freshness.subject.event}\` is missing` });
|
|
121
|
+
continue;
|
|
122
|
+
}
|
|
123
|
+
for (const subject of subjects) {
|
|
124
|
+
let floor = -Infinity;
|
|
125
|
+
let failure = null;
|
|
126
|
+
let error = null;
|
|
127
|
+
for (const selector of freshness.after) {
|
|
128
|
+
const candidates = matching(events, selector);
|
|
129
|
+
const correlated = [];
|
|
130
|
+
for (const candidate of candidates) {
|
|
131
|
+
const checked = compareFields(subject, candidate, freshness.same ?? [], []);
|
|
132
|
+
if (checked.error)
|
|
133
|
+
error ??= checked.error;
|
|
134
|
+
else if (checked.ok)
|
|
135
|
+
correlated.push(candidate);
|
|
136
|
+
}
|
|
137
|
+
const anchor = correlated.at(-1);
|
|
138
|
+
if (!anchor)
|
|
139
|
+
failure = `freshness anchor \`${selector.event}\` is missing for the correlated identity`;
|
|
140
|
+
else
|
|
141
|
+
floor = Math.max(floor, anchor.seq);
|
|
142
|
+
}
|
|
143
|
+
assertions.push(error
|
|
144
|
+
? { kind: "freshness", status: "ERROR", detail: error }
|
|
145
|
+
: failure
|
|
146
|
+
? { kind: "freshness", status: "FAIL", detail: failure }
|
|
147
|
+
: subject.seq > floor
|
|
148
|
+
? { kind: "freshness", status: "PASS", detail: `\`${freshness.subject.event}\` at ${subject.seq} is newer than freshness floor ${floor}` }
|
|
149
|
+
: { kind: "freshness", status: "FAIL", detail: `\`${freshness.subject.event}\` at ${subject.seq} is stale; freshness floor is ${floor}` });
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
for (const uniqueness of assert.unique ?? []) {
|
|
153
|
+
const hits = matching(events, uniqueness.events);
|
|
154
|
+
for (const field of uniqueness.fields) {
|
|
155
|
+
const values = hits.map((event) => fieldValue(event, field));
|
|
156
|
+
const missing = values.findIndex((value) => value === undefined || value === null || value === "");
|
|
157
|
+
if (missing >= 0) {
|
|
158
|
+
assertions.push({ kind: "unique", status: "ERROR", detail: `uniqueness field \`${field}\` is missing on \`${uniqueness.events.event}\` at sequence ${hits[missing].seq}` });
|
|
159
|
+
}
|
|
160
|
+
else {
|
|
161
|
+
const duplicates = values.filter((value, index) => values.findIndex((other) => deepEqual(value, other)) !== index);
|
|
162
|
+
assertions.push({
|
|
163
|
+
kind: "unique",
|
|
164
|
+
status: duplicates.length ? "FAIL" : "PASS",
|
|
165
|
+
detail: duplicates.length ? `\`${field}\` was reused across independent \`${uniqueness.events.event}\` events` : `\`${field}\` is unique across ${hits.length} event(s)`,
|
|
166
|
+
});
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
for (const rule of assert.forbid_after ?? []) {
|
|
171
|
+
const anchors = selected(events, rule.anchor);
|
|
172
|
+
if (!anchors.length) {
|
|
173
|
+
assertions.push({
|
|
174
|
+
kind: "forbid_after",
|
|
175
|
+
status: rule.anchor_optional ? "PASS" : "FAIL",
|
|
176
|
+
detail: rule.anchor_optional ? `optional anchor \`${rule.anchor.event}\` did not occur` : `anchor \`${rule.anchor.event}\` is missing`,
|
|
177
|
+
});
|
|
178
|
+
continue;
|
|
179
|
+
}
|
|
180
|
+
for (const anchor of anchors) {
|
|
181
|
+
let violations = 0;
|
|
182
|
+
let error = null;
|
|
183
|
+
for (const selector of rule.forbidden) {
|
|
184
|
+
for (const candidate of matching(events, selector).filter((event) => event.seq > anchor.seq)) {
|
|
185
|
+
const checked = compareFields(anchor, candidate, rule.same ?? [], []);
|
|
186
|
+
if (checked.error)
|
|
187
|
+
error ??= checked.error;
|
|
188
|
+
else if (checked.ok)
|
|
189
|
+
violations++;
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
assertions.push(error
|
|
193
|
+
? { kind: "forbid_after", status: "ERROR", detail: error }
|
|
194
|
+
: {
|
|
195
|
+
kind: "forbid_after",
|
|
196
|
+
status: violations ? "FAIL" : "PASS",
|
|
197
|
+
detail: violations
|
|
198
|
+
? `${violations} forbidden mutation event(s) occurred after \`${rule.anchor.event}\` for the same identity`
|
|
199
|
+
: `no forbidden mutation followed \`${rule.anchor.event}\``,
|
|
200
|
+
});
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
for (const approval of assert.approvals ?? []) {
|
|
204
|
+
const grants = selected(events, approval.grant);
|
|
205
|
+
const uses = approval.use.select ? selected(events, approval.use) : matching(events, approval.use);
|
|
206
|
+
if (!grants.length || !uses.length) {
|
|
207
|
+
assertions.push({ kind: "approval", status: "FAIL", detail: "approval grant/use evidence is incomplete" });
|
|
208
|
+
continue;
|
|
209
|
+
}
|
|
210
|
+
for (const grant of grants) {
|
|
211
|
+
const matchingUses = [];
|
|
212
|
+
let error = null;
|
|
213
|
+
let usedBeforeGrant = false;
|
|
214
|
+
const identityFields = [...new Set([
|
|
215
|
+
...(approval.same ?? []),
|
|
216
|
+
"approval.id",
|
|
217
|
+
"approval.capability",
|
|
218
|
+
...(approval.scopes ? ["approval.scope"] : []),
|
|
219
|
+
...(approval.sources ? ["approval.source"] : []),
|
|
220
|
+
])];
|
|
221
|
+
for (const use of uses) {
|
|
222
|
+
const checked = compareFields(grant, use, identityFields, []);
|
|
223
|
+
if (checked.error)
|
|
224
|
+
error ??= checked.error;
|
|
225
|
+
else if (checked.ok && use.seq <= grant.seq)
|
|
226
|
+
usedBeforeGrant = true;
|
|
227
|
+
else if (checked.ok)
|
|
228
|
+
matchingUses.push(use);
|
|
229
|
+
}
|
|
230
|
+
const scope = grant.approval?.scope;
|
|
231
|
+
const source = grant.approval?.source;
|
|
232
|
+
if (error) {
|
|
233
|
+
assertions.push({ kind: "approval", status: "ERROR", detail: error });
|
|
234
|
+
continue;
|
|
235
|
+
}
|
|
236
|
+
if (approval.scopes && (!scope || !approval.scopes.includes(scope))) {
|
|
237
|
+
assertions.push({ kind: "approval", status: scope ? "FAIL" : "ERROR", detail: scope ? `approval scope \`${scope}\` is not allowed` : "approval scope is missing" });
|
|
238
|
+
continue;
|
|
239
|
+
}
|
|
240
|
+
if (approval.sources && (!source || !approval.sources.includes(source))) {
|
|
241
|
+
assertions.push({ kind: "approval", status: source ? "FAIL" : "ERROR", detail: source ? `approval source \`${source}\` is not allowed` : "approval source is missing" });
|
|
242
|
+
continue;
|
|
243
|
+
}
|
|
244
|
+
if (usedBeforeGrant) {
|
|
245
|
+
assertions.push({ kind: "approval", status: "FAIL", detail: "approval was used before its grant event" });
|
|
246
|
+
continue;
|
|
247
|
+
}
|
|
248
|
+
if (matchingUses.length === 0) {
|
|
249
|
+
assertions.push({ kind: "approval", status: "FAIL", detail: "no approval use matches the granted scope/identity" });
|
|
250
|
+
continue;
|
|
251
|
+
}
|
|
252
|
+
if (approval.max_uses !== undefined && matchingUses.length > approval.max_uses) {
|
|
253
|
+
assertions.push({ kind: "approval", status: "FAIL", detail: `approval was used ${matchingUses.length} times (max ${approval.max_uses})` });
|
|
254
|
+
continue;
|
|
255
|
+
}
|
|
256
|
+
if (approval.unexpired) {
|
|
257
|
+
const approved = grant.approval?.approved_at ?? grant.at;
|
|
258
|
+
const expires = grant.approval?.expires_at;
|
|
259
|
+
const timestampsInvalid = !approved || !expires || !validDate(approved) || !validDate(expires) ||
|
|
260
|
+
matchingUses.some((use) => !validDate(use.approval?.used_at ?? use.at));
|
|
261
|
+
if (timestampsInvalid) {
|
|
262
|
+
assertions.push({ kind: "approval", status: "ERROR", detail: "approval grant/expiry/use timestamp is missing or invalid" });
|
|
263
|
+
continue;
|
|
264
|
+
}
|
|
265
|
+
const invalidUse = matchingUses.find((use) => {
|
|
266
|
+
const used = use.approval?.used_at ?? use.at;
|
|
267
|
+
return Date.parse(used) < Date.parse(approved) || Date.parse(used) >= Date.parse(expires);
|
|
268
|
+
});
|
|
269
|
+
if (Date.parse(approved) >= Date.parse(expires) || invalidUse) {
|
|
270
|
+
assertions.push({ kind: "approval", status: "FAIL", detail: "approval was used outside its grant/expiry interval" });
|
|
271
|
+
continue;
|
|
272
|
+
}
|
|
273
|
+
}
|
|
274
|
+
assertions.push({ kind: "approval", status: "PASS", detail: `approval was used ${matchingUses.length} time(s) within declared scope and expiry` });
|
|
275
|
+
}
|
|
276
|
+
}
|
|
277
|
+
for (const coverage of assert.coverage ?? []) {
|
|
278
|
+
const hits = coverage.events ? matching(events, coverage.events) : events;
|
|
279
|
+
const covered = new Set(hits.flatMap((event) => event.requirements ?? []));
|
|
280
|
+
const missing = coverage.requirements.filter((requirement) => !covered.has(requirement));
|
|
281
|
+
assertions.push({
|
|
282
|
+
kind: "coverage",
|
|
283
|
+
status: missing.length ? "FAIL" : "PASS",
|
|
284
|
+
detail: missing.length ? `missing requirement coverage: ${missing.join(", ")}` : `requirement coverage recorded: ${coverage.requirements.join(", ")}`,
|
|
285
|
+
});
|
|
286
|
+
}
|
|
287
|
+
return {
|
|
288
|
+
status: assertions.some((result) => result.status === "ERROR") ? "ERROR" : assertions.some((result) => result.status === "FAIL") ? "FAIL" : "PASS",
|
|
289
|
+
event_version: TRAJECTORY_EVENT_VERSION,
|
|
290
|
+
events_sha256: trajectoryEventsSha256(events),
|
|
291
|
+
assertions,
|
|
292
|
+
};
|
|
293
|
+
}
|
|
294
|
+
// ---------------------------------------------------------------------------
|
|
295
|
+
// Parsing
|
|
296
|
+
// ---------------------------------------------------------------------------
|
|
297
|
+
export function parseTrajectoryAssert(raw, ctx) {
|
|
298
|
+
const object = asObject(raw, `${ctx}: \`assert.trajectory\``);
|
|
299
|
+
const allowed = new Set(["version", "require", "forbid", "ordered", "correlate", "freshness", "unique", "forbid_after", "approvals", "coverage"]);
|
|
300
|
+
rejectUnknown(object, allowed, `${ctx}: \`assert.trajectory\``);
|
|
301
|
+
if (object.version !== TRAJECTORY_ASSERT_VERSION)
|
|
302
|
+
throw new Error(`${ctx}: \`assert.trajectory.version\` must be ${TRAJECTORY_ASSERT_VERSION}`);
|
|
303
|
+
const out = { version: TRAJECTORY_ASSERT_VERSION };
|
|
304
|
+
if (object.require !== undefined)
|
|
305
|
+
out.require = nonEmptyArray(object.require, `${ctx}: require`).map((value, index) => parseRequired(value, `${ctx}: require[${index}]`));
|
|
306
|
+
if (object.forbid !== undefined)
|
|
307
|
+
out.forbid = nonEmptyArray(object.forbid, `${ctx}: forbid`).map((value, index) => parseSelector(value, `${ctx}: forbid[${index}]`));
|
|
308
|
+
if (object.ordered !== undefined)
|
|
309
|
+
out.ordered = nonEmptyArray(object.ordered, `${ctx}: ordered`).map((chain, index) => nonEmptyArray(chain, `${ctx}: ordered[${index}]`).map((value, step) => parseSelector(value, `${ctx}: ordered[${index}][${step}]`)));
|
|
310
|
+
if (object.correlate !== undefined)
|
|
311
|
+
out.correlate = nonEmptyArray(object.correlate, `${ctx}: correlate`).map((value, index) => parseCorrelation(value, `${ctx}: correlate[${index}]`));
|
|
312
|
+
if (object.freshness !== undefined)
|
|
313
|
+
out.freshness = nonEmptyArray(object.freshness, `${ctx}: freshness`).map((value, index) => parseFreshness(value, `${ctx}: freshness[${index}]`));
|
|
314
|
+
if (object.unique !== undefined)
|
|
315
|
+
out.unique = nonEmptyArray(object.unique, `${ctx}: unique`).map((value, index) => parseUnique(value, `${ctx}: unique[${index}]`));
|
|
316
|
+
if (object.forbid_after !== undefined)
|
|
317
|
+
out.forbid_after = nonEmptyArray(object.forbid_after, `${ctx}: forbid_after`).map((value, index) => parseForbidAfter(value, `${ctx}: forbid_after[${index}]`));
|
|
318
|
+
if (object.approvals !== undefined)
|
|
319
|
+
out.approvals = nonEmptyArray(object.approvals, `${ctx}: approvals`).map((value, index) => parseApproval(value, `${ctx}: approvals[${index}]`));
|
|
320
|
+
if (object.coverage !== undefined)
|
|
321
|
+
out.coverage = nonEmptyArray(object.coverage, `${ctx}: coverage`).map((value, index) => parseCoverage(value, `${ctx}: coverage[${index}]`));
|
|
322
|
+
if (Object.keys(out).length === 1)
|
|
323
|
+
throw new Error(`${ctx}: \`assert.trajectory\` declares no assertions`);
|
|
324
|
+
return out;
|
|
325
|
+
}
|
|
326
|
+
function parseRequired(raw, ctx) {
|
|
327
|
+
const object = asObject(raw, ctx);
|
|
328
|
+
rejectUnknown(object, new Set(["event", "where", "select", "count"]), ctx);
|
|
329
|
+
const out = parseSelector({ event: object.event, ...(object.where === undefined ? {} : { where: object.where }), ...(object.select === undefined ? {} : { select: object.select }) }, ctx);
|
|
330
|
+
if (object.count !== undefined)
|
|
331
|
+
out.count = parseCount(object.count, `${ctx}.count`);
|
|
332
|
+
return out;
|
|
333
|
+
}
|
|
334
|
+
function parseSelector(raw, ctx) {
|
|
335
|
+
const object = asObject(raw, ctx);
|
|
336
|
+
rejectUnknown(object, new Set(["event", "where", "select"]), ctx);
|
|
337
|
+
if (typeof object.event !== "string" || !object.event.trim())
|
|
338
|
+
throw new Error(`${ctx}.event must be a non-empty string`);
|
|
339
|
+
const out = { event: object.event };
|
|
340
|
+
if (object.where !== undefined) {
|
|
341
|
+
const where = asObject(object.where, `${ctx}.where`);
|
|
342
|
+
out.where = Object.fromEntries(Object.entries(where).map(([field, value]) => [field, parsePredicate(value, `${ctx}.where.${field}`)]));
|
|
343
|
+
}
|
|
344
|
+
if (object.select !== undefined) {
|
|
345
|
+
if (!["first", "last", "all"].includes(object.select))
|
|
346
|
+
throw new Error(`${ctx}.select must be first, last, or all`);
|
|
347
|
+
out.select = object.select;
|
|
348
|
+
}
|
|
349
|
+
return out;
|
|
350
|
+
}
|
|
351
|
+
function parseCorrelation(raw, ctx) {
|
|
352
|
+
const object = asObject(raw, ctx);
|
|
353
|
+
rejectUnknown(object, new Set(["left", "right", "same", "different", "order"]), ctx);
|
|
354
|
+
const out = { left: parseSelector(object.left, `${ctx}.left`), right: parseSelector(object.right, `${ctx}.right`) };
|
|
355
|
+
if (object.same !== undefined)
|
|
356
|
+
out.same = stringList(object.same, `${ctx}.same`);
|
|
357
|
+
if (object.different !== undefined)
|
|
358
|
+
out.different = stringList(object.different, `${ctx}.different`);
|
|
359
|
+
if (object.order !== undefined) {
|
|
360
|
+
if (object.order !== "before" && object.order !== "after")
|
|
361
|
+
throw new Error(`${ctx}.order must be before or after`);
|
|
362
|
+
out.order = object.order;
|
|
363
|
+
}
|
|
364
|
+
if (!out.same?.length && !out.different?.length && !out.order)
|
|
365
|
+
throw new Error(`${ctx} declares no relation`);
|
|
366
|
+
return out;
|
|
367
|
+
}
|
|
368
|
+
function parseFreshness(raw, ctx) {
|
|
369
|
+
const object = asObject(raw, ctx);
|
|
370
|
+
rejectUnknown(object, new Set(["subject", "after", "same"]), ctx);
|
|
371
|
+
return {
|
|
372
|
+
subject: parseSelector(object.subject, `${ctx}.subject`),
|
|
373
|
+
after: nonEmptyArray(object.after, `${ctx}.after`).map((value, index) => parseSelector(value, `${ctx}.after[${index}]`)),
|
|
374
|
+
...(object.same === undefined ? {} : { same: stringList(object.same, `${ctx}.same`) }),
|
|
375
|
+
};
|
|
376
|
+
}
|
|
377
|
+
function parseUnique(raw, ctx) {
|
|
378
|
+
const object = asObject(raw, ctx);
|
|
379
|
+
rejectUnknown(object, new Set(["events", "fields"]), ctx);
|
|
380
|
+
return { events: parseSelector(object.events, `${ctx}.events`), fields: stringList(object.fields, `${ctx}.fields`) };
|
|
381
|
+
}
|
|
382
|
+
function parseForbidAfter(raw, ctx) {
|
|
383
|
+
const object = asObject(raw, ctx);
|
|
384
|
+
rejectUnknown(object, new Set(["anchor", "forbidden", "same", "anchor_optional"]), ctx);
|
|
385
|
+
if (object.anchor_optional !== undefined && typeof object.anchor_optional !== "boolean")
|
|
386
|
+
throw new Error(`${ctx}.anchor_optional must be boolean`);
|
|
387
|
+
return {
|
|
388
|
+
anchor: parseSelector(object.anchor, `${ctx}.anchor`),
|
|
389
|
+
forbidden: nonEmptyArray(object.forbidden, `${ctx}.forbidden`).map((value, index) => parseSelector(value, `${ctx}.forbidden[${index}]`)),
|
|
390
|
+
...(object.same === undefined ? {} : { same: stringList(object.same, `${ctx}.same`) }),
|
|
391
|
+
...(object.anchor_optional === undefined ? {} : { anchor_optional: object.anchor_optional }),
|
|
392
|
+
};
|
|
393
|
+
}
|
|
394
|
+
function parseApproval(raw, ctx) {
|
|
395
|
+
const object = asObject(raw, ctx);
|
|
396
|
+
rejectUnknown(object, new Set(["grant", "use", "same", "scopes", "sources", "unexpired", "max_uses"]), ctx);
|
|
397
|
+
if (object.unexpired !== undefined && typeof object.unexpired !== "boolean")
|
|
398
|
+
throw new Error(`${ctx}.unexpired must be boolean`);
|
|
399
|
+
if (object.max_uses !== undefined && (!Number.isInteger(object.max_uses) || Number(object.max_uses) < 1))
|
|
400
|
+
throw new Error(`${ctx}.max_uses must be a positive integer`);
|
|
401
|
+
return {
|
|
402
|
+
grant: parseSelector(object.grant, `${ctx}.grant`),
|
|
403
|
+
use: parseSelector(object.use, `${ctx}.use`),
|
|
404
|
+
...(object.same === undefined ? {} : { same: stringList(object.same, `${ctx}.same`) }),
|
|
405
|
+
...(object.scopes === undefined ? {} : { scopes: stringList(object.scopes, `${ctx}.scopes`) }),
|
|
406
|
+
...(object.sources === undefined ? {} : { sources: stringList(object.sources, `${ctx}.sources`) }),
|
|
407
|
+
...(object.unexpired === undefined ? {} : { unexpired: object.unexpired }),
|
|
408
|
+
...(object.max_uses === undefined ? {} : { max_uses: Number(object.max_uses) }),
|
|
409
|
+
};
|
|
410
|
+
}
|
|
411
|
+
function parseCoverage(raw, ctx) {
|
|
412
|
+
const object = asObject(raw, ctx);
|
|
413
|
+
rejectUnknown(object, new Set(["requirements", "events"]), ctx);
|
|
414
|
+
return { requirements: stringList(object.requirements, `${ctx}.requirements`), ...(object.events === undefined ? {} : { events: parseSelector(object.events, `${ctx}.events`) }) };
|
|
415
|
+
}
|
|
416
|
+
function parseCount(raw, ctx) {
|
|
417
|
+
const object = asObject(raw, ctx);
|
|
418
|
+
rejectUnknown(object, new Set(["min", "max"]), ctx);
|
|
419
|
+
const out = {};
|
|
420
|
+
for (const key of ["min", "max"]) {
|
|
421
|
+
if (object[key] === undefined)
|
|
422
|
+
continue;
|
|
423
|
+
if (!Number.isInteger(object[key]) || Number(object[key]) < 0)
|
|
424
|
+
throw new Error(`${ctx}.${key} must be a non-negative integer`);
|
|
425
|
+
out[key] = Number(object[key]);
|
|
426
|
+
}
|
|
427
|
+
if (out.min !== undefined && out.max !== undefined && out.min > out.max)
|
|
428
|
+
throw new Error(`${ctx}.min exceeds max`);
|
|
429
|
+
return out;
|
|
430
|
+
}
|
|
431
|
+
/** Fast, deterministic proof that every trajectory assertion class can turn red. */
|
|
432
|
+
export function runTrajectoryMutationSelfTest() {
|
|
433
|
+
const e = (seq, type, extra = {}) => ({ event_version: TRAJECTORY_EVENT_VERSION, seq, type, source: "mutation-self-test", at: `2026-01-01T00:00:${String(seq).padStart(2, "0")}Z`, ...extra });
|
|
434
|
+
const h = "a".repeat(40), t = "b".repeat(40), p = "1".repeat(64);
|
|
435
|
+
const cases = [
|
|
436
|
+
{ id: "remove-required-event", assertion: { version: "1.0", require: [{ event: "risk_classified" }] }, good: [e(1, "risk_classified")], mutate: () => [e(1, "unrelated")] },
|
|
437
|
+
{ id: "add-forbidden-tool-side-effect", assertion: { version: "1.0", forbid: [{ event: "tool_called", where: { tool: { equals: "rm" } } }, { event: "side_effect_performed" }] }, good: [e(1, "tool_called", { tool: "read" })], mutate: (events) => [...events, e(2, "side_effect_performed", { attributes: { destructive: true } })] },
|
|
438
|
+
{ id: "reorder-transition", assertion: { version: "1.0", ordered: [[{ event: "phase_started" }, { event: "phase_completed" }]] }, good: [e(1, "phase_started"), e(2, "phase_completed")], mutate: () => [e(2, "phase_started"), e(1, "phase_completed")] },
|
|
439
|
+
{ id: "substitute-workspace-id", assertion: { version: "1.0", correlate: [{ left: { event: "code_changed" }, right: { event: "evidence_recorded" }, same: ["workspace_id"] }] }, good: [e(1, "code_changed", { workspace_id: "ws-1" }), e(2, "evidence_recorded", { workspace_id: "ws-1" })], mutate: (events) => events.map((x) => x.type === "evidence_recorded" ? { ...x, workspace_id: "ws-2" } : x) },
|
|
440
|
+
{ id: "concurrent-writer", assertion: { version: "1.0", forbid: [{ event: "writer_lease_conflict" }] }, good: [e(1, "writer_lease_acquired", { workspace_id: "ws-1" })], mutate: (events) => [...events, e(2, "writer_lease_conflict", { workspace_id: "ws-1" })] },
|
|
441
|
+
{ id: "approval-expired-or-mismatched", assertion: { version: "1.0", approvals: [{ grant: { event: "approval_granted" }, use: { event: "approval_used" }, same: ["approval.id", "approval.capability"], unexpired: true }] }, good: [e(1, "approval_granted", { approval: { id: "a1", capability: "push", expires_at: "2026-01-01T01:00:00Z" } }), e(2, "approval_used", { approval: { id: "a1", capability: "push", used_at: "2026-01-01T00:01:00Z" } })], mutate: (events) => events.map((x) => x.type === "approval_used" ? { ...x, approval: { ...x.approval, used_at: "2026-01-01T02:00:00Z" } } : x) },
|
|
442
|
+
{ id: "evidence-before-change", assertion: { version: "1.0", freshness: [{ subject: { event: "evidence_recorded" }, after: [{ event: "code_changed" }] }] }, good: [e(1, "code_changed"), e(2, "evidence_recorded")], mutate: () => [e(2, "code_changed"), e(1, "evidence_recorded")] },
|
|
443
|
+
{ id: "evidence-before-authority", assertion: { version: "1.0", freshness: [{ subject: { event: "evidence_recorded" }, after: [{ event: "plan_recorded" }] }] }, good: [e(1, "plan_recorded", { digests: { plan: p } }), e(2, "evidence_recorded")], mutate: () => [e(2, "plan_recorded", { digests: { plan: p } }), e(1, "evidence_recorded")] },
|
|
444
|
+
{ id: "evidence-before-build-completion", assertion: { version: "1.0", freshness: [{ subject: { event: "evidence_recorded" }, after: [{ event: "phase_completed", where: { phase: { equals: "build" } } }] }] }, good: [e(1, "phase_completed", { phase: "build" }), e(2, "evidence_recorded")], mutate: () => [e(2, "phase_completed", { phase: "build" }), e(1, "evidence_recorded")] },
|
|
445
|
+
{ id: "head-equal-tree-different", assertion: { version: "1.0", correlate: [{ left: { event: "code_changed" }, right: { event: "evidence_recorded" }, same: ["digests.head", "digests.tree"] }] }, good: [e(1, "code_changed", { digests: { head: h, tree: t } }), e(2, "evidence_recorded", { digests: { head: h, tree: t } })], mutate: (events) => events.map((x) => x.type === "evidence_recorded" ? { ...x, digests: { head: h, tree: "c".repeat(40) } } : x) },
|
|
446
|
+
{ id: "command-receipt-nonzero", assertion: { version: "1.0", require: [{ event: "evidence_recorded", where: { exit_code: { equals: 0 } } }] }, good: [e(1, "evidence_recorded", { exit_code: 0 })], mutate: (events) => events.map((x) => ({ ...x, exit_code: 1 })) },
|
|
447
|
+
{ id: "remove-requirement-coverage", assertion: { version: "1.0", coverage: [{ requirements: ["AUTH-7"], events: { event: "evidence_recorded" } }] }, good: [e(1, "evidence_recorded", { requirements: ["AUTH-7"] })], mutate: (events) => events.map((x) => ({ ...x, requirements: [] })) },
|
|
448
|
+
{ id: "mutate-superseded-task", assertion: { version: "1.0", forbid_after: [{ anchor: { event: "task_packet_superseded" }, forbidden: [{ event: "code_changed" }, { event: "repair_started" }], same: ["task_id"] }] }, good: [e(1, "task_packet_superseded", { task_id: "old" }), e(2, "code_changed", { task_id: "new" })], mutate: (events) => events.map((x) => x.type === "code_changed" ? { ...x, task_id: "old" } : x) },
|
|
449
|
+
{ id: "reuse-context-id", assertion: { version: "1.0", unique: [{ events: { event: "review_recorded" }, fields: ["context_id"] }] }, good: [e(1, "review_recorded", { context_id: "c1" }), e(2, "review_recorded", { context_id: "c2" })], mutate: (events) => events.map((x) => x.seq === 2 ? { ...x, context_id: "c1" } : x) },
|
|
450
|
+
{ id: "mismatch-finalization-identity", assertion: { version: "1.0", correlate: [{ left: { event: "gate_evaluated", where: { "attributes.gate": { equals: "finalize" } } }, right: { event: "finalization_completed" }, same: ["digests.head", "digests.tree"], order: "before" }] }, good: [e(1, "gate_evaluated", { digests: { head: h, tree: t }, attributes: { gate: "finalize", result: "pass" } }), e(2, "finalization_completed", { digests: { head: h, tree: t } })], mutate: (events) => events.map((x) => x.type === "finalization_completed" ? { ...x, digests: { head: h, tree: "d".repeat(40) } } : x) },
|
|
451
|
+
];
|
|
452
|
+
const results = cases.map((testCase) => {
|
|
453
|
+
const baseline = evaluateTrajectoryGates(testCase.assertion, testCase.good);
|
|
454
|
+
if (baseline.status !== "PASS")
|
|
455
|
+
throw new Error(`mutation self-test baseline ${testCase.id} is ${baseline.status}`);
|
|
456
|
+
const mutated = evaluateTrajectoryGates(testCase.assertion, testCase.mutate(structuredClone(testCase.good)));
|
|
457
|
+
return { id: testCase.id, detected: mutated.status !== "PASS", status: mutated.status, detail: mutated.assertions.find((result) => result.status !== "PASS")?.detail ?? "mutation was not detected" };
|
|
458
|
+
});
|
|
459
|
+
return { baseline: "PASS", cases: results };
|
|
460
|
+
}
|
|
461
|
+
// ---------------------------------------------------------------------------
|
|
462
|
+
// Internals
|
|
463
|
+
// ---------------------------------------------------------------------------
|
|
464
|
+
const EVENT_KEYS = new Set([
|
|
465
|
+
"event_version", "seq", "type", "source", "at", "run_id", "task_id", "workspace_id", "context_id", "finding_id",
|
|
466
|
+
"parent_id", "child_id", "phase", "tool", "capability", "requested_capabilities", "effective_capabilities", "refusal_code",
|
|
467
|
+
"exit_code", "digests", "approval", "requirements", "attributes",
|
|
468
|
+
]);
|
|
469
|
+
const APPROVAL_KEYS = new Set(["id", "capability", "subject", "source", "scope", "approved_at", "expires_at", "used_at"]);
|
|
470
|
+
const ID_RE = /^[A-Za-z0-9][A-Za-z0-9._:-]{0,127}$/;
|
|
471
|
+
const SHA256_RE = /^[a-fA-F0-9]{64}$/;
|
|
472
|
+
const GIT_SHA_RE = /^(?:[a-fA-F0-9]{40}|[a-fA-F0-9]{64})$/;
|
|
473
|
+
const REFUSAL_RE = /^[A-Z][A-Z0-9_]*$/;
|
|
474
|
+
/** Runtime counterpart of trajectory-event-v1.schema.json; evidence is rejected before any assertion can consume it. */
|
|
475
|
+
function validateEvent(event) {
|
|
476
|
+
if (!event || typeof event !== "object" || Array.isArray(event))
|
|
477
|
+
return "event must be an object";
|
|
478
|
+
const object = event;
|
|
479
|
+
const unknown = Object.keys(object).find((key) => !EVENT_KEYS.has(key));
|
|
480
|
+
if (unknown)
|
|
481
|
+
return `unknown field ${unknown}`;
|
|
482
|
+
if (event.event_version !== TRAJECTORY_EVENT_VERSION)
|
|
483
|
+
return `unsupported event_version ${String(event.event_version)}`;
|
|
484
|
+
if (!Number.isInteger(event.seq) || event.seq < 1)
|
|
485
|
+
return "seq must be a positive integer";
|
|
486
|
+
if (typeof event.type !== "string" || !event.type)
|
|
487
|
+
return "type must be a non-empty string";
|
|
488
|
+
if (typeof event.source !== "string" || !event.source)
|
|
489
|
+
return "source must be a non-empty string";
|
|
490
|
+
if (event.at !== undefined && !validDate(event.at))
|
|
491
|
+
return "at must be an RFC 3339 date-time";
|
|
492
|
+
for (const field of ["run_id", "task_id", "workspace_id", "context_id", "finding_id", "parent_id", "child_id"]) {
|
|
493
|
+
if (event[field] !== undefined && (typeof event[field] !== "string" || !ID_RE.test(event[field])))
|
|
494
|
+
return `${field} is not a valid bounded identifier`;
|
|
495
|
+
}
|
|
496
|
+
for (const field of ["phase", "tool", "capability"]) {
|
|
497
|
+
if (event[field] !== undefined && (typeof event[field] !== "string" || !event[field]))
|
|
498
|
+
return `${field} must be a non-empty string`;
|
|
499
|
+
}
|
|
500
|
+
for (const field of ["requested_capabilities", "effective_capabilities", "requirements"]) {
|
|
501
|
+
const values = event[field];
|
|
502
|
+
if (values !== undefined && (!Array.isArray(values) || values.some((value) => typeof value !== "string" || !value) || new Set(values).size !== values.length)) {
|
|
503
|
+
return `${field} must be an array of unique non-empty strings`;
|
|
504
|
+
}
|
|
505
|
+
}
|
|
506
|
+
if (event.refusal_code !== undefined && !REFUSAL_RE.test(event.refusal_code))
|
|
507
|
+
return "refusal_code is invalid";
|
|
508
|
+
if (event.exit_code !== undefined && !Number.isInteger(event.exit_code))
|
|
509
|
+
return "exit_code must be an integer";
|
|
510
|
+
if (event.digests !== undefined) {
|
|
511
|
+
if (!event.digests || typeof event.digests !== "object" || Array.isArray(event.digests))
|
|
512
|
+
return "digests must be an object";
|
|
513
|
+
for (const [key, value] of Object.entries(event.digests)) {
|
|
514
|
+
if (typeof value !== "string")
|
|
515
|
+
return `digests.${key} must be a string`;
|
|
516
|
+
if (["plan", "task", "definition"].includes(key) && !SHA256_RE.test(value))
|
|
517
|
+
return `digests.${key} must be sha256`;
|
|
518
|
+
if (["head", "tree"].includes(key) && !GIT_SHA_RE.test(value))
|
|
519
|
+
return `digests.${key} must be a git object id`;
|
|
520
|
+
}
|
|
521
|
+
}
|
|
522
|
+
if (event.approval !== undefined) {
|
|
523
|
+
if (!event.approval || typeof event.approval !== "object" || Array.isArray(event.approval))
|
|
524
|
+
return "approval must be an object";
|
|
525
|
+
const unknownApproval = Object.keys(event.approval).find((key) => !APPROVAL_KEYS.has(key));
|
|
526
|
+
if (unknownApproval)
|
|
527
|
+
return `approval.${unknownApproval} is unknown`;
|
|
528
|
+
for (const [key, value] of Object.entries(event.approval)) {
|
|
529
|
+
if (typeof value !== "string" || !value)
|
|
530
|
+
return `approval.${key} must be a non-empty string`;
|
|
531
|
+
if (key === "id" && !ID_RE.test(value))
|
|
532
|
+
return "approval.id is invalid";
|
|
533
|
+
if (["approved_at", "expires_at", "used_at"].includes(key) && !validDate(value))
|
|
534
|
+
return `approval.${key} must be an RFC 3339 date-time`;
|
|
535
|
+
}
|
|
536
|
+
}
|
|
537
|
+
if (event.attributes !== undefined && (!event.attributes || typeof event.attributes !== "object" || Array.isArray(event.attributes)))
|
|
538
|
+
return "attributes must be an object";
|
|
539
|
+
return null;
|
|
540
|
+
}
|
|
541
|
+
function matching(events, selector) {
|
|
542
|
+
return events.filter((event) => event.type === selector.event && Object.entries(selector.where ?? {}).every(([field, predicate]) => testPredicate(fieldValue(event, field), predicate)));
|
|
543
|
+
}
|
|
544
|
+
function selected(events, selector) {
|
|
545
|
+
const hits = matching(events, selector);
|
|
546
|
+
if (selector.select === "all")
|
|
547
|
+
return hits;
|
|
548
|
+
return hits.length ? [selector.select === "first" ? hits[0] : hits[hits.length - 1]] : [];
|
|
549
|
+
}
|
|
550
|
+
function fieldValue(event, field) {
|
|
551
|
+
let current = event;
|
|
552
|
+
for (const part of field.split(".")) {
|
|
553
|
+
if (!current || typeof current !== "object" || Array.isArray(current) || !Object.hasOwn(current, part))
|
|
554
|
+
return undefined;
|
|
555
|
+
current = current[part];
|
|
556
|
+
}
|
|
557
|
+
return current;
|
|
558
|
+
}
|
|
559
|
+
function compareFields(left, right, same, different) {
|
|
560
|
+
for (const field of [...same, ...different]) {
|
|
561
|
+
const l = fieldValue(left, field), r = fieldValue(right, field);
|
|
562
|
+
if (l === undefined || l === null || r === undefined || r === null)
|
|
563
|
+
return { ok: false, error: `correlation field \`${field}\` is missing at sequence ${l === undefined || l === null ? left.seq : right.seq}` };
|
|
564
|
+
}
|
|
565
|
+
return { ok: same.every((field) => deepEqual(fieldValue(left, field), fieldValue(right, field))) && different.every((field) => !deepEqual(fieldValue(left, field), fieldValue(right, field))) };
|
|
566
|
+
}
|
|
567
|
+
function deepEqual(left, right) { return JSON.stringify(left) === JSON.stringify(right); }
|
|
568
|
+
function validDate(value) {
|
|
569
|
+
if (typeof value !== "string")
|
|
570
|
+
return false;
|
|
571
|
+
const match = /^(\d{4})-(\d{2})-(\d{2})T(\d{2}):(\d{2}):(\d{2})(?:\.\d+)?(?:Z|[+-]\d{2}:\d{2})$/.exec(value);
|
|
572
|
+
if (!match || !Number.isFinite(Date.parse(value)))
|
|
573
|
+
return false;
|
|
574
|
+
const [, year, month, day, hour, minute, second] = match.map(Number);
|
|
575
|
+
if (month < 1 || month > 12 || hour > 23 || minute > 59 || second > 59)
|
|
576
|
+
return false;
|
|
577
|
+
return day >= 1 && day <= new Date(Date.UTC(year, month, 0)).getUTCDate();
|
|
578
|
+
}
|
|
579
|
+
function bounds(min, max) { return max === undefined ? `at least ${min}` : min === 0 ? `at most ${max}` : `${min}..${max}`; }
|
|
580
|
+
function asObject(value, ctx) { if (!value || typeof value !== "object" || Array.isArray(value))
|
|
581
|
+
throw new Error(`${ctx} must be a mapping`); return value; }
|
|
582
|
+
function nonEmptyArray(value, ctx) { if (!Array.isArray(value) || value.length === 0)
|
|
583
|
+
throw new Error(`${ctx} must be a non-empty list`); return value; }
|
|
584
|
+
function stringList(value, ctx) { const values = nonEmptyArray(value, ctx); if (values.some((item) => typeof item !== "string" || !item.trim()))
|
|
585
|
+
throw new Error(`${ctx} must contain non-empty strings`); if (new Set(values).size !== values.length)
|
|
586
|
+
throw new Error(`${ctx} must not contain duplicates`); return values; }
|
|
587
|
+
function rejectUnknown(object, allowed, ctx) { const unknown = Object.keys(object).find((key) => !allowed.has(key)); if (unknown)
|
|
588
|
+
throw new Error(`${ctx}: unknown key \`${unknown}\``); }
|
|
589
|
+
function stableStringify(value) { if (value === null || typeof value !== "object")
|
|
590
|
+
return JSON.stringify(value) ?? "null"; if (Array.isArray(value))
|
|
591
|
+
return `[${value.map(stableStringify).join(",")}]`; return `{${Object.entries(value).filter(([, entry]) => entry !== undefined).sort(([a], [b]) => a.localeCompare(b)).map(([key, entry]) => `${JSON.stringify(key)}:${stableStringify(entry)}`).join(",")}}`; }
|
|
592
|
+
//# sourceMappingURL=trajectory-gates.js.map
|
package/package.json
CHANGED
|
@@ -1,13 +1,14 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@skill-harness/core",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.9.0",
|
|
4
4
|
"description": "skill-harness engine — spec, discover, run, LLM-judge grade, score, results (internal API)",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "MIT",
|
|
7
7
|
"main": "./dist/index.js",
|
|
8
8
|
"types": "./dist/index.d.ts",
|
|
9
9
|
"exports": {
|
|
10
|
-
".": "./dist/index.js"
|
|
10
|
+
".": "./dist/index.js",
|
|
11
|
+
"./schemas/*.json": "./schemas/*.json"
|
|
11
12
|
},
|
|
12
13
|
"keywords": [
|
|
13
14
|
"agent-skills",
|
|
@@ -23,6 +24,7 @@
|
|
|
23
24
|
"files": [
|
|
24
25
|
"dist/**/*.js",
|
|
25
26
|
"dist/**/*.d.ts",
|
|
27
|
+
"schemas/*.json",
|
|
26
28
|
"LICENSE",
|
|
27
29
|
"README.md"
|
|
28
30
|
],
|
|
@@ -37,7 +39,7 @@
|
|
|
37
39
|
"node": ">=20"
|
|
38
40
|
},
|
|
39
41
|
"scripts": {
|
|
40
|
-
"prepack": "cp ../../LICENSE ./LICENSE"
|
|
42
|
+
"prepack": "rm -rf ./schemas && cp -R ../../schemas ./schemas && cp ../../LICENSE ./LICENSE"
|
|
41
43
|
},
|
|
42
44
|
"dependencies": {
|
|
43
45
|
"js-yaml": "^4.1.0"
|