@ybotok/loam 0.2.0-alpha.1 → 0.2.0-alpha.4

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 (92) hide show
  1. package/CHANGELOG.md +165 -0
  2. package/README.md +6 -6
  3. package/ROADMAP.md +155 -18
  4. package/SCHEMA.md +33 -1
  5. package/dist/commands/archive/plan/gate.js +18 -12
  6. package/dist/commands/archive/plan/gate.js.map +1 -1
  7. package/dist/commands/archive/plan/landscape.js +19 -0
  8. package/dist/commands/archive/plan/landscape.js.map +1 -1
  9. package/dist/commands/archive/run.js +48 -3
  10. package/dist/commands/archive/run.js.map +1 -1
  11. package/dist/commands/seed/plan.js +1 -1
  12. package/dist/commands/seed/plan.js.map +1 -1
  13. package/dist/commands/subsystem/sync.js +1 -1
  14. package/dist/commands/subsystem/sync.js.map +1 -1
  15. package/dist/commands/subsystem/txn/txn.js +1 -1
  16. package/dist/commands/subsystem/txn/txn.js.map +1 -1
  17. package/dist/commands/subsystem/txn/views.js +23 -11
  18. package/dist/commands/subsystem/txn/views.js.map +1 -1
  19. package/dist/commands/validate/arch-coverage.js +11 -1
  20. package/dist/commands/validate/arch-coverage.js.map +1 -1
  21. package/dist/commands/validate/fleet/obligations.js +43 -9
  22. package/dist/commands/validate/fleet/obligations.js.map +1 -1
  23. package/dist/commands/validate/fleet/usecases/steps.js +3 -3
  24. package/dist/commands/validate/fleet/usecases/steps.js.map +1 -1
  25. package/dist/commands/validate/fleet/views/ids.js +6 -1
  26. package/dist/commands/validate/fleet/views/ids.js.map +1 -1
  27. package/dist/commands/validate/fleet/views/stale.js +15 -39
  28. package/dist/commands/validate/fleet/views/stale.js.map +1 -1
  29. package/dist/commands/validate/service/service.js +1 -0
  30. package/dist/commands/validate/service/service.js.map +1 -1
  31. package/dist/commands/validate/service/specs.js +33 -1
  32. package/dist/commands/validate/service/specs.js.map +1 -1
  33. package/dist/core/agent/agents-md/map/subsystem.js +8 -1
  34. package/dist/core/agent/agents-md/map/subsystem.js.map +1 -1
  35. package/dist/core/agent/agents-md.js +4 -0
  36. package/dist/core/agent/agents-md.js.map +1 -1
  37. package/dist/core/agent/workflows/check.js +3 -0
  38. package/dist/core/agent/workflows/check.js.map +1 -1
  39. package/dist/core/brief/unchecked.js +1 -0
  40. package/dist/core/brief/unchecked.js.map +1 -1
  41. package/dist/core/c4/arch.js +72 -180
  42. package/dist/core/c4/arch.js.map +1 -1
  43. package/dist/core/c4/likec4.js +2 -0
  44. package/dist/core/c4/likec4.js.map +1 -1
  45. package/dist/core/c4/parsed/deployment.js +158 -0
  46. package/dist/core/c4/parsed/deployment.js.map +1 -0
  47. package/dist/core/c4/project/load.js +4 -0
  48. package/dist/core/c4/project/load.js.map +1 -1
  49. package/dist/core/c4/project/staged.js +220 -0
  50. package/dist/core/c4/project/staged.js.map +1 -0
  51. package/dist/core/c4/resolve/service.js +46 -0
  52. package/dist/core/c4/resolve/service.js.map +1 -1
  53. package/dist/core/c4/resolve/steps.js +144 -0
  54. package/dist/core/c4/resolve/steps.js.map +1 -0
  55. package/dist/core/c4/splice/delta-blocks.js +15 -2
  56. package/dist/core/c4/splice/delta-blocks.js.map +1 -1
  57. package/dist/core/c4/workspace.js +2 -0
  58. package/dist/core/c4/workspace.js.map +1 -1
  59. package/dist/core/coherence/coherence.js +36 -0
  60. package/dist/core/coherence/coherence.js.map +1 -1
  61. package/dist/core/deployment/delta.js +188 -0
  62. package/dist/core/deployment/delta.js.map +1 -0
  63. package/dist/core/deployment/objects.js +145 -0
  64. package/dist/core/deployment/objects.js.map +1 -0
  65. package/dist/core/pack/living.js +19 -0
  66. package/dist/core/pack/living.js.map +1 -1
  67. package/dist/core/repo/authored/paths.js +18 -0
  68. package/dist/core/repo/authored/paths.js.map +1 -1
  69. package/dist/core/repo/paths.js +44 -3
  70. package/dist/core/repo/paths.js.map +1 -1
  71. package/dist/core/repo/tree/render/body.js +219 -0
  72. package/dist/core/repo/tree/render/body.js.map +1 -0
  73. package/dist/core/repo/tree/render/stale.js +26 -0
  74. package/dist/core/repo/tree/render/stale.js.map +1 -0
  75. package/dist/core/repo/tree/render/text.js +84 -0
  76. package/dist/core/repo/tree/render/text.js.map +1 -0
  77. package/dist/core/repo/tree/{views.js → render/views.js} +38 -34
  78. package/dist/core/repo/tree/render/views.js.map +1 -0
  79. package/dist/core/status/feature/artifacts.js +29 -4
  80. package/dist/core/status/feature/artifacts.js.map +1 -1
  81. package/dist/core/status/fleet/fleet.js +11 -1
  82. package/dist/core/status/fleet/fleet.js.map +1 -1
  83. package/dist/core/usecases/delta/overlay.js +28 -191
  84. package/dist/core/usecases/delta/overlay.js.map +1 -1
  85. package/dist/core/usecases/fleet.js.map +1 -1
  86. package/dist/core/usecases/operations.js +2 -2
  87. package/dist/core/usecases/operations.js.map +1 -1
  88. package/dist/core/vocabulary/issue.js +3 -0
  89. package/dist/core/vocabulary/issue.js.map +1 -1
  90. package/examples/docs/architecture/subsystems.likec4 +4 -0
  91. package/package.json +1 -1
  92. package/dist/core/repo/tree/views.js.map +0 -1
