cohorte 3.0.0-dev.0 → 3.0.0-dev.2

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 (53) hide show
  1. package/assets/manifest.json +4 -4
  2. package/assets/schemas/reconcile-plan.schema.json +1 -2
  3. package/dist/agent-host.mjs +2142 -5
  4. package/dist/agent-host.mjs.map +1 -1
  5. package/dist/bundle-manifest.json +38 -36
  6. package/dist/chunks/{auth-D8rCVY33.mjs → auth-ybswxUPK.mjs} +54 -2
  7. package/dist/chunks/auth-ybswxUPK.mjs.map +1 -0
  8. package/dist/chunks/{brainstorm-CU10T1tm.mjs → brainstorm-BwKA6yZP.mjs} +2 -2
  9. package/dist/chunks/{brainstorm-CU10T1tm.mjs.map → brainstorm-BwKA6yZP.mjs.map} +1 -1
  10. package/dist/chunks/{catalogue-BvwSrM_j.mjs → catalogue-B5uFqfzJ.mjs} +2 -2
  11. package/dist/chunks/{catalogue-BvwSrM_j.mjs.map → catalogue-B5uFqfzJ.mjs.map} +1 -1
  12. package/dist/chunks/{compose-Cr0hP0qm.mjs → compose-C0rEh5wB.mjs} +3354 -3343
  13. package/dist/chunks/compose-C0rEh5wB.mjs.map +1 -0
  14. package/dist/chunks/{contract-B0ra2-bm.mjs → contract-BkMjU1kR.mjs} +2 -2
  15. package/dist/chunks/{contract-B0ra2-bm.mjs.map → contract-BkMjU1kR.mjs.map} +1 -1
  16. package/dist/chunks/{contract-vYbwYr0Y.mjs → contract-bQ9FJl4N.mjs} +2 -2
  17. package/dist/chunks/contract-bQ9FJl4N.mjs.map +1 -0
  18. package/dist/chunks/{diff-Qw0II0sj.mjs → diff-D_0hBwHl.mjs} +2 -2
  19. package/dist/chunks/{diff-Qw0II0sj.mjs.map → diff-D_0hBwHl.mjs.map} +1 -1
  20. package/dist/chunks/{discover-30Ciya_0.mjs → discover-eS01-me1.mjs} +3 -2
  21. package/dist/chunks/{discover-30Ciya_0.mjs.map → discover-eS01-me1.mjs.map} +1 -1
  22. package/dist/chunks/{doctor-DfCrKpfT.mjs → doctor-CginW8y8.mjs} +5 -5
  23. package/dist/chunks/{doctor-DfCrKpfT.mjs.map → doctor-CginW8y8.mjs.map} +1 -1
  24. package/dist/chunks/init-BUnzIkeE.mjs +63 -0
  25. package/dist/chunks/init-BUnzIkeE.mjs.map +1 -0
  26. package/dist/chunks/migrate-DybihKzZ.mjs +33 -0
  27. package/dist/chunks/migrate-DybihKzZ.mjs.map +1 -0
  28. package/dist/chunks/{policy-DUIgzF3W.mjs → policy-Clypo20s.mjs} +3 -3
  29. package/dist/chunks/{policy-DUIgzF3W.mjs.map → policy-Clypo20s.mjs.map} +1 -1
  30. package/dist/chunks/reconcile-Bhy3IIYo.mjs +50 -0
  31. package/dist/chunks/reconcile-Bhy3IIYo.mjs.map +1 -0
  32. package/dist/chunks/scan-8Gy_B6kI.mjs +122 -0
  33. package/dist/chunks/scan-8Gy_B6kI.mjs.map +1 -0
  34. package/dist/chunks/src-CmFjh4M0.mjs +928 -0
  35. package/dist/chunks/src-CmFjh4M0.mjs.map +1 -0
  36. package/dist/chunks/{src-CfAOde7C.mjs → src-DZazJCp1.mjs} +2 -2
  37. package/dist/chunks/{src-CfAOde7C.mjs.map → src-DZazJCp1.mjs.map} +1 -1
  38. package/dist/chunks/{src-CId6kMsY.mjs → src-r0ECB1UJ.mjs} +2 -2
  39. package/dist/chunks/{src-CId6kMsY.mjs.map → src-r0ECB1UJ.mjs.map} +1 -1
  40. package/dist/cli.mjs +12 -12
  41. package/dist/cli.mjs.map +1 -1
  42. package/package.json +1 -1
  43. package/dist/chunks/auth-D8rCVY33.mjs.map +0 -1
  44. package/dist/chunks/compose-Cr0hP0qm.mjs.map +0 -1
  45. package/dist/chunks/contract-vYbwYr0Y.mjs.map +0 -1
  46. package/dist/chunks/init-Okz4mlhW.mjs +0 -30
  47. package/dist/chunks/init-Okz4mlhW.mjs.map +0 -1
  48. package/dist/chunks/migrate-DRoFUg53.mjs +0 -19
  49. package/dist/chunks/migrate-DRoFUg53.mjs.map +0 -1
  50. package/dist/chunks/reconcile-BfD785gd.mjs +0 -26
  51. package/dist/chunks/reconcile-BfD785gd.mjs.map +0 -1
  52. package/dist/chunks/src-JxawBQBn.mjs +0 -446
  53. package/dist/chunks/src-JxawBQBn.mjs.map +0 -1
