@ybotok/loam 0.2.0-alpha.1 → 0.2.0-alpha.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 (65) hide show
  1. package/CHANGELOG.md +87 -0
  2. package/README.md +6 -6
  3. package/ROADMAP.md +123 -18
  4. package/dist/commands/archive/plan/gate.js +18 -12
  5. package/dist/commands/archive/plan/gate.js.map +1 -1
  6. package/dist/commands/archive/plan/landscape.js +19 -0
  7. package/dist/commands/archive/plan/landscape.js.map +1 -1
  8. package/dist/commands/archive/run.js +2 -1
  9. package/dist/commands/archive/run.js.map +1 -1
  10. package/dist/commands/validate/arch-coverage.js +11 -1
  11. package/dist/commands/validate/arch-coverage.js.map +1 -1
  12. package/dist/commands/validate/fleet/obligations.js +43 -9
  13. package/dist/commands/validate/fleet/obligations.js.map +1 -1
  14. package/dist/commands/validate/fleet/usecases/steps.js +3 -3
  15. package/dist/commands/validate/fleet/usecases/steps.js.map +1 -1
  16. package/dist/commands/validate/service/service.js +1 -0
  17. package/dist/commands/validate/service/service.js.map +1 -1
  18. package/dist/commands/validate/service/specs.js +33 -1
  19. package/dist/commands/validate/service/specs.js.map +1 -1
  20. package/dist/core/agent/workflows/check.js +3 -0
  21. package/dist/core/agent/workflows/check.js.map +1 -1
  22. package/dist/core/brief/unchecked.js +1 -0
  23. package/dist/core/brief/unchecked.js.map +1 -1
  24. package/dist/core/c4/arch.js +72 -180
  25. package/dist/core/c4/arch.js.map +1 -1
  26. package/dist/core/c4/likec4.js +2 -0
  27. package/dist/core/c4/likec4.js.map +1 -1
  28. package/dist/core/c4/parsed/deployment.js +158 -0
  29. package/dist/core/c4/parsed/deployment.js.map +1 -0
  30. package/dist/core/c4/project/load.js +4 -0
  31. package/dist/core/c4/project/load.js.map +1 -1
  32. package/dist/core/c4/project/staged.js +220 -0
  33. package/dist/core/c4/project/staged.js.map +1 -0
  34. package/dist/core/c4/resolve/service.js +46 -0
  35. package/dist/core/c4/resolve/service.js.map +1 -1
  36. package/dist/core/c4/resolve/steps.js +144 -0
  37. package/dist/core/c4/resolve/steps.js.map +1 -0
  38. package/dist/core/c4/splice/delta-blocks.js +15 -2
  39. package/dist/core/c4/splice/delta-blocks.js.map +1 -1
  40. package/dist/core/c4/workspace.js +2 -0
  41. package/dist/core/c4/workspace.js.map +1 -1
  42. package/dist/core/coherence/coherence.js +36 -0
  43. package/dist/core/coherence/coherence.js.map +1 -1
  44. package/dist/core/deployment/delta.js +188 -0
  45. package/dist/core/deployment/delta.js.map +1 -0
  46. package/dist/core/deployment/objects.js +145 -0
  47. package/dist/core/deployment/objects.js.map +1 -0
  48. package/dist/core/pack/living.js +19 -0
  49. package/dist/core/pack/living.js.map +1 -1
  50. package/dist/core/repo/authored/paths.js +18 -0
  51. package/dist/core/repo/authored/paths.js.map +1 -1
  52. package/dist/core/repo/paths.js +43 -2
  53. package/dist/core/repo/paths.js.map +1 -1
  54. package/dist/core/status/feature/artifacts.js +29 -4
  55. package/dist/core/status/feature/artifacts.js.map +1 -1
  56. package/dist/core/status/fleet/fleet.js +11 -1
  57. package/dist/core/status/fleet/fleet.js.map +1 -1
  58. package/dist/core/usecases/delta/overlay.js +28 -191
  59. package/dist/core/usecases/delta/overlay.js.map +1 -1
  60. package/dist/core/usecases/fleet.js.map +1 -1
  61. package/dist/core/usecases/operations.js +2 -2
  62. package/dist/core/usecases/operations.js.map +1 -1
  63. package/dist/core/vocabulary/issue.js +3 -0
  64. package/dist/core/vocabulary/issue.js.map +1 -1
  65. package/package.json +1 -1