@@ -1 +1 @@
1
- {"version":3,"file":"steps.js","sourceRoot":"","sources":["../../../../../src/commands/validate/fleet/usecases/steps.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * The three grades one hop of a use case can earn: `usecase.step-unbacked`,\n * `usecase.step-contested` and `usecase.step-unlinked`.\n *\n * The attribution itself is `core/c4/arch.ts`'s `attributeStep` — which declared\n * relationship backs a hop, oriented on `isBackward` and decided by the\n * DISTINCT-op count — and none of that reasoning is repeated here. This module\n * owns three things: which verdict earns which code, the three guards that keep\n * the warn honest, and the sentences.\n *\n * WHY `step-unbacked` IS THE CHECK THAT EARNS THE FEATURE. A hop between two\n * elements that both exist, with no relationship between them anywhere in the\n * model, is ZERO LikeC4 errors: a `dynamic view` is allowed to show a call the\n * model never declared, so the diagram renders, the project parses, and every\n * other check in loam stays green. The flow reads as fine in every tool the\n * fleet has. If loam does not convict it, nothing does — which is why it is an\n * error rather than an advisory.\n *\n * ONE BREACH, ONE FINDING, and here that is structural rather than remembered.\n * `contested` and `attributed` are disjoint verdicts, so deriving\n * `step-unlinked` from the `attributed` arm — never from the candidate\n * relationships — is what makes a contested step report once. A grader that\n * walked `rels` looking for a missing `op` would convict the same hop twice:\n * once for the disagreement, and once for whichever candidate happened to carry\n * no operation at all.\n */\nimport { attributeStep, type StepAttribution, type StepScope } from \"../../../../core/c4/arch.js\";\nimport type { Elem, Rel } from \"../../../../core/c4/likec4.js\";\nimport type { ParsedStep, ParsedView } from \"../../../../core/c4/parsed/dynamic-views.js\";\nimport { ACTOR_KINDS } from \"../../../../core/vocabulary/maturity.js\";\nimport type { Finding } from \"../../../../core/vocabulary/report.js\";\nimport { stepPlace } from \"../../../../core/usecases/place.js\";\nimport { viewPlace } from \"./place.js\";\n\n/** One hop being graded and the view it belongs to — never passed apart. */\nexport interface GradedStep {\n view: ParsedView;\n step: ParsedStep;\n}\n\n/**\n * What a hop is graded against, built ONCE per run.\n *\n * `services` and `model.known` are the same set by construction (`./usecases.ts`\n * builds both from one value), which is what stops \"which service is this\n * element\" being answered one way by the attribution and another by the provider\n * guard below. `resolve` is injected for the same reason `fleetShapeFindings`\n * takes it: the fleet target already holds the shared element→service resolver,\n * and a second one built here could not disagree today but would be a second\n * place to change.\n */\nexport interface StepGrading {\n model: StepScope;\n /** The `services/<id>/` directories that exist. */\n services: ReadonlySet<string>;\n /** The shared element→service resolver every other edge join uses. */\n resolve: (id: string) => string;\n}\n\ntype Unbacked = Extract<StepAttribution, { verdict: \"unbacked\" }>;\ntype Contested = Extract<StepAttribution, { verdict: \"contested\" }>;\ntype Attributed = Extract<StepAttribution, { verdict: \"attributed\" }>;\n\n/** `…checkout.likec4 — dynamic view 'uc_checkout' step 2 'authorizes the payment'`. */\nfunction placeOf(hop: GradedStep): string {\n return `${viewPlace(hop.view)} ${stepPlace(hop.step)}`;\n}\n\n/**\n * A hop nothing in the model backs.\n *\n * Two fixes, because there are genuinely two mistakes that land here and the\n * author is the only one who can say which they made: the edge really is\n * missing, or the hop is a reply written the long way round. The second is\n * offered as the OTHER spelling of what they already typed — suggesting the\n * spelling they used would be a hint that restates the defect — and the sentence\n * says which pair that spelling looks the hop up under, because `<-` attributes\n * a reply to the CALL it answers rather than to the direction the message\n * travels (`callPair` in core/c4/arch.ts holds the measurement).\n */\nfunction unbackedFinding(hop: GradedStep, attribution: Unbacked): Finding {\n const other = hop.step.isBackward\n ? { spelling: `${hop.step.source} -> ${hop.step.target}`, when: \"is not a return step\" }\n : { spelling: `${hop.step.target} <- ${hop.step.source}`, when: \"is a return step\" };\n return {\n severity: \"error\",\n code: \"usecase.step-unbacked\",\n subject: hop.view.id,\n message:\n `${placeOf(hop)}: nothing in the model declares ${attribution.from} -> ${attribution.to}, so this hop is ` +\n \"backed by no relationship — and LikeC4 reports no error for it, so the view still renders and the \" +\n \"project still parses. Draw the edge in `model { }` (with `metadata { op '<operationId>' }` where it is \" +\n `a call), or, if this hop ${other.when}, write it as \\`${other.spelling}\\` — a reply is attributed to the ` +\n `call it answers, so that spelling looks the hop up under ${attribution.to} -> ${attribution.from} instead.`,\n };\n}\n\n/** `web -> orders.api \"Calls createOrder\" (op: createOrder)`, or `(no op)` where the edge declares none. */\nfunction candidateLine(rel: Rel): string {\n const op = rel.op === undefined ? \"no op\" : `op: ${rel.op}`;\n return `${rel.source} -> ${rel.target} \"${rel.title ?? \"\"}\" (${op})`;\n}\n\n/**\n * A hop whose candidates disagree about the operation.\n *\n * WARN, not error, because the model is complete — every candidate is a\n * relationship somebody drew on purpose — and the honest answer is sometimes\n * \"two paths, one flow\". What loam refuses to do is pick: a guessed operation is\n * indistinguishable from a right one in every rollup built over the use case,\n * and it stays wrong invisibly.\n *\n * `details` carries every candidate rather than the distinct ops, because the\n * repair is made on a RELATIONSHIP: the author has to find the edge that names\n * the wrong operation, and a list of operation names does not say which edge\n * that is. An absent `metadata { op }` prints as `no op` and is a candidate like\n * any other — it is exactly as much of a disagreement as a second name.\n */\nfunction contestedFinding(hop: GradedStep, attribution: Contested): Finding {\n return {\n severity: \"warn\",\n code: \"usecase.step-contested\",\n subject: hop.view.id,\n message:\n `${placeOf(hop)}: ${attribution.rels.length} relationships back ${attribution.from} -> ` +\n `${attribution.to} and they name ${attribution.ops.length} different operations, so nothing can say ` +\n \"which one this hop exercises. loam names the candidates instead of picking one — a guessed operation \" +\n \"reads exactly like a right one in every rollup built over this flow. Give the candidates one operation, \" +\n \"or draw the hop between the elements that name exactly one.\",\n details: attribution.rels.map(candidateLine),\n };\n}\n\n/**\n * The service this hop calls, when the hop is a `step-unlinked` candidate at\n * all — `null` when any of the three guards refuses. ALL THREE are mechanical,\n * and each closes a whole class of warnings that would otherwise be simply\n * wrong. The count is stated because a reader auditing this function is meant to\n * find three bullets and three `return null`s below and conclude that none of\n * them is surplus: the one a miscount invites deleting is the last, and deleting\n * it puts the warning back on the first hop of almost every flow.\n *\n * - A `publishes` or `consumes` on ANY candidate means the hop does reach a\n * declared API — an AsyncAPI one. The event spine already grades it\n * (`spine.message-undefined` and its family), and asking for an `op` as well\n * would be loam demanding an HTTP operationId for a Kafka message. Read\n * across every candidate rather than the first, because on the service tier\n * the candidates are one call drawn as containers.\n * - The target must resolve to a real `services/<id>/`. Without it every hop\n * into an `#external` system, an actor, a database, a cache or a queue warns\n * — none of which owns an `openapi.yaml`, so none of them CAN carry the\n * operationId the message would be asking for. Through the shared resolver,\n * so \"the provider\" here and \"the provider\" in the spine check cannot\n * disagree about which element stands for which directory.\n * - The CALLER must not be an actor. A person is not a caller: `customer ->\n * checkoutWeb 'opens the basket'` is somebody using the app, and the app owes\n * no operationId for a click. The guard above does not cover this, because it\n * reads the TARGET — and the target here is one of ours, so without this the\n * warning fired on the FIRST HOP OF ALMOST EVERY FLOW, which is where a\n * sequence diagram puts its actor. Measured on the published example fleet\n * before it was added. This is the same exemption `fleet/census.ts`,\n * `arch-coverage.ts`, `core/gate/partners.ts` and `core/verify/checklist.ts`\n * each already apply, through the same `ACTOR_KINDS` — a person was never a\n * participant in any of loam's other censuses either.\n *\n * The op test reads the ATTRIBUTION rather than the relationships: an\n * `attributed` verdict with no `op` means every candidate agreed the hop names\n * no operation, which is the state this warn is about. Candidates that disagree\n * never reach here at all — that is the suppression, and it is why it cannot be\n * forgotten.\n */\nfunction unlinkedProvider(attribution: Attributed, grading: StepGrading): string | null {\n if (attribution.op !== undefined) return null;\n if (attribution.rels.some((rel) => rel.publishes !== undefined || rel.consumes !== undefined)) return null;\n if (isActor(attribution.from, grading.model.elements)) return null;\n const provider = grading.resolve(attribution.to);\n return grading.services.has(provider) ? provider : null;\n}\n\n/**\n * Is this endpoint a person rather than a system?\n *\n * Exact id, not the ancestor walk the service resolver does: an actor is a\n * top-level element in every landscape loam has seen, and a nested one would be\n * a person inside a system, which is not a shape the model has. An endpoint that\n * names no element cannot occur — a step naming one is a LikeC4 reference error,\n * so the document never reaches a check.\n */\nfunction isActor(id: string, elements: Elem[]): boolean {\n const element = elements.find((candidate) => candidate.id === id);\n return element !== undefined && ACTOR_KINDS.has(element.kind.toLowerCase());\n}\n\n/**\n * A hop backed by the model and joined to no contract.\n *\n * The mirror of `spine.op-link-missing` at flow altitude: the use case reaches\n * the fleet map and stops there, so nothing can say which operation of the\n * provider's contract the step exercises — and the whole point of grading a use\n * case is that the flow joins through to the API it drives.\n */\nfunction unlinkedFinding(hop: GradedStep, attribution: Attributed, provider: string): Finding {\n const backing =\n attribution.rels.length === 1 ? \"one relationship\" : `${attribution.rels.length} relationships`;\n return {\n severity: \"warn\",\n code: \"usecase.step-unlinked\",\n subject: hop.view.id,\n message:\n `${placeOf(hop)}: ${attribution.from} -> ${attribution.to} is backed by ${backing} carrying no ` +\n \"`metadata { op }` and no `publishes`/`consumes`, so this hop names no operation of \" +\n `${provider}'s contract — the use case reaches the fleet map and stops there. Add ` +\n \"`metadata { op '<operationId>' }` to the edge in `model { }`, or `metadata { publishes '<message>' }` / \" +\n \"`consumes` where the hop is an event.\",\n };\n}\n\n/**\n * The findings one hop earns — at most one, whatever is wrong with it.\n *\n * A `switch` over the closed verdict union with every arm returning, so a fourth\n * verdict added upstream is a compile error here rather than a hop that silently\n * stops being graded.\n */\nexport function stepFindings(hop: GradedStep, grading: StepGrading): Finding[] {\n const attribution = attributeStep(hop.step, grading.model);\n switch (attribution.verdict) {\n case \"unbacked\":\n return [unbackedFinding(hop, attribution)];\n case \"contested\":\n return [contestedFinding(hop, attribution)];\n case \"attributed\": {\n const provider = unlinkedProvider(attribution, grading);\n return provider === null ? [] : [unlinkedFinding(hop, attribution, provider)];\n }\n }\n}\n"]}
1
+ {"version":3,"file":"steps.js","sourceRoot":"","sources":["../../../../../src/commands/validate/fleet/usecases/steps.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;GAyBG;AACH,OAAO,EAAE,aAAa,EAAwC,MAAM,sCAAsC,CAAC;AAG3G,OAAO,EAAE,WAAW,EAAE,MAAM,yCAAyC,CAAC;AAEtE,OAAO,EAAE,SAAS,EAAE,MAAM,oCAAoC,CAAC;AAC/D,OAAO,EAAE,SAAS,EAAE,MAAM,YAAY,CAAC;AA+BvC,uFAAuF;AACvF,SAAS,OAAO,CAAC,GAAe;IAC9B,OAAO,GAAG,SAAS,CAAC,GAAG,CAAC,IAAI,CAAC,IAAI,SAAS,CAAC,GAAG,CAAC,IAAI,CAAC,EAAE,CAAC;AACzD,CAAC;AAED;;;;;;;;;;;GAWG;AACH,SAAS,eAAe,CAAC,GAAe,EAAE,WAAqB;IAC7D,MAAM,KAAK,GAAG,GAAG,CAAC,IAAI,CAAC,UAAU;QAC/B,CAAC,CAAC,EAAE,QAAQ,EAAE,GAAG,GAAG,CAAC,IAAI,CAAC,MAAM,OAAO,GAAG,CAAC,IAAI,CAAC,MAAM,EAAE,EAAE,IAAI,EAAE,sBAAsB,EAAE;QACxF,CAAC,CAAC,EAAE,QAAQ,EAAE,GAAG,GAAG,CAAC,IAAI,CAAC,MAAM,OAAO,GAAG,CAAC,IAAI,CAAC,MAAM,EAAE,EAAE,IAAI,EAAE,kBAAkB,EAAE,CAAC;IACvF,OAAO;QACL,QAAQ,EAAE,OAAO;QACjB,IAAI,EAAE,uBAAuB;QAC7B,OAAO,EAAE,GAAG,CAAC,IAAI,CAAC,EAAE;QACpB,OAAO,EACL,GAAG,OAAO,CAAC,GAAG,CAAC,mCAAmC,WAAW,CAAC,IAAI,OAAO,WAAW,CAAC,EAAE,mBAAmB;YAC1G,oGAAoG;YACpG,yGAAyG;YACzG,4BAA4B,KAAK,CAAC,IAAI,mBAAmB,KAAK,CAAC,QAAQ,oCAAoC;YAC3G,4DAA4D,WAAW,CAAC,EAAE,OAAO,WAAW,CAAC,IAAI,WAAW;KAC/G,CAAC;AACJ,CAAC;AAED,4GAA4G;AAC5G,SAAS,aAAa,CAAC,GAAQ;IAC7B,MAAM,EAAE,GAAG,GAAG,CAAC,EAAE,KAAK,SAAS,CAAC,CAAC,CAAC,OAAO,CAAC,CAAC,CAAC,OAAO,GAAG,CAAC,EAAE,EAAE,CAAC;IAC5D,OAAO,GAAG,GAAG,CAAC,MAAM,OAAO,GAAG,CAAC,MAAM,KAAK,GAAG,CAAC,KAAK,IAAI,EAAE,MAAM,EAAE,GAAG,CAAC;AACvE,CAAC;AAED;;;;;;;;;;;;;;GAcG;AACH,SAAS,gBAAgB,CAAC,GAAe,EAAE,WAAsB;IAC/D,OAAO;QACL,QAAQ,EAAE,MAAM;QAChB,IAAI,EAAE,wBAAwB;QAC9B,OAAO,EAAE,GAAG,CAAC,IAAI,CAAC,EAAE;QACpB,OAAO,EACL,GAAG,OAAO,CAAC,GAAG,CAAC,KAAK,WAAW,CAAC,IAAI,CAAC,MAAM,uBAAuB,WAAW,CAAC,IAAI,MAAM;YACxF,GAAG,WAAW,CAAC,EAAE,kBAAkB,WAAW,CAAC,GAAG,CAAC,MAAM,4CAA4C;YACrG,uGAAuG;YACvG,0GAA0G;YAC1G,6DAA6D;QAC/D,OAAO,EAAE,WAAW,CAAC,IAAI,CAAC,GAAG,CAAC,aAAa,CAAC;KAC7C,CAAC;AACJ,CAAC;AAED;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAqCG;AACH,SAAS,gBAAgB,CAAC,WAAuB,EAAE,OAAoB;IACrE,IAAI,WAAW,CAAC,EAAE,KAAK,SAAS;QAAE,OAAO,IAAI,CAAC;IAC9C,IAAI,WAAW,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC,GAAG,EAAE,EAAE,CAAC,GAAG,CAAC,SAAS,KAAK,SAAS,IAAI,GAAG,CAAC,QAAQ,KAAK,SAAS,CAAC;QAAE,OAAO,IAAI,CAAC;IAC3G,IAAI,OAAO,CAAC,WAAW,CAAC,IAAI,EAAE,OAAO,CAAC,KAAK,CAAC,QAAQ,CAAC;QAAE,OAAO,IAAI,CAAC;IACnE,MAAM,QAAQ,GAAG,OAAO,CAAC,OAAO,CAAC,WAAW,CAAC,EAAE,CAAC,CAAC;IACjD,OAAO,OAAO,CAAC,QAAQ,CAAC,GAAG,CAAC,QAAQ,CAAC,CAAC,CAAC,CAAC,QAAQ,CAAC,CAAC,CAAC,IAAI,CAAC;AAC1D,CAAC;AAED;;;;;;;;GAQG;AACH,SAAS,OAAO,CAAC,EAAU,EAAE,QAAgB;IAC3C,MAAM,OAAO,GAAG,QAAQ,CAAC,IAAI,CAAC,CAAC,SAAS,EAAE,EAAE,CAAC,SAAS,CAAC,EAAE,KAAK,EAAE,CAAC,CAAC;IAClE,OAAO,OAAO,KAAK,SAAS,IAAI,WAAW,CAAC,GAAG,CAAC,OAAO,CAAC,IAAI,CAAC,WAAW,EAAE,CAAC,CAAC;AAC9E,CAAC;AAED;;;;;;;GAOG;AACH,SAAS,eAAe,CAAC,GAAe,EAAE,WAAuB,EAAE,QAAgB;IACjF,MAAM,OAAO,GACX,WAAW,CAAC,IAAI,CAAC,MAAM,KAAK,CAAC,CAAC,CAAC,CAAC,kBAAkB,CAAC,CAAC,CAAC,GAAG,WAAW,CAAC,IAAI,CAAC,MAAM,gBAAgB,CAAC;IAClG,OAAO;QACL,QAAQ,EAAE,MAAM;QAChB,IAAI,EAAE,uBAAuB;QAC7B,OAAO,EAAE,GAAG,CAAC,IAAI,CAAC,EAAE;QACpB,OAAO,EACL,GAAG,OAAO,CAAC,GAAG,CAAC,KAAK,WAAW,CAAC,IAAI,OAAO,WAAW,CAAC,EAAE,iBAAiB,OAAO,eAAe;YAChG,qFAAqF;YACrF,GAAG,QAAQ,wEAAwE;YACnF,0GAA0G;YAC1G,uCAAuC;KAC1C,CAAC;AACJ,CAAC;AAED;;;;;;GAMG;AACH,MAAM,UAAU,YAAY,CAAC,GAAe,EAAE,OAAoB;IAChE,MAAM,WAAW,GAAG,aAAa,CAAC,GAAG,CAAC,IAAI,EAAE,OAAO,CAAC,KAAK,CAAC,CAAC;IAC3D,QAAQ,WAAW,CAAC,OAAO,EAAE,CAAC;QAC5B,KAAK,UAAU;YACb,OAAO,CAAC,eAAe,CAAC,GAAG,EAAE,WAAW,CAAC,CAAC,CAAC;QAC7C,KAAK,WAAW;YACd,OAAO,CAAC,gBAAgB,CAAC,GAAG,EAAE,WAAW,CAAC,CAAC,CAAC;QAC9C,KAAK,YAAY,EAAE,CAAC;YAClB,MAAM,QAAQ,GAAG,gBAAgB,CAAC,WAAW,EAAE,OAAO,CAAC,CAAC;YACxD,OAAO,QAAQ,KAAK,IAAI,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,CAAC,eAAe,CAAC,GAAG,EAAE,WAAW,EAAE,QAAQ,CAAC,CAAC,CAAC;QAChF,CAAC;IACH,CAAC;AACH,CAAC","sourcesContent":["/**\n * The three grades one hop of a use case can earn: `usecase.step-unbacked`,\n * `usecase.step-contested` and `usecase.step-unlinked`.\n *\n * The attribution itself is `core/c4/resolve/steps.ts`'s `attributeStep` — which declared\n * relationship backs a hop, oriented on `isBackward` and decided by the\n * DISTINCT-op count — and none of that reasoning is repeated here. This module\n * owns three things: which verdict earns which code, the three guards that keep\n * the warn honest, and the sentences.\n *\n * WHY `step-unbacked` IS THE CHECK THAT EARNS THE FEATURE. A hop between two\n * elements that both exist, with no relationship between them anywhere in the\n * model, is ZERO LikeC4 errors: a `dynamic view` is allowed to show a call the\n * model never declared, so the diagram renders, the project parses, and every\n * other check in loam stays green. The flow reads as fine in every tool the\n * fleet has. If loam does not convict it, nothing does — which is why it is an\n * error rather than an advisory.\n *\n * ONE BREACH, ONE FINDING, and here that is structural rather than remembered.\n * `contested` and `attributed` are disjoint verdicts, so deriving\n * `step-unlinked` from the `attributed` arm — never from the candidate\n * relationships — is what makes a contested step report once. A grader that\n * walked `rels` looking for a missing `op` would convict the same hop twice:\n * once for the disagreement, and once for whichever candidate happened to carry\n * no operation at all.\n */\nimport { attributeStep, type StepAttribution, type StepScope } from \"../../../../core/c4/resolve/steps.js\";\nimport type { Elem, Rel } from \"../../../../core/c4/likec4.js\";\nimport type { ParsedStep, ParsedView } from \"../../../../core/c4/parsed/dynamic-views.js\";\nimport { ACTOR_KINDS } from \"../../../../core/vocabulary/maturity.js\";\nimport type { Finding } from \"../../../../core/vocabulary/report.js\";\nimport { stepPlace } from \"../../../../core/usecases/place.js\";\nimport { viewPlace } from \"./place.js\";\n\n/** One hop being graded and the view it belongs to — never passed apart. */\nexport interface GradedStep {\n view: ParsedView;\n step: ParsedStep;\n}\n\n/**\n * What a hop is graded against, built ONCE per run.\n *\n * `services` and `model.known` are the same set by construction (`./usecases.ts`\n * builds both from one value), which is what stops \"which service is this\n * element\" being answered one way by the attribution and another by the provider\n * guard below. `resolve` is injected for the same reason `fleetShapeFindings`\n * takes it: the fleet target already holds the shared element→service resolver,\n * and a second one built here could not disagree today but would be a second\n * place to change.\n */\nexport interface StepGrading {\n model: StepScope;\n /** The `services/<id>/` directories that exist. */\n services: ReadonlySet<string>;\n /** The shared element→service resolver every other edge join uses. */\n resolve: (id: string) => string;\n}\n\ntype Unbacked = Extract<StepAttribution, { verdict: \"unbacked\" }>;\ntype Contested = Extract<StepAttribution, { verdict: \"contested\" }>;\ntype Attributed = Extract<StepAttribution, { verdict: \"attributed\" }>;\n\n/** `…checkout.likec4 — dynamic view 'uc_checkout' step 2 'authorizes the payment'`. */\nfunction placeOf(hop: GradedStep): string {\n return `${viewPlace(hop.view)} ${stepPlace(hop.step)}`;\n}\n\n/**\n * A hop nothing in the model backs.\n *\n * Two fixes, because there are genuinely two mistakes that land here and the\n * author is the only one who can say which they made: the edge really is\n * missing, or the hop is a reply written the long way round. The second is\n * offered as the OTHER spelling of what they already typed — suggesting the\n * spelling they used would be a hint that restates the defect — and the sentence\n * says which pair that spelling looks the hop up under, because `<-` attributes\n * a reply to the CALL it answers rather than to the direction the message\n * travels (`callPair` in core/c4/resolve/steps.ts holds the measurement).\n */\nfunction unbackedFinding(hop: GradedStep, attribution: Unbacked): Finding {\n const other = hop.step.isBackward\n ? { spelling: `${hop.step.source} -> ${hop.step.target}`, when: \"is not a return step\" }\n : { spelling: `${hop.step.target} <- ${hop.step.source}`, when: \"is a return step\" };\n return {\n severity: \"error\",\n code: \"usecase.step-unbacked\",\n subject: hop.view.id,\n message:\n `${placeOf(hop)}: nothing in the model declares ${attribution.from} -> ${attribution.to}, so this hop is ` +\n \"backed by no relationship — and LikeC4 reports no error for it, so the view still renders and the \" +\n \"project still parses. Draw the edge in `model { }` (with `metadata { op '<operationId>' }` where it is \" +\n `a call), or, if this hop ${other.when}, write it as \\`${other.spelling}\\` — a reply is attributed to the ` +\n `call it answers, so that spelling looks the hop up under ${attribution.to} -> ${attribution.from} instead.`,\n };\n}\n\n/** `web -> orders.api \"Calls createOrder\" (op: createOrder)`, or `(no op)` where the edge declares none. */\nfunction candidateLine(rel: Rel): string {\n const op = rel.op === undefined ? \"no op\" : `op: ${rel.op}`;\n return `${rel.source} -> ${rel.target} \"${rel.title ?? \"\"}\" (${op})`;\n}\n\n/**\n * A hop whose candidates disagree about the operation.\n *\n * WARN, not error, because the model is complete — every candidate is a\n * relationship somebody drew on purpose — and the honest answer is sometimes\n * \"two paths, one flow\". What loam refuses to do is pick: a guessed operation is\n * indistinguishable from a right one in every rollup built over the use case,\n * and it stays wrong invisibly.\n *\n * `details` carries every candidate rather than the distinct ops, because the\n * repair is made on a RELATIONSHIP: the author has to find the edge that names\n * the wrong operation, and a list of operation names does not say which edge\n * that is. An absent `metadata { op }` prints as `no op` and is a candidate like\n * any other — it is exactly as much of a disagreement as a second name.\n */\nfunction contestedFinding(hop: GradedStep, attribution: Contested): Finding {\n return {\n severity: \"warn\",\n code: \"usecase.step-contested\",\n subject: hop.view.id,\n message:\n `${placeOf(hop)}: ${attribution.rels.length} relationships back ${attribution.from} -> ` +\n `${attribution.to} and they name ${attribution.ops.length} different operations, so nothing can say ` +\n \"which one this hop exercises. loam names the candidates instead of picking one — a guessed operation \" +\n \"reads exactly like a right one in every rollup built over this flow. Give the candidates one operation, \" +\n \"or draw the hop between the elements that name exactly one.\",\n details: attribution.rels.map(candidateLine),\n };\n}\n\n/**\n * The service this hop calls, when the hop is a `step-unlinked` candidate at\n * all — `null` when any of the three guards refuses. ALL THREE are mechanical,\n * and each closes a whole class of warnings that would otherwise be simply\n * wrong. The count is stated because a reader auditing this function is meant to\n * find three bullets and three `return null`s below and conclude that none of\n * them is surplus: the one a miscount invites deleting is the last, and deleting\n * it puts the warning back on the first hop of almost every flow.\n *\n * - A `publishes` or `consumes` on ANY candidate means the hop does reach a\n * declared API — an AsyncAPI one. The event spine already grades it\n * (`spine.message-undefined` and its family), and asking for an `op` as well\n * would be loam demanding an HTTP operationId for a Kafka message. Read\n * across every candidate rather than the first, because on the service tier\n * the candidates are one call drawn as containers.\n * - The target must resolve to a real `services/<id>/`. Without it every hop\n * into an `#external` system, an actor, a database, a cache or a queue warns\n * — none of which owns an `openapi.yaml`, so none of them CAN carry the\n * operationId the message would be asking for. Through the shared resolver,\n * so \"the provider\" here and \"the provider\" in the spine check cannot\n * disagree about which element stands for which directory.\n * - The CALLER must not be an actor. A person is not a caller: `customer ->\n * checkoutWeb 'opens the basket'` is somebody using the app, and the app owes\n * no operationId for a click. The guard above does not cover this, because it\n * reads the TARGET — and the target here is one of ours, so without this the\n * warning fired on the FIRST HOP OF ALMOST EVERY FLOW, which is where a\n * sequence diagram puts its actor. Measured on the published example fleet\n * before it was added. This is the same exemption `fleet/census.ts`,\n * `arch-coverage.ts`, `core/gate/partners.ts` and `core/verify/checklist.ts`\n * each already apply, through the same `ACTOR_KINDS` — a person was never a\n * participant in any of loam's other censuses either.\n *\n * The op test reads the ATTRIBUTION rather than the relationships: an\n * `attributed` verdict with no `op` means every candidate agreed the hop names\n * no operation, which is the state this warn is about. Candidates that disagree\n * never reach here at all — that is the suppression, and it is why it cannot be\n * forgotten.\n */\nfunction unlinkedProvider(attribution: Attributed, grading: StepGrading): string | null {\n if (attribution.op !== undefined) return null;\n if (attribution.rels.some((rel) => rel.publishes !== undefined || rel.consumes !== undefined)) return null;\n if (isActor(attribution.from, grading.model.elements)) return null;\n const provider = grading.resolve(attribution.to);\n return grading.services.has(provider) ? provider : null;\n}\n\n/**\n * Is this endpoint a person rather than a system?\n *\n * Exact id, not the ancestor walk the service resolver does: an actor is a\n * top-level element in every landscape loam has seen, and a nested one would be\n * a person inside a system, which is not a shape the model has. An endpoint that\n * names no element cannot occur — a step naming one is a LikeC4 reference error,\n * so the document never reaches a check.\n */\nfunction isActor(id: string, elements: Elem[]): boolean {\n const element = elements.find((candidate) => candidate.id === id);\n return element !== undefined && ACTOR_KINDS.has(element.kind.toLowerCase());\n}\n\n/**\n * A hop backed by the model and joined to no contract.\n *\n * The mirror of `spine.op-link-missing` at flow altitude: the use case reaches\n * the fleet map and stops there, so nothing can say which operation of the\n * provider's contract the step exercises — and the whole point of grading a use\n * case is that the flow joins through to the API it drives.\n */\nfunction unlinkedFinding(hop: GradedStep, attribution: Attributed, provider: string): Finding {\n const backing =\n attribution.rels.length === 1 ? \"one relationship\" : `${attribution.rels.length} relationships`;\n return {\n severity: \"warn\",\n code: \"usecase.step-unlinked\",\n subject: hop.view.id,\n message:\n `${placeOf(hop)}: ${attribution.from} -> ${attribution.to} is backed by ${backing} carrying no ` +\n \"`metadata { op }` and no `publishes`/`consumes`, so this hop names no operation of \" +\n `${provider}'s contract — the use case reaches the fleet map and stops there. Add ` +\n \"`metadata { op '<operationId>' }` to the edge in `model { }`, or `metadata { publishes '<message>' }` / \" +\n \"`consumes` where the hop is an event.\",\n };\n}\n\n/**\n * The findings one hop earns — at most one, whatever is wrong with it.\n *\n * A `switch` over the closed verdict union with every arm returning, so a fourth\n * verdict added upstream is a compile error here rather than a hop that silently\n * stops being graded.\n */\nexport function stepFindings(hop: GradedStep, grading: StepGrading): Finding[] {\n const attribution = attributeStep(hop.step, grading.model);\n switch (attribution.verdict) {\n case \"unbacked\":\n return [unbackedFinding(hop, attribution)];\n case \"contested\":\n return [contestedFinding(hop, attribution)];\n case \"attributed\": {\n const provider = unlinkedProvider(attribution, grading);\n return provider === null ? [] : [unlinkedFinding(hop, attribution, provider)];\n }\n }\n}\n"]}
@@ -1,4 +1,4 @@
1
- import { SUBSYSTEM_VIEW_PREFIX, subsystemViewId } from "../../../../core/repo/tree/views.js";
1
+ import { SUBSYSTEM_VIEW_PREFIX, subsystemViewId } from "../../../../core/repo/tree/render/views.js";
2
2
  /**
3
3
  * `authored` is every view id the `architecture/` project claims, each with the
4
4
  * file that claims it — static views included, because a static view claims an
@@ -28,6 +28,11 @@ export function viewIdFindings(authored, tree) {
28
28
  severity: "error",
29
29
  code: "subsystem.view-id-collision",
30
30
  subject: id,
31
+ // The CLAIMANT file — the one the message tells the reader to edit, and
32
+ // the one the report's target fallback gets wrong: it files this under
33
+ // the landscape, which is exactly the file a reader must NOT rename the
34
+ // view in when the claim came from a document of their own.
35
+ locations: [{ path: `architecture/${file}`, role: "primary" }],
31
36
  message: `subsystems: architecture/${file} declares \`view ${id}\`, which is the id loam generates ` +
32
37
  `into architecture/subsystems.likec4 for services/${owner}/ — LikeC4 merges both files into one project, ` +
33
38
  `so it refuses the whole architecture/ project and renders nothing. ` +
@@ -1 +1 @@
1
- {"version":3,"file":"ids.js","sourceRoot":"","sources":["../../../../../src/commands/validate/fleet/views/ids.ts"],"names":[],"mappings":"AAsCA,OAAO,EAAE,qBAAqB,EAAE,eAAe,EAAE,MAAM,qCAAqC,CAAC;AAI7F;;;;;;GAMG;AACH,MAAM,UAAU,cAAc,CAAC,QAAmC,EAAE,IAAe;IACjF,IAAI,QAAQ,KAAK,SAAS,IAAI,QAAQ,CAAC,MAAM,KAAK,CAAC;QAAE,OAAO,EAAE,CAAC;IAC/D,MAAM,SAAS,GAAG,IAAI,GAAG,CAAC,IAAI,CAAC,UAAU,CAAC,GAAG,CAAC,CAAC,GAAG,EAAE,EAAE,CAAC,CAAC,eAAe,CAAC,GAAG,CAAC,EAAE,GAAG,CAAC,IAAI,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC,CAAC;IACpG,MAAM,QAAQ,GAAc,EAAE,CAAC;IAC/B,2EAA2E;IAC3E,8EAA8E;IAC9E,+EAA+E;IAC/E,2EAA2E;IAC3E,iDAAiD;IACjD,MAAM,SAAS,GAAG,IAAI,GAAG,EAAkB,CAAC;IAC5C,KAAK,MAAM,KAAK,IAAI,QAAQ;QAAE,IAAI,CAAC,SAAS,CAAC,GAAG,CAAC,KAAK,CAAC,EAAE,CAAC;YAAE,SAAS,CAAC,GAAG,CAAC,KAAK,CAAC,EAAE,EAAE,KAAK,CAAC,UAAU,CAAC,CAAC;IACtG,KAAK,MAAM,CAAC,EAAE,EAAE,IAAI,CAAC,IAAI,CAAC,GAAG,SAAS,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,EAAE,CAAC;QAC7E,MAAM,KAAK,GAAG,SAAS,CAAC,GAAG,CAAC,EAAE,CAAC,CAAC;QAChC,IAAI,KAAK,KAAK,SAAS;YAAE,SAAS;QAClC,QAAQ,CAAC,IAAI,CAAC;YACZ,QAAQ,EAAE,OAAO;YACjB,IAAI,EAAE,6BAA6B;YACnC,OAAO,EAAE,EAAE;YACX,OAAO,EACL,4BAA4B,IAAI,oBAAoB,EAAE,qCAAqC;gBAC3F,oDAAoD,KAAK,iDAAiD;gBAC1G,qEAAqE;gBACrE,mCAAmC,IAAI,qBAAqB,qBAAqB,gBAAgB;gBACjG,iHAAiH;SACpH,CAAC,CAAC;IACL,CAAC;IACD,OAAO,QAAQ,CAAC;AAClB,CAAC","sourcesContent":["/**\n * An authored view id that collides with one loam GENERATES into\n * `architecture/subsystems.likec4`.\n *\n * This closes a green-while-broken hole, and the shape of it is measured rather\n * than argued. The LikeC4 renderer merges every `.likec4` file in a project into\n * one model, and `likec4.config.json` scopes the root project to\n * `architecture/` — so `landscape.likec4` and the generated `subsystems.likec4`\n * share ONE flat view-id namespace. Author `view subsystem_commerce { }` in the\n * landscape while the tree has a `commerce` subsystem and the renderer refuses\n * the ENTIRE `architecture/` project with two `Duplicate view` diagnostics, one\n * per file — while `loam validate --all` printed `landscape: N service(s)\n * modelled — architecture/landscape.likec4 and services/ agree`. Reproduced at\n * the 1.59.2 pin.\n *\n * Nothing else in a loam repo can produce this. A duplicate INSIDE one document\n * is already 2 LikeC4 errors, so `landscape.invalid` has it (measured). A\n * service's `model.likec4` and a feature's `delta.likec4` are each their own\n * LikeC4 project — `likec4.config.json` excludes `services/**` and\n * `features/**` from the root — so their view ids cannot collide with the\n * landscape's, and a delta's views never travel through `archive` anyway. That\n * leaves exactly one pair, which is why this check compares exactly one pair\n * rather than taking a census across every document loam parses.\n *\n * ONLY the real collision is refused, never the merely reserved-looking name.\n * A landscape view called `subsystem_overview` with no `overview` subsystem\n * breaks nothing today, and refusing it would be loam inventing a rule about a\n * name that harms no one. The latent case is not lost: the moment somebody\n * creates that subsystem, the generator mints the id, and this same check fires\n * on the same run. The message names the reservation anyway, so an author fixing\n * one collision learns not to make the next.\n *\n * Graded on `validate --all` only, and the caller holds that gate together with\n * the landscape-parsed one: under a single-target run the fleet tree is not\n * walked and the generated ids are unknown, so the question is unanswerable and\n * the check stays silent rather than half-answering.\n */\nimport type { ViewIdClaim } from \"../../../../core/c4/parsed/view-ids.js\";\nimport { SUBSYSTEM_VIEW_PREFIX, subsystemViewId } from \"../../../../core/repo/tree/views.js\";\nimport type { FleetTree } from \"../../../../core/repo/tree/walk.js\";\nimport type { Finding } from \"../../../../core/vocabulary/report.js\";\n\n/**\n * `authored` is every view id the `architecture/` project claims, each with the\n * file that claims it — static views included, because a static view claims an\n * id exactly as firmly as a dynamic one. Absent (the document did not parse, or\n * an older loader did not read them) is treated as \"nothing to compare\", never\n * as \"nothing collides\".\n */\nexport function viewIdFindings(authored: ViewIdClaim[] | undefined, tree: FleetTree): Finding[] {\n if (authored === undefined || authored.length === 0) return [];\n const generated = new Map(tree.subsystems.map((sub) => [subsystemViewId(sub), sub.path.join(\"/\")]));\n const findings: Finding[] = [];\n // Sorted and de-duplicated by id so a project claiming the same id twice —\n // itself already a parse error, but reachable through a partial read — yields\n // one finding per id rather than one per occurrence. The first claimant's file\n // is the one named: with a duplicate the project does not parse at all, so\n // there is never a second one to choose between.\n const claimants = new Map<string, string>();\n for (const claim of authored) if (!claimants.has(claim.id)) claimants.set(claim.id, claim.sourcePath);\n for (const [id, file] of [...claimants].sort(([a], [b]) => (a < b ? -1 : 1))) {\n const owner = generated.get(id);\n if (owner === undefined) continue;\n findings.push({\n severity: \"error\",\n code: \"subsystem.view-id-collision\",\n subject: id,\n message:\n `subsystems: architecture/${file} declares \\`view ${id}\\`, which is the id loam generates ` +\n `into architecture/subsystems.likec4 for services/${owner}/ — LikeC4 merges both files into one project, ` +\n `so it refuses the whole architecture/ project and renders nothing. ` +\n `Rename the view in architecture/${file}; ids beginning \\`${SUBSYSTEM_VIEW_PREFIX}\\` are loam's ` +\n \"to mint, and architecture/subsystems.likec4 is generated (`loam subsystem sync`) and must never be hand-edited.\",\n });\n }\n return findings;\n}\n"]}
1
+ {"version":3,"file":"ids.js","sourceRoot":"","sources":["../../../../../src/commands/validate/fleet/views/ids.ts"],"names":[],"mappings":"AAsCA,OAAO,EAAE,qBAAqB,EAAE,eAAe,EAAE,MAAM,4CAA4C,CAAC;AAIpG;;;;;;GAMG;AACH,MAAM,UAAU,cAAc,CAAC,QAAmC,EAAE,IAAe;IACjF,IAAI,QAAQ,KAAK,SAAS,IAAI,QAAQ,CAAC,MAAM,KAAK,CAAC;QAAE,OAAO,EAAE,CAAC;IAC/D,MAAM,SAAS,GAAG,IAAI,GAAG,CAAC,IAAI,CAAC,UAAU,CAAC,GAAG,CAAC,CAAC,GAAG,EAAE,EAAE,CAAC,CAAC,eAAe,CAAC,GAAG,CAAC,EAAE,GAAG,CAAC,IAAI,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC,CAAC;IACpG,MAAM,QAAQ,GAAc,EAAE,CAAC;IAC/B,2EAA2E;IAC3E,8EAA8E;IAC9E,+EAA+E;IAC/E,2EAA2E;IAC3E,iDAAiD;IACjD,MAAM,SAAS,GAAG,IAAI,GAAG,EAAkB,CAAC;IAC5C,KAAK,MAAM,KAAK,IAAI,QAAQ;QAAE,IAAI,CAAC,SAAS,CAAC,GAAG,CAAC,KAAK,CAAC,EAAE,CAAC;YAAE,SAAS,CAAC,GAAG,CAAC,KAAK,CAAC,EAAE,EAAE,KAAK,CAAC,UAAU,CAAC,CAAC;IACtG,KAAK,MAAM,CAAC,EAAE,EAAE,IAAI,CAAC,IAAI,CAAC,GAAG,SAAS,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,EAAE,CAAC;QAC7E,MAAM,KAAK,GAAG,SAAS,CAAC,GAAG,CAAC,EAAE,CAAC,CAAC;QAChC,IAAI,KAAK,KAAK,SAAS;YAAE,SAAS;QAClC,QAAQ,CAAC,IAAI,CAAC;YACZ,QAAQ,EAAE,OAAO;YACjB,IAAI,EAAE,6BAA6B;YACnC,OAAO,EAAE,EAAE;YACX,wEAAwE;YACxE,uEAAuE;YACvE,wEAAwE;YACxE,4DAA4D;YAC5D,SAAS,EAAE,CAAC,EAAE,IAAI,EAAE,gBAAgB,IAAI,EAAE,EAAE,IAAI,EAAE,SAAkB,EAAE,CAAC;YACvE,OAAO,EACL,4BAA4B,IAAI,oBAAoB,EAAE,qCAAqC;gBAC3F,oDAAoD,KAAK,iDAAiD;gBAC1G,qEAAqE;gBACrE,mCAAmC,IAAI,qBAAqB,qBAAqB,gBAAgB;gBACjG,iHAAiH;SACpH,CAAC,CAAC;IACL,CAAC;IACD,OAAO,QAAQ,CAAC;AAClB,CAAC","sourcesContent":["/**\n * An authored view id that collides with one loam GENERATES into\n * `architecture/subsystems.likec4`.\n *\n * This closes a green-while-broken hole, and the shape of it is measured rather\n * than argued. The LikeC4 renderer merges every `.likec4` file in a project into\n * one model, and `likec4.config.json` scopes the root project to\n * `architecture/` — so `landscape.likec4` and the generated `subsystems.likec4`\n * share ONE flat view-id namespace. Author `view subsystem_commerce { }` in the\n * landscape while the tree has a `commerce` subsystem and the renderer refuses\n * the ENTIRE `architecture/` project with two `Duplicate view` diagnostics, one\n * per file — while `loam validate --all` printed `landscape: N service(s)\n * modelled — architecture/landscape.likec4 and services/ agree`. Reproduced at\n * the 1.59.2 pin.\n *\n * Nothing else in a loam repo can produce this. A duplicate INSIDE one document\n * is already 2 LikeC4 errors, so `landscape.invalid` has it (measured). A\n * service's `model.likec4` and a feature's `delta.likec4` are each their own\n * LikeC4 project — `likec4.config.json` excludes `services/**` and\n * `features/**` from the root — so their view ids cannot collide with the\n * landscape's, and a delta's views never travel through `archive` anyway. That\n * leaves exactly one pair, which is why this check compares exactly one pair\n * rather than taking a census across every document loam parses.\n *\n * ONLY the real collision is refused, never the merely reserved-looking name.\n * A landscape view called `subsystem_overview` with no `overview` subsystem\n * breaks nothing today, and refusing it would be loam inventing a rule about a\n * name that harms no one. The latent case is not lost: the moment somebody\n * creates that subsystem, the generator mints the id, and this same check fires\n * on the same run. The message names the reservation anyway, so an author fixing\n * one collision learns not to make the next.\n *\n * Graded on `validate --all` only, and the caller holds that gate together with\n * the landscape-parsed one: under a single-target run the fleet tree is not\n * walked and the generated ids are unknown, so the question is unanswerable and\n * the check stays silent rather than half-answering.\n */\nimport type { ViewIdClaim } from \"../../../../core/c4/parsed/view-ids.js\";\nimport { SUBSYSTEM_VIEW_PREFIX, subsystemViewId } from \"../../../../core/repo/tree/render/views.js\";\nimport type { FleetTree } from \"../../../../core/repo/tree/walk.js\";\nimport type { Finding } from \"../../../../core/vocabulary/report.js\";\n\n/**\n * `authored` is every view id the `architecture/` project claims, each with the\n * file that claims it — static views included, because a static view claims an\n * id exactly as firmly as a dynamic one. Absent (the document did not parse, or\n * an older loader did not read them) is treated as \"nothing to compare\", never\n * as \"nothing collides\".\n */\nexport function viewIdFindings(authored: ViewIdClaim[] | undefined, tree: FleetTree): Finding[] {\n if (authored === undefined || authored.length === 0) return [];\n const generated = new Map(tree.subsystems.map((sub) => [subsystemViewId(sub), sub.path.join(\"/\")]));\n const findings: Finding[] = [];\n // Sorted and de-duplicated by id so a project claiming the same id twice —\n // itself already a parse error, but reachable through a partial read — yields\n // one finding per id rather than one per occurrence. The first claimant's file\n // is the one named: with a duplicate the project does not parse at all, so\n // there is never a second one to choose between.\n const claimants = new Map<string, string>();\n for (const claim of authored) if (!claimants.has(claim.id)) claimants.set(claim.id, claim.sourcePath);\n for (const [id, file] of [...claimants].sort(([a], [b]) => (a < b ? -1 : 1))) {\n const owner = generated.get(id);\n if (owner === undefined) continue;\n findings.push({\n severity: \"error\",\n code: \"subsystem.view-id-collision\",\n subject: id,\n // The CLAIMANT file — the one the message tells the reader to edit, and\n // the one the report's target fallback gets wrong: it files this under\n // the landscape, which is exactly the file a reader must NOT rename the\n // view in when the claim came from a document of their own.\n locations: [{ path: `architecture/${file}`, role: \"primary\" as const }],\n message:\n `subsystems: architecture/${file} declares \\`view ${id}\\`, which is the id loam generates ` +\n `into architecture/subsystems.likec4 for services/${owner}/ — LikeC4 merges both files into one project, ` +\n `so it refuses the whole architecture/ project and renders nothing. ` +\n `Rename the view in architecture/${file}; ids beginning \\`${SUBSYSTEM_VIEW_PREFIX}\\` are loam's ` +\n \"to mint, and architecture/subsystems.likec4 is generated (`loam subsystem sync`) and must never be hand-edited.\",\n });\n }\n return findings;\n}\n"]}
@@ -1,44 +1,8 @@
1
- /**
2
- * Staleness of the GENERATED `architecture/subsystems.likec4` against the
3
- * tree: exactly ONE error on exactly one file, repaired by exactly one
4
- * command (`loam subsystem sync`).
5
- *
6
- * A content compare, deliberately — never a parse. A views-only document does
7
- * not parse standalone (its `include` lines reference elements the landscape
8
- * defines; the spike in `core/repo/tree/views.ts` records it), and no check
9
- * anywhere reads the file's MEANING: the file is a scoping convenience for
10
- * the LikeC4 renderer, so the only question loam owes it is "is this what the
11
- * tree renders to". Rule 26 leaves this untouched: what loam may
12
- * read is a `dynamic view`'s declared steps out of a document it already
13
- * parses — never a static view's `include` predicates, and never this
14
- * generated file at all. Absent counts as a state of its own: a fleet
15
- * with subsystems and no file is stale, and a fleet with NO subsystems and a
16
- * leftover file is stale too — the render contract says the file must then
17
- * not exist, or a group deleted months ago keeps a view forever.
18
- *
19
- * The compare is over CONTENT, not BYTES, and `viewsAgree` in
20
- * `core/repo/tree/views.ts` both performs it and records why: an ordinary
21
- * Windows clone hands this file back with CRLF and not one fact in it changed,
22
- * and under a byte compare the advertised repair is a loop rather than a
23
- * diagnosis. The rule lives beside the generator because the module that mints
24
- * the bytes is the one that owes an answer about them.
25
- *
26
- * Graded only when the landscape PARSED (the caller holds that gate): the
27
- * expected bytes resolve members through the landscape's element→service
28
- * join, and grading them against a map that is missing or unreadable would
29
- * cascade a second error behind `landscape.missing`/`landscape.invalid`,
30
- * pointing at a file whose repair command would only re-derive the same
31
- * incomplete answer.
32
- */
33
- import { existsSync } from "node:fs";
34
- import { readFile } from "node:fs/promises";
35
1
  import { subsystemViewsPath } from "../../../../core/repo/paths.js";
