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
package/dist/cli.js
ADDED
|
@@ -0,0 +1,561 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { formatProblem, PactwrightError } from "./errors.js";
|
|
3
|
+
import { lifecycleNext, lifecycleStatus, } from "./lifecycle/engine.js";
|
|
4
|
+
import { noExecutor, runLifecycle } from "./lifecycle/run.js";
|
|
5
|
+
import { recordStage } from "./lifecycle/record.js";
|
|
6
|
+
import { loadContext } from "./context.js";
|
|
7
|
+
import { loadConfig } from "./config/config.js";
|
|
8
|
+
import { CORE_DELIVERY_SUITE } from "./eval/core-suite.js";
|
|
9
|
+
import { evalPassed, runEval } from "./eval/runner.js";
|
|
10
|
+
import { addExtension, removeExtension, upgradeExtension, } from "./extension/manage.js";
|
|
11
|
+
import { initProject } from "./init.js";
|
|
12
|
+
import { syncProject } from "./sync.js";
|
|
13
|
+
import { loadProject } from "./loader.js";
|
|
14
|
+
import { resolvePack } from "./pack/resolve.js";
|
|
15
|
+
import { validateProject } from "./validate.js";
|
|
16
|
+
import { findProjectRoot, projectPaths } from "./project.js";
|
|
17
|
+
import { runtimeVersion } from "./version.js";
|
|
18
|
+
const HELP = `Usage: pactwright <command> [options]
|
|
19
|
+
|
|
20
|
+
Commands:
|
|
21
|
+
init [--json] Create the Pactwright-owned core structure
|
|
22
|
+
(.pactwright, specs, .claude directories) in the
|
|
23
|
+
current directory and resolve the lock; existing
|
|
24
|
+
paths are left untouched
|
|
25
|
+
sync [--json] Render the Pactwright-managed .claude/ adapter
|
|
26
|
+
surface from config + lock (deterministic; only
|
|
27
|
+
files carrying the Pactwright banner are written
|
|
28
|
+
or removed, so user-authored files are kept)
|
|
29
|
+
validate [--json] Validate the Delivery Graph and typed-edge store
|
|
30
|
+
context <node-id> [--history] [--json] Print the current core Delivery lineage of a node
|
|
31
|
+
lifecycle status [--intent <id>] [--json] Report stage, completed stages, gates and lineage
|
|
32
|
+
lifecycle next [--intent <id>] [--json] Report the next permitted core Delivery action
|
|
33
|
+
lifecycle run [--intent <id>] [--json] Run automatic stages until a gate, completion,
|
|
34
|
+
a stage failure or a validation error
|
|
35
|
+
lifecycle record <stage> --file <yaml> Record the content of a graph-marking stage
|
|
36
|
+
(capture-intent, approve-contract, write-brief,
|
|
37
|
+
prepare-evidence) after the runtime checks the
|
|
38
|
+
transition
|
|
39
|
+
extension add <id|package> [--json] Enable an extension (and its dependencies),
|
|
40
|
+
validate the capability union and update
|
|
41
|
+
config and lock
|
|
42
|
+
extension remove <id> [--json] Disable and remove an extension; blocked while
|
|
43
|
+
enabled extensions depend on it, and canonical
|
|
44
|
+
extension data is preserved
|
|
45
|
+
extension upgrade <id> [--json] Re-resolve an extension and update the lock
|
|
46
|
+
eval [--json] Run the core Delivery evaluation suite against
|
|
47
|
+
the selected agent pack (deterministic assertions
|
|
48
|
+
and semantic dimensions reported separately)
|
|
49
|
+
|
|
50
|
+
Options:
|
|
51
|
+
-h, --help Show this help
|
|
52
|
+
-v, --version Print the runtime version
|
|
53
|
+
`;
|
|
54
|
+
function parseOptions(args, allow = {}) {
|
|
55
|
+
let intent;
|
|
56
|
+
let file;
|
|
57
|
+
let json = false;
|
|
58
|
+
let history = false;
|
|
59
|
+
const positional = [];
|
|
60
|
+
for (let i = 0; i < args.length; i += 1) {
|
|
61
|
+
const arg = args[i];
|
|
62
|
+
if (arg === "--json")
|
|
63
|
+
json = true;
|
|
64
|
+
else if (arg === "--history" && allow.history === true)
|
|
65
|
+
history = true;
|
|
66
|
+
else if (arg === "--intent" && allow.intent === true) {
|
|
67
|
+
intent = args[i + 1];
|
|
68
|
+
if (intent === undefined || intent.startsWith("--"))
|
|
69
|
+
return "--intent needs an intent id";
|
|
70
|
+
i += 1;
|
|
71
|
+
}
|
|
72
|
+
else if (arg === "--file" && allow.file === true) {
|
|
73
|
+
file = args[i + 1];
|
|
74
|
+
if (file === undefined || file.startsWith("--"))
|
|
75
|
+
return "--file needs a path";
|
|
76
|
+
i += 1;
|
|
77
|
+
}
|
|
78
|
+
else if (arg.startsWith("--"))
|
|
79
|
+
return `unknown option "${arg}"`;
|
|
80
|
+
else
|
|
81
|
+
positional.push(arg);
|
|
82
|
+
}
|
|
83
|
+
return {
|
|
84
|
+
json,
|
|
85
|
+
history,
|
|
86
|
+
positional,
|
|
87
|
+
...(intent === undefined ? {} : { intent }),
|
|
88
|
+
...(file === undefined ? {} : { file }),
|
|
89
|
+
};
|
|
90
|
+
}
|
|
91
|
+
const out = (text) => void process.stdout.write(text);
|
|
92
|
+
const err = (text) => void process.stderr.write(text);
|
|
93
|
+
function printProblems(error, json) {
|
|
94
|
+
if (json) {
|
|
95
|
+
out(`${JSON.stringify({ problems: error.problems }, null, 2)}\n`);
|
|
96
|
+
return;
|
|
97
|
+
}
|
|
98
|
+
out("Validation problems:\n");
|
|
99
|
+
for (const problem of error.problems)
|
|
100
|
+
out(` - ${formatProblem(problem)}\n`);
|
|
101
|
+
}
|
|
102
|
+
function formatStatus(entry) {
|
|
103
|
+
const lines = [];
|
|
104
|
+
lines.push(entry.intent === undefined ? "No active lineage" : `Intent: ${entry.intent}`);
|
|
105
|
+
lines.push(` state: ${entry.state}`);
|
|
106
|
+
lines.push(` current stage: ${entry.currentStage ?? "none (core Delivery lifecycle complete or terminal)"}`);
|
|
107
|
+
lines.push(` completed stages: ${entry.completed.length === 0 ? "none" : entry.completed.join(", ")}`);
|
|
108
|
+
if (entry.blockedStage !== undefined) {
|
|
109
|
+
lines.push(` blocked stage: ${entry.blockedStage} (required actor: ${entry.requiredActor})`);
|
|
110
|
+
}
|
|
111
|
+
if (entry.lineage !== undefined) {
|
|
112
|
+
const { decision, contract, brief, evidence } = entry.lineage;
|
|
113
|
+
const chain = [entry.lineage.intent, decision, contract, brief, evidence]
|
|
114
|
+
.filter((node) => node !== undefined)
|
|
115
|
+
.map((node) => node.id);
|
|
116
|
+
lines.push(` current lineage: ${chain.join(" → ")}`);
|
|
117
|
+
}
|
|
118
|
+
return `${lines.join("\n")}\n`;
|
|
119
|
+
}
|
|
120
|
+
function formatNext(action) {
|
|
121
|
+
const who = action.intent === undefined ? "No active lineage" : `Intent: ${action.intent}`;
|
|
122
|
+
const stage = action.stage === undefined
|
|
123
|
+
? "next stage: none"
|
|
124
|
+
: `next stage: ${action.stage} (${action.execution}${action.actor ? `, actor ${action.actor}` : ""}${action.gate ? ", human gate" : ""})`;
|
|
125
|
+
return `${who}\n ${stage}\n ${action.reason}\n`;
|
|
126
|
+
}
|
|
127
|
+
function formatRun(result) {
|
|
128
|
+
const lines = [];
|
|
129
|
+
lines.push(result.intent === undefined ? "No active lineage" : `Intent: ${result.intent}`);
|
|
130
|
+
lines.push(` executed: ${result.executed.length === 0 ? "none" : result.executed.join(", ")}`);
|
|
131
|
+
switch (result.stop) {
|
|
132
|
+
case "completed":
|
|
133
|
+
lines.push(" stopped: lifecycle complete or no automatic stage to run");
|
|
134
|
+
break;
|
|
135
|
+
case "human-gate":
|
|
136
|
+
lines.push(` stopped: human gate at ${result.stage} (required actor: ${result.requiredActor})`);
|
|
137
|
+
break;
|
|
138
|
+
case "stage-failed":
|
|
139
|
+
lines.push(` stopped: stage ${result.stage} failed: ${result.message}`);
|
|
140
|
+
break;
|
|
141
|
+
case "validation-error":
|
|
142
|
+
lines.push(` stopped: validation error: ${result.message}`);
|
|
143
|
+
break;
|
|
144
|
+
}
|
|
145
|
+
return `${lines.join("\n")}\n`;
|
|
146
|
+
}
|
|
147
|
+
function record(args) {
|
|
148
|
+
const options = parseOptions(args, { file: true });
|
|
149
|
+
if (typeof options === "string" ||
|
|
150
|
+
options.positional.length !== 1 ||
|
|
151
|
+
options.file === undefined) {
|
|
152
|
+
const why = typeof options === "string"
|
|
153
|
+
? options
|
|
154
|
+
: options.positional.length === 0
|
|
155
|
+
? "record needs a <stage>"
|
|
156
|
+
: options.positional.length > 1
|
|
157
|
+
? `unexpected argument "${options.positional[1]}"`
|
|
158
|
+
: "record needs --file <yaml>";
|
|
159
|
+
err(`pactwright: ${why}\n\n${HELP}`);
|
|
160
|
+
return 1;
|
|
161
|
+
}
|
|
162
|
+
try {
|
|
163
|
+
const root = findProjectRoot();
|
|
164
|
+
const result = recordStage(root, options.positional[0], options.file);
|
|
165
|
+
if (options.json) {
|
|
166
|
+
const created = result.created.map((node) => ({ id: node.id, type: node.type }));
|
|
167
|
+
out(`${JSON.stringify({ stage: result.stage, created }, null, 2)}\n`);
|
|
168
|
+
}
|
|
169
|
+
else {
|
|
170
|
+
out(result.created.map((node) => `created ${node.type} ${node.id}\n`).join(""));
|
|
171
|
+
}
|
|
172
|
+
return 0;
|
|
173
|
+
}
|
|
174
|
+
catch (error) {
|
|
175
|
+
if (!(error instanceof PactwrightError))
|
|
176
|
+
throw error;
|
|
177
|
+
printProblems(error, options.json);
|
|
178
|
+
return 1;
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
async function lifecycle(sub, args) {
|
|
182
|
+
if (sub === "record")
|
|
183
|
+
return record(args);
|
|
184
|
+
const options = parseOptions(args, { intent: true });
|
|
185
|
+
if (typeof options === "string" || options.positional.length > 0) {
|
|
186
|
+
const why = typeof options === "string" ? options : `unexpected argument "${options.positional[0]}"`;
|
|
187
|
+
err(`pactwright: ${why}\n\n${HELP}`);
|
|
188
|
+
return 1;
|
|
189
|
+
}
|
|
190
|
+
if (sub === "run") {
|
|
191
|
+
// `runLifecycle` loads the project itself and reports load problems as a
|
|
192
|
+
// validation-error stop; only the root is resolved here.
|
|
193
|
+
let root;
|
|
194
|
+
try {
|
|
195
|
+
root = findProjectRoot();
|
|
196
|
+
}
|
|
197
|
+
catch (error) {
|
|
198
|
+
if (!(error instanceof PactwrightError))
|
|
199
|
+
throw error;
|
|
200
|
+
printProblems(error, options.json);
|
|
201
|
+
return 1;
|
|
202
|
+
}
|
|
203
|
+
const results = await runLifecycle({
|
|
204
|
+
root,
|
|
205
|
+
execute: noExecutor,
|
|
206
|
+
...(options.intent === undefined ? {} : { intentId: options.intent }),
|
|
207
|
+
});
|
|
208
|
+
out(options.json ? `${JSON.stringify(results, null, 2)}\n` : results.map(formatRun).join(""));
|
|
209
|
+
return results.some((r) => r.stop === "stage-failed" || r.stop === "validation-error") ? 1 : 0;
|
|
210
|
+
}
|
|
211
|
+
if (sub !== "status" && sub !== "next") {
|
|
212
|
+
err(`pactwright: unknown lifecycle command "${sub ?? ""}"\n\n${HELP}`);
|
|
213
|
+
return 1;
|
|
214
|
+
}
|
|
215
|
+
try {
|
|
216
|
+
const project = loadProject();
|
|
217
|
+
if (sub === "status") {
|
|
218
|
+
const status = lifecycleStatus(project, options.intent);
|
|
219
|
+
out(options.json
|
|
220
|
+
? `${JSON.stringify(status, null, 2)}\n`
|
|
221
|
+
: `${status.lineages.map(formatStatus).join("")}Validation problems: none\n`);
|
|
222
|
+
}
|
|
223
|
+
else {
|
|
224
|
+
const actions = lifecycleNext(project, options.intent);
|
|
225
|
+
out(options.json ? `${JSON.stringify(actions, null, 2)}\n` : actions.map(formatNext).join(""));
|
|
226
|
+
}
|
|
227
|
+
return 0;
|
|
228
|
+
}
|
|
229
|
+
catch (error) {
|
|
230
|
+
if (!(error instanceof PactwrightError))
|
|
231
|
+
throw error;
|
|
232
|
+
printProblems(error, options.json);
|
|
233
|
+
return 1;
|
|
234
|
+
}
|
|
235
|
+
}
|
|
236
|
+
function initCommand(args) {
|
|
237
|
+
const options = parseOptions(args);
|
|
238
|
+
if (typeof options === "string" || options.positional.length > 0) {
|
|
239
|
+
const why = typeof options === "string" ? options : `unexpected argument "${options.positional[0]}"`;
|
|
240
|
+
err(`pactwright: ${why}\n\n${HELP}`);
|
|
241
|
+
return 1;
|
|
242
|
+
}
|
|
243
|
+
// Init is the one command that must not search for an enclosing project:
|
|
244
|
+
// it creates the project in the current directory.
|
|
245
|
+
const report = initProject();
|
|
246
|
+
if (options.json) {
|
|
247
|
+
out(`${JSON.stringify(report, null, 2)}\n`);
|
|
248
|
+
}
|
|
249
|
+
else {
|
|
250
|
+
for (const entry of report.entries) {
|
|
251
|
+
out(entry.action === "created" ? `created ${entry.path}\n` : `skipped ${entry.path} (exists)\n`);
|
|
252
|
+
}
|
|
253
|
+
if (report.problems.length > 0) {
|
|
254
|
+
out("Validation problems:\n");
|
|
255
|
+
for (const problem of report.problems)
|
|
256
|
+
out(` - ${formatProblem(problem)}\n`);
|
|
257
|
+
}
|
|
258
|
+
}
|
|
259
|
+
return report.ok ? 0 : 1;
|
|
260
|
+
}
|
|
261
|
+
function formatExtensionReport(report) {
|
|
262
|
+
const lines = [];
|
|
263
|
+
for (const change of report.changes) {
|
|
264
|
+
const version = change.action === "upgraded" && change.previousVersion !== undefined
|
|
265
|
+
? ` ${change.previousVersion} → ${change.version ?? "?"}`
|
|
266
|
+
: change.version !== undefined
|
|
267
|
+
? ` ${change.version}`
|
|
268
|
+
: "";
|
|
269
|
+
lines.push(`${change.action} ${change.id}${version}`);
|
|
270
|
+
}
|
|
271
|
+
for (const profile of report.githubProfiles) {
|
|
272
|
+
lines.push(`github profile "${profile}" requires provisioning (not performed: deferred)`);
|
|
273
|
+
}
|
|
274
|
+
if (report.preserved.length > 0) {
|
|
275
|
+
lines.push("preserved canonical extension data (delete separately if unwanted):");
|
|
276
|
+
for (const path of report.preserved)
|
|
277
|
+
lines.push(` - ${path}`);
|
|
278
|
+
}
|
|
279
|
+
return lines.map((line) => `${line}\n`).join("");
|
|
280
|
+
}
|
|
281
|
+
const EXTENSION_OPERATIONS = {
|
|
282
|
+
add: addExtension,
|
|
283
|
+
remove: removeExtension,
|
|
284
|
+
upgrade: upgradeExtension,
|
|
285
|
+
};
|
|
286
|
+
function extensionOperation(sub) {
|
|
287
|
+
// `Object.hasOwn`, not a bare lookup: `sub` is user input, and an inherited
|
|
288
|
+
// member like "constructor" would otherwise pass an `=== undefined` guard
|
|
289
|
+
// and be called as if it were an extension operation.
|
|
290
|
+
return sub !== undefined && Object.hasOwn(EXTENSION_OPERATIONS, sub)
|
|
291
|
+
? EXTENSION_OPERATIONS[sub]
|
|
292
|
+
: undefined;
|
|
293
|
+
}
|
|
294
|
+
function extensionCommand(sub, args) {
|
|
295
|
+
// The verb is checked before the arguments, so an unknown verb is named as
|
|
296
|
+
// such rather than being reported as a missing extension id.
|
|
297
|
+
const operation = extensionOperation(sub);
|
|
298
|
+
if (operation === undefined) {
|
|
299
|
+
err(`pactwright: unknown extension command "${sub ?? ""}"\n\n${HELP}`);
|
|
300
|
+
return 1;
|
|
301
|
+
}
|
|
302
|
+
const options = parseOptions(args);
|
|
303
|
+
if (typeof options === "string" || options.positional.length !== 1) {
|
|
304
|
+
const why = typeof options === "string"
|
|
305
|
+
? options
|
|
306
|
+
: options.positional.length === 0
|
|
307
|
+
? `extension ${sub} needs an extension id`
|
|
308
|
+
: `unexpected argument "${options.positional[1]}"`;
|
|
309
|
+
err(`pactwright: ${why}\n\n${HELP}`);
|
|
310
|
+
return 1;
|
|
311
|
+
}
|
|
312
|
+
let root;
|
|
313
|
+
try {
|
|
314
|
+
root = findProjectRoot();
|
|
315
|
+
}
|
|
316
|
+
catch (error) {
|
|
317
|
+
if (!(error instanceof PactwrightError))
|
|
318
|
+
throw error;
|
|
319
|
+
printProblems(error, options.json);
|
|
320
|
+
return 1;
|
|
321
|
+
}
|
|
322
|
+
const report = operation(root, options.positional[0]);
|
|
323
|
+
if (options.json) {
|
|
324
|
+
out(`${JSON.stringify(report, null, 2)}\n`);
|
|
325
|
+
}
|
|
326
|
+
else {
|
|
327
|
+
out(formatExtensionReport(report));
|
|
328
|
+
if (report.problems.length > 0) {
|
|
329
|
+
// A successful command can still carry an advisory, so the heading
|
|
330
|
+
// follows the outcome rather than the presence of problems.
|
|
331
|
+
out(report.ok ? "Notes:\n" : "Validation problems:\n");
|
|
332
|
+
for (const problem of report.problems)
|
|
333
|
+
out(` - ${formatProblem(problem)}\n`);
|
|
334
|
+
}
|
|
335
|
+
}
|
|
336
|
+
return report.ok ? 0 : 1;
|
|
337
|
+
}
|
|
338
|
+
function syncCommand(args) {
|
|
339
|
+
const options = parseOptions(args);
|
|
340
|
+
if (typeof options === "string" || options.positional.length > 0) {
|
|
341
|
+
const why = typeof options === "string" ? options : `unexpected argument "${options.positional[0]}"`;
|
|
342
|
+
err(`pactwright: ${why}\n\n${HELP}`);
|
|
343
|
+
return 1;
|
|
344
|
+
}
|
|
345
|
+
let root;
|
|
346
|
+
try {
|
|
347
|
+
root = findProjectRoot();
|
|
348
|
+
}
|
|
349
|
+
catch (error) {
|
|
350
|
+
if (!(error instanceof PactwrightError))
|
|
351
|
+
throw error;
|
|
352
|
+
printProblems(error, options.json);
|
|
353
|
+
return 1;
|
|
354
|
+
}
|
|
355
|
+
const report = syncProject(root);
|
|
356
|
+
if (options.json) {
|
|
357
|
+
out(`${JSON.stringify(report, null, 2)}\n`);
|
|
358
|
+
}
|
|
359
|
+
else {
|
|
360
|
+
for (const path of report.changed)
|
|
361
|
+
out(`wrote ${path}\n`);
|
|
362
|
+
for (const path of report.unchanged)
|
|
363
|
+
out(`unchanged ${path}\n`);
|
|
364
|
+
for (const path of report.removed)
|
|
365
|
+
out(`removed ${path}\n`);
|
|
366
|
+
for (const path of report.kept)
|
|
367
|
+
out(`kept ${path} (not pactwright-generated)\n`);
|
|
368
|
+
for (const path of report.conflicts)
|
|
369
|
+
out(`conflict ${path} (not pactwright-generated)\n`);
|
|
370
|
+
if (report.problems.length > 0) {
|
|
371
|
+
out("Validation problems:\n");
|
|
372
|
+
for (const problem of report.problems)
|
|
373
|
+
out(` - ${formatProblem(problem)}\n`);
|
|
374
|
+
}
|
|
375
|
+
}
|
|
376
|
+
return report.ok ? 0 : 1;
|
|
377
|
+
}
|
|
378
|
+
function validate(args) {
|
|
379
|
+
const options = parseOptions(args);
|
|
380
|
+
if (typeof options === "string" || options.positional.length > 0) {
|
|
381
|
+
const why = typeof options === "string" ? options : `unexpected argument "${options.positional[0]}"`;
|
|
382
|
+
err(`pactwright: ${why}\n\n${HELP}`);
|
|
383
|
+
return 1;
|
|
384
|
+
}
|
|
385
|
+
const report = validateProject();
|
|
386
|
+
if (options.json) {
|
|
387
|
+
out(`${JSON.stringify(report, null, 2)}\n`);
|
|
388
|
+
}
|
|
389
|
+
else if (report.ok) {
|
|
390
|
+
const s = report.summary;
|
|
391
|
+
out(`Valid: ${s.nodes} nodes, ${s.edges} edges, ${s.lineages} lineages (revision ${s.revision})\n`);
|
|
392
|
+
}
|
|
393
|
+
else {
|
|
394
|
+
out("Validation problems:\n");
|
|
395
|
+
for (const problem of report.problems)
|
|
396
|
+
out(` - ${formatProblem(problem)}\n`);
|
|
397
|
+
}
|
|
398
|
+
return report.ok ? 0 : 1;
|
|
399
|
+
}
|
|
400
|
+
function formatRecord(node, extra = []) {
|
|
401
|
+
const lines = [`## ${node.type} ${node.id}`, `title: ${node.title}`, `created: ${node.created}`];
|
|
402
|
+
if (node.type === "decision") {
|
|
403
|
+
lines.push(`decided_by: ${String(node.frontmatter["decided_by"])}`);
|
|
404
|
+
lines.push(`outcome: ${String(node.frontmatter["outcome"])}`);
|
|
405
|
+
}
|
|
406
|
+
lines.push(...extra, "", node.body, "");
|
|
407
|
+
return `${lines.join("\n")}\n`;
|
|
408
|
+
}
|
|
409
|
+
function formatHistory(record) {
|
|
410
|
+
const by = record.supersededBy.length === 0 ? "none" : record.supersededBy.join(", ");
|
|
411
|
+
return formatRecord(record.node, [`superseded by: ${by}`]);
|
|
412
|
+
}
|
|
413
|
+
function formatContext(context) {
|
|
414
|
+
const parts = [`# Delivery context for ${context.requested}`, ""];
|
|
415
|
+
parts.push(`intent: ${context.intent}`, `state: ${context.state}`);
|
|
416
|
+
if (!context.requestedIsCurrent) {
|
|
417
|
+
parts.push(`note: ${context.requested} is superseded; the current lineage is shown`);
|
|
418
|
+
}
|
|
419
|
+
parts.push("", ...context.lineage.map((node) => formatRecord(node)));
|
|
420
|
+
if (context.history !== undefined) {
|
|
421
|
+
parts.push("# History (superseded records)", "");
|
|
422
|
+
parts.push(context.history.length === 0 ? "none\n" : context.history.map(formatHistory).join(""));
|
|
423
|
+
}
|
|
424
|
+
return `${parts.join("\n")}`;
|
|
425
|
+
}
|
|
426
|
+
function formatEvalCase(entry) {
|
|
427
|
+
const lines = [];
|
|
428
|
+
const agent = entry.agent === undefined ? "" : ` (agent: ${entry.agent})`;
|
|
429
|
+
lines.push(`${entry.id} — ${entry.title}`);
|
|
430
|
+
lines.push(` capability: ${entry.capability}${agent}`);
|
|
431
|
+
if (entry.error !== undefined)
|
|
432
|
+
lines.push(` error: ${entry.error}`);
|
|
433
|
+
if (entry.deterministic.length > 0) {
|
|
434
|
+
lines.push(" deterministic:");
|
|
435
|
+
for (const assertion of entry.deterministic) {
|
|
436
|
+
lines.push(` ${assertion.passed ? "pass" : "FAIL"} ${assertion.id}: ${assertion.detail}`);
|
|
437
|
+
}
|
|
438
|
+
}
|
|
439
|
+
if (entry.semantic.length > 0) {
|
|
440
|
+
lines.push(" semantic (requires judgement; reported separately, never auto-scored):");
|
|
441
|
+
for (const dimension of entry.semantic) {
|
|
442
|
+
lines.push(dimension.judged
|
|
443
|
+
? ` judged ${dimension.id}: ${dimension.verdict}${dimension.rationale === undefined ? "" : ` — ${dimension.rationale}`}`
|
|
444
|
+
: ` unjudged ${dimension.id}: ${dimension.reason}`);
|
|
445
|
+
}
|
|
446
|
+
}
|
|
447
|
+
return `${lines.join("\n")}\n`;
|
|
448
|
+
}
|
|
449
|
+
function formatEvalReport(report) {
|
|
450
|
+
const assertions = report.cases.flatMap((entry) => entry.deterministic);
|
|
451
|
+
const failed = assertions.filter((assertion) => !assertion.passed).length;
|
|
452
|
+
const dimensions = report.cases.flatMap((entry) => entry.semantic);
|
|
453
|
+
const judged = dimensions.filter((dimension) => dimension.judged).length;
|
|
454
|
+
const errors = report.cases.filter((entry) => entry.error !== undefined).length;
|
|
455
|
+
return [
|
|
456
|
+
`Evaluating ${report.pack.name}@${report.pack.version} (runtime ${report.runtime}, suite ${report.suite})`,
|
|
457
|
+
"",
|
|
458
|
+
...report.cases.map(formatEvalCase),
|
|
459
|
+
`Deterministic assertions: ${assertions.length - failed} passed, ${failed} failed${errors === 0 ? "" : `; ${errors} case(s) not evaluated`}.`,
|
|
460
|
+
`Semantic dimensions: ${judged} judged, ${dimensions.length - judged} unjudged. No aggregate quality score is calculated.`,
|
|
461
|
+
"",
|
|
462
|
+
].join("\n");
|
|
463
|
+
}
|
|
464
|
+
/**
|
|
465
|
+
* `pactwright eval` runs inside or outside a project: inside one it
|
|
466
|
+
* evaluates the configured pack; outside it evaluates the default
|
|
467
|
+
* `@pactwright/standard` pack, since evaluation is independent from any
|
|
468
|
+
* project's Delivery (Distribution §16).
|
|
469
|
+
*/
|
|
470
|
+
async function evalCommand(args) {
|
|
471
|
+
const options = parseOptions(args);
|
|
472
|
+
if (typeof options === "string" || options.positional.length > 0) {
|
|
473
|
+
const why = typeof options === "string" ? options : `unexpected argument "${options.positional[0]}"`;
|
|
474
|
+
err(`pactwright: ${why}\n\n${HELP}`);
|
|
475
|
+
return 1;
|
|
476
|
+
}
|
|
477
|
+
let root;
|
|
478
|
+
let config;
|
|
479
|
+
try {
|
|
480
|
+
root = findProjectRoot();
|
|
481
|
+
const loaded = loadConfig(projectPaths(root).config);
|
|
482
|
+
if (loaded.value === undefined) {
|
|
483
|
+
printProblems(PactwrightError.fromProblems("invalid-config", loaded.problems), options.json);
|
|
484
|
+
return 1;
|
|
485
|
+
}
|
|
486
|
+
config = loaded.value;
|
|
487
|
+
}
|
|
488
|
+
catch (error) {
|
|
489
|
+
if (!(error instanceof PactwrightError))
|
|
490
|
+
throw error;
|
|
491
|
+
root = process.cwd();
|
|
492
|
+
config = {
|
|
493
|
+
version: 1,
|
|
494
|
+
agentPack: { source: "@pactwright/standard" },
|
|
495
|
+
adapter: { type: "claude-code" },
|
|
496
|
+
extensions: {},
|
|
497
|
+
github: { enabled: false },
|
|
498
|
+
};
|
|
499
|
+
}
|
|
500
|
+
const resolved = resolvePack({ root, config });
|
|
501
|
+
if (resolved.value === undefined) {
|
|
502
|
+
printProblems(PactwrightError.fromProblems("pack-unresolved", resolved.problems), options.json);
|
|
503
|
+
return 1;
|
|
504
|
+
}
|
|
505
|
+
const report = await runEval({ pack: resolved.value, suite: CORE_DELIVERY_SUITE });
|
|
506
|
+
out(options.json ? `${JSON.stringify(report, null, 2)}\n` : formatEvalReport(report));
|
|
507
|
+
return evalPassed(report) ? 0 : 1;
|
|
508
|
+
}
|
|
509
|
+
function contextCommand(args) {
|
|
510
|
+
const options = parseOptions(args, { history: true });
|
|
511
|
+
if (typeof options === "string" || options.positional.length !== 1) {
|
|
512
|
+
const why = typeof options === "string"
|
|
513
|
+
? options
|
|
514
|
+
: options.positional.length === 0
|
|
515
|
+
? "context needs a <node-id>"
|
|
516
|
+
: `unexpected argument "${options.positional[1]}"`;
|
|
517
|
+
err(`pactwright: ${why}\n\n${HELP}`);
|
|
518
|
+
return 1;
|
|
519
|
+
}
|
|
520
|
+
try {
|
|
521
|
+
const context = loadContext(loadProject(), options.positional[0], {
|
|
522
|
+
history: options.history,
|
|
523
|
+
});
|
|
524
|
+
out(options.json ? `${JSON.stringify(context, null, 2)}\n` : formatContext(context));
|
|
525
|
+
return 0;
|
|
526
|
+
}
|
|
527
|
+
catch (error) {
|
|
528
|
+
if (!(error instanceof PactwrightError))
|
|
529
|
+
throw error;
|
|
530
|
+
printProblems(error, options.json);
|
|
531
|
+
return 1;
|
|
532
|
+
}
|
|
533
|
+
}
|
|
534
|
+
export async function main(argv) {
|
|
535
|
+
const [first, ...rest] = argv;
|
|
536
|
+
if (first === undefined || first === "--help" || first === "-h" || first === "help") {
|
|
537
|
+
out(HELP);
|
|
538
|
+
return first === undefined ? 1 : 0;
|
|
539
|
+
}
|
|
540
|
+
if (first === "--version" || first === "-v" || first === "version") {
|
|
541
|
+
out(`${runtimeVersion()}\n`);
|
|
542
|
+
return 0;
|
|
543
|
+
}
|
|
544
|
+
if (first === "init")
|
|
545
|
+
return initCommand(rest);
|
|
546
|
+
if (first === "sync")
|
|
547
|
+
return syncCommand(rest);
|
|
548
|
+
if (first === "extension")
|
|
549
|
+
return extensionCommand(rest[0], rest.slice(1));
|
|
550
|
+
if (first === "lifecycle")
|
|
551
|
+
return lifecycle(rest[0], rest.slice(1));
|
|
552
|
+
if (first === "validate")
|
|
553
|
+
return validate(rest);
|
|
554
|
+
if (first === "context")
|
|
555
|
+
return contextCommand(rest);
|
|
556
|
+
if (first === "eval")
|
|
557
|
+
return evalCommand(rest);
|
|
558
|
+
err(`pactwright: unknown command "${first}"\n\n${HELP}`);
|
|
559
|
+
return 1;
|
|
560
|
+
}
|
|
561
|
+
process.exitCode = await main(process.argv.slice(2));
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
import type { Problem } from "../errors.js";
|
|
2
|
+
/** One configured extension: desired state only (Distribution §4). */
|
|
3
|
+
export interface ConfigExtension {
|
|
4
|
+
/** A disabled extension keeps its graph types registered but contributes no behaviour. */
|
|
5
|
+
readonly enabled: boolean;
|
|
6
|
+
/** Package name or project-relative path the extension resolves from. */
|
|
7
|
+
readonly source: string;
|
|
8
|
+
}
|
|
9
|
+
/** `.pactwright/config.yml` — desired installation state (Distribution §3). */
|
|
10
|
+
export interface PactwrightConfig {
|
|
11
|
+
readonly version: 1;
|
|
12
|
+
readonly agentPack: {
|
|
13
|
+
readonly source: string;
|
|
14
|
+
readonly version?: string;
|
|
15
|
+
};
|
|
16
|
+
readonly adapter: {
|
|
17
|
+
readonly type: "claude-code";
|
|
18
|
+
};
|
|
19
|
+
/** Extension id → desired extension state (Distribution §4). */
|
|
20
|
+
readonly extensions: Readonly<Record<string, ConfigExtension>>;
|
|
21
|
+
readonly github: {
|
|
22
|
+
readonly enabled: boolean;
|
|
23
|
+
};
|
|
24
|
+
}
|
|
25
|
+
export declare const CONFIG_VERSION = 1;
|
|
26
|
+
export declare const ADAPTER_TYPES: readonly ["claude-code"];
|
|
27
|
+
export interface ParseResult<T> {
|
|
28
|
+
readonly value: T | undefined;
|
|
29
|
+
readonly problems: readonly Problem[];
|
|
30
|
+
}
|
|
31
|
+
export declare function parseConfig(raw: unknown, path: string): ParseResult<PactwrightConfig>;
|
|
32
|
+
export declare function loadConfig(path: string): ParseResult<PactwrightConfig>;
|
|
33
|
+
/**
|
|
34
|
+
* Serialises a configuration in the canonical `.pactwright/config.yml`
|
|
35
|
+
* shape — the exact bytes `pactwright init` writes for the default state.
|
|
36
|
+
* This is the fallback whenever the previous file cannot be edited in place;
|
|
37
|
+
* it re-emits from parsed values, so comments and incidental layout are not
|
|
38
|
+
* carried over. `rewriteConfig` is what the commands normally use.
|
|
39
|
+
*/
|
|
40
|
+
export declare function serialiseConfig(config: PactwrightConfig): string;
|
|
41
|
+
/**
|
|
42
|
+
* The previous configuration text with only its `extensions:` block replaced
|
|
43
|
+
* (Distribution §3). `extension add` and `remove` change nothing else, so
|
|
44
|
+
* every other byte — comments, key order, spacing a team chose — survives.
|
|
45
|
+
*
|
|
46
|
+
* Falls back to a full `serialiseConfig` rewrite whenever the edit cannot be
|
|
47
|
+
* made confidently, including when the spliced result does not parse back to
|
|
48
|
+
* the intended configuration. The fallback is today's behaviour, so the worst
|
|
49
|
+
* case is losing comments rather than corrupting the file — which matters
|
|
50
|
+
* because `removeExtension` never rolls its write back.
|
|
51
|
+
*
|
|
52
|
+
* Comments *inside* the extensions block are part of the replaced region and
|
|
53
|
+
* are not preserved.
|
|
54
|
+
*/
|
|
55
|
+
export declare function rewriteConfig(previous: string, config: PactwrightConfig): string;
|