@@ -1 +1 @@
1
- {"version":3,"file":"landscape.js","sourceRoot":"","sources":["../../../../src/commands/archive/plan/landscape.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * The fleet map merge, the flows drawn over it, and the services this archive\n * brings into existence.\n *\n * The landscape is the one document every feature writes into, so it is spliced\n * rather than rewritten: the authored file keeps its shape and the delta's\n * elements are placed into it. A service arriving on the architecture axis alone\n * has no `model.likec4` yet, and the warning for that is carried out of the plan\n * — `validate` would report it next, and reporting it here is what stops that\n * being a surprise.\n *\n * `planFlows` at the bottom is the other half of the same axis and shares this\n * module for that reason rather than for the file cap: both write into\n * `architecture/`, and the two merges are read together — the map gains the\n * elements, the flow gains the ordered hops over them. It is a whole-file copy\n * where the landscape is a splice, because a flow is a document of its own and\n * there is nothing to merge partially.\n */\nimport { existsSync } from \"node:fs\";\nimport { planWrite, readUtf8 } from \"../../../core/staging/writes.js\";\nimport { planLandscapeMerge } from \"../../../core/c4/splice/landscape-merge.js\";\nimport { titleOf } from \"../../../core/c4/splice/identity/edges.js\";\nimport { parseServiceId } from \"../../../core/kernel/ids/service.js\";\nimport { landscapePath as landscapeFile } from \"../../../core/repo/paths.js\";\nimport { enumeratedServiceIds } from \"../../../core/repo/service-target.js\";\nimport { type Gated, type Plan } from \"./state.js\";\nimport { ArchiveFailure } from \"./refusal.js\";\nimport { featurePaths } from \"../../../core/repo/paths.js\";\nimport { featureFlows, livingFlowPath, USECASE_SUBDIR } from \"../../../core/usecases/delta/flows.js\";\nimport type { DocsDir } from \"../../../core/kernel/ids/dirs.js\";\n\nexport async function planLandscape(\n config: { docsDir: DocsDir },\n gated: Gated,\n plan: Plan,\n say: (line?: string) => void,\n): Promise<void> {\n const { id, featureDir, deltaDoc, deltaServices } = gated;\n const deltaLikec4 = featurePaths(featureDir).delta;\n const { writes, planWarns, architectureServices } = plan;\n const landscapePath = landscapeFile(config.docsDir);\n if (deltaDoc !== undefined) {\n const delta = deltaDoc;\n if (delta.errors.length > 0) {\n // --approve overrides loam's JUDGMENT about coherence, never its ability to\n // read an axis. Skipping here would silently drop one merge axis in the one\n // command engineered against quiet partial merges — same rule as an\n // unparseable landscape or openapi: the plan stops before anything is written.\n throw new ArchiveFailure(\n \"merge-failed\",\n `delta.likec4 has ${delta.errors.length} parse error(s) — the architecture axis cannot be merged; fix it (\\`loam validate --feature ${id}\\`) or delete the file`,\n );\n }\n // The feature tag is the whole selection mechanism of this merge, and since\n // LikeC4 1.59.0 a specification KIND can carry a tag that every element or\n // edge of that kind then inherits. A delta declaring its own feature tag on\n // a kind makes `newEls`/`newRels` below select the entire document —\n // including the context declarations the scaffold ships commented out\n // precisely so they are NOT merged — and archive would splice somebody\n // else's services into the fleet map at exit 0. Mechanical, so `--approve`\n // does not reach it, and refused before the merge is even planned.\n const kindTagged = [\n ...Object.entries(delta.specification?.elementKindTags ?? {}),\n ...Object.entries(delta.specification?.relationshipKindTags ?? {}),\n ].find(([, tags]) => tags.includes(id));\n if (kindTagged !== undefined) {\n throw new ArchiveFailure(\n \"merge-failed\",\n `delta.likec4 declares the feature tag '#${id}' on kind '${kindTagged[0]}' in its specification block, so every ` +\n `'${kindTagged[0]}' in the file inherits it — the merge would treat the whole document as this feature's ` +\n `additions, context elements included, and splice them into architecture/landscape.likec4. Nothing was ` +\n `written. Remove '#${id}' from the kind and tag only the declarations ${id} actually adds.`,\n );\n }\n const newEls = delta.elements.filter((e) => e.tags.includes(id));\n const newRels = delta.relationships.filter((r) => r.tags.includes(id));\n if (existsSync(landscapePath)) {\n const plan = await planLandscapeMerge({\n landscapeText: await readUtf8(landscapePath),\n deltaText: await readUtf8(deltaLikec4),\n deltaElements: delta.elements,\n newEls,\n newRels,\n featureId: id,\n });\n // A service can arrive on the ARCHITECTURE axis alone: an element this\n // merge ADDS, carrying a `metadata { service }` binding, with no\n // `specs/<svc>/` anywhere in the feature. It is a service the fleet gate\n // will demand a directory for the moment this merge lands, so it owes the\n // same warning as one arriving with a requirement delta — and until it\n // did, the closing \"complete + current\" line printed over a landscape\n // this very archive had just made red. Read off the ADDED elements, not\n // the tagged ones: an element the living landscape already had is not\n // arriving, and one that is never merged is not there to demand anything.\n // A binding is document text, which the service path builders no longer accept.\n // The parse cannot actually filter anything here: an illegal binding is\n // `c4.service-binding-invalid`, a coherence ERROR `--approve` does not\n // override, refused before any merge is planned — so the `ok` test only\n // carries the compiler's proof that document text never reaches a path.\n for (const e of plan.addedEls) {\n const parsed = e.service === undefined ? undefined : parseServiceId(e.service);\n if (parsed?.ok === true) architectureServices.add(parsed.id);\n }\n if (plan.content !== null) writes.push(planWrite(landscapePath, plan.content));\n say(`\\n architecture: merged into landscape.likec4 — +${plan.addedEls.length} element(s), +${plan.addedRels.length} relationship(s)`);\n for (const e of plan.addedEls) say(` + ${e.title} (${e.kind})`);\n for (const r of plan.addedRels) {\n say(` + ${titleOf(delta.elements, r.source)} -> ${titleOf(delta.elements, r.target)} \"${r.title ?? \"\"}\"`);\n }\n } else {\n say(`\\n architecture: no landscape.likec4 — ${newEls.length} element(s) not merged`);\n }\n }\n\n // A service this archive BRINGS INTO EXISTENCE arrives without the one file\n // `validate` demands of every service: its own model.likec4. The merge cannot\n // write it — the delta's tagged subtree is a landscape-level box, not a\n // container model, and inventing a plausible one is the kind of quiet fiction\n // the rest of loam exists to prevent — so the archive says so instead, and\n // stops claiming the docs are complete. Non-gating: the feature is coherent\n // and the merge is correct; what is missing is the next step, and refusing\n // here would make onboarding a new service impossible in one command.\n // \"New\" is enumeration membership, not an existsSync of services/<svc>/ at\n // the root: a FILED service exists wherever the tree walk found it, and the\n // root probe would grade every filed service as newly created here.\n const enumerated = new Set<string>(await enumeratedServiceIds(config.docsDir));\n const newServices = [...new Set([...deltaServices, ...architectureServices])].filter(\n (svc) => !enumerated.has(svc),\n );\n for (const svc of newServices) {\n // Two shapes of the same debt: a service with a requirement delta gets its\n // directory from this merge, one that arrives only in the landscape gets no\n // directory at all — and `validate --all` fails the second harder\n // (`landscape.service-unmodelled` names the binding with nothing behind it).\n const creates = deltaServices.some((d) => d === svc)\n ? `this archive creates services/${svc}/, but nothing writes services/${svc}/model.likec4`\n : `this archive puts '${svc}' in the landscape, but the fleet has no services/${svc}/ at all`;\n planWarns.push({\n severity: \"warn\",\n code: \"service.no-model\",\n subject: svc,\n message: `${svc}: ${creates} — 'loam validate --all' will report the service as incomplete until it exists. Run 'loam adopt --service ${svc}' from the service repo, or write the model by hand.`,\n });\n }\n}\n\n/**\n * The flows this feature brings, copied into `architecture/usecases/`.\n *\n * A create-only whole-file copy, and the smallest merge in the command: the\n * document is views-only, it names nothing the landscape merge has not already\n * placed (`planFlowOverlay` in the gate is what proves that, before this runs),\n * and a flow the living tree already holds was refused at the gate\n * (`usecase.flow-exists`, `core/usecases/delta/flows.ts` has the reasoning). So\n * by the time this runs every path here is a file that does not exist, and\n * `planWrite` marks it `exclusive` — a race that created it between the gate and\n * the commit fails the write rather than overwriting somebody.\n *\n * It lives beside `planLandscape` rather than beside the requirement merges\n * because it writes into `architecture/`, and the two merges are read together:\n * the landscape gains the elements, the flow gains the hops over them.\n * `unarchive` needs nothing new — a create is undone by deleting, which the\n * snapshot manifest already records for every write in the plan.\n */\nexport async function planFlows(\n config: { docsDir: DocsDir },\n gated: Gated,\n plan: Plan,\n say: (line?: string) => void,\n): Promise<void> {\n const flows = await featureFlows(gated.featureDir);\n if (flows.length === 0) return;\n for (const flow of flows) {\n plan.writes.push(planWrite(livingFlowPath(config.docsDir, flow.rel), await readUtf8(flow.path)));\n say(` use case: ${flow.rel} — created architecture/${USECASE_SUBDIR}/${flow.rel}`);\n }\n}\n"]}
1
+ {"version":3,"file":"landscape.js","sourceRoot":"","sources":["../../../../src/commands/archive/plan/landscape.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * The fleet map merge, the flows drawn over it, and the services this archive\n * brings into existence.\n *\n * The landscape is the one document every feature writes into, so it is spliced\n * rather than rewritten: the authored file keeps its shape and the delta's\n * elements are placed into it. A service arriving on the architecture axis alone\n * has no `model.likec4` yet, and the warning for that is carried out of the plan\n * — `validate` would report it next, and reporting it here is what stops that\n * being a surprise.\n *\n * `planFlows` at the bottom is the other half of the same axis and shares this\n * module for that reason rather than for the file cap: both write into\n * `architecture/`, and the two merges are read together — the map gains the\n * elements, the flow gains the ordered hops over them. It is a whole-file copy\n * where the landscape is a splice, because a flow is a document of its own and\n * there is nothing to merge partially.\n */\nimport { existsSync } from \"node:fs\";\nimport { planWrite, readUtf8 } from \"../../../core/staging/writes.js\";\nimport { planLandscapeMerge } from \"../../../core/c4/splice/landscape-merge.js\";\nimport { titleOf } from \"../../../core/c4/splice/identity/edges.js\";\nimport { parseServiceId } from \"../../../core/kernel/ids/service.js\";\nimport { landscapePath as landscapeFile } from \"../../../core/repo/paths.js\";\nimport { enumeratedServiceIds } from \"../../../core/repo/service-target.js\";\nimport { type Gated, type Plan } from \"./state.js\";\nimport { ArchiveFailure } from \"./refusal.js\";\nimport { featurePaths } from \"../../../core/repo/paths.js\";\nimport { featureFlows, livingFlowPath, USECASE_SUBDIR } from \"../../../core/usecases/delta/flows.js\";\nimport { featureDeployments, livingDeploymentPath } from \"../../../core/deployment/delta.js\";\nimport type { DocsDir } from \"../../../core/kernel/ids/dirs.js\";\n\nexport async function planLandscape(\n config: { docsDir: DocsDir },\n gated: Gated,\n plan: Plan,\n say: (line?: string) => void,\n): Promise<void> {\n const { id, featureDir, deltaDoc, deltaServices } = gated;\n const deltaLikec4 = featurePaths(featureDir).delta;\n const { writes, planWarns, architectureServices } = plan;\n const landscapePath = landscapeFile(config.docsDir);\n if (deltaDoc !== undefined) {\n const delta = deltaDoc;\n if (delta.errors.length > 0) {\n // --approve overrides loam's JUDGMENT about coherence, never its ability to\n // read an axis. Skipping here would silently drop one merge axis in the one\n // command engineered against quiet partial merges — same rule as an\n // unparseable landscape or openapi: the plan stops before anything is written.\n throw new ArchiveFailure(\n \"merge-failed\",\n `delta.likec4 has ${delta.errors.length} parse error(s) — the architecture axis cannot be merged; fix it (\\`loam validate --feature ${id}\\`) or delete the file`,\n );\n }\n // The feature tag is the whole selection mechanism of this merge, and since\n // LikeC4 1.59.0 a specification KIND can carry a tag that every element or\n // edge of that kind then inherits. A delta declaring its own feature tag on\n // a kind makes `newEls`/`newRels` below select the entire document —\n // including the context declarations the scaffold ships commented out\n // precisely so they are NOT merged — and archive would splice somebody\n // else's services into the fleet map at exit 0. Mechanical, so `--approve`\n // does not reach it, and refused before the merge is even planned.\n const kindTagged = [\n ...Object.entries(delta.specification?.elementKindTags ?? {}),\n ...Object.entries(delta.specification?.relationshipKindTags ?? {}),\n ].find(([, tags]) => tags.includes(id));\n if (kindTagged !== undefined) {\n throw new ArchiveFailure(\n \"merge-failed\",\n `delta.likec4 declares the feature tag '#${id}' on kind '${kindTagged[0]}' in its specification block, so every ` +\n `'${kindTagged[0]}' in the file inherits it — the merge would treat the whole document as this feature's ` +\n `additions, context elements included, and splice them into architecture/landscape.likec4. Nothing was ` +\n `written. Remove '#${id}' from the kind and tag only the declarations ${id} actually adds.`,\n );\n }\n const newEls = delta.elements.filter((e) => e.tags.includes(id));\n const newRels = delta.relationships.filter((r) => r.tags.includes(id));\n if (existsSync(landscapePath)) {\n const plan = await planLandscapeMerge({\n landscapeText: await readUtf8(landscapePath),\n deltaText: await readUtf8(deltaLikec4),\n deltaElements: delta.elements,\n newEls,\n newRels,\n featureId: id,\n });\n // A service can arrive on the ARCHITECTURE axis alone: an element this\n // merge ADDS, carrying a `metadata { service }` binding, with no\n // `specs/<svc>/` anywhere in the feature. It is a service the fleet gate\n // will demand a directory for the moment this merge lands, so it owes the\n // same warning as one arriving with a requirement delta — and until it\n // did, the closing \"complete + current\" line printed over a landscape\n // this very archive had just made red. Read off the ADDED elements, not\n // the tagged ones: an element the living landscape already had is not\n // arriving, and one that is never merged is not there to demand anything.\n // A binding is document text, which the service path builders no longer accept.\n // The parse cannot actually filter anything here: an illegal binding is\n // `c4.service-binding-invalid`, a coherence ERROR `--approve` does not\n // override, refused before any merge is planned — so the `ok` test only\n // carries the compiler's proof that document text never reaches a path.\n for (const e of plan.addedEls) {\n const parsed = e.service === undefined ? undefined : parseServiceId(e.service);\n if (parsed?.ok === true) architectureServices.add(parsed.id);\n }\n if (plan.content !== null) writes.push(planWrite(landscapePath, plan.content));\n say(`\\n architecture: merged into landscape.likec4 — +${plan.addedEls.length} element(s), +${plan.addedRels.length} relationship(s)`);\n for (const e of plan.addedEls) say(` + ${e.title} (${e.kind})`);\n for (const r of plan.addedRels) {\n say(` + ${titleOf(delta.elements, r.source)} -> ${titleOf(delta.elements, r.target)} \"${r.title ?? \"\"}\"`);\n }\n } else {\n say(`\\n architecture: no landscape.likec4 — ${newEls.length} element(s) not merged`);\n }\n }\n\n // A service this archive BRINGS INTO EXISTENCE arrives without the one file\n // `validate` demands of every service: its own model.likec4. The merge cannot\n // write it — the delta's tagged subtree is a landscape-level box, not a\n // container model, and inventing a plausible one is the kind of quiet fiction\n // the rest of loam exists to prevent — so the archive says so instead, and\n // stops claiming the docs are complete. Non-gating: the feature is coherent\n // and the merge is correct; what is missing is the next step, and refusing\n // here would make onboarding a new service impossible in one command.\n // \"New\" is enumeration membership, not an existsSync of services/<svc>/ at\n // the root: a FILED service exists wherever the tree walk found it, and the\n // root probe would grade every filed service as newly created here.\n const enumerated = new Set<string>(await enumeratedServiceIds(config.docsDir));\n const newServices = [...new Set([...deltaServices, ...architectureServices])].filter(\n (svc) => !enumerated.has(svc),\n );\n for (const svc of newServices) {\n // Two shapes of the same debt: a service with a requirement delta gets its\n // directory from this merge, one that arrives only in the landscape gets no\n // directory at all — and `validate --all` fails the second harder\n // (`landscape.service-unmodelled` names the binding with nothing behind it).\n const creates = deltaServices.some((d) => d === svc)\n ? `this archive creates services/${svc}/, but nothing writes services/${svc}/model.likec4`\n : `this archive puts '${svc}' in the landscape, but the fleet has no services/${svc}/ at all`;\n planWarns.push({\n severity: \"warn\",\n code: \"service.no-model\",\n subject: svc,\n message: `${svc}: ${creates} — 'loam validate --all' will report the service as incomplete until it exists. Run 'loam adopt --service ${svc}' from the service repo, or write the model by hand.`,\n });\n }\n}\n\n/**\n * The flows this feature brings, copied into `architecture/usecases/`.\n *\n * A create-only whole-file copy, and the smallest merge in the command: the\n * document is views-only, it names nothing the landscape merge has not already\n * placed (`planFlowOverlay` in the gate is what proves that, before this runs),\n * and a flow the living tree already holds was refused at the gate\n * (`usecase.flow-exists`, `core/usecases/delta/flows.ts` has the reasoning). So\n * by the time this runs every path here is a file that does not exist, and\n * `planWrite` marks it `exclusive` — a race that created it between the gate and\n * the commit fails the write rather than overwriting somebody.\n *\n * It lives beside `planLandscape` rather than beside the requirement merges\n * because it writes into `architecture/`, and the two merges are read together:\n * the landscape gains the elements, the flow gains the hops over them.\n * `unarchive` needs nothing new — a create is undone by deleting, which the\n * snapshot manifest already records for every write in the plan.\n */\nexport async function planFlows(\n config: { docsDir: DocsDir },\n gated: Gated,\n plan: Plan,\n say: (line?: string) => void,\n): Promise<void> {\n const flows = await featureFlows(gated.featureDir);\n if (flows.length === 0) return;\n for (const flow of flows) {\n plan.writes.push(planWrite(livingFlowPath(config.docsDir, flow.rel), await readUtf8(flow.path)));\n say(` use case: ${flow.rel} — created architecture/${USECASE_SUBDIR}/${flow.rel}`);\n }\n}\n\n/**\n * The same copy for the third whole-file axis: the topology a feature brings.\n *\n * Beside `planFlows` rather than folded into it, because the two write to\n * different places — a flow into `architecture/usecases/`, a topology straight\n * into `architecture/` — and one function taking a subdirectory argument would\n * make that difference a parameter instead of a decision each axis states.\n * `unarchive` needs nothing here either: a create is undone by deleting.\n */\nexport async function planDeployments(\n config: { docsDir: DocsDir },\n gated: Gated,\n plan: Plan,\n say: (line?: string) => void,\n): Promise<void> {\n const docs = await featureDeployments(gated.featureDir);\n if (docs.length === 0) return;\n for (const doc of docs) {\n plan.writes.push(planWrite(livingDeploymentPath(config.docsDir, doc.rel), await readUtf8(doc.path)));\n say(` deployment: ${doc.rel} — created architecture/${doc.rel}`);\n }\n}\n"]}
@@ -21,7 +21,7 @@ import {} from "./plan/refusal.js";
21
21
  import { planCapabilities, planGlossary, planSpecs } from "./plan/specs.js";
22
22
  import { planOpenapiContracts } from "./plan/contracts/openapi.js";
23
23
  import { planAsyncapiContracts } from "./plan/contracts/asyncapi.js";
24
- import { planFlows, planLandscape } from "./plan/landscape.js";
24
+ import { planDeployments, planFlows, planLandscape } from "./plan/landscape.js";
25
25
  import { emptyPlan } from "./plan/state.js";
26
26
  import { issueJson, refuseJson } from "./plan/refusal.js";
27
27
  import { ArchiveFailure } from "./plan/refusal.js";