@@ -0,0 +1,928 @@
1
+ import { n as sha256Hex, t as canonicalJson } from "./canonical-C8UxeEmB.mjs";
2
+ import "./src-Cu3TxNob.mjs";
3
+ import "./contract-bQ9FJl4N.mjs";
4
+ import { r as specContentSha256, s as DEFAULT_CONFIG } from "./schema-C9QbUW4H.mjs";
5
+ import { createHash } from "node:crypto";
6
+ import { appendFile, copyFile, cp, lstat, mkdir, readFile, readdir, rename, rm, stat, unlink, writeFile } from "node:fs/promises";
7
+ import { dirname, join, relative, resolve, sep } from "node:path";
8
+ import { parse, stringify } from "yaml";
9
+ //#region ../../packages/project-model/src/desired/index.ts
10
+ function deriveDesiredState(input) {
11
+ const modelYaml = stringify(input.model);
12
+ const generatedArtifacts = /* @__PURE__ */ new Map();
13
+ for (const path of input.model.generatedArtifacts.value) {
14
+ const relative = path.replace(/^\.cohorte\//u, "");
15
+ if (relative === "manifest.yaml" || relative === "project.yaml") continue;
16
+ generatedArtifacts.set(relative, canonicalJson({
17
+ path: relative,
18
+ model: input.model,
19
+ config: input.config,
20
+ skills: input.skills
21
+ }));
22
+ }
23
+ const manifest = {
24
+ schemaVersion: 1,
25
+ cohorteVersion: input.cohorteVersion,
26
+ createdWith: input.cohorteVersion,
27
+ protocol: {
28
+ min: "3.0",
29
+ max: "3.x"
30
+ },
31
+ stateSchemaVersion: 1,
32
+ generated: [
33
+ {
34
+ path: "project.yaml",
35
+ templateId: "project-model/v1",
36
+ templateSha256: sha256Hex(modelYaml),
37
+ renderedSha256: sha256Hex(modelYaml)
38
+ },
39
+ {
40
+ path: ".gitignore",
41
+ templateId: "cohorte/gitignore/v1",
42
+ templateSha256: sha256Hex("state/\nruns/\n"),
43
+ renderedSha256: sha256Hex("state/\nruns/\n")
44
+ },
45
+ {
46
+ path: "generated/.gitkeep",
47
+ templateId: "cohorte/generated-dir/v1",
48
+ templateSha256: sha256Hex(""),
49
+ renderedSha256: sha256Hex("")
50
+ },
51
+ ...[...generatedArtifacts.keys()].map((path) => ({
52
+ path,
53
+ templateId: path,
54
+ templateSha256: sha256Hex(generatedArtifacts.get(path)),
55
+ renderedSha256: sha256Hex(generatedArtifacts.get(path))
56
+ }))
57
+ ]
58
+ };
59
+ const builtin = {
60
+ "manifest.yaml": stringify(manifest),
61
+ "project.yaml": modelYaml,
62
+ "config.yaml": "schemaVersion: 1\n",
63
+ "ownership.yaml": "surfaces: {}\n",
64
+ ".gitignore": "state/\nruns/\n",
65
+ "generated/.gitkeep": ""
66
+ };
67
+ const stateFiles = [.../* @__PURE__ */ new Set([...Object.keys(builtin), ...generatedArtifacts.keys()])].sort().map((path) => ({
68
+ path,
69
+ class: path === "config.yaml" || path === "ownership.yaml" ? "human" : "generated",
70
+ sha256: sha256Hex(builtin[path] ?? generatedArtifacts.get(path) ?? ""),
71
+ content: builtin[path] ?? generatedArtifacts.get(path) ?? "",
72
+ ...input.skills[path] === void 0 ? {} : { templateId: path }
73
+ }));
74
+ return {
75
+ cohorteVersion: input.cohorteVersion,
76
+ files: stateFiles
77
+ };
78
+ }
79
+ //#endregion
80
+ //#region ../../packages/project-model/src/drift/index.ts
81
+ async function readActualState(root) {
82
+ const stateRoot = join(root, ".cohorte");
83
+ let manifest = null;
84
+ try {
85
+ manifest = parse(await readFile(join(stateRoot, "manifest.yaml"), "utf8"));
86
+ } catch {
87
+ return {
88
+ manifest: null,
89
+ files: []
90
+ };
91
+ }
92
+ const files = [];
93
+ const visit = async (dir) => {
94
+ try {
95
+ const entries = await readdir(dir, { withFileTypes: true });
96
+ for (const entry of entries.sort((a, b) => a.name.localeCompare(b.name))) {
97
+ if (entry.name === "state" || entry.name === "runs") continue;
98
+ const path = join(dir, entry.name);
99
+ if (entry.isDirectory()) await visit(path);
100
+ else if (entry.isFile()) {
101
+ const bytes = await readFile(path);
102
+ const relativePath = relative(stateRoot, path).split("\\").join("/");
103
+ const generated = relativePath === "manifest.yaml" ? {
104
+ path: relativePath,
105
+ templateId: "cohorte/manifest/v1",
106
+ renderedSha256: sha256Hex(bytes)
107
+ } : manifest?.generated.find((item) => item.path === relativePath);
108
+ files.push({
109
+ path: relativePath,
110
+ class: generated ? "generated" : "human",
111
+ sha256: sha256Hex(bytes),
112
+ ...generated?.templateId ? { templateId: generated.templateId } : {}
113
+ });
114
+ }
115
+ }
116
+ } catch {
117
+ return;
118
+ }
119
+ };
120
+ await visit(stateRoot);
121
+ return {
122
+ manifest,
123
+ files
124
+ };
125
+ }
126
+ function diffStates(desired, actual, clock) {
127
+ const desiredByPath = new Map(desired.files.map((file) => [file.path, file]));
128
+ const actualByPath = new Map(actual.files.map((file) => [file.path, file]));
129
+ const recorded = new Map((actual.manifest?.generated ?? []).map((file) => [file.path, file.renderedSha256]));
130
+ const entries = [.../* @__PURE__ */ new Set([...desiredByPath.keys(), ...actualByPath.keys()])].sort().flatMap((path) => {
131
+ const expected = desiredByPath.get(path);
132
+ const found = actualByPath.get(path);
133
+ const recordedSha256 = recorded.get(path);
134
+ if (!found && expected) return [{
135
+ target: path,
136
+ class: expected.class,
137
+ diff: "absent",
138
+ desiredSha256: expected.sha256,
139
+ ...recordedSha256 ? { recordedSha256 } : {},
140
+ detail: "desired file is absent"
141
+ }];
142
+ if (found && !expected) return [{
143
+ target: path,
144
+ class: found.class,
145
+ diff: found.class === "generated" ? "potential-deletion" : "unknown",
146
+ actualSha256: found.sha256,
147
+ ...recordedSha256 ? { recordedSha256 } : {},
148
+ detail: "file exists but is not desired"
149
+ }];
150
+ if (!found || !expected) throw new Error("unreachable drift entry");
151
+ if (found.sha256 === expected.sha256) return [];
152
+ if (found.class === "human") return [{
153
+ target: path,
154
+ class: found.class,
155
+ diff: "human-change",
156
+ desiredSha256: expected.sha256,
157
+ actualSha256: found.sha256,
158
+ ...recordedSha256 ? { recordedSha256 } : {},
159
+ detail: "content changed in a human-owned file"
160
+ }];
161
+ if (recordedSha256 === void 0 || found.sha256 === recordedSha256) return [{
162
+ target: path,
163
+ class: expected.class,
164
+ diff: "expected-change",
165
+ desiredSha256: expected.sha256,
166
+ actualSha256: found.sha256,
167
+ ...recordedSha256 ? { recordedSha256 } : {},
168
+ detail: "generated content differs from the previous render"
169
+ }];
170
+ return [{
171
+ target: path,
172
+ class: found.class,
173
+ diff: "conflict",
174
+ desiredSha256: expected.sha256,
175
+ actualSha256: found.sha256,
176
+ recordedSha256,
177
+ detail: "content changed since the recorded render"
178
+ }];
179
+ });
180
+ return {
181
+ schemaVersion: 1,
182
+ generatedAt: clock.now(),
183
+ entries
184
+ };
185
+ }
186
+ //#endregion
187
+ //#region ../../packages/project-model/src/import-v2/index.ts
188
+ const FORMAT = "cohorte-v2-export";
189
+ const MAX_FILE_BYTES = 10485760;
190
+ const SKIP_DIRS = /* @__PURE__ */ new Set([
191
+ ".git",
192
+ "node_modules",
193
+ ".build",
194
+ "dist",
195
+ "coverage",
196
+ ".cohorte"
197
+ ]);
198
+ const SECRET = /(^|[/\\])(?:\.env(?:\..*)?|credentials?(?:\..*)?|secrets?(?:\..*)?|.*token.*|.*password.*|.*api[-_]?key.*)$/iu;
199
+ function normalisePath(value) {
200
+ const path = value.replaceAll("\\", "/");
201
+ if (!path || path.startsWith("/") || path.split("/").some((part) => part === ".." || part === "")) throw new Error(`configuration/import-invalid: unsafe relative path ${JSON.stringify(value)}`);
202
+ return path;
203
+ }
204
+ async function walk(root, current = root) {
205
+ const entries = await readdir(current, { withFileTypes: true });
206
+ const result = [];
207
+ for (const entry of entries.sort((a, b) => a.name.localeCompare(b.name))) {
208
+ if (entry.isDirectory() && SKIP_DIRS.has(entry.name)) continue;
209
+ const absolute = join(current, entry.name);
210
+ const rel = relative(root, absolute).split(sep).join("/");
211
+ if (entry.isSymbolicLink()) throw new Error(`configuration/import-symlink: refusing symbolic link ${rel}`);
212
+ if (entry.isDirectory()) result.push(...await walk(root, absolute));
213
+ else if (entry.isFile()) result.push(rel);
214
+ }
215
+ return result.sort();
216
+ }
217
+ function fileKind(path) {
218
+ if (path === "PIPELINE.md" || path === "cohorte.config.yaml" || path.startsWith("profile/")) return "config";
219
+ if (path.startsWith("specs/")) return path.startsWith("specs/reports/") ? "history" : "spec";
220
+ return "marker";
221
+ }
222
+ function warning(code, message, path, blocking = false) {
223
+ return {
224
+ code,
225
+ message,
226
+ ...path === void 0 ? {} : { path },
227
+ blocking
228
+ };
229
+ }
230
+ async function digestFiles(root, files) {
231
+ const hash = createHash("sha256");
232
+ for (const path of files) {
233
+ hash.update(path);
234
+ hash.update(await readFile(join(root, path)));
235
+ }
236
+ return hash.digest("hex");
237
+ }
238
+ function sourceVersion(pipeline) {
239
+ return (pipeline?.match(/(?:version|core_version)\s*[:=]\s*["']?([0-9]+\.[0-9]+\.[0-9]+)/iu))?.[1] ?? null;
240
+ }
241
+ async function exportV2(options) {
242
+ const root = resolve(options.root);
243
+ const destination = resolve(options.destination);
244
+ const all = await walk(root);
245
+ const files = [];
246
+ const excluded = [];
247
+ const warnings = [];
248
+ for (const path of all) {
249
+ if (SECRET.test(path)) {
250
+ excluded.push({
251
+ path,
252
+ reason: "sensitive-path"
253
+ });
254
+ warnings.push(warning("sensitive-excluded", "sensitive file excluded from export", path));
255
+ continue;
256
+ }
257
+ if ((await stat(join(root, path))).size > MAX_FILE_BYTES) {
258
+ excluded.push({
259
+ path,
260
+ reason: "file-too-large"
261
+ });
262
+ warnings.push(warning("file-too-large", `file exceeds ${MAX_FILE_BYTES} bytes`, path, true));
263
+ continue;
264
+ }
265
+ const content = await readFile(join(root, path));
266
+ files.push({
267
+ path,
268
+ sha256: sha256Hex(content),
269
+ bytes: content.byteLength,
270
+ kind: fileKind(path)
271
+ });
272
+ }
273
+ const selected = files.map((file) => file.path);
274
+ const pipeline = all.includes("PIPELINE.md") ? await readFile(join(root, "PIPELINE.md"), "utf8") : void 0;
275
+ const manifest = {
276
+ format: FORMAT,
277
+ version: 1,
278
+ sourceVersion: sourceVersion(pipeline),
279
+ createdAt: options.now ?? (/* @__PURE__ */ new Date()).toISOString(),
280
+ projectRootDigest: await digestFiles(root, selected),
281
+ files,
282
+ excluded,
283
+ warnings
284
+ };
285
+ await rm(destination, {
286
+ recursive: true,
287
+ force: true
288
+ });
289
+ await mkdir(join(destination, "files"), {
290
+ recursive: true,
291
+ mode: 448
292
+ });
293
+ for (const file of files) {
294
+ const target = join(destination, "files", file.path);
295
+ await mkdir(dirname(target), {
296
+ recursive: true,
297
+ mode: 448
298
+ });
299
+ await writeFile(target, await readFile(join(root, file.path)), { mode: 384 });
300
+ }
301
+ await writeFile(join(destination, "manifest.json"), `${JSON.stringify(manifest, null, 2)}\n`, { mode: 384 });
302
+ const checksums = files.map((file) => `${file.sha256} files/${file.path}`).concat(`${sha256Hex(canonicalJson(manifest))} manifest.json`).join("\n");
303
+ const checksumsPath = join(destination, "checksums.sha256");
304
+ await writeFile(checksumsPath, `${checksums}\n`, { mode: 384 });
305
+ return {
306
+ bundleRoot: destination,
307
+ manifest,
308
+ checksumsPath
309
+ };
310
+ }
311
+ async function readBundle(bundleRoot) {
312
+ const root = resolve(bundleRoot);
313
+ const manifestPath = join(root, "manifest.json");
314
+ const manifestInfo = await lstat(manifestPath);
315
+ if (!manifestInfo.isFile() || manifestInfo.isSymbolicLink()) throw new Error("security/import-invalid: manifest must be a regular file");
316
+ const manifestText = await readFile(manifestPath, "utf8");
317
+ const manifest = JSON.parse(manifestText);
318
+ if (manifest.format !== FORMAT || manifest.version !== 1) throw new Error("configuration/import-invalid: unsupported V2 export format");
319
+ const files = /* @__PURE__ */ new Map();
320
+ const expectedChecksums = /* @__PURE__ */ new Map();
321
+ const checksums = await readFile(join(root, "checksums.sha256"), "utf8");
322
+ for (const line of checksums.split("\n").map((item) => item.trim()).filter(Boolean)) {
323
+ const match = line.match(/^([0-9a-f]{64}) {2}(.+)$/u);
324
+ if (!match?.[1] || !match[2]) throw new Error("security/import-invalid: malformed checksums file");
325
+ expectedChecksums.set(match[2], match[1]);
326
+ }
327
+ if (expectedChecksums.get("manifest.json") !== sha256Hex(canonicalJson(manifest))) throw new Error("security/import-checksum-mismatch: manifest.json");
328
+ for (const entry of manifest.files) {
329
+ const path = normalisePath(entry.path);
330
+ if (files.has(path)) throw new Error(`configuration/import-invalid: duplicate path ${path}`);
331
+ const filePath = join(root, "files", path);
332
+ const info = await lstat(filePath);
333
+ if (!info.isFile() || info.isSymbolicLink()) throw new Error(`security/import-invalid: symlink or non-file ${path}`);
334
+ const content = await readFile(filePath, "utf8");
335
+ if (sha256Hex(content) !== entry.sha256) throw new Error(`security/import-checksum-mismatch: ${path}`);
336
+ if (expectedChecksums.get(`files/${path}`) !== entry.sha256) throw new Error(`security/import-checksum-mismatch: files/${path}`);
337
+ files.set(path, content);
338
+ }
339
+ const sourceDigest = await digestFiles(join(root, "files"), [...files.keys()]);
340
+ if (sourceDigest !== manifest.projectRootDigest) throw new Error("security/import-checksum-mismatch: project root digest does not match manifest");
341
+ return {
342
+ manifest,
343
+ files,
344
+ sourceDigest
345
+ };
346
+ }
347
+ function pipelineBlock(input) {
348
+ const match = input?.match(/```ya?ml\s+pipeline-profile\s*\n([\s\S]*?)\n```/iu);
349
+ if (!match?.[1]) return {};
350
+ try {
351
+ const parsed = parse(match[1]);
352
+ return parsed && typeof parsed === "object" && !Array.isArray(parsed) ? parsed : {};
353
+ } catch {
354
+ return {};
355
+ }
356
+ }
357
+ function rule(id, program, decision) {
358
+ return {
359
+ id,
360
+ program,
361
+ decision,
362
+ replay: "idempotent",
363
+ network: false,
364
+ origin: "project-config"
365
+ };
366
+ }
367
+ function mapConfig(input, pipeline, model) {
368
+ const warnings = [];
369
+ let document = {};
370
+ if (input !== void 0) try {
371
+ const parsed = parse(input);
372
+ if (parsed && typeof parsed === "object") document = parsed;
373
+ } catch {
374
+ warnings.push(warning("config-invalid-yaml", "V2 configuration is not valid YAML", "cohorte.config.yaml", true));
375
+ }
376
+ const profile = pipelineBlock(pipeline);
377
+ if (profile.name === void 0) warnings.push(warning("config-project-id-missing", "project id needs human confirmation"));
378
+ if (document.kanban !== void 0) warnings.push(warning("kanban-not-imported", "Kanban links remain external and are not synchronized"));
379
+ const id = typeof profile.name === "string" ? profile.name : "imported-project";
380
+ const vcs = profile.vcs ?? {};
381
+ const commands = profile.commands ?? {};
382
+ const gate = profile.gate ?? {};
383
+ const defaultBranch = typeof vcs.default_branch === "string" ? vcs.default_branch : DEFAULT_CONFIG.project.defaultBranch;
384
+ const branchPrefix = typeof vcs.feature_branch_prefix === "string" ? vcs.feature_branch_prefix : DEFAULT_CONFIG.git.branchPrefix;
385
+ const migrated = structuredClone(DEFAULT_CONFIG);
386
+ migrated.project = {
387
+ ...migrated.project,
388
+ id,
389
+ defaultBranch,
390
+ protectedBranches: [defaultBranch]
391
+ };
392
+ migrated.git = {
393
+ ...migrated.git,
394
+ branchPrefix
395
+ };
396
+ migrated.checks = {
397
+ ...migrated.checks,
398
+ ...typeof commands.test === "string" ? { test: commands.test.split(/\s+/u) } : {},
399
+ ...typeof commands.lint === "string" ? { lint: commands.lint.split(/\s+/u) } : {},
400
+ ...typeof commands.typecheck === "string" ? { typecheck: commands.typecheck.split(/\s+/u) } : {}
401
+ };
402
+ const deny = Array.isArray(gate.deny) ? gate.deny.filter((v) => typeof v === "string") : [];
403
+ const ask = Array.isArray(gate.ask) ? gate.ask.filter((v) => typeof v === "string") : [];
404
+ migrated.policy.commands = {
405
+ allow: [],
406
+ ask: ask.map((value, index) => rule(`v2-ask-${index}`, value.split(/\s+/u)[0] ?? value, "ask")),
407
+ deny: deny.map((value, index) => rule(`v2-deny-${index}`, value.split(/\s+/u)[0] ?? value, "deny"))
408
+ };
409
+ warnings.push(warning("runtime-review-required", "V2 runtime adapters are not migrated; review the current runtime selection"));
410
+ const surfaces = Object.fromEntries(Object.entries(model?.surfaces ?? {}).map(([key, value]) => [key, {
411
+ paths: value.paths.value,
412
+ owners: ["implementer"],
413
+ reviewers: ["reviewer"]
414
+ }]));
415
+ return {
416
+ config: stringify(migrated),
417
+ ownership: stringify({ surfaces }),
418
+ warnings
419
+ };
420
+ }
421
+ function section(markdown, number, fallback) {
422
+ return markdown.match(new RegExp(`^##\\s+${number}\\.?.*?\\n([\\s\\S]*?)(?=^##\\s+|$)`, "imu"))?.[1]?.trim() ?? fallback;
423
+ }
424
+ function bullets(text) {
425
+ return text.split("\n").map((line) => line.match(/^\s*(?:[-*]|\d+[.)])\s+(.*)$/u)?.[1]?.trim()).filter((value) => Boolean(value));
426
+ }
427
+ function importedSpec(path, markdown) {
428
+ const front = markdown.match(/^---\s*\n([\s\S]*?)\n---/u);
429
+ const metadata = front?.[1] ? parse(front[1]) : {};
430
+ const filename = path.split("/").pop()?.replace(/\.md$/iu, "") ?? "imported-spec";
431
+ const id = String(metadata.feature_id ?? filename).toLowerCase().replace(/[^a-z0-9]+/gu, "-").replace(/^-|-$/gu, "").slice(0, 48) || "imported-spec";
432
+ const heading = markdown.match(/^#\s+(.+)$/mu)?.[1]?.trim();
433
+ const title = String(metadata.title ?? heading ?? id);
434
+ const rawStatus = String(metadata.status ?? "draft");
435
+ const status = rawStatus === "draft" ? "draft" : "frozen";
436
+ const acceptance = bullets(section(markdown, "9", ""));
437
+ const openQuestions = bullets(section(markdown, "10", ""));
438
+ const surfaces = {};
439
+ const surfaceSection = section(markdown, "6", "");
440
+ for (const match of surfaceSection.matchAll(/^###\s+([^\n]+)\n([\s\S]*?)(?=^###\s+|$)/gmu)) {
441
+ const key = match[1]?.trim().toLowerCase().replace(/[^a-z0-9]+/gu, "-").replace(/^-|-$/gu, "") || "shared";
442
+ surfaces[key] = { tasks: bullets(match[2] ?? "") };
443
+ }
444
+ if (Object.keys(surfaces).length === 0) surfaces.shared = { tasks: ["Review imported V2 specification and assign implementation tasks."] };
445
+ const body = {
446
+ id,
447
+ kind: "feature",
448
+ status,
449
+ title,
450
+ acceptance: acceptance.length ? acceptance : [`Review imported V2 specification: ${title}`],
451
+ surfaces,
452
+ openQuestions: [...openQuestions, ...rawStatus === "draft" ? [] : [`Original V2 status: ${rawStatus}`]]
453
+ };
454
+ if (status === "frozen") return {
455
+ id,
456
+ content: stringify({
457
+ ...body,
458
+ sha256: specContentSha256(body)
459
+ })
460
+ };
461
+ return {
462
+ id,
463
+ content: stringify(body),
464
+ ...rawStatus === "draft" ? {} : { warning: warning("spec-status-normalized", `V2 status ${rawStatus} was normalized to frozen`, path) }
465
+ };
466
+ }
467
+ async function planV2Import(bundleRoot, projectRoot, options = {}) {
468
+ const bundle = await readBundle(bundleRoot);
469
+ const modelSource = bundle.files.get("PIPELINE.md");
470
+ const mapped = mapConfig(bundle.files.get("cohorte.config.yaml"), modelSource, options.model);
471
+ const projectContent = options.model === void 0 ? void 0 : stringify(options.model);
472
+ const generated = projectContent === void 0 ? [] : [{
473
+ path: "project.yaml",
474
+ templateId: "project-model/v1",
475
+ templateSha256: sha256Hex(projectContent),
476
+ renderedSha256: sha256Hex(projectContent)
477
+ }];
478
+ const manifestContent = stringify({
479
+ schemaVersion: 1,
480
+ cohorteVersion: "3.0.0",
481
+ createdWith: "v2-import",
482
+ protocol: {
483
+ min: "3.0",
484
+ max: "3.x"
485
+ },
486
+ stateSchemaVersion: 1,
487
+ generated
488
+ });
489
+ const convertedSpecs = bundle.manifest.files.filter((entry) => entry.kind === "spec").map((entry) => ({
490
+ entry,
491
+ converted: importedSpec(entry.path, bundle.files.get(entry.path) ?? "")
492
+ }));
493
+ const specWarnings = convertedSpecs.flatMap(({ converted }) => converted.warning ? [converted.warning] : []);
494
+ const files = [
495
+ {
496
+ path: ".cohorte/manifest.yaml",
497
+ content: manifestContent,
498
+ action: "create",
499
+ reason: "V3 migration marker"
500
+ },
501
+ {
502
+ path: ".cohorte/.gitignore",
503
+ content: "state/\nruns/\n",
504
+ action: "create",
505
+ reason: "protect V3 local state"
506
+ },
507
+ {
508
+ path: ".cohorte/config.yaml",
509
+ content: mapped.config,
510
+ action: "create",
511
+ reason: "mapped V2 configuration"
512
+ },
513
+ {
514
+ path: ".cohorte/ownership.yaml",
515
+ content: mapped.ownership,
516
+ action: "create",
517
+ reason: "new V3 ownership document"
518
+ },
519
+ ...options.model === void 0 ? [] : [{
520
+ path: ".cohorte/project.yaml",
521
+ content: projectContent ?? "",
522
+ action: "create",
523
+ reason: "deterministic V3 project model generated during import"
524
+ }],
525
+ ...modelSource === void 0 ? [] : [{
526
+ path: ".cohorte/import-source/PIPELINE.md",
527
+ content: modelSource,
528
+ action: "create",
529
+ reason: "preserve V2 source"
530
+ }],
531
+ ...convertedSpecs.map(({ entry, converted }) => ({
532
+ path: `.cohorte/specs/${converted.id}.yaml`,
533
+ content: converted.content,
534
+ action: "create",
535
+ reason: `convert V2 Markdown specification ${entry.path} to native schema`
536
+ })),
537
+ ...bundle.manifest.files.filter((entry) => entry.kind === "history").map((entry) => ({
538
+ path: `.cohorte/artifacts/v2-history/${normalisePath(entry.path).replace(/^specs\/reports\//u, "")}`,
539
+ content: bundle.files.get(entry.path) ?? "",
540
+ action: "create",
541
+ reason: "import V2 historical artifact"
542
+ }))
543
+ ];
544
+ const currentDigest = await digestProject(projectRoot);
545
+ const conflicts = [];
546
+ for (const file of files) try {
547
+ const currentPath = join(projectRoot, file.path);
548
+ if ((await lstat(currentPath)).isSymbolicLink()) {
549
+ file.action = "conflict";
550
+ file.reason = "target is a symbolic link";
551
+ conflicts.push(file.path);
552
+ continue;
553
+ }
554
+ if (await readFile(currentPath, "utf8") !== file.content) {
555
+ file.action = "conflict";
556
+ file.reason = "target exists with different content";
557
+ conflicts.push(file.path);
558
+ } else file.action = "keep";
559
+ } catch {}
560
+ return {
561
+ bundleRoot: resolve(bundleRoot),
562
+ projectRoot: resolve(projectRoot),
563
+ sourceVersion: bundle.manifest.sourceVersion,
564
+ files,
565
+ warnings: [
566
+ ...bundle.manifest.warnings,
567
+ ...mapped.warnings,
568
+ ...specWarnings
569
+ ],
570
+ conflicts,
571
+ sourceDigest: bundle.sourceDigest,
572
+ targetDigest: currentDigest
573
+ };
574
+ }
575
+ async function digestProject(root) {
576
+ return digestFiles(root, (await walk(root)).filter((path) => !path.startsWith(".cohorte/import-preview/")));
577
+ }
578
+ async function applyV2Import(plan, options) {
579
+ if (plan.conflicts.length) throw new Error(`configuration/import-conflict: ${plan.conflicts.join(", ")}`);
580
+ if (plan.warnings.some((item) => item.blocking)) throw new Error("configuration/import-blocked: resolve blocking warnings first");
581
+ if (!options.confirm) throw new Error("configuration/import-confirmation-required: rerun with explicit confirmation");
582
+ if (await digestProject(plan.projectRoot) !== plan.targetDigest) throw new Error("configuration/import-project-changed: project changed since preview");
583
+ const id = sha256Hex(`${plan.sourceDigest}:${plan.targetDigest}:${options.now ?? (/* @__PURE__ */ new Date()).toISOString()}`).slice(0, 16);
584
+ const backupRoot = resolve(options.backupRoot, `cohorte-v2-${id}`);
585
+ const stage = join(backupRoot, ".stage");
586
+ const reportPath = join(plan.projectRoot, ".cohorte", "import-reports", `${id}.json`);
587
+ const existing = [];
588
+ let mutated = false;
589
+ const restore = async () => {
590
+ await rm(stage, {
591
+ recursive: true,
592
+ force: true
593
+ });
594
+ await rm(reportPath, { force: true });
595
+ if (!mutated) return;
596
+ for (const file of plan.files) {
597
+ if (file.action === "keep") continue;
598
+ const target = join(plan.projectRoot, file.path);
599
+ const backup = join(backupRoot, file.path);
600
+ try {
601
+ await stat(backup);
602
+ await mkdir(dirname(target), {
603
+ recursive: true,
604
+ mode: 448
605
+ });
606
+ await cp(backup, target, {
607
+ recursive: true,
608
+ verbatimSymlinks: true
609
+ });
610
+ } catch {
611
+ await rm(target, {
612
+ recursive: true,
613
+ force: true
614
+ });
615
+ }
616
+ }
617
+ };
618
+ try {
619
+ options.fault?.("before-backup");
620
+ await mkdir(backupRoot, {
621
+ recursive: true,
622
+ mode: 448
623
+ });
624
+ for (const file of plan.files) try {
625
+ await lstat(join(plan.projectRoot, file.path));
626
+ existing.push(file.path);
627
+ await mkdir(dirname(join(backupRoot, file.path)), {
628
+ recursive: true,
629
+ mode: 448
630
+ });
631
+ await cp(join(plan.projectRoot, file.path), join(backupRoot, file.path), {
632
+ recursive: true,
633
+ verbatimSymlinks: true
634
+ });
635
+ } catch {}
636
+ options.fault?.("after-backup");
637
+ await mkdir(stage, {
638
+ recursive: true,
639
+ mode: 448
640
+ });
641
+ for (const file of plan.files) {
642
+ if (file.action === "keep") continue;
643
+ const target = join(stage, file.path);
644
+ await mkdir(dirname(target), {
645
+ recursive: true,
646
+ mode: 448
647
+ });
648
+ await writeFile(target, file.content, { mode: 384 });
649
+ }
650
+ options.fault?.("after-stage");
651
+ options.fault?.("before-rename");
652
+ for (const file of plan.files) {
653
+ if (file.action === "keep") continue;
654
+ const target = join(plan.projectRoot, file.path);
655
+ await mkdir(dirname(target), {
656
+ recursive: true,
657
+ mode: 448
658
+ });
659
+ mutated = true;
660
+ await rename(join(stage, file.path), target);
661
+ }
662
+ options.fault?.("after-rename");
663
+ await rm(stage, {
664
+ recursive: true,
665
+ force: true
666
+ });
667
+ } catch (error) {
668
+ await restore();
669
+ throw error;
670
+ }
671
+ const report = {
672
+ reportId: id,
673
+ status: "applied",
674
+ bundleRoot: plan.bundleRoot,
675
+ projectRoot: plan.projectRoot,
676
+ backupRoot,
677
+ sourceDigest: plan.sourceDigest,
678
+ targetDigest: plan.targetDigest,
679
+ files: plan.files.filter((file) => file.action !== "keep").map((file) => file.path),
680
+ warnings: plan.warnings,
681
+ createdAt: options.now ?? (/* @__PURE__ */ new Date()).toISOString()
682
+ };
683
+ try {
684
+ options.fault?.("before-report");
685
+ await mkdir(dirname(reportPath), {
686
+ recursive: true,
687
+ mode: 448
688
+ });
689
+ await writeFile(reportPath, `${JSON.stringify({
690
+ ...report,
691
+ existing
692
+ }, null, 2)}\n`, { mode: 384 });
693
+ } catch (error) {
694
+ await restore();
695
+ throw error;
696
+ }
697
+ return report;
698
+ }
699
+ async function rollbackV2Import(report) {
700
+ for (const path of report.files) {
701
+ const backup = join(report.backupRoot, path);
702
+ try {
703
+ await stat(backup);
704
+ await mkdir(dirname(join(report.projectRoot, path)), {
705
+ recursive: true,
706
+ mode: 448
707
+ });
708
+ await cp(backup, join(report.projectRoot, path), {
709
+ recursive: true,
710
+ verbatimSymlinks: true
711
+ });
712
+ } catch {
713
+ await rm(join(report.projectRoot, path), {
714
+ recursive: true,
715
+ force: true
716
+ });
717
+ }
718
+ }
719
+ return {
720
+ ...report,
721
+ status: "rolled-back"
722
+ };
723
+ }
724
+ //#endregion
725
+ //#region ../../packages/project-model/src/init/index.ts
726
+ async function planInit(options) {
727
+ if (options.semantic) throw new Error("configuration/phase-not-available: semantic init is not available in V3.0");
728
+ const root = options.root;
729
+ const files = [
730
+ {
731
+ path: "project.yaml",
732
+ class: "generated",
733
+ content: stringify(options.model),
734
+ templateId: "project-model/v1"
735
+ },
736
+ {
737
+ path: "config.yaml",
738
+ class: "human",
739
+ content: "schemaVersion: 1\n"
740
+ },
741
+ {
742
+ path: "ownership.yaml",
743
+ class: "human",
744
+ content: "surfaces: {}\n"
745
+ },
746
+ {
747
+ path: ".gitignore",
748
+ class: "generated",
749
+ content: "state/\nruns/\n",
750
+ templateId: "cohorte/gitignore/v1"
751
+ },
752
+ {
753
+ path: "generated/.gitkeep",
754
+ class: "generated",
755
+ content: "",
756
+ templateId: "cohorte/generated-dir/v1"
757
+ }
758
+ ];
759
+ const initFiles = [];
760
+ for (const file of files) {
761
+ let exists = false;
762
+ try {
763
+ await readFile(join(root, ".cohorte", file.path));
764
+ exists = true;
765
+ } catch {}
766
+ const renderedSha256 = sha256Hex(file.content);
767
+ initFiles.push({
768
+ path: file.path,
769
+ action: exists ? "keep-existing" : "create",
770
+ class: file.class,
771
+ content: file.content,
772
+ ...file.templateId === void 0 ? {} : { templateId: file.templateId },
773
+ ...file.class === "generated" ? {
774
+ templateSha256: renderedSha256,
775
+ renderedSha256
776
+ } : {}
777
+ });
778
+ }
779
+ const generated = initFiles.filter((file) => file.class === "generated").map((file) => ({
780
+ path: file.path,
781
+ templateId: file.templateId ?? "builtin",
782
+ templateSha256: file.templateSha256 ?? sha256Hex(file.content),
783
+ renderedSha256: file.renderedSha256 ?? sha256Hex(file.content)
784
+ }));
785
+ return {
786
+ projectRoot: root,
787
+ model: options.model,
788
+ files: initFiles,
789
+ manifest: {
790
+ schemaVersion: 1,
791
+ cohorteVersion: options.cohorteVersion,
792
+ createdWith: options.cohorteVersion,
793
+ protocol: {
794
+ min: "3.0",
795
+ max: "3.x"
796
+ },
797
+ stateSchemaVersion: 1,
798
+ generated
799
+ },
800
+ warnings: []
801
+ };
802
+ }
803
+ async function applyInit(plan) {
804
+ await mkdir(join(plan.projectRoot, ".cohorte"), {
805
+ recursive: true,
806
+ mode: 448
807
+ });
808
+ const written = [];
809
+ const kept = [];
810
+ for (const file of plan.files) {
811
+ const target = join(plan.projectRoot, ".cohorte", file.path);
812
+ if (file.action === "keep-existing") {
813
+ kept.push(file.path);
814
+ continue;
815
+ }
816
+ await mkdir(join(target, ".."), {
817
+ recursive: true,
818
+ mode: 448
819
+ });
820
+ await writeFile(target, file.content, { mode: 384 });
821
+ written.push(file.path);
822
+ }
823
+ const manifestPath = join(plan.projectRoot, ".cohorte", "manifest.yaml");
824
+ try {
825
+ await readFile(manifestPath);
826
+ kept.push("manifest.yaml");
827
+ } catch {
828
+ await writeFile(manifestPath, stringify(plan.manifest), { mode: 384 });
829
+ written.push("manifest.yaml");
830
+ }
831
+ return {
832
+ written,
833
+ kept
834
+ };
835
+ }
836
+ //#endregion
837
+ //#region ../../packages/project-model/src/reconcile/index.ts
838
+ /** READ-ONLY. */
839
+ async function planReconcile(options) {
840
+ const model = await options.scan(options.root);
841
+ let stableModel = model;
842
+ try {
843
+ const generatedAt = parse(await readFile(join(options.root, ".cohorte", "project.yaml"), "utf8")).provenance?.generatedAt;
844
+ const stableGeneratedAt = generatedAt instanceof Date ? generatedAt.toISOString() : typeof generatedAt === "string" ? generatedAt : void 0;
845
+ if (stableGeneratedAt !== void 0) stableModel = {
846
+ ...model,
847
+ provenance: {
848
+ ...model.provenance,
849
+ generatedAt: stableGeneratedAt
850
+ }
851
+ };
852
+ } catch {}
853
+ const drift = diffStates(deriveDesiredState({
854
+ model: stableModel,
855
+ config: DEFAULT_CONFIG,
856
+ cohorteVersion: options.cohorteVersion,
857
+ skills: {}
858
+ }), await readActualState(options.root), options.clock);
859
+ const operations = drift.entries.map((entry) => {
860
+ return {
861
+ op: entry.diff === "absent" ? "create" : entry.diff === "expected-change" ? "replace" : entry.diff === "potential-deletion" ? "delete" : entry.diff === "unknown" ? "keep" : "ask",
862
+ target: entry.target,
863
+ diff: entry.diff,
864
+ reason: entry.detail
865
+ };
866
+ });
867
+ return {
868
+ schemaVersion: 1,
869
+ cohorteVersion: options.cohorteVersion,
870
+ generatedAt: drift.generatedAt,
871
+ drift,
872
+ operations,
873
+ conflicts: drift.entries.filter((entry) => entry.diff === "conflict").map((entry) => entry.target),
874
+ applyAvailable: true
875
+ };
876
+ }
877
+ /** Apply only create/replace/delete operations authorized by a conflict-free plan. */
878
+ async function applyReconcile(options) {
879
+ if (options.plan.conflicts.length > 0) throw new Error(`conflict/reconcile-human-edit: ${options.plan.conflicts.join(", ")}`);
880
+ const desiredByPath = new Map(options.desired.files.map((file) => [file.path, file]));
881
+ const backupDir = options.backup ? join(options.root, ".cohorte", "reconcile-backups", options.clock.now().replaceAll(":", "")) : void 0;
882
+ const journal = join(options.root, ".cohorte", "reconcile.log");
883
+ await mkdir(join(options.root, ".cohorte"), {
884
+ recursive: true,
885
+ mode: 448
886
+ });
887
+ const applied = [];
888
+ const skipped = [];
889
+ for (const operation of options.plan.operations) {
890
+ if (operation.op !== "create" && operation.op !== "replace" && operation.op !== "delete") {
891
+ skipped.push(operation.target);
892
+ continue;
893
+ }
894
+ const target = join(options.root, ".cohorte", operation.target);
895
+ if (!target.startsWith(`${join(options.root, ".cohorte")}/`)) throw new Error("security/path-outside-grant");
896
+ const existing = await readFile(target).catch(() => void 0);
897
+ const drift = options.plan.drift.entries.find((entry) => entry.target === operation.target);
898
+ if ((existing === void 0 ? void 0 : sha256Hex(existing)) !== drift?.actualSha256) throw new Error(`conflict/reconcile-race: ${operation.target} changed after the plan was generated`);
899
+ if (existing !== void 0 && backupDir) {
900
+ const backupPath = join(backupDir, operation.target);
901
+ await mkdir(dirname(backupPath), { recursive: true });
902
+ await copyFile(target, backupPath);
903
+ }
904
+ if (operation.op === "delete") await unlink(target).catch(() => void 0);
905
+ else {
906
+ const file = desiredByPath.get(operation.target);
907
+ if (file?.content === void 0) throw new Error(`configuration/reconcile-content-missing: ${operation.target}`);
908
+ await mkdir(dirname(target), { recursive: true });
909
+ await writeFile(target, file.content, "utf8");
910
+ }
911
+ applied.push(operation.target);
912
+ await appendFile(journal, `${JSON.stringify({
913
+ at: options.clock.now(),
914
+ op: operation.op,
915
+ target: operation.target
916
+ })}\n`);
917
+ }
918
+ return {
919
+ applied,
920
+ skipped,
921
+ ...backupDir ? { backupDir } : {},
922
+ journal
923
+ };
924
+ }
925
+ //#endregion
926
+ export { applyV2Import as a, rollbackV2Import as c, planInit as i, deriveDesiredState as l, planReconcile as n, exportV2 as o, applyInit as r, planV2Import as s, applyReconcile as t };
927
+
928
+ //# sourceMappingURL=src-CmFjh4M0.mjs.map