@ybotok/loam 0.2.0-alpha.6 → 0.2.0-alpha.7

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 (105) hide show
  1. package/CHANGELOG.md +162 -0
  2. package/README.md +4 -4
  3. package/ROADMAP.md +27 -6
  4. package/SCHEMA.md +152 -32
  5. package/dist/commands/archive/run.js +49 -22
  6. package/dist/commands/archive/run.js.map +1 -1
  7. package/dist/commands/seed/commit.js +34 -1
  8. package/dist/commands/seed/commit.js.map +1 -1
  9. package/dist/commands/seed/plan.js +64 -13
  10. package/dist/commands/seed/plan.js.map +1 -1
  11. package/dist/commands/subsystem/edit.js +4 -2
  12. package/dist/commands/subsystem/edit.js.map +1 -1
  13. package/dist/commands/subsystem/move.js +55 -17
  14. package/dist/commands/subsystem/move.js.map +1 -1
  15. package/dist/commands/subsystem/sync.js +142 -45
  16. package/dist/commands/subsystem/sync.js.map +1 -1
  17. package/dist/commands/subsystem/txn/report.js +55 -5
  18. package/dist/commands/subsystem/txn/report.js.map +1 -1
  19. package/dist/commands/subsystem/txn/txn.js +118 -21
  20. package/dist/commands/subsystem/txn/txn.js.map +1 -1
  21. package/dist/commands/subsystem/txn/views.js +51 -8
  22. package/dist/commands/subsystem/txn/views.js.map +1 -1
  23. package/dist/commands/validate/checks/health-deps.js +9 -6
  24. package/dist/commands/validate/checks/health-deps.js.map +1 -1
  25. package/dist/commands/validate/fleet/census.js +26 -3
  26. package/dist/commands/validate/fleet/census.js.map +1 -1
  27. package/dist/commands/validate/fleet/landscape.js +12 -9
  28. package/dist/commands/validate/fleet/landscape.js.map +1 -1
  29. package/dist/commands/validate/fleet/map/attest.js +4 -5
  30. package/dist/commands/validate/fleet/map/attest.js.map +1 -1
  31. package/dist/commands/validate/fleet/map/bindings.js +15 -2
  32. package/dist/commands/validate/fleet/map/bindings.js.map +1 -1
  33. package/dist/commands/validate/fleet/map/consumers.js +43 -122
  34. package/dist/commands/validate/fleet/map/consumers.js.map +1 -1
  35. package/dist/commands/validate/fleet/scorecard/scorecard.js.map +1 -1
  36. package/dist/commands/validate/fleet/views/fleet-project.js +1 -1
  37. package/dist/commands/validate/fleet/views/fleet-project.js.map +1 -1
  38. package/dist/commands/validate/fleet/views/ids.js +33 -7
  39. package/dist/commands/validate/fleet/views/ids.js.map +1 -1
  40. package/dist/commands/validate/fleet/views/root-documents.js +9 -7
  41. package/dist/commands/validate/fleet/views/root-documents.js.map +1 -1
  42. package/dist/commands/validate/fleet/views/stale.js +31 -14
  43. package/dist/commands/validate/fleet/views/stale.js.map +1 -1
  44. package/dist/core/agent/agents-md/command-map.js +1 -1
  45. package/dist/core/agent/agents-md/command-map.js.map +1 -1
  46. package/dist/core/agent/agents-md/map/subsystem.js +27 -14
  47. package/dist/core/agent/agents-md/map/subsystem.js.map +1 -1
  48. package/dist/core/agent/agents-md/refusals.js +5 -3
  49. package/dist/core/agent/agents-md/refusals.js.map +1 -1
  50. package/dist/core/agent/agents-md.js +16 -7
  51. package/dist/core/agent/agents-md.js.map +1 -1
  52. package/dist/core/agent/workflows/check.js +5 -6
  53. package/dist/core/agent/workflows/check.js.map +1 -1
  54. package/dist/core/agent/workflows/reference/spine.js +12 -6
  55. package/dist/core/agent/workflows/reference/spine.js.map +1 -1
  56. package/dist/core/brief/brief.js +25 -8
  57. package/dist/core/brief/brief.js.map +1 -1
  58. package/dist/core/brief/targets.js +64 -0
  59. package/dist/core/brief/targets.js.map +1 -1
  60. package/dist/core/brief/unchecked.js +1 -1
  61. package/dist/core/brief/unchecked.js.map +1 -1
  62. package/dist/core/c4/project/architecture.js +31 -9
  63. package/dist/core/c4/project/architecture.js.map +1 -1
  64. package/dist/core/c4/project/documents.js +5 -4
  65. package/dist/core/c4/project/documents.js.map +1 -1
  66. package/dist/core/c4/project/load.js +8 -4
  67. package/dist/core/c4/project/load.js.map +1 -1
  68. package/dist/core/c4/project/staged.js +7 -3
  69. package/dist/core/c4/project/staged.js.map +1 -1
  70. package/dist/core/deployment/delta.js +12 -10
  71. package/dist/core/deployment/delta.js.map +1 -1
  72. package/dist/core/gate/checks.js +5 -1
  73. package/dist/core/gate/checks.js.map +1 -1
  74. package/dist/core/gate/partners.js +60 -17
  75. package/dist/core/gate/partners.js.map +1 -1
  76. package/dist/core/repo/paths.js +34 -29
  77. package/dist/core/repo/paths.js.map +1 -1
  78. package/dist/core/repo/tree/render/body.js +45 -6
  79. package/dist/core/repo/tree/render/body.js.map +1 -1
  80. package/dist/core/repo/tree/render/service/header.js +125 -0
  81. package/dist/core/repo/tree/render/service/header.js.map +1 -0
  82. package/dist/core/repo/tree/render/service/peers.js +28 -0
  83. package/dist/core/repo/tree/render/service/peers.js.map +1 -0
  84. package/dist/core/repo/tree/render/service/view-id.js +67 -0
  85. package/dist/core/repo/tree/render/service/view-id.js.map +1 -0
  86. package/dist/core/repo/tree/render/service/views.js +78 -0
  87. package/dist/core/repo/tree/render/service/views.js.map +1 -0
  88. package/dist/core/repo/tree/render/stale.js +19 -2
  89. package/dist/core/repo/tree/render/stale.js.map +1 -1
  90. package/dist/core/repo/tree/render/views.js +102 -33
  91. package/dist/core/repo/tree/render/views.js.map +1 -1
  92. package/dist/core/verify/checklist.js +42 -10
  93. package/dist/core/verify/checklist.js.map +1 -1
  94. package/dist/core/vocabulary/issue.js.map +1 -1
  95. package/dist/core/vocabulary/maturity.js +41 -1
  96. package/dist/core/vocabulary/maturity.js.map +1 -1
  97. package/examples/README.md +7 -3
  98. package/examples/docs/architecture/landscape.likec4 +13 -7
  99. package/examples/docs/architecture/services.likec4 +70 -0
  100. package/examples/docs/architecture/subsystems.likec4 +3 -0
  101. package/examples/docs/services/order-service/model.likec4 +10 -8
  102. package/examples/docs/services/payment-service/model.likec4 +6 -4
  103. package/examples/docs/services/platform/identity-service/model.likec4 +9 -7
  104. package/examples/docs/services/platform/notification-service/model.likec4 +4 -5
  105. package/package.json +1 -1
@@ -16,8 +16,8 @@ import { clearCommitIntent, writeCommitIntent } from "../../core/staging/recover
16
16
  import { SNAPSHOT_DIR, SnapshotClobberError, snapshotDir, writeSnapshot, } from "../../core/staging/snapshot.js";
17
17
  import { enumeratedServices } from "../../core/repo/service-target.js";
18
18
  import { listFleetTree } from "../../core/repo/repo.js";
19
- import { subsystemViewsPath } from "../../core/repo/paths.js";
20
- import { viewsState } from "../../core/repo/tree/render/stale.js";
19
+ import { serviceViewsPath, subsystemViewsPath } from "../../core/repo/paths.js";
20
+ import { serviceViewsState, viewsState } from "../../core/repo/tree/render/stale.js";
21
21
  import { loadArchitecture } from "../../core/c4/project/architecture.js";
22
22
  import { FleetContext } from "../../core/fleet-context.js";
23
23
  import { gate } from "./plan/gate.js";
@@ -234,19 +234,28 @@ export async function archiveLocked(config, featureId, opts) {
234
234
  : "";
235
235
  throw new ArchiveFailure(failures.length > 0 ? "rollback-incomplete" : "merge-failed", wrapped.message + kept);
236
236
  }
237
- // This merge just changed the map, and the generated views file is a
237
+ // This merge just changed the map, and both generated views files are a
238
238
  // function of the tree AND the map: a delta whose element lands inside a
239
239
  // group that exactly covered a subsystem takes that subsystem's boundary
240
- // line away, and a delta binding an element to a filed service adds an
241
- // include. Archive does not rewrite the file — `loam subsystem sync` owns it
242
- // outright, and folding a second generated artifact into this transaction
243
- // would mean archive computing the post-merge tree AND the post-merge map,
244
- // which is how the two writers disagree — but it must not report a repo as
245
- // current when the next `validate --all` will call it stale. Graded through
246
- // validate's own function, so the two cannot answer differently.
247
- const staleViews = await subsystemViewsStale(config.docsDir);
240
+ // line away, a delta binding an element to a filed service adds an include,
241
+ // and one new edge changes exactly one service's overview. Archive does not
242
+ // rewrite either file — `loam subsystem sync` owns them outright, and folding
243
+ // a second generated artifact into this transaction would mean archive
244
+ // computing the post-merge tree AND the post-merge map, which is how the two
245
+ // writers disagree — but it must not report a repo as current when the next
246
+ // `validate --all` will call it stale. Graded through validate's own
247
+ // functions, so the two cannot answer differently.
248
+ const staleViews = await staleGeneratedViews(config.docsDir);
248
249
  if (json) {
249
- emitJson({ ...payload(true), ...(staleViews ? { subsystemViewsStale: true } : {}) });
250
+ emitJson({
251
+ ...payload(true),
252
+ // The original boolean keeps its exact meaning — `architecture/subsystems.likec4`
253
+ // and nothing else — because a payload key's meaning is frozen; the list
254
+ // beside it is the additive answer that can name the second file. Both are
255
+ // omitted when nothing is stale, exactly as the boolean always was.
256
+ ...(staleViews.length === 0 ? {} : { staleViews }),
257
+ ...(staleViews.includes("architecture/subsystems.likec4") ? { subsystemViewsStale: true } : {}),
258
+ });
250
259
  return;
251
260
  }