@@ -68,6 +68,7 @@ export async function archiveLocked(config, featureId, opts) {
68
68
  // merge so a reader of the plan sees the map change before the sequence drawn
69
69
  // on it.
70
70
  await planFlows(config, gated, planned, say);
71
+ await planDeployments(config, gated, planned, say);
71
72
  const { writes, planWarns, planGates, openapiRemovals, asyncapiRemovals } = planned;
72
73
  // Gate on what only the plan could see: a merged operation pointing at a
73
74
  // component that exists nowhere, a removal marker addressing no operation.
@@ -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 { 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 { 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 await planLandscape(config, 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 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 if (json) {\n emitJson(payload(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) {\n console.log(\" living spec + landscape are now complete + current.\");\n return;\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"]}
1
+ {"version":3,"file":"run.js","sourceRoot":"","sources":["../../../src/commands/archive/run.ts"],"names":[],"mappings":"AAAA;;;;;;;;;GASG;AACH,OAAO,EAAE,UAAU,EAAE,MAAM,SAAS,CAAC;AACrC,OAAO,EAAE,KAAK,EAAE,MAAM,EAAE,MAAM,kBAAkB,CAAC;AACjD,OAAO,EAAE,QAAQ,EAAE,QAAQ,EAAE,UAAU,EAAE,MAAM,6BAA6B,CAAC;AAC7E,OAAO,EACL,OAAO,EACP,sBAAsB,EACtB,OAAO,EACP,aAAa,EACb,cAAc,EACd,WAAW,EACX,UAAU,GACX,MAAM,8BAA8B,CAAC;AACtC,OAAO,EAAE,iBAAiB,EAAE,iBAAiB,EAAE,MAAM,uCAAuC,CAAC;AAC7F,OAAO,EACL,YAAY,EACZ,oBAAoB,EACpB,WAAW,EACX,aAAa,GAEd,MAAM,gCAAgC,CAAC;AACxC,OAAO,EAAE,kBAAkB,EAAE,MAAM,mCAAmC,CAAC;AACvE,OAAO,EAAE,YAAY,EAAE,MAAM,6BAA6B,CAAC;AAC3D,OAAO,EAAE,IAAI,EAAE,MAAM,gBAAgB,CAAC;AACtC,OAAO,EAAuB,MAAM,mBAAmB,CAAC;AACxD,OAAO,EAAE,gBAAgB,EAAE,YAAY,EAAE,SAAS,EAAE,MAAM,iBAAiB,CAAC;AAC5E,OAAO,EAAE,oBAAoB,EAAE,MAAM,6BAA6B,CAAC;AACnE,OAAO,EAAE,qBAAqB,EAAE,MAAM,8BAA8B,CAAC;AACrE,OAAO,EAAE,eAAe,EAAE,SAAS,EAAE,aAAa,EAAE,MAAM,qBAAqB,CAAC;AAChF,OAAO,EAAE,SAAS,EAAE,MAAM,iBAAiB,CAAC;AAC5C,OAAO,EAAE,SAAS,EAAE,UAAU,EAAE,MAAM,mBAAmB,CAAC;AAC1D,OAAO,EAAE,cAAc,EAAE,MAAM,mBAAmB,CAAC;AACnD,OAAO,EAAE,SAAS,EAAE,MAAM,mBAAmB,CAAC;AAG9C,MAAM,CAAC,KAAK,UAAU,aAAa,CACjC,MAA4B,EAC5B,SAAiB,EACjB,IAAoB;IAEpB,MAAM,MAAM,GAAG,IAAI,CAAC,MAAM,KAAK,IAAI,CAAC;IACpC,MAAM,IAAI,GAAG,IAAI,CAAC,IAAI,KAAK,IAAI,CAAC;IAChC,+EAA+E;IAC/E,MAAM,GAAG,GAAG,CAAC,IAAI,GAAG,EAAE,EAAQ,EAAE;QAC9B,IAAI,CAAC,IAAI;YAAE,OAAO,CAAC,GAAG,CAAC,IAAI,CAAC,CAAC;IAC/B,CAAC,CAAC;IAEF,qEAAqE;IACrE,yEAAyE;IACzE,2EAA2E;IAC3E,yEAAyE;IACzE,yEAAyE;IACzE,yEAAyE;IACzE,4BAA4B;IAC5B,MAAM,IAAI,GAAG,EAAE,OAAO,EAAE,MAAM,CAAC,OAAO,EAAE,KAAK,EAAE,IAAI,YAAY,EAAE,EAAE,CAAC;IACpE,MAAM,KAAK,GAAG,MAAM,IAAI,CAAC,IAAI,EAAE,SAAS,EAAE,IAAI,EAAE,GAAG,CAAC,CAAC;IACrD,IAAI,KAAK,KAAK,IAAI;QAAE,OAAO;IAC3B,MAAM,EAAE,EAAE,EAAE,OAAO,EAAE,UAAU,EAAE,MAAM,EAAE,QAAQ,EAAE,UAAU,EAAE,WAAW,EAAE,SAAS,EAAE,MAAM,EAAE,GAAG,KAAK,CAAC;IAExG,GAAG,CAAC,WAAW,EAAE,GAAG,MAAM,CAAC,CAAC,CAAC,aAAa,CAAC,CAAC,CAAC,EAAE,IAAI,CAAC,CAAC;IAErD,gFAAgF;IAChF,uEAAuE;IACvE,MAAM,OAAO,GAAG,SAAS,EAAE,CAAC;IAC5B,MAAM,SAAS,CAAC,IAAI,EAAE,KAAK,EAAE,OAAO,EAAE,GAAG,CAAC,CAAC;IAC3C,6EAA6E;IAC7E,wEAAwE;IACxE,yEAAyE;IACzE,MAAM,gBAAgB,CAAC,IAAI,EAAE,KAAK,EAAE,OAAO,EAAE,GAAG,CAAC,CAAC;IAClD,0EAA0E;IAC1E,yEAAyE;IACzE,oCAAoC;IACpC,MAAM,YAAY,CAAC,IAAI,EAAE,KAAK,EAAE,OAAO,EAAE,GAAG,CAAC,CAAC;IAC9C,MAAM,oBAAoB,CAAC,IAAI,EAAE,KAAK,EAAE,OAAO,EAAE,GAAG,CAAC,CAAC;IACtD,MAAM,qBAAqB,CAAC,IAAI,EAAE,KAAK,EAAE,OAAO,EAAE,GAAG,CAAC,CAAC;IACvD,MAAM,aAAa,CAAC,MAAM,EAAE,KAAK,EAAE,OAAO,EAAE,GAAG,CAAC,CAAC;IACjD,6EAA6E;IAC7E,+EAA+E;IAC/E,8EAA8E;IAC9E,8EAA8E;IAC9E,SAAS;IACT,MAAM,SAAS,CAAC,MAAM,EAAE,KAAK,EAAE,OAAO,EAAE,GAAG,CAAC,CAAC;IAC7C,MAAM,eAAe,CAAC,MAAM,EAAE,KAAK,EAAE,OAAO,EAAE,GAAG,CAAC,CAAC;IACnD,MAAM,EAAE,MAAM,EAAE,SAAS,EAAE,SAAS,EAAE,eAAe,EAAE,gBAAgB,EAAE,GAAG,OAAO,CAAC;IAEpF,yEAAyE;IACzE,2EAA2E;IAC3E,4EAA4E;IAC5E,6EAA6E;IAC7E,0EAA0E;IAC1E,2BAA2B;IAC3B,IAAI,SAAS,CAAC,MAAM,GAAG,CAAC,IAAI,CAAC,IAAI,CAAC,OAAO,EAAE,CAAC;QAC1C,MAAM,GAAG,GAAG,WAAW,EAAE,eAAe,SAAS,CAAC,MAAM,iCAAiC,CAAC;QAC1F,IAAI,IAAI,EAAE,CAAC;YACT,UAAU,CACR,cAAc,EACd,GAAG,EACH,CAAC,GAAG,MAAM,EAAE,GAAG,SAAS,EAAE,GAAG,SAAS,CAAC,EACvC,YAAY,OAAO,EAAE,CACtB,CAAC;YACF,OAAO;QACT,CAAC;QACD,OAAO,CAAC,KAAK,CAAC,GAAG,GAAG,GAAG,CAAC,CAAC;QACzB,KAAK,MAAM,CAAC,IAAI,SAAS;YAAE,OAAO,CAAC,KAAK,CAAC,OAAO,CAAC,CAAC,OAAO,EAAE,CAAC,CAAC;QAC7D,OAAO,CAAC,KAAK,CAAC,sGAAsG,CAAC,CAAC;QACtH,UAAU,CAAC,cAAc,CAAC,CAAC;QAC3B,OAAO,CAAC,QAAQ,GAAG,CAAC,CAAC;QACrB,OAAO;IACT,CAAC;IACD,IAAI,SAAS,CAAC,MAAM,GAAG,CAAC,EAAE,CAAC;QACzB,GAAG,CAAC,2BAA2B,SAAS,CAAC,MAAM,uCAAuC,CAAC,CAAC;QACxF,KAAK,MAAM,CAAC,IAAI,SAAS;YAAE,GAAG,CAAC,WAAW,CAAC,CAAC,OAAO,EAAE,CAAC,CAAC;IACzD,CAAC;IAED,8EAA8E;IAC9E,6EAA6E;IAC7E,sEAAsE;IACtE,MAAM,QAAQ,GAAG,CAAC,GAAG,QAAQ,EAAE,GAAG,SAAS,CAAC,CAAC;IAC7C,MAAM,UAAU,GAAG,IAAI,CAAC,OAAO,KAAK,IAAI,CAAC,CAAC,CAAC,CAAC,GAAG,MAAM,EAAE,GAAG,SAAS,CAAC,CAAC,CAAC,CAAC,EAAE,CAAC;IAC1E,MAAM,IAAI,GAAmC,MAAM,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC;QAC9D,IAAI,EAAE,QAAQ,CAAC,MAAM,CAAC,OAAO,EAAE,CAAC,CAAC,IAAI,CAAC;QACtC,MAAM,EAAE,UAAU,CAAC,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC,QAAQ,CAAC,CAAC,CAAC,QAAQ;KACjD,CAAC,CAAC,CAAC;IACJ,IAAI,CAAC,IAAI,CAAC,EAAE,IAAI,EAAE,YAAY,OAAO,EAAE,EAAE,MAAM,EAAE,MAAM,EAAE,EAAE,EAAE,oBAAoB,OAAO,EAAE,EAAE,CAAC,CAAC;IAC9F,MAAM,OAAO,GAAG,CAAC,QAAiB,EAA2B,EAAE,CAAC,CAAC;QAC/D,OAAO,EAAE,SAAS;QAClB,OAAO,EAAE,EAAE;QACX,QAAQ;QACR,IAAI,EAAE,QAAQ,CAAC,MAAM,CAAC,OAAO,EAAE,WAAW,CAAC;QAC3C,IAAI;QACJ,QAAQ,EAAE,QAAQ,CAAC,GAAG,CAAC,CAAC,KAAK,EAAE,EAAE,CAAC,SAAS,CAAC,KAAK,EAAE,QAAQ,CAAC,MAAM,CAAC,OAAO,EAAE,WAAW,CAAC,CAAC,CAAC;QAC1F,UAAU,EAAE,UAAU,CAAC,GAAG,CAAC,CAAC,KAAK,EAAE,EAAE,CAAC,SAAS,CAAC,KAAK,EAAE,QAAQ,CAAC,MAAM,CAAC,OAAO,EAAE,WAAW,CAAC,CAAC,CAAC;QAC9F,eAAe;QACf,gBAAgB;QAChB,4EAA4E;QAC5E,2EAA2E;QAC3E,yCAAyC;QACzC,GAAG,CAAC,SAAS,KAAK,IAAI,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,EAAE,SAAS,EAAE,CAAC;KAC7C,CAAC,CAAC;IAEH,IAAI,MAAM,EAAE,CAAC;QACX,IAAI,IAAI;YAAE,QAAQ,CAAC,OAAO,CAAC,KAAK,CAAC,CAAC,CAAC;;YAC9B,SAAS,CAAC,MAAM,CAAC,OAAO,EAAE,MAAM,EAAE,OAAO,CAAC,CAAC;QAChD,OAAO;IACT,CAAC;IAED,+EAA+E;IAC/E,+EAA+E;IAC/E,6EAA6E;IAC7E,8EAA8E;IAC9E,wEAAwE;IACxE,6EAA6E;IAC7E,IAAI,MAAM,CAAC;IACX,IAAI,CAAC;QACH,MAAM,GAAG,MAAM,WAAW,CAAC,MAAM,CAAC,CAAC;IACrC,CAAC;IAAC,OAAO,GAAG,EAAE,CAAC;QACb,MAAM,IAAI,cAAc,CAAC,cAAc,EAAE,GAAG,OAAO,CAAC,GAAG,CAAC,wBAAwB,CAAC,CAAC;IACpF,CAAC;IACD,IAAI,QAAQ,GAAG,KAAK,CAAC;IACrB,IAAI,iBAAqC,CAAC;IAC1C,IAAI,CAAC;QACH,2EAA2E;QAC3E,wEAAwE;QACxE,0EAA0E;QAC1E,mEAAmE;QACnE,uEAAuE;QACvE,uEAAuE;QACvE,uEAAuE;QACvE,uEAAuE;QACvE,0EAA0E;QAC1E,uDAAuD;QACvD,mEAAmE;QACnE,wEAAwE;QACxE,iEAAiE;QACjE,sEAAsE;QACtE,MAAM,KAAK,GAAG,IAAI,GAAG,CACnB,CAAC,MAAM,kBAAkB,CAAC,MAAM,CAAC,OAAO,EAAE,IAAI,CAAC,KAAK,CAAC,CAAC,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,CAAC,EAAY,EAAE,QAAQ,CAAC,MAAM,CAAC,OAAO,EAAE,CAAC,CAAC,GAAG,CAAC,CAAC,CAAC,CACrH,CAAC;QACF,MAAM,YAAY,GAAG,CAAC,GAAW,EAAqB,EAAE;YACtD,KAAK,MAAM,CAAC,OAAO,EAAE,GAAG,CAAC,IAAI,KAAK,EAAE,CAAC;gBACnC,IAAI,GAAG,CAAC,UAAU,CAAC,GAAG,GAAG,GAAG,CAAC;oBAAE,OAAO,EAAE,OAAO,EAAE,QAAQ,EAAE,GAAG,CAAC,KAAK,CAAC,GAAG,CAAC,MAAM,GAAG,CAAC,CAAC,EAAE,CAAC;YACzF,CAAC;YACD,MAAM,CAAC,GAAG,2BAA2B,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC;YAChD,OAAO,CAAC,CAAC,CAAC,CAAC,EAAE,OAAO,EAAE,CAAC,CAAC,CAAC,CAAE,EAAE,QAAQ,EAAE,CAAC,CAAC,CAAC,CAAE,EAAE,CAAC,CAAC,CAAC,IAAI,CAAC;QACxD,CAAC,CAAC;QACF,MAAM,YAAY,GAAG,CAAC,OAAe,EAAiB,EAAE,CAAC,KAAK,CAAC,GAAG,CAAC,OAAO,CAAC,IAAI,IAAI,CAAC;QACpF,MAAM,aAAa,CAAC;YAClB,UAAU;YACV,OAAO,EAAE,MAAM,CAAC,OAAO;YACvB,OAAO,EAAE,EAAE,SAAS,EAAE,EAAE,EAAE,OAAO,EAAE;YACnC,MAAM;YACN,YAAY;YACZ,YAAY;SACb,CAAC,CAAC;QACH,QAAQ,GAAG,IAAI,CAAC;QAChB,6EAA6E;QAC7E,yEAAyE;QACzE,6EAA6E;QAC7E,2EAA2E;QAC3E,8DAA8D;QAC9D,MAAM,iBAAiB,CACrB,MAAM,CAAC,OAAO,EACd,EAAE,OAAO,EAAE,SAAS,EAAE,OAAO,EAAE,QAAQ,EAAE,OAAO,EAAE,EAAE,EAAE,QAAQ,EAAE,UAAU,EAAE,MAAM,EAAE,WAAW,EAAE,EACjG,MAAM,CACP,CAAC;QACF,MAAM,UAAU,CAAC,MAAM,CAAC,CAAC;QACzB,iBAAiB,GAAG,MAAM,KAAK,CAAC,UAAU,EAAE,EAAE,SAAS,EAAE,IAAI,EAAE,CAAC,CAAC;QACjE,MAAM,MAAM,CAAC,UAAU,EAAE,WAAW,CAAC,CAAC;QACtC,kDAAkD;QAClD,MAAM,iBAAiB,CAAC,MAAM,CAAC,OAAO,CAAC,CAAC;IAC1C,CAAC;IAAC,OAAO,GAAG,EAAE,CAAC;QACb,IAAI,GAAG,YAAY,oBAAoB,EAAE,CAAC;YACxC,mEAAmE;YACnE,0EAA0E;YAC1E,0EAA0E;YAC1E,8BAA8B;YAC9B,MAAM,cAAc,CAAC,MAAM,CAAC,CAAC;YAC7B,MAAM,IAAI,cAAc,CAAC,oBAAoB,EAAE,GAAG,CAAC,OAAO,CAAC,CAAC;QAC9D,CAAC;QACD,+EAA+E;QAC/E,4EAA4E;QAC5E,gEAAgE;QAChE,MAAM,QAAQ,GAAG,MAAM,cAAc,CAAC,MAAM,CAAC,CAAC;QAC9C,uEAAuE;QACvE,4EAA4E;QAC5E,iDAAiD;QACjD,MAAM,iBAAiB,CAAC,MAAM,CAAC,OAAO,CAAC,CAAC;QACxC,2EAA2E;QAC3E,4EAA4E;QAC5E,4EAA4E;QAC5E,6EAA6E;QAC7E,4EAA4E;QAC5E,IAAI,QAAQ,IAAI,QAAQ,CAAC,MAAM,KAAK,CAAC;YAAE,MAAM,OAAO,CAAC,WAAW,CAAC,UAAU,CAAC,CAAC,CAAC;QAC9E,0EAA0E;QAC1E,0EAA0E;QAC1E,2EAA2E;QAC3E,qEAAqE;QACrE,uDAAuD;QACvD,IAAI,iBAAiB,KAAK,SAAS;YAAE,MAAM,sBAAsB,CAAC,UAAU,EAAE,iBAAiB,CAAC,CAAC;QACjG,yEAAyE;QACzE,yEAAyE;QACzE,MAAM,OAAO,GAAG,aAAa,CAAC,GAAG,EAAE,QAAQ,CAAC,CAAC;QAC7C,MAAM,IAAI,GACR,QAAQ,IAAI,QAAQ,CAAC,MAAM,GAAG,CAAC;YAC7B,CAAC,CAAC,6DAA6D,OAAO,IAAI,YAAY,UAAU;YAChG,CAAC,CAAC,EAAE,CAAC;QACT,MAAM,IAAI,cAAc,CAAC,QAAQ,CAAC,MAAM,GAAG,CAAC,CAAC,CAAC,CAAC,qBAAqB,CAAC,CAAC,CAAC,cAAc,EAAE,OAAO,CAAC,OAAO,GAAG,IAAI,CAAC,CAAC;IACjH,CAAC;IAED,IAAI,IAAI,EAAE,CAAC;QACT,QAAQ,CAAC,OAAO,CAAC,IAAI,CAAC,CAAC,CAAC;QACxB,OAAO;IACT,CAAC;IACD,OAAO,CAAC,GAAG,CAAC,0BAA0B,OAAO,uBAAuB,OAAO,EAAE,CAAC,CAAC;IAC/E,OAAO,CAAC,GAAG,CAAC,gCAAgC,OAAO,IAAI,YAAY,wBAAwB,EAAE,iBAAiB,CAAC,CAAC;IAChH,8EAA8E;IAC9E,6EAA6E;IAC7E,8EAA8E;IAC9E,2EAA2E;IAC3E,mCAAmC;IACnC,MAAM,UAAU,GAAG,SAAS,CAAC,MAAM,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,IAAI,KAAK,kBAAkB,CAAC,CAAC;IAC1E,IAAI,UAAU,CAAC,MAAM,KAAK,CAAC,IAAI,UAAU,CAAC,MAAM,KAAK,CAAC,EAAE,CAAC;QACvD,OAAO,CAAC,GAAG,CAAC,uDAAuD,CAAC,CAAC;QACrE,OAAO;IACT,CAAC;IACD,KAAK,MAAM,CAAC,IAAI,UAAU;QAAE,OAAO,CAAC,GAAG,CAAC,OAAO,CAAC,CAAC,OAAO,EAAE,CAAC,CAAC;IAC5D,IAAI,UAAU,CAAC,MAAM,GAAG,CAAC,EAAE,CAAC;QAC1B,OAAO,CAAC,GAAG,CACT,mBAAmB,UAAU,CAAC,MAAM,gHAAgH,CACrJ,CAAC;IACJ,CAAC;AACH,CAAC","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 { 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 await planLandscape(config, 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 if (json) {\n emitJson(payload(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) {\n console.log(\" living spec + landscape are now complete + current.\");\n return;\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"]}
@@ -153,12 +153,22 @@ export async function deltaArchCoverage(delta) {
153
153
  const landParses = land !== null && land.errors.length === 0 ? land : null;
154
154
  const baseElements = [...elements, ...(landParses?.elements ?? [])];
155
155
  const baseRels = [...relationships, ...(landParses?.relationships ?? [])];
156
+ // A feature never brings topology through `delta.likec4` — a `deployment { }`
157
+ // block in one is refused by the merge — so the living map is the whole
158
+ // scope for a `node:` entry in a delta, exactly as it is in service scope.
159
+ const baseDeployment = landParses?.deployment;
156
160
  for (const { service: svc, reqs } of archDeltas) {
157
161
  // An unreadable living health.yaml mutes the alert:/sli: entries here
158
162
  // exactly as in service scope — the health.invalid finding itself
159
163
  // belongs to the service target, which owns the file's diagnosis.
160
164
  const health = await readHealth((await locateServicePaths(docsDir, svc, fleet)).health);
161
- let scope = { elements: baseElements, relationships: baseRels, health: health.ids, known };
165
+ let scope = {
166
+ elements: baseElements,
167
+ relationships: baseRels,
168
+ ...(baseDeployment === undefined ? {} : { deployment: baseDeployment }),
169
+ health: health.ids,
170
+ known,
171
+ };
162
172
  const unresolved = coversUnknownFindings(reqs, { where: `${svc}: arch.spec.md`, subject: svc }, scope, health.unreadable);
163
173
  if (unresolved.length > 0) {
164
174
  const modelPath = (await locateServicePaths(docsDir, svc, fleet)).model;
@@ -1 +1 @@
1
- {"version":3,"file":"arch-coverage.js","sourceRoot":"","sources":["../../../src/commands/validate/arch-coverage.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * Whether the architecture a delta INTRODUCES is covered by a requirement that\n * will actually merge.\n *\n * Three findings, one question asked three ways: `archedge.uncovered` (a\n * scenario names the new edge — heuristic, warn-only), `c4.uncovered` (an\n * arch.spec.md requirement covers the new element or edge — mechanical), and\n * `covers.unknown` (the `Covers:` line resolves to something real). This is\n * where agent-built code cuts its corners — the outbox, the retries, the alerts\n * — because no business scenario was ever going to mention them.\n *\n * Split out of `validateFeature` because it is the only part of the feature\n * target that needs the LIVING landscape: everything else there reads the\n * feature directory alone. Keeping the lazy load and its two exemption sets in\n * one module is what stops \"new\" being decided differently in three places.\n */\nimport { existsSync } from \"node:fs\";\nimport { loadFile, type Elem, type LoadedDoc, type Rel } from \"../../core/c4/likec4.js\";\nimport { elementService, serviceResolver } from \"../../core/c4/resolve/service.js\";\nimport { type PathableService } from \"../../core/kernel/ids/service.js\";\nimport { landscapePath as landscapeFile } from \"../../core/repo/paths.js\";\nimport { locateServicePaths } from \"../../core/repo/service-target.js\";\nimport { enumeratedServiceIds } from \"../../core/repo/service-target.js\";\nimport { type Finding } from \"../../core/vocabulary/report.js\";\nimport { type Requirement } from \"../../core/document/spec.js\";\nimport { coversEdge, coversElement, type CoverageScope } from \"../../core/c4/arch.js\";\nimport { readHealth } from \"../../core/vocabulary/health.js\";\nimport { FleetContext } from \"../../core/fleet-context.js\";\nimport { ACTOR_KINDS, EXTERNAL_TAG } from \"../../core/vocabulary/maturity.js\";\nimport { coversEntries, coversUnknownFindings } from \"./checks/requirements.js\";\nimport type { DocsDir } from \"../../core/kernel/ids/dirs.js\";\n\n/** One feature delta's architecture, and everything needed to decide what in it is new. */\nexport interface DeltaArch {\n docsDir: DocsDir;\n /** Every element the delta declares — the resolver's index, and `coversEdge`'s scope. */\n elements: Elem[];\n /** Every relationship the delta declares. */\n relationships: Rel[];\n /** The delta's own additions: elements and edges carrying the feature id as a tag. */\n taggedEls: Elem[];\n taggedRels: Rel[];\n /** Each addressed service's arch.spec.md delta, in enumeration order. */\n archDeltas: Array<{ service: PathableService; reqs: Requirement[] }>;\n /**\n * Every service the feature's `specs/` addresses — not just the ones with an\n * arch delta. The resolver's fleet set unions these in because a service the\n * feature INTRODUCES has no `services/` directory yet, and without its name\n * an edge into that service's own modelled container resolved to the\n * container's title — a phantom the fleet half alone cannot rule out.\n */\n featureServices: PathableService[];\n /** Every scenario the feature's deltas carry, lowercased — the heuristic's haystack. */\n scenarioText: string;\n /** The living landscape under --all; undefined means \"load it if you need it\", null \"there is none\". */\n preloadedLand?: LoadedDoc | null;\n fleet?: FleetContext;\n}\n\nexport async function deltaArchCoverage(delta: DeltaArch): Promise<Finding[]> {\n const {\n docsDir,\n elements,\n relationships,\n taggedEls,\n taggedRels,\n archDeltas,\n featureServices,\n scenarioText,\n preloadedLand,\n fleet,\n } = delta;\n const findings: Finding[] = [];\n\n // The delta's own element→service resolver, built once for the whole feature.\n // `serviceOf` is a one-shot wrapper that rebuilds its index on every call, and\n // the two loops below ask it up to five times per tagged edge — so a delta\n // over a large model paid for one Map of every element per question asked.\n // The enumerated fleet — `services/` plus the feature's own `specs/` names,\n // where a service it introduces lives — rides into it, and into `baseSvcOf`\n // below, which MUST resolve the same way or the already-living exemption\n // keys never match: so an edge drawn into a modelled container\n // (`payment.api`) grades against the service that owns it instead of a\n // service called \"api\" that has never existed. An unenumerable services/\n // degrades to the feature's own names, exactly what every caller got before\n // the set existed.\n const known: ReadonlySet<string> = new Set<string>([\n ...(await enumeratedServiceIds(docsDir, fleet)),\n ...featureServices,\n ]);\n const svcOf = serviceResolver(elements, known);\n\n // Arch-edge coverage (heuristic, warn-only): each new tagged edge should be named by a scenario.\n for (const r of taggedRels) {\n const target = svcOf(r.target);\n const covered = edgeCovered(target, r.title, scenarioText);\n findings.push({\n severity: covered ? \"ok\" : \"warn\",\n code: covered ? \"archedge.covered\" : \"archedge.uncovered\",\n subject: target,\n message: `${svcOf(r.source)} → ${target} \"${r.title ?? \"\"}\"${covered ? \"\" : \" — no scenario names it\"}`,\n text: { indent: 4, header: \"arch-edge coverage (heuristic):\" },\n });\n }\n\n // The architecture spec axis, feature scope — the mechanical counterpart of\n // the heuristic above. Every NEW tagged element and edge in the delta wants a\n // `Covers:` line in one of the feature's arch.spec.md deltas (c4.uncovered):\n // this is where agent-built code cuts its corners — the outbox, the retries,\n // the alerts — because no business scenario was ever going to mention them.\n // Grouping-only elements take the same exemptions the fleet map applies\n // (person kinds, #external) — one set, in checks/vocabulary.ts, so \"not a\n // service\" cannot mean two things. Warnings, never archive gates; `--strict`\n // escalates.\n //\n // Only requirements the archive will MERGE grant coverage here. In a delta,\n // BASE means \"the living state, quoted\": it merges nothing, emits no\n // .feature, and yields no scenario.tested claim — so a Covers: line under a\n // plain `## Requirements` quote is an obligation that ships nowhere, and\n // counting it silenced c4.uncovered for free. (The service-scope pass keeps\n // the unfiltered call: a LIVING spec is legitimately all BASE.)\n const activeCovers = archDeltas.flatMap(({ reqs }) =>\n coversEntries(reqs.filter((r) => r.kind === \"ADDED\" || r.kind === \"MODIFIED\")),\n );\n\n // What the living landscape ALREADY holds. A delta has to re-declare the\n // elements its new edges attach to, and authors tag those re-declarations\n // along with everything else — so a requirements-only feature that touches an\n // existing service was told to write `Covers:` lines for architecture it is\n // not adding. c4.uncovered is an obligation on NEW architecture; an element\n // the living landscape already resolves is not new, whatever the tag says.\n // Loaded lazily: a delta with nothing tagged never pays for the parse.\n let living: LoadedDoc | null | undefined = preloadedLand;\n const livingLandscape = async (): Promise<LoadedDoc | null> => {\n if (living === undefined) {\n const lp = landscapeFile(docsDir);\n living = existsSync(lp)\n ? fleet === undefined\n ? await loadFile(lp)\n : await fleet.loadLikeC4(lp)\n : null;\n }\n return living;\n };\n const alreadyLiving = async (): Promise<LoadedDoc | null> => {\n if (taggedEls.length === 0 && taggedRels.length === 0) return null;\n const doc = await livingLandscape();\n return doc !== null && doc.errors.length === 0 ? doc : null;\n };\n const base = await alreadyLiving();\n const baseSvcOf = base === null ? null : serviceResolver(base.elements, known);\n const baseIds = new Set(base?.elements.map((e) => e.id) ?? []);\n const baseServices = new Set((base?.elements ?? []).map(elementService));\n // How a service→service pair is keyed, spelled ONCE. The two sides used to\n // join with different separators — a NUL where the set was built, a space\n // where it was queried — so the exemption never matched anything, and every\n // edge a delta re-declares verbatim (which it must, to attach anything to\n // it) was reported as new architecture nobody covers. The join is structural\n // rather than a separator character: the last one was a NUL, and a raw NUL in\n // a template literal makes the source read as binary to `file` and invisible\n // to `grep` — which is how a one-character mismatch survived review.\n const edgeKey = (source: string, target: string): string => JSON.stringify([source, target]);\n const baseEdges = new Set(\n (base?.relationships ?? []).map((r) => edgeKey(baseSvcOf!(r.source), baseSvcOf!(r.target))),\n );\n\n for (const e of taggedEls) {\n if (ACTOR_KINDS.has(e.kind.toLowerCase()) || e.tags.includes(EXTERNAL_TAG)) continue;\n if (baseIds.has(e.id) || baseServices.has(elementService(e))) continue;\n if (activeCovers.some((c) => coversElement(c, e))) continue;\n findings.push({\n severity: \"warn\",\n code: \"c4.uncovered\",\n subject: elementService(e),\n message: `delta adds '${e.title}' (${e.id}) but no arch requirement covers it — add 'Covers: ${e.id}' to a specs/<svc>/arch.spec.md delta, or its architectural obligations ship unchecked`,\n });\n }\n for (const r of taggedRels) {\n if (baseEdges.has(edgeKey(svcOf(r.source), svcOf(r.target)))) continue;\n if (activeCovers.some((c) => coversEdge(c, r, elements, known))) continue;\n findings.push({\n severity: \"warn\",\n code: \"c4.uncovered\",\n subject: svcOf(r.target),\n message: `delta adds edge ${svcOf(r.source)} → ${svcOf(r.target)} (\"${r.title ?? \"\"}\") but no arch requirement covers it — add 'Covers: ${r.source} -> ${r.target}' to a specs/<svc>/arch.spec.md delta`,\n });\n }\n\n // covers.unknown, feature scope. Resolution looks at the delta itself, the\n // living landscape, the service's own model and its health.yaml — a delta's\n // arch requirement may cover an element it adds, one that already exists, or\n // an alert the service declares. The landscape and each model are loaded\n // lazily, and only when an entry fails against what is already in hand: the\n // clean path never pays for a workspace spin.\n if (archDeltas.some(({ reqs }) => coversEntries(reqs).length > 0)) {\n const land = await livingLandscape();\n const landParses = land !== null && land.errors.length === 0 ? land : null;\n const baseElements = [...elements, ...(landParses?.elements ?? [])];\n const baseRels = [...relationships, ...(landParses?.relationships ?? [])];\n for (const { service: svc, reqs } of archDeltas) {\n // An unreadable living health.yaml mutes the alert:/sli: entries here\n // exactly as in service scope — the health.invalid finding itself\n // belongs to the service target, which owns the file's diagnosis.\n const health = await readHealth((await locateServicePaths(docsDir, svc, fleet)).health);\n let scope: CoverageScope = { elements: baseElements, relationships: baseRels, health: health.ids, known };\n const unresolved = coversUnknownFindings(reqs, { where: `${svc}: arch.spec.md`, subject: svc }, scope, health.unreadable);\n if (unresolved.length > 0) {\n const modelPath = (await locateServicePaths(docsDir, svc, fleet)).model;\n const model = existsSync(modelPath)\n ? fleet === undefined\n ? await loadFile(modelPath)\n : await fleet.loadLikeC4(modelPath)\n : null;\n if (model !== null && model.errors.length === 0) {\n scope = {\n elements: [...baseElements, ...model.elements],\n relationships: [...baseRels, ...model.relationships],\n health: health.ids,\n known,\n };\n }\n findings.push(...coversUnknownFindings(reqs, { where: `${svc}: arch.spec.md`, subject: svc }, scope, health.unreadable));\n }\n }\n }\n\n return findings;\n}\n\n/** Heuristic: an edge is \"covered\" if a scenario names the target or a keyword from the edge title. */\nfunction edgeCovered(target: string, title: string | undefined, scenarioText: string): boolean {\n if (scenarioText.includes(target.toLowerCase())) return true;\n for (const token of (title ?? \"\").split(/[^A-Za-z0-9]+/)) {\n if (token.length >= 5 && scenarioText.includes(token.toLowerCase())) return true;\n }\n return false;\n}\n"]}
1
+ {"version":3,"file":"arch-coverage.js","sourceRoot":"","sources":["../../../src/commands/validate/arch-coverage.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * Whether the architecture a delta INTRODUCES is covered by a requirement that\n * will actually merge.\n *\n * Three findings, one question asked three ways: `archedge.uncovered` (a\n * scenario names the new edge — heuristic, warn-only), `c4.uncovered` (an\n * arch.spec.md requirement covers the new element or edge — mechanical), and\n * `covers.unknown` (the `Covers:` line resolves to something real). This is\n * where agent-built code cuts its corners — the outbox, the retries, the alerts\n * — because no business scenario was ever going to mention them.\n *\n * Split out of `validateFeature` because it is the only part of the feature\n * target that needs the LIVING landscape: everything else there reads the\n * feature directory alone. Keeping the lazy load and its two exemption sets in\n * one module is what stops \"new\" being decided differently in three places.\n */\nimport { existsSync } from \"node:fs\";\nimport { loadFile, type Elem, type LoadedDoc, type Rel } from \"../../core/c4/likec4.js\";\nimport { elementService, serviceResolver } from \"../../core/c4/resolve/service.js\";\nimport { type PathableService } from \"../../core/kernel/ids/service.js\";\nimport { landscapePath as landscapeFile } from \"../../core/repo/paths.js\";\nimport { locateServicePaths } from \"../../core/repo/service-target.js\";\nimport { enumeratedServiceIds } from \"../../core/repo/service-target.js\";\nimport { type Finding } from \"../../core/vocabulary/report.js\";\nimport { type Requirement } from \"../../core/document/spec.js\";\nimport { coversEdge, coversElement, type CoverageScope } from \"../../core/c4/arch.js\";\nimport { readHealth } from \"../../core/vocabulary/health.js\";\nimport { FleetContext } from \"../../core/fleet-context.js\";\nimport { ACTOR_KINDS, EXTERNAL_TAG } from \"../../core/vocabulary/maturity.js\";\nimport { coversEntries, coversUnknownFindings } from \"./checks/requirements.js\";\nimport type { DocsDir } from \"../../core/kernel/ids/dirs.js\";\n\n/** One feature delta's architecture, and everything needed to decide what in it is new. */\nexport interface DeltaArch {\n docsDir: DocsDir;\n /** Every element the delta declares — the resolver's index, and `coversEdge`'s scope. */\n elements: Elem[];\n /** Every relationship the delta declares. */\n relationships: Rel[];\n /** The delta's own additions: elements and edges carrying the feature id as a tag. */\n taggedEls: Elem[];\n taggedRels: Rel[];\n /** Each addressed service's arch.spec.md delta, in enumeration order. */\n archDeltas: Array<{ service: PathableService; reqs: Requirement[] }>;\n /**\n * Every service the feature's `specs/` addresses — not just the ones with an\n * arch delta. The resolver's fleet set unions these in because a service the\n * feature INTRODUCES has no `services/` directory yet, and without its name\n * an edge into that service's own modelled container resolved to the\n * container's title — a phantom the fleet half alone cannot rule out.\n */\n featureServices: PathableService[];\n /** Every scenario the feature's deltas carry, lowercased — the heuristic's haystack. */\n scenarioText: string;\n /** The living landscape under --all; undefined means \"load it if you need it\", null \"there is none\". */\n preloadedLand?: LoadedDoc | null;\n fleet?: FleetContext;\n}\n\nexport async function deltaArchCoverage(delta: DeltaArch): Promise<Finding[]> {\n const {\n docsDir,\n elements,\n relationships,\n taggedEls,\n taggedRels,\n archDeltas,\n featureServices,\n scenarioText,\n preloadedLand,\n fleet,\n } = delta;\n const findings: Finding[] = [];\n\n // The delta's own element→service resolver, built once for the whole feature.\n // `serviceOf` is a one-shot wrapper that rebuilds its index on every call, and\n // the two loops below ask it up to five times per tagged edge — so a delta\n // over a large model paid for one Map of every element per question asked.\n // The enumerated fleet — `services/` plus the feature's own `specs/` names,\n // where a service it introduces lives — rides into it, and into `baseSvcOf`\n // below, which MUST resolve the same way or the already-living exemption\n // keys never match: so an edge drawn into a modelled container\n // (`payment.api`) grades against the service that owns it instead of a\n // service called \"api\" that has never existed. An unenumerable services/\n // degrades to the feature's own names, exactly what every caller got before\n // the set existed.\n const known: ReadonlySet<string> = new Set<string>([\n ...(await enumeratedServiceIds(docsDir, fleet)),\n ...featureServices,\n ]);\n const svcOf = serviceResolver(elements, known);\n\n // Arch-edge coverage (heuristic, warn-only): each new tagged edge should be named by a scenario.\n for (const r of taggedRels) {\n const target = svcOf(r.target);\n const covered = edgeCovered(target, r.title, scenarioText);\n findings.push({\n severity: covered ? \"ok\" : \"warn\",\n code: covered ? \"archedge.covered\" : \"archedge.uncovered\",\n subject: target,\n message: `${svcOf(r.source)} → ${target} \"${r.title ?? \"\"}\"${covered ? \"\" : \" — no scenario names it\"}`,\n text: { indent: 4, header: \"arch-edge coverage (heuristic):\" },\n });\n }\n\n // The architecture spec axis, feature scope — the mechanical counterpart of\n // the heuristic above. Every NEW tagged element and edge in the delta wants a\n // `Covers:` line in one of the feature's arch.spec.md deltas (c4.uncovered):\n // this is where agent-built code cuts its corners — the outbox, the retries,\n // the alerts — because no business scenario was ever going to mention them.\n // Grouping-only elements take the same exemptions the fleet map applies\n // (person kinds, #external) — one set, in checks/vocabulary.ts, so \"not a\n // service\" cannot mean two things. Warnings, never archive gates; `--strict`\n // escalates.\n //\n // Only requirements the archive will MERGE grant coverage here. In a delta,\n // BASE means \"the living state, quoted\": it merges nothing, emits no\n // .feature, and yields no scenario.tested claim — so a Covers: line under a\n // plain `## Requirements` quote is an obligation that ships nowhere, and\n // counting it silenced c4.uncovered for free. (The service-scope pass keeps\n // the unfiltered call: a LIVING spec is legitimately all BASE.)\n const activeCovers = archDeltas.flatMap(({ reqs }) =>\n coversEntries(reqs.filter((r) => r.kind === \"ADDED\" || r.kind === \"MODIFIED\")),\n );\n\n // What the living landscape ALREADY holds. A delta has to re-declare the\n // elements its new edges attach to, and authors tag those re-declarations\n // along with everything else — so a requirements-only feature that touches an\n // existing service was told to write `Covers:` lines for architecture it is\n // not adding. c4.uncovered is an obligation on NEW architecture; an element\n // the living landscape already resolves is not new, whatever the tag says.\n // Loaded lazily: a delta with nothing tagged never pays for the parse.\n let living: LoadedDoc | null | undefined = preloadedLand;\n const livingLandscape = async (): Promise<LoadedDoc | null> => {\n if (living === undefined) {\n const lp = landscapeFile(docsDir);\n living = existsSync(lp)\n ? fleet === undefined\n ? await loadFile(lp)\n : await fleet.loadLikeC4(lp)\n : null;\n }\n return living;\n };\n const alreadyLiving = async (): Promise<LoadedDoc | null> => {\n if (taggedEls.length === 0 && taggedRels.length === 0) return null;\n const doc = await livingLandscape();\n return doc !== null && doc.errors.length === 0 ? doc : null;\n };\n const base = await alreadyLiving();\n const baseSvcOf = base === null ? null : serviceResolver(base.elements, known);\n const baseIds = new Set(base?.elements.map((e) => e.id) ?? []);\n const baseServices = new Set((base?.elements ?? []).map(elementService));\n // How a service→service pair is keyed, spelled ONCE. The two sides used to\n // join with different separators — a NUL where the set was built, a space\n // where it was queried — so the exemption never matched anything, and every\n // edge a delta re-declares verbatim (which it must, to attach anything to\n // it) was reported as new architecture nobody covers. The join is structural\n // rather than a separator character: the last one was a NUL, and a raw NUL in\n // a template literal makes the source read as binary to `file` and invisible\n // to `grep` — which is how a one-character mismatch survived review.\n const edgeKey = (source: string, target: string): string => JSON.stringify([source, target]);\n const baseEdges = new Set(\n (base?.relationships ?? []).map((r) => edgeKey(baseSvcOf!(r.source), baseSvcOf!(r.target))),\n );\n\n for (const e of taggedEls) {\n if (ACTOR_KINDS.has(e.kind.toLowerCase()) || e.tags.includes(EXTERNAL_TAG)) continue;\n if (baseIds.has(e.id) || baseServices.has(elementService(e))) continue;\n if (activeCovers.some((c) => coversElement(c, e))) continue;\n findings.push({\n severity: \"warn\",\n code: \"c4.uncovered\",\n subject: elementService(e),\n message: `delta adds '${e.title}' (${e.id}) but no arch requirement covers it — add 'Covers: ${e.id}' to a specs/<svc>/arch.spec.md delta, or its architectural obligations ship unchecked`,\n });\n }\n for (const r of taggedRels) {\n if (baseEdges.has(edgeKey(svcOf(r.source), svcOf(r.target)))) continue;\n if (activeCovers.some((c) => coversEdge(c, r, elements, known))) continue;\n findings.push({\n severity: \"warn\",\n code: \"c4.uncovered\",\n subject: svcOf(r.target),\n message: `delta adds edge ${svcOf(r.source)} → ${svcOf(r.target)} (\"${r.title ?? \"\"}\") but no arch requirement covers it — add 'Covers: ${r.source} -> ${r.target}' to a specs/<svc>/arch.spec.md delta`,\n });\n }\n\n // covers.unknown, feature scope. Resolution looks at the delta itself, the\n // living landscape, the service's own model and its health.yaml — a delta's\n // arch requirement may cover an element it adds, one that already exists, or\n // an alert the service declares. The landscape and each model are loaded\n // lazily, and only when an entry fails against what is already in hand: the\n // clean path never pays for a workspace spin.\n if (archDeltas.some(({ reqs }) => coversEntries(reqs).length > 0)) {\n const land = await livingLandscape();\n const landParses = land !== null && land.errors.length === 0 ? land : null;\n const baseElements = [...elements, ...(landParses?.elements ?? [])];\n const baseRels = [...relationships, ...(landParses?.relationships ?? [])];\n // A feature never brings topology through `delta.likec4` — a `deployment { }`\n // block in one is refused by the merge — so the living map is the whole\n // scope for a `node:` entry in a delta, exactly as it is in service scope.\n const baseDeployment = landParses?.deployment;\n for (const { service: svc, reqs } of archDeltas) {\n // An unreadable living health.yaml mutes the alert:/sli: entries here\n // exactly as in service scope — the health.invalid finding itself\n // belongs to the service target, which owns the file's diagnosis.\n const health = await readHealth((await locateServicePaths(docsDir, svc, fleet)).health);\n let scope: CoverageScope = {\n elements: baseElements,\n relationships: baseRels,\n ...(baseDeployment === undefined ? {} : { deployment: baseDeployment }),\n health: health.ids,\n known,\n };\n const unresolved = coversUnknownFindings(reqs, { where: `${svc}: arch.spec.md`, subject: svc }, scope, health.unreadable);\n if (unresolved.length > 0) {\n const modelPath = (await locateServicePaths(docsDir, svc, fleet)).model;\n const model = existsSync(modelPath)\n ? fleet === undefined\n ? await loadFile(modelPath)\n : await fleet.loadLikeC4(modelPath)\n : null;\n if (model !== null && model.errors.length === 0) {\n scope = {\n elements: [...baseElements, ...model.elements],\n relationships: [...baseRels, ...model.relationships],\n health: health.ids,\n known,\n };\n }\n findings.push(...coversUnknownFindings(reqs, { where: `${svc}: arch.spec.md`, subject: svc }, scope, health.unreadable));\n }\n }\n }\n\n return findings;\n}\n\n/** Heuristic: an edge is \"covered\" if a scenario names the target or a keyword from the edge title. */\nfunction edgeCovered(target: string, title: string | undefined, scenarioText: string): boolean {\n if (scenarioText.includes(target.toLowerCase())) return true;\n for (const token of (title ?? \"\").split(/[^A-Za-z0-9]+/)) {\n if (token.length >= 5 && scenarioText.includes(token.toLowerCase())) return true;\n }\n return false;\n}\n"]}
@@ -28,7 +28,9 @@
28
28
  import { existsSync } from "node:fs";
29
29
  import { join } from "node:path";
30
30
  import {} from "../../../core/c4/likec4.js";
31
- import { closeIds, coversEdge, coversElement } from "../../../core/c4/arch.js";
31
+ import { closeIds, coversDeployEdge, coversDeployNode, coversEdge, coversElement, } from "../../../core/c4/arch.js";
32
+ import { NO_DEPLOYMENT } from "../../../core/c4/parsed/deployment.js";
33
+ import { taggedDeployment } from "../../../core/deployment/objects.js";
32
34
  import { elementService } from "../../../core/c4/resolve/service.js";
33
35
  import { OBLIGATION_TAG_PREFIX } from "../../../core/obligations/obligations.js";
34
36
  import { servicePathsAt } from "../../../core/repo/paths.js";
@@ -76,7 +78,13 @@ export async function obligationFindings(check) {
76
78
  // together because every question below treats them alike: an obligation
77
79
  // applies to a thing on the map, and whether that thing is a box or an arrow
78
80
  // changes only how the message spells it and how `Covers:` matches it.
79
- const tagged = taggedObjects(check.land);
81
+ // The enumerated fleet rides in for the deployment half of the walk: an
82
+ // instance deploys a CONTAINER more often than a whole system, and resolving
83
+ // which service owns one needs the ancestor walk `serviceResolver` performs.
84
+ // It is the same set `uncoveredFindings` resolves edge endpoints with, or the
85
+ // message would name one service and the coverage check count another.
86
+ const known = new Set(check.services.map((s) => s.id));
87
+ const tagged = taggedObjects(check.land, known);
80
88
  const applied = new Set(tagged.map((t) => t.obligation));
81
89
  for (const id of [...applied].sort()) {
82
90
  if (vocabulary.byId.has(id))
@@ -123,7 +131,7 @@ export async function obligationFindings(check) {
123
131
  * resolves to `obligation.unknown` in every fleet that did not declare an empty
124
132
  * id, which is the honest answer.
125
133
  */
126
- function taggedObjects(land) {
134
+ function taggedObjects(land, known) {
127
135
  const out = [];
128
136
  for (const element of land.elements) {
129
137
  for (const tag of element.tags.filter((t) => t.startsWith(OBLIGATION_TAG_PREFIX))) {
@@ -145,6 +153,20 @@ function taggedObjects(land) {
145
153
  });
146
154
  }
147
155
  }
156
+ // The topology, walked by the same rule and for the reason the rule exists: a
157
+ // tag is a claim about the object it sits on, and which MODEL that object
158
+ // lives in is not something a reader of the tag can see. Until this walk the
159
+ // same undeclared tag was an error on a container and silence on a
160
+ // datacenter — fail-open, and the case FEAT-1's ARCH-LOAM-DEPLOY-TAGGED was
161
+ // written about. A fleet that draws no topology contributes nothing here.
162
+ for (const d of taggedDeployment(land.deployment ?? NO_DEPLOYMENT, land.elements, known, OBLIGATION_TAG_PREFIX)) {
163
+ out.push({
164
+ obligation: d.obligation,
165
+ deployed: d,
166
+ where: d.where,
167
+ ...(d.service === undefined ? {} : { subject: d.service }),
168
+ });
169
+ }
148
170
  return out;
149
171
  }
150
172
  /**
@@ -167,17 +189,29 @@ async function uncoveredFindings(check, tagged) {
167
189
  const covers = await livingCovers(check);
168
190
  const elements = check.land.elements;
169
191
  const known = new Set(check.services.map((s) => s.id));
192
+ const covered = (t) => {
193
+ if (t.element !== undefined)
194
+ return covers.some((c) => coversElement(c, t.element));
195
+ if (t.edge !== undefined)
196
+ return covers.some((c) => coversEdge(c, t.edge, elements, known));
197
+ const d = t.deployed;
198
+ return d.edge === undefined
199
+ ? covers.some((c) => coversDeployNode(c, d.id))
200
+ : covers.some((c) => coversDeployEdge(c, d.edge.source, d.edge.target));
201
+ };
170
202
  return tagged
171
- .filter((t) => t.element !== undefined
172
- ? !covers.some((c) => coversElement(c, t.element))
173
- : !covers.some((c) => coversEdge(c, t.edge, elements, known)))
203
+ .filter((t) => !covered(t))
174
204
  .map((t) => ({
175
205
  severity: "warn",
176
206
  code: "obligation.uncovered",
177
- subject: t.subject,
207
+ ...(t.subject === undefined ? {} : { subject: t.subject }),
178
208
  message: `${t.where} carries #${OBLIGATION_TAG_PREFIX}${t.obligation} and no living arch requirement covers it — ` +
179
- `the obligation is placed and nothing says it is met. Write the requirement in ${t.subject}'s arch.spec.md with ` +
180
- `\`Covers: ${t.where}\` and a scenario that proves it, or drop the tag if the decision no longer applies here.`,
209
+ `the obligation is placed and nothing says it is met. Write the requirement in ` +
210
+ // A region, a datacenter or a cluster is owned by no service, so the
211
+ // sentence stays general rather than naming a team at random. Every
212
+ // other object here has a real owner and keeps the actionable form.
213
+ `${t.subject === undefined ? "the arch.spec.md of whichever service answers for this topology" : `${t.subject}'s arch.spec.md`}` +
214
+ ` with \`Covers: ${t.where}\` and a scenario that proves it, or drop the tag if the decision no longer applies here.`,
181
215
  }));
182
216
  }
183
217
  /**
@@ -1 +1 @@
1
- {"version":3,"file":"obligations.js","sourceRoot":"","sources":["../../../../src/commands/validate/fleet/obligations.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * The architectural obligation axis, graded against the fleet it is about.\n *\n * `core/obligations/obligations.ts` states what the axis IS and why it has this\n * shape. This module is the four questions asked of a fleet that has one, and\n * the order they are asked in — the same division `checks/fleet-shape.ts` keeps\n * for the authorization and capability vocabularies, which this sits beside on\n * the `landscape` target for their reason: an obligation crosses services by\n * construction, so no service target can answer for it.\n *\n * THE FOUR ARE TWO PAIRS, and each pair is one join read in both directions.\n * A tag nothing declares is an ERROR (`obligation.unknown`) and a declaration\n * nothing tags is a WARNING (`obligation.unapplied`) — exactly the asymmetry\n * `permissions.unknown`/`permissions.unenforced` already carry, and for the\n * same reason: a typo in a tag reads like a rule, while a word nobody has\n * adopted yet is an honest state. Then a tagged object no requirement covers is\n * a WARNING (`obligation.uncovered`) and an `adr:` naming no file is an ERROR\n * (`obligation.adr-missing`): the team's work being outstanding is normal, and\n * a pointer at a decision nobody can read is not.\n *\n * `obligation.uncovered` IS THE PREREQUISITE THE ROADMAP NAMED. `c4.uncovered`\n * has always been able to say \"this architecture object owes a requirement\",\n * but only about a NEW tagged element in a feature's `delta.likec4` — never\n * about the map the fleet actually runs on. This asks the same question of the\n * LIVING landscape, against the union of every service's living `arch.spec.md`\n * `Covers:` lines, which is the index that did not exist before.\n */\nimport { existsSync } from \"node:fs\";\nimport { join } from \"node:path\";\nimport { type Elem, type LoadedDoc, type Rel } from \"../../../core/c4/likec4.js\";\nimport { closeIds, coversEdge, coversElement, type CoversEntry } from \"../../../core/c4/arch.js\";\nimport { elementService } from \"../../../core/c4/resolve/service.js\";\nimport { OBLIGATION_TAG_PREFIX, type ObligationVocabulary } from \"../../../core/obligations/obligations.js\";\nimport { servicePathsAt } from \"../../../core/repo/paths.js\";\nimport { type ServiceEntry } from \"../../../core/repo/entries.js\";\nimport { type Finding } from \"../../../core/vocabulary/report.js\";\nimport { FleetContext } from \"../../../core/fleet-context.js\";\nimport { readRequirementsDocument, parseRequirements } from \"../../../core/document/parse.js\";\nimport { coversEntries } from \"../checks/requirements.js\";\nimport type { DocsDir } from \"../../../core/kernel/ids/dirs.js\";\n\nexport interface ObligationCheck {\n docsDir: DocsDir;\n vocabulary: ObligationVocabulary;\n /**\n * The living landscape, PARSED. Not nullable, and the absence is the point:\n * the landscape target returns before it reaches this call when the map is\n * missing or did not parse, so a run that could not read the map asks none of\n * the three questions below rather than answering them empty. \"Loam did not\n * look\" is never the same answer as \"there is nothing there\" — an unreadable\n * map graded as empty would report every declared obligation as applied\n * nowhere — and the suspension lives at the call site because that is where\n * the fact is known. It is also why the vocabulary half is a separate\n * function: those two verdicts must survive exactly those early returns.\n */\n land: LoadedDoc;\n services: ServiceEntry[];\n fleet?: FleetContext;\n}\n\n/**\n * The half that does not depend on the map: the file reads, and its `adr:`\n * pointers resolve.\n *\n * Separate from the map questions because the landscape target returns early\n * twice — an absent map, and one that did not parse — and a broken vocabulary\n * must still be named in exactly the runs where somebody is fixing something\n * else. The same placement `permissionFindings` and `capabilityFleetFindings`\n * already have, one level of precision finer.\n */\nexport function obligationVocabularyFindings(docsDir: DocsDir, vocabulary: ObligationVocabulary): Finding[] {\n if (!vocabulary.present) return [];\n if (vocabulary.invalid !== undefined) {\n // Reported ALONE, exactly as `permissions.invalid` and `capability.invalid`\n // are: every `#obl-` tag in the fleet resolves against this file, so grading\n // them on top of a broken vocabulary is a cascade, not a diagnosis.\n return [\n {\n severity: \"error\",\n code: \"obligation.invalid\",\n message:\n `landscape: architecture/obligations.yaml does not read as a vocabulary — ${vocabulary.invalid}. ` +\n \"Every `#obl-` tag on the fleet map resolves against this file, so none of them can be graded until it parses. \" +\n \"The shape is `obligations: {<id>: {description, adr}}`, ids spelled with letters, digits, `_` and `-` only.\",\n },\n ];\n }\n\n return adrFindings(docsDir, [...vocabulary.byId.values()]);\n}\n\n/** The three questions that need the map, suspended entirely when it could not be read. */\nexport async function obligationFindings(check: ObligationCheck): Promise<Finding[]> {\n const { vocabulary } = check;\n if (!vocabulary.present || vocabulary.invalid !== undefined) return [];\n const findings: Finding[] = [];\n\n // Which objects carry which obligation. Elements and edges are collected\n // together because every question below treats them alike: an obligation\n // applies to a thing on the map, and whether that thing is a box or an arrow\n // changes only how the message spells it and how `Covers:` matches it.\n const tagged = taggedObjects(check.land);\n const applied = new Set(tagged.map((t) => t.obligation));\n\n for (const id of [...applied].sort()) {\n if (vocabulary.byId.has(id)) continue;\n const close = closeIds(id, [...vocabulary.byId.keys()]);\n findings.push({\n severity: \"error\",\n code: \"obligation.unknown\",\n subject: id,\n message:\n `landscape: #${OBLIGATION_TAG_PREFIX}${id} tags ${count(tagged.filter((t) => t.obligation === id).length, \"object\")} on the fleet map, ` +\n `and architecture/obligations.yaml declares no obligation '${id}'` +\n (close.length > 0 ? ` — did you mean: ${close.join(\", \")}?` : \"\") +\n \". A tag that resolves to nothing reads exactly like a rule the fleet keeps: declare it, or fix the tag.\",\n details: tagged.filter((t) => t.obligation === id).map((t) => t.where),\n });\n }\n\n const unapplied = [...vocabulary.byId.keys()].filter((id) => !applied.has(id)).sort();\n if (unapplied.length > 0) {\n findings.push({\n severity: \"warn\",\n code: \"obligation.unapplied\",\n message:\n `landscape: ${unapplied.length} declared obligation(s) that no \\`#${OBLIGATION_TAG_PREFIX}\\` tag applies anywhere on the fleet map — ` +\n \"either a decision that was reversed and left its word behind, or one nobody has placed yet. \" +\n \"Tag the elements and edges it governs, or drop the declaration; a vocabulary is worth what cites it.\",\n details: unapplied,\n });\n }\n\n // ONLY A RESOLVED APPLICATION IS ASKED ABOUT COVERAGE. A tag naming no\n // declared obligation has already earned its error above, and \"no requirement\n // covers #obl-typo\" is not a second breach — it is the same one restated in\n // words that ask the reader to write a requirement for a rule that does not\n // exist. The use-case axis draws this line in the same place: only a resolved\n // `#cap-` claim keeps a promise.\n findings.push(...(await uncoveredFindings(check, tagged.filter((t) => vocabulary.byId.has(t.obligation)))));\n return findings;\n}\n\n/** One place an obligation is applied: which obligation, on what, and how a message names it. */\ninterface TaggedObject {\n obligation: string;\n /** The element, or the edge — exactly one is set. */\n element?: Elem;\n edge?: Rel;\n /** How the finding spells this object: an element id, or `source -> target`. */\n where: string;\n /** What the finding is filed under — the service that owns it, where one does. */\n subject: string;\n}\n\n/**\n * Every `#obl-` tag on the living map, one entry per (obligation, object) pair.\n *\n * A bare `#obl-` slugs to the empty string and is kept as an application on\n * purpose, exactly as a bare `#cap-` is kept as a claim: the prefix is the\n * author's opt-in however little follows it, and dropping it as unreserved\n * would leave an object that asked to be graded silently ungraded. It then\n * resolves to `obligation.unknown` in every fleet that did not declare an empty\n * id, which is the honest answer.\n */\nfunction taggedObjects(land: LoadedDoc): TaggedObject[] {\n const out: TaggedObject[] = [];\n for (const element of land.elements) {\n for (const tag of element.tags.filter((t) => t.startsWith(OBLIGATION_TAG_PREFIX))) {\n out.push({\n obligation: tag.slice(OBLIGATION_TAG_PREFIX.length),\n element,\n where: element.id,\n subject: elementService(element),\n });\n }\n }\n for (const edge of land.relationships) {\n for (const tag of edge.tags.filter((t) => t.startsWith(OBLIGATION_TAG_PREFIX))) {\n out.push({\n obligation: tag.slice(OBLIGATION_TAG_PREFIX.length),\n edge,\n where: `${edge.source} -> ${edge.target}`,\n subject: edge.target,\n });\n }\n }\n return out;\n}\n\n/**\n * `obligation.uncovered` — a tagged object no living arch requirement covers.\n *\n * The index is the union of EVERY service's living `arch.spec.md` `Covers:`\n * lines, because an obligation is placed by the architect on the map and met by\n * whichever team owns the thing it sits on — and a per-service index would\n * report an edge as uncovered because the requirement that covers it lives in\n * the service at the other end. `coversEntries` already drops REMOVED\n * requirements: a retired requirement covers nothing.\n *\n * ONE FINDING PER TAGGED OBJECT, subject = the service that owns it, so a\n * fleet-wide report reads as a worklist per team rather than as one line naming\n * forty edges.\n */\nasync function uncoveredFindings(check: ObligationCheck, tagged: TaggedObject[]): Promise<Finding[]> {\n if (tagged.length === 0) return [];\n const covers = await livingCovers(check);\n const elements = check.land.elements;\n const known = new Set(check.services.map((s) => s.id));\n return tagged\n .filter((t) =>\n t.element !== undefined\n ? !covers.some((c) => coversElement(c, t.element!))\n : !covers.some((c) => coversEdge(c, t.edge!, elements, known)),\n )\n .map((t) => ({\n severity: \"warn\" as const,\n code: \"obligation.uncovered\",\n subject: t.subject,\n message:\n `${t.where} carries #${OBLIGATION_TAG_PREFIX}${t.obligation} and no living arch requirement covers it — ` +\n `the obligation is placed and nothing says it is met. Write the requirement in ${t.subject}'s arch.spec.md with ` +\n `\\`Covers: ${t.where}\\` and a scenario that proves it, or drop the tag if the decision no longer applies here.`,\n }));\n}\n\n/**\n * Every `Covers:` entry the LIVING fleet writes, across both requirement\n * documents of every service.\n *\n * Existence is checked for both files and for the same reason\n * `permissionFindings` checks it: `arch.spec.md` is optional and most of a\n * legacy fleet has none, while `spec.md` is missing on a half-adopted service —\n * and `FleetContext.readRequirements` throws ENOENT, which surfaces as\n * `repository-unavailable` and takes the whole `--all` run down. Both axes are\n * read, not only the architecture one: `Covers:` is parsed everywhere for one\n * grammar's sake, and a fleet that wrote its outbox requirement in `spec.md`\n * has said the thing loam is asking about.\n */\nasync function livingCovers(check: ObligationCheck): Promise<CoversEntry[]> {\n const entries: CoversEntry[] = [];\n for (const service of check.services) {\n const paths = servicePathsAt(service.dir);\n for (const path of [paths.spec, paths.archSpec]) {\n if (!existsSync(path)) continue;\n const reqs =\n check.fleet === undefined\n ? parseRequirements(await readRequirementsDocument(path))\n : await check.fleet.readRequirements(path);\n entries.push(...coversEntries(reqs));\n }\n }\n return entries;\n}\n\n/**\n * `obligation.adr-missing` — an `adr:` path that names no file.\n *\n * The path is relative to the docs repo root rather than to\n * `architecture/obligations.yaml`, because that is how a reader of a fleet\n * document thinks about a fleet path, and because the value is a YAML field\n * rather than a markdown link — `link.unresolved` grades links written in prose,\n * and this one is written in a slot loam defined.\n */\nfunction adrFindings(docsDir: DocsDir, obligations: readonly { id: string; adr?: string }[]): Finding[] {\n return obligations\n .filter((o) => o.adr !== undefined && !existsSync(join(docsDir, o.adr)))\n .map((o) => ({\n severity: \"error\" as const,\n code: \"obligation.adr-missing\",\n subject: o.id,\n message:\n `landscape: obligation '${o.id}' names \\`adr: ${o.adr!}\\` and no such file exists — ` +\n \"the ADR is where the decision itself is written, so a pointer at nothing leaves every tag citing a rule nobody can read. \" +\n \"Fix the path (it is relative to the docs repo root), write the record, or drop the field.\",\n }));\n}\n\n/** `1 object` / `3 objects` — the plural spelled once, because three messages count things. */\nfunction count(n: number, noun: string): string {\n return `${n} ${noun}${n === 1 ? \"\" : \"s\"}`;\n}\n"]}
1
+ {"version":3,"file":"obligations.js","sourceRoot":"","sources":["../../../../src/commands/validate/fleet/obligations.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * The architectural obligation axis, graded against the fleet it is about.\n *\n * `core/obligations/obligations.ts` states what the axis IS and why it has this\n * shape. This module is the four questions asked of a fleet that has one, and\n * the order they are asked in — the same division `checks/fleet-shape.ts` keeps\n * for the authorization and capability vocabularies, which this sits beside on\n * the `landscape` target for their reason: an obligation crosses services by\n * construction, so no service target can answer for it.\n *\n * THE FOUR ARE TWO PAIRS, and each pair is one join read in both directions.\n * A tag nothing declares is an ERROR (`obligation.unknown`) and a declaration\n * nothing tags is a WARNING (`obligation.unapplied`) — exactly the asymmetry\n * `permissions.unknown`/`permissions.unenforced` already carry, and for the\n * same reason: a typo in a tag reads like a rule, while a word nobody has\n * adopted yet is an honest state. Then a tagged object no requirement covers is\n * a WARNING (`obligation.uncovered`) and an `adr:` naming no file is an ERROR\n * (`obligation.adr-missing`): the team's work being outstanding is normal, and\n * a pointer at a decision nobody can read is not.\n *\n * `obligation.uncovered` IS THE PREREQUISITE THE ROADMAP NAMED. `c4.uncovered`\n * has always been able to say \"this architecture object owes a requirement\",\n * but only about a NEW tagged element in a feature's `delta.likec4` — never\n * about the map the fleet actually runs on. This asks the same question of the\n * LIVING landscape, against the union of every service's living `arch.spec.md`\n * `Covers:` lines, which is the index that did not exist before.\n */\nimport { existsSync } from \"node:fs\";\nimport { join } from \"node:path\";\nimport { type Elem, type LoadedDoc, type Rel } from \"../../../core/c4/likec4.js\";\nimport {\n closeIds,\n coversDeployEdge,\n coversDeployNode,\n coversEdge,\n coversElement,\n type CoversEntry,\n} from \"../../../core/c4/arch.js\";\nimport { NO_DEPLOYMENT } from \"../../../core/c4/parsed/deployment.js\";\nimport { taggedDeployment, type TaggedDeployment } from \"../../../core/deployment/objects.js\";\nimport { elementService } from \"../../../core/c4/resolve/service.js\";\nimport { OBLIGATION_TAG_PREFIX, type ObligationVocabulary } from \"../../../core/obligations/obligations.js\";\nimport { servicePathsAt } from \"../../../core/repo/paths.js\";\nimport { type ServiceEntry } from \"../../../core/repo/entries.js\";\nimport { type Finding } from \"../../../core/vocabulary/report.js\";\nimport { FleetContext } from \"../../../core/fleet-context.js\";\nimport { readRequirementsDocument, parseRequirements } from \"../../../core/document/parse.js\";\nimport { coversEntries } from \"../checks/requirements.js\";\nimport type { DocsDir } from \"../../../core/kernel/ids/dirs.js\";\n\nexport interface ObligationCheck {\n docsDir: DocsDir;\n vocabulary: ObligationVocabulary;\n /**\n * The living landscape, PARSED. Not nullable, and the absence is the point:\n * the landscape target returns before it reaches this call when the map is\n * missing or did not parse, so a run that could not read the map asks none of\n * the three questions below rather than answering them empty. \"Loam did not\n * look\" is never the same answer as \"there is nothing there\" — an unreadable\n * map graded as empty would report every declared obligation as applied\n * nowhere — and the suspension lives at the call site because that is where\n * the fact is known. It is also why the vocabulary half is a separate\n * function: those two verdicts must survive exactly those early returns.\n */\n land: LoadedDoc;\n services: ServiceEntry[];\n fleet?: FleetContext;\n}\n\n/**\n * The half that does not depend on the map: the file reads, and its `adr:`\n * pointers resolve.\n *\n * Separate from the map questions because the landscape target returns early\n * twice — an absent map, and one that did not parse — and a broken vocabulary\n * must still be named in exactly the runs where somebody is fixing something\n * else. The same placement `permissionFindings` and `capabilityFleetFindings`\n * already have, one level of precision finer.\n */\nexport function obligationVocabularyFindings(docsDir: DocsDir, vocabulary: ObligationVocabulary): Finding[] {\n if (!vocabulary.present) return [];\n if (vocabulary.invalid !== undefined) {\n // Reported ALONE, exactly as `permissions.invalid` and `capability.invalid`\n // are: every `#obl-` tag in the fleet resolves against this file, so grading\n // them on top of a broken vocabulary is a cascade, not a diagnosis.\n return [\n {\n severity: \"error\",\n code: \"obligation.invalid\",\n message:\n `landscape: architecture/obligations.yaml does not read as a vocabulary — ${vocabulary.invalid}. ` +\n \"Every `#obl-` tag on the fleet map resolves against this file, so none of them can be graded until it parses. \" +\n \"The shape is `obligations: {<id>: {description, adr}}`, ids spelled with letters, digits, `_` and `-` only.\",\n },\n ];\n }\n\n return adrFindings(docsDir, [...vocabulary.byId.values()]);\n}\n\n/** The three questions that need the map, suspended entirely when it could not be read. */\nexport async function obligationFindings(check: ObligationCheck): Promise<Finding[]> {\n const { vocabulary } = check;\n if (!vocabulary.present || vocabulary.invalid !== undefined) return [];\n const findings: Finding[] = [];\n\n // Which objects carry which obligation. Elements and edges are collected\n // together because every question below treats them alike: an obligation\n // applies to a thing on the map, and whether that thing is a box or an arrow\n // changes only how the message spells it and how `Covers:` matches it.\n // The enumerated fleet rides in for the deployment half of the walk: an\n // instance deploys a CONTAINER more often than a whole system, and resolving\n // which service owns one needs the ancestor walk `serviceResolver` performs.\n // It is the same set `uncoveredFindings` resolves edge endpoints with, or the\n // message would name one service and the coverage check count another.\n const known = new Set(check.services.map((s) => s.id));\n const tagged = taggedObjects(check.land, known);\n const applied = new Set(tagged.map((t) => t.obligation));\n\n for (const id of [...applied].sort()) {\n if (vocabulary.byId.has(id)) continue;\n const close = closeIds(id, [...vocabulary.byId.keys()]);\n findings.push({\n severity: \"error\",\n code: \"obligation.unknown\",\n subject: id,\n message:\n `landscape: #${OBLIGATION_TAG_PREFIX}${id} tags ${count(tagged.filter((t) => t.obligation === id).length, \"object\")} on the fleet map, ` +\n `and architecture/obligations.yaml declares no obligation '${id}'` +\n (close.length > 0 ? ` — did you mean: ${close.join(\", \")}?` : \"\") +\n \". A tag that resolves to nothing reads exactly like a rule the fleet keeps: declare it, or fix the tag.\",\n details: tagged.filter((t) => t.obligation === id).map((t) => t.where),\n });\n }\n\n const unapplied = [...vocabulary.byId.keys()].filter((id) => !applied.has(id)).sort();\n if (unapplied.length > 0) {\n findings.push({\n severity: \"warn\",\n code: \"obligation.unapplied\",\n message:\n `landscape: ${unapplied.length} declared obligation(s) that no \\`#${OBLIGATION_TAG_PREFIX}\\` tag applies anywhere on the fleet map — ` +\n \"either a decision that was reversed and left its word behind, or one nobody has placed yet. \" +\n \"Tag the elements and edges it governs, or drop the declaration; a vocabulary is worth what cites it.\",\n details: unapplied,\n });\n }\n\n // ONLY A RESOLVED APPLICATION IS ASKED ABOUT COVERAGE. A tag naming no\n // declared obligation has already earned its error above, and \"no requirement\n // covers #obl-typo\" is not a second breach — it is the same one restated in\n // words that ask the reader to write a requirement for a rule that does not\n // exist. The use-case axis draws this line in the same place: only a resolved\n // `#cap-` claim keeps a promise.\n findings.push(...(await uncoveredFindings(check, tagged.filter((t) => vocabulary.byId.has(t.obligation)))));\n return findings;\n}\n\n/** One place an obligation is applied: which obligation, on what, and how a message names it. */\ninterface TaggedObject {\n obligation: string;\n /** The element, the edge, or the deployment object — exactly one is set. */\n element?: Elem;\n edge?: Rel;\n deployed?: TaggedDeployment;\n /**\n * How the finding spells this object, and how a `Covers:` line has to: an\n * element id, `source -> target`, or the `node:` forms.\n */\n where: string;\n /**\n * What the finding is filed under — the service that owns it, where one\n * does. ABSENT for a region, a datacenter or a cluster: nobody-s service\n * runs one, and naming a team that did not ask for the work is worse than\n * leaving the sentence general.\n */\n subject?: string;\n}\n\n/**\n * Every `#obl-` tag on the living map, one entry per (obligation, object) pair.\n *\n * A bare `#obl-` slugs to the empty string and is kept as an application on\n * purpose, exactly as a bare `#cap-` is kept as a claim: the prefix is the\n * author's opt-in however little follows it, and dropping it as unreserved\n * would leave an object that asked to be graded silently ungraded. It then\n * resolves to `obligation.unknown` in every fleet that did not declare an empty\n * id, which is the honest answer.\n */\nfunction taggedObjects(land: LoadedDoc, known: ReadonlySet<string>): TaggedObject[] {\n const out: TaggedObject[] = [];\n for (const element of land.elements) {\n for (const tag of element.tags.filter((t) => t.startsWith(OBLIGATION_TAG_PREFIX))) {\n out.push({\n obligation: tag.slice(OBLIGATION_TAG_PREFIX.length),\n element,\n where: element.id,\n subject: elementService(element),\n });\n }\n }\n for (const edge of land.relationships) {\n for (const tag of edge.tags.filter((t) => t.startsWith(OBLIGATION_TAG_PREFIX))) {\n out.push({\n obligation: tag.slice(OBLIGATION_TAG_PREFIX.length),\n edge,\n where: `${edge.source} -> ${edge.target}`,\n subject: edge.target,\n });\n }\n }\n // The topology, walked by the same rule and for the reason the rule exists: a\n // tag is a claim about the object it sits on, and which MODEL that object\n // lives in is not something a reader of the tag can see. Until this walk the\n // same undeclared tag was an error on a container and silence on a\n // datacenter — fail-open, and the case FEAT-1's ARCH-LOAM-DEPLOY-TAGGED was\n // written about. A fleet that draws no topology contributes nothing here.\n for (const d of taggedDeployment(land.deployment ?? NO_DEPLOYMENT, land.elements, known, OBLIGATION_TAG_PREFIX)) {\n out.push({\n obligation: d.obligation,\n deployed: d,\n where: d.where,\n ...(d.service === undefined ? {} : { subject: d.service }),\n });\n }\n return out;\n}\n\n/**\n * `obligation.uncovered` — a tagged object no living arch requirement covers.\n *\n * The index is the union of EVERY service's living `arch.spec.md` `Covers:`\n * lines, because an obligation is placed by the architect on the map and met by\n * whichever team owns the thing it sits on — and a per-service index would\n * report an edge as uncovered because the requirement that covers it lives in\n * the service at the other end. `coversEntries` already drops REMOVED\n * requirements: a retired requirement covers nothing.\n *\n * ONE FINDING PER TAGGED OBJECT, subject = the service that owns it, so a\n * fleet-wide report reads as a worklist per team rather than as one line naming\n * forty edges.\n */\nasync function uncoveredFindings(check: ObligationCheck, tagged: TaggedObject[]): Promise<Finding[]> {\n if (tagged.length === 0) return [];\n const covers = await livingCovers(check);\n const elements = check.land.elements;\n const known = new Set(check.services.map((s) => s.id));\n const covered = (t: TaggedObject): boolean => {\n if (t.element !== undefined) return covers.some((c) => coversElement(c, t.element!));\n if (t.edge !== undefined) return covers.some((c) => coversEdge(c, t.edge!, elements, known));\n const d = t.deployed!;\n return d.edge === undefined\n ? covers.some((c) => coversDeployNode(c, d.id!))\n : covers.some((c) => coversDeployEdge(c, d.edge!.source, d.edge!.target));\n };\n return tagged\n .filter((t) => !covered(t))\n .map((t) => ({\n severity: \"warn\" as const,\n code: \"obligation.uncovered\",\n ...(t.subject === undefined ? {} : { subject: t.subject }),\n message:\n `${t.where} carries #${OBLIGATION_TAG_PREFIX}${t.obligation} and no living arch requirement covers it — ` +\n `the obligation is placed and nothing says it is met. Write the requirement in ` +\n // A region, a datacenter or a cluster is owned by no service, so the\n // sentence stays general rather than naming a team at random. Every\n // other object here has a real owner and keeps the actionable form.\n `${t.subject === undefined ? \"the arch.spec.md of whichever service answers for this topology\" : `${t.subject}'s arch.spec.md`}` +\n ` with \\`Covers: ${t.where}\\` and a scenario that proves it, or drop the tag if the decision no longer applies here.`,\n }));\n}\n\n/**\n * Every `Covers:` entry the LIVING fleet writes, across both requirement\n * documents of every service.\n *\n * Existence is checked for both files and for the same reason\n * `permissionFindings` checks it: `arch.spec.md` is optional and most of a\n * legacy fleet has none, while `spec.md` is missing on a half-adopted service —\n * and `FleetContext.readRequirements` throws ENOENT, which surfaces as\n * `repository-unavailable` and takes the whole `--all` run down. Both axes are\n * read, not only the architecture one: `Covers:` is parsed everywhere for one\n * grammar's sake, and a fleet that wrote its outbox requirement in `spec.md`\n * has said the thing loam is asking about.\n */\nasync function livingCovers(check: ObligationCheck): Promise<CoversEntry[]> {\n const entries: CoversEntry[] = [];\n for (const service of check.services) {\n const paths = servicePathsAt(service.dir);\n for (const path of [paths.spec, paths.archSpec]) {\n if (!existsSync(path)) continue;\n const reqs =\n check.fleet === undefined\n ? parseRequirements(await readRequirementsDocument(path))\n : await check.fleet.readRequirements(path);\n entries.push(...coversEntries(reqs));\n }\n }\n return entries;\n}\n\n/**\n * `obligation.adr-missing` — an `adr:` path that names no file.\n *\n * The path is relative to the docs repo root rather than to\n * `architecture/obligations.yaml`, because that is how a reader of a fleet\n * document thinks about a fleet path, and because the value is a YAML field\n * rather than a markdown link — `link.unresolved` grades links written in prose,\n * and this one is written in a slot loam defined.\n */\nfunction adrFindings(docsDir: DocsDir, obligations: readonly { id: string; adr?: string }[]): Finding[] {\n return obligations\n .filter((o) => o.adr !== undefined && !existsSync(join(docsDir, o.adr)))\n .map((o) => ({\n severity: \"error\" as const,\n code: \"obligation.adr-missing\",\n subject: o.id,\n message:\n `landscape: obligation '${o.id}' names \\`adr: ${o.adr!}\\` and no such file exists — ` +\n \"the ADR is where the decision itself is written, so a pointer at nothing leaves every tag citing a rule nobody can read. \" +\n \"Fix the path (it is relative to the docs repo root), write the record, or drop the field.\",\n }));\n}\n\n/** `1 object` / `3 objects` — the plural spelled once, because three messages count things. */\nfunction count(n: number, noun: string): string {\n return `${n} ${noun}${n === 1 ? \"\" : \"s\"}`;\n}\n"]}
@@ -2,7 +2,7 @@
2
2
  * The three grades one hop of a use case can earn: `usecase.step-unbacked`,
3
3
  * `usecase.step-contested` and `usecase.step-unlinked`.
4
4
  *
5
- * The attribution itself is `core/c4/arch.ts`'s `attributeStep` — which declared
5
+ * The attribution itself is `core/c4/resolve/steps.ts`'s `attributeStep` — which declared
6
6
  * relationship backs a hop, oriented on `isBackward` and decided by the
7
7
  * DISTINCT-op count — and none of that reasoning is repeated here. This module
8
8
  * owns three things: which verdict earns which code, the three guards that keep
@@ -24,7 +24,7 @@
24
24
  * once for the disagreement, and once for whichever candidate happened to carry
25
25
  * no operation at all.
26
26
  */
27
- import { attributeStep } from "../../../../core/c4/arch.js";
27
+ import { attributeStep } from "../../../../core/c4/resolve/steps.js";
28
28
  import { ACTOR_KINDS } from "../../../../core/vocabulary/maturity.js";
29
29
  import { stepPlace } from "../../../../core/usecases/place.js";
30
30
  import { viewPlace } from "./place.js";
@@ -42,7 +42,7 @@ function placeOf(hop) {
42
42
  * spelling they used would be a hint that restates the defect — and the sentence
43
43
  * says which pair that spelling looks the hop up under, because `<-` attributes
44
44
  * a reply to the CALL it answers rather than to the direction the message
45
- * travels (`callPair` in core/c4/arch.ts holds the measurement).
45
+ * travels (`callPair` in core/c4/resolve/steps.ts holds the measurement).
46
46
  */
47
47
  function unbackedFinding(hop, attribution) {
48
48
  const other = hop.step.isBackward