36
- import { renderSubsystemViews, viewsAgree } from "../../../../core/repo/tree/views.js";
2
+ import { viewsState } from "../../../../core/repo/tree/render/stale.js";
37
3
  export async function viewsStaleFindings(docsDir, tree, elements) {
38
- const path = subsystemViewsPath(docsDir);
39
- const expected = renderSubsystemViews(tree, elements);
40
- const actual = existsSync(path) ? await readFile(path, "utf8") : null;
41
- if (viewsAgree(actual, expected))
4
+ const { actual, expected, agrees } = await viewsState(subsystemViewsPath(docsDir), tree, elements);
5
+ if (agrees)
42
6
  return [];
43
7
  const state = actual === null
44
8
  ? "does not exist, but the tree has subsystems to mirror"
@@ -50,6 +14,18 @@ export async function viewsStaleFindings(docsDir, tree, elements) {
50
14
  severity: "error",
51
15
  code: "subsystem.views-stale",
52
16
  subject: "architecture/subsystems.likec4",
17
+ // The file the message names, so an editor jumping to the finding lands
18
+ // on it. Without a location the report's fallback files this under the
19
+ // TARGET, which is the landscape — a reader was sent to
20
+ // architecture/landscape.likec4 to fix a file the sentence beside it
21
+ // named as architecture/subsystems.likec4. A file that does not exist
22
+ // yet cannot be `primary`: the actionable place is then the directory
23
+ // the regenerator will write into.
24
+ locations: [
25
+ actual === null
26
+ ? { path: "architecture", role: "scope" }
27
+ : { path: "architecture/subsystems.likec4", role: "primary" },
28
+ ],
53
29
  message: `subsystems: architecture/subsystems.likec4 ${state} — ` +
54
30
  `it is generated, one view per subsystem, and every derived scope drawn from it is wrong until it agrees. ` +
55
31
  "Run `loam subsystem sync` to regenerate it; never edit it by hand.",
@@ -1 +1 @@
1
- {"version":3,"file":"stale.js","sourceRoot":"","sources":["../../../../../src/commands/validate/fleet/views/stale.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA+BG;AACH,OAAO,EAAE,UAAU,EAAE,MAAM,SAAS,CAAC;AACrC,OAAO,EAAE,QAAQ,EAAE,MAAM,kBAAkB,CAAC;AAG5C,OAAO,EAAE,kBAAkB,EAAE,MAAM,gCAAgC,CAAC;AACpE,OAAO,EAAE,oBAAoB,EAAE,UAAU,EAAE,MAAM,qCAAqC,CAAC;AAIvF,MAAM,CAAC,KAAK,UAAU,kBAAkB,CACtC,OAAgB,EAChB,IAAe,EACf,QAAgB;IAEhB,MAAM,IAAI,GAAG,kBAAkB,CAAC,OAAO,CAAC,CAAC;IACzC,MAAM,QAAQ,GAAG,oBAAoB,CAAC,IAAI,EAAE,QAAQ,CAAC,CAAC;IACtD,MAAM,MAAM,GAAG,UAAU,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC,MAAM,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC,CAAC,CAAC,CAAC,IAAI,CAAC;IACtE,IAAI,UAAU,CAAC,MAAM,EAAE,QAAQ,CAAC;QAAE,OAAO,EAAE,CAAC;IAC5C,MAAM,KAAK,GACT,MAAM,KAAK,IAAI;QACb,CAAC,CAAC,uDAAuD;QACzD,CAAC,CAAC,QAAQ,KAAK,IAAI;YACjB,CAAC,CAAC,4EAA4E;YAC9E,CAAC,CAAC,yBAAyB,CAAC;IAClC,OAAO;QACL;YACE,QAAQ,EAAE,OAAO;YACjB,IAAI,EAAE,uBAAuB;YAC7B,OAAO,EAAE,gCAAgC;YACzC,OAAO,EACL,8CAA8C,KAAK,KAAK;gBACxD,2GAA2G;gBAC3G,oEAAoE;SACvE;KACF,CAAC;AACJ,CAAC","sourcesContent":["/**\n * Staleness of the GENERATED `architecture/subsystems.likec4` against the\n * tree: exactly ONE error on exactly one file, repaired by exactly one\n * command (`loam subsystem sync`).\n *\n * A content compare, deliberately — never a parse. A views-only document does\n * not parse standalone (its `include` lines reference elements the landscape\n * defines; the spike in `core/repo/tree/views.ts` records it), and no check\n * anywhere reads the file's MEANING: the file is a scoping convenience for\n * the LikeC4 renderer, so the only question loam owes it is \"is this what the\n * tree renders to\". Rule 26 leaves this untouched: what loam may\n * read is a `dynamic view`'s declared steps out of a document it already\n * parses — never a static view's `include` predicates, and never this\n * generated file at all. Absent counts as a state of its own: a fleet\n * with subsystems and no file is stale, and a fleet with NO subsystems and a\n * leftover file is stale too — the render contract says the file must then\n * not exist, or a group deleted months ago keeps a view forever.\n *\n * The compare is over CONTENT, not BYTES, and `viewsAgree` in\n * `core/repo/tree/views.ts` both performs it and records why: an ordinary\n * Windows clone hands this file back with CRLF and not one fact in it changed,\n * and under a byte compare the advertised repair is a loop rather than a\n * diagnosis. The rule lives beside the generator because the module that mints\n * the bytes is the one that owes an answer about them.\n *\n * Graded only when the landscape PARSED (the caller holds that gate): the\n * expected bytes resolve members through the landscape's element→service\n * join, and grading them against a map that is missing or unreadable would\n * cascade a second error behind `landscape.missing`/`landscape.invalid`,\n * pointing at a file whose repair command would only re-derive the same\n * incomplete answer.\n */\nimport { existsSync } from \"node:fs\";\nimport { readFile } from \"node:fs/promises\";\nimport type { Elem } from \"../../../../core/c4/likec4.js\";\nimport type { DocsDir } from \"../../../../core/kernel/ids/dirs.js\";\nimport { subsystemViewsPath } from \"../../../../core/repo/paths.js\";\nimport { renderSubsystemViews, viewsAgree } from \"../../../../core/repo/tree/views.js\";\nimport type { FleetTree } from \"../../../../core/repo/tree/walk.js\";\nimport type { Finding } from \"../../../../core/vocabulary/report.js\";\n\nexport async function viewsStaleFindings(\n docsDir: DocsDir,\n tree: FleetTree,\n elements: Elem[],\n): Promise<Finding[]> {\n const path = subsystemViewsPath(docsDir);\n const expected = renderSubsystemViews(tree, elements);\n const actual = existsSync(path) ? await readFile(path, \"utf8\") : null;\n if (viewsAgree(actual, expected)) return [];\n const state =\n actual === null\n ? \"does not exist, but the tree has subsystems to mirror\"\n : expected === null\n ? \"exists, but the tree has no subsystems — the generated file must be absent\"\n : \"does not match the tree\";\n return [\n {\n severity: \"error\",\n code: \"subsystem.views-stale\",\n subject: \"architecture/subsystems.likec4\",\n message:\n `subsystems: architecture/subsystems.likec4 ${state} — ` +\n `it is generated, one view per subsystem, and every derived scope drawn from it is wrong until it agrees. ` +\n \"Run `loam subsystem sync` to regenerate it; never edit it by hand.\",\n },\n ];\n}\n"]}
1
+ {"version":3,"file":"stale.js","sourceRoot":"","sources":["../../../../../src/commands/validate/fleet/views/stale.ts"],"names":[],"mappings":"AAkCA,OAAO,EAAE,kBAAkB,EAAE,MAAM,gCAAgC,CAAC;AACpE,OAAO,EAAE,UAAU,EAAE,MAAM,4CAA4C,CAAC;AAIxE,MAAM,CAAC,KAAK,UAAU,kBAAkB,CACtC,OAAgB,EAChB,IAAe,EACf,QAAgB;IAEhB,MAAM,EAAE,MAAM,EAAE,QAAQ,EAAE,MAAM,EAAE,GAAG,MAAM,UAAU,CAAC,kBAAkB,CAAC,OAAO,CAAC,EAAE,IAAI,EAAE,QAAQ,CAAC,CAAC;IACnG,IAAI,MAAM;QAAE,OAAO,EAAE,CAAC;IACtB,MAAM,KAAK,GACT,MAAM,KAAK,IAAI;QACb,CAAC,CAAC,uDAAuD;QACzD,CAAC,CAAC,QAAQ,KAAK,IAAI;YACjB,CAAC,CAAC,4EAA4E;YAC9E,CAAC,CAAC,yBAAyB,CAAC;IAClC,OAAO;QACL;YACE,QAAQ,EAAE,OAAO;YACjB,IAAI,EAAE,uBAAuB;YAC7B,OAAO,EAAE,gCAAgC;YACzC,wEAAwE;YACxE,uEAAuE;YACvE,wDAAwD;YACxD,qEAAqE;YACrE,sEAAsE;YACtE,sEAAsE;YACtE,mCAAmC;YACnC,SAAS,EAAE;gBACT,MAAM,KAAK,IAAI;oBACb,CAAC,CAAC,EAAE,IAAI,EAAE,cAAc,EAAE,IAAI,EAAE,OAAgB,EAAE;oBAClD,CAAC,CAAC,EAAE,IAAI,EAAE,gCAAgC,EAAE,IAAI,EAAE,SAAkB,EAAE;aACzE;YACD,OAAO,EACL,8CAA8C,KAAK,KAAK;gBACxD,2GAA2G;gBAC3G,oEAAoE;SACvE;KACF,CAAC;AACJ,CAAC","sourcesContent":["/**\n * Staleness of the GENERATED `architecture/subsystems.likec4` against the\n * tree: exactly ONE error on exactly one file, repaired by exactly one\n * command (`loam subsystem sync`).\n *\n * A content compare, deliberately — never a parse. A views-only document does\n * not parse standalone (its `include` lines reference elements the landscape\n * defines; the spike in `core/repo/tree/render/views.ts` records it), and no check\n * anywhere reads the file's MEANING: the file is a scoping convenience for\n * the LikeC4 renderer, so the only question loam owes it is \"is this what the\n * tree renders to\". Rule 26 leaves this untouched: what loam may\n * read is a `dynamic view`'s declared steps out of a document it already\n * parses — never a static view's `include` predicates, and never this\n * generated file at all. Absent counts as a state of its own: a fleet\n * with subsystems and no file is stale, and a fleet with NO subsystems and a\n * leftover file is stale too — the render contract says the file must then\n * not exist, or a group deleted months ago keeps a view forever.\n *\n * The compare is over CONTENT, not BYTES, and `viewsAgree` in\n * `core/repo/tree/render/views.ts` both performs it and records why: an ordinary\n * Windows clone hands this file back with CRLF and not one fact in it changed,\n * and under a byte compare the advertised repair is a loop rather than a\n * diagnosis. The rule lives beside the generator because the module that mints\n * the bytes is the one that owes an answer about them.\n *\n * Graded only when the landscape PARSED (the caller holds that gate): the\n * expected bytes resolve members through the landscape's element→service\n * join, and grading them against a map that is missing or unreadable would\n * cascade a second error behind `landscape.missing`/`landscape.invalid`,\n * pointing at a file whose repair command would only re-derive the same\n * incomplete answer.\n */\nimport type { Elem } from \"../../../../core/c4/likec4.js\";\nimport type { DocsDir } from \"../../../../core/kernel/ids/dirs.js\";\nimport { subsystemViewsPath } from \"../../../../core/repo/paths.js\";\nimport { viewsState } from \"../../../../core/repo/tree/render/stale.js\";\nimport type { FleetTree } from \"../../../../core/repo/tree/walk.js\";\nimport type { Finding } from \"../../../../core/vocabulary/report.js\";\n\nexport async function viewsStaleFindings(\n docsDir: DocsDir,\n tree: FleetTree,\n elements: Elem[],\n): Promise<Finding[]> {\n const { actual, expected, agrees } = await viewsState(subsystemViewsPath(docsDir), tree, elements);\n if (agrees) return [];\n const state =\n actual === null\n ? \"does not exist, but the tree has subsystems to mirror\"\n : expected === null\n ? \"exists, but the tree has no subsystems — the generated file must be absent\"\n : \"does not match the tree\";\n return [\n {\n severity: \"error\",\n code: \"subsystem.views-stale\",\n subject: \"architecture/subsystems.likec4\",\n // The file the message names, so an editor jumping to the finding lands\n // on it. Without a location the report's fallback files this under the\n // TARGET, which is the landscape — a reader was sent to\n // architecture/landscape.likec4 to fix a file the sentence beside it\n // named as architecture/subsystems.likec4. A file that does not exist\n // yet cannot be `primary`: the actionable place is then the directory\n // the regenerator will write into.\n locations: [\n actual === null\n ? { path: \"architecture\", role: \"scope\" as const }\n : { path: \"architecture/subsystems.likec4\", role: \"primary\" as const },\n ],\n message:\n `subsystems: architecture/subsystems.likec4 ${state} — ` +\n `it is generated, one view per subsystem, and every derived scope drawn from it is wrong until it agrees. ` +\n \"Run `loam subsystem sync` to regenerate it; never edit it by hand.\",\n },\n ];\n}\n"]}
@@ -177,6 +177,7 @@ export async function validateService(check) {
177
177
  relationships,
178
178
  land,
179
179
  known,
180
+ docsDir,
180
181
  })));
