@skill-harness/core 0.8.0 → 0.10.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/seeded.js CHANGED
@@ -168,12 +168,15 @@ export async function runSeeded(scenario, opts) {
168
168
  cwd: repo,
169
169
  // Resolved against the spec dir, exactly like fixtures and post-tests.
170
170
  extensions: scenario.extensions?.map((e) => resolve(opts.specDir, e)),
171
+ eventSources: scenario.eventSources,
171
172
  };
172
173
  // A trace-gated seeded scenario runs through the structured path so the tool
173
174
  // calls are recorded; everything downstream (gates, diff, transcript) is
174
175
  // identical, because the rebuilt transcript is what print mode would have
175
176
  // emitted anyway.
176
177
  let traces = [];
178
+ let events = [];
179
+ let eventErrors = [];
177
180
  let harnessOut;
178
181
  if (opts.trace) {
179
182
  if (!opts.adapter.runStructured) {
@@ -187,12 +190,15 @@ export async function runSeeded(scenario, opts) {
187
190
  });
188
191
  harnessOut = structured.transcript;
189
192
  traces = structured.traces;
193
+ events = structured.events ?? [];
194
+ eventErrors = structured.eventErrors ?? [];
190
195
  }
191
196
  else {
192
197
  harnessOut = await opts.adapter.run(req);
193
198
  }
194
199
  const parts = [harnessOut, "", "=== SEEDED GATES ==="];
195
200
  let gateFailure = null;
201
+ let gateError = null;
196
202
  const runVitest = opts.runVitest ??
197
203
  ((args, cwd) => exec("npx", ["vitest", "run", ...args], { cwd, timeoutMs: VITEST_TIMEOUT_MS }));
198
204
  // `exec` never throws on a non-zero exit and, on timeout, SIGKILLs the child and
@@ -213,7 +219,8 @@ export async function runSeeded(scenario, opts) {
213
219
  (gitFailure.stderr.trim() ? `: ${gitFailure.stderr.trim().split("\n")[0]}` : "");
214
220
  parts.push(` staged diff: ERROR (${msg})`);
215
221
  gateFailure = msg;
216
- return finish(parts, gateFailure, diff, traces);
222
+ gateError = msg;
223
+ return finish(parts, gateFailure, gateError, diff, traces, events, eventErrors);
217
224
  }
218
225
  // BOTH needle gates read the changed lines only, never context. A unified diff
219
226
  // carries three lines of context per hunk, so an untouched symbol near the edit
@@ -241,10 +248,14 @@ export async function runSeeded(scenario, opts) {
241
248
  ? ` vitest run: ERROR (timed out after ${VITEST_TIMEOUT_MS}ms — infrastructure, not model behavior)`
242
249
  : ` vitest run: ${passed ? "PASS" : `FAIL (exit ${v.code})`}`);
243
250
  parts.push(indent(bothStreams(v)));
244
- if (!passed && !gateFailure) {
245
- gateFailure = killed
251
+ if (!passed) {
252
+ const problem = killed
246
253
  ? `vitest timed out after ${VITEST_TIMEOUT_MS}ms — infrastructure, not model behavior`
247
254
  : `vitest failed (exit ${v.code})`;
255
+ if (!gateFailure)
256
+ gateFailure = problem;
257
+ if (killed)
258
+ gateError = problem;
248
259
  }
249
260
  }
250
261
  const postTest = scenario.assert?.post_test;
@@ -263,6 +274,7 @@ export async function runSeeded(scenario, opts) {
263
274
  parts.push(` post_test: ERROR (${msg})`);
264
275
  if (!gateFailure)
265
276
  gateFailure = msg;
277
+ gateError = msg;
266
278
  }
