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.
Files changed (83) hide show
  1. package/LICENSE +201 -0
  2. package/README.md +68 -0
  3. package/dist/adapter/claude-code.d.ts +53 -0
  4. package/dist/adapter/claude-code.js +241 -0
  5. package/dist/adapter/commands.d.ts +19 -0
  6. package/dist/adapter/commands.js +162 -0
  7. package/dist/atomic.d.ts +6 -0
  8. package/dist/atomic.js +11 -0
  9. package/dist/cli.d.ts +2 -0
  10. package/dist/cli.js +561 -0
  11. package/dist/config/config.d.ts +55 -0
  12. package/dist/config/config.js +199 -0
  13. package/dist/config/lifecycle.d.ts +34 -0
  14. package/dist/config/lifecycle.js +81 -0
  15. package/dist/config/lock.d.ts +43 -0
  16. package/dist/config/lock.js +141 -0
  17. package/dist/context.d.ts +59 -0
  18. package/dist/context.js +111 -0
  19. package/dist/errors.d.ts +21 -0
  20. package/dist/errors.js +25 -0
  21. package/dist/eval/case.d.ts +123 -0
  22. package/dist/eval/case.js +17 -0
  23. package/dist/eval/core-suite.d.ts +3 -0
  24. package/dist/eval/core-suite.js +431 -0
  25. package/dist/eval/runner.d.ts +75 -0
  26. package/dist/eval/runner.js +159 -0
  27. package/dist/eval/sandbox.d.ts +39 -0
  28. package/dist/eval/sandbox.js +143 -0
  29. package/dist/extension/manage.d.ts +65 -0
  30. package/dist/extension/manage.js +372 -0
  31. package/dist/extension/manifest.d.ts +36 -0
  32. package/dist/extension/manifest.js +164 -0
  33. package/dist/extension/resolve.d.ts +77 -0
  34. package/dist/extension/resolve.js +271 -0
  35. package/dist/graph/edge-schema.d.ts +55 -0
  36. package/dist/graph/edge-schema.js +0 -0
  37. package/dist/graph/edges.d.ts +22 -0
  38. package/dist/graph/edges.js +63 -0
  39. package/dist/graph/ids.d.ts +14 -0
  40. package/dist/graph/ids.js +38 -0
  41. package/dist/graph/lineage.d.ts +48 -0
  42. package/dist/graph/lineage.js +226 -0
  43. package/dist/graph/mutations.d.ts +108 -0
  44. package/dist/graph/mutations.js +356 -0
  45. package/dist/graph/nodes.d.ts +46 -0
  46. package/dist/graph/nodes.js +137 -0
  47. package/dist/graph/revision.d.ts +50 -0
  48. package/dist/graph/revision.js +75 -0
  49. package/dist/graph/schema.d.ts +54 -0
  50. package/dist/graph/schema.js +90 -0
  51. package/dist/index.d.ts +33 -0
  52. package/dist/index.js +33 -0
  53. package/dist/init.d.ts +47 -0
  54. package/dist/init.js +132 -0
  55. package/dist/lifecycle/engine.d.ts +75 -0
  56. package/dist/lifecycle/engine.js +146 -0
  57. package/dist/lifecycle/record.d.ts +18 -0
  58. package/dist/lifecycle/record.js +157 -0
  59. package/dist/lifecycle/run.d.ts +62 -0
  60. package/dist/lifecycle/run.js +167 -0
  61. package/dist/loader.d.ts +38 -0
  62. package/dist/loader.js +64 -0
  63. package/dist/pack/capabilities.d.ts +22 -0
  64. package/dist/pack/capabilities.js +31 -0
  65. package/dist/pack/locate.d.ts +22 -0
  66. package/dist/pack/locate.js +80 -0
  67. package/dist/pack/manifest.d.ts +34 -0
  68. package/dist/pack/manifest.js +168 -0
  69. package/dist/pack/resolve.d.ts +92 -0
  70. package/dist/pack/resolve.js +238 -0
  71. package/dist/project.d.ts +22 -0
  72. package/dist/project.js +37 -0
  73. package/dist/sync.d.ts +54 -0
  74. package/dist/sync.js +98 -0
  75. package/dist/validate.d.ts +23 -0
  76. package/dist/validate.js +32 -0
  77. package/dist/validation.d.ts +24 -0
  78. package/dist/validation.js +83 -0
  79. package/dist/version.d.ts +2 -0
  80. package/dist/version.js +8 -0
  81. package/dist/yaml.d.ts +12 -0
  82. package/dist/yaml.js +32 -0
  83. package/package.json +65 -0
