@skill-harness/core 0.6.0 → 0.7.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 +37 -0
- package/dist/adjudication.d.ts +210 -0
- package/dist/adjudication.js +392 -0
- package/dist/affected.d.ts +88 -0
- package/dist/affected.js +222 -0
- package/dist/capture-trace-types.d.ts +228 -0
- package/dist/capture-trace-types.js +23 -0
- package/dist/capture.d.ts +193 -0
- package/dist/capture.js +344 -0
- package/dist/execution-trace.d.ts +61 -0
- package/dist/execution-trace.js +299 -0
- package/dist/index.d.ts +8 -0
- package/dist/index.js +8 -0
- package/dist/instruction-coverage.d.ts +106 -0
- package/dist/instruction-coverage.js +253 -0
- package/dist/journal.d.ts +17 -0
- package/dist/lint.d.ts +1 -1
- package/dist/lint.js +23 -0
- package/dist/regate.js +80 -17
- package/dist/regrade.js +17 -3
- package/dist/report.d.ts +24 -0
- package/dist/report.js +24 -0
- package/dist/reps.d.ts +14 -1
- package/dist/reps.js +28 -2
- package/dist/rescore.js +11 -2
- package/dist/results.d.ts +128 -6
- package/dist/results.js +155 -6
- package/dist/run.js +114 -8
- package/dist/seeded.d.ts +11 -0
- package/dist/seeded.js +31 -7
- package/dist/sources.js +40 -3
- package/dist/spec-write.d.ts +62 -0
- package/dist/spec-write.js +106 -0
- package/dist/spec.d.ts +29 -0
- package/dist/spec.js +55 -0
- package/dist/trace-gates.d.ts +133 -0
- package/dist/trace-gates.js +519 -0
- package/dist/workspace.d.ts +36 -0
- package/dist/workspace.js +61 -0
- package/package.json +1 -1
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
import { createHash } from "node:crypto";
|
|
2
|
+
import { readFileSync, renameSync, unlinkSync, writeFileSync } from "node:fs";
|
|
3
|
+
import { dirname, join } from "node:path";
|
|
4
|
+
import yaml from "js-yaml";
|
|
5
|
+
import { parseSpec } from "./spec.js";
|
|
6
|
+
/**
|
|
7
|
+
* The single choke point for appending a scenario to an existing
|
|
8
|
+
* `specification.yaml`.
|
|
9
|
+
*
|
|
10
|
+
* Two callers need this — `add-test` and capture promotion — and a second
|
|
11
|
+
* implementation is how they would drift into disagreeing about what a valid
|
|
12
|
+
* write is. Everything here is deliberately append-shaped: a spec is
|
|
13
|
+
* hand-authored and full of comments, and a round trip through
|
|
14
|
+
* `yaml.load`/`yaml.dump` would silently reformat it and drop every comment the
|
|
15
|
+
* author wrote. So the existing bytes are never re-serialized — the new block is
|
|
16
|
+
* concatenated onto them and the *result* is validated before anything is
|
|
17
|
+
* written.
|
|
18
|
+
*/
|
|
19
|
+
/** Thrown when the spec on disk moved between the caller reading it and writing. */
|
|
20
|
+
export class ConcurrentSpecModification extends Error {
|
|
21
|
+
constructor(specPath) {
|
|
22
|
+
super(`${specPath} changed on disk since it was read — refusing to append. ` +
|
|
23
|
+
`Re-read the spec and retry; appending now would validate against a file that no longer exists.`);
|
|
24
|
+
this.name = "ConcurrentSpecModification";
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
/** Thrown when the scenario being appended collides with one already in the spec. */
|
|
28
|
+
export class DuplicateScenarioId extends Error {
|
|
29
|
+
constructor(id, specPath) {
|
|
30
|
+
super(`scenario id \`${id}\` already exists in ${specPath}`);
|
|
31
|
+
this.name = "DuplicateScenarioId";
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
/** SHA-256 of spec text. Callers hold one across a read→confirm→write cycle. */
|
|
35
|
+
export function specSha256(text) {
|
|
36
|
+
return createHash("sha256").update(text, "utf8").digest("hex");
|
|
37
|
+
}
|
|
38
|
+
/**
|
|
39
|
+
* Render one scenario as a YAML block that can be concatenated onto a spec.
|
|
40
|
+
*
|
|
41
|
+
* Dumps `{ scenarios: [scenario] }` and strips the top-level key, leaving the
|
|
42
|
+
* correctly-indented list item. Going through `yaml.dump` rather than string
|
|
43
|
+
* templating is what makes arbitrary user text — quotes, colons, newlines,
|
|
44
|
+
* leading dashes — safe to embed.
|
|
45
|
+
*/
|
|
46
|
+
export function renderScenarioBlock(scenario) {
|
|
47
|
+
const dumped = yaml.dump({ scenarios: [scenario] }, { lineWidth: -1, noRefs: true });
|
|
48
|
+
return "\n" + dumped.replace(/^scenarios:\n/, "");
|
|
49
|
+
}
|
|
50
|
+
/**
|
|
51
|
+
* Validate and atomically append a scenario.
|
|
52
|
+
*
|
|
53
|
+
* Order matters and is load-bearing: read → detect concurrent modification →
|
|
54
|
+
* reject duplicate id → build → **validate the merged text** → write. The
|
|
55
|
+
* validation is on the merged result, not the block alone, because a block that
|
|
56
|
+
* parses in isolation can still break the file it lands in.
|
|
57
|
+
*
|
|
58
|
+
* The write is temp-file-plus-rename rather than `appendFileSync`. An append
|
|
59
|
+
* interrupted partway through leaves a syntactically broken spec on disk; a
|
|
60
|
+
* rename either happened or did not.
|
|
61
|
+
*/
|
|
62
|
+
export function appendScenario(opts) {
|
|
63
|
+
const { specPath, scenario, baseSha256 } = opts;
|
|
64
|
+
const current = readFileSync(specPath, "utf8");
|
|
65
|
+
if (baseSha256 !== undefined && specSha256(current) !== baseSha256) {
|
|
66
|
+
throw new ConcurrentSpecModification(specPath);
|
|
67
|
+
}
|
|
68
|
+
const id = scenario.id;
|
|
69
|
+
if (typeof id !== "string" || id.trim() === "") {
|
|
70
|
+
throw new Error("scenario needs a non-empty string `id`");
|
|
71
|
+
}
|
|
72
|
+
const existing = parseSpec(current, specPath);
|
|
73
|
+
if (existing.scenarios.some((s) => s.id === id)) {
|
|
74
|
+
throw new DuplicateScenarioId(id, specPath);
|
|
75
|
+
}
|
|
76
|
+
const block = renderScenarioBlock(scenario);
|
|
77
|
+
const merged = current + block;
|
|
78
|
+
parseSpec(merged, specPath); // throws if the append broke the spec
|
|
79
|
+
atomicWrite(specPath, merged);
|
|
80
|
+
return { id, sha256: specSha256(merged), block };
|
|
81
|
+
}
|
|
82
|
+
/**
|
|
83
|
+
* Write via a sibling temp file and rename.
|
|
84
|
+
*
|
|
85
|
+
* Sibling, not `/tmp`: `rename(2)` is only atomic within a filesystem, and a
|
|
86
|
+
* cross-device rename would silently degrade to copy-then-delete — exactly the
|
|
87
|
+
* torn write this exists to prevent.
|
|
88
|
+
*/
|
|
89
|
+
function atomicWrite(path, text) {
|
|
90
|
+
const tmp = join(dirname(path), `.${Date.now()}-${process.pid}.specwrite.tmp`);
|
|
91
|
+
try {
|
|
92
|
+
writeFileSync(tmp, text, "utf8");
|
|
93
|
+
renameSync(tmp, path);
|
|
94
|
+
}
|
|
95
|
+
catch (err) {
|
|
96
|
+
try {
|
|
97
|
+
unlinkSync(tmp);
|
|
98
|
+
}
|
|
99
|
+
catch {
|
|
100
|
+
// Best effort: the original file is untouched either way, and masking the
|
|
101
|
+
// real failure with a cleanup error would hide why the write failed.
|
|
102
|
+
}
|
|
103
|
+
throw err;
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
//# sourceMappingURL=spec-write.js.map
|
package/dist/spec.d.ts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import type { WorkspaceKind } from "./workspace.js";
|
|
2
|
+
import { type TraceAssert } from "./trace-gates.js";
|
|
2
3
|
export type ScenarioMode = "inline" | "seeded";
|
|
3
4
|
export interface SeededAssert {
|
|
4
5
|
vitest?: boolean;
|
|
@@ -28,9 +29,37 @@ export interface Scenario {
|
|
|
28
29
|
checklist: string[];
|
|
29
30
|
fixture?: string;
|
|
30
31
|
assert?: SeededAssert;
|
|
32
|
+
/**
|
|
33
|
+
* Objective assertions over the execution trace.
|
|
34
|
+
*
|
|
35
|
+
* Deliberately NOT part of `SeededAssert`: the other gates read a staged git
|
|
36
|
+
* diff and are meaningless without a fixture, while a trace exists for any run.
|
|
37
|
+
* Declaring it opts the scenario into structured (`--mode json`) execution.
|
|
38
|
+
*/
|
|
39
|
+
traceAssert?: TraceAssert;
|
|
31
40
|
workspace: WorkspaceKind;
|
|
32
41
|
remote: boolean;
|
|
33
42
|
systemPromptFile?: string;
|
|
43
|
+
/**
|
|
44
|
+
* `env.extensions`: pi extension files to load, resolved relative to the spec dir.
|
|
45
|
+
*
|
|
46
|
+
* Loading is CLOSED, not additive — the adapter passes `--no-extensions` plus one
|
|
47
|
+
* `--extension` per entry, so exactly these load and nothing discovered does.
|
|
48
|
+
* (Measured on pi 0.83.0: that flag pair isolates even under `-a` project-local
|
|
49
|
+
* trust.) Without it, whatever the developer happened to have installed would
|
|
50
|
+
* silently become part of the test.
|
|
51
|
+
*/
|
|
52
|
+
extensions?: string[];
|
|
53
|
+
/**
|
|
54
|
+
* `covers`: instruction sections this scenario is declared to exercise, e.g.
|
|
55
|
+
* `SKILL.md#core-principle`.
|
|
56
|
+
*
|
|
57
|
+
* METADATA. It stales nothing — see `sources.ts`, where it is deliberately in
|
|
58
|
+
* no digest. A `covers` edit changes which tests `--affected` selects, not what
|
|
59
|
+
* any past run measured, so charging a re-run for it would be the exact
|
|
60
|
+
* "pay tokens to fix a label" trap the facet split exists to remove.
|
|
61
|
+
*/
|
|
62
|
+
covers?: string[];
|
|
34
63
|
reps?: number;
|
|
35
64
|
passThreshold?: number;
|
|
36
65
|
}
|
package/dist/spec.js
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { readFileSync } from "node:fs";
|
|
2
2
|
import yaml from "js-yaml";
|
|
3
|
+
import { parseTraceAssert } from "./trace-gates.js";
|
|
3
4
|
/** Thrown on any validation failure. Message always carries the spec file path. */
|
|
4
5
|
export class SpecError extends Error {
|
|
5
6
|
constructor(message, file) {
|
|
@@ -54,6 +55,34 @@ function resolveWorkspace(env, mode, fixture, id, file) {
|
|
|
54
55
|
}
|
|
55
56
|
throw new SpecError(`scenario \`${id}\` env.workspace must be none | empty-git | fixture:<path>`, file);
|
|
56
57
|
}
|
|
58
|
+
/**
|
|
59
|
+
* Resolve `env.extensions` into a list of paths.
|
|
60
|
+
*
|
|
61
|
+
* Incompatible with `system_prompt_file` by construction: that flag REPLACES the
|
|
62
|
+
* system prompt to test a subagent definition in isolation, while an
|
|
63
|
+
* orchestration scenario tests the PARENT that delegates to one. Allowing both
|
|
64
|
+
* would silently test neither — the parent's instructions would be gone.
|
|
65
|
+
*/
|
|
66
|
+
function resolveExtensions(env, hasSystemPrompt, id, file) {
|
|
67
|
+
const raw = env && typeof env === "object" ? env.extensions : undefined;
|
|
68
|
+
if (raw === undefined)
|
|
69
|
+
return undefined;
|
|
70
|
+
if (!Array.isArray(raw) || raw.length === 0) {
|
|
71
|
+
throw new SpecError(`scenario \`${id}\` env.extensions must be a non-empty list of paths`, file);
|
|
72
|
+
}
|
|
73
|
+
const paths = raw.map((p, i) => {
|
|
74
|
+
if (typeof p !== "string" || p.trim() === "") {
|
|
75
|
+
throw new SpecError(`scenario \`${id}\` env.extensions[${i}] must be a non-empty path`, file);
|
|
76
|
+
}
|
|
77
|
+
return p.trim();
|
|
78
|
+
});
|
|
79
|
+
if (hasSystemPrompt) {
|
|
80
|
+
throw new SpecError(`scenario \`${id}\` sets both env.extensions and system_prompt_file — ` +
|
|
81
|
+
`system_prompt_file replaces the system prompt to test a subagent in isolation, ` +
|
|
82
|
+
`while env.extensions tests the parent that delegates to one. Pick one.`, file);
|
|
83
|
+
}
|
|
84
|
+
return paths;
|
|
85
|
+
}
|
|
57
86
|
/**
|
|
58
87
|
* Resolve `env.remote`. A remote needs a repo to attach to, so it is only meaningful
|
|
59
88
|
* with empty-git or a fixture — asking for one on a bare cwd is an authoring mistake,
|
|
@@ -144,6 +173,12 @@ export function parseSpec(text, file) {
|
|
|
144
173
|
workspace: "none",
|
|
145
174
|
remote: false,
|
|
146
175
|
};
|
|
176
|
+
// `assert.trace` is legal for inline AND seeded scenarios — it reads the
|
|
177
|
+
// execution trace, which every run produces, not a staged diff.
|
|
178
|
+
const rawAssert = s.assert;
|
|
179
|
+
if (rawAssert?.trace !== undefined) {
|
|
180
|
+
scenario.traceAssert = parseTraceAssert(rawAssert.trace, `${file}: scenario \`${id}\``);
|
|
181
|
+
}
|
|
147
182
|
if (mode === "seeded") {
|
|
148
183
|
if (typeof s.fixture !== "string" || s.fixture.length === 0) {
|
|
149
184
|
throw new SpecError(`seeded scenario \`${id}\` requires a \`fixture\` path`, file);
|
|
@@ -208,6 +243,26 @@ export function parseSpec(text, file) {
|
|
|
208
243
|
}
|
|
209
244
|
scenario.systemPromptFile = s.system_prompt_file.trim();
|
|
210
245
|
}
|
|
246
|
+
if (s.covers !== undefined) {
|
|
247
|
+
if (!isStringArray(s.covers) || s.covers.length === 0) {
|
|
248
|
+
throw new SpecError(`scenario \`${id}\` \`covers\` must be a non-empty list of strings`, file);
|
|
249
|
+
}
|
|
250
|
+
const bad = s.covers.find((c) => c.trim() === "");
|
|
251
|
+
if (bad !== undefined)
|
|
252
|
+
throw new SpecError(`scenario \`${id}\` \`covers\` has an empty entry`, file);
|
|
253
|
+
scenario.covers = s.covers.map((c) => c.trim());
|
|
254
|
+
}
|
|
255
|
+
// `unchanged_paths` is checked against the workspace's git state, so a scenario
|
|
256
|
+
// with no repo has nothing to observe. Refused here — free and offline — rather
|
|
257
|
+
// than at run time, because the alternative shipped for a while: the assertion
|
|
258
|
+
// silently passed against an empty change list and reported a green safety gate.
|
|
259
|
+
if (scenario.traceAssert?.unchanged_paths?.length && scenario.workspace === "none") {
|
|
260
|
+
throw new SpecError(`scenario \`${id}\` declares \`assert.trace.unchanged_paths\` but has no workspace to observe — ` +
|
|
261
|
+
`set \`env.workspace: empty-git\` or \`fixture:<path>\`, or drop the assertion. ` +
|
|
262
|
+
`A path policy with nothing to compare against would pass unconditionally.`, file);
|
|
263
|
+
}
|
|
264
|
+
// After system_prompt_file, so the incompatibility check sees the resolved value.
|
|
265
|
+
scenario.extensions = resolveExtensions(s.env, scenario.systemPromptFile !== undefined, id, file);
|
|
211
266
|
if (s.reps !== undefined) {
|
|
212
267
|
if (typeof s.reps !== "number" || !Number.isInteger(s.reps) || s.reps < 1) {
|
|
213
268
|
throw new SpecError(`scenario \`${id}\` \`reps\` must be a positive integer`, file);
|
|
@@ -0,0 +1,133 @@
|
|
|
1
|
+
import type { ExecutionTraceV1 } from "./capture-trace-types.js";
|
|
2
|
+
/**
|
|
3
|
+
* The objective gate layer: assertions evaluated against a saved execution
|
|
4
|
+
* trace, before any judge is asked anything.
|
|
5
|
+
*
|
|
6
|
+
* The DSL is deliberately tiny and entirely declarative — no expressions, no
|
|
7
|
+
* callbacks, no executable predicates. A spec is data that arrives from a
|
|
8
|
+
* repository; giving it a code path would make "add a test" and "run arbitrary
|
|
9
|
+
* code in CI" the same act. Everything here is a comparison between a value the
|
|
10
|
+
* trace recorded and a literal the spec wrote down.
|
|
11
|
+
*
|
|
12
|
+
* What these assertions can and cannot prove is a hard boundary, restated here
|
|
13
|
+
* because it is easy to over-claim: a trace proves **a registered tool was
|
|
14
|
+
* called with given arguments**. It proves nothing about what that tool then did
|
|
15
|
+
* to the machine. A `bash` command string is not a filesystem audit.
|
|
16
|
+
*/
|
|
17
|
+
export interface ArgPredicate {
|
|
18
|
+
equals?: unknown;
|
|
19
|
+
contains?: string;
|
|
20
|
+
starts_with?: string;
|
|
21
|
+
ends_with?: string;
|
|
22
|
+
matches?: string;
|
|
23
|
+
exists?: boolean;
|
|
24
|
+
/** For array-valued arguments: at least one element satisfies the inner predicate. */
|
|
25
|
+
any?: ArgPredicate;
|
|
26
|
+
}
|
|
27
|
+
export declare const PREDICATE_KEYS: readonly ["equals", "contains", "starts_with", "ends_with", "matches", "exists", "any"];
|
|
28
|
+
export interface CountConstraint {
|
|
29
|
+
min?: number;
|
|
30
|
+
max?: number;
|
|
31
|
+
}
|
|
32
|
+
export interface RequireCall {
|
|
33
|
+
tool: string;
|
|
34
|
+
count?: CountConstraint;
|
|
35
|
+
args?: Record<string, ArgPredicate>;
|
|
36
|
+
}
|
|
37
|
+
export interface ForbidCall {
|
|
38
|
+
tool: string;
|
|
39
|
+
/** When present, only calls whose arguments match are forbidden. */
|
|
40
|
+
args?: Record<string, ArgPredicate>;
|
|
41
|
+
}
|
|
42
|
+
/**
|
|
43
|
+
* Convenience syntax for the orchestration case: "the parent delegated to
|
|
44
|
+
* `plan`, and the handoff carried X but not Y".
|
|
45
|
+
*
|
|
46
|
+
* Sugar over `require_calls`, not a second mechanism — it normalizes the known
|
|
47
|
+
* subagent argument shapes and then evaluates through the same path. There is
|
|
48
|
+
* deliberately no universal subagent extension assumed: an unknown extension can
|
|
49
|
+
* still be asserted on with plain `require_calls`, which is why this stays
|
|
50
|
+
* optional sugar rather than the only way in.
|
|
51
|
+
*/
|
|
52
|
+
export interface RequireSubagent {
|
|
53
|
+
/** The registered tool name — declared by the spec, since pi has no standard one. */
|
|
54
|
+
tool: string;
|
|
55
|
+
/** Which subagent the parent should have selected. */
|
|
56
|
+
agent: string;
|
|
57
|
+
count?: CountConstraint;
|
|
58
|
+
/** Substrings the handoff MUST carry. */
|
|
59
|
+
task_contains?: string[];
|
|
60
|
+
/** Substrings the handoff must NOT carry — the leak check. */
|
|
61
|
+
task_excludes?: string[];
|
|
62
|
+
}
|
|
63
|
+
export interface TraceAssert {
|
|
64
|
+
require_calls?: RequireCall[];
|
|
65
|
+
require_subagents?: RequireSubagent[];
|
|
66
|
+
forbid_calls?: ForbidCall[];
|
|
67
|
+
unchanged_paths?: string[];
|
|
68
|
+
}
|
|
69
|
+
/**
|
|
70
|
+
* Subagent invocations extracted from one tool call.
|
|
71
|
+
*
|
|
72
|
+
* A single call can carry several: `{tasks: [...]}` fans out and `{chain: [...]}`
|
|
73
|
+
* sequences. Normalizing to a flat list means a `count` constraint means the same
|
|
74
|
+
* thing — how many subagent invocations happened — whichever shape the extension
|
|
75
|
+
* uses to express them.
|
|
76
|
+
*/
|
|
77
|
+
export interface SubagentInvocation {
|
|
78
|
+
agent: string;
|
|
79
|
+
task: string;
|
|
80
|
+
}
|
|
81
|
+
/**
|
|
82
|
+
* Recognize the known subagent argument shapes.
|
|
83
|
+
*
|
|
84
|
+
* Three are supported because three exist in the wild; anything else yields an
|
|
85
|
+
* empty list, and the scenario should use plain `require_calls` instead. It
|
|
86
|
+
* deliberately does NOT guess: inventing an `agent` from an unrecognized shape
|
|
87
|
+
* would produce a confident assertion about a field nobody wrote.
|
|
88
|
+
*/
|
|
89
|
+
export declare function normalizeSubagentCall(args: Record<string, unknown>): SubagentInvocation[];
|
|
90
|
+
/**
|
|
91
|
+
* ERROR is a third outcome, not a shade of FAIL: it means the assertion could
|
|
92
|
+
* not be evaluated because the evidence is absent. The two call for different
|
|
93
|
+
* fixes — a FAIL means change the skill, an ERROR means the harness could not
|
|
94
|
+
* look — and only one of them is a finding about the model.
|
|
95
|
+
*/
|
|
96
|
+
export type AssertionStatus = "PASS" | "FAIL" | "ERROR";
|
|
97
|
+
export interface AssertionResult {
|
|
98
|
+
kind: "require_call" | "require_subagent" | "forbid_call" | "unchanged_path";
|
|
99
|
+
status: AssertionStatus;
|
|
100
|
+
detail: string;
|
|
101
|
+
}
|
|
102
|
+
export interface TraceGateResult {
|
|
103
|
+
status: AssertionStatus;
|
|
104
|
+
assertions: AssertionResult[];
|
|
105
|
+
}
|
|
106
|
+
/**
|
|
107
|
+
* Evaluate every assertion. All of them run even after the first failure — a
|
|
108
|
+
* scorecard that reports one problem per run makes the author re-run to find the
|
|
109
|
+
* second, and re-running is the expensive thing this whole layer exists to avoid.
|
|
110
|
+
*
|
|
111
|
+
* (One deliberate exception, marked inline in the `require_subagents` loop: the
|
|
112
|
+
* three sub-questions there are reported separately, and a later one is skipped
|
|
113
|
+
* when an earlier one already established there is nothing to ask it about.)
|
|
114
|
+
*/
|
|
115
|
+
export declare function evaluateTraceGates(assert: TraceAssert, trace: ExecutionTraceV1): TraceGateResult;
|
|
116
|
+
export declare function testPredicate(value: unknown, p: ArgPredicate): boolean;
|
|
117
|
+
/**
|
|
118
|
+
* Minimal glob over workspace-relative paths: `**` any depth, `*` one segment.
|
|
119
|
+
*
|
|
120
|
+
* Paths are normalized to forward slashes and stripped of a leading `./` first,
|
|
121
|
+
* so `./src/a.ts` and `src/a.ts` are the same path — otherwise an assertion
|
|
122
|
+
* would pass or fail on how the runner happened to spell it.
|
|
123
|
+
*/
|
|
124
|
+
export declare function matchesGlob(pattern: string, path: string): boolean;
|
|
125
|
+
/**
|
|
126
|
+
* Validate an `assert.trace` block from a spec.
|
|
127
|
+
*
|
|
128
|
+
* Strict on purpose: an unknown key is an error, not something ignored. A
|
|
129
|
+
* silently-ignored `forbid_call` (singular, say) would read in review as a gate
|
|
130
|
+
* that is protecting something while asserting nothing at all — the worst
|
|
131
|
+
* possible failure for a safety check.
|
|
132
|
+
*/
|
|
133
|
+
export declare function parseTraceAssert(raw: unknown, ctx: string): TraceAssert;
|