181
182
  // The authorization axis. Both requirement documents are graded against one
182
183
  // fleet vocabulary — a permission is a fleet fact, and an arch requirement
@@ -1 +1 @@
1
- {"version":3,"file":"service.js","sourceRoot":"","sources":["../../../../src/commands/validate/service/service.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;GAUG;AACH,OAAO,EAAE,UAAU,EAAE,MAAM,SAAS,CAAC;AACrC,OAAO,EAAE,QAAQ,EAAE,MAAM,WAAW,CAAC;AACrC,OAAO,EAAE,QAAQ,EAAuB,MAAM,4BAA4B,CAAC;AAC3E,OAAO,EAAE,eAAe,EAAE,MAAM,qCAAqC,CAAC;AACtE,OAAO,EAAwB,MAAM,qCAAqC,CAAC;AAC3E,OAAO,EAAE,aAAa,IAAI,aAAa,EAAE,eAAe,EAAE,MAAM,6BAA6B,CAAC;AAC9F,OAAO,EAAE,kBAAkB,EAAE,MAAM,sCAAsC,CAAC;AAC1E,OAAO,EAAE,cAAc,EAAE,MAAM,0CAA0C,CAAC;AAC1E,OAAO,EAAE,wBAAwB,EAAE,MAAM,4CAA4C,CAAC;AACtF,OAAO,EAAE,0BAA0B,EAAE,yBAAyB,EAAE,MAAM,wCAAwC,CAAC;AAC/G,OAAO,EAAE,qBAAqB,EAAE,uBAAuB,EAAE,MAAM,iDAAiD,CAAC;AACjH,OAAO,EAAE,iBAAiB,EAAE,wBAAwB,EAAE,MAAM,iCAAiC,CAAC;AAC9F,OAAO,EAAE,uBAAuB,EAAE,MAAM,2BAA2B,CAAC;AACpE,OAAO,EAAE,YAAY,EAAE,MAAM,4BAA4B,CAAC;AAC1D,OAAO,EAAkB,MAAM,4BAA4B,CAAC;AAC5D,OAAO,EAAmC,MAAM,oCAAoC,CAAC;AACrF,OAAO,EAAE,QAAQ,EAAE,MAAM,0BAA0B,CAAC;AACpD,OAAO,EAAE,iBAAiB,EAAE,MAAM,sCAAsC,CAAC;AACzE,OAAO,EAAE,eAAe,EAAE,MAAM,gCAAgC,CAAC;AACjE,OAAO,EAAE,WAAW,EAAE,MAAM,gCAAgC,CAAC;AAC7D,OAAO,EAAE,eAAe,EAAE,MAAM,qCAAqC,CAAC;AACtE,OAAO,EAAE,wBAAwB,EAAE,MAAM,2CAA2C,CAAC;AACrF,OAAO,EAAE,YAAY,EAAE,MAAM,gCAAgC,CAAC;AAC9D,OAAO,EAAE,SAAS,EAAE,MAAM,yBAAyB,CAAC;AACpD,OAAO,EAAE,mBAAmB,EAAE,MAAM,oBAAoB,CAAC;AACzD,OAAO,EAAE,mBAAmB,EAAE,MAAM,sBAAsB,CAAC;AAC3D,OAAO,EAAE,eAAe,EAAE,yBAAyB,EAAE,MAAM,UAAU,CAAC;AACtE,OAAO,EAAE,mBAAmB,EAAE,MAAM,oBAAoB,CAAC;AACzD,OAAO,EAAE,aAAa,EAAE,MAAM,YAAY,CAAC;AAC3C,OAAO,EAAE,iBAAiB,EAAE,MAAM,oBAAoB,CAAC;AACvD,OAAO,EAAE,gBAAgB,EAAE,gBAAgB,EAAE,MAAM,YAAY,CAAC;AAChE,OAAO,EAAE,wBAAwB,EAAE,MAAM,8CAA8C,CAAC;AA4CxF,MAAM,CAAC,KAAK,UAAU,eAAe,CAAC,KAAmB;IACvD,MAAM,EAAE,OAAO,EAAE,OAAO,EAAE,OAAO,EAAE,SAAS,EAAE,UAAU,EAAE,SAAS,EAAE,KAAK,EAAE,GAAG,KAAK,CAAC;IACrF,uEAAuE;IACvE,uEAAuE;IACvE,0EAA0E;IAC1E,4EAA4E;IAC5E,qEAAqE;IACrE,qFAAqF;IACrF,4DAA4D;IAC5D,MAAM,EAAE,GAAW,OAAO,CAAC;IAC3B,MAAM,QAAQ,GAAc,EAAE,CAAC;IAC/B,MAAM,KAAK,GAAG,MAAM,kBAAkB,CAAC,OAAO,EAAE,OAAO,EAAE,KAAK,CAAC,CAAC;IAChE,MAAM,MAAM,GAAiB;QAC3B,IAAI,EAAE,SAAS;QACf,EAAE,EAAE,OAAO;QACX,IAAI,EAAE,QAAQ,CAAC,OAAO,EAAE,KAAK,CAAC,GAAG,CAAC,CAAC,UAAU,CAAC,IAAI,EAAE,GAAG,CAAC;QACxD,QAAQ;KACT,CAAC;IAEF,4EAA4E;IAC5E,0EAA0E;IAC1E,IAAI,CAAC,UAAU,CAAC,KAAK,CAAC,GAAG,CAAC,EAAE,CAAC;QAC3B,MAAM,KAAK,GAAG,QAAQ,CAAC,OAAO,EAAE,CAAC,MAAM,YAAY,CAAC,OAAO,EAAE,KAAK,CAAC,CAAC,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC;QACvF,QAAQ,CAAC,IAAI,CAAC;YACZ,QAAQ,EAAE,OAAO;YACjB,IAAI,EAAE,iBAAiB;YACvB,OAAO,EACL,2BAA2B,KAAK,CAAC,GAAG,GAAG;gBACvC,CAAC,KAAK,CAAC,MAAM,GAAG,CAAC;oBACf,CAAC,CAAC,kBAAkB,KAAK,CAAC,IAAI,CAAC,IAAI,CAAC,GAAG;oBACvC,CAAC,CAAC,0CAA0C,CAAC;YACjD,IAAI,EAAE,EAAE,MAAM,EAAE,KAAK,EAAE;SACxB,CAAC,CAAC;QACH,OAAO,MAAM,CAAC;IAChB,CAAC;IAED,8EAA8E;IAC9E,2EAA2E;IAC3E,+EAA+E;IAC/E,0EAA0E;IAC1E,2EAA2E;IAC3E,6EAA6E;IAC7E,6EAA6E;IAC7E,qCAAqC;IACrC,MAAM,QAAQ,GAAG,UAAU,CAAC,KAAK,CAAC,KAAK,CAAC,CAAC;IACzC,IAAI,QAAQ,GAAW,EAAE,CAAC;IAC1B,IAAI,aAAa,GAAU,EAAE,CAAC;IAC9B,IAAI,CAAC,QAAQ,EAAE,CAAC;QACd,QAAQ,CAAC,IAAI,CAAC;YACZ,QAAQ,EAAE,OAAO;YACjB,IAAI,EAAE,kBAAkB;YACxB,OAAO,EAAE,kBAAkB,KAAK,CAAC,KAAK,6BAA6B,OAAO,UAAU;YACpF,IAAI,EAAE,EAAE,MAAM,EAAE,KAAK,EAAE;SACxB,CAAC,CAAC;IACL,CAAC;SAAM,CAAC;QACN,MAAM,KAAK,GAAG,KAAK,KAAK,SAAS,CAAC,CAAC,CAAC,MAAM,QAAQ,CAAC,KAAK,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC,MAAM,KAAK,CAAC,UAAU,CAAC,KAAK,CAAC,KAAK,CAAC,CAAC;QACtG,QAAQ,GAAG,KAAK,CAAC,QAAQ,CAAC;QAC1B,aAAa,GAAG,KAAK,CAAC,aAAa,CAAC;QACpC,IAAI,KAAK,CAAC,MAAM,CAAC,MAAM,GAAG,CAAC,EAAE,CAAC;YAC5B,QAAQ,CAAC,IAAI,CAAC;gBACZ,QAAQ,EAAE,OAAO;gBACjB,IAAI,EAAE,YAAY;gBAClB,OAAO,EAAE,GAAG,OAAO,kBAAkB,KAAK,CAAC,MAAM,CAAC,MAAM,WAAW;gBACnE,OAAO,EAAE,KAAK,CAAC,MAAM,CAAC,GAAG,CAAC,SAAS,CAAC;aACrC,CAAC,CAAC;QACL,CAAC;aAAM,CAAC;YACN,QAAQ,CAAC,IAAI,CAAC;gBACZ,QAAQ,EAAE,IAAI;gBACd,IAAI,EAAE,UAAU;gBAChB,OAAO,EAAE,GAAG,OAAO,qBAAqB,QAAQ,CAAC,MAAM,eAAe,aAAa,CAAC,MAAM,iBAAiB;aAC5G,CAAC,CAAC;QACL,CAAC;IACH,CAAC;IAED,4EAA4E;IAC5E,6EAA6E;IAC7E,gEAAgE;IAChE,MAAM,IAAI,GACR,SAAS;QACT,CAAC,UAAU,CAAC,aAAa,CAAC,OAAO,CAAC,CAAC;YACjC,CAAC,CAAC,KAAK,KAAK,SAAS;gBACnB,CAAC,CAAC,MAAM,QAAQ,CAAC,aAAa,CAAC,OAAO,CAAC,CAAC;gBACxC,CAAC,CAAC,MAAM,KAAK,CAAC,UAAU,CAAC,aAAa,CAAC,OAAO,CAAC,CAAC;YAClD,CAAC,CAAC,IAAI,CAAC,CAAC;IAEZ,mEAAmE;IACnE,uEAAuE;IACvE,4EAA4E;IAC5E,4EAA4E;IAC5E,0DAA0D;IAC1D,MAAM,KAAK,GAAG,IAAI,GAAG,CAAC,CAAC,MAAM,YAAY,CAAC,OAAO,EAAE,KAAK,CAAC,CAAC,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC;IAG7E,uEAAuE;IACvE,yEAAyE;IACzE,6EAA6E;IAC7E,wEAAwE;IACxE,MAAM,SAAS,GAAG,IAAI,KAAK,IAAI,CAAC,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC,eAAe,CAAC,IAAI,CAAC,QAAQ,EAAE,KAAK,CAAC,CAAC;IAE/E,MAAM,KAAK,GAAG,MAAM,gBAAgB,CAAC,EAAE,OAAO,EAAE,KAAK,EAAE,KAAK,EAAE,CAAC,CAAC;IAChE,QAAQ,CAAC,IAAI,CAAC,GAAG,KAAK,CAAC,QAAQ,CAAC,CAAC;IACjC,MAAM,EAAE,IAAI,EAAE,UAAU,EAAE,QAAQ,EAAE,QAAQ,EAAE,GAAG,KAAK,CAAC;IAEvD,MAAM,GAAG,GAAG,MAAM,eAAe,CAAC;QAChC,OAAO;QACP,EAAE;QACF,KAAK;QACL,IAAI;QACJ,UAAU;QACV,IAAI;QACJ,SAAS;QACT,KAAK;KACN,CAAC,CAAC;IACH,QAAQ,CAAC,IAAI,CAAC,GAAG,GAAG,CAAC,QAAQ,CAAC,CAAC;IAC/B,QAAQ,CAAC,IAAI,CACX,GAAG,aAAa,CAAC;QACf,OAAO;QACP,EAAE;QACF,IAAI;QACJ,SAAS;QACT,QAAQ,EAAE,GAAG,CAAC,QAAQ;QACtB,iBAAiB,EAAE,KAAK,CAAC,iBAAiB;KAC3C,CAAC,CACH,CAAC;IACF,QAAQ,CAAC,IAAI,CACX,GAAG,CAAC,MAAM,iBAAiB,CAAC;QAC1B,OAAO;QACP,OAAO;QACP,EAAE;QACF,KAAK;QACL,IAAI;QACJ,UAAU;QACV,QAAQ;QACR,IAAI;QACJ,SAAS;QACT,KAAK;QACL,KAAK;KACN,CAAC,CAAC,CACJ,CAAC;IACF,QAAQ,CAAC,IAAI,CACX,GAAG,CAAC,MAAM,gBAAgB,CAAC;QACzB,OAAO;QACP,KAAK;QACL,QAAQ;QACR,QAAQ;QACR,QAAQ;QACR,aAAa;QACb,IAAI;QACJ,KAAK;KACN,CAAC,CAAC,CACJ,CAAC;IAEF,4EAA4E;IAC5E,2EAA2E;IAC3E,2EAA2E;IAC3E,0EAA0E;IAC1E,+CAA+C;IAC/C,MAAM,UAAU,GAAG,MAAM,cAAc,CAAC,eAAe,CAAC,OAAO,CAAC,CAAC,CAAC;IAClE,MAAM,YAAY,GAChB,KAAK,KAAK,SAAS,CAAC,CAAC,CAAC,MAAM,wBAAwB,CAAC,OAAO,CAAC,CAAC,CAAC,CAAC,MAAM,KAAK,CAAC,YAAY,CAAC,OAAO,CAAC,CAAC;IACpG,4EAA4E;IAC5E,6EAA6E;IAC7E,2EAA2E;IAC3E,wEAAwE;IACxE,MAAM,cAAc,GAAG,MAAM,0BAA0B,CACrD,YAAY,EACZ,KAAK,KAAK,SAAS,CAAC,CAAC,CAAC,KAAK,EAAE,CAAC,EAAE,EAAE,CAAC,iBAAiB,CAAC,MAAM,wBAAwB,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,KAAK,CAAC,gBAAgB,CAAC,CAAC,CAAC,CAC3H,CAAC;IACF,KAAK,MAAM,CAAC,KAAK,EAAE,OAAO,CAAC,IAAI;QAC7B,CAAC,SAAS,EAAE,IAAI,CAAC;QACjB,CAAC,cAAc,EAAE,QAAQ,CAAC;KAClB,EAAE,CAAC;QACX,MAAM,MAAM,GAAG,EAAE,KAAK,EAAE,GAAG,OAAO,KAAK,KAAK,EAAE,EAAE,OAAO,EAAE,OAAO,EAAE,CAAC;QACnE,QAAQ,CAAC,IAAI,CAAC,GAAG,uBAAuB,CAAC,OAAO,EAAE,MAAM,EAAE,UAAU,CAAC,CAAC,CAAC;QACvE,QAAQ,CAAC,IAAI,CAAC,GAAG,yBAAyB,CAAC,OAAO,EAAE,MAAM,EAAE,YAAY,CAAC,CAAC,CAAC;QAC3E,QAAQ,CAAC,IAAI,CAAC,GAAG,uBAAuB,CAAC,OAAO,EAAE,MAAM,EAAE,cAAc,CAAC,CAAC,CAAC;QAC3E,QAAQ,CAAC,IAAI,CAAC,GAAG,qBAAqB,CAAC,OAAO,EAAE,MAAM,EAAE,cAAc,CAAC,CAAC,CAAC;QACzE,2EAA2E;QAC3E,yEAAyE;QACzE,sEAAsE;QACtE,QAAQ,CAAC,IAAI,CAAC,GAAG,wBAAwB,CAAC,OAAO,EAAE,MAAM,CAAC,CAAC,CAAC;IAC9D,CAAC;IAED,0EAA0E;IAC1E,+EAA+E;IAC/E,yEAAyE;IACzE,2DAA2D;IAC3D,QAAQ,CAAC,IAAI,CAAC,GAAG,CAAC,MAAM,mBAAmB,CAAC,EAAE,OAAO,EAAE,OAAO,EAAE,OAAO,EAAE,KAAK,EAAE,EAAE,KAAK,CAAC,CAAC,CAAC,CAAC;IAE3F,4EAA4E;IAC5E,QAAQ,CAAC,IAAI,CAAC,GAAG,CAAC,MAAM,iBAAiB,CAAC,OAAO,EAAE,OAAO,EAAE,EAAE,OAAO,EAAE,KAAK,EAAE,CAAC,CAAC,CAAC,CAAC;IAClF,QAAQ,CAAC,IAAI,CAAC,GAAG,CAAC,MAAM,mBAAmB,CAAC,OAAO,EAAE,OAAO,EAAE,OAAO,EAAE,KAAK,CAAC,CAAC,CAAC,CAAC;IAChF,0EAA0E;IAC1E,4EAA4E;IAC5E,4EAA4E;IAC5E,IAAI,OAAO,KAAK,SAAS,EAAE,CAAC;QAC1B,QAAQ,CAAC,IAAI,CAAC,GAAG,CAAC,MAAM,yBAAyB,CAAC,OAAO,EAAE,OAAO,EAAE,SAAS,EAAE,OAAO,EAAE,KAAK,CAAC,OAAO,CAAC,CAAC,CAAC,CAAC;IAC3G,CAAC;IAED,6EAA6E;IAC7E,sEAAsE;IACtE,+EAA+E;IAC/E,QAAQ,CAAC,IAAI,CAAC,GAAG,CAAC,MAAM,eAAe,CAAC,EAAE,OAAO,EAAE,OAAO,EAAE,OAAO,EAAE,UAAU,EAAE,KAAK,EAAE,CAAC,CAAC,CAAC,CAAC;IAE5F,2EAA2E;IAC3E,0EAA0E;IAC1E,4EAA4E;IAC5E,QAAQ,CAAC,IAAI,CAAC,GAAG,CAAC,MAAM,mBAAmB,CAAC,OAAO,EAAE,OAAO,EAAE,OAAO,EAAE,KAAK,CAAC,CAAC,CAAC,CAAC;IAEhF,4EAA4E;IAC5E,yEAAyE;IACzE,2EAA2E;IAC3E,0EAA0E;IAC1E,0EAA0E;IAC1E,sBAAsB;IACtB,IAAI,OAAO,KAAK,SAAS,EAAE,CAAC;QAC1B,IAAI,CAAC;YACH,MAAM,WAAW,GAAG,MAAM,wBAAwB,CAAC,WAAW,CAAC,OAAO,EAAE,UAAU,CAAC,CAAC,CAAC;YACrF,IAAI,WAAW,KAAK,IAAI;gBAAE,QAAQ,CAAC,OAAO,CAAC,WAAW,CAAC,CAAC;QAC1D,CAAC;QAAC,OAAO,GAAG,EAAE,CAAC;YACb,qEAAqE;YACrE,+DAA+D;YAC/D,IAAI,CAAC,CAAC,GAAG,YAAY,eAAe,CAAC;gBAAE,MAAM,GAAG,CAAC;QACnD,CAAC;IACH,CAAC;IAED,OAAO,MAAM,CAAC;AAChB,CAAC","sourcesContent":["/**\n * The service target: one adopted service graded across every axis it owns.\n *\n * This module is the ORDER, not the checks. It reads what more than one axis\n * joins through — the C4 model, the living landscape and its resolver, the two\n * requirement documents — hands each of those to the axis that grades it, and\n * pushes the findings in the sequence the report is read in. The axes\n * themselves cannot see each other, which is the point: `./api.js` decides once\n * whether there is a contract to read, and `./spine.js` is handed the answer\n * instead of re-deriving it against a file it would open a second time.\n */\nimport { existsSync } from \"node:fs\";\nimport { relative } from \"node:path\";\nimport { loadFile, type Elem, type Rel } from \"../../../core/c4/likec4.js\";\nimport { serviceResolver } from \"../../../core/c4/resolve/service.js\";\nimport { type PathableService } from \"../../../core/kernel/ids/service.js\";\nimport { landscapePath as landscapeFile, permissionsPath } from \"../../../core/repo/paths.js\";\nimport { locateServicePaths } from \"../../../core/repo/service-target.js\";\nimport { readVocabulary } from \"../../../core/permissions/permissions.js\";\nimport { readCapabilityVocabulary } from \"../../../core/capabilities/capabilities.js\";\nimport { capabilityRequirementIndex, capabilityUnknownFindings } from \"../../../core/capabilities/findings.js\";\nimport { realizesStaleFindings, realizesUnknownFindings } from \"../../../core/capabilities/realizes/findings.js\";\nimport { parseRequirements, readRequirementsDocument } from \"../../../core/document/parse.js\";\nimport { requiresUnknownFindings } from \"../checks/requirements.js\";\nimport { listServices } from \"../../../core/repo/repo.js\";\nimport { type LoadedDoc } from \"../../../core/c4/likec4.js\";\nimport { type Finding, type TargetReport } from \"../../../core/vocabulary/report.js\";\nimport { closeIds } from \"../../../core/c4/arch.js\";\nimport { serviceProvenance } from \"../../../core/provenance/findings.js\";\nimport { gherkinFindings } from \"../../../core/gherkin/stale.js\";\nimport { gherkinRoot } from \"../../../core/gherkin/stamp.js\";\nimport { UnsafePathError } from \"../../../core/kernel/path-safety.js\";\nimport { interruptedCommitFinding } from \"../../../core/staging/recovery/finding.js\";\nimport { FleetContext } from \"../../../core/fleet-context.js\";\nimport { errorText } from \"../checks/vocabulary.js\";\nimport { serviceLinkFindings } from \"../links/corpus.js\";\nimport { sourceScopeFindings } from \"../checks/sources.js\";\nimport { apiAxisFindings, generatedContractFindings } from \"./api.js\";\nimport { evidencePinFindings } from \"./evidence-pins.js\";\nimport { spineFindings } from \"./spine.js\";\nimport { eventAxisFindings } from \"./events/events.js\";\nimport { archAxisFindings, readServiceSpecs } from \"./specs.js\";\nimport { unknownDirectiveFindings } from \"../../../core/document/grammar/directives.js\";\nimport type { DocsDir } from \"../../../core/kernel/ids/dirs.js\";\n\n/**\n * A service's absences are graded by what each one proves.\n *\n * `service.unknown` (error): the directory itself does not exist — the id is a\n * typo until proven otherwise, so the hint names ids that DO exist and never\n * `loam adopt`, which would faithfully document the misspelling.\n * `service.no-model` (error): the directory is real but the C4 center is not —\n * adopt is the right hint. Every check that does not read the model still runs\n * (spec coverage, the arch axis, provenance, gherkin staleness): archive\n * creates exactly this state for a new service, and those signals must not go\n * quiet there.\n * `service.no-spec` / `service.no-openapi` (warn): the adopt brief marks both\n * required, but a fleet mid-rollout legitimately has part-adopted services —\n * the absence must stay visible without gating CI for months. The openapi warn\n * keeps quiet when the landscape proves nobody calls an operation on this\n * service: a worker with no API is not missing one.\n * `api.ops-unlinked` (warn): an OpenAPI and requirements that never name each\n * other pass every cross-axis check vacuously — a repo migrated from OpenSpec\n * does exactly that by default, and vacuous is not the same as checked.\n */\ninterface ServiceCheck {\n docsDir: DocsDir;\n service: PathableService;\n /** The service's own repo, when loam is standing in it. Undefined from the docs repo. */\n repoDir?: string;\n /** The build outputs `loam.json` names, when this repo declares any. */\n contracts?: { openapi?: string };\n /** The living landscape under --all; undefined means \"load it if you need it\", null means \"there is none\". */\n preloaded?: LoadedDoc | null;\n gherkinDir?: string;\n fleet?: FleetContext;\n /**\n * True when this run ALSO emits a `landscape` target. It decides one thing:\n * whether a landscape parse error's details are repeated here. Under --all\n * they are not — the landscape target already carries them once, and N copies\n * of one parser's cascade is the whole report.\n */\n landscapeReported?: boolean;\n}\n\n\nexport async function validateService(check: ServiceCheck): Promise<TargetReport> {\n const { docsDir, service, repoDir, preloaded, gherkinDir, contracts, fleet } = check;\n // The spine and event joins below all ask the landscape's resolver one\n // question: does this edge's endpoint resolve into the directory being\n // validated? The resolver answers with DOCUMENT text and `service` is the\n // repository's own name — disjoint brands, so `===` between them is TS2367.\n // Widening to `string` is not a cast: membership questions cross the\n // provenance line through plain-string containers by design (kernel/ids/service.ts),\n // and nothing pathable ever comes back out of a comparison.\n const me: string = service;\n const findings: Finding[] = [];\n const paths = await locateServicePaths(docsDir, service, fleet);\n const report: TargetReport = {\n kind: \"service\",\n id: service,\n path: relative(docsDir, paths.dir).replaceAll(\"\\\\\", \"/\"),\n findings,\n };\n\n // A directory that does not exist is a different fact from a directory with\n // everything missing: validating a typo must say \"typo\", not \"unadopted\".\n if (!existsSync(paths.dir)) {\n const close = closeIds(service, (await listServices(docsDir, fleet)).map((s) => s.id));\n findings.push({\n severity: \"error\",\n code: \"service.unknown\",\n message:\n `No service directory at ${paths.dir}.` +\n (close.length > 0\n ? ` Did you mean: ${close.join(\", \")}?`\n : \" `loam list services` shows what exists.\"),\n text: { marker: false },\n });\n return report;\n }\n\n // C4 model. Its absence is an error — this is where `adopt` comes in — but it\n // must NOT silence the rest of the gate stack: `loam archive` of a feature\n // introducing a new service creates exactly this state (spec.md, arch.spec.md,\n // openapi.yaml, no model.likec4), and an early return here suspended arch\n // coverage, health.uncovered, provenance (content_digest included) and the\n // gherkin staleness chain for the very services vouch had just promised them\n // to. So the finding is emitted and the walk CONTINUES; only the checks that\n // read the model itself are guarded.\n const hasModel = existsSync(paths.model);\n let elements: Elem[] = [];\n let relationships: Rel[] = [];\n if (!hasModel) {\n findings.push({\n severity: \"error\",\n code: \"service.no-model\",\n message: `No C4 model at ${paths.model}. Run \\`loam adopt\\` for '${service}' first.`,\n text: { marker: false },\n });\n } else {\n const model = fleet === undefined ? await loadFile(paths.model) : await fleet.loadLikeC4(paths.model);\n elements = model.elements;\n relationships = model.relationships;\n if (model.errors.length > 0) {\n findings.push({\n severity: \"error\",\n code: \"c4.invalid\",\n message: `${service}: C4 model has ${model.errors.length} error(s)`,\n details: model.errors.map(errorText),\n });\n } else {\n findings.push({\n severity: \"ok\",\n code: \"c4.valid\",\n message: `${service}: C4 model valid (${elements.length} elements · ${relationships.length} relationships)`,\n });\n }\n }\n\n // The living landscape, parsed at most once per run: under --all the caller\n // hands in the doc it already loaded, single-service runs load on demand. It\n // serves two checks below — the no-openapi grace and the spine.\n const land =\n preloaded ??\n (existsSync(landscapeFile(docsDir))\n ? fleet === undefined\n ? await loadFile(landscapeFile(docsDir))\n : await fleet.loadLikeC4(landscapeFile(docsDir))\n : null);\n\n // Which service directories actually exist — the positive evidence\n // `serviceOf` needs to resolve an edge drawn into a modelled CONTAINER\n // (`paymentService.api`) back to the service that owns it. Without it every\n // such edge resolves to the container's own title, i.e. to a service nobody\n // has ever adopted, and drops out of the spine unnoticed.\n const known = new Set((await listServices(docsDir, fleet)).map((s) => s.id));\n\n\n // The living landscape's element→service resolver, container-aware and\n // memoized. Every question below that asks \"does this edge point at me?\"\n // asks it through this one function, so the no-openapi grace, the HTTP spine\n // and the event spine can never disagree about which edges are inbound.\n const landSvcOf = land === null ? null : serviceResolver(land.elements, known);\n\n const specs = await readServiceSpecs({ service, paths, fleet });\n findings.push(...specs.findings);\n const { reqs, livingReqs, archText, archReqs } = specs;\n\n const api = await apiAxisFindings({\n service,\n me,\n paths,\n reqs,\n livingReqs,\n land,\n landSvcOf,\n fleet,\n });\n findings.push(...api.findings);\n findings.push(\n ...spineFindings({\n service,\n me,\n land,\n landSvcOf,\n contract: api.contract,\n landscapeReported: check.landscapeReported,\n }),\n );\n findings.push(\n ...(await eventAxisFindings({\n docsDir,\n service,\n me,\n paths,\n reqs,\n livingReqs,\n archReqs,\n land,\n landSvcOf,\n known,\n fleet,\n })),\n );\n findings.push(\n ...(await archAxisFindings({\n service,\n paths,\n archText,\n archReqs,\n elements,\n relationships,\n land,\n known,\n })),\n );\n\n // The authorization axis. Both requirement documents are graded against one\n // fleet vocabulary — a permission is a fleet fact, and an arch requirement\n // gates on one exactly as a business requirement does. The capability axis\n // rides the same loop against ITS fleet vocabulary; under --all the fleet\n // memo makes that one parse for the whole run.\n const vocabulary = await readVocabulary(permissionsPath(docsDir));\n const capabilities =\n fleet === undefined ? await readCapabilityVocabulary(docsDir) : await fleet.capabilities(docsDir);\n // The requirement-level index behind `Realizes:`. Built once for both axes,\n // and behind the same ladder `capabilityUnknownFindings` applies — an absent\n // or unreadable vocabulary means no reads and no grades at all, so a fleet\n // that has not adopted the axis pays nothing for a line it never wrote.\n const capabilityReqs = await capabilityRequirementIndex(\n capabilities,\n fleet === undefined ? async (p) => parseRequirements(await readRequirementsDocument(p)) : (p) => fleet.readRequirements(p),\n );\n for (const [label, docReqs] of [\n [\"spec.md\", reqs],\n [\"arch.spec.md\", archReqs],\n ] as const) {\n const target = { where: `${service}: ${label}`, subject: service };\n findings.push(...requiresUnknownFindings(docReqs, target, vocabulary));\n findings.push(...capabilityUnknownFindings(docReqs, target, capabilities));\n findings.push(...realizesUnknownFindings(docReqs, target, capabilityReqs));\n findings.push(...realizesStaleFindings(docReqs, target, capabilityReqs));\n // The grammar guard, last in the loop: it grades the KEYS the three checks\n // above resolve values from, so a line none of them saw is reported here\n // rather than staying invisible for having produced nothing to check.\n findings.push(...unknownDirectiveFindings(docReqs, target));\n }\n\n // The links this service's own documents write. Its place in the order is\n // deliberate: every axis above joins through a LINE loam defined, and this one\n // grades the join an author writes in prose — so it reads last among the\n // document checks and adds no input any of them depend on.\n findings.push(...(await serviceLinkFindings({ docsDir, subject: service, fleet }, paths)));\n\n // Provenance last: who vouched for this, and what code it was written from.\n findings.push(...(await serviceProvenance(docsDir, service, { repoDir, fleet })));\n findings.push(...(await sourceScopeFindings(docsDir, service, repoDir, fleet)));\n // The build's own contract against the committed copy. Service-repo only:\n // from the docs repo there is no build output to read, and the family stays\n // silent rather than guessing — the same rule `sources` provenance follows.\n if (repoDir !== undefined) {\n findings.push(...(await generatedContractFindings(service, repoDir, contracts?.openapi, paths.openapi)));\n }\n\n // The generated-gherkin freshness chain, service-repo-scoped like sources.*:\n // it needs the repo (the suite lives there), and it stays quiet until\n // <gherkinDir>/loam/ exists — a service that never generated has not opted in.\n findings.push(...(await gherkinFindings({ docsDir, service, repoDir, gherkinDir, fleet })));\n\n // The evidence-pin re-check, the same service-repo-scoped family: recorded\n // verification citations graded against THIS working tree, quiet from the\n // docs repo — sources.unverifiable-from-here already names that blind spot.\n findings.push(...(await evidencePinFindings(docsDir, service, repoDir, fleet)));\n\n // The one journal that does NOT live in the docs repo: gherkin commits into\n // the service repo's emission root. A half-committed suite graded by the\n // freshness chain above reads as merely stale — warn-severity, which never\n // gates — while doctor calls the same state a blocker. Same finding, same\n // code, led rather than appended, exactly as validate leads with the docs\n // repo's own journal.\n if (repoDir !== undefined) {\n try {\n const interrupted = await interruptedCommitFinding(gherkinRoot(repoDir, gherkinDir));\n if (interrupted !== null) findings.unshift(interrupted);\n } catch (err) {\n // An unsafe gherkinDir is `loam gherkin`'s refusal to make, with its\n // own sentence; grading declines to scan what it cannot trust.\n if (!(err instanceof UnsafePathError)) throw err;\n }\n }\n\n return report;\n}\n"]}
1
+ {"version":3,"file":"service.js","sourceRoot":"","sources":["../../../../src/commands/validate/service/service.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * The service target: one adopted service graded across every axis it owns.\n *\n * This module is the ORDER, not the checks. It reads what more than one axis\n * joins through — the C4 model, the living landscape and its resolver, the two\n * requirement documents — hands each of those to the axis that grades it, and\n * pushes the findings in the sequence the report is read in. The axes\n * themselves cannot see each other, which is the point: `./api.js` decides once\n * whether there is a contract to read, and `./spine.js` is handed the answer\n * instead of re-deriving it against a file it would open a second time.\n */\nimport { existsSync } from \"node:fs\";\nimport { relative } from \"node:path\";\nimport { loadFile, type Elem, type Rel } from \"../../../core/c4/likec4.js\";\nimport { serviceResolver } from \"../../../core/c4/resolve/service.js\";\nimport { type PathableService } from \"../../../core/kernel/ids/service.js\";\nimport { landscapePath as landscapeFile, permissionsPath } from \"../../../core/repo/paths.js\";\nimport { locateServicePaths } from \"../../../core/repo/service-target.js\";\nimport { readVocabulary } from \"../../../core/permissions/permissions.js\";\nimport { readCapabilityVocabulary } from \"../../../core/capabilities/capabilities.js\";\nimport { capabilityRequirementIndex, capabilityUnknownFindings } from \"../../../core/capabilities/findings.js\";\nimport { realizesStaleFindings, realizesUnknownFindings } from \"../../../core/capabilities/realizes/findings.js\";\nimport { parseRequirements, readRequirementsDocument } from \"../../../core/document/parse.js\";\nimport { requiresUnknownFindings } from \"../checks/requirements.js\";\nimport { listServices } from \"../../../core/repo/repo.js\";\nimport { type LoadedDoc } from \"../../../core/c4/likec4.js\";\nimport { type Finding, type TargetReport } from \"../../../core/vocabulary/report.js\";\nimport { closeIds } from \"../../../core/c4/arch.js\";\nimport { serviceProvenance } from \"../../../core/provenance/findings.js\";\nimport { gherkinFindings } from \"../../../core/gherkin/stale.js\";\nimport { gherkinRoot } from \"../../../core/gherkin/stamp.js\";\nimport { UnsafePathError } from \"../../../core/kernel/path-safety.js\";\nimport { interruptedCommitFinding } from \"../../../core/staging/recovery/finding.js\";\nimport { FleetContext } from \"../../../core/fleet-context.js\";\nimport { errorText } from \"../checks/vocabulary.js\";\nimport { serviceLinkFindings } from \"../links/corpus.js\";\nimport { sourceScopeFindings } from \"../checks/sources.js\";\nimport { apiAxisFindings, generatedContractFindings } from \"./api.js\";\nimport { evidencePinFindings } from \"./evidence-pins.js\";\nimport { spineFindings } from \"./spine.js\";\nimport { eventAxisFindings } from \"./events/events.js\";\nimport { archAxisFindings, readServiceSpecs } from \"./specs.js\";\nimport { unknownDirectiveFindings } from \"../../../core/document/grammar/directives.js\";\nimport type { DocsDir } from \"../../../core/kernel/ids/dirs.js\";\n\n/**\n * A service's absences are graded by what each one proves.\n *\n * `service.unknown` (error): the directory itself does not exist — the id is a\n * typo until proven otherwise, so the hint names ids that DO exist and never\n * `loam adopt`, which would faithfully document the misspelling.\n * `service.no-model` (error): the directory is real but the C4 center is not —\n * adopt is the right hint. Every check that does not read the model still runs\n * (spec coverage, the arch axis, provenance, gherkin staleness): archive\n * creates exactly this state for a new service, and those signals must not go\n * quiet there.\n * `service.no-spec` / `service.no-openapi` (warn): the adopt brief marks both\n * required, but a fleet mid-rollout legitimately has part-adopted services —\n * the absence must stay visible without gating CI for months. The openapi warn\n * keeps quiet when the landscape proves nobody calls an operation on this\n * service: a worker with no API is not missing one.\n * `api.ops-unlinked` (warn): an OpenAPI and requirements that never name each\n * other pass every cross-axis check vacuously — a repo migrated from OpenSpec\n * does exactly that by default, and vacuous is not the same as checked.\n */\ninterface ServiceCheck {\n docsDir: DocsDir;\n service: PathableService;\n /** The service's own repo, when loam is standing in it. Undefined from the docs repo. */\n repoDir?: string;\n /** The build outputs `loam.json` names, when this repo declares any. */\n contracts?: { openapi?: string };\n /** The living landscape under --all; undefined means \"load it if you need it\", null means \"there is none\". */\n preloaded?: LoadedDoc | null;\n gherkinDir?: string;\n fleet?: FleetContext;\n /**\n * True when this run ALSO emits a `landscape` target. It decides one thing:\n * whether a landscape parse error's details are repeated here. Under --all\n * they are not — the landscape target already carries them once, and N copies\n * of one parser's cascade is the whole report.\n */\n landscapeReported?: boolean;\n}\n\n\nexport async function validateService(check: ServiceCheck): Promise<TargetReport> {\n const { docsDir, service, repoDir, preloaded, gherkinDir, contracts, fleet } = check;\n // The spine and event joins below all ask the landscape's resolver one\n // question: does this edge's endpoint resolve into the directory being\n // validated? The resolver answers with DOCUMENT text and `service` is the\n // repository's own name — disjoint brands, so `===` between them is TS2367.\n // Widening to `string` is not a cast: membership questions cross the\n // provenance line through plain-string containers by design (kernel/ids/service.ts),\n // and nothing pathable ever comes back out of a comparison.\n const me: string = service;\n const findings: Finding[] = [];\n const paths = await locateServicePaths(docsDir, service, fleet);\n const report: TargetReport = {\n kind: \"service\",\n id: service,\n path: relative(docsDir, paths.dir).replaceAll(\"\\\\\", \"/\"),\n findings,\n };\n\n // A directory that does not exist is a different fact from a directory with\n // everything missing: validating a typo must say \"typo\", not \"unadopted\".\n if (!existsSync(paths.dir)) {\n const close = closeIds(service, (await listServices(docsDir, fleet)).map((s) => s.id));\n findings.push({\n severity: \"error\",\n code: \"service.unknown\",\n message:\n `No service directory at ${paths.dir}.` +\n (close.length > 0\n ? ` Did you mean: ${close.join(\", \")}?`\n : \" `loam list services` shows what exists.\"),\n text: { marker: false },\n });\n return report;\n }\n\n // C4 model. Its absence is an error — this is where `adopt` comes in — but it\n // must NOT silence the rest of the gate stack: `loam archive` of a feature\n // introducing a new service creates exactly this state (spec.md, arch.spec.md,\n // openapi.yaml, no model.likec4), and an early return here suspended arch\n // coverage, health.uncovered, provenance (content_digest included) and the\n // gherkin staleness chain for the very services vouch had just promised them\n // to. So the finding is emitted and the walk CONTINUES; only the checks that\n // read the model itself are guarded.\n const hasModel = existsSync(paths.model);\n let elements: Elem[] = [];\n let relationships: Rel[] = [];\n if (!hasModel) {\n findings.push({\n severity: \"error\",\n code: \"service.no-model\",\n message: `No C4 model at ${paths.model}. Run \\`loam adopt\\` for '${service}' first.`,\n text: { marker: false },\n });\n } else {\n const model = fleet === undefined ? await loadFile(paths.model) : await fleet.loadLikeC4(paths.model);\n elements = model.elements;\n relationships = model.relationships;\n if (model.errors.length > 0) {\n findings.push({\n severity: \"error\",\n code: \"c4.invalid\",\n message: `${service}: C4 model has ${model.errors.length} error(s)`,\n details: model.errors.map(errorText),\n });\n } else {\n findings.push({\n severity: \"ok\",\n code: \"c4.valid\",\n message: `${service}: C4 model valid (${elements.length} elements · ${relationships.length} relationships)`,\n });\n }\n }\n\n // The living landscape, parsed at most once per run: under --all the caller\n // hands in the doc it already loaded, single-service runs load on demand. It\n // serves two checks below — the no-openapi grace and the spine.\n const land =\n preloaded ??\n (existsSync(landscapeFile(docsDir))\n ? fleet === undefined\n ? await loadFile(landscapeFile(docsDir))\n : await fleet.loadLikeC4(landscapeFile(docsDir))\n : null);\n\n // Which service directories actually exist — the positive evidence\n // `serviceOf` needs to resolve an edge drawn into a modelled CONTAINER\n // (`paymentService.api`) back to the service that owns it. Without it every\n // such edge resolves to the container's own title, i.e. to a service nobody\n // has ever adopted, and drops out of the spine unnoticed.\n const known = new Set((await listServices(docsDir, fleet)).map((s) => s.id));\n\n\n // The living landscape's element→service resolver, container-aware and\n // memoized. Every question below that asks \"does this edge point at me?\"\n // asks it through this one function, so the no-openapi grace, the HTTP spine\n // and the event spine can never disagree about which edges are inbound.\n const landSvcOf = land === null ? null : serviceResolver(land.elements, known);\n\n const specs = await readServiceSpecs({ service, paths, fleet });\n findings.push(...specs.findings);\n const { reqs, livingReqs, archText, archReqs } = specs;\n\n const api = await apiAxisFindings({\n service,\n me,\n paths,\n reqs,\n livingReqs,\n land,\n landSvcOf,\n fleet,\n });\n findings.push(...api.findings);\n findings.push(\n ...spineFindings({\n service,\n me,\n land,\n landSvcOf,\n contract: api.contract,\n landscapeReported: check.landscapeReported,\n }),\n );\n findings.push(\n ...(await eventAxisFindings({\n docsDir,\n service,\n me,\n paths,\n reqs,\n livingReqs,\n archReqs,\n land,\n landSvcOf,\n known,\n fleet,\n })),\n );\n findings.push(\n ...(await archAxisFindings({\n service,\n paths,\n archText,\n archReqs,\n elements,\n relationships,\n land,\n known,\n docsDir,\n })),\n );\n\n // The authorization axis. Both requirement documents are graded against one\n // fleet vocabulary — a permission is a fleet fact, and an arch requirement\n // gates on one exactly as a business requirement does. The capability axis\n // rides the same loop against ITS fleet vocabulary; under --all the fleet\n // memo makes that one parse for the whole run.\n const vocabulary = await readVocabulary(permissionsPath(docsDir));\n const capabilities =\n fleet === undefined ? await readCapabilityVocabulary(docsDir) : await fleet.capabilities(docsDir);\n // The requirement-level index behind `Realizes:`. Built once for both axes,\n // and behind the same ladder `capabilityUnknownFindings` applies — an absent\n // or unreadable vocabulary means no reads and no grades at all, so a fleet\n // that has not adopted the axis pays nothing for a line it never wrote.\n const capabilityReqs = await capabilityRequirementIndex(\n capabilities,\n fleet === undefined ? async (p) => parseRequirements(await readRequirementsDocument(p)) : (p) => fleet.readRequirements(p),\n );\n for (const [label, docReqs] of [\n [\"spec.md\", reqs],\n [\"arch.spec.md\", archReqs],\n ] as const) {\n const target = { where: `${service}: ${label}`, subject: service };\n findings.push(...requiresUnknownFindings(docReqs, target, vocabulary));\n findings.push(...capabilityUnknownFindings(docReqs, target, capabilities));\n findings.push(...realizesUnknownFindings(docReqs, target, capabilityReqs));\n findings.push(...realizesStaleFindings(docReqs, target, capabilityReqs));\n // The grammar guard, last in the loop: it grades the KEYS the three checks\n // above resolve values from, so a line none of them saw is reported here\n // rather than staying invisible for having produced nothing to check.\n findings.push(...unknownDirectiveFindings(docReqs, target));\n }\n\n // The links this service's own documents write. Its place in the order is\n // deliberate: every axis above joins through a LINE loam defined, and this one\n // grades the join an author writes in prose — so it reads last among the\n // document checks and adds no input any of them depend on.\n findings.push(...(await serviceLinkFindings({ docsDir, subject: service, fleet }, paths)));\n\n // Provenance last: who vouched for this, and what code it was written from.\n findings.push(...(await serviceProvenance(docsDir, service, { repoDir, fleet })));\n findings.push(...(await sourceScopeFindings(docsDir, service, repoDir, fleet)));\n // The build's own contract against the committed copy. Service-repo only:\n // from the docs repo there is no build output to read, and the family stays\n // silent rather than guessing — the same rule `sources` provenance follows.\n if (repoDir !== undefined) {\n findings.push(...(await generatedContractFindings(service, repoDir, contracts?.openapi, paths.openapi)));\n }\n\n // The generated-gherkin freshness chain, service-repo-scoped like sources.*:\n // it needs the repo (the suite lives there), and it stays quiet until\n // <gherkinDir>/loam/ exists — a service that never generated has not opted in.\n findings.push(...(await gherkinFindings({ docsDir, service, repoDir, gherkinDir, fleet })));\n\n // The evidence-pin re-check, the same service-repo-scoped family: recorded\n // verification citations graded against THIS working tree, quiet from the\n // docs repo — sources.unverifiable-from-here already names that blind spot.\n findings.push(...(await evidencePinFindings(docsDir, service, repoDir, fleet)));\n\n // The one journal that does NOT live in the docs repo: gherkin commits into\n // the service repo's emission root. A half-committed suite graded by the\n // freshness chain above reads as merely stale — warn-severity, which never\n // gates — while doctor calls the same state a blocker. Same finding, same\n // code, led rather than appended, exactly as validate leads with the docs\n // repo's own journal.\n if (repoDir !== undefined) {\n try {\n const interrupted = await interruptedCommitFinding(gherkinRoot(repoDir, gherkinDir));\n if (interrupted !== null) findings.unshift(interrupted);\n } catch (err) {\n // An unsafe gherkinDir is `loam gherkin`'s refusal to make, with its\n // own sentence; grading declines to scan what it cannot trust.\n if (!(err instanceof UnsafePathError)) throw err;\n }\n }\n\n return report;\n}\n"]}
@@ -25,6 +25,9 @@ import { parseRequirements } from "../../../core/document/parse.js";
25
25
  import { assertionlessFindings, examplesFindings, steplessFindings, } from "../../../core/document/scenarios.js";
26
26
  import {} from "../../../core/document/spec.js";
27
27
  import {} from "../../../core/c4/arch.js";
28
+ import { hasDeployment } from "../../../core/c4/parsed/deployment.js";
29
+ import { loadArchitecture } from "../../../core/c4/project/architecture.js";
30
+ import {} from "../../../core/kernel/ids/dirs.js";
28
31
  import { readHealth } from "../../../core/vocabulary/health.js";
29
32
  import { documentConflictFinding } from "../../../core/conflict-markers.js";
30
33
  import { FleetContext } from "../../../core/fleet-context.js";
@@ -117,8 +120,35 @@ export async function readServiceSpecs(input) {
117
120
  : await fleet.readRequirements(paths.archSpec);
118
121
  return { findings, reqs, livingReqs, archText, archReqs };
119
122
  }
123
+ /**
124
+ * The topology a `node:` Covers entry resolves against, loaded at most once and
125
+ * only when something asks for it.
126
+ *
127
+ * Under `--all` the caller hands in the doc it already loaded — the whole
128
+ * `architecture/` project — so the deployment model is already here and this
129
+ * returns it. A SINGLE-service run loads only `landscape.likec4`, and a fleet
130
+ * that keeps its `deployment { }` block in a file beside it, which is the
131
+ * ordinary layout once one landscape file is the fleet's convention, would then
132
+ * resolve one `Covers:` line two different ways depending on which command
133
+ * asked. `loam status`'s two forms had exactly that disagreement and it was a
134
+ * defect both times; this is the same class, prevented rather than found.
135
+ *
136
+ * The cost is paid only by a service whose `arch.spec.md` actually writes a
137
+ * `node:` entry. A fleet with no topology at all pays one project load per such
138
+ * service — a state in which the entry is a typo anyway, and one load is what
139
+ * buys it a message that names the real ids.
140
+ */
141
+ async function topology(land, archReqs, docsDir) {
142
+ if (land?.deployment !== undefined && hasDeployment(land.deployment))
143
+ return land.deployment;
144
+ const wanted = coversEntries(archReqs).some((e) => e.form === "node" || e.form === "node-edge");
145
+ if (!wanted)
146
+ return undefined;
147
+ const project = await loadArchitecture(docsDir);
148
+ return project.errors.length === 0 ? project.deployment : undefined;
149
+ }
120
150
  export async function archAxisFindings(axis) {
121
- const { service, paths, archText, archReqs, elements, relationships, land, known } = axis;
151
+ const { service, paths, archText, archReqs, elements, relationships, land, known, docsDir } = axis;
122
152
  const findings = [];
123
153
  // The architecture spec axis — the obligations a business spec never carries
124
154
  // (the transactional outbox, retries, metrics, alerts). arch.spec.md is
@@ -188,9 +218,11 @@ export async function archAxisFindings(axis) {
188
218
  });
189
219
  }