252
261
  console.log(`\n archived: features/${dirName} → features/archive/${dirName}`);
@@ -257,12 +266,15 @@ export async function archiveLocked(config, featureId, opts) {
257
266
  // caller told it to merge past. Printing it over either is how a red fleet
258
267
  // gets reported as a finished one.
259
268
  const incomplete = planWarns.filter((w) => w.code === "service.no-model");
260
- if (incomplete.length === 0 && overridden.length === 0 && !staleViews) {
269
+ if (incomplete.length === 0 && overridden.length === 0 && staleViews.length === 0) {
261
270
  console.log(" living spec + landscape are now complete + current.");
262
271
  return;
263
272
  }
264
- if (staleViews) {
265
- console.log(" ⚠ architecture/subsystems.likec4 no longer matches the tree and this map — run `loam subsystem sync`.");
273
+ // One line per stale file rather than one line listing them: each names a file
274
+ // a reader opens, and a comma-joined series would have to choose a verb that
275
+ // reads wrong for one of the two counts.
276
+ for (const path of staleViews) {
277
+ console.log(` ⚠ ${path} no longer matches the tree and this map — run \`loam subsystem sync\`.`);
266
278
  }
267
279
  for (const w of incomplete)
268
280
  console.log(` ⚠ ${w.message}`);
@@ -271,29 +283,44 @@ export async function archiveLocked(config, featureId, opts) {
271
283
  }
272
284
  }
273
285
  /**
274
- * Would the next `validate --all` report `subsystem.views-stale`?
286
+ * Which generated files would the next `validate --all` report
287
+ * `subsystem.views-stale` on? Docs-relative paths, sorted, empty for none.
275
288
  *
276
- * Asked through validate's OWN function rather than a second compare, so the
289
+ * Asked through validate's OWN functions rather than a second compare, so the
277
290
  * warning archive prints and the error validate raises can never disagree —
278
291
  * including about the gate validate holds: a map that does not parse is
279
292
  * `landscape.invalid`'s business, and grading a generated file against a map
280
293
  * nobody can read would cascade a second complaint behind the first. An
281
294
  * unreadable map therefore answers "no claim", exactly as validate does.
295
+ *
296
+ * BOTH generated files, because this merge just changed the map and the
297
+ * per-service overviews are a function of it in a way the subsystem views are
298
+ * not: a delta adding one edge changes exactly one service's peer set, and a
299
+ * delta binding a new element adds a whole view. Naming only the file loam
300
+ * happened to check first would let a repo be reported current while the very
301
+ * next `validate --all` calls it stale — the one thing this advisory exists to
302
+ * prevent.
282
303
  */
283
- async function subsystemViewsStale(docsDir) {
304
+ async function staleGeneratedViews(docsDir) {
284
305
  try {
285
306
  const doc = await loadArchitecture(docsDir);
286
307
  if (doc.errors.length > 0)
287
- return false;
288
- const state = await viewsState(subsystemViewsPath(docsDir), await listFleetTree(docsDir), doc);
289
- return !state.agrees;
308
+ return [];
309
+ const tree = await listFleetTree(docsDir);
310
+ const stale = [];
311
+ if (!(await viewsState(subsystemViewsPath(docsDir), tree, doc)).agrees)
312
+ stale.push("architecture/subsystems.likec4");
313
+ if (!(await serviceViewsState(serviceViewsPath(docsDir), tree, doc)).agrees) {
314
+ stale.push("architecture/services.likec4");
315
+ }
316
+ return stale.sort();
290
317
  }
291
318
  catch {
292
319
  // Tolerant for the reason the snapshot resolver above is: a docs repo with
293
320
  // no `services/` at all still archives a landscape-only feature, and this
294
321
  // question is an ADVISORY printed after a commit that already succeeded.
295
322
  // Nothing here may turn a landed archive into a non-zero exit.
296
- return false;
323
+ return [];
297
324
  }
298
325
  }
299
326
  //# sourceMappingURL=run.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"run.js","sourceRoot":"","sources":["../../../src/commands/archive/run.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * The archive itself, under the docs lock: gate, plan, commit.\n *\n * The order is the command's whole safety property. Every refusal happens before\n * a byte is planned (`./plan/gate.ts`), the plan is computed entirely in memory\n * so a failure on any axis leaves the living docs untouched (`./plan/specs.ts`,\n * `./plan/contracts/`, `./plan/landscape.ts`), and only a plan that succeeded\n * on every axis is staged\n * and swapped. `--dry-run` returns between the second and the third.\n */\nimport { existsSync } from \"node:fs\";\nimport { mkdir, rename } from \"node:fs/promises\";\nimport { emitJson, repoPath, sayExplain } from \"../../core/envelope/json.js\";\nimport {\n message,\n quietPruneEmptyParents,\n quietRm,\n rollbackError,\n rollbackStaged,\n stageWrites,\n swapStaged,\n} from \"../../core/staging/commit.js\";\nimport { clearCommitIntent, writeCommitIntent } from \"../../core/staging/recovery/intent.js\";\nimport {\n SNAPSHOT_DIR,\n SnapshotClobberError,\n snapshotDir,\n writeSnapshot,\n type ServiceKey,\n} from \"../../core/staging/snapshot.js\";\nimport { enumeratedServices } from \"../../core/repo/service-target.js\";\nimport { listFleetTree } from \"../../core/repo/repo.js\";\nimport { subsystemViewsPath } from \"../../core/repo/paths.js\";\nimport { viewsState } from \"../../core/repo/tree/render/stale.js\";\nimport { loadArchitecture } from \"../../core/c4/project/architecture.js\";\nimport { FleetContext } from \"../../core/fleet-context.js\";\nimport { gate } from \"./plan/gate.js\";\nimport { type ArchiveOptions } from \"./plan/refusal.js\";\nimport { planCapabilities, planGlossary, planSpecs } from \"./plan/specs.js\";\nimport { planOpenapiContracts } from \"./plan/contracts/openapi.js\";\nimport { planAsyncapiContracts } from \"./plan/contracts/asyncapi.js\";\nimport { planDeployments, planFlows, planLandscape } from \"./plan/landscape.js\";\nimport { emptyPlan } from \"./plan/state.js\";\nimport { issueJson, refuseJson } from \"./plan/refusal.js\";\nimport { ArchiveFailure } from \"./plan/refusal.js\";\nimport { printPlan } from \"./plan/refusal.js\";\nimport type { DocsDir } from \"../../core/kernel/ids/dirs.js\";\n\nexport async function archiveLocked(\n config: { docsDir: DocsDir },\n featureId: string,\n opts: ArchiveOptions,\n): Promise<void> {\n const dryRun = opts.dryRun === true;\n const json = opts.json === true;\n // All prose goes through here so `--json` keeps stdout a single JSON document.\n const say = (line = \"\"): void => {\n if (!json) console.log(line);\n };\n\n // One read index for the whole run, threaded through every planner's\n // config record: gate, the spec merge and both contract merges each hold\n // per-service loops that resolve living paths through the enumeration, and\n // without a shared context every one of those calls re-walks the fleet —\n // O(services²) work under a lock that guarantees the tree is not moving.\n // The snapshot resolver below reads through the same context, so it pays\n // for that enumeration too.\n const read = { docsDir: config.docsDir, fleet: new FleetContext() };\n const gated = await gate(read, featureId, opts, say);\n if (gated === null) return;\n const { id, dirName, featureDir, gating, advisory, archiveDir, archiveDest, recovered, issues } = gated;\n\n say(`archive ${id}${dryRun ? \" (dry run)\" : \"\"}\\n`);\n\n // PLAN — compute every merge in memory. Nothing is written until the whole plan\n // succeeds, so a failure on any axis leaves the living docs untouched.\n const planned = emptyPlan();\n await planSpecs(read, gated, planned, say);\n // The business corpus, beside the service one and before the contracts: both\n // are requirement merges, and reading them together is how a plan stays\n // legible as \"what this feature promises\" followed by \"how it is built\".\n await planCapabilities(read, gated, planned, say);\n // The vocabulary the change brings with it, beside the promises it makes:\n // a create-only whole-file copy, refused at the gate if the word already\n // lives (`core/glossary/delta.ts`).\n await planGlossary(read, gated, planned, say);\n await planOpenapiContracts(read, gated, planned, say);\n await planAsyncapiContracts(read, gated, planned, say);\n // `read`, not `config`: the C4 merge now asks which services own their own\n // interior, and that answer comes from the fleet enumeration this run has\n // already paid for.\n await planLandscape(read, gated, planned, say);\n // The flows this feature brings, after the elements they draw hops over: the\n // copy is create-only and refused at the gate if the living tree already holds\n // one (`core/usecases/delta/flows.ts`), and it is ordered after the landscape\n // merge so a reader of the plan sees the map change before the sequence drawn\n // on it.\n await planFlows(config, gated, planned, say);\n await planDeployments(config, gated, planned, say);\n const { writes, planWarns, planGates, openapiRemovals, asyncapiRemovals } = planned;\n\n // Gate on what only the plan could see: a merged operation pointing at a\n // component that exists nowhere, a removal marker addressing no operation.\n // Same doctrine as the coherence gate — a judgment call --approve overrides\n // (unlike the mechanical merge-failed refusals), and a dry run is gated too.\n // Checked after the whole plan so the refusal costs nothing: no write has\n // happened yet either way.\n if (planGates.length > 0 && !opts.approve) {\n const msg = `archive ${id} — BLOCKED: ${planGates.length} issue(s) in the contract merge`;\n if (json) {\n refuseJson(\n \"not-coherent\",\n msg,\n [...issues, ...planWarns, ...planGates],\n `features/${dirName}`,\n );\n return;\n }\n console.error(`${msg}:`);\n for (const i of planGates) console.error(` ✗ ${i.message}`);\n console.error(`\\nFix them in the feature's openapi.yaml / asyncapi.yaml — or re-run with --approve to merge anyway.`);\n sayExplain(\"not-coherent\");\n process.exitCode = 1;\n return;\n }\n if (planGates.length > 0) {\n say(`\\n ⚠ archiving despite ${planGates.length} contract merge issue(s) (--approve):`);\n for (const i of planGates) say(` ✗ ${i.message}`);\n }\n\n // The plan as data, verb decided now — after the commit everything would read\n // as an update. The `--json` payload is identical for a dry run and the real\n // thing except for `archived`; what WOULD happen is what DOES happen.\n const warnings = [...advisory, ...planWarns];\n const overridden = opts.approve === true ? [...gating, ...planGates] : [];\n const plan: Array<Record<string, unknown>> = writes.map((w) => ({\n path: repoPath(config.docsDir, w.path),\n action: existsSync(w.path) ? \"update\" : \"create\",\n }));\n plan.push({ path: `features/${dirName}`, action: \"move\", to: `features/archive/${dirName}` });\n const payload = (archived: boolean): Record<string, unknown> => ({\n command: \"archive\",\n feature: id,\n archived,\n path: repoPath(config.docsDir, archiveDest),\n plan,\n warnings: warnings.map((issue) => issueJson(issue, repoPath(config.docsDir, archiveDest))),\n overridden: overridden.map((issue) => issueJson(issue, repoPath(config.docsDir, archiveDest))),\n openapiRemovals,\n asyncapiRemovals,\n // Present only when this run found an interrupted commit and dealt with it:\n // an agent that sees the docs change under it deserves to be told why, and\n // the absent field is the ordinary case.\n ...(recovered === null ? {} : { recovered }),\n });\n\n if (dryRun) {\n if (json) emitJson(payload(false));\n else printPlan(config.docsDir, writes, dirName);\n return;\n }\n\n // COMMIT — the whole plan computed cleanly. Stage every new version beside its\n // target, snapshot what is about to be overwritten, then swap them into place.\n // Staging touches the filesystem — a read-only `architecture/`, a full disk,\n // a target whose pre-image cannot be read. None of that is a bug in loam, and\n // reporting `internal` sends the reader looking for one; it is the same\n // answer as any other merge that could not be computed: nothing was written.\n let staged;\n try {\n staged = await stageWrites(writes);\n } catch (err) {\n throw new ArchiveFailure(\"merge-failed\", `${message(err)} — nothing was written`);\n }\n let snapshot = false;\n let createdArchiveDir: string | undefined;\n try {\n // The snapshot's resolver pair: how a docs-relative path decomposes into a\n // service identity, and where that identity lives NOW. Built here — the\n // command layer's view of the tree — and injected, so the snapshot module\n // never learns the repo layout. The seam the flat wave left is now\n // threaded: both answers come from the enumeration, so a service filed\n // into a subsystem keys its snapshot rows by the directory it actually\n // lives in, and the clobber guard grades a leftover claim at that same\n // address. The root-level parse stays as the fallback for the one path\n // shape the enumeration cannot know yet — a service this archive CREATES,\n // which always materialises unfiled at services/<id>/.\n // Tolerant on purpose: a docs repo whose services/ is absent still\n // archives a landscape-only feature exactly as before — the enumeration\n // is empty, every row keys by the flat fallback, and the missing\n // directory stays doctor's diagnosis rather than a merge-failed here.\n const dirOf = new Map(\n (await enumeratedServices(config.docsDir, read.fleet)).map((s) => [s.id as string, repoPath(config.docsDir, s.dir)]),\n );\n const serviceKeyOf = (rel: string): ServiceKey | null => {\n for (const [service, dir] of dirOf) {\n if (rel.startsWith(`${dir}/`)) return { service, artifact: rel.slice(dir.length + 1) };\n }\n const m = /^services\\/([^/]+)\\/(.+)$/.exec(rel);\n return m ? { service: m[1]!, artifact: m[2]! } : null;\n };\n const serviceDirOf = (service: string): string | null => dirOf.get(service) ?? null;\n await writeSnapshot({\n featureDir,\n docsDir: config.docsDir,\n feature: { featureId: id, dirName },\n staged,\n serviceKeyOf,\n serviceDirOf,\n });\n snapshot = true;\n // The journal, fsynced, BEFORE the first rename: swapStaged is N renames and\n // only each one of them is atomic, so a kill between two used to leave a\n // half-merged repo that nothing could name — `doctor` and `status` called it\n // healthy and `validate --all` blamed the delta. Written from `staged`, so\n // it records the same digests the swaps are about to produce.\n await writeCommitIntent(\n config.docsDir,\n { command: \"archive\", restore: \"before\", feature: id, moveFrom: featureDir, moveTo: archiveDest },\n staged,\n );\n await swapStaged(staged);\n createdArchiveDir = await mkdir(archiveDir, { recursive: true });\n await rename(featureDir, archiveDest);\n // Last: while it exists, the commit is in flight.\n await clearCommitIntent(config.docsDir);\n } catch (err) {\n if (err instanceof SnapshotClobberError) {\n // Nothing swapped — the refusal happens before the journal is even\n // written — so this only takes the temp files away. Its own code, because\n // the answer is not \"re-run\": a previous archive of this feature is still\n // sitting in the living docs.\n await rollbackStaged(staged);\n throw new ArchiveFailure(\"commit-interrupted\", err.message);\n }\n // Everything this run made, unmade: the swapped files, the snapshot inside the\n // feature that is staying put, and features/archive/ if we are the ones who\n // created it (mkdir reports nothing when it was already there).\n const failures = await rollbackStaged(staged);\n // The rollback decided the outcome, so the journal has nothing left to\n // describe — including on rollback-incomplete, where the files it names are\n // the ones the message tells a human to look at.\n await clearCommitIntent(config.docsDir);\n // The snapshot goes only when the rollback HELD. On rollback-incomplete it\n // holds the only on-disk pre-images of the very files the message tells the\n // reader to repair by hand — a MODIFIED requirement's previous text appears\n // nowhere else. Retaining it is safe: the next archive of this feature reads\n // it before it would replace it, and refuses if the living docs have moved.\n if (snapshot && failures.length === 0) await quietRm(snapshotDir(featureDir));\n // `features/archive/` is shared the moment it exists: another archive can\n // have moved a whole feature into it between our mkdir and this rollback,\n // and a recursive remove of \"the directory we created\" took that feature —\n // snapshot and all — with it, on OUR failure path, in silence. Empty\n // directories only, stopping at the first that is not.\n if (createdArchiveDir !== undefined) await quietPruneEmptyParents(archiveDir, createdArchiveDir);\n // The code is a caller's answer to \"can I trust the repo?\": merge-failed\n // means yes (rolled back), rollback-incomplete means look at it by hand.\n const wrapped = rollbackError(err, failures);\n const kept =\n snapshot && failures.length > 0\n ? ` Pre-images of the overwritten files are kept in features/${dirName}/${SNAPSHOT_DIR}/files/.`\n : \"\";\n throw new ArchiveFailure(failures.length > 0 ? \"rollback-incomplete\" : \"merge-failed\", wrapped.message + kept);\n }\n\n // This merge just changed the map, and the generated views file is a\n // function of the tree AND the map: a delta whose element lands inside a\n // group that exactly covered a subsystem takes that subsystem's boundary\n // line away, and a delta binding an element to a filed service adds an\n // include. Archive does not rewrite the file — `loam subsystem sync` owns it\n // outright, and folding a second generated artifact into this transaction\n // would mean archive computing the post-merge tree AND the post-merge map,\n // which is how the two writers disagree — but it must not report a repo as\n // current when the next `validate --all` will call it stale. Graded through\n // validate's own function, so the two cannot answer differently.\n const staleViews = await subsystemViewsStale(config.docsDir);\n\n if (json) {\n emitJson({ ...payload(true), ...(staleViews ? { subsystemViewsStale: true } : {}) });\n return;\n }\n console.log(`\\n archived: features/${dirName} → features/archive/${dirName}`);\n console.log(` snapshot: features/archive/${dirName}/${SNAPSHOT_DIR}/ — \\`loam unarchive ${id}\\` puts it back`);\n // The closing line is a claim about the whole docs repo, and it is the line a\n // reader stops at. It may be printed only when this archive left nothing the\n // next `validate --all` will fail on — a service with no model, or a gate the\n // caller told it to merge past. Printing it over either is how a red fleet\n // gets reported as a finished one.\n const incomplete = planWarns.filter((w) => w.code === \"service.no-model\");\n if (incomplete.length === 0 && overridden.length === 0 && !staleViews) {\n console.log(\" living spec + landscape are now complete + current.\");\n return;\n }\n if (staleViews) {\n console.log(\n \" ⚠ architecture/subsystems.likec4 no longer matches the tree and this map — run `loam subsystem sync`.\",\n );\n }\n for (const w of incomplete) console.log(` ⚠ ${w.message}`);\n if (overridden.length > 0) {\n console.log(\n ` ⚠ merged past ${overridden.length} gating issue(s) with --approve — the living docs carry them now; \\`loam validate --all\\` says what they cost.`,\n );\n }\n}\n\n/**\n * Would the next `validate --all` report `subsystem.views-stale`?\n *\n * Asked through validate's OWN function rather than a second compare, so the\n * warning archive prints and the error validate raises can never disagree —\n * including about the gate validate holds: a map that does not parse is\n * `landscape.invalid`'s business, and grading a generated file against a map\n * nobody can read would cascade a second complaint behind the first. An\n * unreadable map therefore answers \"no claim\", exactly as validate does.\n */\nasync function subsystemViewsStale(docsDir: DocsDir): Promise<boolean> {\n try {\n const doc = await loadArchitecture(docsDir);\n if (doc.errors.length > 0) return false;\n const state = await viewsState(subsystemViewsPath(docsDir), await listFleetTree(docsDir), doc);\n return !state.agrees;\n } catch {\n // Tolerant for the reason the snapshot resolver above is: a docs repo with\n // no `services/` at all still archives a landscape-only feature, and this\n // question is an ADVISORY printed after a commit that already succeeded.\n // Nothing here may turn a landed archive into a non-zero exit.\n return false;\n }\n}\n"]}
1
+ {"version":3,"file":"run.js","sourceRoot":"","sources":["../../../src/commands/archive/run.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * The archive itself, under the docs lock: gate, plan, commit.\n *\n * The order is the command's whole safety property. Every refusal happens before\n * a byte is planned (`./plan/gate.ts`), the plan is computed entirely in memory\n * so a failure on any axis leaves the living docs untouched (`./plan/specs.ts`,\n * `./plan/contracts/`, `./plan/landscape.ts`), and only a plan that succeeded\n * on every axis is staged\n * and swapped. `--dry-run` returns between the second and the third.\n */\nimport { existsSync } from \"node:fs\";\nimport { mkdir, rename } from \"node:fs/promises\";\nimport { emitJson, repoPath, sayExplain } from \"../../core/envelope/json.js\";\nimport {\n message,\n quietPruneEmptyParents,\n quietRm,\n rollbackError,\n rollbackStaged,\n stageWrites,\n swapStaged,\n} from \"../../core/staging/commit.js\";\nimport { clearCommitIntent, writeCommitIntent } from \"../../core/staging/recovery/intent.js\";\nimport {\n SNAPSHOT_DIR,\n SnapshotClobberError,\n snapshotDir,\n writeSnapshot,\n type ServiceKey,\n} from \"../../core/staging/snapshot.js\";\nimport { enumeratedServices } from \"../../core/repo/service-target.js\";\nimport { listFleetTree } from \"../../core/repo/repo.js\";\nimport { serviceViewsPath, subsystemViewsPath } from \"../../core/repo/paths.js\";\nimport { serviceViewsState, viewsState } from \"../../core/repo/tree/render/stale.js\";\nimport { loadArchitecture } from \"../../core/c4/project/architecture.js\";\nimport { FleetContext } from \"../../core/fleet-context.js\";\nimport { gate } from \"./plan/gate.js\";\nimport { type ArchiveOptions } from \"./plan/refusal.js\";\nimport { planCapabilities, planGlossary, planSpecs } from \"./plan/specs.js\";\nimport { planOpenapiContracts } from \"./plan/contracts/openapi.js\";\nimport { planAsyncapiContracts } from \"./plan/contracts/asyncapi.js\";\nimport { planDeployments, planFlows, planLandscape } from \"./plan/landscape.js\";\nimport { emptyPlan } from \"./plan/state.js\";\nimport { issueJson, refuseJson } from \"./plan/refusal.js\";\nimport { ArchiveFailure } from \"./plan/refusal.js\";\nimport { printPlan } from \"./plan/refusal.js\";\nimport type { DocsDir } from \"../../core/kernel/ids/dirs.js\";\n\nexport async function archiveLocked(\n config: { docsDir: DocsDir },\n featureId: string,\n opts: ArchiveOptions,\n): Promise<void> {\n const dryRun = opts.dryRun === true;\n const json = opts.json === true;\n // All prose goes through here so `--json` keeps stdout a single JSON document.\n const say = (line = \"\"): void => {\n if (!json) console.log(line);\n };\n\n // One read index for the whole run, threaded through every planner's\n // config record: gate, the spec merge and both contract merges each hold\n // per-service loops that resolve living paths through the enumeration, and\n // without a shared context every one of those calls re-walks the fleet —\n // O(services²) work under a lock that guarantees the tree is not moving.\n // The snapshot resolver below reads through the same context, so it pays\n // for that enumeration too.\n const read = { docsDir: config.docsDir, fleet: new FleetContext() };\n const gated = await gate(read, featureId, opts, say);\n if (gated === null) return;\n const { id, dirName, featureDir, gating, advisory, archiveDir, archiveDest, recovered, issues } = gated;\n\n say(`archive ${id}${dryRun ? \" (dry run)\" : \"\"}\\n`);\n\n // PLAN — compute every merge in memory. Nothing is written until the whole plan\n // succeeds, so a failure on any axis leaves the living docs untouched.\n const planned = emptyPlan();\n await planSpecs(read, gated, planned, say);\n // The business corpus, beside the service one and before the contracts: both\n // are requirement merges, and reading them together is how a plan stays\n // legible as \"what this feature promises\" followed by \"how it is built\".\n await planCapabilities(read, gated, planned, say);\n // The vocabulary the change brings with it, beside the promises it makes:\n // a create-only whole-file copy, refused at the gate if the word already\n // lives (`core/glossary/delta.ts`).\n await planGlossary(read, gated, planned, say);\n await planOpenapiContracts(read, gated, planned, say);\n await planAsyncapiContracts(read, gated, planned, say);\n // `read`, not `config`: the C4 merge now asks which services own their own\n // interior, and that answer comes from the fleet enumeration this run has\n // already paid for.\n await planLandscape(read, gated, planned, say);\n // The flows this feature brings, after the elements they draw hops over: the\n // copy is create-only and refused at the gate if the living tree already holds\n // one (`core/usecases/delta/flows.ts`), and it is ordered after the landscape\n // merge so a reader of the plan sees the map change before the sequence drawn\n // on it.\n await planFlows(config, gated, planned, say);\n await planDeployments(config, gated, planned, say);\n const { writes, planWarns, planGates, openapiRemovals, asyncapiRemovals } = planned;\n\n // Gate on what only the plan could see: a merged operation pointing at a\n // component that exists nowhere, a removal marker addressing no operation.\n // Same doctrine as the coherence gate — a judgment call --approve overrides\n // (unlike the mechanical merge-failed refusals), and a dry run is gated too.\n // Checked after the whole plan so the refusal costs nothing: no write has\n // happened yet either way.\n if (planGates.length > 0 && !opts.approve) {\n const msg = `archive ${id} — BLOCKED: ${planGates.length} issue(s) in the contract merge`;\n if (json) {\n refuseJson(\n \"not-coherent\",\n msg,\n [...issues, ...planWarns, ...planGates],\n `features/${dirName}`,\n );\n return;\n }\n console.error(`${msg}:`);\n for (const i of planGates) console.error(` ✗ ${i.message}`);\n console.error(`\\nFix them in the feature's openapi.yaml / asyncapi.yaml — or re-run with --approve to merge anyway.`);\n sayExplain(\"not-coherent\");\n process.exitCode = 1;\n return;\n }\n if (planGates.length > 0) {\n say(`\\n ⚠ archiving despite ${planGates.length} contract merge issue(s) (--approve):`);\n for (const i of planGates) say(` ✗ ${i.message}`);\n }\n\n // The plan as data, verb decided now — after the commit everything would read\n // as an update. The `--json` payload is identical for a dry run and the real\n // thing except for `archived`; what WOULD happen is what DOES happen.\n const warnings = [...advisory, ...planWarns];\n const overridden = opts.approve === true ? [...gating, ...planGates] : [];\n const plan: Array<Record<string, unknown>> = writes.map((w) => ({\n path: repoPath(config.docsDir, w.path),\n action: existsSync(w.path) ? \"update\" : \"create\",\n }));\n plan.push({ path: `features/${dirName}`, action: \"move\", to: `features/archive/${dirName}` });\n const payload = (archived: boolean): Record<string, unknown> => ({\n command: \"archive\",\n feature: id,\n archived,\n path: repoPath(config.docsDir, archiveDest),\n plan,\n warnings: warnings.map((issue) => issueJson(issue, repoPath(config.docsDir, archiveDest))),\n overridden: overridden.map((issue) => issueJson(issue, repoPath(config.docsDir, archiveDest))),\n openapiRemovals,\n asyncapiRemovals,\n // Present only when this run found an interrupted commit and dealt with it:\n // an agent that sees the docs change under it deserves to be told why, and\n // the absent field is the ordinary case.\n ...(recovered === null ? {} : { recovered }),\n });\n\n if (dryRun) {\n if (json) emitJson(payload(false));\n else printPlan(config.docsDir, writes, dirName);\n return;\n }\n\n // COMMIT — the whole plan computed cleanly. Stage every new version beside its\n // target, snapshot what is about to be overwritten, then swap them into place.\n // Staging touches the filesystem — a read-only `architecture/`, a full disk,\n // a target whose pre-image cannot be read. None of that is a bug in loam, and\n // reporting `internal` sends the reader looking for one; it is the same\n // answer as any other merge that could not be computed: nothing was written.\n let staged;\n try {\n staged = await stageWrites(writes);\n } catch (err) {\n throw new ArchiveFailure(\"merge-failed\", `${message(err)} — nothing was written`);\n }\n let snapshot = false;\n let createdArchiveDir: string | undefined;\n try {\n // The snapshot's resolver pair: how a docs-relative path decomposes into a\n // service identity, and where that identity lives NOW. Built here — the\n // command layer's view of the tree — and injected, so the snapshot module\n // never learns the repo layout. The seam the flat wave left is now\n // threaded: both answers come from the enumeration, so a service filed\n // into a subsystem keys its snapshot rows by the directory it actually\n // lives in, and the clobber guard grades a leftover claim at that same\n // address. The root-level parse stays as the fallback for the one path\n // shape the enumeration cannot know yet — a service this archive CREATES,\n // which always materialises unfiled at services/<id>/.\n // Tolerant on purpose: a docs repo whose services/ is absent still\n // archives a landscape-only feature exactly as before — the enumeration\n // is empty, every row keys by the flat fallback, and the missing\n // directory stays doctor's diagnosis rather than a merge-failed here.\n const dirOf = new Map(\n (await enumeratedServices(config.docsDir, read.fleet)).map((s) => [s.id as string, repoPath(config.docsDir, s.dir)]),\n );\n const serviceKeyOf = (rel: string): ServiceKey | null => {\n for (const [service, dir] of dirOf) {\n if (rel.startsWith(`${dir}/`)) return { service, artifact: rel.slice(dir.length + 1) };\n }\n const m = /^services\\/([^/]+)\\/(.+)$/.exec(rel);\n return m ? { service: m[1]!, artifact: m[2]! } : null;\n };\n const serviceDirOf = (service: string): string | null => dirOf.get(service) ?? null;\n await writeSnapshot({\n featureDir,\n docsDir: config.docsDir,\n feature: { featureId: id, dirName },\n staged,\n serviceKeyOf,\n serviceDirOf,\n });\n snapshot = true;\n // The journal, fsynced, BEFORE the first rename: swapStaged is N renames and\n // only each one of them is atomic, so a kill between two used to leave a\n // half-merged repo that nothing could name — `doctor` and `status` called it\n // healthy and `validate --all` blamed the delta. Written from `staged`, so\n // it records the same digests the swaps are about to produce.\n await writeCommitIntent(\n config.docsDir,\n { command: \"archive\", restore: \"before\", feature: id, moveFrom: featureDir, moveTo: archiveDest },\n staged,\n );\n await swapStaged(staged);\n createdArchiveDir = await mkdir(archiveDir, { recursive: true });\n await rename(featureDir, archiveDest);\n // Last: while it exists, the commit is in flight.\n await clearCommitIntent(config.docsDir);\n } catch (err) {\n if (err instanceof SnapshotClobberError) {\n // Nothing swapped — the refusal happens before the journal is even\n // written — so this only takes the temp files away. Its own code, because\n // the answer is not \"re-run\": a previous archive of this feature is still\n // sitting in the living docs.\n await rollbackStaged(staged);\n throw new ArchiveFailure(\"commit-interrupted\", err.message);\n }\n // Everything this run made, unmade: the swapped files, the snapshot inside the\n // feature that is staying put, and features/archive/ if we are the ones who\n // created it (mkdir reports nothing when it was already there).\n const failures = await rollbackStaged(staged);\n // The rollback decided the outcome, so the journal has nothing left to\n // describe — including on rollback-incomplete, where the files it names are\n // the ones the message tells a human to look at.\n await clearCommitIntent(config.docsDir);\n // The snapshot goes only when the rollback HELD. On rollback-incomplete it\n // holds the only on-disk pre-images of the very files the message tells the\n // reader to repair by hand — a MODIFIED requirement's previous text appears\n // nowhere else. Retaining it is safe: the next archive of this feature reads\n // it before it would replace it, and refuses if the living docs have moved.\n if (snapshot && failures.length === 0) await quietRm(snapshotDir(featureDir));\n // `features/archive/` is shared the moment it exists: another archive can\n // have moved a whole feature into it between our mkdir and this rollback,\n // and a recursive remove of \"the directory we created\" took that feature —\n // snapshot and all — with it, on OUR failure path, in silence. Empty\n // directories only, stopping at the first that is not.\n if (createdArchiveDir !== undefined) await quietPruneEmptyParents(archiveDir, createdArchiveDir);\n // The code is a caller's answer to \"can I trust the repo?\": merge-failed\n // means yes (rolled back), rollback-incomplete means look at it by hand.\n const wrapped = rollbackError(err, failures);\n const kept =\n snapshot && failures.length > 0\n ? ` Pre-images of the overwritten files are kept in features/${dirName}/${SNAPSHOT_DIR}/files/.`\n : \"\";\n throw new ArchiveFailure(failures.length > 0 ? \"rollback-incomplete\" : \"merge-failed\", wrapped.message + kept);\n }\n\n // This merge just changed the map, and both generated views files are a\n // function of the tree AND the map: a delta whose element lands inside a\n // group that exactly covered a subsystem takes that subsystem's boundary\n // line away, a delta binding an element to a filed service adds an include,\n // and one new edge changes exactly one service's overview. Archive does not\n // rewrite either file — `loam subsystem sync` owns them outright, and folding\n // a second generated artifact into this transaction would mean archive\n // computing the post-merge tree AND the post-merge map, which is how the two\n // writers disagree — but it must not report a repo as current when the next\n // `validate --all` will call it stale. Graded through validate's own\n // functions, so the two cannot answer differently.\n const staleViews = await staleGeneratedViews(config.docsDir);\n\n if (json) {\n emitJson({\n ...payload(true),\n // The original boolean keeps its exact meaning — `architecture/subsystems.likec4`\n // and nothing else — because a payload key's meaning is frozen; the list\n // beside it is the additive answer that can name the second file. Both are\n // omitted when nothing is stale, exactly as the boolean always was.\n ...(staleViews.length === 0 ? {} : { staleViews }),\n ...(staleViews.includes(\"architecture/subsystems.likec4\") ? { subsystemViewsStale: true } : {}),\n });\n return;\n }\n console.log(`\\n archived: features/${dirName} → features/archive/${dirName}`);\n console.log(` snapshot: features/archive/${dirName}/${SNAPSHOT_DIR}/ — \\`loam unarchive ${id}\\` puts it back`);\n // The closing line is a claim about the whole docs repo, and it is the line a\n // reader stops at. It may be printed only when this archive left nothing the\n // next `validate --all` will fail on — a service with no model, or a gate the\n // caller told it to merge past. Printing it over either is how a red fleet\n // gets reported as a finished one.\n const incomplete = planWarns.filter((w) => w.code === \"service.no-model\");\n if (incomplete.length === 0 && overridden.length === 0 && staleViews.length === 0) {\n console.log(\" living spec + landscape are now complete + current.\");\n return;\n }\n // One line per stale file rather than one line listing them: each names a file\n // a reader opens, and a comma-joined series would have to choose a verb that\n // reads wrong for one of the two counts.\n for (const path of staleViews) {\n console.log(` ⚠ ${path} no longer matches the tree and this map — run \\`loam subsystem sync\\`.`);\n }\n for (const w of incomplete) console.log(` ⚠ ${w.message}`);\n if (overridden.length > 0) {\n console.log(\n ` ⚠ merged past ${overridden.length} gating issue(s) with --approve — the living docs carry them now; \\`loam validate --all\\` says what they cost.`,\n );\n }\n}\n\n/**\n * Which generated files would the next `validate --all` report\n * `subsystem.views-stale` on? Docs-relative paths, sorted, empty for none.\n *\n * Asked through validate's OWN functions rather than a second compare, so the\n * warning archive prints and the error validate raises can never disagree —\n * including about the gate validate holds: a map that does not parse is\n * `landscape.invalid`'s business, and grading a generated file against a map\n * nobody can read would cascade a second complaint behind the first. An\n * unreadable map therefore answers \"no claim\", exactly as validate does.\n *\n * BOTH generated files, because this merge just changed the map and the\n * per-service overviews are a function of it in a way the subsystem views are\n * not: a delta adding one edge changes exactly one service's peer set, and a\n * delta binding a new element adds a whole view. Naming only the file loam\n * happened to check first would let a repo be reported current while the very\n * next `validate --all` calls it stale — the one thing this advisory exists to\n * prevent.\n */\nasync function staleGeneratedViews(docsDir: DocsDir): Promise<string[]> {\n try {\n const doc = await loadArchitecture(docsDir);\n if (doc.errors.length > 0) return [];\n const tree = await listFleetTree(docsDir);\n const stale: string[] = [];\n if (!(await viewsState(subsystemViewsPath(docsDir), tree, doc)).agrees) stale.push(\"architecture/subsystems.likec4\");\n if (!(await serviceViewsState(serviceViewsPath(docsDir), tree, doc)).agrees) {\n stale.push(\"architecture/services.likec4\");\n }\n return stale.sort();\n } catch {\n // Tolerant for the reason the snapshot resolver above is: a docs repo with\n // no `services/` at all still archives a landscape-only feature, and this\n // question is an ADVISORY printed after a commit that already succeeded.\n // Nothing here may turn a landed archive into a non-zero exit.\n return [];\n }\n}\n"]}
@@ -5,7 +5,7 @@
5
5
  * the docs lock, nothing outside it does, and the module prints nothing —
6
6
  * the caller renders every arm.
7
7
  *
8
- * The checks live UNDER the lock on purpose: the never-overwrite promise is a
8
+ * The three refusals live UNDER the lock on purpose: the never-overwrite promise is a
9
9
  * statement about the landscape as it is at the moment of the write, and an
10
10
  * unlocked classification would be a TOCTOU against every other docs writer.
11
11
  */
@@ -16,6 +16,7 @@ import { isLandscapeStub } from "../../core/scaffold/landscape.js";
16
16
  import { parseServiceId } from "../../core/kernel/ids/service.js";
17
17
  import { landscapePath } from "../../core/repo/paths.js";
18
18
  import { listFleetTree } from "../../core/repo/repo.js";
19
+ import { authoredRefusal } from "../../core/repo/tree/render/service/header.js";
19
20
  import { DocsRepoUnavailableError } from "../../core/repo/state.js";
20
21
  import { stageWrites } from "../../core/staging/commit.js";
21
22
  import { InterruptedCommitError } from "../../core/staging/interrupted.js";
@@ -63,6 +64,38 @@ export async function commitSeed(req) {
63
64
  if (inconsistency !== null)
64
65
  return inconsistency;
65
66
  const plan = await planSeedWrites(req, tree);
67
+ // THE OTHER never-overwrite promise, and the third refusal this window owns.
68
+ //
69
+ // `classifyLandscape` above keeps it for `architecture/landscape.likec4`;
70
+ // this keeps it for the two files seed does not share with the team at all —
71
+ // `architecture/subsystems.likec4` and `architecture/services.likec4` are
72
+ // filenames loam RESERVES inside a directory the team owns, and seed writes
73
+ // them whole. Until this line existed a `loam seed` into a repository that
74
+ // kept a hand-authored diagram at one of those paths destroyed it in
75
+ // silence: the same clobber reproduced through `loam subsystem new`, and the
76
+ // guard that caught it lived in `subsystem sync` alone.
77
+ //
78
+ // THE WHOLE SEED IS REFUSED, exactly as an edited landscape refuses it, and
79
+ // for the same reason: seed regenerates every file it owns wholesale from
80
+ // fleet.yaml, so landing the landscape and the markers while declining a
81
+ // views file would leave a tree whose very next `validate --all` — seed's
82
+ // own `next` command — reports `subsystem.views-stale` over a file no
83
+ // command could ever repair. `planSeedWrites` has staged nothing; the
84
+ // transaction below is the first byte, and it does not run.
85
+ if (plan.authored.length > 0) {
86
+ return {
87
+ ok: false,
88
+ // `already-exists` for the reason commands/subsystem/txn/txn.ts gives at
89
+ // its own copy of this refusal: it is what that code has always meant —
90
+ // refusing to create over something that already exists, because
91
+ // overwriting would destroy work nobody asked to lose.
92
+ code: "already-exists",
93
+ message: plan.authored.map((file) => authoredRefusal(file)).join(" ") +
94
+ " NOTHING was written: not the landscape, not a subsystem marker, not a `.gitkeep`. Seed regenerates " +
95
+ "every file it owns wholesale, so it never proceeds past human work standing at one of its paths — the " +
96
+ "same promise `seed-landscape-edited` makes one file over.",
97
+ };
98
+ }
66
99
  const staged = await stageWrites(plan.writes);
67
100
  const committed = await commitStaged({ root: docsDir, command: "seed", rerun: req.rerun, target: "landscape" }, staged, "seeded");
68
101
  if (!committed.ok) {
@@ -1 +1 @@
1
- {"version":3,"file":"commit.js","sourceRoot":"","sources":["../../../src/commands/seed/commit.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * Seed's commit window: lock, recover, the under-lock provenance and\n * fleet-consistency checks, one journaled transaction. Modeled line for line\n * on commands/new/commit.ts and on the same seam: everything in here holds\n * the docs lock, nothing outside it does, and the module prints nothing —\n * the caller renders every arm.\n *\n * The checks live UNDER the lock on purpose: the never-overwrite promise is a\n * statement about the landscape as it is at the moment of the write, and an\n * unlocked classification would be a TOCTOU against every other docs writer.\n */\nimport { existsSync } from \"node:fs\";\nimport { readFile } from \"node:fs/promises\";\nimport type { FleetSeed } from \"../../core/c4/seed/fleet-file.js\";\nimport { landscapeProvenance } from \"../../core/c4/seed/stamp.js\";\nimport { isLandscapeStub } from \"../../core/scaffold/landscape.js\";\nimport type { DocsDir } from \"../../core/kernel/ids/dirs.js\";\nimport { parseServiceId } from \"../../core/kernel/ids/service.js\";\nimport { landscapePath } from \"../../core/repo/paths.js\";\nimport { listFleetTree } from \"../../core/repo/repo.js\";\nimport { DocsRepoUnavailableError } from \"../../core/repo/state.js\";\nimport type { FleetTree } from \"../../core/repo/tree/walk.js\";\nimport { stageWrites } from \"../../core/staging/commit.js\";\nimport { type CommitRecovery, InterruptedCommitError } from \"../../core/staging/interrupted.js\";\nimport { acquireDocsLockWaiting, DocsBusyError, LOCK_WAIT_MS } from \"../../core/staging/lock.js\";\nimport { recoverInterruptedCommit } from \"../../core/staging/recovery/recover.js\";\nimport { commitStaged } from \"../../core/staging/txn/transaction.js\";\nimport { planSeedWrites, type SeedPlanInput } from \"./plan.js\";\n\n/** The plan's input, plus the one fact only the transaction needs. */\nexport interface SeedCommitRequest extends SeedPlanInput {\n /** How a human re-runs this command — the transaction journal's `rerun`. */\n rerun: string;\n}\n\n/** What happened to architecture/landscape.likec4 — the three writes seed may prove safe. */\nexport type LandscapeDisposition = \"created\" | \"replaced-stub\" | \"regenerated\";\n\nexport type SeedCommit =\n | {\n ok: true;\n landscape: LandscapeDisposition;\n /** Absolute path of every file the transaction wrote, in write order. */\n written: string[];\n /**\n * Absolute path of every file the transaction REMOVED — only ever the\n * generated views file, when the resulting tree has no subsystems left\n * to view. Separate from `written` because \"+ path\" is a lie about a\n * delete.\n */\n removed: string[];\n /** Service ids split by whether this run created the directory. */\n services: { created: string[]; existing: string[] };\n recovered: CommitRecovery | null;\n }\n | {\n ok: false;\n code:\n | \"docs-busy\"\n | \"commit-interrupted\"\n | \"merge-failed\"\n | \"rollback-incomplete\"\n | \"seed-landscape-edited\";\n message: string;\n }\n /** The under-lock fleet-consistency preflight refusing; see `missingServices`. */\n | { ok: false; code: \"seed-file-invalid\"; message: string; missingServices: string[] }\n /** The docs repo went away between the command's gate and the locked read — new/commit.ts says why this is not a `code:` arm. */\n | { ok: false; repoGone: DocsRepoUnavailableError };\n\nexport async function commitSeed(req: SeedCommitRequest): Promise<SeedCommit> {\n const { docsDir } = req;\n let releaseLock: () => Promise<void>;\n try {\n releaseLock = await acquireDocsLockWaiting(docsDir, LOCK_WAIT_MS);\n } catch (err) {\n if (err instanceof DocsBusyError) return { ok: false, code: \"docs-busy\", message: err.message };\n throw err;\n }\n try {\n let recovered: CommitRecovery | null;\n try {\n recovered = await recoverInterruptedCommit(docsDir);\n } catch (err) {\n if (!(err instanceof InterruptedCommitError)) throw err;\n return { ok: false, code: \"commit-interrupted\", message: err.message };\n }\n\n // Provenance first, then the tree: both read the repo as this run will\n // leave it, and both refusals must prove nothing was written.\n const landscape = await classifyLandscape(docsDir);\n if (\"refusal\" in landscape) {\n return { ok: false, code: \"seed-landscape-edited\", message: landscape.refusal };\n }\n\n let tree: FleetTree;\n try {\n tree = await listFleetTree(docsDir);\n } catch (err) {\n if (!(err instanceof DocsRepoUnavailableError)) throw err;\n return { ok: false, repoGone: err };\n }\n const inconsistency = preflight(tree, req.seed);\n if (inconsistency !== null) return inconsistency;\n\n const plan = await planSeedWrites(req, tree);\n const staged = await stageWrites(plan.writes);\n const committed = await commitStaged(\n { root: docsDir, command: \"seed\", rerun: req.rerun, target: \"landscape\" },\n staged,\n \"seeded\",\n );\n if (!committed.ok) {\n // `committed.raced` is deliberately NOT remapped: the lock serialises\n // loam writers, so a fired CAS or a lost exclusive create here means an\n // out-of-band editor, and seed has no softer race answer the way `new`\n // has already-exists — merge-failed's \"nothing was written, re-run\" is\n // the honest sentence either way.\n return { ok: false, code: committed.code, message: committed.message };\n }\n return {\n ok: true,\n landscape: landscape.disposition,\n written: plan.writes.filter((w) => w.content !== null).map((w) => w.path),\n removed: plan.writes.filter((w) => w.content === null).map((w) => w.path),\n services: plan.services,\n recovered,\n };\n } finally {\n await releaseLock();\n }\n}\n\n/**\n * May seed write the landscape? Absent and untouched-scaffold-stub are the\n * two states with provably no human work in them; a matching stamp digest is\n * the third (`core/c4/seed/stamp.ts`). Everything else refuses.\n */\nasync function classifyLandscape(\n docsDir: DocsDir,\n): Promise<{ disposition: LandscapeDisposition } | { refusal: string }> {\n const path = landscapePath(docsDir);\n if (!existsSync(path)) return { disposition: \"created\" };\n const text = (await readFile(path)).toString(\"utf8\");\n // `isLandscapeStub`, never a byte-compare of our own: that function IS the\n // repo's answer to \"is this still the scaffold's untouched map\", it is what\n // `loam status` reads before telling the reader to run seed, and it is\n // suffix-matched and CRLF-normalised for two reasons seed shares —\n // `migrate-openspec` prepends a preamble to the identical stub, and a\n // checkout under `core.autocrlf` (Git for Windows' installer default)\n // rewrites every line without touching a fact. A stricter compare here made\n // status and seed contradict each other about the same file.\n if (isLandscapeStub(text)) return { disposition: \"replaced-stub\" };\n const provenance = landscapeProvenance(text);\n if (provenance === \"seed-stamped\") return { disposition: \"regenerated\" };\n return {\n refusal:\n `architecture/landscape.likec4 ${\n provenance === \"seed-edited\"\n ? \"was seeded but has been hand-edited since — the line-1 stamp's digest no longer matches the content\"\n : \"was not written by `loam seed`: it carries no line-1 stamp and is not the scaffold's untouched stub\"\n }. Seed never overwrites human work; nothing was written. Either fold the edits into ` +\n `fleet.yaml and delete the file, then re-run — or keep the hand-authored map and stop ` +\n `using seed here.`,\n };\n}\n\n/**\n * The fleet-consistency preflight. Seed regenerates the landscape WHOLESALE —\n * that is the only posture the stamp can prove safe — so every existing\n * services/<id>/ must be named in fleet.yaml, or the regenerate would orphan\n * it into `landscape.service-unmodelled`. And the file's names must not\n * collide with what the tree already holds under a different kind: one flat\n * namespace, the same rule the walk enforces.\n */\nfunction preflight(tree: FleetTree, seed: FleetSeed): SeedCommit | null {\n const treeSubs = new Set(tree.subsystems.map((s) => s.name));\n const treeIds = new Set(tree.services.map((s) => s.id as string));\n\n // The collision scan runs BEFORE the missing-service scan, and the order is\n // load-bearing. A name the file declares as a subsystem or an external while\n // the tree holds it as a service DIRECTORY is both facts at once: it is a\n // namespace collision, and it is also an existing service the file does not\n // name. With the missing scan first, the caller was told to paste that name\n // into `services:` — advice that walks straight into `seed-duplicate-service`\n // on the next run, because the file already declares it as something else.\n const collisions: string[] = [];\n for (const sub of seed.subsystems) {\n if (treeIds.has(sub as string)) collisions.push(`subsystem '${sub}' is an existing service directory`);\n }\n for (const s of seed.services) {\n if (treeSubs.has(s.id as string)) collisions.push(`service '${s.id}' is an existing subsystem directory`);\n }\n for (const x of seed.externals) {\n if (treeIds.has(x)) collisions.push(`external '${x}' is an existing service directory`);\n if (treeSubs.has(x)) collisions.push(`external '${x}' is an existing subsystem directory`);\n }\n if (collisions.length > 0) {\n return {\n ok: false,\n code: \"seed-file-invalid\",\n message:\n `fleet.yaml collides with the services/ tree — service ids, subsystem names and externals ` +\n `share one flat namespace: ${collisions.join(\"; \")}. Rename the fleet.yaml side (or the ` +\n `directory, with \\`loam subsystem rename <from> <to>\\`), then re-run. Nothing was written.`,\n missingServices: [],\n };\n }\n\n const ids = new Set(seed.services.map((s) => s.id as string));\n const missing = tree.services.map((s) => s.id as string).filter((id) => !ids.has(id)).sort();\n if (missing.length === 0) return null;\n // The ids are partitioned because one half cannot take the other's advice.\n // `tree.services` carries RAW ids — a directory whose name fails the service\n // grammar is still a service to the walk, by design — and \"add it to\n // services: exactly as spelled\" is impossible for one of those: the fleet\n // file would refuse the entry. Without this split, seed was simply unusable\n // on such a repo and never said so.\n const legal = missing.filter((id) => parseServiceId(id, \"services entry\").ok);\n const illegal = missing.filter((id) => !parseServiceId(id, \"services entry\").ok);\n const fixes: string[] = [];\n if (legal.length > 0) fixes.push(`add ${legal.join(\", \")} to services:, exactly as spelled`);\n if (illegal.length > 0) {\n fixes.push(\n `rename the ${illegal.length === 1 ? \"directory\" : \"directories\"} ${illegal.join(\", \")} ` +\n `first — ${illegal.length === 1 ? \"that name is not a legal service id\" : \"those names are not legal service ids\"}, ` +\n `so no services: entry can name ${illegal.length === 1 ? \"it\" : \"them\"}`,\n );\n }\n return {\n ok: false,\n code: \"seed-file-invalid\",\n message:\n `fleet.yaml must name every existing service — seed regenerates the landscape wholesale, ` +\n `and these services/ directories are not in it: ${missing.join(\", \")}. ` +\n `To fix: ${fixes.join(\"; and \")}. Then re-run. Nothing was written.`,\n missingServices: missing,\n };\n}\n"]}
1
+ {"version":3,"file":"commit.js","sourceRoot":"","sources":["../../../src/commands/seed/commit.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * Seed's commit window: lock, recover, the under-lock provenance and\n * fleet-consistency checks, one journaled transaction. Modeled line for line\n * on commands/new/commit.ts and on the same seam: everything in here holds\n * the docs lock, nothing outside it does, and the module prints nothing —\n * the caller renders every arm.\n *\n * The three refusals live UNDER the lock on purpose: the never-overwrite promise is a\n * statement about the landscape as it is at the moment of the write, and an\n * unlocked classification would be a TOCTOU against every other docs writer.\n */\nimport { existsSync } from \"node:fs\";\nimport { readFile } from \"node:fs/promises\";\nimport type { FleetSeed } from \"../../core/c4/seed/fleet-file.js\";\nimport { landscapeProvenance } from \"../../core/c4/seed/stamp.js\";\nimport { isLandscapeStub } from \"../../core/scaffold/landscape.js\";\nimport type { DocsDir } from \"../../core/kernel/ids/dirs.js\";\nimport { parseServiceId } from \"../../core/kernel/ids/service.js\";\nimport { landscapePath } from \"../../core/repo/paths.js\";\nimport { listFleetTree } from \"../../core/repo/repo.js\";\nimport { authoredRefusal } from \"../../core/repo/tree/render/service/header.js\";\nimport { DocsRepoUnavailableError } from \"../../core/repo/state.js\";\nimport type { FleetTree } from \"../../core/repo/tree/walk.js\";\nimport { stageWrites } from \"../../core/staging/commit.js\";\nimport { type CommitRecovery, InterruptedCommitError } from \"../../core/staging/interrupted.js\";\nimport { acquireDocsLockWaiting, DocsBusyError, LOCK_WAIT_MS } from \"../../core/staging/lock.js\";\nimport { recoverInterruptedCommit } from \"../../core/staging/recovery/recover.js\";\nimport { commitStaged } from \"../../core/staging/txn/transaction.js\";\nimport { planSeedWrites, type SeedPlanInput } from \"./plan.js\";\n\n/** The plan's input, plus the one fact only the transaction needs. */\nexport interface SeedCommitRequest extends SeedPlanInput {\n /** How a human re-runs this command — the transaction journal's `rerun`. */\n rerun: string;\n}\n\n/** What happened to architecture/landscape.likec4 — the three writes seed may prove safe. */\nexport type LandscapeDisposition = \"created\" | \"replaced-stub\" | \"regenerated\";\n\nexport type SeedCommit =\n | {\n ok: true;\n landscape: LandscapeDisposition;\n /** Absolute path of every file the transaction wrote, in write order. */\n written: string[];\n /**\n * Absolute path of every file the transaction REMOVED — only ever the\n * generated views file, when the resulting tree has no subsystems left\n * to view. Separate from `written` because \"+ path\" is a lie about a\n * delete.\n */\n removed: string[];\n /** Service ids split by whether this run created the directory. */\n services: { created: string[]; existing: string[] };\n recovered: CommitRecovery | null;\n }\n | {\n ok: false;\n code:\n | \"docs-busy\"\n | \"commit-interrupted\"\n | \"merge-failed\"\n | \"rollback-incomplete\"\n | \"seed-landscape-edited\"\n /** A hand-authored document standing at one of the two generated views paths — see the refusal in `commitSeed`. */\n | \"already-exists\";\n message: string;\n }\n /** The under-lock fleet-consistency preflight refusing; see `missingServices`. */\n | { ok: false; code: \"seed-file-invalid\"; message: string; missingServices: string[] }\n /** The docs repo went away between the command's gate and the locked read — new/commit.ts says why this is not a `code:` arm. */\n | { ok: false; repoGone: DocsRepoUnavailableError };\n\nexport async function commitSeed(req: SeedCommitRequest): Promise<SeedCommit> {\n const { docsDir } = req;\n let releaseLock: () => Promise<void>;\n try {\n releaseLock = await acquireDocsLockWaiting(docsDir, LOCK_WAIT_MS);\n } catch (err) {\n if (err instanceof DocsBusyError) return { ok: false, code: \"docs-busy\", message: err.message };\n throw err;\n }\n try {\n let recovered: CommitRecovery | null;\n try {\n recovered = await recoverInterruptedCommit(docsDir);\n } catch (err) {\n if (!(err instanceof InterruptedCommitError)) throw err;\n return { ok: false, code: \"commit-interrupted\", message: err.message };\n }\n\n // Provenance first, then the tree: both read the repo as this run will\n // leave it, and both refusals must prove nothing was written.\n const landscape = await classifyLandscape(docsDir);\n if (\"refusal\" in landscape) {\n return { ok: false, code: \"seed-landscape-edited\", message: landscape.refusal };\n }\n\n let tree: FleetTree;\n try {\n tree = await listFleetTree(docsDir);\n } catch (err) {\n if (!(err instanceof DocsRepoUnavailableError)) throw err;\n return { ok: false, repoGone: err };\n }\n const inconsistency = preflight(tree, req.seed);\n if (inconsistency !== null) return inconsistency;\n\n const plan = await planSeedWrites(req, tree);\n // THE OTHER never-overwrite promise, and the third refusal this window owns.\n //\n // `classifyLandscape` above keeps it for `architecture/landscape.likec4`;\n // this keeps it for the two files seed does not share with the team at all —\n // `architecture/subsystems.likec4` and `architecture/services.likec4` are\n // filenames loam RESERVES inside a directory the team owns, and seed writes\n // them whole. Until this line existed a `loam seed` into a repository that\n // kept a hand-authored diagram at one of those paths destroyed it in\n // silence: the same clobber reproduced through `loam subsystem new`, and the\n // guard that caught it lived in `subsystem sync` alone.\n //\n // THE WHOLE SEED IS REFUSED, exactly as an edited landscape refuses it, and\n // for the same reason: seed regenerates every file it owns wholesale from\n // fleet.yaml, so landing the landscape and the markers while declining a\n // views file would leave a tree whose very next `validate --all` — seed's\n // own `next` command — reports `subsystem.views-stale` over a file no\n // command could ever repair. `planSeedWrites` has staged nothing; the\n // transaction below is the first byte, and it does not run.\n if (plan.authored.length > 0) {\n return {\n ok: false,\n // `already-exists` for the reason commands/subsystem/txn/txn.ts gives at\n // its own copy of this refusal: it is what that code has always meant —\n // refusing to create over something that already exists, because\n // overwriting would destroy work nobody asked to lose.\n code: \"already-exists\",\n message:\n plan.authored.map((file) => authoredRefusal(file)).join(\" \") +\n \" NOTHING was written: not the landscape, not a subsystem marker, not a `.gitkeep`. Seed regenerates \" +\n \"every file it owns wholesale, so it never proceeds past human work standing at one of its paths — the \" +\n \"same promise `seed-landscape-edited` makes one file over.\",\n };\n }\n const staged = await stageWrites(plan.writes);\n const committed = await commitStaged(\n { root: docsDir, command: \"seed\", rerun: req.rerun, target: \"landscape\" },\n staged,\n \"seeded\",\n );\n if (!committed.ok) {\n // `committed.raced` is deliberately NOT remapped: the lock serialises\n // loam writers, so a fired CAS or a lost exclusive create here means an\n // out-of-band editor, and seed has no softer race answer the way `new`\n // has already-exists — merge-failed's \"nothing was written, re-run\" is\n // the honest sentence either way.\n return { ok: false, code: committed.code, message: committed.message };\n }\n return {\n ok: true,\n landscape: landscape.disposition,\n written: plan.writes.filter((w) => w.content !== null).map((w) => w.path),\n removed: plan.writes.filter((w) => w.content === null).map((w) => w.path),\n services: plan.services,\n recovered,\n };\n } finally {\n await releaseLock();\n }\n}\n\n/**\n * May seed write the landscape? Absent and untouched-scaffold-stub are the\n * two states with provably no human work in them; a matching stamp digest is\n * the third (`core/c4/seed/stamp.ts`). Everything else refuses.\n */\nasync function classifyLandscape(\n docsDir: DocsDir,\n): Promise<{ disposition: LandscapeDisposition } | { refusal: string }> {\n const path = landscapePath(docsDir);\n if (!existsSync(path)) return { disposition: \"created\" };\n const text = (await readFile(path)).toString(\"utf8\");\n // `isLandscapeStub`, never a byte-compare of our own: that function IS the\n // repo's answer to \"is this still the scaffold's untouched map\", it is what\n // `loam status` reads before telling the reader to run seed, and it is\n // suffix-matched and CRLF-normalised for two reasons seed shares —\n // `migrate-openspec` prepends a preamble to the identical stub, and a\n // checkout under `core.autocrlf` (Git for Windows' installer default)\n // rewrites every line without touching a fact. A stricter compare here made\n // status and seed contradict each other about the same file.\n if (isLandscapeStub(text)) return { disposition: \"replaced-stub\" };\n const provenance = landscapeProvenance(text);\n if (provenance === \"seed-stamped\") return { disposition: \"regenerated\" };\n return {\n refusal:\n `architecture/landscape.likec4 ${\n provenance === \"seed-edited\"\n ? \"was seeded but has been hand-edited since — the line-1 stamp's digest no longer matches the content\"\n : \"was not written by `loam seed`: it carries no line-1 stamp and is not the scaffold's untouched stub\"\n }. Seed never overwrites human work; nothing was written. Either fold the edits into ` +\n `fleet.yaml and delete the file, then re-run — or keep the hand-authored map and stop ` +\n `using seed here.`,\n };\n}\n\n/**\n * The fleet-consistency preflight. Seed regenerates the landscape WHOLESALE —\n * that is the only posture the stamp can prove safe — so every existing\n * services/<id>/ must be named in fleet.yaml, or the regenerate would orphan\n * it into `landscape.service-unmodelled`. And the file's names must not\n * collide with what the tree already holds under a different kind: one flat\n * namespace, the same rule the walk enforces.\n */\nfunction preflight(tree: FleetTree, seed: FleetSeed): SeedCommit | null {\n const treeSubs = new Set(tree.subsystems.map((s) => s.name));\n const treeIds = new Set(tree.services.map((s) => s.id as string));\n\n // The collision scan runs BEFORE the missing-service scan, and the order is\n // load-bearing. A name the file declares as a subsystem or an external while\n // the tree holds it as a service DIRECTORY is both facts at once: it is a\n // namespace collision, and it is also an existing service the file does not\n // name. With the missing scan first, the caller was told to paste that name\n // into `services:` — advice that walks straight into `seed-duplicate-service`\n // on the next run, because the file already declares it as something else.\n const collisions: string[] = [];\n for (const sub of seed.subsystems) {\n if (treeIds.has(sub as string)) collisions.push(`subsystem '${sub}' is an existing service directory`);\n }\n for (const s of seed.services) {\n if (treeSubs.has(s.id as string)) collisions.push(`service '${s.id}' is an existing subsystem directory`);\n }\n for (const x of seed.externals) {\n if (treeIds.has(x)) collisions.push(`external '${x}' is an existing service directory`);\n if (treeSubs.has(x)) collisions.push(`external '${x}' is an existing subsystem directory`);\n }\n if (collisions.length > 0) {\n return {\n ok: false,\n code: \"seed-file-invalid\",\n message:\n `fleet.yaml collides with the services/ tree — service ids, subsystem names and externals ` +\n `share one flat namespace: ${collisions.join(\"; \")}. Rename the fleet.yaml side (or the ` +\n `directory, with \\`loam subsystem rename <from> <to>\\`), then re-run. Nothing was written.`,\n missingServices: [],\n };\n }\n\n const ids = new Set(seed.services.map((s) => s.id as string));\n const missing = tree.services.map((s) => s.id as string).filter((id) => !ids.has(id)).sort();\n if (missing.length === 0) return null;\n // The ids are partitioned because one half cannot take the other's advice.\n // `tree.services` carries RAW ids — a directory whose name fails the service\n // grammar is still a service to the walk, by design — and \"add it to\n // services: exactly as spelled\" is impossible for one of those: the fleet\n // file would refuse the entry. Without this split, seed was simply unusable\n // on such a repo and never said so.\n const legal = missing.filter((id) => parseServiceId(id, \"services entry\").ok);\n const illegal = missing.filter((id) => !parseServiceId(id, \"services entry\").ok);\n const fixes: string[] = [];\n if (legal.length > 0) fixes.push(`add ${legal.join(\", \")} to services:, exactly as spelled`);\n if (illegal.length > 0) {\n fixes.push(\n `rename the ${illegal.length === 1 ? \"directory\" : \"directories\"} ${illegal.join(\", \")} ` +\n `first — ${illegal.length === 1 ? \"that name is not a legal service id\" : \"those names are not legal service ids\"}, ` +\n `so no services: entry can name ${illegal.length === 1 ? \"it\" : \"them\"}`,\n );\n }\n return {\n ok: false,\n code: \"seed-file-invalid\",\n message:\n `fleet.yaml must name every existing service — seed regenerates the landscape wholesale, ` +\n `and these services/ directories are not in it: ${missing.join(\", \")}. ` +\n `To fix: ${fixes.join(\"; and \")}. Then re-run. Nothing was written.`,\n missingServices: missing,\n };\n}\n"]}
@@ -1,22 +1,23 @@
1
1
  /**
2
2
  * What a seed run will write, decided in full before a byte lands. Split from
3
3
  * `./commit.ts` on the phase seam: that module owns the WINDOW — the lock, the
4
- * interrupted-commit recovery, the two refusals and the transaction — while
4
+ * interrupted-commit recovery, the refusals and the transaction — while
5
5
  * this one answers the single question "given the tree as it is and the fleet
6
6
  * file as it reads, which files change and how". Nothing here acquires
7
7
  * anything, and nothing here writes: it is called under the lock and hands
8
8
  * back a plan the transaction executes or rolls back whole.
9
9
  */
10
- import { existsSync } from "node:fs";
11
- import { copyFile, mkdir, mkdtemp, readdir, readFile, rm, writeFile } from "node:fs/promises";
10
+ import { copyFile, mkdir, mkdtemp, readdir, rm, writeFile } from "node:fs/promises";
12
11
  import { tmpdir } from "node:os";
13
12
  import { dirname, join, relative, resolve } from "node:path";
14
13
  import { architectureDir, architectureProjectDocuments } from "../../core/c4/project/architecture.js";
15
14
  import { loadProject } from "../../core/c4/project/load.js";
15
+ import { repoPath } from "../../core/envelope/json.js";
16
16
  import { serviceDirOf } from "../../core/kernel/ids/dirs.js";
17
- import { landscapePath, servicePathsUnder, servicesDir, subsystemPathUnder, subsystemViewsPath, unfiledServicePaths, } from "../../core/repo/paths.js";
17
+ import { landscapePath, servicePathsUnder, servicesDir, serviceViewsPath, subsystemPathUnder, subsystemViewsPath, unfiledServicePaths, } from "../../core/repo/paths.js";
18
18
  import { SUBSYSTEM_MARKER } from "../../core/repo/tree/marker.js";
19
- import { renderSubsystemViews, viewsAgree } from "../../core/repo/tree/render/views.js";
19
+ import { readGeneratedFile } from "../../core/repo/tree/render/service/header.js";
20
+ import { renderServiceOverviews, renderSubsystemViews, viewsAgree, } from "../../core/repo/tree/render/views.js";
20
21
  import { TEMP_FILE_RE } from "../../core/staging/commit.js";
21
22
  import { planWrite } from "../../core/staging/writes.js";
22
23
  /**
@@ -98,7 +99,16 @@ async function projectFacts(req) {
98
99
  staged.push(dest);
99
100
  }
100
101
  const project = await loadProject(root, staged);
101
- return project.clean ? { elements: project.elements, globalStyles: project.globalStyles } : req.map;
102
+ // RELATIONSHIPS travel with the elements, and dropping them is silent: they
103
+ // are what each per-service overview computes its peer set from, so a facts
104
+ // record without them renders every service view peerless — the service and
105
+ // its own interior, and nothing it touches — with no error anywhere
106
+ // (`core/repo/tree/render/views.ts` states the consequence beside the
107
+ // optional field). The fallback `req.map` is the whole self-check document
108
+ // and already carries them.
109
+ return project.clean
110
+ ? { elements: project.elements, globalStyles: project.globalStyles, relationships: project.relationships }
111
+ : req.map;
102
112
  }
103
113
  catch {
104
114
  // `loadProject` throws on an error it cannot attribute to a document, and
@@ -216,12 +226,53 @@ export async function planSeedWrites(req, tree) {
216
226
  // to disk that writes.ts forbids; a views file whose bytes are not UTF-8
217
227
  // therefore fails the compare and is regenerated, which is the right answer
218
228
  // for it anyway.
219
- const viewsPath = subsystemViewsPath(docsDir);
220
- const views = renderSubsystemViews(plannedTree, await projectFacts(req));
221
- const wanted = views === null ? null : Buffer.from(views, "utf8");
222
- const onDisk = existsSync(viewsPath) ? await readFile(viewsPath, "utf8") : null;
223
- if (!viewsAgree(onDisk, views))
224
- writes.push({ path: viewsPath, content: wanted });
225
- return { writes, services: { created: created.sort(), existing: existing.sort() } };
229
+ //
230
+ // BOTH generated files, from ONE read of the project: `architecture/services.likec4`
231
+ // is the same contract one file over — one overview view per service the map
232
+ // draws, absent when it draws none — and a seed that landed only the subsystem
233
+ // views would send its caller, through its own `next` list, into a
234
+ // `subsystem.views-stale` no `seed` could clear. This is where the seeded
235
+ // services first exist AND the sealed landscape first says what they are, so
236
+ // it is the only moment the pair can be rendered together.
237
+ const facts = await projectFacts(req);
238
+ const authored = [
239
+ await stageGenerated(writes, subsystemViewsPath(docsDir), renderSubsystemViews(plannedTree, facts)),
240
+ await stageGenerated(writes, serviceViewsPath(docsDir), renderServiceOverviews(plannedTree, facts)),
241
+ ].filter((path) => path !== null);
242
+ return {
243
+ writes,
244
+ services: { created: created.sort(), existing: existing.sort() },
245
+ authored: authored.map((path) => repoPath(docsDir, path)),
246
+ };
247
+ }
248
+ /**
249
+ * Append the write for ONE generated file, or nothing when the file already
250
+ * says it — the comparison the block above argues for, spelled once so the two
251
+ * generated files cannot drift into two rules. Answers the path when the file
252
+ * standing there is SOMEBODY ELSE'S, and stages nothing for it.
253
+ *
254
+ * OWNERSHIP FIRST, before the compare, exactly as `commands/subsystem/sync.ts`
255
+ * asks it — through the same reader, from beneath every writer
256
+ * (`core/repo/tree/render/service/header.ts`). Both of these are filenames loam
257
+ * RESERVES inside `architecture/`, a directory the team owns, and seed rewrites
258
+ * them whole; until this line existed a `loam seed` into a repository that kept
259
+ * a hand-authored diagram at one of those paths destroyed it without a word.
260
+ * That is the same promise `seed-landscape-edited` makes one file over — seed
261
+ * never overwrites human work — and it was missing for the two files seed
262
+ * reserves rather than shares.
263
+ *
264
+ * It is checked before the compare and not after, so that "the file already says
265
+ * this" can never vouch for a file loam did not write. An authored file cannot
266
+ * in fact agree with a render that opens with loam's mark, but a reader must not
267
+ * have to prove that to know the guard holds.
268
+ */
269
+ async function stageGenerated(writes, path, content) {
270
+ const { text: onDisk, authored } = await readGeneratedFile(path);
271
+ if (authored)
272
+ return path;
273
+ if (viewsAgree(onDisk, content))
274
+ return null;
275
+ writes.push({ path, content: content === null ? null : Buffer.from(content, "utf8") });
276
+ return null;
226
277
  }
227
278
  //# sourceMappingURL=plan.js.map