267
279
  else {
268
280
  // `.test` is part of the name we build, never taken from the source: extname()
@@ -281,7 +293,8 @@ export async function runSeeded(scenario, opts) {
281
293
  parts.push(` post_test: ERROR (${msg})`);
282
294
  if (!gateFailure)
283
295
  gateFailure = msg;
284
- return finish(parts, gateFailure, diff, traces);
296
+ gateError = msg;
297
+ return finish(parts, gateFailure, gateError, diff, traces, events, eventErrors);
285
298
  }
286
299
  const v = await runVitest([POST_TEST_BASE], repo);
287
300
  const out = `${v.stdout}\n${v.stderr}`;
@@ -300,13 +313,17 @@ export async function runSeeded(scenario, opts) {
300
313
  const notCollected = /^\s*No test files found/im.test(out);
301
314
  const killed = v.code === null;
302
315
  let problem = null;
316
+ let problemIsError = false;
303
317
  if (killed) {
318
+ problemIsError = true;
304
319
  problem = `post_test ${JSON.stringify(postTest)} timed out after ${VITEST_TIMEOUT_MS}ms — infrastructure, not model behavior`;
305
320
  }
306
321
  else if (notCollected) {
322
+ problemIsError = true;
307
323
  problem = `post_test ${JSON.stringify(postTest)} was never collected by vitest — spec/fixture error, not model behavior`;
308
324
  }
309
325
  else if (tally === null) {
326
+ problemIsError = true;
310
327
  problem = `post_test ${JSON.stringify(postTest)} produced no parseable vitest summary (exit ${v.code}) — cannot confirm it ran`;
311
328
  }
312
329
  else if (v.code !== 0 || tally.failed > 0) {
@@ -317,20 +334,24 @@ export async function runSeeded(scenario, opts) {
317
334
  problem = `post_test ${JSON.stringify(postTest)} failed (exit ${v.code})`;
318
335
  }
319
336
  else if (tally.skipped > 0 || tally.todo > 0) {
337
+ problemIsError = true;
320
338
  problem = `post_test ${JSON.stringify(postTest)} has ${tally.skipped + tally.todo} skipped/todo test(s) — a hidden gate must actually run; spec error, not model behavior`;
321
339
  }
322
340
  else if (tally.passed === 0) {
341
+ problemIsError = true;
323
342
  problem = `post_test ${JSON.stringify(postTest)} ran no assertions — spec error, not model behavior`;
324
343
  }
325
344
  parts.push(problem === null
326
345
  ? ` post_test ${JSON.stringify(postTest)}: PASS (${tally.passed} assertion-bearing test(s))`
327
- : ` post_test ${JSON.stringify(postTest)}: ${v.code === 0 && !killed ? "ERROR" : "FAIL"} (${problem})`);
346
+ : ` post_test ${JSON.stringify(postTest)}: ${problemIsError ? "ERROR" : "FAIL"} (${problem})`);
328
347
  parts.push(indent(bothStreams(v)));
329
348
  if (problem && !gateFailure)
330
349
  gateFailure = problem;
350
+ if (problem && problemIsError)
351
+ gateError = problem;
331
352
  }
332
353
  }
333
- return finish(parts, gateFailure, diff, traces);
354
+ return finish(parts, gateFailure, gateError, diff, traces, events, eventErrors);
334
355
  }
