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,92 @@
1
+ import type { PactwrightConfig } from "../config/config.js";
2
+ import type { LockFile } from "../config/lock.js";
3
+ import { type Problem } from "../errors.js";
4
+ import { type Project } from "../loader.js";
5
+ import { type ResolvedExtension } from "../extension/resolve.js";
6
+ import { isValidRange, satisfiesRange, sha256 } from "./locate.js";
7
+ import { type PackManifest } from "./manifest.js";
8
+ /** A pack resolved from a project's configuration, with every hash the lock records. */
9
+ export interface ResolvedPack {
10
+ /** Absolute pack root: where `pack.yml`, `agents/` and `skills/` live. */
11
+ readonly dir: string;
12
+ readonly manifest: PackManifest;
13
+ readonly hashes: {
14
+ /** Hash of the whole resolved pack: manifest plus every agent and skill hash. */
15
+ readonly pack: string;
16
+ /** Agent key → hash of its prompt file. */
17
+ readonly agents: Readonly<Record<string, string>>;
18
+ /** Skill name → hash of its file. */
19
+ readonly skills: Readonly<Record<string, string>>;
20
+ };
21
+ }
22
+ export interface ResolvePackOptions {
23
+ /** Project root; path sources and package lookups start here. */
24
+ readonly root: string;
25
+ readonly config: PactwrightConfig;
26
+ /** Defaults to the running runtime's version. */
27
+ readonly runtimeVersion?: string;
28
+ }
29
+ export { isValidRange, satisfiesRange, sha256 };
30
+ /** Where the configured pack lives; see `locatePackage`. */
31
+ export declare function locatePack(root: string, source: string): string | Problem;
32
+ /**
33
+ * Resolves the configured pack: locate → load/validate the manifest → check
34
+ * runtime and requested-version compatibility → hash prompts, skills and
35
+ * the pack. Returns every problem found; a pack that resolves is complete
36
+ * in itself but not yet checked against required capabilities (see
37
+ * `assertPackComplete`).
38
+ */
39
+ export declare function resolvePack(options: ResolvePackOptions): {
40
+ value: ResolvedPack | undefined;
41
+ problems: readonly Problem[];
42
+ };
43
+ /**
44
+ * The capability check that guards canonical graph mutation (Distribution
45
+ * §7): resolves the project's pack and throws `missing-capability` — listing
46
+ * every missing capability — or the resolution problems. Nothing is written
47
+ * by this function; callers run it before their first write.
48
+ */
49
+ export declare function assertPackComplete(project: Project): ResolvedPack;
50
+ /** Desired installation state resolved to exact state: pack, extensions and the lock recording them. */
51
+ export interface DesiredState {
52
+ readonly pack: ResolvedPack;
53
+ readonly extensions: readonly ResolvedExtension[];
54
+ readonly lock: LockFile;
55
+ }
56
+ /**
57
+ * Resolves desired state (configuration) to exact state (a lock value):
58
+ * resolve every configured extension, resolve the configured pack, check
59
+ * the required capability union, record runtime version and
60
+ * pack/agent/skill/extension hashes (Distribution §§3–6). Pure — nothing
61
+ * is written — and deterministic: the same desired state always resolves to
62
+ * the same lock. Never throws; mirrors `resolvePack`'s result idiom.
63
+ */
64
+ export declare function resolveDesiredState(options: ResolvePackOptions): {
65
+ value: DesiredState | undefined;
66
+ problems: readonly Problem[];
67
+ };
68
+ /** The agent key and definition implementing `capability`, if the pack maps it. */
69
+ export declare function agentFor(pack: ResolvedPack, capability: string): {
70
+ readonly key: string;
71
+ readonly prompt: string;
72
+ readonly skills: readonly string[];
73
+ } | undefined;
74
+ /**
75
+ * The lock-file value recording exactly this resolved pack and runtime.
76
+ * `extensions` is the seam for extension resolution: nothing resolves
77
+ * extensions in this checkpoint, so it defaults to empty.
78
+ */
79
+ export declare function lockEntriesFor(pack: ResolvedPack, runtime?: string, extensions?: LockFile["extensions"]): LockFile;
80
+ /** Serialises a lock file deterministically in the `.pactwright/lock.yml` shape. */
81
+ export declare function serialiseLock(lock: LockFile): string;
82
+ /** Writes the lock file atomically (temporary sibling + rename). */
83
+ export declare function writeLock(lockPath: string, lock: LockFile): void;
84
+ /**
85
+ * Resolves the configured pack, checks required capabilities and only then
86
+ * records the exact runtime/pack/agent/skill state in `.pactwright/lock.yml`.
87
+ * Any failure leaves the lock file untouched.
88
+ */
89
+ export declare function resolveAndLock(root: string): {
90
+ pack: ResolvedPack;
91
+ lock: LockFile;
92
+ };
@@ -0,0 +1,238 @@
1
+ import { renameSync, writeFileSync } from "node:fs";
2
+ import { dump } from "js-yaml";
3
+ import { join } from "node:path";
4
+ import { tempSibling } from "../atomic.js";
5
+ import { PactwrightError } from "../errors.js";
6
+ import { canonicalJson } from "../graph/revision.js";
7
+ import { loadProject } from "../loader.js";
8
+ import { runtimeVersion } from "../version.js";
9
+ import { enabledManifests, extensionLockEntries, resolveExtensions, } from "../extension/resolve.js";
10
+ import { missingCapabilities, requiredCapabilities } from "./capabilities.js";
11
+ import { isPathSource, isValidRange, locatePackage, satisfiesRange, sha256 } from "./locate.js";
12
+ import { loadPackManifest, readPackFile, skillPath } from "./manifest.js";
13
+ export { isValidRange, satisfiesRange, sha256 };
14
+ /** Where the configured pack lives; see `locatePackage`. */
15
+ export function locatePack(root, source) {
16
+ return locatePackage(root, source, "agent pack");
17
+ }
18
+ /**
19
+ * Resolves the configured pack: locate → load/validate the manifest → check
20
+ * runtime and requested-version compatibility → hash prompts, skills and
21
+ * the pack. Returns every problem found; a pack that resolves is complete
22
+ * in itself but not yet checked against required capabilities (see
23
+ * `assertPackComplete`).
24
+ */
25
+ export function resolvePack(options) {
26
+ const source = options.config.agentPack.source;
27
+ const located = locatePack(options.root, source);
28
+ if (typeof located !== "string")
29
+ return { value: undefined, problems: [located] };
30
+ const dir = located;
31
+ const loaded = loadPackManifest(dir);
32
+ if (loaded.value === undefined)
33
+ return { value: undefined, problems: loaded.problems };
34
+ const manifest = loaded.value;
35
+ const path = join(dir, "pack.yml");
36
+ const problems = [];
37
+ if (manifest.name !== source && !isPathSource(source)) {
38
+ problems.push({
39
+ code: "pack-name-mismatch",
40
+ message: `pack manifest declares name "${manifest.name}" but config.agent_pack.source is "${source}"`,
41
+ path,
42
+ });
43
+ }
44
+ const runtime = options.runtimeVersion ?? runtimeVersion();
45
+ if (!satisfiesRange(runtime, manifest.pactwright)) {
46
+ problems.push({
47
+ code: "incompatible-runtime",
48
+ message: `pack "${manifest.name}@${manifest.version}" requires pactwright ${manifest.pactwright}; this runtime is ${runtime}`,
49
+ path,
50
+ });
51
+ }
52
+ const wanted = options.config.agentPack.version;
53
+ if (wanted !== undefined && !isValidRange(wanted)) {
54
+ // A range the runtime cannot parse is its own problem, not a mismatch.
55
+ problems.push({
56
+ code: "invalid-version-range",
57
+ message: `config.agent_pack.version "${wanted}" is not a supported range; use x.y.z or ^x.y.z`,
58
+ path,
59
+ });
60
+ }
61
+ else if (wanted !== undefined && !satisfiesRange(manifest.version, wanted)) {
62
+ problems.push({
63
+ code: "incompatible-pack-version",
64
+ message: `config.agent_pack.version wants ${wanted} but the installed pack "${manifest.name}" is ${manifest.version}`,
65
+ path,
66
+ });
67
+ }
68
+ if (problems.length > 0)
69
+ return { value: undefined, problems };
70
+ const agents = {};
71
+ const skills = {};
72
+ for (const key of Object.keys(manifest.agents).sort()) {
73
+ const agent = manifest.agents[key];
74
+ agents[key] = sha256(readPackFile(dir, agent.prompt));
75
+ for (const skill of agent.skills) {
76
+ skills[skill] ??= sha256(readPackFile(dir, skillPath("", skill)));
77
+ }
78
+ }
79
+ const sortedSkills = Object.fromEntries(Object.keys(skills)
80
+ .sort()
81
+ .map((k) => [k, skills[k]]));
82
+ const pack = sha256(canonicalJson({
83
+ name: manifest.name,
84
+ version: manifest.version,
85
+ pactwright: manifest.pactwright,
86
+ capabilities: manifest.capabilities,
87
+ agents: Object.fromEntries(Object.keys(manifest.agents)
88
+ .sort()
89
+ .map((key) => [key, { prompt: agents[key], skills: manifest.agents[key].skills }])),
90
+ skills: sortedSkills,
91
+ }));
92
+ return {
93
+ value: { dir, manifest, hashes: { pack, agents, skills: sortedSkills } },
94
+ problems: [],
95
+ };
96
+ }
97
+ /**
98
+ * The one resolution pipeline behind every desired-state surface: resolve
99
+ * the configured pack, then check it provides every required capability —
100
+ * the core set plus the capabilities of the given enabled extensions.
101
+ */
102
+ function resolveComplete(options, extensions = []) {
103
+ const resolved = resolvePack(options);
104
+ if (resolved.value === undefined)
105
+ return { kind: "unresolved", problems: resolved.problems };
106
+ const pack = resolved.value;
107
+ const required = requiredCapabilities(extensions);
108
+ const missing = missingCapabilities(pack.manifest, required);
109
+ if (missing.length === 0)
110
+ return { kind: "ok", pack };
111
+ return {
112
+ kind: "incomplete",
113
+ pack,
114
+ missing,
115
+ required,
116
+ problems: missing.map((capability) => ({
117
+ code: "missing-capability",
118
+ message: `required capability "${capability}" is not provided by the selected agent pack`,
119
+ path: join(pack.dir, "pack.yml"),
120
+ })),
121
+ };
122
+ }
123
+ /**
124
+ * The capability check that guards canonical graph mutation (Distribution
125
+ * §7): resolves the project's pack and throws `missing-capability` — listing
126
+ * every missing capability — or the resolution problems. Nothing is written
127
+ * by this function; callers run it before their first write.
128
+ */
129
+ export function assertPackComplete(project) {
130
+ const resolution = resolveComplete({ root: project.paths.root, config: project.config }, enabledManifests(project.extensions));
131
+ if (resolution.kind === "unresolved") {
132
+ throw PactwrightError.fromProblems("pack-unresolved", resolution.problems);
133
+ }
134
+ if (resolution.kind === "incomplete") {
135
+ const { pack, missing, required } = resolution;
136
+ throw new PactwrightError("missing-capability", `agent pack "${pack.manifest.name}@${pack.manifest.version}" does not provide required capabilit${missing.length === 1 ? "y" : "ies"}: ${missing.join(", ")} (required: ${required.join(", ")})`, resolution.problems);
137
+ }
138
+ return resolution.pack;
139
+ }
140
+ /**
141
+ * Resolves desired state (configuration) to exact state (a lock value):
142
+ * resolve every configured extension, resolve the configured pack, check
143
+ * the required capability union, record runtime version and
144
+ * pack/agent/skill/extension hashes (Distribution §§3–6). Pure — nothing
145
+ * is written — and deterministic: the same desired state always resolves to
146
+ * the same lock. Never throws; mirrors `resolvePack`'s result idiom.
147
+ */
148
+ export function resolveDesiredState(options) {
149
+ const extensions = resolveExtensions(options);
150
+ if (extensions.value === undefined)
151
+ return { value: undefined, problems: extensions.problems };
152
+ const resolution = resolveComplete(options, enabledManifests(extensions.value));
153
+ if (resolution.kind !== "ok")
154
+ return { value: undefined, problems: resolution.problems };
155
+ const lock = lockEntriesFor(resolution.pack, options.runtimeVersion ?? runtimeVersion(), extensionLockEntries(extensions.value));
156
+ return { value: { pack: resolution.pack, extensions: extensions.value, lock }, problems: [] };
157
+ }
158
+ /** The agent key and definition implementing `capability`, if the pack maps it. */
159
+ export function agentFor(pack, capability) {
160
+ const key = pack.manifest.capabilities[capability];
161
+ if (key === undefined)
162
+ return undefined;
163
+ const agent = pack.manifest.agents[key];
164
+ if (agent === undefined)
165
+ return undefined;
166
+ return { key, prompt: join(pack.dir, agent.prompt), skills: agent.skills };
167
+ }
168
+ /**
169
+ * The lock-file value recording exactly this resolved pack and runtime.
170
+ * `extensions` is the seam for extension resolution: nothing resolves
171
+ * extensions in this checkpoint, so it defaults to empty.
172
+ */
173
+ export function lockEntriesFor(pack, runtime = runtimeVersion(), extensions = {}) {
174
+ return {
175
+ runtime: { version: runtime },
176
+ agentPack: { name: pack.manifest.name, version: pack.manifest.version, hash: pack.hashes.pack },
177
+ agents: pack.hashes.agents,
178
+ skills: pack.hashes.skills,
179
+ extensions,
180
+ };
181
+ }
182
+ function sortedMap(entries) {
183
+ return Object.fromEntries(Object.keys(entries)
184
+ .sort()
185
+ .map((key) => [key, entries[key]]));
186
+ }
187
+ /** Extensions in serialisation order: sorted ids, fixed key order, sorted dependencies. */
188
+ function serialisedExtensions(extensions) {
189
+ return Object.fromEntries(Object.keys(extensions)
190
+ .sort()
191
+ .map((id) => {
192
+ const extension = extensions[id];
193
+ return [
194
+ id,
195
+ {
196
+ package: extension.package,
197
+ version: extension.version,
198
+ hash: extension.hash,
199
+ ...(extension.dependencies === undefined ||
200
+ Object.keys(extension.dependencies).length === 0
201
+ ? {}
202
+ : { dependencies: sortedMap(extension.dependencies) }),
203
+ },
204
+ ];
205
+ }));
206
+ }
207
+ /** Serialises a lock file deterministically in the `.pactwright/lock.yml` shape. */
208
+ export function serialiseLock(lock) {
209
+ return dump({
210
+ runtime: { version: lock.runtime.version },
211
+ agent_pack: {
212
+ name: lock.agentPack.name,
213
+ version: lock.agentPack.version,
214
+ hash: lock.agentPack.hash,
215
+ },
216
+ agents: sortedMap(lock.agents),
217
+ skills: sortedMap(lock.skills),
218
+ extensions: serialisedExtensions(lock.extensions),
219
+ }, { lineWidth: -1, noRefs: true });
220
+ }
221
+ /** Writes the lock file atomically (temporary sibling + rename). */
222
+ export function writeLock(lockPath, lock) {
223
+ const temp = tempSibling(lockPath);
224
+ writeFileSync(temp, serialiseLock(lock), "utf8");
225
+ renameSync(temp, lockPath);
226
+ }
227
+ /**
228
+ * Resolves the configured pack, checks required capabilities and only then
229
+ * records the exact runtime/pack/agent/skill state in `.pactwright/lock.yml`.
230
+ * Any failure leaves the lock file untouched.
231
+ */
232
+ export function resolveAndLock(root) {
233
+ const project = loadProject({ root });
234
+ const pack = assertPackComplete(project);
235
+ const lock = lockEntriesFor(pack, runtimeVersion(), extensionLockEntries(project.extensions));
236
+ writeLock(project.paths.lock, lock);
237
+ return { pack, lock };
238
+ }
@@ -0,0 +1,22 @@
1
+ /** File and directory layout of a Pactwright project (Delivery Graph §4). */
2
+ export interface ProjectPaths {
3
+ readonly root: string;
4
+ readonly pactwrightDir: string;
5
+ readonly config: string;
6
+ readonly lifecycle: string;
7
+ readonly lock: string;
8
+ readonly specsDir: string;
9
+ readonly nodesDir: string;
10
+ readonly edges: string;
11
+ }
12
+ export declare const CONFIG_FILE = ".pactwright/config.yml";
13
+ export declare const LIFECYCLE_FILE = ".pactwright/lifecycle.yml";
14
+ export declare const LOCK_FILE = ".pactwright/lock.yml";
15
+ export declare const NODES_DIR = "specs/nodes";
16
+ export declare const EDGES_FILE = "specs/graph/edges.yml";
17
+ export declare function projectPaths(root: string): ProjectPaths;
18
+ /**
19
+ * Walks up from `cwd` until a directory containing `.pactwright/config.yml`
20
+ * is found. Throws when no Pactwright project encloses `cwd`.
21
+ */
22
+ export declare function findProjectRoot(cwd?: string): string;
@@ -0,0 +1,37 @@
1
+ import { existsSync } from "node:fs";
2
+ import { dirname, join, resolve } from "node:path";
3
+ import { PactwrightError } from "./errors.js";
4
+ export const CONFIG_FILE = ".pactwright/config.yml";
5
+ export const LIFECYCLE_FILE = ".pactwright/lifecycle.yml";
6
+ export const LOCK_FILE = ".pactwright/lock.yml";
7
+ export const NODES_DIR = "specs/nodes";
8
+ export const EDGES_FILE = "specs/graph/edges.yml";
9
+ export function projectPaths(root) {
10
+ const absoluteRoot = resolve(root);
11
+ return {
12
+ root: absoluteRoot,
13
+ pactwrightDir: join(absoluteRoot, ".pactwright"),
14
+ config: join(absoluteRoot, CONFIG_FILE),
15
+ lifecycle: join(absoluteRoot, LIFECYCLE_FILE),
16
+ lock: join(absoluteRoot, LOCK_FILE),
17
+ specsDir: join(absoluteRoot, "specs"),
18
+ nodesDir: join(absoluteRoot, NODES_DIR),
19
+ edges: join(absoluteRoot, EDGES_FILE),
20
+ };
21
+ }
22
+ /**
23
+ * Walks up from `cwd` until a directory containing `.pactwright/config.yml`
24
+ * is found. Throws when no Pactwright project encloses `cwd`.
25
+ */
26
+ export function findProjectRoot(cwd = process.cwd()) {
27
+ let current = resolve(cwd);
28
+ for (;;) {
29
+ if (existsSync(join(current, CONFIG_FILE)))
30
+ return current;
31
+ const parent = dirname(current);
32
+ if (parent === current) {
33
+ throw new PactwrightError("project-not-found", `no Pactwright project found from ${resolve(cwd)} (expected ${CONFIG_FILE} in this directory or a parent)`);
34
+ }
35
+ current = parent;
36
+ }
37
+ }
package/dist/sync.d.ts ADDED
@@ -0,0 +1,54 @@
1
+ import { type RenderedFiles } from "./adapter/claude-code.js";
2
+ import { type Problem } from "./errors.js";
3
+ import { type Project } from "./loader.js";
4
+ import { type ResolvedPack } from "./pack/resolve.js";
5
+ /** `pactwright sync` result. */
6
+ export interface SyncReport {
7
+ readonly ok: boolean;
8
+ readonly root: string;
9
+ /** Rendered files whose on-disk bytes differed (or were absent) before this sync. */
10
+ readonly changed: readonly string[];
11
+ /** Rendered files that were already byte-identical on disk. */
12
+ readonly unchanged: readonly string[];
13
+ /** Files Pactwright had generated that the render no longer produces, deleted. */
14
+ readonly removed: readonly string[];
15
+ /**
16
+ * Files inside the managed directories that Pactwright did not generate, so
17
+ * are not Pactwright's to remove. Reported, never deleted.
18
+ */
19
+ readonly kept: readonly string[];
20
+ /**
21
+ * Rendered paths already occupied by an unmarked file. The file is left
22
+ * untouched and the sync fails, so the collision is resolved deliberately
23
+ * rather than by overwriting user state.
24
+ */
25
+ readonly conflicts: readonly string[];
26
+ /** Empty when `ok`. */
27
+ readonly problems: readonly Problem[];
28
+ }
29
+ /**
30
+ * The Distribution §8 GitHub-workflow rendering step. It is a seam in this
31
+ * checkpoint: GitHub provisioning arrives in Checkpoint 2, so no workflow
32
+ * files are rendered and nothing under `.github/` is ever Pactwright-managed
33
+ * yet. When it activates it renders only files Pactwright explicitly owns
34
+ * (e.g. `.github/workflows/pactwright*.yml`) — `sync` never claims
35
+ * ownership of user-authored `.github/workflows/**`.
36
+ */
37
+ export declare function renderGitHubWorkflows(project: Project, pack: ResolvedPack): RenderedFiles;
38
+ /**
39
+ * Deterministic local synchronisation (Distribution §8): load configuration,
40
+ * lock and extensions through the canonical loader, validate the required
41
+ * capability union, assemble agents and skills from the resolved pack, and
42
+ * render only the Pactwright-managed `.claude/` adapter surface. Enabled
43
+ * extensions contribute through the same adapter process; in this checkpoint
44
+ * their contribution is the capability union, since extension manifests
45
+ * declare no renderable adapter content yet. Repeated sync with unchanged
46
+ * inputs is byte-identical. Never throws for expected failures, and writes
47
+ * nothing when resolution fails.
48
+ *
49
+ * Only files whose banner stands in its rendered position are overwritten or
50
+ * removed, so a hand-written file inside `.claude/agents` or
51
+ * `.claude/commands` survives every sync — including one that quotes the
52
+ * banner in its prose.
53
+ */
54
+ export declare function syncProject(root?: string): SyncReport;
package/dist/sync.js ADDED
@@ -0,0 +1,98 @@
1
+ import { existsSync, readFileSync } from "node:fs";
2
+ import { join } from "node:path";
3
+ import { GENERATED_MARKER, renderClaudeCodeAdapter, writeAdapter, } from "./adapter/claude-code.js";
4
+ import { PactwrightError } from "./errors.js";
5
+ import { loadProject } from "./loader.js";
6
+ import { assertPackComplete } from "./pack/resolve.js";
7
+ import { projectPaths } from "./project.js";
8
+ /**
9
+ * The Distribution §8 GitHub-workflow rendering step. It is a seam in this
10
+ * checkpoint: GitHub provisioning arrives in Checkpoint 2, so no workflow
11
+ * files are rendered and nothing under `.github/` is ever Pactwright-managed
12
+ * yet. When it activates it renders only files Pactwright explicitly owns
13
+ * (e.g. `.github/workflows/pactwright*.yml`) — `sync` never claims
14
+ * ownership of user-authored `.github/workflows/**`.
15
+ */
16
+ export function renderGitHubWorkflows(project, pack) {
17
+ void project;
18
+ void pack;
19
+ return new Map();
20
+ }
21
+ /**
22
+ * Deterministic local synchronisation (Distribution §8): load configuration,
23
+ * lock and extensions through the canonical loader, validate the required
24
+ * capability union, assemble agents and skills from the resolved pack, and
25
+ * render only the Pactwright-managed `.claude/` adapter surface. Enabled
26
+ * extensions contribute through the same adapter process; in this checkpoint
27
+ * their contribution is the capability union, since extension manifests
28
+ * declare no renderable adapter content yet. Repeated sync with unchanged
29
+ * inputs is byte-identical. Never throws for expected failures, and writes
30
+ * nothing when resolution fails.
31
+ *
32
+ * Only files whose banner stands in its rendered position are overwritten or
33
+ * removed, so a hand-written file inside `.claude/agents` or
34
+ * `.claude/commands` survives every sync — including one that quotes the
35
+ * banner in its prose.
36
+ */
37
+ export function syncProject(root = process.cwd()) {
38
+ const paths = projectPaths(root);
39
+ const failure = (problems) => ({
40
+ ok: false,
41
+ root: paths.root,
42
+ changed: [],
43
+ unchanged: [],
44
+ removed: [],
45
+ kept: [],
46
+ conflicts: [],
47
+ problems,
48
+ });
49
+ let project;
50
+ let pack;
51
+ try {
52
+ project = loadProject({ root: paths.root });
53
+ pack = assertPackComplete(project);
54
+ }
55
+ catch (error) {
56
+ if (!(error instanceof PactwrightError))
57
+ throw error;
58
+ return failure(error.problems);
59
+ }
60
+ const files = new Map([
61
+ ...renderClaudeCodeAdapter(pack),
62
+ ...renderGitHubWorkflows(project, pack),
63
+ ]);
64
+ // Compared before the write, or every rendered file would read back as
65
+ // unchanged. Paths that turn out to collide with user state are dropped
66
+ // afterwards: nothing was written there.
67
+ const changed = [];
68
+ const unchanged = [];
69
+ for (const [relPath, content] of files) {
70
+ const target = join(paths.root, relPath);
71
+ if (existsSync(target) && readFileSync(target, "utf8") === content) {
72
+ unchanged.push(relPath);
73
+ }
74
+ else {
75
+ changed.push(relPath);
76
+ }
77
+ }
78
+ const written = writeAdapter(paths.root, files);
79
+ const conflicted = new Set(written.conflicts);
80
+ // A collision with an unmarked file is reported, never resolved by
81
+ // overwriting: `sync` fails so the state is visible to the user and to CI
82
+ // (Distribution §14 — leave ambiguous state intact and report it).
83
+ const problems = written.conflicts.map((path) => ({
84
+ code: "unmanaged-conflict",
85
+ message: `"${path}" is not a Pactwright-generated file, so it was not overwritten: delete it, or restore its "${GENERATED_MARKER}" banner, then run \`pactwright sync\` again`,
86
+ path,
87
+ }));
88
+ return {
89
+ ok: problems.length === 0,
90
+ root: paths.root,
91
+ changed: changed.filter((path) => !conflicted.has(path)).sort(),
92
+ unchanged: unchanged.filter((path) => !conflicted.has(path)).sort(),
93
+ removed: written.removed,
94
+ kept: written.kept,
95
+ conflicts: written.conflicts,
96
+ problems,
97
+ };
98
+ }
@@ -0,0 +1,23 @@
1
+ import { type Problem } from "./errors.js";
2
+ import { type LoadProjectOptions } from "./loader.js";
3
+ /** `pactwright validate` result (Delivery Graph §21). */
4
+ export interface ValidationReport {
5
+ readonly ok: boolean;
6
+ /** Every problem found in one pass; empty when `ok`. */
7
+ readonly problems: readonly Problem[];
8
+ /** Present when `ok`: what was validated. */
9
+ readonly summary?: {
10
+ readonly nodes: number;
11
+ readonly edges: number;
12
+ readonly lineages: number;
13
+ readonly revision: string;
14
+ };
15
+ }
16
+ /**
17
+ * Validates core Delivery Graph integrity and shared typed-edge integrity
18
+ * through the one canonical loading path (config, lifecycle, lock, node
19
+ * schemas, typed-edge registry, current-lineage ambiguity). Validation does
20
+ * not require the lifecycle to be complete. Never throws for expected
21
+ * failures: an unloadable or absent project is reported as problems.
22
+ */
23
+ export declare function validateProject(options?: LoadProjectOptions): ValidationReport;
@@ -0,0 +1,32 @@
1
+ import { PactwrightError } from "./errors.js";
2
+ import { deriveLineages } from "./graph/lineage.js";
3
+ import { graphRevision } from "./graph/revision.js";
4
+ import { loadProject } from "./loader.js";
5
+ /**
6
+ * Validates core Delivery Graph integrity and shared typed-edge integrity
7
+ * through the one canonical loading path (config, lifecycle, lock, node
8
+ * schemas, typed-edge registry, current-lineage ambiguity). Validation does
9
+ * not require the lifecycle to be complete. Never throws for expected
10
+ * failures: an unloadable or absent project is reported as problems.
11
+ */
12
+ export function validateProject(options = {}) {
13
+ try {
14
+ const project = loadProject(options);
15
+ const { nodes, edges } = project.graph;
16
+ return {
17
+ ok: true,
18
+ problems: [],
19
+ summary: {
20
+ nodes: nodes.length,
21
+ edges: edges.length,
22
+ lineages: deriveLineages(nodes, edges).lineages.length,
23
+ revision: graphRevision({ nodes, edges }),
24
+ },
25
+ };
26
+ }
27
+ catch (error) {
28
+ if (!(error instanceof PactwrightError))
29
+ throw error;
30
+ return { ok: false, problems: error.problems };
31
+ }
32
+ }
@@ -0,0 +1,24 @@
1
+ import type { Problem } from "./errors.js";
2
+ /**
3
+ * Collects problems for one file. Every `expect*` helper records a problem
4
+ * and returns `undefined` on failure so parsers can keep going and report
5
+ * everything wrong with a file in one pass.
6
+ */
7
+ export declare class Checker {
8
+ readonly path: string;
9
+ readonly problems: Problem[];
10
+ constructor(path: string);
11
+ fail(code: string, message: string): undefined;
12
+ get ok(): boolean;
13
+ }
14
+ export type Unknown = unknown;
15
+ export type UnknownRecord = Record<string, unknown>;
16
+ export declare function isRecord(value: unknown): value is UnknownRecord;
17
+ export declare function expectRecord(c: Checker, value: unknown, label: string): UnknownRecord | undefined;
18
+ export declare function expectString(c: Checker, value: unknown, label: string): string | undefined;
19
+ export declare function expectInteger(c: Checker, value: unknown, label: string): number | undefined;
20
+ export declare function expectBoolean(c: Checker, value: unknown, label: string): boolean | undefined;
21
+ export declare function expectEnum<T extends string>(c: Checker, value: unknown, label: string, allowed: readonly T[]): T | undefined;
22
+ export declare function expectVersion(c: Checker, value: unknown, label: string, expected: number): void;
23
+ export declare function requireKeys(c: Checker, record: UnknownRecord, label: string, keys: readonly string[]): void;
24
+ export declare function rejectUnknownKeys(c: Checker, record: UnknownRecord, label: string, allowed: readonly string[]): void;