pactwright 0.0.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/LICENSE +201 -0
- package/README.md +68 -0
- package/dist/adapter/claude-code.d.ts +53 -0
- package/dist/adapter/claude-code.js +241 -0
- package/dist/adapter/commands.d.ts +19 -0
- package/dist/adapter/commands.js +162 -0
- package/dist/atomic.d.ts +6 -0
- package/dist/atomic.js +11 -0
- package/dist/cli.d.ts +2 -0
- package/dist/cli.js +561 -0
- package/dist/config/config.d.ts +55 -0
- package/dist/config/config.js +199 -0
- package/dist/config/lifecycle.d.ts +34 -0
- package/dist/config/lifecycle.js +81 -0
- package/dist/config/lock.d.ts +43 -0
- package/dist/config/lock.js +141 -0
- package/dist/context.d.ts +59 -0
- package/dist/context.js +111 -0
- package/dist/errors.d.ts +21 -0
- package/dist/errors.js +25 -0
- package/dist/eval/case.d.ts +123 -0
- package/dist/eval/case.js +17 -0
- package/dist/eval/core-suite.d.ts +3 -0
- package/dist/eval/core-suite.js +431 -0
- package/dist/eval/runner.d.ts +75 -0
- package/dist/eval/runner.js +159 -0
- package/dist/eval/sandbox.d.ts +39 -0
- package/dist/eval/sandbox.js +143 -0
- package/dist/extension/manage.d.ts +65 -0
- package/dist/extension/manage.js +372 -0
- package/dist/extension/manifest.d.ts +36 -0
- package/dist/extension/manifest.js +164 -0
- package/dist/extension/resolve.d.ts +77 -0
- package/dist/extension/resolve.js +271 -0
- package/dist/graph/edge-schema.d.ts +55 -0
- package/dist/graph/edge-schema.js +0 -0
- package/dist/graph/edges.d.ts +22 -0
- package/dist/graph/edges.js +63 -0
- package/dist/graph/ids.d.ts +14 -0
- package/dist/graph/ids.js +38 -0
- package/dist/graph/lineage.d.ts +48 -0
- package/dist/graph/lineage.js +226 -0
- package/dist/graph/mutations.d.ts +108 -0
- package/dist/graph/mutations.js +356 -0
- package/dist/graph/nodes.d.ts +46 -0
- package/dist/graph/nodes.js +137 -0
- package/dist/graph/revision.d.ts +50 -0
- package/dist/graph/revision.js +75 -0
- package/dist/graph/schema.d.ts +54 -0
- package/dist/graph/schema.js +90 -0
- package/dist/index.d.ts +33 -0
- package/dist/index.js +33 -0
- package/dist/init.d.ts +47 -0
- package/dist/init.js +132 -0
- package/dist/lifecycle/engine.d.ts +75 -0
- package/dist/lifecycle/engine.js +146 -0
- package/dist/lifecycle/record.d.ts +18 -0
- package/dist/lifecycle/record.js +157 -0
- package/dist/lifecycle/run.d.ts +62 -0
- package/dist/lifecycle/run.js +167 -0
- package/dist/loader.d.ts +38 -0
- package/dist/loader.js +64 -0
- package/dist/pack/capabilities.d.ts +22 -0
- package/dist/pack/capabilities.js +31 -0
- package/dist/pack/locate.d.ts +22 -0
- package/dist/pack/locate.js +80 -0
- package/dist/pack/manifest.d.ts +34 -0
- package/dist/pack/manifest.js +168 -0
- package/dist/pack/resolve.d.ts +92 -0
- package/dist/pack/resolve.js +238 -0
- package/dist/project.d.ts +22 -0
- package/dist/project.js +37 -0
- package/dist/sync.d.ts +54 -0
- package/dist/sync.js +98 -0
- package/dist/validate.d.ts +23 -0
- package/dist/validate.js +32 -0
- package/dist/validation.d.ts +24 -0
- package/dist/validation.js +83 -0
- package/dist/version.d.ts +2 -0
- package/dist/version.js +8 -0
- package/dist/yaml.d.ts +12 -0
- package/dist/yaml.js +32 -0
- package/package.json +65 -0
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
import { type ResolvedPack } from "../pack/resolve.js";
|
|
2
|
+
import type { CandidateRunner, EvalSuite, SemanticJudge } from "./case.js";
|
|
3
|
+
export interface DeterministicResult {
|
|
4
|
+
readonly id: string;
|
|
5
|
+
readonly description: string;
|
|
6
|
+
readonly passed: boolean;
|
|
7
|
+
readonly detail: string;
|
|
8
|
+
}
|
|
9
|
+
export interface SemanticResult {
|
|
10
|
+
readonly id: string;
|
|
11
|
+
readonly question: string;
|
|
12
|
+
readonly judged: boolean;
|
|
13
|
+
readonly verdict?: string;
|
|
14
|
+
readonly rationale?: string;
|
|
15
|
+
/** Why the dimension was not judged. */
|
|
16
|
+
readonly reason?: string;
|
|
17
|
+
}
|
|
18
|
+
export interface EvalCaseResult {
|
|
19
|
+
readonly id: string;
|
|
20
|
+
readonly title: string;
|
|
21
|
+
readonly capability: string;
|
|
22
|
+
/** The pack agent key that implemented the capability, when the pack provides it. */
|
|
23
|
+
readonly agent?: string;
|
|
24
|
+
/** The case could not be evaluated (missing capability, candidate error). */
|
|
25
|
+
readonly error?: string;
|
|
26
|
+
readonly deterministic: readonly DeterministicResult[];
|
|
27
|
+
/** Reported separately from the deterministic results, never merged or scored. */
|
|
28
|
+
readonly semantic: readonly SemanticResult[];
|
|
29
|
+
}
|
|
30
|
+
/**
|
|
31
|
+
* One evaluation run: per-case, per-assertion and per-dimension results
|
|
32
|
+
* only. Deliberately no aggregate quality score (Distribution §16); the
|
|
33
|
+
* report is a generated artefact, never Project Graph state.
|
|
34
|
+
*/
|
|
35
|
+
export interface EvalReport {
|
|
36
|
+
readonly suite: string;
|
|
37
|
+
readonly runtime: string;
|
|
38
|
+
readonly pack: {
|
|
39
|
+
readonly name: string;
|
|
40
|
+
readonly version: string;
|
|
41
|
+
readonly hash: string;
|
|
42
|
+
};
|
|
43
|
+
readonly cases: readonly EvalCaseResult[];
|
|
44
|
+
}
|
|
45
|
+
export interface EvalOptions {
|
|
46
|
+
/** The agent pack supplying the implementation being evaluated. */
|
|
47
|
+
readonly pack: ResolvedPack;
|
|
48
|
+
readonly suite: EvalSuite;
|
|
49
|
+
/** Overrides each case's scripted reference candidate (model-backed runs plug in here). */
|
|
50
|
+
readonly candidate?: CandidateRunner;
|
|
51
|
+
/** Judges semantic dimensions; absent means they are reported unjudged. */
|
|
52
|
+
readonly judge?: SemanticJudge;
|
|
53
|
+
/** Directory sandboxes are created under; defaults to the OS temp directory. */
|
|
54
|
+
readonly workDir?: string;
|
|
55
|
+
/**
|
|
56
|
+
* Per-case candidate deadline in ms; `undefined` disables. Scripted
|
|
57
|
+
* reference candidates are synchronous, so only model-backed runners need
|
|
58
|
+
* this. On timeout the case is reported as an error and its sandbox is
|
|
59
|
+
* removed immediately; a still-running candidate writes into the void.
|
|
60
|
+
*/
|
|
61
|
+
readonly candidateTimeoutMs?: number;
|
|
62
|
+
/** Per-dimension judge deadline in ms; `undefined` disables. */
|
|
63
|
+
readonly judgeTimeoutMs?: number;
|
|
64
|
+
}
|
|
65
|
+
/**
|
|
66
|
+
* Runs a suite against a resolved pack, one throw-away sandbox per case.
|
|
67
|
+
* Never throws for a failing case: failures are data in the report.
|
|
68
|
+
*/
|
|
69
|
+
export declare function runEval(options: EvalOptions): Promise<EvalReport>;
|
|
70
|
+
/**
|
|
71
|
+
* Whether every case was evaluated and every deterministic assertion
|
|
72
|
+
* passed — the CLI's exit-code gate. This is not a quality score: semantic
|
|
73
|
+
* dimensions never enter it, and no aggregate is calculated anywhere.
|
|
74
|
+
*/
|
|
75
|
+
export declare function evalPassed(report: EvalReport): boolean;
|
|
@@ -0,0 +1,159 @@
|
|
|
1
|
+
import { rmSync } from "node:fs";
|
|
2
|
+
import { agentFor } from "../pack/resolve.js";
|
|
3
|
+
import { runtimeVersion } from "../version.js";
|
|
4
|
+
import { createSandbox, diffSnapshots, sandboxRevision, snapshotFiles } from "./sandbox.js";
|
|
5
|
+
const message = (error) => error instanceof Error ? error.message : String(error);
|
|
6
|
+
async function withTimeout(work, ms, label) {
|
|
7
|
+
if (ms === undefined)
|
|
8
|
+
return work;
|
|
9
|
+
let timer;
|
|
10
|
+
try {
|
|
11
|
+
return await Promise.race([
|
|
12
|
+
work,
|
|
13
|
+
new Promise((_, reject) => {
|
|
14
|
+
// The timer stays referenced so the deadline fires even when the
|
|
15
|
+
// hung work holds nothing else on the event loop; it is cleared as
|
|
16
|
+
// soon as the race settles.
|
|
17
|
+
timer = setTimeout(() => reject(new Error(`${label} timed out after ${ms}ms`)), ms);
|
|
18
|
+
}),
|
|
19
|
+
]);
|
|
20
|
+
}
|
|
21
|
+
finally {
|
|
22
|
+
clearTimeout(timer);
|
|
23
|
+
void work.catch(() => { }); // the losing promise must not become an unhandled rejection
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
async function runCase(options, evalCase) {
|
|
27
|
+
const base = { id: evalCase.id, title: evalCase.title, capability: evalCase.capability };
|
|
28
|
+
const agent = agentFor(options.pack, evalCase.capability);
|
|
29
|
+
if (agent === undefined) {
|
|
30
|
+
return {
|
|
31
|
+
...base,
|
|
32
|
+
error: `pack "${options.pack.manifest.name}@${options.pack.manifest.version}" does not provide capability "${evalCase.capability}"; the case was not evaluated`,
|
|
33
|
+
deterministic: [],
|
|
34
|
+
semantic: [],
|
|
35
|
+
};
|
|
36
|
+
}
|
|
37
|
+
const withAgent = { ...base, agent: agent.key };
|
|
38
|
+
let root;
|
|
39
|
+
try {
|
|
40
|
+
root = createSandbox(options.pack, options.workDir);
|
|
41
|
+
evalCase.setup(root);
|
|
42
|
+
const filesBefore = snapshotFiles(root);
|
|
43
|
+
const revisionBefore = sandboxRevision(root);
|
|
44
|
+
let output;
|
|
45
|
+
try {
|
|
46
|
+
const run = options.candidate ?? ((task) => evalCase.reference.run(task.root));
|
|
47
|
+
output = await withTimeout(Promise.resolve(run({
|
|
48
|
+
caseId: evalCase.id,
|
|
49
|
+
capability: evalCase.capability,
|
|
50
|
+
instruction: evalCase.instruction,
|
|
51
|
+
root,
|
|
52
|
+
agent,
|
|
53
|
+
})), options.candidateTimeoutMs, "candidate");
|
|
54
|
+
}
|
|
55
|
+
catch (error) {
|
|
56
|
+
return {
|
|
57
|
+
...withAgent,
|
|
58
|
+
error: `candidate failed: ${message(error)}`,
|
|
59
|
+
deterministic: [],
|
|
60
|
+
semantic: [],
|
|
61
|
+
};
|
|
62
|
+
}
|
|
63
|
+
const observation = {
|
|
64
|
+
root,
|
|
65
|
+
changedFiles: diffSnapshots(filesBefore, snapshotFiles(root)),
|
|
66
|
+
revisionBefore,
|
|
67
|
+
revisionAfter: sandboxRevision(root),
|
|
68
|
+
output,
|
|
69
|
+
};
|
|
70
|
+
const deterministic = evalCase.deterministic.map((assertion) => {
|
|
71
|
+
try {
|
|
72
|
+
const result = assertion.check(observation);
|
|
73
|
+
return {
|
|
74
|
+
id: assertion.id,
|
|
75
|
+
description: assertion.description,
|
|
76
|
+
passed: result.passed,
|
|
77
|
+
detail: result.detail,
|
|
78
|
+
};
|
|
79
|
+
}
|
|
80
|
+
catch (error) {
|
|
81
|
+
return {
|
|
82
|
+
id: assertion.id,
|
|
83
|
+
description: assertion.description,
|
|
84
|
+
passed: false,
|
|
85
|
+
detail: `assertion threw: ${message(error)}`,
|
|
86
|
+
};
|
|
87
|
+
}
|
|
88
|
+
});
|
|
89
|
+
const semantic = [];
|
|
90
|
+
for (const dimension of evalCase.semantic) {
|
|
91
|
+
const entry = { id: dimension.id, question: dimension.question };
|
|
92
|
+
if (options.judge === undefined) {
|
|
93
|
+
semantic.push({
|
|
94
|
+
...entry,
|
|
95
|
+
judged: false,
|
|
96
|
+
reason: "no semantic judge configured; semantic quality is never decided deterministically",
|
|
97
|
+
});
|
|
98
|
+
continue;
|
|
99
|
+
}
|
|
100
|
+
try {
|
|
101
|
+
const judgement = await withTimeout(Promise.resolve(options.judge({ caseId: evalCase.id, dimension, observation })), options.judgeTimeoutMs, "judge");
|
|
102
|
+
semantic.push({
|
|
103
|
+
...entry,
|
|
104
|
+
judged: true,
|
|
105
|
+
verdict: judgement.verdict,
|
|
106
|
+
...(judgement.rationale === undefined ? {} : { rationale: judgement.rationale }),
|
|
107
|
+
});
|
|
108
|
+
}
|
|
109
|
+
catch (error) {
|
|
110
|
+
semantic.push({ ...entry, judged: false, reason: `judge failed: ${message(error)}` });
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
return { ...withAgent, deterministic, semantic };
|
|
114
|
+
}
|
|
115
|
+
catch (error) {
|
|
116
|
+
// Sandbox creation, case setup or observation building failed: the
|
|
117
|
+
// failure is data in the report, never a thrown run (Distribution §16).
|
|
118
|
+
return {
|
|
119
|
+
...withAgent,
|
|
120
|
+
error: `case failed: ${message(error)}`,
|
|
121
|
+
deterministic: [],
|
|
122
|
+
semantic: [],
|
|
123
|
+
};
|
|
124
|
+
}
|
|
125
|
+
finally {
|
|
126
|
+
if (root !== undefined)
|
|
127
|
+
rmSync(root, { recursive: true, force: true });
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
/**
|
|
131
|
+
* Runs a suite against a resolved pack, one throw-away sandbox per case.
|
|
132
|
+
* Never throws for a failing case: failures are data in the report.
|
|
133
|
+
*/
|
|
134
|
+
export async function runEval(options) {
|
|
135
|
+
const cases = [];
|
|
136
|
+
for (const evalCase of options.suite.cases)
|
|
137
|
+
cases.push(await runCase(options, evalCase));
|
|
138
|
+
return {
|
|
139
|
+
suite: options.suite.name,
|
|
140
|
+
runtime: runtimeVersion(),
|
|
141
|
+
pack: {
|
|
142
|
+
name: options.pack.manifest.name,
|
|
143
|
+
version: options.pack.manifest.version,
|
|
144
|
+
hash: options.pack.hashes.pack,
|
|
145
|
+
},
|
|
146
|
+
cases,
|
|
147
|
+
};
|
|
148
|
+
}
|
|
149
|
+
/**
|
|
150
|
+
* Whether every case was evaluated and every deterministic assertion
|
|
151
|
+
* passed — the CLI's exit-code gate. This is not a quality score: semantic
|
|
152
|
+
* dimensions never enter it, and no aggregate is calculated anywhere.
|
|
153
|
+
*/
|
|
154
|
+
export function evalPassed(report) {
|
|
155
|
+
// An empty run proves nothing; the gate must not pass vacuously.
|
|
156
|
+
if (report.cases.length === 0)
|
|
157
|
+
return false;
|
|
158
|
+
return report.cases.every((entry) => entry.error === undefined && entry.deterministic.every((a) => a.passed));
|
|
159
|
+
}
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import type { Edge } from "../graph/edges.js";
|
|
2
|
+
import { type ResolvedPack } from "../pack/resolve.js";
|
|
3
|
+
/** Every seeded node carries the same fixed date: fixtures are deterministic. */
|
|
4
|
+
export declare const SEED_CREATED = "2026-08-20";
|
|
5
|
+
/**
|
|
6
|
+
* A throw-away sandbox project for one evaluation case: a complete,
|
|
7
|
+
* loadable Pactwright project whose config selects the pack under
|
|
8
|
+
* evaluation by its absolute path and whose graph starts empty. The caller
|
|
9
|
+
* removes the directory when the case is done.
|
|
10
|
+
*/
|
|
11
|
+
export declare function createSandbox(pack: ResolvedPack, workDir?: string): string;
|
|
12
|
+
export interface SeedNode {
|
|
13
|
+
readonly id: string;
|
|
14
|
+
readonly type: string;
|
|
15
|
+
readonly title: string;
|
|
16
|
+
readonly body: string;
|
|
17
|
+
/** Extra frontmatter fields, e.g. a decision's `decided_by`/`outcome`. */
|
|
18
|
+
readonly fields?: Readonly<Record<string, string>>;
|
|
19
|
+
}
|
|
20
|
+
/** Serialises a seed node in the canonical node-file shape. */
|
|
21
|
+
export declare function seedNodeFile(node: SeedNode): string;
|
|
22
|
+
export declare function seedNode(root: string, node: SeedNode): void;
|
|
23
|
+
export declare function seedEdges(root: string, edges: readonly Edge[]): void;
|
|
24
|
+
/** Writes an ordinary repository file into the sandbox. */
|
|
25
|
+
export declare function seedFile(root: string, relativePath: string, content: string): void;
|
|
26
|
+
/** Sandbox-relative POSIX path → sha256 of the file's bytes. */
|
|
27
|
+
export type FileSnapshot = ReadonlyMap<string, string>;
|
|
28
|
+
/** Hashes every file under `root`; the observation diff comes from two snapshots. */
|
|
29
|
+
export declare function snapshotFiles(root: string): FileSnapshot;
|
|
30
|
+
/** Paths whose bytes changed, appeared or disappeared between two snapshots, sorted. */
|
|
31
|
+
export declare function diffSnapshots(before: FileSnapshot, after: FileSnapshot): readonly string[];
|
|
32
|
+
/**
|
|
33
|
+
* The sandbox's deterministic Project Graph revision (Delivery Graph §5)
|
|
34
|
+
* over whatever canonical state currently parses. Load problems are
|
|
35
|
+
* deliberately ignored here: a candidate that corrupts the graph is caught
|
|
36
|
+
* by the changed-files observation, while the revision proves whether the
|
|
37
|
+
* canonical state it left behind still equals the seeded state.
|
|
38
|
+
*/
|
|
39
|
+
export declare function sandboxRevision(root: string): string;
|
|
@@ -0,0 +1,143 @@
|
|
|
1
|
+
import { createHash } from "node:crypto";
|
|
2
|
+
import { mkdirSync, mkdtempSync, readdirSync, readFileSync, readlinkSync, writeFileSync, } from "node:fs";
|
|
3
|
+
import { tmpdir } from "node:os";
|
|
4
|
+
import { dirname, join, relative, sep } from "node:path";
|
|
5
|
+
import { loadEdges } from "../graph/edges.js";
|
|
6
|
+
import { serialiseEdges } from "../graph/mutations.js";
|
|
7
|
+
import { loadNodes } from "../graph/nodes.js";
|
|
8
|
+
import { graphRevision } from "../graph/revision.js";
|
|
9
|
+
import { lockEntriesFor, serialiseLock } from "../pack/resolve.js";
|
|
10
|
+
import { CONFIG_FILE, EDGES_FILE, LIFECYCLE_FILE, LOCK_FILE, NODES_DIR } from "../project.js";
|
|
11
|
+
/** Every seeded node carries the same fixed date: fixtures are deterministic. */
|
|
12
|
+
export const SEED_CREATED = "2026-08-20";
|
|
13
|
+
/** The §17 default lifecycle configuration seeded into every sandbox. */
|
|
14
|
+
const SANDBOX_LIFECYCLE = `version: 1
|
|
15
|
+
|
|
16
|
+
stages:
|
|
17
|
+
capture-intent:
|
|
18
|
+
execution: manual
|
|
19
|
+
propose-contracts:
|
|
20
|
+
execution: automatic
|
|
21
|
+
approve-contract:
|
|
22
|
+
execution: manual
|
|
23
|
+
actor: human
|
|
24
|
+
write-brief:
|
|
25
|
+
execution: automatic
|
|
26
|
+
deliver-brief:
|
|
27
|
+
execution: automatic
|
|
28
|
+
review:
|
|
29
|
+
execution: automatic
|
|
30
|
+
prepare-evidence:
|
|
31
|
+
execution: automatic
|
|
32
|
+
`;
|
|
33
|
+
function write(root, relativePath, content) {
|
|
34
|
+
const target = join(root, relativePath);
|
|
35
|
+
mkdirSync(dirname(target), { recursive: true });
|
|
36
|
+
writeFileSync(target, content, "utf8");
|
|
37
|
+
}
|
|
38
|
+
/**
|
|
39
|
+
* A throw-away sandbox project for one evaluation case: a complete,
|
|
40
|
+
* loadable Pactwright project whose config selects the pack under
|
|
41
|
+
* evaluation by its absolute path and whose graph starts empty. The caller
|
|
42
|
+
* removes the directory when the case is done.
|
|
43
|
+
*/
|
|
44
|
+
export function createSandbox(pack, workDir = tmpdir()) {
|
|
45
|
+
const root = mkdtempSync(join(workDir, "pactwright-eval-"));
|
|
46
|
+
write(root, CONFIG_FILE, [
|
|
47
|
+
"version: 1",
|
|
48
|
+
"",
|
|
49
|
+
"agent_pack:",
|
|
50
|
+
` source: ${JSON.stringify(pack.dir)}`,
|
|
51
|
+
"",
|
|
52
|
+
"adapter:",
|
|
53
|
+
" type: claude-code",
|
|
54
|
+
"",
|
|
55
|
+
"extensions: {}",
|
|
56
|
+
"",
|
|
57
|
+
"github:",
|
|
58
|
+
" enabled: false",
|
|
59
|
+
"",
|
|
60
|
+
].join("\n"));
|
|
61
|
+
write(root, LIFECYCLE_FILE, SANDBOX_LIFECYCLE);
|
|
62
|
+
write(root, LOCK_FILE, serialiseLock(lockEntriesFor(pack)));
|
|
63
|
+
mkdirSync(join(root, NODES_DIR), { recursive: true });
|
|
64
|
+
write(root, EDGES_FILE, "edges: []\n");
|
|
65
|
+
return root;
|
|
66
|
+
}
|
|
67
|
+
/** Serialises a seed node in the canonical node-file shape. */
|
|
68
|
+
export function seedNodeFile(node) {
|
|
69
|
+
const fields = Object.entries(node.fields ?? {})
|
|
70
|
+
.map(([key, value]) => `${key}: ${value}\n`)
|
|
71
|
+
.join("");
|
|
72
|
+
return `---\nid: ${node.id}\ntype: ${node.type}\ntitle: ${node.title}\ncreated: ${SEED_CREATED}\n${fields}---\n\n${node.body}\n`;
|
|
73
|
+
}
|
|
74
|
+
export function seedNode(root, node) {
|
|
75
|
+
write(root, `${NODES_DIR}/${node.id}.md`, seedNodeFile(node));
|
|
76
|
+
}
|
|
77
|
+
export function seedEdges(root, edges) {
|
|
78
|
+
write(root, EDGES_FILE, serialiseEdges(edges));
|
|
79
|
+
}
|
|
80
|
+
/** Writes an ordinary repository file into the sandbox. */
|
|
81
|
+
export function seedFile(root, relativePath, content) {
|
|
82
|
+
write(root, relativePath, content);
|
|
83
|
+
}
|
|
84
|
+
function walk(dir, base, into) {
|
|
85
|
+
for (const entry of readdirSync(dir, { withFileTypes: true }).sort((a, b) => a.name < b.name ? -1 : a.name > b.name ? 1 : 0)) {
|
|
86
|
+
const absolute = join(dir, entry.name);
|
|
87
|
+
const key = relative(base, absolute).split(sep).join("/");
|
|
88
|
+
// Symlinks first, and never followed: a link is recorded by its target
|
|
89
|
+
// path, so creating, deleting or retargeting one is a visible change,
|
|
90
|
+
// while the tree behind a directory link stays outside the snapshot.
|
|
91
|
+
if (entry.isSymbolicLink()) {
|
|
92
|
+
into.set(key, createHash("sha256")
|
|
93
|
+
.update(`symlink:${readlinkSync(absolute)}`)
|
|
94
|
+
.digest("hex"));
|
|
95
|
+
}
|
|
96
|
+
else if (entry.isDirectory()) {
|
|
97
|
+
walk(absolute, base, into);
|
|
98
|
+
}
|
|
99
|
+
else if (entry.isFile()) {
|
|
100
|
+
into.set(key, createHash("sha256").update(readFileSync(absolute)).digest("hex"));
|
|
101
|
+
}
|
|
102
|
+
else {
|
|
103
|
+
// FIFOs, sockets, devices: content is meaningless, presence is not.
|
|
104
|
+
into.set(key, createHash("sha256").update("special").digest("hex"));
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
/** Hashes every file under `root`; the observation diff comes from two snapshots. */
|
|
109
|
+
export function snapshotFiles(root) {
|
|
110
|
+
const snapshot = new Map();
|
|
111
|
+
walk(root, root, snapshot);
|
|
112
|
+
return snapshot;
|
|
113
|
+
}
|
|
114
|
+
/** Paths whose bytes changed, appeared or disappeared between two snapshots, sorted. */
|
|
115
|
+
export function diffSnapshots(before, after) {
|
|
116
|
+
const changed = new Set();
|
|
117
|
+
for (const [key, hash] of before)
|
|
118
|
+
if (after.get(key) !== hash)
|
|
119
|
+
changed.add(key);
|
|
120
|
+
for (const [key, hash] of after)
|
|
121
|
+
if (before.get(key) !== hash)
|
|
122
|
+
changed.add(key);
|
|
123
|
+
return [...changed].sort();
|
|
124
|
+
}
|
|
125
|
+
/**
|
|
126
|
+
* The sandbox's deterministic Project Graph revision (Delivery Graph §5)
|
|
127
|
+
* over whatever canonical state currently parses. Load problems are
|
|
128
|
+
* deliberately ignored here: a candidate that corrupts the graph is caught
|
|
129
|
+
* by the changed-files observation, while the revision proves whether the
|
|
130
|
+
* canonical state it left behind still equals the seeded state.
|
|
131
|
+
*/
|
|
132
|
+
export function sandboxRevision(root) {
|
|
133
|
+
try {
|
|
134
|
+
const nodes = loadNodes(join(root, NODES_DIR));
|
|
135
|
+
const edges = loadEdges(join(root, EDGES_FILE));
|
|
136
|
+
return graphRevision({ nodes: nodes.nodes, edges: edges.edges });
|
|
137
|
+
}
|
|
138
|
+
catch {
|
|
139
|
+
// Never equals a real sha256: revision, so revision-comparing
|
|
140
|
+
// assertions fail instead of the case erroring out.
|
|
141
|
+
return "revision-unavailable";
|
|
142
|
+
}
|
|
143
|
+
}
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
import type { Problem } from "../errors.js";
|
|
2
|
+
/** One extension the operation touched. */
|
|
3
|
+
export interface ExtensionChange {
|
|
4
|
+
readonly id: string;
|
|
5
|
+
readonly action: "added" | "removed" | "upgraded" | "unchanged";
|
|
6
|
+
readonly version?: string;
|
|
7
|
+
readonly previousVersion?: string;
|
|
8
|
+
}
|
|
9
|
+
/** Result of `pactwright extension add|remove|upgrade`. */
|
|
10
|
+
export interface ExtensionChangeReport {
|
|
11
|
+
readonly ok: boolean;
|
|
12
|
+
readonly root: string;
|
|
13
|
+
readonly changes: readonly ExtensionChange[];
|
|
14
|
+
/**
|
|
15
|
+
* GitHub profiles declared by newly added extensions. Reported only:
|
|
16
|
+
* provisioning does not exist in this checkpoint, and `github.enabled`
|
|
17
|
+
* controls whether it ever runs.
|
|
18
|
+
*/
|
|
19
|
+
readonly githubProfiles: readonly string[];
|
|
20
|
+
/**
|
|
21
|
+
* Paths of canonical records that stayed on disk after a removal. Removal
|
|
22
|
+
* never deletes user-authored extension graph data; the user chooses
|
|
23
|
+
* separately whether to delete it.
|
|
24
|
+
*/
|
|
25
|
+
readonly preserved: readonly string[];
|
|
26
|
+
readonly problems: readonly Problem[];
|
|
27
|
+
}
|
|
28
|
+
/**
|
|
29
|
+
* Enables an extension (Distribution §4): resolve the package, resolve and
|
|
30
|
+
* enable missing dependencies first, validate the complete required
|
|
31
|
+
* capability union, then record the exact state in config and lock. Fails
|
|
32
|
+
* before any write when compatibility is incomplete; rolls both files back
|
|
33
|
+
* if the resulting project state does not validate.
|
|
34
|
+
*
|
|
35
|
+
* "Missing" means not enabled, not merely absent: a dependency the
|
|
36
|
+
* configuration already names but has disabled is enabled too. So adding an
|
|
37
|
+
* extension that is itself already enabled still repairs a disabled
|
|
38
|
+
* dependency underneath it, at any depth, and reports what it enabled rather
|
|
39
|
+
* than `unchanged`.
|
|
40
|
+
*
|
|
41
|
+
* Because the walk crosses already-enabled dependencies to reach what is
|
|
42
|
+
* below them, an add whose enabled dependency is itself broken reports that
|
|
43
|
+
* problem rather than `unchanged`. The write would have failed on it anyway;
|
|
44
|
+
* saying so up front is the more truthful answer.
|
|
45
|
+
*/
|
|
46
|
+
export declare function addExtension(root: string, spec: string): ExtensionChangeReport;
|
|
47
|
+
/**
|
|
48
|
+
* Removes an extension (Distribution §4). Blocked while an enabled
|
|
49
|
+
* extension still depends on it. Canonical graph data owned by the
|
|
50
|
+
* extension is never deleted — it is reported as preserved, and the user
|
|
51
|
+
* chooses separately whether to delete it.
|
|
52
|
+
*
|
|
53
|
+
* Removal is the remedy for a broken extension set, so nothing about that set
|
|
54
|
+
* being broken may block it: the dependant scan runs best effort, and when the
|
|
55
|
+
* remaining configuration still does not resolve the lock is derived from its
|
|
56
|
+
* previous contents rather than re-resolved. Both degradations are reported.
|
|
57
|
+
*/
|
|
58
|
+
export declare function removeExtension(root: string, id: string): ExtensionChangeReport;
|
|
59
|
+
/**
|
|
60
|
+
* Upgrades an extension (Distribution §15): re-resolves the configured
|
|
61
|
+
* package, validates the complete dependency graph and capability union,
|
|
62
|
+
* and updates the lock. The configuration is desired state and does not
|
|
63
|
+
* change; canonical Project Graph state is never reinterpreted.
|
|
64
|
+
*/
|
|
65
|
+
export declare function upgradeExtension(root: string, id: string): ExtensionChangeReport;
|