335
356
  function git(cwd, args) {
336
357
  return exec("git", args, { cwd, timeoutMs: 30_000 });
@@ -364,13 +385,13 @@ function bothStreams(v) {
364
385
  return o || e;
365
386
  }
366
387
  /** Append the staged diff and return the outcome. Every exit path goes through here, so the judge always sees the same sections in the same order. */
367
- function finish(parts, gateFailure, diff, traces = []) {
388
+ function finish(parts, gateFailure, gateError, diff, traces = [], events = [], eventErrors = []) {
368
389
  // The code itself, last — the gates above only prove that keywords appeared.
369
390
  // Without this section a seeded checklist item about what the code *does* is
370
391
  // graded from the model's own description of its work.
371
392
  parts.push("", "=== STAGED DIFF ===");
372
393
  parts.push(diff.trim() === "" ? " (empty — the model left no staged changes)" : capDiff(diff));
373
- return { transcript: parts.join("\n"), gateFailure, diff, traces };
394
+ return { transcript: parts.join("\n"), gateFailure, gateError, diff, traces, events, eventErrors };
374
395
  }
375
396
  /**
376
397
  * Parse vitest's `Tests N passed | M skipped (T)` summary line.
package/dist/sources.js CHANGED
@@ -168,7 +168,7 @@ function walk(dir, prefix = "") {
168
168
  * transcripts are actually invalid.
169
169
  */
170
170
  function facets(s) {
171
- const { id, title, critical, mode, turns, checklist, fixture, assert, traceAssert, workspace, remote, systemPromptFile, extensions, reps, passThreshold, covers: _coversIsMetadata, ...restScenario } = s;
171
+ const { id, title, critical, mode, turns, checklist, fixture, assert, traceAssert, trajectoryAssert, workspace, remote, systemPromptFile, extensions, eventSources, reps, passThreshold, covers: _coversIsMetadata, ...restScenario } = s;
172
172
  const _scenarioExhaustive = restScenario;
173
173
  void _scenarioExhaustive;
174
174
  // `covers` is destructured into a discard on purpose, and this comment is the
@@ -185,7 +185,7 @@ function facets(s) {
185
185
  // already saved, so `regate` (free) can re-answer it without re-running the model.
186
186
  // Note the asymmetry with `env.extensions` in Phase 3, which IS stimulus — one
187
187
  // changes what gets executed, the other only what we conclude from it.
188
- const hasGates = diff_contains !== undefined || diff_excludes !== undefined || traceAssert !== undefined;
188
+ const hasGates = diff_contains !== undefined || diff_excludes !== undefined || traceAssert !== undefined || trajectoryAssert !== undefined;
189
189
  return {
190
190
  // `vitest` and the `post_test` PATH are stimulus, not gates: both change what the
191
191
  // run executes in the workspace, and neither can be re-evaluated from a saved
@@ -204,13 +204,21 @@ function facets(s) {
204
204
  id, mode, turns, workspace, remote, systemPromptFile ?? null,
205
205
  fixture ?? null, vitest ?? null, post_test ?? null,
206
206
  ...(extensions ? [extensions] : []),
207
+ // Event-source paths choose which native ledgers are captured. A saved
208
+ // normalized artifact cannot answer for a source that was never collected,
209
+ // so changing this is stimulus and needs a re-run.
210
+ ...(eventSources ? [eventSources] : []),
207
211
  ]),
208
212
  rubric: JSON.stringify([id, title, checklist]),
209
213
  policy: JSON.stringify([id, critical, reps ?? null, passThreshold ?? null]),
210
214
  // Same rule as `stimulus` above: conditional, so a needle-gated scenario that
211
215
  // declares no trace assertions keeps the digest it was published with.
212
216
  gates: hasGates
213
- ? JSON.stringify([id, diff_contains ?? null, diff_excludes ?? null, ...(traceAssert ? [traceAssert] : [])])
217
+ ? JSON.stringify([
218
+ id, diff_contains ?? null, diff_excludes ?? null,
219
+ ...(traceAssert ? [traceAssert] : []),
220
+ ...(trajectoryAssert ? [trajectoryAssert] : []),
221
+ ])
214
222
  : null,
215
223
  };
216
224
  }
package/dist/spec.d.ts CHANGED
@@ -1,5 +1,6 @@
1
1
  import type { WorkspaceKind } from "./workspace.js";
2
2
  import { type TraceAssert } from "./trace-gates.js";
3
+ import { type TrajectoryAssert } from "./trajectory-gates.js";
3
4
  export type ScenarioMode = "inline" | "seeded";
4
5
  export interface SeededAssert {
5
6
  vitest?: boolean;
@@ -20,6 +21,14 @@ export interface SeededAssert {
20
21
  */
21
22
  post_test?: string;
22
23
  }
24
+ export interface TrajectoryEventSource {
25
+ /** Native format adapter registered by the harness adapter. */
26
+ adapter: "normalized-v1" | "principal-assurance-v1" | "pi-daddy-v1";
27
+ /** Workspace-relative file/glob. Never an absolute path or traversal. */
28
+ path: string;
29
+ /** Missing evidence is an error by default; false is for optional supplemental ledgers. */
30
+ required: boolean;
31
+ }
23
32
  export interface Scenario {
24
33
  id: string;
25
34
  title: string;
@@ -37,6 +46,10 @@ export interface Scenario {
37
46
  * Declaring it opts the scenario into structured (`--mode json`) execution.
38
47
  */
39
48
  traceAssert?: TraceAssert;
49
+ /** Objective assertions over adapter-neutral, versioned workflow events. */
50
+ trajectoryAssert?: TrajectoryAssert;
51
+ /** Native event/ledger files the harness normalizes after execution. */
52
+ eventSources?: TrajectoryEventSource[];
40
53
  workspace: WorkspaceKind;
41
54
  remote: boolean;
42
55
  systemPromptFile?: string;
@@ -69,6 +82,8 @@ export interface ShipBar {
69
82
  no_critical_fail: boolean;
70
83
  }
71
84
  export interface Spec {
85
+ /** Specification schema. Omitted YAML is read as v1 for 0.8 compatibility. */
86
+ schema?: 1;
72
87
  skill: string;
73
88
  judge_persona: string;
74
89
  ship_bar: ShipBar;
package/dist/spec.js CHANGED
@@ -1,6 +1,7 @@
1
1
  import { readFileSync } from "node:fs";
2
2
  import yaml from "js-yaml";
3
3
  import { parseTraceAssert } from "./trace-gates.js";
4
+ import { parseTrajectoryAssert } from "./trajectory-gates.js";
4
5
  /** Thrown on any validation failure. Message always carries the spec file path. */
5
6
  export class SpecError extends Error {
6
7
  constructor(message, file) {
@@ -88,6 +89,46 @@ function resolveExtensions(env, hasSystemPrompt, id, file) {
88
89
  * with empty-git or a fixture — asking for one on a bare cwd is an authoring mistake,
89
90
  * not something to silently ignore.
90
91
  */
92
+ function resolveEventSources(env, id, file) {
93
+ const raw = env && typeof env === "object" ? env.event_sources : undefined;
94
+ if (raw === undefined)
95
+ return undefined;
96
+ if (!Array.isArray(raw) || raw.length === 0) {
97
+ throw new SpecError(`scenario \`${id}\` env.event_sources must be a non-empty list`, file);
98
+ }
99
+ const allowedAdapters = new Set([
100
+ "normalized-v1", "principal-assurance-v1", "pi-daddy-v1",
101
+ ]);
102
+ return raw.map((entry, index) => {
103
+ if (!entry || typeof entry !== "object" || Array.isArray(entry)) {
104
+ throw new SpecError(`scenario \`${id}\` env.event_sources[${index}] must be a mapping`, file);
105
+ }
106
+ const source = entry;
107
+ for (const key of Object.keys(source)) {
108
+ if (!["adapter", "path", "required"].includes(key)) {
109
+ throw new SpecError(`scenario \`${id}\` env.event_sources[${index}] has unknown key \`${key}\``, file);
110
+ }
111
+ }
112
+ if (!allowedAdapters.has(source.adapter)) {
113
+ throw new SpecError(`scenario \`${id}\` env.event_sources[${index}].adapter is unsupported`, file);
114
+ }
115
+ if (typeof source.path !== "string" || !source.path.trim()) {
116
+ throw new SpecError(`scenario \`${id}\` env.event_sources[${index}].path must be non-empty`, file);
117
+ }
118
+ const path = source.path.trim();
119
+ if (path.startsWith("/") || /^[A-Za-z]:[\\/]/.test(path) || path.includes("\\") || path.split("/").some((part) => part === ".." || part === "." || part === "")) {
120
+ throw new SpecError(`scenario \`${id}\` env.event_sources paths must be workspace-relative without traversal (got \`${path}\`)`, file);
121
+ }
122
+ if (source.required !== undefined && typeof source.required !== "boolean") {
123
+ throw new SpecError(`scenario \`${id}\` env.event_sources[${index}].required must be true or false`, file);
124
+ }
125
+ return {
126
+ adapter: source.adapter,
127
+ path,
128
+ required: source.required !== false,
129
+ };
130
+ });
131
+ }
91
132
  function resolveRemote(env, workspace, id, file) {
92
133
  const raw = env && typeof env === "object" ? env.remote : undefined;
93
134
  if (raw === undefined)
@@ -114,6 +155,9 @@ export function parseSpec(text, file) {
114
155
  throw new SpecError("spec must be a YAML mapping", file);
115
156
  }
116
157
  const o = doc;
158
+ if (o.schema !== undefined && o.schema !== 1) {
159
+ throw new SpecError(`unsupported \`schema\` ${JSON.stringify(o.schema)} (expected 1)`, file);
160
+ }
117
161
  if (typeof o.skill !== "string" || o.skill.length === 0) {
118
162
  throw new SpecError("missing or invalid `skill` (string)", file);
119
163
  }
@@ -179,6 +223,9 @@ export function parseSpec(text, file) {
179
223
  if (rawAssert?.trace !== undefined) {
180
224
  scenario.traceAssert = parseTraceAssert(rawAssert.trace, `${file}: scenario \`${id}\``);
181
225
  }
226
+ if (rawAssert?.trajectory !== undefined) {
227
+ scenario.trajectoryAssert = parseTrajectoryAssert(rawAssert.trajectory, `${file}: scenario \`${id}\``);
228
+ }
182
229
  if (mode === "seeded") {
183
230
  if (typeof s.fixture !== "string" || s.fixture.length === 0) {
184
231
  throw new SpecError(`seeded scenario \`${id}\` requires a \`fixture\` path`, file);
@@ -231,6 +278,10 @@ export function parseSpec(text, file) {
231
278
  }
232
279
  scenario.workspace = resolveWorkspace(s.env, mode, scenario.fixture, id, file);
233
280
  scenario.remote = resolveRemote(s.env, scenario.workspace, id, file);
281
+ scenario.eventSources = resolveEventSources(s.env, id, file);
282
+ if (scenario.eventSources && !scenario.trajectoryAssert) {
283
+ throw new SpecError(`scenario \`${id}\` declares env.event_sources without assert.trajectory`, file);
284
+ }
234
285
  if (s.system_prompt_file !== undefined) {
235
286
  if (typeof s.system_prompt_file !== "string" || !s.system_prompt_file.trim()) {
236
287
  throw new SpecError(`scenario \`${id}\` \`system_prompt_file\` must be a non-empty string`, file);
@@ -277,7 +328,8 @@ export function parseSpec(text, file) {
277
328
  }
278
329
  return scenario;
279
330
  });
280
- return { skill: o.skill, judge_persona: o.judge_persona, ship_bar, critical, scenarios };
331
+ const effectiveCritical = [...new Set([...critical, ...scenarios.filter((scenario) => scenario.critical).map((scenario) => scenario.id)])];
332
+ return { schema: 1, skill: o.skill, judge_persona: o.judge_persona, ship_bar, critical: effectiveCritical, scenarios };
281
333
  }
282
334
  /** Load + validate a specification.yaml from disk. */
283
335
  export function loadSpec(file) {
@@ -95,7 +95,7 @@ export declare function normalizeSubagentCall(args: Record<string, unknown>): Su
95
95
  */
96
96
  export type AssertionStatus = "PASS" | "FAIL" | "ERROR";
97
97
  export interface AssertionResult {
98
- kind: "require_call" | "require_subagent" | "forbid_call" | "unchanged_path";
98
+ kind: "require_call" | "require_subagent" | "forbid_call" | "unchanged_path" | "trace_evidence";
99
99
  status: AssertionStatus;
100
100
  detail: string;
101
101
  }
@@ -131,3 +131,4 @@ export declare function matchesGlob(pattern: string, path: string): boolean;
131
131
  * possible failure for a safety check.
132
132
  */
133
133
  export declare function parseTraceAssert(raw: unknown, ctx: string): TraceAssert;
134
+ export declare function parsePredicate(raw: unknown, ctx: string): ArgPredicate;
@@ -39,6 +39,9 @@ export function normalizeSubagentCall(args) {
39
39
  */
40
40
  export function evaluateTraceGates(assert, trace) {
41
41
  const assertions = [];
42
+ for (const error of trace.capture_errors ?? []) {
43
+ assertions.push({ kind: "trace_evidence", status: "ERROR", detail: error });
44
+ }
42
45
  for (const req of assert.require_calls ?? []) {
43
46
  const matched = trace.tool_calls.filter((c) => c.name === req.tool && argsMatch(c, req.args));
44
47
  const min = req.count?.min ?? 1;
@@ -469,7 +472,7 @@ function parseArgs(raw, ctx) {
469
472
  }
470
473
  return out;
471
474
  }
472
- function parsePredicate(raw, ctx) {
475
+ export function parsePredicate(raw, ctx) {
473
476
  // `agent: plan` is shorthand for `agent: { equals: plan }` — the common case
474
477
  // should not require the author to know the operator vocabulary.
475
478
  if (typeof raw === "string" || typeof raw === "number" || typeof raw === "boolean") {
@@ -0,0 +1,137 @@
1
+ import { type ArgPredicate, type CountConstraint } from "./trace-gates.js";
2
+ /** Adapter-neutral event contract used by objective trajectory assertions. */
3
+ export declare const TRAJECTORY_EVENT_VERSION: "1.0";
4
+ export declare const TRAJECTORY_ASSERT_VERSION: "1.0";
5
+ export interface TrajectoryApproval {
6
+ id?: string;
7
+ capability?: string;
8
+ subject?: string;
9
+ source?: string;
10
+ scope?: string;
11
+ approved_at?: string;
12
+ expires_at?: string;
13
+ used_at?: string;
14
+ }
15
+ export interface TrajectoryDigests {
16
+ plan?: string;
17
+ task?: string;
18
+ definition?: string;
19
+ head?: string;
20
+ tree?: string;
21
+ [key: string]: string | undefined;
22
+ }
23
+ /**
24
+ * One normalized workflow event. Native field names remain available in
25
+ * `attributes`; common correlation fields are promoted so the evaluator never
26
+ * needs to know whether the source was pi, pi-daddy, or principal assurance.
27
+ */
28
+ export interface TrajectoryEventV1 {
29
+ event_version: typeof TRAJECTORY_EVENT_VERSION;
30
+ seq: number;
31
+ type: string;
32
+ source: string;
33
+ at?: string;
34
+ run_id?: string;
35
+ task_id?: string;
36
+ workspace_id?: string;
37
+ context_id?: string;
38
+ finding_id?: string;
39
+ parent_id?: string;
40
+ child_id?: string;
41
+ phase?: string;
42
+ tool?: string;
43
+ capability?: string;
44
+ requested_capabilities?: string[];
45
+ effective_capabilities?: string[];
46
+ refusal_code?: string;
47
+ exit_code?: number;
48
+ digests?: TrajectoryDigests;
49
+ approval?: TrajectoryApproval;
50
+ requirements?: string[];
51
+ attributes?: Record<string, unknown>;
52
+ }
53
+ export interface EventSelector {
54
+ event: string;
55
+ where?: Record<string, ArgPredicate>;
56
+ select?: "first" | "last" | "all";
57
+ }
58
+ export interface RequiredEvent extends EventSelector {
59
+ count?: CountConstraint;
60
+ }
61
+ export interface CorrelationAssert {
62
+ left: EventSelector;
63
+ right: EventSelector;
64
+ same?: string[];
65
+ different?: string[];
66
+ order?: "before" | "after";
67
+ }
68
+ export interface FreshnessAssert {
69
+ subject: EventSelector;
70
+ after: EventSelector[];
71
+ same?: string[];
72
+ }
73
+ export interface UniqueAssert {
74
+ events: EventSelector;
75
+ fields: string[];
76
+ }
77
+ export interface ForbidAfterAssert {
78
+ anchor: EventSelector;
79
+ forbidden: EventSelector[];
80
+ same?: string[];
81
+ anchor_optional?: boolean;
82
+ }
83
+ export interface ApprovalAssert {
84
+ grant: EventSelector;
85
+ use: EventSelector;
86
+ same?: string[];
87
+ scopes?: string[];
88
+ sources?: string[];
89
+ unexpired?: boolean;
90
+ max_uses?: number;
91
+ }
92
+ export interface CoverageAssert {
93
+ requirements: string[];
94
+ events?: EventSelector;
95
+ }
96
+ export interface TrajectoryAssert {
97
+ version: typeof TRAJECTORY_ASSERT_VERSION;
98
+ require?: RequiredEvent[];
99
+ forbid?: EventSelector[];
100
+ ordered?: EventSelector[][];
101
+ correlate?: CorrelationAssert[];
102
+ freshness?: FreshnessAssert[];
103
+ unique?: UniqueAssert[];
104
+ forbid_after?: ForbidAfterAssert[];
105
+ approvals?: ApprovalAssert[];
106
+ coverage?: CoverageAssert[];
107
+ }
108
+ export type TrajectoryAssertionStatus = "PASS" | "FAIL" | "ERROR";
109
+ export interface TrajectoryAssertionResult {
110
+ kind: "require_event" | "forbid_event" | "ordered_events" | "correlation" | "freshness" | "unique" | "forbid_after" | "approval" | "coverage" | "evidence";
111
+ status: TrajectoryAssertionStatus;
112
+ detail: string;
113
+ }
114
+ export interface TrajectoryGateResult {
115
+ status: TrajectoryAssertionStatus;
116
+ event_version: typeof TRAJECTORY_EVENT_VERSION;
117
+ events_sha256: string;
118
+ assertions: TrajectoryAssertionResult[];
119
+ }
120
+ export declare function trajectoryEventsSha256(events: TrajectoryEventV1[]): string;
121
+ export declare function serializeTrajectoryEvents(events: TrajectoryEventV1[]): string;
122
+ export declare function deserializeTrajectoryEvents(text: string): TrajectoryEventV1[] | null;
123
+ /** Evaluate a closed, declarative assertion set over saved normalized events. */
124
+ export declare function evaluateTrajectoryGates(assert: TrajectoryAssert, input: TrajectoryEventV1[]): TrajectoryGateResult;
125
+ export declare function parseTrajectoryAssert(raw: unknown, ctx: string): TrajectoryAssert;
126
+ export interface MutationSelfTestCase {
127
+ id: string;
128
+ detected: boolean;
129
+ status: TrajectoryAssertionStatus;
130
+ detail: string;
131
+ }
132
+ export interface MutationSelfTestReport {
133
+ baseline: "PASS";
134
+ cases: MutationSelfTestCase[];
135
+ }
136
+ /** Fast, deterministic proof that every trajectory assertion class can turn red. */
137
+ export declare function runTrajectoryMutationSelfTest(): MutationSelfTestReport;