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
@@ -0,0 +1,199 @@
1
+ import { Checker, expectBoolean, expectEnum, expectRecord, expectString, expectVersion, rejectUnknownKeys, requireKeys, } from "../validation.js";
2
+ import { parseYaml, readYamlFile } from "../yaml.js";
3
+ import { EXTENSION_ID_PATTERN } from "./lock.js";
4
+ export const CONFIG_VERSION = 1;
5
+ export const ADAPTER_TYPES = ["claude-code"];
6
+ export function parseConfig(raw, path) {
7
+ const c = new Checker(path);
8
+ const root = expectRecord(c, raw, "config");
9
+ if (root === undefined)
10
+ return { value: undefined, problems: c.problems };
11
+ requireKeys(c, root, "config", ["version", "agent_pack", "adapter", "github"]);
12
+ rejectUnknownKeys(c, root, "config", [
13
+ "version",
14
+ "agent_pack",
15
+ "adapter",
16
+ "extensions",
17
+ "github",
18
+ ]);
19
+ expectVersion(c, root["version"], "config.version", CONFIG_VERSION);
20
+ let source;
21
+ let packVersion;
22
+ const agentPack = expectRecord(c, root["agent_pack"], "config.agent_pack");
23
+ if (agentPack !== undefined) {
24
+ requireKeys(c, agentPack, "config.agent_pack", ["source"]);
25
+ rejectUnknownKeys(c, agentPack, "config.agent_pack", ["source", "version"]);
26
+ source = expectString(c, agentPack["source"], "config.agent_pack.source");
27
+ packVersion = expectString(c, agentPack["version"], "config.agent_pack.version");
28
+ }
29
+ let adapterType;
30
+ const adapter = expectRecord(c, root["adapter"], "config.adapter");
31
+ if (adapter !== undefined) {
32
+ requireKeys(c, adapter, "config.adapter", ["type"]);
33
+ rejectUnknownKeys(c, adapter, "config.adapter", ["type"]);
34
+ adapterType = expectEnum(c, adapter["type"], "config.adapter.type", ADAPTER_TYPES);
35
+ }
36
+ const extensions = Object.create(null);
37
+ if (root["extensions"] !== undefined) {
38
+ const raw = expectRecord(c, root["extensions"], "config.extensions");
39
+ if (raw !== undefined) {
40
+ for (const id of Object.keys(raw).sort()) {
41
+ if (!EXTENSION_ID_PATTERN.test(id)) {
42
+ c.fail("invalid-extension-id", `config.extensions key "${id}" is not a valid extension id`);
43
+ continue;
44
+ }
45
+ const label = `config.extensions.${id}`;
46
+ const entry = expectRecord(c, raw[id], label);
47
+ if (entry === undefined)
48
+ continue;
49
+ requireKeys(c, entry, label, ["enabled", "source"]);
50
+ rejectUnknownKeys(c, entry, label, ["enabled", "source"]);
51
+ const extensionEnabled = expectBoolean(c, entry["enabled"], `${label}.enabled`);
52
+ const extensionSource = expectString(c, entry["source"], `${label}.source`);
53
+ if (extensionEnabled !== undefined && extensionSource !== undefined) {
54
+ extensions[id] = { enabled: extensionEnabled, source: extensionSource };
55
+ }
56
+ }
57
+ }
58
+ }
59
+ let enabled;
60
+ const github = expectRecord(c, root["github"], "config.github");
61
+ if (github !== undefined) {
62
+ requireKeys(c, github, "config.github", ["enabled"]);
63
+ rejectUnknownKeys(c, github, "config.github", ["enabled"]);
64
+ enabled = expectBoolean(c, github["enabled"], "config.github.enabled");
65
+ }
66
+ if (!c.ok || source === undefined || adapterType === undefined || enabled === undefined) {
67
+ return { value: undefined, problems: c.problems };
68
+ }
69
+ return {
70
+ value: {
71
+ version: 1,
72
+ agentPack: packVersion === undefined ? { source } : { source, version: packVersion },
73
+ adapter: { type: adapterType },
74
+ // Copied to a plain object; callers guard dynamic id lookups with
75
+ // `Object.hasOwn` so an id like "constructor" cannot resolve to an
76
+ // Object.prototype member.
77
+ extensions: { ...extensions },
78
+ github: { enabled },
79
+ },
80
+ problems: [],
81
+ };
82
+ }
83
+ export function loadConfig(path) {
84
+ const read = readYamlFile(path);
85
+ if (read.problems.length > 0)
86
+ return { value: undefined, problems: read.problems };
87
+ return parseConfig(read.value, path);
88
+ }
89
+ /**
90
+ * A YAML double-quoted scalar holding `text`. JSON is a subset of YAML 1.2
91
+ * and the two share their double-quoted escape set, so `JSON.stringify` is a
92
+ * correct encoder here — and for an ordinary package name or relative path it
93
+ * emits exactly the bytes a hand-written config already has. Interpolating
94
+ * such values raw would let a `"`, `\` or newline in a source break out of
95
+ * the field and leave an unparseable config behind.
96
+ */
97
+ function scalar(text) {
98
+ return JSON.stringify(text);
99
+ }
100
+ /** The canonical `extensions:` block, the only region the commands rewrite. */
101
+ function extensionsBlock(config) {
102
+ const ids = Object.keys(config.extensions).sort();
103
+ if (ids.length === 0)
104
+ return ["extensions: {}"];
105
+ const lines = ["extensions:"];
106
+ ids.forEach((id, index) => {
107
+ const extension = config.extensions[id];
108
+ if (index > 0)
109
+ lines.push("");
110
+ lines.push(` ${id}:`, ` enabled: ${extension.enabled}`, ` source: ${scalar(extension.source)}`);
111
+ });
112
+ return lines;
113
+ }
114
+ /**
115
+ * Serialises a configuration in the canonical `.pactwright/config.yml`
116
+ * shape — the exact bytes `pactwright init` writes for the default state.
117
+ * This is the fallback whenever the previous file cannot be edited in place;
118
+ * it re-emits from parsed values, so comments and incidental layout are not
119
+ * carried over. `rewriteConfig` is what the commands normally use.
120
+ */
121
+ export function serialiseConfig(config) {
122
+ const lines = [
123
+ "version: 1",
124
+ "",
125
+ "agent_pack:",
126
+ ` source: ${scalar(config.agentPack.source)}`,
127
+ ];
128
+ if (config.agentPack.version !== undefined) {
129
+ lines.push(` version: ${scalar(config.agentPack.version)}`);
130
+ }
131
+ // `adapter.type` is a validated enum and extension ids are validated
132
+ // kebab-case, so both are safe bare; quoting them would also change the
133
+ // bytes `init` writes.
134
+ lines.push("", "adapter:", ` type: ${config.adapter.type}`, "");
135
+ lines.push(...extensionsBlock(config));
136
+ lines.push("", "github:", ` enabled: ${config.github.enabled}`, "");
137
+ return lines.join("\n");
138
+ }
139
+ /** The line range of the top-level `extensions:` key, or `undefined`. */
140
+ function extensionsRegion(lines) {
141
+ const start = lines.findIndex((line) => /^extensions:/.test(line));
142
+ if (start === -1)
143
+ return undefined;
144
+ if (lines.some((line) => line.includes("\t")))
145
+ return undefined;
146
+ // A flow mapping (`extensions: {}`) is the whole region. Anything else on
147
+ // the key line is a shape this editor does not claim to understand.
148
+ const inline = lines[start].slice("extensions:".length).trim();
149
+ if (inline !== "")
150
+ return inline === "{}" ? { start, end: start } : undefined;
151
+ let end = start;
152
+ for (let i = start + 1; i < lines.length; i += 1) {
153
+ const line = lines[i];
154
+ if (line.trim() === "")
155
+ continue;
156
+ if (!/^\s/.test(line))
157
+ break;
158
+ end = i;
159
+ }
160
+ return { start, end };
161
+ }
162
+ /**
163
+ * The previous configuration text with only its `extensions:` block replaced
164
+ * (Distribution §3). `extension add` and `remove` change nothing else, so
165
+ * every other byte — comments, key order, spacing a team chose — survives.
166
+ *
167
+ * Falls back to a full `serialiseConfig` rewrite whenever the edit cannot be
168
+ * made confidently, including when the spliced result does not parse back to
169
+ * the intended configuration. The fallback is today's behaviour, so the worst
170
+ * case is losing comments rather than corrupting the file — which matters
171
+ * because `removeExtension` never rolls its write back.
172
+ *
173
+ * Comments *inside* the extensions block are part of the replaced region and
174
+ * are not preserved.
175
+ */
176
+ export function rewriteConfig(previous, config) {
177
+ const canonical = serialiseConfig(config);
178
+ const newline = previous.includes("\r\n") ? "\r\n" : "\n";
179
+ const lines = previous.split(/\r?\n/);
180
+ const block = [...extensionsBlock(config)];
181
+ const region = extensionsRegion(lines);
182
+ let spliced;
183
+ if (region !== undefined) {
184
+ spliced = [...lines.slice(0, region.start), ...block, ...lines.slice(region.end + 1)].join(newline);
185
+ }
186
+ else {
187
+ // No `extensions:` key at all: place it before `github:`, else append.
188
+ const github = lines.findIndex((line) => /^github:/.test(line));
189
+ const at = github === -1 ? lines.length : github;
190
+ spliced = [...lines.slice(0, at), ...block, "", ...lines.slice(at)].join(newline);
191
+ }
192
+ const read = parseYaml(spliced, "config.yml");
193
+ if (read.problems.length > 0)
194
+ return canonical;
195
+ const reparsed = parseConfig(read.value, "config.yml");
196
+ if (reparsed.value === undefined)
197
+ return canonical;
198
+ return serialiseConfig(reparsed.value) === canonical ? spliced : canonical;
199
+ }
@@ -0,0 +1,34 @@
1
+ import type { ParseResult } from "./config.js";
2
+ /** The seven core Delivery lifecycle stages, in lifecycle order (Delivery Graph §17). */
3
+ export declare const CORE_STAGES: readonly ["capture-intent", "propose-contracts", "approve-contract", "write-brief", "deliver-brief", "review", "prepare-evidence"];
4
+ export type StageName = (typeof CORE_STAGES)[number];
5
+ export declare const EXECUTION_MODES: readonly ["manual", "automatic"];
6
+ export type ExecutionMode = (typeof EXECUTION_MODES)[number];
7
+ export declare const ACTORS: readonly ["human", "agent"];
8
+ export type Actor = (typeof ACTORS)[number];
9
+ export interface StageConfig {
10
+ readonly execution: ExecutionMode;
11
+ readonly actor?: Actor;
12
+ }
13
+ /** `.pactwright/lifecycle.yml` — how the repository operates the lifecycle. */
14
+ export interface LifecycleConfig {
15
+ readonly version: 1;
16
+ readonly stages: Readonly<Record<StageName, StageConfig>>;
17
+ }
18
+ export declare const LIFECYCLE_VERSION = 1;
19
+ /** The stage whose configured actor authorises Decisions (Delivery Graph §8). */
20
+ export declare const DECISION_STAGE: "approve-contract";
21
+ export declare function parseLifecycle(raw: unknown, path: string): ParseResult<LifecycleConfig>;
22
+ export declare function loadLifecycle(path: string): ParseResult<LifecycleConfig>;
23
+ /** The actor authorised to make Decisions: `approve-contract`'s configured actor. */
24
+ export declare function decisionActor(lifecycle: LifecycleConfig): Actor;
25
+ /**
26
+ * A human gate is a stage that cannot proceed without a human: manual
27
+ * execution or a human actor. `lifecycle run` stops here and never skips one
28
+ * (Delivery Graph §20). In the §17 default example the gates are
29
+ * capture-intent and approve-contract; in the automated example only
30
+ * capture-intent remains.
31
+ */
32
+ export declare function isHumanGate(stage: StageConfig): boolean;
33
+ /** The human gates of a lifecycle, in stage order. */
34
+ export declare function humanGates(lifecycle: LifecycleConfig): readonly StageName[];
@@ -0,0 +1,81 @@
1
+ import { Checker, expectEnum, expectRecord, expectVersion, rejectUnknownKeys, requireKeys, } from "../validation.js";
2
+ import { readYamlFile } from "../yaml.js";
3
+ /** The seven core Delivery lifecycle stages, in lifecycle order (Delivery Graph §17). */
4
+ export const CORE_STAGES = [
5
+ "capture-intent",
6
+ "propose-contracts",
7
+ "approve-contract",
8
+ "write-brief",
9
+ "deliver-brief",
10
+ "review",
11
+ "prepare-evidence",
12
+ ];
13
+ export const EXECUTION_MODES = ["manual", "automatic"];
14
+ export const ACTORS = ["human", "agent"];
15
+ export const LIFECYCLE_VERSION = 1;
16
+ /** The stage whose configured actor authorises Decisions (Delivery Graph §8). */
17
+ export const DECISION_STAGE = "approve-contract";
18
+ export function parseLifecycle(raw, path) {
19
+ const c = new Checker(path);
20
+ const root = expectRecord(c, raw, "lifecycle");
21
+ if (root === undefined)
22
+ return { value: undefined, problems: c.problems };
23
+ requireKeys(c, root, "lifecycle", ["version", "stages"]);
24
+ rejectUnknownKeys(c, root, "lifecycle", ["version", "stages"]);
25
+ expectVersion(c, root["version"], "lifecycle.version", LIFECYCLE_VERSION);
26
+ const stages = {};
27
+ const rawStages = expectRecord(c, root["stages"], "lifecycle.stages");
28
+ if (rawStages !== undefined) {
29
+ requireKeys(c, rawStages, "lifecycle.stages", CORE_STAGES);
30
+ for (const key of Object.keys(rawStages)) {
31
+ if (!CORE_STAGES.includes(key)) {
32
+ c.fail("unknown-stage", `lifecycle.stages has unknown stage "${key}"; only core Delivery stages are configurable here`);
33
+ }
34
+ }
35
+ for (const name of CORE_STAGES) {
36
+ const label = `lifecycle.stages.${name}`;
37
+ const stage = expectRecord(c, rawStages[name], label);
38
+ if (stage === undefined)
39
+ continue;
40
+ requireKeys(c, stage, label, ["execution"]);
41
+ rejectUnknownKeys(c, stage, label, ["execution", "actor"]);
42
+ const execution = expectEnum(c, stage["execution"], `${label}.execution`, EXECUTION_MODES);
43
+ const actor = stage["actor"] === undefined
44
+ ? undefined
45
+ : expectEnum(c, stage["actor"], `${label}.actor`, ACTORS);
46
+ if (name === DECISION_STAGE && stage["actor"] === undefined) {
47
+ c.fail("missing-actor", `${label} must declare "actor"; Decisions must be authorised by lifecycle.yml (Delivery Graph §8)`);
48
+ }
49
+ if (execution !== undefined) {
50
+ stages[name] = actor === undefined ? { execution } : { execution, actor };
51
+ }
52
+ }
53
+ }
54
+ if (!c.ok)
55
+ return { value: undefined, problems: c.problems };
56
+ return { value: { version: 1, stages: stages }, problems: [] };
57
+ }
58
+ export function loadLifecycle(path) {
59
+ const read = readYamlFile(path);
60
+ if (read.problems.length > 0)
61
+ return { value: undefined, problems: read.problems };
62
+ return parseLifecycle(read.value, path);
63
+ }
64
+ /** The actor authorised to make Decisions: `approve-contract`'s configured actor. */
65
+ export function decisionActor(lifecycle) {
66
+ return lifecycle.stages[DECISION_STAGE].actor;
67
+ }
68
+ /**
69
+ * A human gate is a stage that cannot proceed without a human: manual
70
+ * execution or a human actor. `lifecycle run` stops here and never skips one
71
+ * (Delivery Graph §20). In the §17 default example the gates are
72
+ * capture-intent and approve-contract; in the automated example only
73
+ * capture-intent remains.
74
+ */
75
+ export function isHumanGate(stage) {
76
+ return stage.execution === "manual" || stage.actor === "human";
77
+ }
78
+ /** The human gates of a lifecycle, in stage order. */
79
+ export function humanGates(lifecycle) {
80
+ return CORE_STAGES.filter((name) => isHumanGate(lifecycle.stages[name]));
81
+ }
@@ -0,0 +1,43 @@
1
+ import type { ParseResult } from "./config.js";
2
+ /**
3
+ * One locked extension: the exact package and version it resolved to, and a
4
+ * hash of its manifest (Distribution §6).
5
+ */
6
+ export interface LockExtension {
7
+ readonly package: string;
8
+ readonly version: string;
9
+ /**
10
+ * Hash of the extension's declared manifest — its id, package, version,
11
+ * runtime range, dependencies, graph types, namespaces, capabilities and
12
+ * GitHub profile. It pins what the extension declares, not the bytes of
13
+ * the code it ships.
14
+ */
15
+ readonly hash: string;
16
+ /** Extension id → exact version of a required peer extension. Omitted when empty. */
17
+ readonly dependencies?: Readonly<Record<string, string>>;
18
+ }
19
+ /** `.pactwright/lock.yml` — the exact resolved setup (Distribution §6). */
20
+ export interface LockFile {
21
+ readonly runtime: {
22
+ readonly version: string;
23
+ };
24
+ readonly agentPack: {
25
+ readonly name: string;
26
+ readonly version: string;
27
+ readonly hash: string;
28
+ };
29
+ /** Agent name → content hash. */
30
+ readonly agents: Readonly<Record<string, string>>;
31
+ /** Skill name → content hash. A runtime addition relative to the §6 shape. */
32
+ readonly skills: Readonly<Record<string, string>>;
33
+ /**
34
+ * Extension id → locked extension (Distribution §6). Empty only when no
35
+ * extension is configured.
36
+ */
37
+ readonly extensions: Readonly<Record<string, LockExtension>>;
38
+ }
39
+ export declare const HASH_PATTERN: RegExp;
40
+ /** Extension ids are kebab-case identifiers, like `project-intelligence`. */
41
+ export declare const EXTENSION_ID_PATTERN: RegExp;
42
+ export declare function parseLock(raw: unknown, path: string): ParseResult<LockFile>;
43
+ export declare function loadLock(path: string): ParseResult<LockFile>;
@@ -0,0 +1,141 @@
1
+ import { Checker, expectRecord, expectString, rejectUnknownKeys, requireKeys, } from "../validation.js";
2
+ import { readYamlFile } from "../yaml.js";
3
+ export const HASH_PATTERN = /^sha256:[0-9a-f]{64}$/;
4
+ /** Extension ids are kebab-case identifiers, like `project-intelligence`. */
5
+ export const EXTENSION_ID_PATTERN = /^[a-z][a-z0-9]*(-[a-z0-9]+)*$/;
6
+ /** The lock records exact resolved state, so versions are never ranges. */
7
+ const EXACT_VERSION_PATTERN = /^\d+\.\d+\.\d+$/;
8
+ function expectHash(c, value, label) {
9
+ const text = expectString(c, value, label);
10
+ if (text === undefined)
11
+ return undefined;
12
+ if (!HASH_PATTERN.test(text))
13
+ return c.fail("invalid-hash", `${label} must match sha256:<64 hex>`);
14
+ return text;
15
+ }
16
+ function expectExactVersion(c, value, label) {
17
+ const text = expectString(c, value, label);
18
+ if (text === undefined)
19
+ return undefined;
20
+ if (!EXACT_VERSION_PATTERN.test(text)) {
21
+ return c.fail("invalid-version", `${label} must be an exact x.y.z version, found "${text}"`);
22
+ }
23
+ return text;
24
+ }
25
+ function parseExtension(c, raw, label) {
26
+ const record = expectRecord(c, raw, label);
27
+ if (record === undefined)
28
+ return undefined;
29
+ requireKeys(c, record, label, ["package", "version", "hash"]);
30
+ rejectUnknownKeys(c, record, label, ["package", "version", "hash", "dependencies"]);
31
+ const pkg = expectString(c, record["package"], `${label}.package`);
32
+ const version = expectExactVersion(c, record["version"], `${label}.version`);
33
+ const hash = expectHash(c, record["hash"], `${label}.hash`);
34
+ let dependencies;
35
+ if (record["dependencies"] !== undefined) {
36
+ const deps = expectRecord(c, record["dependencies"], `${label}.dependencies`);
37
+ if (deps !== undefined) {
38
+ dependencies = {};
39
+ for (const id of Object.keys(deps).sort()) {
40
+ if (!EXTENSION_ID_PATTERN.test(id)) {
41
+ c.fail("invalid-extension-id", `${label}.dependencies key "${id}" is not a valid extension id`);
42
+ continue;
43
+ }
44
+ const wanted = expectExactVersion(c, deps[id], `${label}.dependencies.${id}`);
45
+ if (wanted !== undefined)
46
+ dependencies[id] = wanted;
47
+ }
48
+ }
49
+ }
50
+ if (pkg === undefined || version === undefined || hash === undefined)
51
+ return undefined;
52
+ return {
53
+ package: pkg,
54
+ version,
55
+ hash,
56
+ ...(dependencies === undefined || Object.keys(dependencies).length === 0
57
+ ? {}
58
+ : { dependencies }),
59
+ };
60
+ }
61
+ function parseExtensions(c, raw) {
62
+ const out = {};
63
+ if (raw === undefined)
64
+ return out;
65
+ const record = expectRecord(c, raw, "lock.extensions");
66
+ if (record === undefined)
67
+ return out;
68
+ for (const id of Object.keys(record).sort()) {
69
+ if (!EXTENSION_ID_PATTERN.test(id)) {
70
+ c.fail("invalid-extension-id", `lock.extensions key "${id}" is not a valid extension id`);
71
+ continue;
72
+ }
73
+ const extension = parseExtension(c, record[id], `lock.extensions.${id}`);
74
+ if (extension !== undefined)
75
+ out[id] = extension;
76
+ }
77
+ return out;
78
+ }
79
+ function parseHashMap(c, raw, label) {
80
+ const out = {};
81
+ if (raw === undefined)
82
+ return out;
83
+ const record = expectRecord(c, raw, label);
84
+ if (record === undefined)
85
+ return out;
86
+ for (const key of Object.keys(record).sort()) {
87
+ const hash = expectHash(c, record[key], `${label}.${key}`);
88
+ if (hash !== undefined)
89
+ out[key] = hash;
90
+ }
91
+ return out;
92
+ }
93
+ export function parseLock(raw, path) {
94
+ const c = new Checker(path);
95
+ const root = expectRecord(c, raw, "lock");
96
+ if (root === undefined)
97
+ return { value: undefined, problems: c.problems };
98
+ requireKeys(c, root, "lock", ["runtime", "agent_pack"]);
99
+ rejectUnknownKeys(c, root, "lock", ["runtime", "agent_pack", "agents", "skills", "extensions"]);
100
+ let runtimeVersion;
101
+ const runtime = expectRecord(c, root["runtime"], "lock.runtime");
102
+ if (runtime !== undefined) {
103
+ requireKeys(c, runtime, "lock.runtime", ["version"]);
104
+ rejectUnknownKeys(c, runtime, "lock.runtime", ["version"]);
105
+ runtimeVersion = expectString(c, runtime["version"], "lock.runtime.version");
106
+ }
107
+ let pack;
108
+ const agentPack = expectRecord(c, root["agent_pack"], "lock.agent_pack");
109
+ if (agentPack !== undefined) {
110
+ requireKeys(c, agentPack, "lock.agent_pack", ["name", "version", "hash"]);
111
+ rejectUnknownKeys(c, agentPack, "lock.agent_pack", ["name", "version", "hash"]);
112
+ const name = expectString(c, agentPack["name"], "lock.agent_pack.name");
113
+ const version = expectString(c, agentPack["version"], "lock.agent_pack.version");
114
+ const hash = expectHash(c, agentPack["hash"], "lock.agent_pack.hash");
115
+ if (name !== undefined && version !== undefined && hash !== undefined) {
116
+ pack = { name, version, hash };
117
+ }
118
+ }
119
+ const agents = parseHashMap(c, root["agents"], "lock.agents");
120
+ const skills = parseHashMap(c, root["skills"], "lock.skills");
121
+ const extensions = parseExtensions(c, root["extensions"]);
122
+ if (!c.ok || runtimeVersion === undefined || pack === undefined) {
123
+ return { value: undefined, problems: c.problems };
124
+ }
125
+ return {
126
+ value: {
127
+ runtime: { version: runtimeVersion },
128
+ agentPack: pack,
129
+ agents,
130
+ skills,
131
+ extensions,
132
+ },
133
+ problems: [],
134
+ };
135
+ }
136
+ export function loadLock(path) {
137
+ const read = readYamlFile(path);
138
+ if (read.problems.length > 0)
139
+ return { value: undefined, problems: read.problems };
140
+ return parseLock(read.value, path);
141
+ }
@@ -0,0 +1,59 @@
1
+ import type { Edge } from "./graph/edges.js";
2
+ import { type DeliveryState, type Lineage } from "./graph/lineage.js";
3
+ import type { GraphNode } from "./graph/nodes.js";
4
+ import type { Project } from "./loader.js";
5
+ /** A superseded record of the lineage's history (`--history` only). */
6
+ export interface HistoryRecord {
7
+ readonly node: GraphNode;
8
+ /** Ids of the records that supersede this one. */
9
+ readonly supersededBy: readonly string[];
10
+ }
11
+ /** One namespaced extension contribution (Delivery Graph §22). */
12
+ export interface ExtensionContext {
13
+ readonly namespace: string;
14
+ readonly context: unknown;
15
+ }
16
+ /**
17
+ * The extension-context seam. Enabled extensions contribute after the core
18
+ * lineage is resolved; they receive it read-only and can only add one
19
+ * namespaced entry, so extension context never alters the Delivery lineage.
20
+ * No core contributor exists in this checkpoint.
21
+ */
22
+ export type ContextContributor = (input: {
23
+ readonly project: Project;
24
+ readonly lineage: Lineage;
25
+ readonly history: boolean;
26
+ }) => ExtensionContext | undefined;
27
+ /** `pactwright context <node-id>` result (Delivery Graph §22). */
28
+ export interface DeliveryContext {
29
+ /** The node id that was asked for. */
30
+ readonly requested: string;
31
+ readonly intent: string;
32
+ readonly state: DeliveryState;
33
+ /** Current core lineage in stage order; only existing stages. */
34
+ readonly lineage: readonly GraphNode[];
35
+ /** False when the requested node is superseded (it is then not in `lineage`). */
36
+ readonly requestedIsCurrent: boolean;
37
+ /** Superseded records of this intent's tree, sorted by id; only with `history`. */
38
+ readonly history?: readonly HistoryRecord[];
39
+ /** Namespaced extension context; empty in this checkpoint. */
40
+ readonly extensions: Readonly<Record<string, unknown>>;
41
+ }
42
+ export interface ContextOptions {
43
+ readonly history?: boolean;
44
+ readonly contributors?: readonly ContextContributor[];
45
+ }
46
+ /**
47
+ * The intent a core Delivery node belongs to, found by walking structural
48
+ * edges (superseded records included). `undefined` when the node is not
49
+ * linked to any intent.
50
+ */
51
+ export declare function findIntentOf(nodeId: string, nodes: readonly GraphNode[], edges: readonly Edge[]): GraphNode | undefined;
52
+ /**
53
+ * Resolves the current core Delivery lineage the node belongs to (§22).
54
+ * Only the five core node types exist in the graph, so rejected
55
+ * alternatives, review transcripts, obsolete reasoning and execution
56
+ * provenance can never appear; superseded records appear only under
57
+ * `history` when asked for.
58
+ */
59
+ export declare function loadContext(project: Project, nodeId: string, options?: ContextOptions): DeliveryContext;
@@ -0,0 +1,111 @@
1
+ import { PactwrightError } from "./errors.js";
2
+ import { deriveLineage } from "./graph/lineage.js";
3
+ /** Structural edge from a core node towards its intent, regardless of currency. */
4
+ const TOWARDS_INTENT = {
5
+ evidence: { type: "evidences", direction: "out" }, // evidence --evidences--> brief
6
+ brief: { type: "decomposes", direction: "out" }, // brief --decomposes--> contract
7
+ contract: { type: "selects", direction: "in" }, // decision --selects--> contract
8
+ decision: { type: "resolves", direction: "out" }, // decision --resolves--> intent
9
+ };
10
+ /**
11
+ * The intent a core Delivery node belongs to, found by walking structural
12
+ * edges (superseded records included). `undefined` when the node is not
13
+ * linked to any intent.
14
+ */
15
+ export function findIntentOf(nodeId, nodes, edges) {
16
+ const byId = new Map(nodes.map((node) => [node.id, node]));
17
+ const seen = new Set();
18
+ let current = byId.get(nodeId);
19
+ while (current !== undefined && !seen.has(current.id)) {
20
+ if (current.type === "intent")
21
+ return current;
22
+ seen.add(current.id);
23
+ const step = TOWARDS_INTENT[current.type];
24
+ if (step === undefined)
25
+ return undefined;
26
+ const id = current.id;
27
+ const link = edges.find((edge) => step.direction === "out"
28
+ ? edge.type === step.type && edge.source === id
29
+ : edge.type === step.type && edge.target === id);
30
+ current =
31
+ link === undefined
32
+ ? undefined
33
+ : byId.get(step.direction === "out" ? link.target : link.source);
34
+ }
35
+ return undefined;
36
+ }
37
+ /** Every core node in the intent's tree, current or not, sorted by id. */
38
+ function lineageTree(intent, nodes, edges) {
39
+ const byId = new Map(nodes.map((node) => [node.id, node]));
40
+ const collect = (targets, type, direction, nodeType) => edges
41
+ .filter((edge) => edge.type === type &&
42
+ targets.includes(direction === "sources" ? edge.target : edge.source))
43
+ .map((edge) => byId.get(direction === "sources" ? edge.source : edge.target))
44
+ .filter((node) => node !== undefined && node.type === nodeType);
45
+ const decisions = collect([intent.id], "resolves", "sources", "decision");
46
+ const contracts = collect(decisions.map((d) => d.id), "selects", "targets", "contract");
47
+ const briefs = collect(contracts.map((c) => c.id), "decomposes", "sources", "brief");
48
+ const evidence = collect(briefs.map((b) => b.id), "evidences", "sources", "evidence");
49
+ return [intent, ...decisions, ...contracts, ...briefs, ...evidence].sort((a, b) => a.id < b.id ? -1 : a.id > b.id ? 1 : 0);
50
+ }
51
+ /**
52
+ * Resolves the current core Delivery lineage the node belongs to (§22).
53
+ * Only the five core node types exist in the graph, so rejected
54
+ * alternatives, review transcripts, obsolete reasoning and execution
55
+ * provenance can never appear; superseded records appear only under
56
+ * `history` when asked for.
57
+ */
58
+ export function loadContext(project, nodeId, options = {}) {
59
+ const { nodes, edges } = project.graph;
60
+ const node = nodes.find((candidate) => candidate.id === nodeId);
61
+ if (node === undefined) {
62
+ throw new PactwrightError("unknown-node", `"${nodeId}" is not a node in this project`);
63
+ }
64
+ const intent = findIntentOf(nodeId, nodes, edges);
65
+ if (intent === undefined) {
66
+ throw new PactwrightError("unlinked-node", `${node.type} "${nodeId}" is not linked to any intent; it has no Delivery lineage`);
67
+ }
68
+ const lineage = deriveLineage(intent.id, nodes, edges);
69
+ if (lineage === undefined) {
70
+ // The loader rejects ambiguous lineages, so this cannot happen for a loaded project.
71
+ throw new PactwrightError("ambiguous-lineage", `intent "${intent.id}" has an ambiguous lineage`);
72
+ }
73
+ const current = [
74
+ lineage.intent,
75
+ lineage.decision,
76
+ lineage.contract,
77
+ lineage.brief,
78
+ lineage.evidence,
79
+ ].filter((record) => record !== undefined);
80
+ const currentIds = new Set(current.map((record) => record.id));
81
+ const extensions = {};
82
+ for (const contribute of options.contributors ?? []) {
83
+ const contribution = contribute({ project, lineage, history: options.history === true });
84
+ if (contribution === undefined)
85
+ continue;
86
+ if (contribution.namespace in extensions) {
87
+ throw new PactwrightError("duplicate-context-namespace", `extension context namespace "${contribution.namespace}" was contributed twice`);
88
+ }
89
+ extensions[contribution.namespace] = contribution.context;
90
+ }
91
+ const base = {
92
+ requested: nodeId,
93
+ intent: intent.id,
94
+ state: lineage.state,
95
+ lineage: current,
96
+ requestedIsCurrent: currentIds.has(nodeId),
97
+ extensions,
98
+ };
99
+ if (options.history !== true)
100
+ return base;
101
+ const history = lineageTree(intent, nodes, edges)
102
+ .filter((record) => !currentIds.has(record.id))
103
+ .map((record) => ({
104
+ node: record,
105
+ supersededBy: edges
106
+ .filter((edge) => edge.type === "supersedes" && edge.target === record.id)
107
+ .map((edge) => edge.source)
108
+ .sort(),
109
+ }));
110
+ return { ...base, history };
111
+ }