190
220
  const landParses = land !== null && land.errors.length === 0 ? land : null;
221
+ const deployment = await topology(landParses, archReqs, docsDir);
191
222
  const scope = {
192
223
  elements: [...elements, ...(landParses?.elements ?? [])],
193
224
  relationships: [...relationships, ...(landParses?.relationships ?? [])],
225
+ ...(deployment === undefined ? {} : { deployment }),
194
226
  health: health.ids,
195
227
  known,
196
228
  };
@@ -1 +1 @@
1
- {"version":3,"file":"specs.js","sourceRoot":"","sources":["../../../../src/commands/validate/service/specs.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;GAgBG;AACH,OAAO,EAAE,UAAU,EAAE,MAAM,SAAS,CAAC;AACrC,OAAO,EAAE,QAAQ,EAAE,MAAM,kBAAkB,CAAC;AAC5C,OAAO,EAAuC,MAAM,4BAA4B,CAAC;AACjF,OAAO,EAAwB,MAAM,qCAAqC,CAAC;AAC3E,OAAO,EAAqB,MAAM,6BAA6B,CAAC;AAChE,OAAO,EAAgB,MAAM,oCAAoC,CAAC;AAClE,OAAO,EAAE,iBAAiB,EAAE,MAAM,iCAAiC,CAAC;AACpE,OAAO,EACL,qBAAqB,EACrB,gBAAgB,EAChB,gBAAgB,GACjB,MAAM,qCAAqC,CAAC;AAC7C,OAAO,EAAoB,MAAM,gCAAgC,CAAC;AAClE,OAAO,EAAsB,MAAM,0BAA0B,CAAC;AAC9D,OAAO,EAAE,UAAU,EAAE,MAAM,oCAAoC,CAAC;AAChE,OAAO,EAAE,uBAAuB,EAAE,MAAM,mCAAmC,CAAC;AAC5E,OAAO,EAAE,YAAY,EAAE,MAAM,gCAAgC,CAAC;AAC9D,OAAO,EAAE,wBAAwB,EAAE,MAAM,0BAA0B,CAAC;AACpE,OAAO,EACL,eAAe,EACf,aAAa,EACb,qBAAqB,EACrB,4BAA4B,EAC5B,wBAAwB,EACxB,qBAAqB,GACtB,MAAM,2BAA2B,CAAC;AAenC,MAAM,CAAC,KAAK,UAAU,gBAAgB,CAAC,KAItC;IACC,MAAM,EAAE,OAAO,EAAE,KAAK,EAAE,KAAK,EAAE,GAAG,KAAK,CAAC;IACxC,MAAM,QAAQ,GAAc,EAAE,CAAC;IAE/B,wBAAwB;IACxB,IAAI,IAAI,GAAkB,EAAE,CAAC;IAC7B,IAAI,UAAU,CAAC,KAAK,CAAC,IAAI,CAAC,EAAE,CAAC;QAC3B,MAAM,QAAQ,GACZ,KAAK,KAAK,SAAS,CAAC,CAAC,CAAC,MAAM,QAAQ,CAAC,KAAK,CAAC,IAAI,EAAE,MAAM,CAAC,CAAC,CAAC,CAAC,MAAM,KAAK,CAAC,QAAQ,CAAC,KAAK,CAAC,IAAI,CAAC,CAAC;QAC9F,IAAI,GAAG,KAAK,KAAK,SAAS,CAAC,CAAC,CAAC,iBAAiB,CAAC,QAAQ,CAAC,CAAC,CAAC,CAAC,MAAM,KAAK,CAAC,gBAAgB,CAAC,KAAK,CAAC,IAAI,CAAC,CAAC;QACpG,2EAA2E;QAC3E,uEAAuE;QACvE,yEAAyE;QACzE,uEAAuE;QACvE,uEAAuE;QACvE,oEAAoE;QACpE,0EAA0E;QAC1E,2EAA2E;QAC3E,qBAAqB;QACrB,MAAM,QAAQ,GAAG,uBAAuB,CAAC,GAAG,OAAO,WAAW,EAAE,OAAO,EAAE,QAAQ,CAAC,CAAC;QACnF,IAAI,QAAQ,KAAK,IAAI;YAAE,QAAQ,CAAC,IAAI,CAAC,QAAQ,CAAC,CAAC;QAC/C,wEAAwE;QACxE,0EAA0E;QAC1E,4EAA4E;QAC5E,kEAAkE;QAClE,2EAA2E;QAC3E,0EAA0E;QAC1E,SAAS;QACT,IAAI,IAAI,CAAC,MAAM,KAAK,CAAC,EAAE,CAAC;YACtB,QAAQ,CAAC,IAAI,CAAC;gBACZ,QAAQ,EAAE,MAAM;gBAChB,IAAI,EAAE,sBAAsB;gBAC5B,OAAO,EAAE,OAAO;gBAChB,OAAO,EAAE,GAAG,OAAO,qKAAqK;aACzL,CAAC,CAAC;QACL,CAAC;aAAM,CAAC;YACN,QAAQ,CAAC,IAAI,CAAC,eAAe,CAAC,GAAG,OAAO,gBAAgB,EAAE,IAAI,CAAC,CAAC,CAAC;QACnE,CAAC;QACD,qEAAqE;QACrE,kEAAkE;QAClE,wEAAwE;QACxE,2EAA2E;QAC3E,0EAA0E;QAC1E,0DAA0D;QAC1D,QAAQ,CAAC,IAAI,CAAC,GAAG,gBAAgB,CAAC,GAAG,OAAO,gBAAgB,EAAE,OAAO,EAAE,IAAI,CAAC,CAAC,CAAC;QAC9E,QAAQ,CAAC,IAAI,CAAC,GAAG,qBAAqB,CAAC,GAAG,OAAO,gBAAgB,EAAE,OAAO,EAAE,IAAI,CAAC,CAAC,CAAC;QACnF,QAAQ,CAAC,IAAI,CAAC,GAAG,gBAAgB,CAAC,GAAG,OAAO,gBAAgB,EAAE,OAAO,EAAE,IAAI,CAAC,CAAC,CAAC;QAC9E,QAAQ,CAAC,IAAI,CAAC,GAAG,4BAA4B,CAAC,IAAI,EAAE,GAAG,OAAO,WAAW,EAAE,OAAO,CAAC,CAAC,CAAC;QACrF,QAAQ,CAAC,IAAI,CAAC,GAAG,qBAAqB,CAAC,IAAI,EAAE,GAAG,OAAO,WAAW,EAAE,OAAO,CAAC,CAAC,CAAC;QAC9E,QAAQ,CAAC,IAAI,CAAC,GAAG,wBAAwB,CAAC,IAAI,EAAE,GAAG,OAAO,WAAW,EAAE,OAAO,CAAC,CAAC,CAAC;IACnF,CAAC;SAAM,CAAC;QACN,QAAQ,CAAC,IAAI,CAAC;YACZ,QAAQ,EAAE,MAAM;YAChB,IAAI,EAAE,iBAAiB;YACvB,OAAO,EAAE,qBAAqB,KAAK,CAAC,IAAI,0DAA0D;SACnG,CAAC,CAAC;IACL,CAAC;IAED,2EAA2E;IAC3E,8EAA8E;IAC9E,6EAA6E;IAC7E,2DAA2D;IAC3D,EAAE;IACF,6EAA6E;IAC7E,oEAAoE;IACpE,0EAA0E;IAC1E,8EAA8E;IAC9E,uDAAuD;IACvD,MAAM,UAAU,GAAG,IAAI,CAAC,MAAM,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,IAAI,KAAK,SAAS,CAAC,CAAC;IAE5D,6EAA6E;IAC7E,+EAA+E;IAC/E,8EAA8E;IAC9E,2EAA2E;IAC3E,+EAA+E;IAC/E,8DAA8D;IAC9D,MAAM,QAAQ,GAAG,UAAU,CAAC,KAAK,CAAC,QAAQ,CAAC;QACzC,CAAC,CAAC,KAAK,KAAK,SAAS;YACnB,CAAC,CAAC,MAAM,QAAQ,CAAC,KAAK,CAAC,QAAQ,EAAE,MAAM,CAAC;YACxC,CAAC,CAAC,MAAM,KAAK,CAAC,QAAQ,CAAC,KAAK,CAAC,QAAQ,CAAC;QACxC,CAAC,CAAC,IAAI,CAAC;IACT,MAAM,QAAQ,GACZ,QAAQ,KAAK,IAAI;QACf,CAAC,CAAC,EAAE;QACJ,CAAC,CAAC,KAAK,KAAK,SAAS;YACnB,CAAC,CAAC,iBAAiB,CAAC,QAAQ,CAAC;YAC7B,CAAC,CAAC,MAAM,KAAK,CAAC,gBAAgB,CAAC,KAAK,CAAC,QAAQ,CAAC,CAAC;IAErD,OAAO,EAAE,QAAQ,EAAE,IAAI,EAAE,UAAU,EAAE,QAAQ,EAAE,QAAQ,EAAE,CAAC;AAC5D,CAAC;AAsBD,MAAM,CAAC,KAAK,UAAU,gBAAgB,CAAC,IAAc;IACnD,MAAM,EAAE,OAAO,EAAE,KAAK,EAAE,QAAQ,EAAE,QAAQ,EAAE,QAAQ,EAAE,aAAa,EAAE,IAAI,EAAE,KAAK,EAAE,GAAG,IAAI,CAAC;IAC1F,MAAM,QAAQ,GAAc,EAAE,CAAC;IAE/B,6EAA6E;IAC7E,wEAAwE;IACxE,6EAA6E;IAC7E,4EAA4E;IAC5E,0EAA0E;IAC1E,6EAA6E;IAC7E,6EAA6E;IAC7E,qBAAqB;IACrB,MAAM,MAAM,GAAG,MAAM,UAAU,CAAC,KAAK,CAAC,MAAM,CAAC,CAAC;IAE9C,oEAAoE;IACpE,2EAA2E;IAC3E,2EAA2E;IAC3E,qEAAqE;IACrE,2EAA2E;IAC3E,yEAAyE;IACzE,2EAA2E;IAC3E,yEAAyE;IACzE,uEAAuE;IACvE,MAAM,MAAM,GAAG,QAAQ,CAAC,MAAM,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC,QAAQ,CAAC,GAAG,CAAC,CAAC,CAAC,MAAM,CAAC;IACjE,MAAM,IAAI,GAAG,MAAM,CAAC,UAAU,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,MAAM,CAAC,YAAY,CAAC;IAC1D,IAAI,QAAQ,CAAC,MAAM,GAAG,CAAC,IAAI,aAAa,CAAC,MAAM,KAAK,CAAC,IAAI,CAAC,MAAM,GAAG,CAAC,IAAI,IAAI,CAAC,MAAM,GAAG,CAAC,CAAC,EAAE,CAAC;QACzF,MAAM,QAAQ,GAAG;YACf,GAAG,CAAC,MAAM,GAAG,CAAC,CAAC,CAAC,CAAC,CAAC,GAAG,MAAM,4CAA4C,CAAC,CAAC,CAAC,CAAC,EAAE,CAAC;YAC9E,GAAG,CAAC,IAAI,CAAC,MAAM,GAAG,CAAC;gBACjB,CAAC,CAAC,CAAC,wBAAwB,IAAI,CAAC,MAAM,aAAa,IAAI,CAAC,MAAM,KAAK,CAAC,CAAC,CAAC,CAAC,GAAG,CAAC,CAAC,CAAC,KAAK,KAAK,IAAI,CAAC,IAAI,CAAC,IAAI,CAAC,GAAG,CAAC;gBAC1G,CAAC,CAAC,EAAE,CAAC;SACR,CAAC,IAAI,CAAC,QAAQ,CAAC,CAAC;QACjB,QAAQ,CAAC,IAAI,CAAC;YACZ,QAAQ,EAAE,MAAM;YAChB,IAAI,EAAE,qBAAqB;YAC3B,OAAO,EAAE,OAAO;YAChB,OAAO,EACL,GAAG,OAAO,2BAA2B,QAAQ,CAAC,MAAM,wCAAwC,QAAQ,KAAK;gBACzG,6FAA6F;gBAC7F,6FAA6F;SAChG,CAAC,CAAC;IACL,CAAC;IAED,IAAI,QAAQ,KAAK,IAAI,EAAE,CAAC;QACtB,iEAAiE;QACjE,0EAA0E;QAC1E,yEAAyE;QACzE,iEAAiE;QACjE,MAAM,QAAQ,GAAG,uBAAuB,CAAC,GAAG,OAAO,gBAAgB,EAAE,OAAO,EAAE,QAAQ,CAAC,CAAC;QACxF,IAAI,QAAQ,KAAK,IAAI;YAAE,QAAQ,CAAC,IAAI,CAAC,QAAQ,CAAC,CAAC;QAC/C,QAAQ,CAAC,IAAI,CAAC,eAAe,CAAC,GAAG,OAAO,qBAAqB,EAAE,QAAQ,CAAC,CAAC,CAAC;QAC1E,QAAQ,CAAC,IAAI,CAAC,GAAG,gBAAgB,CAAC,GAAG,OAAO,qBAAqB,EAAE,OAAO,EAAE,QAAQ,CAAC,CAAC,CAAC;QACvF,QAAQ,CAAC,IAAI,CAAC,GAAG,qBAAqB,CAAC,GAAG,OAAO,qBAAqB,EAAE,OAAO,EAAE,QAAQ,CAAC,CAAC,CAAC;QAC5F,QAAQ,CAAC,IAAI,CAAC,GAAG,gBAAgB,CAAC,GAAG,OAAO,qBAAqB,EAAE,OAAO,EAAE,QAAQ,CAAC,CAAC,CAAC;QACvF,QAAQ,CAAC,IAAI,CAAC,GAAG,4BAA4B,CAAC,QAAQ,EAAE,GAAG,OAAO,gBAAgB,EAAE,OAAO,CAAC,CAAC,CAAC;QAC9F,QAAQ,CAAC,IAAI,CAAC,GAAG,qBAAqB,CAAC,QAAQ,EAAE,GAAG,OAAO,gBAAgB,EAAE,OAAO,CAAC,CAAC,CAAC;QACvF,QAAQ,CAAC,IAAI,CAAC,GAAG,wBAAwB,CAAC,QAAQ,EAAE,GAAG,OAAO,gBAAgB,EAAE,OAAO,CAAC,CAAC,CAAC;IAC5F,CAAC;IACD,wEAAwE;IACxE,qEAAqE;IACrE,wEAAwE;IACxE,2EAA2E;IAC3E,2EAA2E;IAC3E,0DAA0D;IAC1D,IAAI,MAAM,CAAC,UAAU,EAAE,CAAC;QACtB,QAAQ,CAAC,IAAI,CAAC;YACZ,QAAQ,EAAE,MAAM;YAChB,IAAI,EAAE,gBAAgB;YACtB,OAAO,EAAE,OAAO;YAChB,OAAO,EAAE,GAAG,OAAO,+HAA+H;YAClJ,GAAG,CAAC,MAAM,CAAC,KAAK,KAAK,SAAS,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,EAAE,OAAO,EAAE,CAAC,MAAM,CAAC,KAAK,CAAC,EAAE,CAAC;SACnE,CAAC,CAAC;IACL,CAAC;IACD,MAAM,UAAU,GAAG,IAAI,KAAK,IAAI,IAAI,IAAI,CAAC,MAAM,CAAC,MAAM,KAAK,CAAC,CAAC,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC,IAAI,CAAC;IAC3E,MAAM,KAAK,GAAkB;QAC3B,QAAQ,EAAE,CAAC,GAAG,QAAQ,EAAE,GAAG,CAAC,UAAU,EAAE,QAAQ,IAAI,EAAE,CAAC,CAAC;QACxD,aAAa,EAAE,CAAC,GAAG,aAAa,EAAE,GAAG,CAAC,UAAU,EAAE,aAAa,IAAI,EAAE,CAAC,CAAC;QACvE,MAAM,EAAE,MAAM,CAAC,GAAG;QAClB,KAAK;KACN,CAAC;IACF,QAAQ,CAAC,IAAI,CACX,GAAG,qBAAqB,CAAC,QAAQ,EAAE,EAAE,KAAK,EAAE,GAAG,OAAO,gBAAgB,EAAE,OAAO,EAAE,OAAO,EAAE,EAAE,KAAK,EAAE,MAAM,CAAC,UAAU,CAAC,CACtH,CAAC;IACF,MAAM,YAAY,GAAG,aAAa,CAAC,QAAQ,CAAC,CAAC;IAC7C,KAAK,MAAM,EAAE,IAAI,EAAE,GAAG,EAAE,IAAI;QAC1B,EAAE,IAAI,EAAE,OAAgB,EAAE,GAAG,EAAE,MAAM,CAAC,GAAG,CAAC,MAAM,EAAE;QAClD,EAAE,IAAI,EAAE,KAAc,EAAE,GAAG,EAAE,MAAM,CAAC,GAAG,CAAC,IAAI,EAAE;KAC/C,EAAE,CAAC;QACF,KAAK,MAAM,EAAE,IAAI,GAAG,EAAE,CAAC;YACrB,IAAI,YAAY,CAAC,IAAI,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,IAAI,KAAK,IAAI,IAAI,CAAC,CAAC,EAAE,KAAK,EAAE,CAAC;gBAAE,SAAS;YACvE,QAAQ,CAAC,IAAI,CAAC;gBACZ,QAAQ,EAAE,MAAM;gBAChB,IAAI,EAAE,kBAAkB;gBACxB,OAAO,EAAE,OAAO;gBAChB,OAAO,EAAE,GAAG,OAAO,0BAA0B,IAAI,KAAK,OAAO,CAAC,CAAC,CAAC,OAAO,CAAC,CAAC,CAAC,KAAK,KAAK,EAAE,yEAAyE,IAAI,IAAI,EAAE,gDAAgD;aAC1N,CAAC,CAAC;QACL,CAAC;IACH,CAAC;IACD,2EAA2E;IAC3E,6EAA6E;IAC7E,QAAQ,CAAC,IAAI,CAAC,GAAG,wBAAwB,CAAC,EAAE,OAAO,EAAE,YAAY,EAAE,IAAI,EAAE,QAAQ,EAAE,SAAS,EAAE,KAAK,CAAC,KAAK,EAAE,CAAC,CAAC,CAAC;IAE9G,OAAO,QAAQ,CAAC;AAClB,CAAC","sourcesContent":["/**\n * This service's requirement documents: spec.md, arch.spec.md, and the\n * obligations the second of them carries that the first never does.\n *\n * Two entry points because the two happen at different moments in a service's\n * grading and the order of findings is part of the report. `readServiceSpecs`\n * runs first — everything downstream joins through the requirements it parses —\n * while `archAxisFindings` runs last, after both spines, because `covers.unknown`\n * resolves against the C4 model and the landscape and cannot be answered until\n * those are in hand.\n *\n * The arch axis is advisory in what it ASKS for (covers.unknown,\n * health.uncovered are warnings; arch.spec.md's absence is not a finding at all,\n * because partial adoption is supported) but not in whether the file is\n * readable: a conflict marker or a stepless scenario is graded exactly as it is\n * on the business axis. One file, one rule.\n */\nimport { existsSync } from \"node:fs\";\nimport { readFile } from \"node:fs/promises\";\nimport { type Elem, type LoadedDoc, type Rel } from \"../../../core/c4/likec4.js\";\nimport { type PathableService } from \"../../../core/kernel/ids/service.js\";\nimport { type ServicePaths } from \"../../../core/repo/paths.js\";\nimport { type Finding } from \"../../../core/vocabulary/report.js\";\nimport { parseRequirements } from \"../../../core/document/parse.js\";\nimport {\n assertionlessFindings,\n examplesFindings,\n steplessFindings,\n} from \"../../../core/document/scenarios.js\";\nimport { type Requirement } from \"../../../core/document/spec.js\";\nimport { type CoverageScope } from \"../../../core/c4/arch.js\";\nimport { readHealth } from \"../../../core/vocabulary/health.js\";\nimport { documentConflictFinding } from \"../../../core/conflict-markers.js\";\nimport { FleetContext } from \"../../../core/fleet-context.js\";\nimport { healthDependencyFindings } from \"../checks/health-deps.js\";\nimport {\n coverageFinding,\n coversEntries,\n coversUnknownFindings,\n duplicateRequirementFindings,\n repeatedListLineFindings,\n requirementIdFindings,\n} from \"../checks/requirements.js\";\n\n/** Both requirement documents, parsed once for every axis that joins through them. */\nexport interface ServiceSpecs {\n /** spec.md's findings, emitted before any cross-axis check runs. */\n findings: Finding[];\n /** Every requirement spec.md declares, REMOVED ones included. */\n reqs: Requirement[];\n /** The ones that still govern something. */\n livingReqs: Requirement[];\n /** arch.spec.md's text, or null when the service has none. */\n archText: string | null;\n archReqs: Requirement[];\n}\n\nexport async function readServiceSpecs(input: {\n service: PathableService;\n paths: ServicePaths;\n fleet?: FleetContext;\n}): Promise<ServiceSpecs> {\n const { service, paths, fleet } = input;\n const findings: Finding[] = [];\n\n // Requirement coverage.\n let reqs: Requirement[] = [];\n if (existsSync(paths.spec)) {\n const specText =\n fleet === undefined ? await readFile(paths.spec, \"utf8\") : await fleet.readText(paths.spec);\n reqs = fleet === undefined ? parseRequirements(specText) : await fleet.readRequirements(paths.spec);\n // Conflict markers first, because they say that nothing below this line is\n // anyone's text. An error, gating by the default rule and deliberately\n // without `gates`: that field is coherence's (issue.ts) and archive does\n // not read validate's findings, so claiming it here would be a promise\n // about a gate that is not wired. What makes it an error rather than a\n // warning is what the next merge does — `loam archive` rewrites the\n // requirements section of this very file and takes whichever marker lines\n // fall inside it, turning a conflict anyone can see into a document nobody\n // can tell is wrong.\n const conflict = documentConflictFinding(`${service}: spec.md`, service, specText);\n if (conflict !== null) findings.push(conflict);\n // A living spec with no `### Requirement:` block at all is the baseline\n // `loam adopt` scaffolds and nobody ever filled in. \"requirements covered\n // (0 requirements, all with scenarios)\" is true of it and says the opposite\n // of what a reader needs: every cross-axis check downstream — API\n // governance, the Operations: spine, gherkin generation — is vacuous here,\n // and the green tick is what let a whole fleet score `sourced` over empty\n // files.\n if (reqs.length === 0) {\n findings.push({\n severity: \"warn\",\n code: \"spec.no-requirements\",\n subject: service,\n message: `${service}: spec.md holds no '### Requirement:' blocks — every requirement-driven check below is vacuous, and nothing here can go out of date because nothing is written down`,\n });\n } else {\n findings.push(coverageFinding(`${service}: requirements`, reqs));\n }\n // The other half of the coverage question, and an error for the same\n // reason the first half is: the whole claim of coverage is that a\n // requirement has a scenario something can RUN. A heading with no steps\n // satisfies the count and tests nothing — cucumber runs it vacuously green\n // and `loam verify --results` can never confirm it — so a fleet gate that\n // called it covered was certifying the absence of a test.\n findings.push(...steplessFindings(`${service}: requirements`, service, reqs));\n findings.push(...assertionlessFindings(`${service}: requirements`, service, reqs));\n findings.push(...examplesFindings(`${service}: requirements`, service, reqs));\n findings.push(...duplicateRequirementFindings(reqs, `${service}: spec.md`, service));\n findings.push(...requirementIdFindings(reqs, `${service}: spec.md`, service));\n findings.push(...repeatedListLineFindings(reqs, `${service}: spec.md`, service));\n } else {\n findings.push({\n severity: \"warn\",\n code: \"service.no-spec\",\n message: `No living spec at ${paths.spec} — requirement coverage and API governance are unchecked`,\n });\n }\n\n // The requirements that still govern anything. A REMOVED requirement is on\n // its way out together with the operations it names: it makes no claim on the\n // contract and governs nothing once the retiring feature archives — the same\n // position coherence/coherence.ts takes on the delta side.\n //\n // Spelled once because it was spelled three times and dropped on the fourth:\n // `governedOps` filtered, `spec-api.op-undefined` filtered, and the\n // `api.covered` join did not — so an operation whose only requirement was\n // REMOVED counted as governed, and the one operation about to be left without\n // a requirement was the one the report called covered.\n const livingReqs = reqs.filter((r) => r.kind !== \"REMOVED\");\n\n // The architecture spec, read HERE with spec.md rather than with the rest of\n // its own axis below, because the event links are a requirement-level join and\n // BOTH requirement files carry them. Events are architecture as often as they\n // are business — the outbox requirement in a service's arch.spec.md is the\n // canonical place a `Publishes:` line belongs — so handing the event axis only\n // spec.md would silently ignore the lines most authors write.\n const archText = existsSync(paths.archSpec)\n ? fleet === undefined\n ? await readFile(paths.archSpec, \"utf8\")\n : await fleet.readText(paths.archSpec)\n : null;\n const archReqs =\n archText === null\n ? []\n : fleet === undefined\n ? parseRequirements(archText)\n : await fleet.readRequirements(paths.archSpec);\n\n return { findings, reqs, livingReqs, archText, archReqs };\n}\n\n/** What the arch axis needs once the model and the landscape have been read. */\nexport interface ArchAxis {\n service: PathableService;\n paths: ServicePaths;\n /** arch.spec.md's text, or null — its absence is not a finding. */\n archText: string | null;\n archReqs: Requirement[];\n /** The service's own C4 model, empty when it has none. */\n elements: Elem[];\n relationships: Rel[];\n /** The living landscape, or null when the repo has none. */\n land: LoadedDoc | null;\n /**\n * The enumerated fleet, for the Covers matcher's endpoint resolution — the\n * same set the caller's own spine resolution uses, so a `Covers:` line\n * naming a service matches an edge drawn into that service's container.\n */\n known?: ReadonlySet<string>;\n}\n\nexport async function archAxisFindings(axis: ArchAxis): Promise<Finding[]> {\n const { service, paths, archText, archReqs, elements, relationships, land, known } = axis;\n const findings: Finding[] = [];\n\n // The architecture spec axis — the obligations a business spec never carries\n // (the transactional outbox, retries, metrics, alerts). arch.spec.md is\n // optional and its ABSENCE is not a finding (partial adoption is supported);\n // what exists must hold together: every requirement needs a scenario, every\n // `Covers:` entry must resolve (covers.unknown), and every alert/SLI that\n // health.yaml declares wants a covering requirement (health.uncovered) — the\n // moment health.yaml stops being inert. Warnings, never gates: `--strict` is\n // the CI escalation.\n const health = await readHealth(paths.health);\n\n // A model that reaches nothing is almost never true — but only when\n // something else attests it should reach. A deliberately shallow synthetic\n // fixture can hold elements without relationships beside sources that name\n // dependencies, yet `0 errors, 0 warnings` would still read as done.\n // Evidence-gated on purpose: a bare root-plus-one-container baseline stays\n // silent (nothing PROVES it thin — the standard mid-adoption shape), the\n // warn needs either a second nested element with no edge joining anything,\n // or dependencies declared in this service's own health.yaml. The health\n // half goes quiet when the file is unreadable — no claims on bad data.\n const nested = elements.filter((e) => e.id.includes(\".\")).length;\n const deps = health.unreadable ? [] : health.dependencies;\n if (elements.length > 0 && relationships.length === 0 && (nested > 1 || deps.length > 0)) {\n const evidence = [\n ...(nested > 1 ? [`${nested} nested elements with no edge joining them`] : []),\n ...(deps.length > 0\n ? [`health.yaml declares ${deps.length} dependenc${deps.length === 1 ? \"y\" : \"ies\"} (${deps.join(\", \")})`]\n : []),\n ].join(\", and \");\n findings.push({\n severity: \"warn\",\n code: \"c4.no-relationships\",\n subject: service,\n message:\n `${service}: model.likec4 declares ${elements.length} element(s) and 0 relationships, yet ${evidence} — ` +\n `a model that reaches nothing is almost never true; draw the edges the service actually has ` +\n `(what its containers call, what reads and writes its stores, what it depends on at runtime)`,\n });\n }\n\n if (archText !== null) {\n // The arch axis is advisory in what it ASKS for (covers.unknown,\n // health.uncovered are warnings) but not in whether the file is readable:\n // both breaches below are about the document itself, and they are graded\n // exactly as they are on the business axis — one file, one rule.\n const conflict = documentConflictFinding(`${service}: arch.spec.md`, service, archText);\n if (conflict !== null) findings.push(conflict);\n findings.push(coverageFinding(`${service}: arch requirements`, archReqs));\n findings.push(...steplessFindings(`${service}: arch requirements`, service, archReqs));\n findings.push(...assertionlessFindings(`${service}: arch requirements`, service, archReqs));\n findings.push(...examplesFindings(`${service}: arch requirements`, service, archReqs));\n findings.push(...duplicateRequirementFindings(archReqs, `${service}: arch.spec.md`, service));\n findings.push(...requirementIdFindings(archReqs, `${service}: arch.spec.md`, service));\n findings.push(...repeatedListLineFindings(archReqs, `${service}: arch.spec.md`, service));\n }\n // A health.yaml that exists but does not read is reported once, and the\n // checks that read it go quiet BOTH ways: its ids are unknown, so no\n // health.uncovered obligation exists (the empty id set below is already\n // silent), and no `Covers: alert:/sli:` entry may be graded a typo against\n // ids nobody could read (coversUnknownFindings mutes on the flag). A warn,\n // not an error: the axis it feeds is advisory end to end.\n if (health.unreadable) {\n findings.push({\n severity: \"warn\",\n code: \"health.invalid\",\n subject: service,\n message: `${service}: health.yaml does not parse — alert/SLI ids are unreadable, so Covers: alert:/sli: entries and health coverage are unchecked`,\n ...(health.error === undefined ? {} : { details: [health.error] }),\n });\n }\n const landParses = land !== null && land.errors.length === 0 ? land : null;\n const scope: CoverageScope = {\n elements: [...elements, ...(landParses?.elements ?? [])],\n relationships: [...relationships, ...(landParses?.relationships ?? [])],\n health: health.ids,\n known,\n };\n findings.push(\n ...coversUnknownFindings(archReqs, { where: `${service}: arch.spec.md`, subject: service }, scope, health.unreadable),\n );\n const activeCovers = coversEntries(archReqs);\n for (const { form, ids } of [\n { form: \"alert\" as const, ids: health.ids.alerts },\n { form: \"sli\" as const, ids: health.ids.slis },\n ]) {\n for (const id of ids) {\n if (activeCovers.some((e) => e.form === form && e.id === id)) continue;\n findings.push({\n severity: \"warn\",\n code: \"health.uncovered\",\n subject: service,\n message: `${service}: health.yaml declares ${form === \"alert\" ? \"alert\" : \"SLI\"} '${id}' but no arch.spec.md requirement covers it — write one with 'Covers: ${form}:${id}', or the signal ships with nothing testing it`,\n });\n }\n }\n // The other direction health.yaml is read in: its dependencies must be the\n // model's dependencies. `deps` is already muted on an unreadable file above.\n findings.push(...healthDependencyFindings({ service, dependencies: deps, elements, modelPath: paths.model }));\n\n return findings;\n}\n"]}
1
+ {"version":3,"file":"specs.js","sourceRoot":"","sources":["../../../../src/commands/validate/service/specs.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * This service's requirement documents: spec.md, arch.spec.md, and the\n * obligations the second of them carries that the first never does.\n *\n * Two entry points because the two happen at different moments in a service's\n * grading and the order of findings is part of the report. `readServiceSpecs`\n * runs first — everything downstream joins through the requirements it parses —\n * while `archAxisFindings` runs last, after both spines, because `covers.unknown`\n * resolves against the C4 model and the landscape and cannot be answered until\n * those are in hand.\n *\n * The arch axis is advisory in what it ASKS for (covers.unknown,\n * health.uncovered are warnings; arch.spec.md's absence is not a finding at all,\n * because partial adoption is supported) but not in whether the file is\n * readable: a conflict marker or a stepless scenario is graded exactly as it is\n * on the business axis. One file, one rule.\n */\nimport { existsSync } from \"node:fs\";\nimport { readFile } from \"node:fs/promises\";\nimport { type Elem, type LoadedDoc, type Rel } from \"../../../core/c4/likec4.js\";\nimport { type PathableService } from \"../../../core/kernel/ids/service.js\";\nimport { type ServicePaths } from \"../../../core/repo/paths.js\";\nimport { type Finding } from \"../../../core/vocabulary/report.js\";\nimport { parseRequirements } from \"../../../core/document/parse.js\";\nimport {\n assertionlessFindings,\n examplesFindings,\n steplessFindings,\n} from \"../../../core/document/scenarios.js\";\nimport { type Requirement } from \"../../../core/document/spec.js\";\nimport { type CoverageScope } from \"../../../core/c4/arch.js\";\nimport { hasDeployment, type DeploymentModel } from \"../../../core/c4/parsed/deployment.js\";\nimport { loadArchitecture } from \"../../../core/c4/project/architecture.js\";\nimport { type DocsDir } from \"../../../core/kernel/ids/dirs.js\";\nimport { readHealth } from \"../../../core/vocabulary/health.js\";\nimport { documentConflictFinding } from \"../../../core/conflict-markers.js\";\nimport { FleetContext } from \"../../../core/fleet-context.js\";\nimport { healthDependencyFindings } from \"../checks/health-deps.js\";\nimport {\n coverageFinding,\n coversEntries,\n coversUnknownFindings,\n duplicateRequirementFindings,\n repeatedListLineFindings,\n requirementIdFindings,\n} from \"../checks/requirements.js\";\n\n/** Both requirement documents, parsed once for every axis that joins through them. */\nexport interface ServiceSpecs {\n /** spec.md's findings, emitted before any cross-axis check runs. */\n findings: Finding[];\n /** Every requirement spec.md declares, REMOVED ones included. */\n reqs: Requirement[];\n /** The ones that still govern something. */\n livingReqs: Requirement[];\n /** arch.spec.md's text, or null when the service has none. */\n archText: string | null;\n archReqs: Requirement[];\n}\n\nexport async function readServiceSpecs(input: {\n service: PathableService;\n paths: ServicePaths;\n fleet?: FleetContext;\n}): Promise<ServiceSpecs> {\n const { service, paths, fleet } = input;\n const findings: Finding[] = [];\n\n // Requirement coverage.\n let reqs: Requirement[] = [];\n if (existsSync(paths.spec)) {\n const specText =\n fleet === undefined ? await readFile(paths.spec, \"utf8\") : await fleet.readText(paths.spec);\n reqs = fleet === undefined ? parseRequirements(specText) : await fleet.readRequirements(paths.spec);\n // Conflict markers first, because they say that nothing below this line is\n // anyone's text. An error, gating by the default rule and deliberately\n // without `gates`: that field is coherence's (issue.ts) and archive does\n // not read validate's findings, so claiming it here would be a promise\n // about a gate that is not wired. What makes it an error rather than a\n // warning is what the next merge does — `loam archive` rewrites the\n // requirements section of this very file and takes whichever marker lines\n // fall inside it, turning a conflict anyone can see into a document nobody\n // can tell is wrong.\n const conflict = documentConflictFinding(`${service}: spec.md`, service, specText);\n if (conflict !== null) findings.push(conflict);\n // A living spec with no `### Requirement:` block at all is the baseline\n // `loam adopt` scaffolds and nobody ever filled in. \"requirements covered\n // (0 requirements, all with scenarios)\" is true of it and says the opposite\n // of what a reader needs: every cross-axis check downstream — API\n // governance, the Operations: spine, gherkin generation — is vacuous here,\n // and the green tick is what let a whole fleet score `sourced` over empty\n // files.\n if (reqs.length === 0) {\n findings.push({\n severity: \"warn\",\n code: \"spec.no-requirements\",\n subject: service,\n message: `${service}: spec.md holds no '### Requirement:' blocks — every requirement-driven check below is vacuous, and nothing here can go out of date because nothing is written down`,\n });\n } else {\n findings.push(coverageFinding(`${service}: requirements`, reqs));\n }\n // The other half of the coverage question, and an error for the same\n // reason the first half is: the whole claim of coverage is that a\n // requirement has a scenario something can RUN. A heading with no steps\n // satisfies the count and tests nothing — cucumber runs it vacuously green\n // and `loam verify --results` can never confirm it — so a fleet gate that\n // called it covered was certifying the absence of a test.\n findings.push(...steplessFindings(`${service}: requirements`, service, reqs));\n findings.push(...assertionlessFindings(`${service}: requirements`, service, reqs));\n findings.push(...examplesFindings(`${service}: requirements`, service, reqs));\n findings.push(...duplicateRequirementFindings(reqs, `${service}: spec.md`, service));\n findings.push(...requirementIdFindings(reqs, `${service}: spec.md`, service));\n findings.push(...repeatedListLineFindings(reqs, `${service}: spec.md`, service));\n } else {\n findings.push({\n severity: \"warn\",\n code: \"service.no-spec\",\n message: `No living spec at ${paths.spec} — requirement coverage and API governance are unchecked`,\n });\n }\n\n // The requirements that still govern anything. A REMOVED requirement is on\n // its way out together with the operations it names: it makes no claim on the\n // contract and governs nothing once the retiring feature archives — the same\n // position coherence/coherence.ts takes on the delta side.\n //\n // Spelled once because it was spelled three times and dropped on the fourth:\n // `governedOps` filtered, `spec-api.op-undefined` filtered, and the\n // `api.covered` join did not — so an operation whose only requirement was\n // REMOVED counted as governed, and the one operation about to be left without\n // a requirement was the one the report called covered.\n const livingReqs = reqs.filter((r) => r.kind !== \"REMOVED\");\n\n // The architecture spec, read HERE with spec.md rather than with the rest of\n // its own axis below, because the event links are a requirement-level join and\n // BOTH requirement files carry them. Events are architecture as often as they\n // are business — the outbox requirement in a service's arch.spec.md is the\n // canonical place a `Publishes:` line belongs — so handing the event axis only\n // spec.md would silently ignore the lines most authors write.\n const archText = existsSync(paths.archSpec)\n ? fleet === undefined\n ? await readFile(paths.archSpec, \"utf8\")\n : await fleet.readText(paths.archSpec)\n : null;\n const archReqs =\n archText === null\n ? []\n : fleet === undefined\n ? parseRequirements(archText)\n : await fleet.readRequirements(paths.archSpec);\n\n return { findings, reqs, livingReqs, archText, archReqs };\n}\n\n/** What the arch axis needs once the model and the landscape have been read. */\nexport interface ArchAxis {\n service: PathableService;\n paths: ServicePaths;\n /** arch.spec.md's text, or null — its absence is not a finding. */\n archText: string | null;\n archReqs: Requirement[];\n /** The service's own C4 model, empty when it has none. */\n elements: Elem[];\n relationships: Rel[];\n /** The living landscape, or null when the repo has none. */\n land: LoadedDoc | null;\n /** The docs root, for the one lazy load below — see `deployment` in the scope. */\n docsDir: DocsDir;\n /**\n * The enumerated fleet, for the Covers matcher's endpoint resolution — the\n * same set the caller's own spine resolution uses, so a `Covers:` line\n * naming a service matches an edge drawn into that service's container.\n */\n known?: ReadonlySet<string>;\n}\n\n/**\n * The topology a `node:` Covers entry resolves against, loaded at most once and\n * only when something asks for it.\n *\n * Under `--all` the caller hands in the doc it already loaded — the whole\n * `architecture/` project — so the deployment model is already here and this\n * returns it. A SINGLE-service run loads only `landscape.likec4`, and a fleet\n * that keeps its `deployment { }` block in a file beside it, which is the\n * ordinary layout once one landscape file is the fleet's convention, would then\n * resolve one `Covers:` line two different ways depending on which command\n * asked. `loam status`'s two forms had exactly that disagreement and it was a\n * defect both times; this is the same class, prevented rather than found.\n *\n * The cost is paid only by a service whose `arch.spec.md` actually writes a\n * `node:` entry. A fleet with no topology at all pays one project load per such\n * service — a state in which the entry is a typo anyway, and one load is what\n * buys it a message that names the real ids.\n */\nasync function topology(\n land: LoadedDoc | null,\n archReqs: Requirement[],\n docsDir: DocsDir,\n): Promise<DeploymentModel | undefined> {\n if (land?.deployment !== undefined && hasDeployment(land.deployment)) return land.deployment;\n const wanted = coversEntries(archReqs).some((e) => e.form === \"node\" || e.form === \"node-edge\");\n if (!wanted) return undefined;\n const project = await loadArchitecture(docsDir);\n return project.errors.length === 0 ? project.deployment : undefined;\n}\n\nexport async function archAxisFindings(axis: ArchAxis): Promise<Finding[]> {\n const { service, paths, archText, archReqs, elements, relationships, land, known, docsDir } = axis;\n const findings: Finding[] = [];\n\n // The architecture spec axis — the obligations a business spec never carries\n // (the transactional outbox, retries, metrics, alerts). arch.spec.md is\n // optional and its ABSENCE is not a finding (partial adoption is supported);\n // what exists must hold together: every requirement needs a scenario, every\n // `Covers:` entry must resolve (covers.unknown), and every alert/SLI that\n // health.yaml declares wants a covering requirement (health.uncovered) — the\n // moment health.yaml stops being inert. Warnings, never gates: `--strict` is\n // the CI escalation.\n const health = await readHealth(paths.health);\n\n // A model that reaches nothing is almost never true — but only when\n // something else attests it should reach. A deliberately shallow synthetic\n // fixture can hold elements without relationships beside sources that name\n // dependencies, yet `0 errors, 0 warnings` would still read as done.\n // Evidence-gated on purpose: a bare root-plus-one-container baseline stays\n // silent (nothing PROVES it thin — the standard mid-adoption shape), the\n // warn needs either a second nested element with no edge joining anything,\n // or dependencies declared in this service's own health.yaml. The health\n // half goes quiet when the file is unreadable — no claims on bad data.\n const nested = elements.filter((e) => e.id.includes(\".\")).length;\n const deps = health.unreadable ? [] : health.dependencies;\n if (elements.length > 0 && relationships.length === 0 && (nested > 1 || deps.length > 0)) {\n const evidence = [\n ...(nested > 1 ? [`${nested} nested elements with no edge joining them`] : []),\n ...(deps.length > 0\n ? [`health.yaml declares ${deps.length} dependenc${deps.length === 1 ? \"y\" : \"ies\"} (${deps.join(\", \")})`]\n : []),\n ].join(\", and \");\n findings.push({\n severity: \"warn\",\n code: \"c4.no-relationships\",\n subject: service,\n message:\n `${service}: model.likec4 declares ${elements.length} element(s) and 0 relationships, yet ${evidence} — ` +\n `a model that reaches nothing is almost never true; draw the edges the service actually has ` +\n `(what its containers call, what reads and writes its stores, what it depends on at runtime)`,\n });\n }\n\n if (archText !== null) {\n // The arch axis is advisory in what it ASKS for (covers.unknown,\n // health.uncovered are warnings) but not in whether the file is readable:\n // both breaches below are about the document itself, and they are graded\n // exactly as they are on the business axis — one file, one rule.\n const conflict = documentConflictFinding(`${service}: arch.spec.md`, service, archText);\n if (conflict !== null) findings.push(conflict);\n findings.push(coverageFinding(`${service}: arch requirements`, archReqs));\n findings.push(...steplessFindings(`${service}: arch requirements`, service, archReqs));\n findings.push(...assertionlessFindings(`${service}: arch requirements`, service, archReqs));\n findings.push(...examplesFindings(`${service}: arch requirements`, service, archReqs));\n findings.push(...duplicateRequirementFindings(archReqs, `${service}: arch.spec.md`, service));\n findings.push(...requirementIdFindings(archReqs, `${service}: arch.spec.md`, service));\n findings.push(...repeatedListLineFindings(archReqs, `${service}: arch.spec.md`, service));\n }\n // A health.yaml that exists but does not read is reported once, and the\n // checks that read it go quiet BOTH ways: its ids are unknown, so no\n // health.uncovered obligation exists (the empty id set below is already\n // silent), and no `Covers: alert:/sli:` entry may be graded a typo against\n // ids nobody could read (coversUnknownFindings mutes on the flag). A warn,\n // not an error: the axis it feeds is advisory end to end.\n if (health.unreadable) {\n findings.push({\n severity: \"warn\",\n code: \"health.invalid\",\n subject: service,\n message: `${service}: health.yaml does not parse — alert/SLI ids are unreadable, so Covers: alert:/sli: entries and health coverage are unchecked`,\n ...(health.error === undefined ? {} : { details: [health.error] }),\n });\n }\n const landParses = land !== null && land.errors.length === 0 ? land : null;\n const deployment = await topology(landParses, archReqs, docsDir);\n const scope: CoverageScope = {\n elements: [...elements, ...(landParses?.elements ?? [])],\n relationships: [...relationships, ...(landParses?.relationships ?? [])],\n ...(deployment === undefined ? {} : { deployment }),\n health: health.ids,\n known,\n };\n findings.push(\n ...coversUnknownFindings(archReqs, { where: `${service}: arch.spec.md`, subject: service }, scope, health.unreadable),\n );\n const activeCovers = coversEntries(archReqs);\n for (const { form, ids } of [\n { form: \"alert\" as const, ids: health.ids.alerts },\n { form: \"sli\" as const, ids: health.ids.slis },\n ]) {\n for (const id of ids) {\n if (activeCovers.some((e) => e.form === form && e.id === id)) continue;\n findings.push({\n severity: \"warn\",\n code: \"health.uncovered\",\n subject: service,\n message: `${service}: health.yaml declares ${form === \"alert\" ? \"alert\" : \"SLI\"} '${id}' but no arch.spec.md requirement covers it — write one with 'Covers: ${form}:${id}', or the signal ships with nothing testing it`,\n });\n }\n }\n // The other direction health.yaml is read in: its dependencies must be the\n // model's dependencies. `deps` is already muted on an unreadable file above.\n findings.push(...healthDependencyFindings({ service, dependencies: deps, elements, modelPath: paths.model }));\n\n return findings;\n}\n"]}
@@ -26,7 +26,14 @@ export const SUBSYSTEM_COMMANDS = `- \`loam subsystem <verb>\` manages the group
26
26
  moved and answers nothing — exit 0, no finding — when git will not say;
27
27
  \`sync\` regenerates \`architecture/subsystems.likec4\`, the one repair for
28
28
  \`subsystem.views-stale\`; a landscape view claiming a generated id is
29
- \`subsystem.view-id-collision\`. \`loam adopt --subsystem <name>\` briefs a new
29
+ \`subsystem.view-id-collision\`. Each generated view is titled by its PATH —
30
+ every marked ancestor's label and its own, which the renderer reads as
31
+ folders — where a label is the marker's \`title\` or the directory name; the
32
+ marker's \`description\` rides along. So editing \`subsystem.yaml\` changes
33
+ what the renderer LABELS the group, a parent's title changes every view
34
+ beneath it, and either makes the file stale until the next \`sync\`. It is generated wholesale:
35
+ an authored diagram of the same services belongs in the landscape or in a
36
+ file of your own name, never here. \`loam adopt --subsystem <name>\` briefs a new
30
37
  service's baseline directly into a group, so adoption need not land unfiled.
31
38
  `;
32
39
  //# sourceMappingURL=subsystem.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"subsystem.js","sourceRoot":"","sources":["../../../../../src/core/agent/agents-md/map/subsystem.ts"],"names":[],"mappings":"AAAA;;;;;;;;;GASG;AACH,MAAM,CAAC,MAAM,kBAAkB,GAAG;;;;;;;;;;;;;;;;;;;;CAoBjC,CAAC","sourcesContent":["/**\n * The `loam subsystem` half of the command map — its own section module\n * because command-map.ts sat against the file-line limit and the agents-md\n * package against the five-file cap, so the tree surface's documentation\n * lives one package down and is concatenated after the map.\n *\n * Same assembly contract as every section: ../agents-md.ts concatenates with\n * NO join separator, so this string starts at the first character of its\n * opening line and ends with the newline that closes its last one.\n */\nexport const SUBSYSTEM_COMMANDS = `- \\`loam subsystem <verb>\\` manages the grouping tree under \\`services/\\` — the\n navigable tree no identity depends on: a service id stays its leaf directory\n name, every join key is byte-identical before and after a move, and the tree\n carries no policy. Verbs: \\`new <name> [--under <sub>] [--title] [--description]\n [--owner]\\` creates a group (an empty one is legal — its \\`subsystem.yaml\\` is a\n marker, never a member list); \\`move <name>... --into <sub|.>\\` relocates\n services and whole subtrees in ONE journaled transaction over the renames\n plus the regenerated views file (\\`--into .\\` unfiles; the renames and the\n regenerated views file are staged in git without committing, where git answers); \\`rename <old> <new>\\` renames a\n group through the same transaction (services are never renamed — the id is\n the identity); \\`rm <name>\\` removes an EMPTY group; \\`list\\` shows the tree\n with member counts and the unfiled count (unfiled is permanent and normal —\n a count, never a finding; \\`loam list --json\\` carries the same facts as\n additive keys \\`services[].subsystem\\`, \\`subsystems[]\\` and\n \\`unfiledServices\\`); \\`history <name>\\` asks git how a service or subsystem\n moved and answers nothing — exit 0, no finding — when git will not say;\n \\`sync\\` regenerates \\`architecture/subsystems.likec4\\`, the one repair for\n \\`subsystem.views-stale\\`; a landscape view claiming a generated id is\n \\`subsystem.view-id-collision\\`. \\`loam adopt --subsystem <name>\\` briefs a new\n service's baseline directly into a group, so adoption need not land unfiled.\n`;\n"]}
1
+ {"version":3,"file":"subsystem.js","sourceRoot":"","sources":["../../../../../src/core/agent/agents-md/map/subsystem.ts"],"names":[],"mappings":"AAAA;;;;;;;;;GASG;AACH,MAAM,CAAC,MAAM,kBAAkB,GAAG;;;;;;;;;;;;;;;;;;;;;;;;;;;CA2BjC,CAAC","sourcesContent":["/**\n * The `loam subsystem` half of the command map — its own section module\n * because command-map.ts sat against the file-line limit and the agents-md\n * package against the five-file cap, so the tree surface's documentation\n * lives one package down and is concatenated after the map.\n *\n * Same assembly contract as every section: ../agents-md.ts concatenates with\n * NO join separator, so this string starts at the first character of its\n * opening line and ends with the newline that closes its last one.\n */\nexport const SUBSYSTEM_COMMANDS = `- \\`loam subsystem <verb>\\` manages the grouping tree under \\`services/\\` — the\n navigable tree no identity depends on: a service id stays its leaf directory\n name, every join key is byte-identical before and after a move, and the tree\n carries no policy. Verbs: \\`new <name> [--under <sub>] [--title] [--description]\n [--owner]\\` creates a group (an empty one is legal — its \\`subsystem.yaml\\` is a\n marker, never a member list); \\`move <name>... --into <sub|.>\\` relocates\n services and whole subtrees in ONE journaled transaction over the renames\n plus the regenerated views file (\\`--into .\\` unfiles; the renames and the\n regenerated views file are staged in git without committing, where git answers); \\`rename <old> <new>\\` renames a\n group through the same transaction (services are never renamed — the id is\n the identity); \\`rm <name>\\` removes an EMPTY group; \\`list\\` shows the tree\n with member counts and the unfiled count (unfiled is permanent and normal —\n a count, never a finding; \\`loam list --json\\` carries the same facts as\n additive keys \\`services[].subsystem\\`, \\`subsystems[]\\` and\n \\`unfiledServices\\`); \\`history <name>\\` asks git how a service or subsystem\n moved and answers nothing — exit 0, no finding — when git will not say;\n \\`sync\\` regenerates \\`architecture/subsystems.likec4\\`, the one repair for\n \\`subsystem.views-stale\\`; a landscape view claiming a generated id is\n \\`subsystem.view-id-collision\\`. Each generated view is titled by its PATH —\n every marked ancestor's label and its own, which the renderer reads as\n folders — where a label is the marker's \\`title\\` or the directory name; the\n marker's \\`description\\` rides along. So editing \\`subsystem.yaml\\` changes\n what the renderer LABELS the group, a parent's title changes every view\n beneath it, and either makes the file stale until the next \\`sync\\`. It is generated wholesale:\n an authored diagram of the same services belongs in the landscape or in a\n file of your own name, never here. \\`loam adopt --subsystem <name>\\` briefs a new\n service's baseline directly into a group, so adoption need not land unfiled.\n`;\n"]}
@@ -18,6 +18,10 @@ loam has no server or database. Delete it and the documents still make sense.
18
18
 
19
19
  \`architecture/landscape.likec4\` is the fleet map, and
20
20
  \`architecture/adrs/\` optional system-level decisions — only their links are graded.
21
+ \`architecture/subsystems.likec4\` is GENERATED by \`loam subsystem sync\` from the
22
+ directory tree under \`services/\` — one view per subsystem, a scoping aid, never
23
+ hand-edited and never a second fleet map. A diagram you author belongs in the
24
+ landscape or in a file of your own name beside it.
21
25
  \`services/<id>/\` holds a boundary's living \`model.likec4\`, \`spec.md\`,
22
26
  \`arch.spec.md\`, OpenAPI, AsyncAPI, runbook and health facts.
23
27
  \`features/<FEAT>/\` holds a change in flight: \`intent.md\`, \`delta.likec4\`,