package/dist/init.js ADDED
@@ -0,0 +1,132 @@
1
+ import { existsSync, mkdirSync, renameSync, writeFileSync } from "node:fs";
2
+ import { dirname, join } from "node:path";
3
+ import { MANAGED_DIRS } from "./adapter/claude-code.js";
4
+ import { tempSibling } from "./atomic.js";
5
+ import { loadConfig } from "./config/config.js";
6
+ import { serialiseEdges } from "./graph/mutations.js";
7
+ import { resolveDesiredState, writeLock } from "./pack/resolve.js";
8
+ import { runtimeVersion } from "./version.js";
9
+ import { CONFIG_FILE, EDGES_FILE, LIFECYCLE_FILE, LOCK_FILE, NODES_DIR, projectPaths, } from "./project.js";
10
+ import { validateProject } from "./validate.js";
11
+ /**
12
+ * Default `.pactwright/config.yml` (Distribution §3): the `@pactwright/standard`
13
+ * pack with Claude Code defaults. `github.enabled` stays `false` until GitHub
14
+ * provisioning exists (Distribution §§9–14); `init` creates no `.github/`
15
+ * content.
16
+ */
17
+ export const CONFIG_TEMPLATE = `version: 1
18
+
19
+ agent_pack:
20
+ source: "@pactwright/standard"
21
+ version: "^${runtimeVersion()}"
22
+
23
+ adapter:
24
+ type: claude-code
25
+
26
+ extensions: {}
27
+
28
+ github:
29
+ enabled: false
30
+ `;
31
+ /** Default `.pactwright/lifecycle.yml`: the human-gated core Delivery lifecycle (Delivery Graph §17). */
32
+ export const LIFECYCLE_TEMPLATE = `version: 1
33
+
34
+ stages:
35
+ capture-intent:
36
+ execution: manual
37
+ propose-contracts:
38
+ execution: automatic
39
+ approve-contract:
40
+ execution: manual
41
+ actor: human
42
+ write-brief:
43
+ execution: automatic
44
+ deliver-brief:
45
+ execution: automatic
46
+ review:
47
+ execution: automatic
48
+ prepare-evidence:
49
+ execution: automatic
50
+ `;
51
+ /** Keeps `specs/nodes/` tracked by git; `loadNodes` reads only `*.md` and never sees it. */
52
+ const NODES_KEEP_FILE = `${NODES_DIR}/.gitkeep`;
53
+ /**
54
+ * The files `init` owns, in report order: relative path → content. The lock
55
+ * is not a template — it is resolved from the on-disk configuration after
56
+ * these files exist.
57
+ */
58
+ export function initTemplates() {
59
+ return new Map([
60
+ [CONFIG_FILE, CONFIG_TEMPLATE],
61
+ [LIFECYCLE_FILE, LIFECYCLE_TEMPLATE],
62
+ [NODES_KEEP_FILE, ""],
63
+ [EDGES_FILE, serialiseEdges([])],
64
+ ]);
65
+ }
66
+ /**
67
+ * Directories `init` creates empty: the Claude Code adapter surface, filled
68
+ * by `pactwright sync` (Distribution §8) — `init` never copies runtime
69
+ * scripts, agents or commands into the repository (Distribution §2).
70
+ */
71
+ export const INIT_DIRS = MANAGED_DIRS;
72
+ /**
73
+ * Initialises the Pactwright-owned core structure (Distribution §§2–3) in
74
+ * `root`: configuration, lifecycle, the empty graph, the empty adapter
75
+ * directories, then the resolved `.pactwright/lock.yml`. Existing paths are
76
+ * never read or overwritten — each is reported `skipped` — so re-running in
77
+ * an initialised repository changes nothing. Finishes by validating the
78
+ * resulting project state; never throws for expected failures.
79
+ */
80
+ export function initProject(root = process.cwd()) {
81
+ const paths = projectPaths(root);
82
+ const entries = [];
83
+ const problems = [];
84
+ const failed = () => ({ ok: false, root: paths.root, entries, problems });
85
+ for (const [relPath, content] of initTemplates()) {
86
+ const target = join(paths.root, relPath);
87
+ if (existsSync(target)) {
88
+ entries.push({ path: relPath, kind: "file", action: "skipped" });
89
+ continue;
90
+ }
91
+ mkdirSync(dirname(target), { recursive: true });
92
+ const temp = tempSibling(target);
93
+ writeFileSync(temp, content, "utf8");
94
+ renameSync(temp, target);
95
+ entries.push({ path: relPath, kind: "file", action: "created" });
96
+ }
97
+ for (const relPath of INIT_DIRS) {
98
+ const target = join(paths.root, relPath);
99
+ if (existsSync(target)) {
100
+ entries.push({ path: relPath, kind: "dir", action: "skipped" });
101
+ }
102
+ else {
103
+ mkdirSync(target, { recursive: true });
104
+ entries.push({ path: relPath, kind: "dir", action: "created" });
105
+ }
106
+ }
107
+ // The lock records exact resolved state (Distribution §3), so a fresh one
108
+ // is derived from whatever configuration is on disk — which after the step
109
+ // above is either the template or pre-existing user content. An existing
110
+ // lock is trusted as-is; `resolveDesiredState` is used rather than
111
+ // `resolveAndLock` because the latter loads the full project, which
112
+ // requires the lock to already exist.
113
+ if (existsSync(paths.lock)) {
114
+ entries.push({ path: LOCK_FILE, kind: "file", action: "skipped" });
115
+ }
116
+ else {
117
+ const config = loadConfig(paths.config);
118
+ if (config.value === undefined) {
119
+ problems.push(...config.problems);
120
+ return failed();
121
+ }
122
+ const resolved = resolveDesiredState({ root: paths.root, config: config.value });
123
+ if (resolved.value === undefined) {
124
+ problems.push(...resolved.problems);
125
+ return failed();
126
+ }
127
+ writeLock(paths.lock, resolved.value.lock);
128
+ entries.push({ path: LOCK_FILE, kind: "file", action: "created" });
129
+ }
130
+ problems.push(...validateProject({ root: paths.root }).problems);
131
+ return { ok: problems.length === 0, root: paths.root, entries, problems };
132
+ }
@@ -0,0 +1,75 @@
1
+ import type { Problem } from "../errors.js";
2
+ import { type Actor, type ExecutionMode, type StageName } from "../config/lifecycle.js";
3
+ import { type DeliveryState, type Lineage } from "../graph/lineage.js";
4
+ import type { Project } from "../loader.js";
5
+ /**
6
+ * Stages that leave a durable record in the Delivery Graph, and the record
7
+ * they leave (Delivery Graph §§6–12): capture-intent → Intent,
8
+ * approve-contract → Decision (+ Contract), write-brief → Brief,
9
+ * prepare-evidence → Evidence.
10
+ */
11
+ export declare const GRAPH_MARKING_STAGES: readonly ["capture-intent", "approve-contract", "write-brief", "prepare-evidence"];
12
+ /**
13
+ * Stages whose output is transient (§7 alternatives, §11 delivery execution
14
+ * and review): they never mutate the graph, so completion is only known
15
+ * inside the `lifecycle run` that executed them.
16
+ */
17
+ export declare const TRANSIENT_STAGES: readonly ["propose-contracts", "deliver-brief", "review"];
18
+ export declare function isTransientStage(stage: StageName): boolean;
19
+ /** Stages already completed by a lineage, in lifecycle order. */
20
+ export declare function completedStages(lineage: Lineage | undefined): readonly StageName[];
21
+ /**
22
+ * Stages still to run for a lineage, in order. Empty for terminal lineages.
23
+ * With no lineage, the only pending stage is capture-intent.
24
+ */
25
+ export declare function pendingStages(lineage: Lineage | undefined): readonly StageName[];
26
+ /** A lineage still progressing through the core lifecycle. */
27
+ export declare function isActive(lineage: Lineage): boolean;
28
+ /** `lifecycle status` for one lineage (Delivery Graph §20). */
29
+ export interface LineageStatus {
30
+ /** Absent for the "no lineage yet" entry. */
31
+ readonly intent?: string;
32
+ readonly state: DeliveryState | "none";
33
+ /** Set when the intent itself is superseded: the lineage is frozen (§15). */
34
+ readonly superseded?: true;
35
+ readonly completed: readonly StageName[];
36
+ /** First pending stage; absent when the core lifecycle has no next stage. */
37
+ readonly currentStage?: StageName;
38
+ /** Set when `currentStage` is a human gate. */
39
+ readonly blockedStage?: StageName;
40
+ readonly requiredActor?: Actor;
41
+ readonly lineage?: Lineage;
42
+ }
43
+ export interface LifecycleStatus {
44
+ readonly lineages: readonly LineageStatus[];
45
+ /** Current-lineage problems found while deriving (empty for a loaded project). */
46
+ readonly problems: readonly Problem[];
47
+ }
48
+ /** `lifecycle next` for one lineage: the next permitted action, not executed. */
49
+ export interface NextAction {
50
+ readonly intent?: string;
51
+ /** Absent when there is no further core Delivery stage. */
52
+ readonly stage?: StageName;
53
+ readonly execution?: ExecutionMode;
54
+ readonly actor?: Actor;
55
+ /** True when the stage needs a human: `lifecycle run` stops here. */
56
+ readonly gate: boolean;
57
+ readonly reason: string;
58
+ }
59
+ /**
60
+ * The lineages `status`/`next`/`run` operate on: the one named by `intentId`,
61
+ * else every lineage sorted by intent id. When the graph has no active
62
+ * lineage and no id was given, one `undefined` entry stands for the
63
+ * capture-intent entry point.
64
+ */
65
+ export declare function selectLineages(project: Project, intentId?: string): readonly (Lineage | undefined)[];
66
+ /**
67
+ * Derives lifecycle status from graph state + lifecycle.yml (§18, §20).
68
+ * Validation problems of a project that failed to load are the caller's to
69
+ * report: a `Project` here has already passed the canonical loader.
70
+ */
71
+ export declare function lifecycleStatus(project: Project, intentId?: string): LifecycleStatus;
72
+ /** The next permitted action for one lineage, given optionally which transient stages already ran. */
73
+ export declare function nextActionFor(project: Project, lineage: Lineage | undefined, done?: ReadonlySet<StageName>): NextAction;
74
+ /** `lifecycle next`: the next permitted core Delivery action per lineage (§20). */
75
+ export declare function lifecycleNext(project: Project, intentId?: string): readonly NextAction[];
@@ -0,0 +1,146 @@
1
+ import { PactwrightError } from "../errors.js";
2
+ import { CORE_STAGES, isHumanGate, } from "../config/lifecycle.js";
3
+ import { deriveLineages } from "../graph/lineage.js";
4
+ /**
5
+ * Stages that leave a durable record in the Delivery Graph, and the record
6
+ * they leave (Delivery Graph §§6–12): capture-intent → Intent,
7
+ * approve-contract → Decision (+ Contract), write-brief → Brief,
8
+ * prepare-evidence → Evidence.
9
+ */
10
+ export const GRAPH_MARKING_STAGES = [
11
+ "capture-intent",
12
+ "approve-contract",
13
+ "write-brief",
14
+ "prepare-evidence",
15
+ ];
16
+ /**
17
+ * Stages whose output is transient (§7 alternatives, §11 delivery execution
18
+ * and review): they never mutate the graph, so completion is only known
19
+ * inside the `lifecycle run` that executed them.
20
+ */
21
+ export const TRANSIENT_STAGES = [
22
+ "propose-contracts",
23
+ "deliver-brief",
24
+ "review",
25
+ ];
26
+ export function isTransientStage(stage) {
27
+ return TRANSIENT_STAGES.includes(stage);
28
+ }
29
+ /**
30
+ * How many leading core stages each derived state has completed (§14, §18).
31
+ * `deferred`/`rejected` completed the decision stage and are terminal for the
32
+ * core lifecycle: resuming needs a new Decision (§15), which is not a stage
33
+ * the engine loops back to. `undefined` = no lineage yet: nothing completed.
34
+ */
35
+ const COMPLETED_COUNT = {
36
+ open: 1, // capture-intent
37
+ deferred: 3, // …propose-contracts, approve-contract
38
+ rejected: 3,
39
+ contracted: 3,
40
+ delivering: 4, // …write-brief
41
+ done: 7,
42
+ };
43
+ const TERMINAL_STATES = ["deferred", "rejected", "done"];
44
+ /** Stages already completed by a lineage, in lifecycle order. */
45
+ export function completedStages(lineage) {
46
+ return CORE_STAGES.slice(0, lineage === undefined ? 0 : COMPLETED_COUNT[lineage.state]);
47
+ }
48
+ /**
49
+ * Stages still to run for a lineage, in order. Empty for terminal lineages.
50
+ * With no lineage, the only pending stage is capture-intent.
51
+ */
52
+ export function pendingStages(lineage) {
53
+ if (lineage === undefined)
54
+ return ["capture-intent"];
55
+ if (lineage.superseded)
56
+ return [];
57
+ if (TERMINAL_STATES.includes(lineage.state))
58
+ return [];
59
+ return CORE_STAGES.slice(COMPLETED_COUNT[lineage.state]);
60
+ }
61
+ /** A lineage still progressing through the core lifecycle. */
62
+ export function isActive(lineage) {
63
+ return !lineage.superseded && !TERMINAL_STATES.includes(lineage.state);
64
+ }
65
+ function findLineage(project, intentId, lineages) {
66
+ const lineage = lineages.find((candidate) => candidate.intent.id === intentId);
67
+ if (lineage === undefined) {
68
+ const exists = project.graph.nodes.some((node) => node.id === intentId && node.type === "intent");
69
+ throw new PactwrightError(exists ? "ambiguous-lineage" : "unknown-intent", exists
70
+ ? `intent "${intentId}" has an ambiguous lineage; fix validation problems first`
71
+ : `"${intentId}" is not an intent in this project`);
72
+ }
73
+ return lineage;
74
+ }
75
+ /**
76
+ * The lineages `status`/`next`/`run` operate on: the one named by `intentId`,
77
+ * else every lineage sorted by intent id. When the graph has no active
78
+ * lineage and no id was given, one `undefined` entry stands for the
79
+ * capture-intent entry point.
80
+ */
81
+ export function selectLineages(project, intentId) {
82
+ const { lineages } = deriveLineages(project.graph.nodes, project.graph.edges);
83
+ if (intentId !== undefined)
84
+ return [findLineage(project, intentId, lineages)];
85
+ if (!lineages.some(isActive))
86
+ return [...lineages, undefined];
87
+ return lineages;
88
+ }
89
+ function statusOf(project, lineage) {
90
+ const currentStage = pendingStages(lineage)[0];
91
+ const base = {
92
+ ...(lineage === undefined ? {} : { intent: lineage.intent.id, lineage }),
93
+ state: lineage === undefined ? "none" : lineage.state,
94
+ ...(lineage?.superseded === true ? { superseded: true } : {}),
95
+ completed: completedStages(lineage),
96
+ ...(currentStage === undefined ? {} : { currentStage }),
97
+ };
98
+ if (currentStage === undefined)
99
+ return base;
100
+ const config = project.lifecycle.stages[currentStage];
101
+ if (!isHumanGate(config))
102
+ return base;
103
+ return { ...base, blockedStage: currentStage, requiredActor: "human" };
104
+ }
105
+ /**
106
+ * Derives lifecycle status from graph state + lifecycle.yml (§18, §20).
107
+ * Validation problems of a project that failed to load are the caller's to
108
+ * report: a `Project` here has already passed the canonical loader.
109
+ */
110
+ export function lifecycleStatus(project, intentId) {
111
+ const { problems } = deriveLineages(project.graph.nodes, project.graph.edges);
112
+ return {
113
+ lineages: selectLineages(project, intentId).map((lineage) => statusOf(project, lineage)),
114
+ problems,
115
+ };
116
+ }
117
+ /** The next permitted action for one lineage, given optionally which transient stages already ran. */
118
+ export function nextActionFor(project, lineage, done = new Set()) {
119
+ const intent = lineage === undefined ? {} : { intent: lineage.intent.id };
120
+ const stage = pendingStages(lineage).find((candidate) => !done.has(candidate));
121
+ if (stage === undefined) {
122
+ const reason = lineage?.superseded === true
123
+ ? `intent "${lineage.intent.id}" is superseded; work continues on the superseding intent's lineage (Delivery Graph §15)`
124
+ : lineage?.state === "done"
125
+ ? "current Evidence exists; the core Delivery lifecycle is complete and has no next stage"
126
+ : `lineage is ${lineage?.state}; record a new Decision with approve-contract to resume (Delivery Graph §15)`;
127
+ return { ...intent, gate: false, reason };
128
+ }
129
+ const config = project.lifecycle.stages[stage];
130
+ const gate = isHumanGate(config);
131
+ const who = config.actor === undefined ? "" : ` by ${config.actor}`;
132
+ return {
133
+ ...intent,
134
+ stage,
135
+ execution: config.execution,
136
+ ...(config.actor === undefined ? {} : { actor: config.actor }),
137
+ gate,
138
+ reason: gate
139
+ ? `${stage} is a human gate (${config.execution}${who}); it waits for a human`
140
+ : `${stage} runs ${config.execution}${who}`,
141
+ };
142
+ }
143
+ /** `lifecycle next`: the next permitted core Delivery action per lineage (§20). */
144
+ export function lifecycleNext(project, intentId) {
145
+ return selectLineages(project, intentId).map((lineage) => nextActionFor(project, lineage));
146
+ }
@@ -0,0 +1,18 @@
1
+ import type { StageName } from "../config/lifecycle.js";
2
+ import type { GraphNode } from "../graph/nodes.js";
3
+ import { GRAPH_MARKING_STAGES } from "./engine.js";
4
+ /** A stage that leaves a durable record (Delivery Graph §§6–12). */
5
+ export type RecordingStage = (typeof GRAPH_MARKING_STAGES)[number];
6
+ export declare function isRecordingStage(stage: string): stage is RecordingStage;
7
+ /** The nodes one `lifecycle record` created, in creation order. */
8
+ export interface RecordResult {
9
+ readonly stage: RecordingStage;
10
+ readonly created: readonly GraphNode[];
11
+ }
12
+ /**
13
+ * `pactwright lifecycle record <stage> --file <yaml>`: the runtime
14
+ * responsibility an adapter command hands finished content to. The runtime
15
+ * checks the transition, then the Step 7 mutation validates and writes the
16
+ * complete proposed state atomically. Nothing is written on any failure.
17
+ */
18
+ export declare function recordStage(root: string, stage: StageName, inputPath: string): RecordResult;
@@ -0,0 +1,157 @@
1
+ import { PactwrightError } from "../errors.js";
2
+ import { findIntentOf } from "../context.js";
3
+ import { createBrief, createEvidence, createIntent, recordDecision, } from "../graph/mutations.js";
4
+ import { DECISION_OUTCOMES } from "../graph/schema.js";
5
+ import { loadProject } from "../loader.js";
6
+ import { Checker, expectEnum, expectRecord, expectString, isRecord, rejectUnknownKeys, requireKeys, } from "../validation.js";
7
+ import { readYamlFile } from "../yaml.js";
8
+ import { GRAPH_MARKING_STAGES, isTransientStage, nextActionFor, pendingStages, selectLineages, } from "./engine.js";
9
+ export function isRecordingStage(stage) {
10
+ return GRAPH_MARKING_STAGES.includes(stage);
11
+ }
12
+ /**
13
+ * Reads the content file an adapter command hands to the runtime. The
14
+ * shape is per stage (see `REQUIRED`/`ALLOWED`); every problem in the file
15
+ * is reported in one pass.
16
+ */
17
+ const REQUIRED = {
18
+ "capture-intent": ["title", "body"],
19
+ "approve-contract": ["intent", "outcome", "decided_by", "body"],
20
+ "write-brief": ["contract", "title", "body"],
21
+ "prepare-evidence": ["brief", "title", "body"],
22
+ };
23
+ const ALLOWED = {
24
+ "capture-intent": ["title", "body"],
25
+ "approve-contract": ["intent", "outcome", "decided_by", "title", "body", "contract"],
26
+ "write-brief": ["contract", "title", "body"],
27
+ "prepare-evidence": ["brief", "title", "body"],
28
+ };
29
+ function readFields(stage, path) {
30
+ const file = readYamlFile(path);
31
+ if (file.problems.length > 0)
32
+ throw PactwrightError.fromProblems("invalid-record-input", file.problems);
33
+ const c = new Checker(path);
34
+ if (!isRecord(file.value)) {
35
+ c.fail("invalid-type", "record input must be a mapping");
36
+ throw PactwrightError.fromProblems("invalid-record-input", c.problems);
37
+ }
38
+ const record = file.value;
39
+ requireKeys(c, record, "record input", REQUIRED[stage]);
40
+ rejectUnknownKeys(c, record, "record input", ALLOWED[stage]);
41
+ const fields = {};
42
+ for (const key of ALLOWED[stage]) {
43
+ if (!(key in record))
44
+ continue;
45
+ if (stage === "approve-contract" && key === "contract") {
46
+ const contract = expectRecord(c, record[key], "contract");
47
+ if (contract !== undefined) {
48
+ requireKeys(c, contract, "contract", ["title", "body"]);
49
+ rejectUnknownKeys(c, contract, "contract", ["title", "body"]);
50
+ fields[key] = {
51
+ title: expectString(c, contract["title"], "contract.title"),
52
+ body: expectString(c, contract["body"], "contract.body"),
53
+ };
54
+ }
55
+ }
56
+ else if (key === "outcome") {
57
+ fields[key] = expectEnum(c, record[key], "outcome", DECISION_OUTCOMES);
58
+ }
59
+ else {
60
+ fields[key] = expectString(c, record[key], key);
61
+ }
62
+ }
63
+ if (!c.ok)
64
+ throw PactwrightError.fromProblems("invalid-record-input", c.problems);
65
+ return fields;
66
+ }
67
+ /**
68
+ * The runtime's transition check (Delivery Graph §18): the stage being
69
+ * recorded must be pending for the lineage the input refers to, with only
70
+ * transient stages (whose completion the graph cannot show) before it.
71
+ * capture-intent starts a new lineage and is always permitted.
72
+ */
73
+ function assertPermitted(project, stage, anchor) {
74
+ if (stage === "capture-intent")
75
+ return;
76
+ const intent = stage === "approve-contract"
77
+ ? project.graph.nodes.find((node) => node.id === anchor && node.type === "intent")
78
+ : findIntentOf(anchor, project.graph.nodes, project.graph.edges);
79
+ if (intent === undefined) {
80
+ throw new PactwrightError("unknown-node", `"${anchor}" is not part of any Delivery lineage`);
81
+ }
82
+ const [lineage] = selectLineages(project, intent.id);
83
+ // §15: deferred and rejected lineages resume by recording a new Decision,
84
+ // which is exactly what approve-contract does. Frozen (superseded)
85
+ // lineages stay refused.
86
+ if (stage === "approve-contract" &&
87
+ lineage !== undefined &&
88
+ !lineage.superseded &&
89
+ (lineage.state === "deferred" || lineage.state === "rejected")) {
90
+ return;
91
+ }
92
+ const pending = pendingStages(lineage);
93
+ const index = pending.indexOf(stage);
94
+ if (index < 0 || !pending.slice(0, index).every(isTransientStage)) {
95
+ const action = nextActionFor(project, lineage);
96
+ throw new PactwrightError("stage-not-permitted", `${stage} is not a permitted action for intent "${intent.id}" now: ${action.reason}`);
97
+ }
98
+ }
99
+ /**
100
+ * `pactwright lifecycle record <stage> --file <yaml>`: the runtime
101
+ * responsibility an adapter command hands finished content to. The runtime
102
+ * checks the transition, then the Step 7 mutation validates and writes the
103
+ * complete proposed state atomically. Nothing is written on any failure.
104
+ */
105
+ export function recordStage(root, stage, inputPath) {
106
+ if (!isRecordingStage(stage)) {
107
+ throw new PactwrightError("no-graph-record", `stage "${stage}" leaves no graph record; only ${GRAPH_MARKING_STAGES.join(", ")} can be recorded`);
108
+ }
109
+ const fields = readFields(stage, inputPath);
110
+ const project = loadProject({ root });
111
+ switch (stage) {
112
+ case "capture-intent":
113
+ return { stage, created: [createIntent(root, { title: fields.title, body: fields.body })] };
114
+ case "approve-contract": {
115
+ assertPermitted(project, stage, fields.intent);
116
+ const input = {
117
+ intentId: fields.intent,
118
+ outcome: fields.outcome,
119
+ decidedBy: fields.decided_by,
120
+ body: fields.body,
121
+ ...(fields.title === undefined ? {} : { title: fields.title }),
122
+ ...(fields.contract === undefined || typeof fields.contract === "string"
123
+ ? {}
124
+ : { contract: fields.contract }),
125
+ };
126
+ const result = recordDecision(root, input);
127
+ return {
128
+ stage,
129
+ created: result.contract === undefined ? [result.decision] : [result.decision, result.contract],
130
+ };
131
+ }
132
+ case "write-brief":
133
+ assertPermitted(project, stage, fields.contract);
134
+ return {
135
+ stage,
136
+ created: [
137
+ createBrief(root, {
138
+ contractId: fields.contract,
139
+ title: fields.title,
140
+ body: fields.body,
141
+ }),
142
+ ],
143
+ };
144
+ case "prepare-evidence":
145
+ assertPermitted(project, stage, fields.brief);
146
+ return {
147
+ stage,
148
+ created: [
149
+ createEvidence(root, {
150
+ briefId: fields.brief,
151
+ title: fields.title,
152
+ body: fields.body,
153
+ }),
154
+ ],
155
+ };
156
+ }
157
+ }
@@ -0,0 +1,62 @@
1
+ import { type Problem } from "../errors.js";
2
+ import type { Actor, StageName } from "../config/lifecycle.js";
3
+ import { type Lineage } from "../graph/lineage.js";
4
+ import { type Project } from "../loader.js";
5
+ /** What a stage executor reports back to the runtime. */
6
+ export type StageOutcome = {
7
+ readonly status: "completed";
8
+ } | {
9
+ readonly status: "failed";
10
+ readonly message: string;
11
+ };
12
+ export interface StageRequest {
13
+ readonly stage: StageName;
14
+ /**
15
+ * Read-only derivation input (which lineage, which config) — never a
16
+ * mutation base. Executors that mutate the graph pass
17
+ * `project.paths.root` to the typed mutations, which load and validate
18
+ * the current graph state themselves at commit time.
19
+ */
20
+ readonly project: Project;
21
+ /** Absent for capture-intent on a graph with no active lineage. */
22
+ readonly lineage?: Lineage;
23
+ }
24
+ /**
25
+ * Executes one automatic stage's responsibility (Delivery Graph §16). The
26
+ * runtime decides *which* stage runs and when; the executor only performs
27
+ * it, mutating the graph through the Step 7 mutations where the stage
28
+ * leaves a record. Later checkpoints plug the agent pack in here.
29
+ */
30
+ export type StageExecutor = (request: StageRequest) => StageOutcome | Promise<StageOutcome>;
31
+ /** Why `lifecycle run` stopped (Delivery Graph §20). */
32
+ export type RunStop = "completed" | "human-gate" | "stage-failed" | "validation-error";
33
+ export interface RunResult {
34
+ readonly intent?: string;
35
+ readonly stop: RunStop;
36
+ /** The gate reached or the stage that failed. */
37
+ readonly stage?: StageName;
38
+ readonly requiredActor?: Actor;
39
+ /** Stages executed in this run, in order. */
40
+ readonly executed: readonly StageName[];
41
+ readonly message?: string;
42
+ readonly problems?: readonly Problem[];
43
+ }
44
+ export interface RunOptions {
45
+ readonly root: string;
46
+ readonly execute: StageExecutor;
47
+ readonly intentId?: string;
48
+ }
49
+ /**
50
+ * The runtime has no way to perform automatic stages until an agent pack is
51
+ * installed (Checkpoint 1, Step 10): every automatic stage fails, so `run`
52
+ * stops there instead of pretending.
53
+ */
54
+ export declare const noExecutor: StageExecutor;
55
+ /**
56
+ * `lifecycle run` (Delivery Graph §20): runs automatic stages of every
57
+ * active lineage (or the one `intentId`) until a human gate, completion, a
58
+ * stage failure or a validation error. Gates are checked by the runtime on
59
+ * every step, so a configured gate is never skipped whatever the executor
60
+ * could do. Never throws for expected failures.
61
+ */
62
+ export declare function runLifecycle(options: RunOptions): Promise<readonly RunResult[]>;