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/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[]>;
|