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

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 (273) hide show
  1. package/CHANGELOG.md +901 -0
  2. package/MIGRATING-from-OpenSpec.md +7 -4
  3. package/QUICKSTART.md +8 -3
  4. package/README.md +13 -7
  5. package/ROADMAP.md +264 -11
  6. package/SCHEMA.md +439 -38
  7. package/WORKFLOW.md +23 -10
  8. package/dist/commands/adopt/render.js +47 -8
  9. package/dist/commands/adopt/render.js.map +1 -1
  10. package/dist/commands/archive/plan/landscape.js +45 -1
  11. package/dist/commands/archive/plan/landscape.js.map +1 -1
  12. package/dist/commands/archive/plan/model/extending.js +85 -0
  13. package/dist/commands/archive/plan/model/extending.js.map +1 -0
  14. package/dist/commands/archive/run.js +5 -2
  15. package/dist/commands/archive/run.js.map +1 -1
  16. package/dist/commands/context/print.js +11 -1
  17. package/dist/commands/context/print.js.map +1 -1
  18. package/dist/commands/doctor.js +46 -0
  19. package/dist/commands/doctor.js.map +1 -1
  20. package/dist/commands/explore.js +12 -2
  21. package/dist/commands/explore.js.map +1 -1
  22. package/dist/commands/list/views.js +5 -1
  23. package/dist/commands/list/views.js.map +1 -1
  24. package/dist/commands/seed/plan.js +97 -3
  25. package/dist/commands/seed/plan.js.map +1 -1
  26. package/dist/commands/seed/seed.js +7 -2
  27. package/dist/commands/seed/seed.js.map +1 -1
  28. package/dist/commands/show/feature.js +2 -1
  29. package/dist/commands/show/feature.js.map +1 -1
  30. package/dist/commands/show/marks.js +3 -5
  31. package/dist/commands/show/marks.js.map +1 -1
  32. package/dist/commands/show/service.js +39 -18
  33. package/dist/commands/show/service.js.map +1 -1
  34. package/dist/commands/subsystem/sync.js +128 -74
  35. package/dist/commands/subsystem/sync.js.map +1 -1
  36. package/dist/commands/subsystem/txn/exclude.js +132 -0
  37. package/dist/commands/subsystem/txn/exclude.js.map +1 -0
  38. package/dist/commands/subsystem/txn/report.js +124 -0
  39. package/dist/commands/subsystem/txn/report.js.map +1 -0
  40. package/dist/commands/subsystem/txn/txn.js +10 -3
  41. package/dist/commands/subsystem/txn/txn.js.map +1 -1
  42. package/dist/commands/subsystem/txn/views.js +113 -16
  43. package/dist/commands/subsystem/txn/views.js.map +1 -1
  44. package/dist/commands/validate/arch-coverage.js +16 -9
  45. package/dist/commands/validate/arch-coverage.js.map +1 -1
  46. package/dist/commands/validate/checks/fleet-shape.js +30 -77
  47. package/dist/commands/validate/checks/fleet-shape.js.map +1 -1
  48. package/dist/commands/validate/checks/vocabulary.js +4 -19
  49. package/dist/commands/validate/checks/vocabulary.js.map +1 -1
  50. package/dist/commands/validate/feature.js +1 -1
  51. package/dist/commands/validate/feature.js.map +1 -1
  52. package/dist/commands/validate/fleet/census.js +11 -10
  53. package/dist/commands/validate/fleet/census.js.map +1 -1
  54. package/dist/commands/validate/fleet/landscape.js +127 -119
  55. package/dist/commands/validate/fleet/landscape.js.map +1 -1
  56. package/dist/commands/validate/fleet/load.js +66 -0
  57. package/dist/commands/validate/fleet/load.js.map +1 -1
  58. package/dist/commands/validate/fleet/map/attest.js +103 -0
  59. package/dist/commands/validate/fleet/map/attest.js.map +1 -0
  60. package/dist/commands/validate/fleet/map/bindings.js +96 -0
  61. package/dist/commands/validate/fleet/map/bindings.js.map +1 -0
  62. package/dist/commands/validate/fleet/map/consumers.js +298 -0
  63. package/dist/commands/validate/fleet/map/consumers.js.map +1 -0
  64. package/dist/commands/validate/fleet/map/isolation.js +116 -0
  65. package/dist/commands/validate/fleet/map/isolation.js.map +1 -0
  66. package/dist/commands/validate/fleet/map/usecases.js +56 -0
  67. package/dist/commands/validate/fleet/map/usecases.js.map +1 -0
  68. package/dist/commands/validate/fleet/usecases/steps.js +49 -17
  69. package/dist/commands/validate/fleet/usecases/steps.js.map +1 -1
  70. package/dist/commands/validate/fleet/views/fleet-project.js +217 -0
  71. package/dist/commands/validate/fleet/views/fleet-project.js.map +1 -0
  72. package/dist/commands/validate/fleet/views/ids.js +13 -9
  73. package/dist/commands/validate/fleet/views/ids.js.map +1 -1
  74. package/dist/commands/validate/fleet/views/projects.js +258 -70
  75. package/dist/commands/validate/fleet/views/projects.js.map +1 -1
  76. package/dist/commands/validate/fleet/views/root-documents.js +97 -0
  77. package/dist/commands/validate/fleet/views/root-documents.js.map +1 -0
  78. package/dist/commands/validate/fleet/views/stale.js +2 -2
  79. package/dist/commands/validate/fleet/views/stale.js.map +1 -1
  80. package/dist/commands/validate/service/model/diverged.js +226 -0
  81. package/dist/commands/validate/service/model/diverged.js.map +1 -0
  82. package/dist/commands/validate/service/model/grade.js +163 -0
  83. package/dist/commands/validate/service/model/grade.js.map +1 -0
  84. package/dist/commands/validate/service/service.js +110 -34
  85. package/dist/commands/validate/service/service.js.map +1 -1
  86. package/dist/commands/validate/service/specs.js +39 -5
  87. package/dist/commands/validate/service/specs.js.map +1 -1
  88. package/dist/commands/validate/service/spine.js +41 -3
  89. package/dist/commands/validate/service/spine.js.map +1 -1
  90. package/dist/commands/validate/service/usecases/tags.js +113 -0
  91. package/dist/commands/validate/service/usecases/tags.js.map +1 -0
  92. package/dist/commands/validate/service/usecases/usecases.js +209 -0
  93. package/dist/commands/validate/service/usecases/usecases.js.map +1 -0
  94. package/dist/commands/validate/validate.js +10 -24
  95. package/dist/commands/validate/validate.js.map +1 -1
  96. package/dist/core/agent/agents-md/artifacts.js +1 -0
  97. package/dist/core/agent/agents-md/artifacts.js.map +1 -1
  98. package/dist/core/agent/agents-md/command-map.js +18 -4
  99. package/dist/core/agent/agents-md/command-map.js.map +1 -1
  100. package/dist/core/agent/agents-md/cycle.js +24 -9
  101. package/dist/core/agent/agents-md/cycle.js.map +1 -1
  102. package/dist/core/agent/agents-md/map/lenses/context.js +4 -3
  103. package/dist/core/agent/agents-md/map/lenses/context.js.map +1 -1
  104. package/dist/core/agent/agents-md/map/subsystem.js +51 -11
  105. package/dist/core/agent/agents-md/map/subsystem.js.map +1 -1
  106. package/dist/core/agent/agents-md/map/usecases.js +45 -3
  107. package/dist/core/agent/agents-md/map/usecases.js.map +1 -1
  108. package/dist/core/agent/agents-md.js +14 -4
  109. package/dist/core/agent/agents-md.js.map +1 -1
  110. package/dist/core/agent/support/report.js +39 -5
  111. package/dist/core/agent/support/report.js.map +1 -1
  112. package/dist/core/agent/workflows/adopt.js +123 -14
  113. package/dist/core/agent/workflows/adopt.js.map +1 -1
  114. package/dist/core/agent/workflows/check.js +35 -20
  115. package/dist/core/agent/workflows/check.js.map +1 -1
  116. package/dist/core/agent/workflows/closing.js +3 -1
  117. package/dist/core/agent/workflows/closing.js.map +1 -1
  118. package/dist/core/brief/brief.js +63 -11
  119. package/dist/core/brief/brief.js.map +1 -1
  120. package/dist/core/brief/checks.js +133 -7
  121. package/dist/core/brief/checks.js.map +1 -1
  122. package/dist/core/brief/landscape.js +212 -129
  123. package/dist/core/brief/landscape.js.map +1 -1
  124. package/dist/core/brief/map/extend.js +81 -0
  125. package/dist/core/brief/map/extend.js.map +1 -0
  126. package/dist/core/brief/map/owed.js +284 -0
  127. package/dist/core/brief/map/owed.js.map +1 -0
  128. package/dist/core/brief/targets.js +30 -29
  129. package/dist/core/brief/targets.js.map +1 -1
  130. package/dist/core/brief/unchecked.js +12 -2
  131. package/dist/core/brief/unchecked.js.map +1 -1
  132. package/dist/core/c4/likec4.js +23 -1
  133. package/dist/core/c4/likec4.js.map +1 -1
  134. package/dist/core/c4/parsed/deployment.js +5 -3
  135. package/dist/core/c4/parsed/deployment.js.map +1 -1
  136. package/dist/core/c4/parsed/view-ids.js +39 -1
  137. package/dist/core/c4/parsed/view-ids.js.map +1 -1
  138. package/dist/core/c4/prefetch/fleet.js +142 -0
  139. package/dist/core/c4/prefetch/fleet.js.map +1 -0
  140. package/dist/core/c4/project/architecture.js +52 -4
  141. package/dist/core/c4/project/architecture.js.map +1 -1
  142. package/dist/core/c4/project/batch.js +140 -0
  143. package/dist/core/c4/project/batch.js.map +1 -0
  144. package/dist/core/c4/project/documents.js +9 -4
  145. package/dist/core/c4/project/documents.js.map +1 -1
  146. package/dist/core/c4/project/load.js +77 -38
  147. package/dist/core/c4/project/load.js.map +1 -1
  148. package/dist/core/c4/project/staged.js +46 -12
  149. package/dist/core/c4/project/staged.js.map +1 -1
  150. package/dist/core/c4/resolve/attested.js +0 -0
  151. package/dist/core/c4/resolve/attested.js.map +1 -0
  152. package/dist/core/c4/resolve/service.js +8 -17
  153. package/dist/core/c4/resolve/service.js.map +1 -1
  154. package/dist/core/c4/resolve/steps.js +18 -0
  155. package/dist/core/c4/resolve/steps.js.map +1 -1
  156. package/dist/core/c4/root-project/exclude.js +352 -0
  157. package/dist/core/c4/root-project/exclude.js.map +1 -0
  158. package/dist/core/c4/service-model/documents.js +47 -0
  159. package/dist/core/c4/service-model/documents.js.map +1 -0
  160. package/dist/core/c4/service-model/fleet/extending.js +82 -0
  161. package/dist/core/c4/service-model/fleet/extending.js.map +1 -0
  162. package/dist/core/c4/service-model/load.js +165 -0
  163. package/dist/core/c4/service-model/load.js.map +1 -0
  164. package/dist/core/c4/service-model/renderer.js +128 -0
  165. package/dist/core/c4/service-model/renderer.js.map +1 -0
  166. package/dist/core/c4/service-model/shape.js +126 -0
  167. package/dist/core/c4/service-model/shape.js.map +1 -0
  168. package/dist/core/c4/service-model/slice.js +188 -0
  169. package/dist/core/c4/service-model/slice.js.map +1 -0
  170. package/dist/core/c4/source-scan.js +26 -0
  171. package/dist/core/c4/source-scan.js.map +1 -1
  172. package/dist/core/c4/splice/authored-source.js +60 -13
  173. package/dist/core/c4/splice/authored-source.js.map +1 -1
  174. package/dist/core/c4/splice/contract.js.map +1 -1
  175. package/dist/core/c4/splice/identity/declared.js +87 -0
  176. package/dist/core/c4/splice/identity/declared.js.map +1 -0
  177. package/dist/core/c4/splice/identity/edges.js +17 -0
  178. package/dist/core/c4/splice/identity/edges.js.map +1 -1
  179. package/dist/core/c4/splice/identity/existing.js +80 -0
  180. package/dist/core/c4/splice/identity/existing.js.map +1 -0
  181. package/dist/core/c4/splice/identity/interior.js +80 -0
  182. package/dist/core/c4/splice/identity/interior.js.map +1 -0
  183. package/dist/core/c4/splice/identity/owner.js +50 -0
  184. package/dist/core/c4/splice/identity/owner.js.map +1 -0
  185. package/dist/core/c4/splice/landscape-merge.js +154 -100
  186. package/dist/core/c4/splice/landscape-merge.js.map +1 -1
  187. package/dist/core/c4/splice/model/merge.js +140 -0
  188. package/dist/core/c4/splice/model/merge.js.map +1 -0
  189. package/dist/core/c4/splice/placement.js +14 -4
  190. package/dist/core/c4/splice/placement.js.map +1 -1
  191. package/dist/core/c4/workspace.js +2 -1
  192. package/dist/core/c4/workspace.js.map +1 -1
  193. package/dist/core/coherence/coherence.js +7 -0
  194. package/dist/core/coherence/coherence.js.map +1 -1
  195. package/dist/core/delta/delta.js +9 -0
  196. package/dist/core/delta/delta.js.map +1 -1
  197. package/dist/core/deployment/delta.js +4 -0
  198. package/dist/core/deployment/delta.js.map +1 -1
  199. package/dist/core/diff/base-state.js +21 -5
  200. package/dist/core/diff/base-state.js.map +1 -1
  201. package/dist/core/diff/victims.js +10 -1
  202. package/dist/core/diff/victims.js.map +1 -1
  203. package/dist/core/docs.js +29 -24
  204. package/dist/core/docs.js.map +1 -1
  205. package/dist/core/doctor/config.js +7 -3
  206. package/dist/core/doctor/config.js.map +1 -1
  207. package/dist/core/doctor/doctor.js +36 -8
  208. package/dist/core/doctor/doctor.js.map +1 -1
  209. package/dist/core/doctor/report.js.map +1 -1
  210. package/dist/core/doctor/reports/scan.js +242 -0
  211. package/dist/core/doctor/reports/scan.js.map +1 -0
  212. package/dist/core/explain/refusals.js +1 -1
  213. package/dist/core/explain/refusals.js.map +1 -1
  214. package/dist/core/explore/explore.js +44 -9
  215. package/dist/core/explore/explore.js.map +1 -1
  216. package/dist/core/fleet-context.js +47 -0
  217. package/dist/core/fleet-context.js.map +1 -1
  218. package/dist/core/gate/partners.js +5 -9
  219. package/dist/core/gate/partners.js.map +1 -1
  220. package/dist/core/kernel/ids/fqn/ancestors.js +65 -0
  221. package/dist/core/kernel/ids/fqn/ancestors.js.map +1 -0
  222. package/dist/core/pack/living.js +47 -13
  223. package/dist/core/pack/living.js.map +1 -1
  224. package/dist/core/pack/pack.js +9 -6
  225. package/dist/core/pack/pack.js.map +1 -1
  226. package/dist/core/projection/arch-slice.js +2 -2
  227. package/dist/core/projection/arch-slice.js.map +1 -1
  228. package/dist/core/repo/paths.js +11 -9
  229. package/dist/core/repo/paths.js.map +1 -1
  230. package/dist/core/repo/tree/render/projects.js +93 -35
  231. package/dist/core/repo/tree/render/projects.js.map +1 -1
  232. package/dist/core/repo/tree/render/stale.js +2 -3
  233. package/dist/core/repo/tree/render/stale.js.map +1 -1
  234. package/dist/core/repo/tree/render/views.js +51 -3
  235. package/dist/core/repo/tree/render/views.js.map +1 -1
  236. package/dist/core/repo/tree/walk.js +45 -2
  237. package/dist/core/repo/tree/walk.js.map +1 -1
  238. package/dist/core/scaffold/landscape.js +219 -9
  239. package/dist/core/scaffold/landscape.js.map +1 -1
  240. package/dist/core/staging/writes.js.map +1 -1
  241. package/dist/core/usecases/delta/claims.js +1 -0
  242. package/dist/core/usecases/delta/claims.js.map +1 -1
  243. package/dist/core/usecases/delta/overlay.js +4 -0
  244. package/dist/core/usecases/delta/overlay.js.map +1 -1
  245. package/dist/core/usecases/fleet.js +18 -6
  246. package/dist/core/usecases/fleet.js.map +1 -1
  247. package/dist/core/usecases/operations.js +7 -0
  248. package/dist/core/usecases/operations.js.map +1 -1
  249. package/dist/core/usecases/service/exclude.js +119 -0
  250. package/dist/core/usecases/service/exclude.js.map +1 -0
  251. package/dist/core/usecases/service/flows.js +161 -0
  252. package/dist/core/usecases/service/flows.js.map +1 -0
  253. package/dist/core/vocabulary/maturity.js +10 -0
  254. package/dist/core/vocabulary/maturity.js.map +1 -1
  255. package/examples/README.md +24 -6
  256. package/examples/docs/architecture/landscape.likec4 +41 -0
  257. package/examples/docs/architecture/usecases/order-notification.likec4 +5 -2
  258. package/examples/docs/likec4.config.json +0 -1
  259. package/examples/docs/services/checkout-web/model.likec4 +39 -56
  260. package/examples/docs/services/order-service/arch.spec.md +13 -6
  261. package/examples/docs/services/order-service/model.likec4 +53 -67
  262. package/examples/docs/services/payment-service/arch.spec.md +7 -4
  263. package/examples/docs/services/payment-service/model.likec4 +32 -35
  264. package/examples/docs/services/platform/identity-service/arch.spec.md +7 -4
  265. package/examples/docs/services/platform/identity-service/model.likec4 +29 -33
  266. package/examples/docs/services/platform/notification-service/health.yaml +8 -3
  267. package/examples/docs/services/platform/notification-service/model.likec4 +39 -59
  268. package/package.json +1 -1
  269. package/examples/docs/services/checkout-web/likec4.config.json +0 -4
  270. package/examples/docs/services/order-service/likec4.config.json +0 -4
  271. package/examples/docs/services/payment-service/likec4.config.json +0 -4
  272. package/examples/docs/services/platform/identity-service/likec4.config.json +0 -4
  273. package/examples/docs/services/platform/notification-service/likec4.config.json +0 -4
@@ -1 +1 @@
1
- {"version":3,"file":"closing.js","sourceRoot":"","sources":["../../../../src/core/agent/workflows/closing.ts"],"names":[],"mappings":"AAEA;;;;;;GAMG;AACH,MAAM,CAAC,MAAM,WAAW,GAAmB;IACzC,IAAI,EAAE,aAAa;IACnB,WAAW,EACT,qFAAqF;IACvF,YAAY,EAAE,WAAW;IACzB,OAAO,EACL,6NAA6N;IAC/N,UAAU,EAAE,kCAAkC;IAC9C,YAAY,EAAE,CAAC,SAAS,CAAC;IACzB,KAAK,EAAE;QACL,yEAAyE;QACzE,uGAAuG;QACvG,sFAAsF;QACtF,2EAA2E;QAC3E,6FAA6F;KAC9F;IACD,IAAI,EAAE;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;CAoJP;CACA,CAAC;AAEF,MAAM,CAAC,MAAM,SAAS,GAAmB;IACvC,IAAI,EAAE,WAAW;IACjB,WAAW,EAAE,yEAAyE;IACtF,YAAY,EAAE,WAAW;IACzB,OAAO,EACL,0MAA0M;IAC5M,UAAU,EAAE,gCAAgC;IAC5C,YAAY,EAAE,CAAC,SAAS,CAAC;IACzB,KAAK,EAAE;QACL,oHAAoH;QACpH,sHAAsH;QACtH,uGAAuG;QACvG,6DAA6D;QAC7D,0EAA0E;KAC3E;IACD,IAAI,EAAE;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;CA0EP;CACA,CAAC","sourcesContent":["import type { CommandContent } from \"../contract.js\";\n\n/**\n * The two post-build workflows, paired: the done-check (`loam-verify`) and the\n * merge it precedes (`loam-ship`). One file for two commands because six\n * workflows meet the five-file package cap somewhere, and this is the seam the\n * cycle already has — both describe what happens after the code is built, and\n * each body names the other's refusals (`record-federated`, `not-coherent`).\n */\nexport const LOAM_VERIFY: CommandContent = {\n name: \"loam-verify\",\n description:\n \"Verify a built loam feature against its own promises, claim by claim, with evidence\",\n argumentHint: \"<FEAT-id>\",\n purpose:\n \"Check that the code somebody built is the feature that was designed. loam derives the questions; a test runner and you answer them. loam never reads the service, so a verdict is worth exactly what its evidence is worth.\",\n invocation: \"loam instructions loam-verify $1\",\n placeholders: [\"feature\"],\n spine: [\n \"`loam verify` — the checklist, derived from the feature's own artifacts\",\n \"run the generated suite with a JSON report: the scenario claims are the runner's to answer, not yours\",\n \"answer the remaining claims yourself, every one carrying a `file:line` that resolves\",\n \"record from each affected service's own repository, bound to that service\",\n \"read the verdict honestly — a scenario confirmed on your word is `attested`, not `verified`\",\n ],\n body: `Check that the code somebody just built is the feature that was designed.\n\nloam derives the questions; the test runner and you answer them. loam never\nreads the service, so a verdict is worth exactly what its evidence is worth.\n\n1. \\`loam verify $1 --json\\`. \\`claims[]\\` is the checklist, derived from the feature's\n own artifacts — one claim per new service, per operation its openapi delta adds,\n per (direction, message) its asyncapi delta newly declares against the living\n contract's send/receive sets (quotes and removals are never questions),\n per tagged edge that names an operation, and per scenario of every changed\n requirement, arch.spec.md deltas included. Each has a stable \\`id\\` and a\n \\`subject\\` (the service whose code answers it); a claim that says arch.spec.md\n wants an integration/ops test, not an acceptance test. An ARCHIVED feature returns its record as frozen history instead\n (\\`frozen: true\\`) and \\`--record\\` / \\`--results\\` / \\`--contract-results\\` /\n \\`--diff-answers\\` refuse it — \\`loam unarchive\\` first if the answers really\n must change.\n2. The \\`scenario.tested\\` claims are the runner's to answer, not yours. In the\n service's repo, run the generated suite with a JSON report —\n \\`npx cucumber-js --format json:report.json\\` (cucumber-jvm, behave and\n SpecFlow emit the same format). The generated scenarios carry\n \\`@loam-digest-…\\` tags that ride into the report; that digest is the match,\n so never retitle or hand-edit a generated scenario to make one pass.\n If there is no runnable suite yet you MAY answer them yourself, and the record\n will say so: \\`answered_by: agent\\` makes the feature **attested**, not verified\n (\\`verified: false\\`, \\`verdict: \"attested\"\\`, the \\`verify.scenario-attested\\`\n notice, and a \\`next.verify-attested\\` step in \\`loam status\\`). Nothing gates on\n it — a legacy service with no suite must still be able to ship — but say so\n when you hand back, and close it with \\`--results\\` the moment a suite runs.\n \\`verify.digest-contested\\` (warn) is the case where two services word a\n scenario identically: they share one digest, so no single report can say whose\n suite ran it, and each service must record its own with \\`--service\\`.\n The \\`api.exposes\\` claims have a mechanical channel too, when an API\n contract-test suite exists: emit loam's contract-results shape from its\n report — \\`{\"loamContractReport\": 1, \"results\": [{\"operationId\": \"…\",\n \"status\": \"passed\"}]}\\`, a one-line transform for Specmatic, Pact, Dredd or a\n property harness — and pass \\`--contract-results contract.json\\` alongside.\n Only a status of exactly \\`passed\\` confirms the named operation\n (\\`answered_by: external-runner\\`); anything else, and an operation the report\n never exercised, stays \\`unconfirmed\\` with the reason, and under the flag the\n report owns every api.exposes claim, so take those out of the answers file.\n3. Answer every OTHER claim by finding it in the code. Not by reasoning that it\n must be there — by opening the file:\n - \\`service.exists\\` — the service is deployable: its build, its entry point.\n - \\`api.exposes\\` — the route handler serving that operationId, not the spec that\n declares it — unless a contract-test report already answered it in step 2.\n - \\`event.declares\\` — the code that puts the message on the wire (sends) or\n handles it off the wire (receives), not the contract that names it.\n - \\`c4.calls\\` — the call site in the CALLER.\n4. Write those answers as JSON — evidence is \\`file:line\\`, one entry per place it can\n be seen:\n \\`\\`\\`json\n { \"answers\": [\n { \"id\": \"<claim id>\", \"verdict\": \"confirmed\", \"evidence\": [\"src/split/Api.ts:42\"] },\n { \"id\": \"<claim id>\", \"verdict\": \"unconfirmed\", \"note\": \"the split service has no entry point yet\" }\n ] }\n \\`\\`\\`\n5. Optional, before recording: cross-examine. Have a SECOND agent answer the\n same checklist blind — a fresh context, given only \\`loam verify $1 --json\\`\n and the code, never your answers file — then compare the two sets:\n\n \\`\\`\\`sh\n loam verify $1 --diff-answers yours.json theirs.json --json\n \\`\\`\\`\n\n Both files must answer EVERY claim in the compared scope,\n \\`scenario.tested\\` included: the diff consults no test report, so this is a\n DIFFERENT file from the one step 6 records under \\`--results\\` (that one\n must omit the runner's claims; a step-6-shaped file here refuses\n \\`answers-mismatch\\`). Narrowing with \\`--service <id>\\` compares one\n service's claims — give the second agent the same lens, or its file will\n not match the compared scope. An archived feature refuses the diff too:\n frozen history has no current checklist. It writes nothing and changes no\n verdict: \\`cross.agree-confirmed\\` / \\`cross.agree-unconfirmed\\` rows are the\n cheap-to-skip queue, \\`disagreements[]\\` (\\`cross.disagree\\`) is what a human\n reads first, and the \\`cross.evidence-disjoint\\` notice flags agreements\n citing non-overlapping files — read those second. Agreement between two\n same-model agents is correlated, so it ranks review; it never upgrades\n \\`attested\\` to \\`verified\\`. Record only the answer set you stand behind, in\n a separate run.\n6. Record, **in each affected service's own repository**, one service at a time:\n\n \\`\\`\\`sh\n loam verify $1 --service <id> --results report.json [--contract-results contract.json] --record answers.json --json\n \\`\\`\\`\n\n \\`--service <id>\\` narrows the checklist to that service's claims and binds the\n answers to this repository's commit, writing ONE attestation into a shared\n \\`schema: 2\\` \\`verification.yaml\\`. Running it in the next repo adds that repo's\n attestation and leaves the others standing. In a repo whose ./loam.json\n declares a \\`service\\`, \\`--service\\` may be omitted — it is substituted, and the\n run still federates. \\`--results report.json\\` alone is legal when the\n checklist is all scenarios.\n\n ⚠ **Never run \\`loam verify $1 --record answers.json\\` without \\`--service\\`.**\n That is the legacy all-at-once form: it writes the WHOLE record from one place,\n and over a record that already carries other services' attestations it is\n refused (\\`record-federated\\`, naming them) precisely because it would erase\n them. If you meet that refusal, re-run with \\`--service\\`; do not work around it.\n\n The other refusals: \\`answers-mismatch\\` (an id\n nobody asked about, a claim you left out — re-run step 1, the feature moved —\n or an answers entry for a \\`scenario.tested\\` claim: the runner owns those),\n \\`answers-unevidenced\\` (a \\`confirmed\\` with nothing behind it),\n \\`answers-unreadable\\` (a report or answers file it cannot read),\n \\`record-unreadable\\` (a \\`verification.yaml\\` that exists but cannot be read as a\n record — it is never overwritten; a human repairs it, and\n \\`verify.record-miscounted\\` is the reason when its \\`summary\\` block contradicts\n its own \\`claims[]\\`: neither half can be believed, so re-record rather than\n editing the counts),\n \\`service-mismatch\\` (\\`--service\\` names a service this repo does not declare) and\n \\`repository-unavailable\\` (\\`--service\\` from a repo that declares none, or whose\n commit cannot be read — an attestation must be bound to the code it is about).\n7. Report every \\`unconfirmed\\` claim and what it would take to close it, and read\n \\`discarded\\` in the payload: it names every previous answer the new record does\n not carry, and each one is either evidence that went stale or evidence that was\n just dropped. A scenario claim the report failed or missed closes by fixing the\n code and re-running the suite — never by editing the generated .feature. While\n any claim stays unconfirmed or unanswered, \\`notices[]\\` carries\n \\`verify.claims-open\\` (warn, gating nothing): the one-line \"not a clean\n result\" summary with all four counts, so a partial record can never read as a\n finished one from the notice lines alone.\n8. Reading is safe from anywhere: \\`loam verify $1 --service <id> --json\\` without\n \\`--record\\` writes nothing and works from the docs repo too. \\`services\\` lists\n every subject of the checklist, so it is how you find out which repos still\n owe an attestation, and \\`loam status $1 --json\\` says the same thing in the\n shape of a next step.\n\nAnything you cannot show is \\`unconfirmed\\` with a note. That is a successful record,\nand it is the useful one: nothing gates on this file, so the only thing it is for is\ntelling the next reader the truth. \\`loam archive\\` will ship the feature either way —\nwhich is exactly why a \\`confirmed\\` you cannot back up costs nothing now and misleads\neveryone later.\n\n\\`loam verify\\` — the notice and record codes its reports carry in \\`notices[]\\`,\ngathered from the steps above; branch on the code, never the prose:\n\n| code | what it means | what to do |\n|---|---|---|\n| \\`verify.claims-open\\` (warn) | a recorded feature still has unconfirmed or unanswered claims — the one-line \"not a clean result\" summary with all four counts, so a partial record can never read as finished from the notice lines alone; a feature with no record at all never carries it | close each open claim: fix the code and re-run the suite for a failed scenario, re-answer the rest with evidence, then re-record |\n| \\`verify.scenario-attested\\` (warn) | scenario claims were answered on an agent's word (\\`answered_by: agent\\`), so the verdict is **attested**, not verified — nothing gates on it, but say so when you hand back | run the generated suite with a JSON report and re-record with \\`--results report.json\\` the moment a suite exists |\n| \\`verify.digest-contested\\` (warn) | two or more services word a scenario identically, so they share one \\`@loam-digest-…\\` tag and no single cucumber report can say whose suite ran it — those scenario claims stay unconfirmed | each service records its own claims from its own repository with \\`--service <id>\\` |\n| \\`verify.operation-contested\\` (warn) | two services on the checklist expose the same operationId, so a contract report — which names no service — cannot say whose suite exercised it; those claims stay unconfirmed | each service records its own with \\`--service <id>\\`, passing its own contract report |\n| \\`verify.checklist-forked\\` (warn) | one or more services answered a DIFFERENT version of the feature's question set than the one it now asks — each attestation carries the \\`checklist\\` digest it answered, and the notice names the ones that do not match the current checklist. Measured against the present, not pairwise: services that all attested against a since-rewritten delta agree with each other perfectly and are all stale. Normal mid-rollout, since services attest as they finish. Attestations written before the field existed carry no digest and are never called stale | re-record the services on the older version from their own repositories (\\`loam verify <FEAT> --service <id> --record\\`), or accept the split knowingly — nothing gates on it |\n| \\`verify.record-miscounted\\` (error) | a verification.yaml whose \\`summary\\` block contradicts its own \\`claims[]\\` — neither half can be believed, so the record is refused as unreadable rather than trusted in either direction | re-record from the service's repo; never edit the counts by hand |\n| \\`verify.evidence-token-missing\\` (warn) | a just-confirmed citation's file at the attested commit does not contain the claim's own token (the operationId, message name or edge op) — the evidence may not show what the answer says | re-read that evidence while the answer can still be corrected; verdict-neutral, it gates nothing |\n| \\`cross.agree-confirmed\\` / \\`cross.agree-unconfirmed\\` (ok) | a \\`--diff-answers\\` row where both blind answer sets reach the same verdict — the cheap-to-skip review queue, though same-model agreement is correlated and proves nothing | nothing on its own; record only the answer set you stand behind, in a separate run |\n| \\`cross.disagree\\` (the rows a human reads first) | a \\`--diff-answers\\` row where the two blind answer sets contradict each other — one of them is wrong about the code | read both cited evidences and decide; the diff writes nothing and never moves a verdict |\n| \\`cross.evidence-disjoint\\` (warn) | a \\`--diff-answers\\` notice: claims BOTH sets confirm while citing non-overlapping files — two yeses that looked at different evidence | read those agreements second, after the disagreements; agreement over disjoint evidence is two guesses, not one proof |\n`,\n};\n\nexport const LOAM_SHIP: CommandContent = {\n name: \"loam-ship\",\n description: \"Archive a finished loam feature — merge its deltas into the living docs\",\n argumentHint: \"<FEAT-id>\",\n purpose:\n \"Merge a shipped feature into the living specs, API and landscape. This is the one command that rewrites the source of truth, so every step before it exists to make the merge reviewable before it runs.\",\n invocation: \"loam instructions loam-ship $1\",\n placeholders: [\"feature\"],\n spine: [\n \"confirm the code is actually built and merged — archiving early makes the docs claim something that does not exist\",\n \"`loam validate --feature $1` must come back valid; if it says nothing pinned the delta, rebase and re-validate first\",\n \"`loam archive $1 --dry-run` — read the whole plan and every warning before the merge touches anything\",\n \"`loam archive $1` for real, and read what it reports it did\",\n \"`loam unarchive $1` is the undo if the plan turns out to have been wrong\",\n ],\n body: `Archive a shipped feature.\n\n1. Confirm the code is actually built and merged. Archiving folds the delta into the\n living docs; doing it early makes the docs claim something that does not exist.\n \\`loam status $1 --json\\` answers \"is this actually finished\" in one call, and its\n \\`next[]\\` names what is outstanding if it is not.\n2. \\`loam validate --feature $1 --json\\` — must come back \\`valid: true\\`.\n If it reports \\`delta.baseline-missing\\` or \\`openapi.baseline-missing\\`, run\n \\`loam rebase $1\\` and re-validate BEFORE going on: those two say nothing has\n pinned what this delta was written against, so the merge cannot tell the text\n you EDITED from the text you merely restated. Both gate the archive — the\n dry run in step 3 refuses on them too — and the only way past without the\n pins is a human's \\`--approve\\`.\n3. \\`loam archive $1 --dry-run --json\\` first. \\`plan[]\\` is every file the merge would\n write (\\`create\\` / \\`update\\`, then the final \\`move\\` into \\`features/archive/\\`), and\n none of them are written — read it before letting the merge touch the source of\n truth, and stop if a file you did not expect is on it. \\`warnings[]\\` is what the\n merge will do that is legal but lossy — advisory warnings never block, which is\n exactly why you read them now: \\`openapi.op-modified\\` means an operation the living\n contract already defines gets overwritten wholesale (\\`openapi.component-modified\\`\n is the same story for a component the merged operations carry),\n \\`service.no-model\\` means a service arrives with no C4 centre behind it, and\n \\`delta.added-conflict\\`\n means another feature in flight adds the same requirement — the first to archive\n lands it, and the other's archive is then refused (\\`delta.added-duplicate\\`: its\n ADDED now collides with the living spec) unless a human \\`--approve\\`s the\n replacement.\n4. \\`loam archive $1 --json\\`. It merges three axes into the living state — requirements\n into \\`services/<svc>/spec.md\\`, endpoints into \\`services/<svc>/openapi.yaml\\`,\n elements and edges into \\`architecture/landscape.likec4\\` — then moves the feature\n under \\`features/archive/\\`. Success is \\`ok: true\\`; on \\`ok: false\\`, branch on\n \\`error.code\\`:\n - \\`not-coherent\\` — gating coherence issues; \\`issues[]\\` in the envelope lists them,\n each with \\`gates\\` and \\`overridable\\` resolved (advisory warnings do not block).\n Fix the breaches. \\`--approve\\` overrides the gating issues and is a human's\n call to make, not yours: report the breach and stop. Two codes it does not\n override even then — \\`delta.service-id-invalid\\` (a \\`specs/<svc>/\\` name the\n id grammar refuses) and \\`c4.service-binding-invalid\\` (a \\`metadata { service }\\`\n binding it refuses, anywhere inside a tagged element's block): each names a\n path the merge would write mechanically, they arrive with\n \\`overridable: false\\`, and the fix is a rename, never the flag.\n \\`openapi.remove-marker-path-level\\` arrives\n here too: an \\`x-loam-remove\\` written beside the methods instead of inside the\n operation retires nothing.\n - \\`living-outside-requirements\\` — the LIVING spec holds a requirement outside\n \\`## Requirements\\`, and the merge would duplicate it. Re-home it, then re-run.\n \\`--approve\\` does not move this one.\n - \\`archive-exists\\` — \\`features/archive/\\` already has that directory; a human\n decides what it is.\n - \\`merge-failed\\` — the merge could not be computed, or failed and was rolled\n back. Either way the living docs are unchanged; fix the reported cause, re-run.\n A file loam cannot decode as UTF-8 refuses here by name rather than being\n rewritten with replacement characters. So does a living document that still\n holds git conflict markers — the \\`spec.merge-conflict\\` /\n \\`landscape.merge-conflict\\` findings ride in \\`findings[]\\`, and \\`--approve\\`\n does not override them: the loss is mechanical, not a judgment call.\n - \\`docs-busy\\` — another \\`loam archive\\` or \\`loam unarchive\\` holds the docs\n repo's lock. Nothing was read or written; wait for it and re-run once.\n - \\`commit-interrupted\\` — a previous archive or unarchive was killed mid-commit\n and this run cannot repair it on its own: a half-written file was edited\n since, its pre-image is gone or altered, or \\`.loam-commit\\` cannot be read.\n Run \\`loam doctor\\` (\\`doctor.commit-interrupted\\` / \\`doctor.commit-unreadable\\`\n name the same state) and hand it to a human — re-running does not help.\n - \\`rollback-incomplete\\` — the merge failed AND some files could not be restored.\n Stop and hand it to a human; the message lists the files to check.\n\nArchived by mistake? \\`loam unarchive $1\\` restores the living docs from the snapshot\narchive left in \\`features/archive/$1*/.loam-before/\\` and re-opens the feature. Do not\nhand-edit the living docs back instead: the previous text of a \\`MODIFIED\\` requirement\nis in that snapshot and nowhere else, so anything you write by hand is a guess. It\nrefuses under \\`snapshot-corrupt\\` when a pre-image's bytes no longer match the digest\nthe archive recorded — \\`--force\\` does not override that one, because the damage is\nto the undo itself — and under \\`snapshot-missing\\` for an archive older than the\ncurrent snapshot format. Both mean version control is the way back.\n`,\n};\n"]}
1
+ {"version":3,"file":"closing.js","sourceRoot":"","sources":["../../../../src/core/agent/workflows/closing.ts"],"names":[],"mappings":"AAEA;;;;;;GAMG;AACH,MAAM,CAAC,MAAM,WAAW,GAAmB;IACzC,IAAI,EAAE,aAAa;IACnB,WAAW,EACT,qFAAqF;IACvF,YAAY,EAAE,WAAW;IACzB,OAAO,EACL,6NAA6N;IAC/N,UAAU,EAAE,kCAAkC;IAC9C,YAAY,EAAE,CAAC,SAAS,CAAC;IACzB,KAAK,EAAE;QACL,yEAAyE;QACzE,uGAAuG;QACvG,sFAAsF;QACtF,2EAA2E;QAC3E,6FAA6F;KAC9F;IACD,IAAI,EAAE;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;CAoJP;CACA,CAAC;AAEF,MAAM,CAAC,MAAM,SAAS,GAAmB;IACvC,IAAI,EAAE,WAAW;IACjB,WAAW,EAAE,yEAAyE;IACtF,YAAY,EAAE,WAAW;IACzB,OAAO,EACL,0MAA0M;IAC5M,UAAU,EAAE,gCAAgC;IAC5C,YAAY,EAAE,CAAC,SAAS,CAAC;IACzB,KAAK,EAAE;QACL,oHAAoH;QACpH,sHAAsH;QACtH,uGAAuG;QACvG,6DAA6D;QAC7D,0EAA0E;KAC3E;IACD,IAAI,EAAE;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;CA4EP;CACA,CAAC","sourcesContent":["import type { CommandContent } from \"../contract.js\";\n\n/**\n * The two post-build workflows, paired: the done-check (`loam-verify`) and the\n * merge it precedes (`loam-ship`). One file for two commands because six\n * workflows meet the five-file package cap somewhere, and this is the seam the\n * cycle already has — both describe what happens after the code is built, and\n * each body names the other's refusals (`record-federated`, `not-coherent`).\n */\nexport const LOAM_VERIFY: CommandContent = {\n name: \"loam-verify\",\n description:\n \"Verify a built loam feature against its own promises, claim by claim, with evidence\",\n argumentHint: \"<FEAT-id>\",\n purpose:\n \"Check that the code somebody built is the feature that was designed. loam derives the questions; a test runner and you answer them. loam never reads the service, so a verdict is worth exactly what its evidence is worth.\",\n invocation: \"loam instructions loam-verify $1\",\n placeholders: [\"feature\"],\n spine: [\n \"`loam verify` — the checklist, derived from the feature's own artifacts\",\n \"run the generated suite with a JSON report: the scenario claims are the runner's to answer, not yours\",\n \"answer the remaining claims yourself, every one carrying a `file:line` that resolves\",\n \"record from each affected service's own repository, bound to that service\",\n \"read the verdict honestly — a scenario confirmed on your word is `attested`, not `verified`\",\n ],\n body: `Check that the code somebody just built is the feature that was designed.\n\nloam derives the questions; the test runner and you answer them. loam never\nreads the service, so a verdict is worth exactly what its evidence is worth.\n\n1. \\`loam verify $1 --json\\`. \\`claims[]\\` is the checklist, derived from the feature's\n own artifacts — one claim per new service, per operation its openapi delta adds,\n per (direction, message) its asyncapi delta newly declares against the living\n contract's send/receive sets (quotes and removals are never questions),\n per tagged edge that names an operation, and per scenario of every changed\n requirement, arch.spec.md deltas included. Each has a stable \\`id\\` and a\n \\`subject\\` (the service whose code answers it); a claim that says arch.spec.md\n wants an integration/ops test, not an acceptance test. An ARCHIVED feature returns its record as frozen history instead\n (\\`frozen: true\\`) and \\`--record\\` / \\`--results\\` / \\`--contract-results\\` /\n \\`--diff-answers\\` refuse it — \\`loam unarchive\\` first if the answers really\n must change.\n2. The \\`scenario.tested\\` claims are the runner's to answer, not yours. In the\n service's repo, run the generated suite with a JSON report —\n \\`npx cucumber-js --format json:report.json\\` (cucumber-jvm, behave and\n SpecFlow emit the same format). The generated scenarios carry\n \\`@loam-digest-…\\` tags that ride into the report; that digest is the match,\n so never retitle or hand-edit a generated scenario to make one pass.\n If there is no runnable suite yet you MAY answer them yourself, and the record\n will say so: \\`answered_by: agent\\` makes the feature **attested**, not verified\n (\\`verified: false\\`, \\`verdict: \"attested\"\\`, the \\`verify.scenario-attested\\`\n notice, and a \\`next.verify-attested\\` step in \\`loam status\\`). Nothing gates on\n it — a legacy service with no suite must still be able to ship — but say so\n when you hand back, and close it with \\`--results\\` the moment a suite runs.\n \\`verify.digest-contested\\` (warn) is the case where two services word a\n scenario identically: they share one digest, so no single report can say whose\n suite ran it, and each service must record its own with \\`--service\\`.\n The \\`api.exposes\\` claims have a mechanical channel too, when an API\n contract-test suite exists: emit loam's contract-results shape from its\n report — \\`{\"loamContractReport\": 1, \"results\": [{\"operationId\": \"…\",\n \"status\": \"passed\"}]}\\`, a one-line transform for Specmatic, Pact, Dredd or a\n property harness — and pass \\`--contract-results contract.json\\` alongside.\n Only a status of exactly \\`passed\\` confirms the named operation\n (\\`answered_by: external-runner\\`); anything else, and an operation the report\n never exercised, stays \\`unconfirmed\\` with the reason, and under the flag the\n report owns every api.exposes claim, so take those out of the answers file.\n3. Answer every OTHER claim by finding it in the code. Not by reasoning that it\n must be there — by opening the file:\n - \\`service.exists\\` — the service is deployable: its build, its entry point.\n - \\`api.exposes\\` — the route handler serving that operationId, not the spec that\n declares it — unless a contract-test report already answered it in step 2.\n - \\`event.declares\\` — the code that puts the message on the wire (sends) or\n handles it off the wire (receives), not the contract that names it.\n - \\`c4.calls\\` — the call site in the CALLER.\n4. Write those answers as JSON — evidence is \\`file:line\\`, one entry per place it can\n be seen:\n \\`\\`\\`json\n { \"answers\": [\n { \"id\": \"<claim id>\", \"verdict\": \"confirmed\", \"evidence\": [\"src/split/Api.ts:42\"] },\n { \"id\": \"<claim id>\", \"verdict\": \"unconfirmed\", \"note\": \"the split service has no entry point yet\" }\n ] }\n \\`\\`\\`\n5. Optional, before recording: cross-examine. Have a SECOND agent answer the\n same checklist blind — a fresh context, given only \\`loam verify $1 --json\\`\n and the code, never your answers file — then compare the two sets:\n\n \\`\\`\\`sh\n loam verify $1 --diff-answers yours.json theirs.json --json\n \\`\\`\\`\n\n Both files must answer EVERY claim in the compared scope,\n \\`scenario.tested\\` included: the diff consults no test report, so this is a\n DIFFERENT file from the one step 6 records under \\`--results\\` (that one\n must omit the runner's claims; a step-6-shaped file here refuses\n \\`answers-mismatch\\`). Narrowing with \\`--service <id>\\` compares one\n service's claims — give the second agent the same lens, or its file will\n not match the compared scope. An archived feature refuses the diff too:\n frozen history has no current checklist. It writes nothing and changes no\n verdict: \\`cross.agree-confirmed\\` / \\`cross.agree-unconfirmed\\` rows are the\n cheap-to-skip queue, \\`disagreements[]\\` (\\`cross.disagree\\`) is what a human\n reads first, and the \\`cross.evidence-disjoint\\` notice flags agreements\n citing non-overlapping files — read those second. Agreement between two\n same-model agents is correlated, so it ranks review; it never upgrades\n \\`attested\\` to \\`verified\\`. Record only the answer set you stand behind, in\n a separate run.\n6. Record, **in each affected service's own repository**, one service at a time:\n\n \\`\\`\\`sh\n loam verify $1 --service <id> --results report.json [--contract-results contract.json] --record answers.json --json\n \\`\\`\\`\n\n \\`--service <id>\\` narrows the checklist to that service's claims and binds the\n answers to this repository's commit, writing ONE attestation into a shared\n \\`schema: 2\\` \\`verification.yaml\\`. Running it in the next repo adds that repo's\n attestation and leaves the others standing. In a repo whose ./loam.json\n declares a \\`service\\`, \\`--service\\` may be omitted — it is substituted, and the\n run still federates. \\`--results report.json\\` alone is legal when the\n checklist is all scenarios.\n\n ⚠ **Never run \\`loam verify $1 --record answers.json\\` without \\`--service\\`.**\n That is the legacy all-at-once form: it writes the WHOLE record from one place,\n and over a record that already carries other services' attestations it is\n refused (\\`record-federated\\`, naming them) precisely because it would erase\n them. If you meet that refusal, re-run with \\`--service\\`; do not work around it.\n\n The other refusals: \\`answers-mismatch\\` (an id\n nobody asked about, a claim you left out — re-run step 1, the feature moved —\n or an answers entry for a \\`scenario.tested\\` claim: the runner owns those),\n \\`answers-unevidenced\\` (a \\`confirmed\\` with nothing behind it),\n \\`answers-unreadable\\` (a report or answers file it cannot read),\n \\`record-unreadable\\` (a \\`verification.yaml\\` that exists but cannot be read as a\n record — it is never overwritten; a human repairs it, and\n \\`verify.record-miscounted\\` is the reason when its \\`summary\\` block contradicts\n its own \\`claims[]\\`: neither half can be believed, so re-record rather than\n editing the counts),\n \\`service-mismatch\\` (\\`--service\\` names a service this repo does not declare) and\n \\`repository-unavailable\\` (\\`--service\\` from a repo that declares none, or whose\n commit cannot be read — an attestation must be bound to the code it is about).\n7. Report every \\`unconfirmed\\` claim and what it would take to close it, and read\n \\`discarded\\` in the payload: it names every previous answer the new record does\n not carry, and each one is either evidence that went stale or evidence that was\n just dropped. A scenario claim the report failed or missed closes by fixing the\n code and re-running the suite — never by editing the generated .feature. While\n any claim stays unconfirmed or unanswered, \\`notices[]\\` carries\n \\`verify.claims-open\\` (warn, gating nothing): the one-line \"not a clean\n result\" summary with all four counts, so a partial record can never read as a\n finished one from the notice lines alone.\n8. Reading is safe from anywhere: \\`loam verify $1 --service <id> --json\\` without\n \\`--record\\` writes nothing and works from the docs repo too. \\`services\\` lists\n every subject of the checklist, so it is how you find out which repos still\n owe an attestation, and \\`loam status $1 --json\\` says the same thing in the\n shape of a next step.\n\nAnything you cannot show is \\`unconfirmed\\` with a note. That is a successful record,\nand it is the useful one: nothing gates on this file, so the only thing it is for is\ntelling the next reader the truth. \\`loam archive\\` will ship the feature either way —\nwhich is exactly why a \\`confirmed\\` you cannot back up costs nothing now and misleads\neveryone later.\n\n\\`loam verify\\` — the notice and record codes its reports carry in \\`notices[]\\`,\ngathered from the steps above; branch on the code, never the prose:\n\n| code | what it means | what to do |\n|---|---|---|\n| \\`verify.claims-open\\` (warn) | a recorded feature still has unconfirmed or unanswered claims — the one-line \"not a clean result\" summary with all four counts, so a partial record can never read as finished from the notice lines alone; a feature with no record at all never carries it | close each open claim: fix the code and re-run the suite for a failed scenario, re-answer the rest with evidence, then re-record |\n| \\`verify.scenario-attested\\` (warn) | scenario claims were answered on an agent's word (\\`answered_by: agent\\`), so the verdict is **attested**, not verified — nothing gates on it, but say so when you hand back | run the generated suite with a JSON report and re-record with \\`--results report.json\\` the moment a suite exists |\n| \\`verify.digest-contested\\` (warn) | two or more services word a scenario identically, so they share one \\`@loam-digest-…\\` tag and no single cucumber report can say whose suite ran it — those scenario claims stay unconfirmed | each service records its own claims from its own repository with \\`--service <id>\\` |\n| \\`verify.operation-contested\\` (warn) | two services on the checklist expose the same operationId, so a contract report — which names no service — cannot say whose suite exercised it; those claims stay unconfirmed | each service records its own with \\`--service <id>\\`, passing its own contract report |\n| \\`verify.checklist-forked\\` (warn) | one or more services answered a DIFFERENT version of the feature's question set than the one it now asks — each attestation carries the \\`checklist\\` digest it answered, and the notice names the ones that do not match the current checklist. Measured against the present, not pairwise: services that all attested against a since-rewritten delta agree with each other perfectly and are all stale. Normal mid-rollout, since services attest as they finish. Attestations written before the field existed carry no digest and are never called stale | re-record the services on the older version from their own repositories (\\`loam verify <FEAT> --service <id> --record\\`), or accept the split knowingly — nothing gates on it |\n| \\`verify.record-miscounted\\` (error) | a verification.yaml whose \\`summary\\` block contradicts its own \\`claims[]\\` — neither half can be believed, so the record is refused as unreadable rather than trusted in either direction | re-record from the service's repo; never edit the counts by hand |\n| \\`verify.evidence-token-missing\\` (warn) | a just-confirmed citation's file at the attested commit does not contain the claim's own token (the operationId, message name or edge op) — the evidence may not show what the answer says | re-read that evidence while the answer can still be corrected; verdict-neutral, it gates nothing |\n| \\`cross.agree-confirmed\\` / \\`cross.agree-unconfirmed\\` (ok) | a \\`--diff-answers\\` row where both blind answer sets reach the same verdict — the cheap-to-skip review queue, though same-model agreement is correlated and proves nothing | nothing on its own; record only the answer set you stand behind, in a separate run |\n| \\`cross.disagree\\` (the rows a human reads first) | a \\`--diff-answers\\` row where the two blind answer sets contradict each other — one of them is wrong about the code | read both cited evidences and decide; the diff writes nothing and never moves a verdict |\n| \\`cross.evidence-disjoint\\` (warn) | a \\`--diff-answers\\` notice: claims BOTH sets confirm while citing non-overlapping files — two yeses that looked at different evidence | read those agreements second, after the disagreements; agreement over disjoint evidence is two guesses, not one proof |\n`,\n};\n\nexport const LOAM_SHIP: CommandContent = {\n name: \"loam-ship\",\n description: \"Archive a finished loam feature — merge its deltas into the living docs\",\n argumentHint: \"<FEAT-id>\",\n purpose:\n \"Merge a shipped feature into the living specs, API and landscape. This is the one command that rewrites the source of truth, so every step before it exists to make the merge reviewable before it runs.\",\n invocation: \"loam instructions loam-ship $1\",\n placeholders: [\"feature\"],\n spine: [\n \"confirm the code is actually built and merged — archiving early makes the docs claim something that does not exist\",\n \"`loam validate --feature $1` must come back valid; if it says nothing pinned the delta, rebase and re-validate first\",\n \"`loam archive $1 --dry-run` — read the whole plan and every warning before the merge touches anything\",\n \"`loam archive $1` for real, and read what it reports it did\",\n \"`loam unarchive $1` is the undo if the plan turns out to have been wrong\",\n ],\n body: `Archive a shipped feature.\n\n1. Confirm the code is actually built and merged. Archiving folds the delta into the\n living docs; doing it early makes the docs claim something that does not exist.\n \\`loam status $1 --json\\` answers \"is this actually finished\" in one call, and its\n \\`next[]\\` names what is outstanding if it is not.\n2. \\`loam validate --feature $1 --json\\` — must come back \\`valid: true\\`.\n If it reports \\`delta.baseline-missing\\` or \\`openapi.baseline-missing\\`, run\n \\`loam rebase $1\\` and re-validate BEFORE going on: those two say nothing has\n pinned what this delta was written against, so the merge cannot tell the text\n you EDITED from the text you merely restated. Both gate the archive — the\n dry run in step 3 refuses on them too — and the only way past without the\n pins is a human's \\`--approve\\`.\n3. \\`loam archive $1 --dry-run --json\\` first. \\`plan[]\\` is every file the merge would\n write (\\`create\\` / \\`update\\`, then the final \\`move\\` into \\`features/archive/\\`), and\n none of them are written — read it before letting the merge touch the source of\n truth, and stop if a file you did not expect is on it. \\`warnings[]\\` is what the\n merge will do that is legal but lossy — advisory warnings never block, which is\n exactly why you read them now: \\`openapi.op-modified\\` means an operation the living\n contract already defines gets overwritten wholesale (\\`openapi.component-modified\\`\n is the same story for a component the merged operations carry),\n \\`service.no-model\\` means a service arrives with no C4 centre behind it, and\n \\`delta.added-conflict\\`\n means another feature in flight adds the same requirement — the first to archive\n lands it, and the other's archive is then refused (\\`delta.added-duplicate\\`: its\n ADDED now collides with the living spec) unless a human \\`--approve\\`s the\n replacement.\n4. \\`loam archive $1 --json\\`. It merges three axes into the living state — requirements\n into \\`services/<svc>/spec.md\\`, endpoints into \\`services/<svc>/openapi.yaml\\`,\n elements and edges into \\`architecture/landscape.likec4\\` and, for an addition nested\n under a service whose model EXTENDS the map, into that\n \\`services/<…>/<svc>/model.likec4\\` instead — then moves the feature\n under \\`features/archive/\\`. Success is \\`ok: true\\`; on \\`ok: false\\`, branch on\n \\`error.code\\`:\n - \\`not-coherent\\` — gating coherence issues; \\`issues[]\\` in the envelope lists them,\n each with \\`gates\\` and \\`overridable\\` resolved (advisory warnings do not block).\n Fix the breaches. \\`--approve\\` overrides the gating issues and is a human's\n call to make, not yours: report the breach and stop. Two codes it does not\n override even then — \\`delta.service-id-invalid\\` (a \\`specs/<svc>/\\` name the\n id grammar refuses) and \\`c4.service-binding-invalid\\` (a \\`metadata { service }\\`\n binding it refuses, anywhere inside a tagged element's block): each names a\n path the merge would write mechanically, they arrive with\n \\`overridable: false\\`, and the fix is a rename, never the flag.\n \\`openapi.remove-marker-path-level\\` arrives\n here too: an \\`x-loam-remove\\` written beside the methods instead of inside the\n operation retires nothing.\n - \\`living-outside-requirements\\` — the LIVING spec holds a requirement outside\n \\`## Requirements\\`, and the merge would duplicate it. Re-home it, then re-run.\n \\`--approve\\` does not move this one.\n - \\`archive-exists\\` — \\`features/archive/\\` already has that directory; a human\n decides what it is.\n - \\`merge-failed\\` — the merge could not be computed, or failed and was rolled\n back. Either way the living docs are unchanged; fix the reported cause, re-run.\n A file loam cannot decode as UTF-8 refuses here by name rather than being\n rewritten with replacement characters. So does a living document that still\n holds git conflict markers — the \\`spec.merge-conflict\\` /\n \\`landscape.merge-conflict\\` findings ride in \\`findings[]\\`, and \\`--approve\\`\n does not override them: the loss is mechanical, not a judgment call.\n - \\`docs-busy\\` — another \\`loam archive\\` or \\`loam unarchive\\` holds the docs\n repo's lock. Nothing was read or written; wait for it and re-run once.\n - \\`commit-interrupted\\` — a previous archive or unarchive was killed mid-commit\n and this run cannot repair it on its own: a half-written file was edited\n since, its pre-image is gone or altered, or \\`.loam-commit\\` cannot be read.\n Run \\`loam doctor\\` (\\`doctor.commit-interrupted\\` / \\`doctor.commit-unreadable\\`\n name the same state) and hand it to a human — re-running does not help.\n - \\`rollback-incomplete\\` — the merge failed AND some files could not be restored.\n Stop and hand it to a human; the message lists the files to check.\n\nArchived by mistake? \\`loam unarchive $1\\` restores the living docs from the snapshot\narchive left in \\`features/archive/$1*/.loam-before/\\` and re-opens the feature. Do not\nhand-edit the living docs back instead: the previous text of a \\`MODIFIED\\` requirement\nis in that snapshot and nowhere else, so anything you write by hand is a guess. It\nrefuses under \\`snapshot-corrupt\\` when a pre-image's bytes no longer match the digest\nthe archive recorded — \\`--force\\` does not override that one, because the damage is\nto the undo itself — and under \\`snapshot-missing\\` for an archive older than the\ncurrent snapshot format. Both mean version control is the way back.\n`,\n};\n"]}
@@ -26,8 +26,11 @@ import { landscapePath } from "../repo/paths.js";
26
26
  import { locateServicePaths } from "../repo/service-target.js";
27
27
  import { VALIDATE_CHECKS } from "./checks.js";
28
28
  import { UNCHECKED } from "./unchecked.js";
29
- import { landscapeArtifact, landscapeContext } from "./landscape.js";
29
+ import { landscapeContext } from "./landscape.js";
30
+ import { landscapeArtifact } from "./map/owed.js";
31
+ import { exampleFor, extendPoint, FQN } from "./map/extend.js";
30
32
  import { ARTIFACTS } from "./targets.js";
33
+ import { EXTERNAL_TAG } from "../vocabulary/maturity.js";
31
34
  /**
32
35
  * The architecture walk: the order a service is read in, and what each stop is
33
36
  * for.
@@ -135,9 +138,17 @@ export async function serviceBrief(docsDir, service, at) {
135
138
  const paths = at ?? (await locateServicePaths(docsDir, service));
136
139
  const rel = (abs) => relative(docsDir, abs).split(/[\\/]/).join("/");
137
140
  // Read before the artifact loop, not after it: what the fleet already says
138
- // about this service decides whether it owes an API contract at all.
139
- const landscape = await landscapeContext(docsDir, service);
141
+ // about this service decides whether it owes an API contract at all. The
142
+ // model path travels with it — located through the enumeration above, never
143
+ // joined at `services/<id>/` — so the edgeless state can name the calls the
144
+ // service's own model attests.
145
+ const landscape = await landscapeContext(docsDir, service, paths);
140
146
  const owesApi = apiExpected(landscape);
147
+ // The one id the model target's grammar and example are written against,
148
+ // resolved here because `ARTIFACTS` is static on purpose (a target list that
149
+ // varied per repository would be a second thing to keep true) and because this
150
+ // is the only place the fleet map has already been read.
151
+ const extend = extendPoint(landscape);
141
152
  const targets = [];
142
153
  for (const { key, ...artifact } of ARTIFACTS) {
143
154
  // `adrs/` is a directory, and an empty one is not an existing artifact —
@@ -147,20 +158,54 @@ export async function serviceBrief(docsDir, service, at) {
147
158
  targets.push({
148
159
  ...artifact,
149
160
  ...(key === "openapi" ? { required: owesApi } : {}),
161
+ // Every `<fqn>` in the model target, replaced by the element this service
162
+ // actually extends. The placeholder is left literal in `targets.ts` — the
163
+ // brief is handed to an agent that will WRITE the file, and
164
+ // `extend <fqn> {` copied verbatim is a document that does not parse.
165
+ shape: artifact.shape.map((rule) => rule.replaceAll(FQN, extend.id)),
166
+ ...(artifact.example === undefined ? {} : { example: exampleFor(artifact.example, extend, service) }),
150
167
  path: dir ? `${rel(paths[key])}/` : rel(paths[key]),
151
168
  exists,
152
169
  action: exists ? "diff" : "create",
153
170
  });
154
171
  }
155
- // The eighth target is the fleet map, and it is conditional: a service the
156
- // landscape ALREADY resolves an element to owes it nothing, and briefing an
157
- // edit there would invite an agent to draw a second box for a service that
158
- // has one. Everything else about the target is unconditional — the file is
172
+ // The eighth target is the fleet map, and it is conditional on TWO facts: an
173
+ // element resolving to the service, and an edge touching it. A service the
174
+ // landscape already draws AND reaches owes it nothing, and briefing an edit
175
+ // there would invite an agent to draw a second box for a service that has
176
+ // one. A service with a box and no edge — what `loam seed` leaves for every
177
+ // service nobody listed under `calls:` — owes it the edges, and until
178
+ // 2026-09-03 element existence alone read as "the map is complete" here, so
179
+ // the brief declared the map finished for exactly the services it had not
180
+ // reached. Everything else about the target is unconditional — the file is
159
181
  // shared, so `action` is `edit` whenever it exists and `create` only for the
160
182
  // very first service of a brand-new docs repo.
161
- if (landscape.modelled !== true) {
183
+ //
184
+ // `modelled === false` and not `!== true`: `null` is "loam could not read the
185
+ // map", and it fell through the same branch as "the map genuinely draws
186
+ // nothing for this service". The brief then handed over the UNDRAWN target —
187
+ // whose shape rule is "add one top-level element bound to its directory" and
188
+ // whose example is a block to splice — for services the map already declares
189
+ // and binds, and an agent that spliced it earned `landscape.binding-duplicate`
190
+ // for doing exactly what the brief asked. With the map unreadable there is no
191
+ // write to ask for; `landscape.instruction` carries the whole answer and names
192
+ // the documents that failed.
193
+ if (landscape.modelled === false || landscape.touched === false) {
162
194
  targets.push({
163
- ...landscapeArtifact(service, landscape.expects, landscape.present, rel(paths.dir)),
195
+ ...landscapeArtifact({
196
+ service,
197
+ expects: landscape.expects,
198
+ present: landscape.present,
199
+ servicePath: rel(paths.dir),
200
+ // The existing element's id, in the edgeless state only: it switches
201
+ // the example from a block to edges on that element.
202
+ // …and whether it is `#external`, which decides what the shape rule may
203
+ // promise: `landscape.service-isolated` never fires on such an element.
204
+ ...(landscape.touched === false && landscape.elements[0] !== undefined
205
+ ? { elementId: landscape.elements[0].id, external: landscape.elements[0].tags.includes(EXTERNAL_TAG) }
206
+ : {}),
207
+ attested: landscape.attested,
208
+ }),
164
209
  path: rel(landscapePath(docsDir)),
165
210
  exists: landscape.present,
166
211
  action: landscape.present ? "edit" : "create",
@@ -215,11 +260,18 @@ async function hasMarkdown(dir) {
215
260
  * in `openapi.yaml`'s shape rules (`ARTIFACTS`, targets.ts).
216
261
  *
217
262
  * Positive evidence only, exactly as `list` reads it: a landscape that is
218
- * absent or does not parse proves nothing about who calls this service, so the
263
+ * absent or unreadable proves nothing about who calls this service, so the
219
264
  * contract is still owed. `expects` is the ops on inbound edges, which is
220
265
  * `list`'s `called` set seen from one service.
266
+ *
267
+ * `callersKnown` and not `parses`, because since W4 `parses` answers about the
268
+ * whole `architecture/` PROJECT: a broken palette or use case beside a landscape
269
+ * loam reads perfectly made this discard the map and demand an OpenAPI contract
270
+ * of a browser UI — inventing an API into the source of truth, which is the one
271
+ * class of guess this module exists to refuse. `callersKnown` is the narrower
272
+ * fact this question actually needs, and it is the one `list` and `context` read.
221
273
  */
222
274
  function apiExpected(landscape) {
223
- return !landscape.parses || landscape.expects.length > 0;
275
+ return !landscape.callersKnown || landscape.expects.length > 0;
224
276
  }
225
277
  //# sourceMappingURL=brief.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"brief.js","sourceRoot":"","sources":["../../../src/core/brief/brief.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * The adoption brief: what an agent needs in order to write one service's\n * baseline documentation, and what will be checked when it does.\n *\n * `adopt` was specified for years as \"read the code, emit the C4\". It is not\n * being built, because it cannot be: nothing deterministic reads a legacy\n * service and says what its architecture MEANS, and a guessed model is worse\n * than no model — everyone downstream has to re-derive it to know whether to\n * believe it.\n *\n * So loam takes the half of the job that IS deterministic. It states the work\n * precisely (which files, in which grammar, bound to which existing elements),\n * it says exactly which checks the result will face, and then it says which\n * checks do not exist — because everything in that last list is honesty no\n * validator will ever supply, and an agent that does not know where the checking\n * stops will assume it never does.\n *\n * Nothing here reads code. `sources` in the frontmatter is the only line that\n * ties the result to a repository, which is why the brief argues for it instead\n * of merely listing it.\n */\nimport { existsSync, statSync } from \"node:fs\";\nimport { readdir } from \"node:fs/promises\";\nimport { join, relative } from \"node:path\";\nimport type { PathableService } from \"../kernel/ids/service.js\";\nimport { landscapePath, type ServicePaths } from \"../repo/paths.js\";\nimport { locateServicePaths } from \"../repo/service-target.js\";\nimport { VALIDATE_CHECKS, type BriefCheck } from \"./checks.js\";\nimport { UNCHECKED } from \"./unchecked.js\";\nimport { landscapeArtifact, landscapeContext, type LandscapeContext } from \"./landscape.js\";\nimport { ARTIFACTS, type BriefTarget } from \"./targets.js\";\nimport type { DocsDir } from \"../kernel/ids/dirs.js\";\n\n/* ------------------------------------------------------------------ */\n/* The walk — what to read, in what order, and where each finding lands */\n/* ------------------------------------------------------------------ */\n\nexport interface WalkStop {\n /** What to open, named as a thing to go and find rather than a file name. */\n where: string;\n /** What to take out of it. */\n find: string;\n /** The artifacts this stop feeds, by `artifact` name from `targets[]`. */\n lands: string[];\n}\n\n/**\n * The architecture walk: the order a service is read in, and what each stop is\n * for.\n *\n * The brief said \"read the code: entry points, HTTP routes and handlers,\n * published events, config, deploy manifests, tests\" — one sentence, six nouns,\n * no order and no landing site. What came back matched the sentence: the HTTP\n * surface documented well, because it is the noun an agent recognises fastest,\n * and the scheduler, the consumer group and the outbox missing entirely. Not\n * because they were judged unimportant — because nothing said to go and look,\n * and every check loam owns is computed from what was written rather than from\n * what is there. `UNCHECKED` has said COMPLETENESS is unverifiable since the\n * beginning; that is an argument for stating the sweep, not for leaving it\n * implied.\n *\n * The order is deliberate and it is not the order a reader browses in. Stops 1\n * and 2 fix the SHAPE of the service — which processes exist at all — before\n * any surface is enumerated, because an agent that starts at the HTTP routes\n * has already concluded the service is an API by the time it meets the\n * scheduler. Everything after them is one surface each.\n *\n * `lands` names artifacts rather than describing them, because the two lists\n * are joined: test/agent-contract.test.ts requires every name here to be a\n * target the same brief hands over, so a stop can never feed a file nobody was\n * asked to write.\n */\nexport const WALK: WalkStop[] = [\n {\n where: \"the build and dependency manifests — pom.xml, build.gradle, package.json, go.mod, requirements.txt, and whatever the repo's own README calls the build\",\n find: \"what this service is built from, and every client library it links against. A broker client, an HTTP client, a database driver: each one is a dependency that has to appear as an edge or a container later, and this list is the only place they are all named at once. A library whose SEMANTICS decide behaviour — a retry budget, a delivery guarantee, a serialization format — is an arch requirement (arch.spec.md), not a fact to leave in your head.\",\n lands: [\"model.likec4\", \"runbook.md\", \"arch.spec.md\"],\n },\n {\n where: \"the entry points — main/Application/server bootstrap, the DI or module wiring, and anything the deploy manifests actually start\",\n find: \"how many PROCESSES this service is, and what each one is. An API, a worker, a scheduler, a consumer — a service that is three of those and documented as one is the most common way a baseline is wrong, and it is wrong in a shape no later check can see.\",\n lands: [\"model.likec4\"],\n },\n {\n where: \"the HTTP surface — every route, controller, handler and middleware, counted\",\n find: \"the operation set, in full, and inside each operation the DECISIONS its code makes: the permission and role checks — each one is a `Requires:` entry, and a name `architecture/permissions.yaml` does not declare yet is a line to ADD to that shared file, never to invent locally — the fields required only when another field is present, the defaults and normalisations applied before anything is stored, the transitions a request is refused for. Count both — operations, and branches per operation — before you write, and say both counts in the hand-back. `api.ungoverned` can tell you an operation you WROTE has no requirement; nothing at all can tell you about the twelve you never wrote down, or about the thirty branches inside the one you did. A guard is not implementation detail: what a service REFUSES is behaviour its caller observes and a test pins, and it is the half of a legacy service that lives only in the code and in one developer's head.\",\n lands: [\"openapi.yaml\", \"spec.md\"],\n },\n {\n where: \"the message surface — producers, consumers, listener annotations, topic and queue names, the outbox table and its relay\",\n find: \"every message this service puts on and takes off the bus, with its direction, and — on the consuming side — the DECISIONS the handler makes about a message before it acts on one. The direction is the half that gets lost: a consumer documented as a producer is a contract the whole fleet then joins against backwards. The guards are the half nobody looks for at all: which messages are filtered out and on what field, what happens to one whose entity has since been deleted, what is idempotency-keyed against a replay, what is dead-lettered rather than retried. Each of those is a scenario, and none of them appears anywhere in the contract — asyncapi.yaml describes the message, never what this service refuses to do with it.\",\n lands: [\"asyncapi.yaml\", \"landscape.likec4\"],\n },\n {\n where: \"the scheduled and background work — cron entries, timers, retry loops, reconcilers, migrations that run at boot\",\n find: \"behaviour no request ever triggers, and the conditions under which it does or does not run. It has requirements like anything else, and it is invisible to every surface above — which is why a baseline written from routes alone describes a service that does half of what it does. Take the decisions with it: which rows a reconciler SELECTS and which it skips, what a retry loop gives up on and after how long, what a migration does when it finds the state it is meant to fix already fixed. A scheduled job documented as 'runs nightly' is a requirement with no behaviour in it.\",\n lands: [\"spec.md\", \"arch.spec.md\"],\n },\n {\n where: \"the state — datastores, caches, buckets, and the schema or migration directory that says what is in them\",\n find: \"what this service OWNS versus what it borrows. Ownership is the fact the C4 is for, and it is the one nobody can reconstruct later from the code without re-reading all of it.\",\n lands: [\"model.likec4\"],\n },\n {\n where: \"the outbound calls — HTTP clients, generated stubs, gRPC channels, and the config that says which host each one points at\",\n find: \"who this service calls, and which operation each call names. These become the fleet map's edges; an edge missing here is a dependency the fleet cannot see, and `loam explore` will under-report the blast radius of every future change to the callee. An edge that only configuration attests is PROVISIONAL until the runtime stop: hold it until the deploy manifests confirm nothing switches that client off.\",\n lands: [\"landscape.likec4\"],\n },\n {\n where: \"the runtime — config and env vars, deploy manifests, pipelines, alert rules, dashboards\",\n find: \"how it is run and what pages whom. Alerts are the honest source for health.yaml: an SLO nobody agreed to is invention, but an alert that already fires is a fact somebody wrote down. Deployment manifests OVERRIDE configuration files, and they usually live in another repository — go and find them before any dependency that only configuration attests goes in the model, and check for an environment variable that disables the feature. A capability switched off at deploy time is not a dependency, however completely the config wires it: delete its edge and container, and record it in runbook.md's 'configured but not a dependency, and why' list. A manifest read from another repository is named in the runbook's prose — never in `sources`, which may only hold paths of this service's own repository. An EFFECTIVE configuration value behaviour depends on — the timeout the chart sets, the flag that switches a code path — is an arch requirement in arch.spec.md, with a Covers: line naming what it decides; loam holds its coverage even though it can never check the value.\",\n lands: [\"runbook.md\", \"health.yaml\", \"arch.spec.md\"],\n },\n {\n where: \"the tests, integration and component ones first\",\n find: \"the behaviours somebody already thought worth pinning. This is the PRIMARY source for scenarios, not the last sweep: a behaviour a test already pins is written FROM that test, and the code is read only for what no test covers. A test suite is the only part of a legacy service that states intent rather than mechanism, and a scenario derived from one is the only kind that starts out true. Carry the shape across as well as the content — a parameterised test (`@ParameterizedTest`, `@parametrize`, `test.each`) is one scenario with a table, not one scenario per case. Then say in the hand-back how well this service is covered and how many of its tests became scenarios: that number is the ceiling on what this whole baseline can be worth, and nothing downstream can compute it.\",\n lands: [\"spec.md\", \"arch.spec.md\"],\n },\n];\n\n/**\n * The stop that closes the walk. Separate from `WALK` because it is not a place\n * to go — it is the question asked once the walking is done, and the only\n * defence against a sweep that felt thorough because the parts it covered were\n * covered well.\n */\nexport const WALK_CLOSE =\n \"Then list the directories you did NOT open, and say why for each. `loam validate --service <id>`, run inside the service's own repo, reports the same list back as `sources.unwalked` — it compares your `sources` against the files git tracks. A directory you deliberately skipped is a sentence in the hand-back; a directory you skipped without noticing is the gap this walk exists to find.\";\n\n/* ------------------------------------------------------------------ */\n/* The frontmatter */\n/* ------------------------------------------------------------------ */\n\nexport interface FrontmatterBrief {\n /** Field -> what to write in it. */\n fields: Record<string, string>;\n /** Fields to leave alone entirely. */\n never: string[];\n /** Why `sources` carries the weight. */\n why: string;\n}\n\nconst FRONTMATTER_BRIEF: FrontmatterBrief = {\n fields: {\n service: \"the directory this file lives under, exactly. A mismatch is an error (`frontmatter.field-mismatch`).\",\n status:\n \"`draft`, always. `verified` is a human's word, stamped by `loam vouch --service <id>` run inside the service's own repo — never written by hand.\",\n owner: \"the team or person who answers for this service.\",\n sources:\n \"the paths in THIS SERVICE'S repository you actually read to write the document — files and directories only, a directory covering everything beneath it; glob patterns are refused. Not the paths a reader would expect to have been read. This is NOT a reading list for a human: nobody follows these, and nothing about the document asks them to.\",\n },\n // `vouch_scope` is in this list for a sharper reason than its siblings: the\n // others are stamps an agent could only forge, while this one is a stamp an\n // agent could DELETE — and deleting it turns a vouch that read four sections\n // into one that read the document, on every surface at once.\n never: [\"last_verified\", \"sources_digest\", \"content_digest\", \"sources_files\", \"vouch_scope\"],\n why:\n \"`sources` is digest input, not a citation. It is the only mechanical tie between this document and the code — everything else loam checks is internal consistency, and a corpus can agree with itself perfectly while describing nothing that exists. `loam validate`, run inside the service's repo, checks every listed path is still there; `loam vouch` hashes their CONTENT so that a later `validate` can say the code has moved since anyone read it. That hash is the entire point, and it is why padding the list costs something real rather than merely being untidy: a path nobody read still gets hashed, so its next change reports as drift in a document it never described, and a path that WAS read and went unlisted moves under the document in silence. Neither failure is visible to a reader; both are decided by whether this list is honest.\",\n};\n\nexport interface Brief {\n service: string;\n /** Absolute docs repo root — the anchor the repo-relative paths hang off. */\n docsDir: DocsDir;\n /** Repo-relative path of services/<id>/. */\n path: string;\n targets: BriefTarget[];\n /** The order to read the service in, and what each stop feeds. */\n walk: WalkStop[];\n /** The question the walk ends on — see `WALK_CLOSE`. */\n walkClose: string;\n landscape: LandscapeContext;\n frontmatter: FrontmatterBrief;\n checks: BriefCheck[];\n unchecked: string[];\n /** The one instruction that outranks the rest. */\n rule: string;\n}\n\nconst NEVER_OVERWRITE =\n \"Do not overwrite an artifact that already exists. Read it, diff your findings against it, and report what disagrees — a document somebody wrote is evidence, and replacing it destroys the only copy of what they knew.\";\n\n/**\n * Assemble the brief for one service. Reads the docs repo; writes nothing.\n * `at` overrides where the artifacts are briefed to land — `adopt\n * --subsystem`'s target inside a group — and is only ever passed for a\n * service the enumeration does NOT answer: an existing service is briefed\n * where it already lives, because the brief's one rule is to never overwrite,\n * and a second address would brief a duplicate.\n */\nexport async function serviceBrief(\n docsDir: DocsDir,\n service: PathableService,\n at?: ServicePaths,\n): Promise<Brief> {\n const paths = at ?? (await locateServicePaths(docsDir, service));\n const rel = (abs: string): string => relative(docsDir, abs).split(/[\\\\/]/).join(\"/\");\n\n // Read before the artifact loop, not after it: what the fleet already says\n // about this service decides whether it owes an API contract at all.\n const landscape = await landscapeContext(docsDir, service);\n const owesApi = apiExpected(landscape);\n\n const targets: BriefTarget[] = [];\n for (const { key, ...artifact } of ARTIFACTS) {\n // `adrs/` is a directory, and an empty one is not an existing artifact —\n // reporting it as present would tell the agent to diff against nothing.\n const dir = key === \"adrsDir\";\n const exists = dir ? await hasMarkdown(paths[key]) : existsSync(paths[key]);\n targets.push({\n ...artifact,\n ...(key === \"openapi\" ? { required: owesApi } : {}),\n path: dir ? `${rel(paths[key])}/` : rel(paths[key]),\n exists,\n action: exists ? \"diff\" : \"create\",\n });\n }\n\n // The eighth target is the fleet map, and it is conditional: a service the\n // landscape ALREADY resolves an element to owes it nothing, and briefing an\n // edit there would invite an agent to draw a second box for a service that\n // has one. Everything else about the target is unconditional — the file is\n // shared, so `action` is `edit` whenever it exists and `create` only for the\n // very first service of a brand-new docs repo.\n if (landscape.modelled !== true) {\n targets.push({\n ...landscapeArtifact(service, landscape.expects, landscape.present, rel(paths.dir)),\n path: rel(landscapePath(docsDir)),\n exists: landscape.present,\n action: landscape.present ? \"edit\" : \"create\",\n });\n }\n\n return {\n service,\n docsDir,\n path: rel(paths.dir),\n targets,\n walk: WALK,\n walkClose: WALK_CLOSE,\n landscape,\n frontmatter: FRONTMATTER_BRIEF,\n checks: VALIDATE_CHECKS,\n unchecked: UNCHECKED,\n rule: NEVER_OVERWRITE,\n };\n}\n\nasync function hasMarkdown(dir: string): Promise<boolean> {\n if (!existsSync(dir)) return false;\n const entries = await readdir(dir, { withFileTypes: true });\n // `statSync` for the symlink case: a Dirent never follows one, so `isFile()`\n // is false for a symlinked ADR and the directory reads as empty (repo.ts's\n // `entryIs` is the same rule for `services/` and `features/`).\n return entries.some((e) => {\n if (!e.name.endsWith(\".md\")) return false;\n if (e.isFile()) return true;\n if (!e.isSymbolicLink()) return false;\n try {\n return statSync(join(dir, e.name)).isFile();\n } catch {\n return false;\n }\n });\n}\n\n/**\n * Does the fleet expect an API of this service?\n *\n * The brief marked `openapi.yaml` required unconditionally, and printed the\n * absence as `MISSING` in capitals — while `loam validate` and `loam list`\n * derive the opposite from the same landscape (`service.no-openapi` stays quiet\n * when nothing calls the service, and `list`'s maturity ladder stops demanding\n * a contract). For a UI, a worker or a cron the brief was therefore telling an\n * agent to INVENT an API contract into the source of truth — the one class of\n * guess this whole module exists to refuse, and the one the brief itself names\n * in `openapi.yaml`'s shape rules (`ARTIFACTS`, targets.ts).\n *\n * Positive evidence only, exactly as `list` reads it: a landscape that is\n * absent or does not parse proves nothing about who calls this service, so the\n * contract is still owed. `expects` is the ops on inbound edges, which is\n * `list`'s `called` set seen from one service.\n */\nfunction apiExpected(landscape: LandscapeContext): boolean {\n return !landscape.parses || landscape.expects.length > 0;\n}\n"]}
1
+ {"version":3,"file":"brief.js","sourceRoot":"","sources":["../../../src/core/brief/brief.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;GAoBG;AACH,OAAO,EAAE,UAAU,EAAE,QAAQ,EAAE,MAAM,SAAS,CAAC;AAC/C,OAAO,EAAE,OAAO,EAAE,MAAM,kBAAkB,CAAC;AAC3C,OAAO,EAAE,IAAI,EAAE,QAAQ,EAAE,MAAM,WAAW,CAAC;AAE3C,OAAO,EAAE,aAAa,EAAqB,MAAM,kBAAkB,CAAC;AACpE,OAAO,EAAE,kBAAkB,EAAE,MAAM,2BAA2B,CAAC;AAC/D,OAAO,EAAE,eAAe,EAAmB,MAAM,aAAa,CAAC;AAC/D,OAAO,EAAE,SAAS,EAAE,MAAM,gBAAgB,CAAC;AAC3C,OAAO,EAAE,gBAAgB,EAAyB,MAAM,gBAAgB,CAAC;AACzE,OAAO,EAAE,iBAAiB,EAAE,MAAM,eAAe,CAAC;AAClD,OAAO,EAAE,UAAU,EAAE,WAAW,EAAE,GAAG,EAAE,MAAM,iBAAiB,CAAC;AAC/D,OAAO,EAAE,SAAS,EAAoB,MAAM,cAAc,CAAC;AAE3D,OAAO,EAAE,YAAY,EAAE,MAAM,2BAA2B,CAAC;AAezD;;;;;;;;;;;;;;;;;;;;;;;;;GAyBG;AACH,MAAM,CAAC,MAAM,IAAI,GAAe;IAC9B;QACE,KAAK,EAAE,wJAAwJ;QAC/J,IAAI,EAAE,+bAA+b;QACrc,KAAK,EAAE,CAAC,cAAc,EAAE,YAAY,EAAE,cAAc,CAAC;KACtD;IACD;QACE,KAAK,EAAE,iIAAiI;QACxI,IAAI,EAAE,6PAA6P;QACnQ,KAAK,EAAE,CAAC,cAAc,CAAC;KACxB;IACD;QACE,KAAK,EAAE,6EAA6E;QACpF,IAAI,EAAE,07BAA07B;QACh8B,KAAK,EAAE,CAAC,cAAc,EAAE,SAAS,CAAC;KACnC;IACD;QACE,KAAK,EAAE,yHAAyH;QAChI,IAAI,EAAE,utBAAutB;QAC7tB,KAAK,EAAE,CAAC,eAAe,EAAE,kBAAkB,CAAC;KAC7C;IACD;QACE,KAAK,EAAE,iHAAiH;QACxH,IAAI,EAAE,ikBAAikB;QACvkB,KAAK,EAAE,CAAC,SAAS,EAAE,cAAc,CAAC;KACnC;IACD;QACE,KAAK,EAAE,0GAA0G;QACjH,IAAI,EAAE,gLAAgL;QACtL,KAAK,EAAE,CAAC,cAAc,CAAC;KACxB;IACD;QACE,KAAK,EAAE,2HAA2H;QAClI,IAAI,EAAE,qZAAqZ;QAC3Z,KAAK,EAAE,CAAC,kBAAkB,CAAC;KAC5B;IACD;QACE,KAAK,EAAE,yFAAyF;QAChG,IAAI,EAAE,gjCAAgjC;QACtjC,KAAK,EAAE,CAAC,YAAY,EAAE,aAAa,EAAE,cAAc,CAAC;KACrD;IACD;QACE,KAAK,EAAE,iDAAiD;QACxD,IAAI,EAAE,4wBAA4wB;QAClxB,KAAK,EAAE,CAAC,SAAS,EAAE,cAAc,CAAC;KACnC;CACF,CAAC;AAEF;;;;;GAKG;AACH,MAAM,CAAC,MAAM,UAAU,GACrB,qYAAqY,CAAC;AAexY,MAAM,iBAAiB,GAAqB;IAC1C,MAAM,EAAE;QACN,OAAO,EAAE,sGAAsG;QAC/G,MAAM,EACJ,kJAAkJ;QACpJ,KAAK,EAAE,kDAAkD;QACzD,OAAO,EACL,uVAAuV;KAC1V;IACD,4EAA4E;IAC5E,4EAA4E;IAC5E,6EAA6E;IAC7E,6DAA6D;IAC7D,KAAK,EAAE,CAAC,eAAe,EAAE,gBAAgB,EAAE,gBAAgB,EAAE,eAAe,EAAE,aAAa,CAAC;IAC5F,GAAG,EACD,u0BAAu0B;CAC10B,CAAC;AAqBF,MAAM,eAAe,GACnB,yNAAyN,CAAC;AAE5N;;;;;;;GAOG;AACH,MAAM,CAAC,KAAK,UAAU,YAAY,CAChC,OAAgB,EAChB,OAAwB,EACxB,EAAiB;IAEjB,MAAM,KAAK,GAAG,EAAE,IAAI,CAAC,MAAM,kBAAkB,CAAC,OAAO,EAAE,OAAO,CAAC,CAAC,CAAC;IACjE,MAAM,GAAG,GAAG,CAAC,GAAW,EAAU,EAAE,CAAC,QAAQ,CAAC,OAAO,EAAE,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,CAAC,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC;IAErF,2EAA2E;IAC3E,yEAAyE;IACzE,4EAA4E;IAC5E,4EAA4E;IAC5E,+BAA+B;IAC/B,MAAM,SAAS,GAAG,MAAM,gBAAgB,CAAC,OAAO,EAAE,OAAO,EAAE,KAAK,CAAC,CAAC;IAClE,MAAM,OAAO,GAAG,WAAW,CAAC,SAAS,CAAC,CAAC;IAEvC,yEAAyE;IACzE,6EAA6E;IAC7E,+EAA+E;IAC/E,yDAAyD;IACzD,MAAM,MAAM,GAAG,WAAW,CAAC,SAAS,CAAC,CAAC;IAEtC,MAAM,OAAO,GAAkB,EAAE,CAAC;IAClC,KAAK,MAAM,EAAE,GAAG,EAAE,GAAG,QAAQ,EAAE,IAAI,SAAS,EAAE,CAAC;QAC7C,yEAAyE;QACzE,wEAAwE;QACxE,MAAM,GAAG,GAAG,GAAG,KAAK,SAAS,CAAC;QAC9B,MAAM,MAAM,GAAG,GAAG,CAAC,CAAC,CAAC,MAAM,WAAW,CAAC,KAAK,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC,CAAC,UAAU,CAAC,KAAK,CAAC,GAAG,CAAC,CAAC,CAAC;QAC5E,OAAO,CAAC,IAAI,CAAC;YACX,GAAG,QAAQ;YACX,GAAG,CAAC,GAAG,KAAK,SAAS,CAAC,CAAC,CAAC,EAAE,QAAQ,EAAE,OAAO,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC;YACnD,0EAA0E;YAC1E,0EAA0E;YAC1E,4DAA4D;YAC5D,sEAAsE;YACtE,KAAK,EAAE,QAAQ,CAAC,KAAK,CAAC,GAAG,CAAC,CAAC,IAAI,EAAE,EAAE,CAAC,IAAI,CAAC,UAAU,CAAC,GAAG,EAAE,MAAM,CAAC,EAAE,CAAC,CAAC;YACpE,GAAG,CAAC,QAAQ,CAAC,OAAO,KAAK,SAAS,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,EAAE,OAAO,EAAE,UAAU,CAAC,QAAQ,CAAC,OAAO,EAAE,MAAM,EAAE,OAAO,CAAC,EAAE,CAAC;YACrG,IAAI,EAAE,GAAG,CAAC,CAAC,CAAC,GAAG,GAAG,CAAC,KAAK,CAAC,GAAG,CAAC,CAAC,GAAG,CAAC,CAAC,CAAC,GAAG,CAAC,KAAK,CAAC,GAAG,CAAC,CAAC;YACnD,MAAM;YACN,MAAM,EAAE,MAAM,CAAC,CAAC,CAAC,MAAM,CAAC,CAAC,CAAC,QAAQ;SACnC,CAAC,CAAC;IACL,CAAC;IAED,6EAA6E;IAC7E,2EAA2E;IAC3E,4EAA4E;IAC5E,0EAA0E;IAC1E,4EAA4E;IAC5E,sEAAsE;IACtE,4EAA4E;IAC5E,0EAA0E;IAC1E,2EAA2E;IAC3E,6EAA6E;IAC7E,+CAA+C;IAC/C,EAAE;IACF,8EAA8E;IAC9E,wEAAwE;IACxE,6EAA6E;IAC7E,6EAA6E;IAC7E,6EAA6E;IAC7E,+EAA+E;IAC/E,8EAA8E;IAC9E,+EAA+E;IAC/E,6BAA6B;IAC7B,IAAI,SAAS,CAAC,QAAQ,KAAK,KAAK,IAAI,SAAS,CAAC,OAAO,KAAK,KAAK,EAAE,CAAC;QAChE,OAAO,CAAC,IAAI,CAAC;YACX,GAAG,iBAAiB,CAAC;gBACnB,OAAO;gBACP,OAAO,EAAE,SAAS,CAAC,OAAO;gBAC1B,OAAO,EAAE,SAAS,CAAC,OAAO;gBAC1B,WAAW,EAAE,GAAG,CAAC,KAAK,CAAC,GAAG,CAAC;gBAC3B,qEAAqE;gBACrE,qDAAqD;gBACrD,wEAAwE;gBACxE,wEAAwE;gBACxE,GAAG,CAAC,SAAS,CAAC,OAAO,KAAK,KAAK,IAAI,SAAS,CAAC,QAAQ,CAAC,CAAC,CAAC,KAAK,SAAS;oBACpE,CAAC,CAAC,EAAE,SAAS,EAAE,SAAS,CAAC,QAAQ,CAAC,CAAC,CAAC,CAAC,EAAE,EAAE,QAAQ,EAAE,SAAS,CAAC,QAAQ,CAAC,CAAC,CAAC,CAAC,IAAI,CAAC,QAAQ,CAAC,YAAY,CAAC,EAAE;oBACtG,CAAC,CAAC,EAAE,CAAC;gBACP,QAAQ,EAAE,SAAS,CAAC,QAAQ;aAC7B,CAAC;YACF,IAAI,EAAE,GAAG,CAAC,aAAa,CAAC,OAAO,CAAC,CAAC;YACjC,MAAM,EAAE,SAAS,CAAC,OAAO;YACzB,MAAM,EAAE,SAAS,CAAC,OAAO,CAAC,CAAC,CAAC,MAAM,CAAC,CAAC,CAAC,QAAQ;SAC9C,CAAC,CAAC;IACL,CAAC;IAED,OAAO;QACL,OAAO;QACP,OAAO;QACP,IAAI,EAAE,GAAG,CAAC,KAAK,CAAC,GAAG,CAAC;QACpB,OAAO;QACP,IAAI,EAAE,IAAI;QACV,SAAS,EAAE,UAAU;QACrB,SAAS;QACT,WAAW,EAAE,iBAAiB;QAC9B,MAAM,EAAE,eAAe;QACvB,SAAS,EAAE,SAAS;QACpB,IAAI,EAAE,eAAe;KACtB,CAAC;AACJ,CAAC;AAED,KAAK,UAAU,WAAW,CAAC,GAAW;IACpC,IAAI,CAAC,UAAU,CAAC,GAAG,CAAC;QAAE,OAAO,KAAK,CAAC;IACnC,MAAM,OAAO,GAAG,MAAM,OAAO,CAAC,GAAG,EAAE,EAAE,aAAa,EAAE,IAAI,EAAE,CAAC,CAAC;IAC5D,6EAA6E;IAC7E,2EAA2E;IAC3E,+DAA+D;IAC/D,OAAO,OAAO,CAAC,IAAI,CAAC,CAAC,CAAC,EAAE,EAAE;QACxB,IAAI,CAAC,CAAC,CAAC,IAAI,CAAC,QAAQ,CAAC,KAAK,CAAC;YAAE,OAAO,KAAK,CAAC;QAC1C,IAAI,CAAC,CAAC,MAAM,EAAE;YAAE,OAAO,IAAI,CAAC;QAC5B,IAAI,CAAC,CAAC,CAAC,cAAc,EAAE;YAAE,OAAO,KAAK,CAAC;QACtC,IAAI,CAAC;YACH,OAAO,QAAQ,CAAC,IAAI,CAAC,GAAG,EAAE,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC,MAAM,EAAE,CAAC;QAC9C,CAAC;QAAC,MAAM,CAAC;YACP,OAAO,KAAK,CAAC;QACf,CAAC;IACH,CAAC,CAAC,CAAC;AACL,CAAC;AAED;;;;;;;;;;;;;;;;;;;;;;;GAuBG;AACH,SAAS,WAAW,CAAC,SAA2B;IAC9C,OAAO,CAAC,SAAS,CAAC,YAAY,IAAI,SAAS,CAAC,OAAO,CAAC,MAAM,GAAG,CAAC,CAAC;AACjE,CAAC","sourcesContent":["/**\n * The adoption brief: what an agent needs in order to write one service's\n * baseline documentation, and what will be checked when it does.\n *\n * `adopt` was specified for years as \"read the code, emit the C4\". It is not\n * being built, because it cannot be: nothing deterministic reads a legacy\n * service and says what its architecture MEANS, and a guessed model is worse\n * than no model — everyone downstream has to re-derive it to know whether to\n * believe it.\n *\n * So loam takes the half of the job that IS deterministic. It states the work\n * precisely (which files, in which grammar, bound to which existing elements),\n * it says exactly which checks the result will face, and then it says which\n * checks do not exist — because everything in that last list is honesty no\n * validator will ever supply, and an agent that does not know where the checking\n * stops will assume it never does.\n *\n * Nothing here reads code. `sources` in the frontmatter is the only line that\n * ties the result to a repository, which is why the brief argues for it instead\n * of merely listing it.\n */\nimport { existsSync, statSync } from \"node:fs\";\nimport { readdir } from \"node:fs/promises\";\nimport { join, relative } from \"node:path\";\nimport type { PathableService } from \"../kernel/ids/service.js\";\nimport { landscapePath, type ServicePaths } from \"../repo/paths.js\";\nimport { locateServicePaths } from \"../repo/service-target.js\";\nimport { VALIDATE_CHECKS, type BriefCheck } from \"./checks.js\";\nimport { UNCHECKED } from \"./unchecked.js\";\nimport { landscapeContext, type LandscapeContext } from \"./landscape.js\";\nimport { landscapeArtifact } from \"./map/owed.js\";\nimport { exampleFor, extendPoint, FQN } from \"./map/extend.js\";\nimport { ARTIFACTS, type BriefTarget } from \"./targets.js\";\nimport type { DocsDir } from \"../kernel/ids/dirs.js\";\nimport { EXTERNAL_TAG } from \"../vocabulary/maturity.js\";\n\n/* ------------------------------------------------------------------ */\n/* The walk — what to read, in what order, and where each finding lands */\n/* ------------------------------------------------------------------ */\n\nexport interface WalkStop {\n /** What to open, named as a thing to go and find rather than a file name. */\n where: string;\n /** What to take out of it. */\n find: string;\n /** The artifacts this stop feeds, by `artifact` name from `targets[]`. */\n lands: string[];\n}\n\n/**\n * The architecture walk: the order a service is read in, and what each stop is\n * for.\n *\n * The brief said \"read the code: entry points, HTTP routes and handlers,\n * published events, config, deploy manifests, tests\" — one sentence, six nouns,\n * no order and no landing site. What came back matched the sentence: the HTTP\n * surface documented well, because it is the noun an agent recognises fastest,\n * and the scheduler, the consumer group and the outbox missing entirely. Not\n * because they were judged unimportant — because nothing said to go and look,\n * and every check loam owns is computed from what was written rather than from\n * what is there. `UNCHECKED` has said COMPLETENESS is unverifiable since the\n * beginning; that is an argument for stating the sweep, not for leaving it\n * implied.\n *\n * The order is deliberate and it is not the order a reader browses in. Stops 1\n * and 2 fix the SHAPE of the service — which processes exist at all — before\n * any surface is enumerated, because an agent that starts at the HTTP routes\n * has already concluded the service is an API by the time it meets the\n * scheduler. Everything after them is one surface each.\n *\n * `lands` names artifacts rather than describing them, because the two lists\n * are joined: test/agent-contract.test.ts requires every name here to be a\n * target the same brief hands over, so a stop can never feed a file nobody was\n * asked to write.\n */\nexport const WALK: WalkStop[] = [\n {\n where: \"the build and dependency manifests — pom.xml, build.gradle, package.json, go.mod, requirements.txt, and whatever the repo's own README calls the build\",\n find: \"what this service is built from, and every client library it links against. A broker client, an HTTP client, a database driver: each one is a dependency that has to appear as an edge or a container later, and this list is the only place they are all named at once. A library whose SEMANTICS decide behaviour — a retry budget, a delivery guarantee, a serialization format — is an arch requirement (arch.spec.md), not a fact to leave in your head.\",\n lands: [\"model.likec4\", \"runbook.md\", \"arch.spec.md\"],\n },\n {\n where: \"the entry points — main/Application/server bootstrap, the DI or module wiring, and anything the deploy manifests actually start\",\n find: \"how many PROCESSES this service is, and what each one is. An API, a worker, a scheduler, a consumer — a service that is three of those and documented as one is the most common way a baseline is wrong, and it is wrong in a shape no later check can see.\",\n lands: [\"model.likec4\"],\n },\n {\n where: \"the HTTP surface — every route, controller, handler and middleware, counted\",\n find: \"the operation set, in full, and inside each operation the DECISIONS its code makes: the permission and role checks — each one is a `Requires:` entry, and a name `architecture/permissions.yaml` does not declare yet is a line to ADD to that shared file, never to invent locally — the fields required only when another field is present, the defaults and normalisations applied before anything is stored, the transitions a request is refused for. Count both — operations, and branches per operation — before you write, and say both counts in the hand-back. `api.ungoverned` can tell you an operation you WROTE has no requirement; nothing at all can tell you about the twelve you never wrote down, or about the thirty branches inside the one you did. A guard is not implementation detail: what a service REFUSES is behaviour its caller observes and a test pins, and it is the half of a legacy service that lives only in the code and in one developer's head.\",\n lands: [\"openapi.yaml\", \"spec.md\"],\n },\n {\n where: \"the message surface — producers, consumers, listener annotations, topic and queue names, the outbox table and its relay\",\n find: \"every message this service puts on and takes off the bus, with its direction, and — on the consuming side — the DECISIONS the handler makes about a message before it acts on one. The direction is the half that gets lost: a consumer documented as a producer is a contract the whole fleet then joins against backwards. The guards are the half nobody looks for at all: which messages are filtered out and on what field, what happens to one whose entity has since been deleted, what is idempotency-keyed against a replay, what is dead-lettered rather than retried. Each of those is a scenario, and none of them appears anywhere in the contract — asyncapi.yaml describes the message, never what this service refuses to do with it.\",\n lands: [\"asyncapi.yaml\", \"landscape.likec4\"],\n },\n {\n where: \"the scheduled and background work — cron entries, timers, retry loops, reconcilers, migrations that run at boot\",\n find: \"behaviour no request ever triggers, and the conditions under which it does or does not run. It has requirements like anything else, and it is invisible to every surface above — which is why a baseline written from routes alone describes a service that does half of what it does. Take the decisions with it: which rows a reconciler SELECTS and which it skips, what a retry loop gives up on and after how long, what a migration does when it finds the state it is meant to fix already fixed. A scheduled job documented as 'runs nightly' is a requirement with no behaviour in it.\",\n lands: [\"spec.md\", \"arch.spec.md\"],\n },\n {\n where: \"the state — datastores, caches, buckets, and the schema or migration directory that says what is in them\",\n find: \"what this service OWNS versus what it borrows. Ownership is the fact the C4 is for, and it is the one nobody can reconstruct later from the code without re-reading all of it.\",\n lands: [\"model.likec4\"],\n },\n {\n where: \"the outbound calls — HTTP clients, generated stubs, gRPC channels, and the config that says which host each one points at\",\n find: \"who this service calls, and which operation each call names. These become the fleet map's edges; an edge missing here is a dependency the fleet cannot see, and `loam explore` will under-report the blast radius of every future change to the callee. An edge that only configuration attests is PROVISIONAL until the runtime stop: hold it until the deploy manifests confirm nothing switches that client off.\",\n lands: [\"landscape.likec4\"],\n },\n {\n where: \"the runtime — config and env vars, deploy manifests, pipelines, alert rules, dashboards\",\n find: \"how it is run and what pages whom. Alerts are the honest source for health.yaml: an SLO nobody agreed to is invention, but an alert that already fires is a fact somebody wrote down. Deployment manifests OVERRIDE configuration files, and they usually live in another repository — go and find them before any dependency that only configuration attests goes in the model, and check for an environment variable that disables the feature. A capability switched off at deploy time is not a dependency, however completely the config wires it: delete its edge and container, and record it in runbook.md's 'configured but not a dependency, and why' list. A manifest read from another repository is named in the runbook's prose — never in `sources`, which may only hold paths of this service's own repository. An EFFECTIVE configuration value behaviour depends on — the timeout the chart sets, the flag that switches a code path — is an arch requirement in arch.spec.md, with a Covers: line naming what it decides; loam holds its coverage even though it can never check the value.\",\n lands: [\"runbook.md\", \"health.yaml\", \"arch.spec.md\"],\n },\n {\n where: \"the tests, integration and component ones first\",\n find: \"the behaviours somebody already thought worth pinning. This is the PRIMARY source for scenarios, not the last sweep: a behaviour a test already pins is written FROM that test, and the code is read only for what no test covers. A test suite is the only part of a legacy service that states intent rather than mechanism, and a scenario derived from one is the only kind that starts out true. Carry the shape across as well as the content — a parameterised test (`@ParameterizedTest`, `@parametrize`, `test.each`) is one scenario with a table, not one scenario per case. Then say in the hand-back how well this service is covered and how many of its tests became scenarios: that number is the ceiling on what this whole baseline can be worth, and nothing downstream can compute it.\",\n lands: [\"spec.md\", \"arch.spec.md\"],\n },\n];\n\n/**\n * The stop that closes the walk. Separate from `WALK` because it is not a place\n * to go — it is the question asked once the walking is done, and the only\n * defence against a sweep that felt thorough because the parts it covered were\n * covered well.\n */\nexport const WALK_CLOSE =\n \"Then list the directories you did NOT open, and say why for each. `loam validate --service <id>`, run inside the service's own repo, reports the same list back as `sources.unwalked` — it compares your `sources` against the files git tracks. A directory you deliberately skipped is a sentence in the hand-back; a directory you skipped without noticing is the gap this walk exists to find.\";\n\n/* ------------------------------------------------------------------ */\n/* The frontmatter */\n/* ------------------------------------------------------------------ */\n\nexport interface FrontmatterBrief {\n /** Field -> what to write in it. */\n fields: Record<string, string>;\n /** Fields to leave alone entirely. */\n never: string[];\n /** Why `sources` carries the weight. */\n why: string;\n}\n\nconst FRONTMATTER_BRIEF: FrontmatterBrief = {\n fields: {\n service: \"the directory this file lives under, exactly. A mismatch is an error (`frontmatter.field-mismatch`).\",\n status:\n \"`draft`, always. `verified` is a human's word, stamped by `loam vouch --service <id>` run inside the service's own repo — never written by hand.\",\n owner: \"the team or person who answers for this service.\",\n sources:\n \"the paths in THIS SERVICE'S repository you actually read to write the document — files and directories only, a directory covering everything beneath it; glob patterns are refused. Not the paths a reader would expect to have been read. This is NOT a reading list for a human: nobody follows these, and nothing about the document asks them to.\",\n },\n // `vouch_scope` is in this list for a sharper reason than its siblings: the\n // others are stamps an agent could only forge, while this one is a stamp an\n // agent could DELETE — and deleting it turns a vouch that read four sections\n // into one that read the document, on every surface at once.\n never: [\"last_verified\", \"sources_digest\", \"content_digest\", \"sources_files\", \"vouch_scope\"],\n why:\n \"`sources` is digest input, not a citation. It is the only mechanical tie between this document and the code — everything else loam checks is internal consistency, and a corpus can agree with itself perfectly while describing nothing that exists. `loam validate`, run inside the service's repo, checks every listed path is still there; `loam vouch` hashes their CONTENT so that a later `validate` can say the code has moved since anyone read it. That hash is the entire point, and it is why padding the list costs something real rather than merely being untidy: a path nobody read still gets hashed, so its next change reports as drift in a document it never described, and a path that WAS read and went unlisted moves under the document in silence. Neither failure is visible to a reader; both are decided by whether this list is honest.\",\n};\n\nexport interface Brief {\n service: string;\n /** Absolute docs repo root — the anchor the repo-relative paths hang off. */\n docsDir: DocsDir;\n /** Repo-relative path of services/<id>/. */\n path: string;\n targets: BriefTarget[];\n /** The order to read the service in, and what each stop feeds. */\n walk: WalkStop[];\n /** The question the walk ends on — see `WALK_CLOSE`. */\n walkClose: string;\n landscape: LandscapeContext;\n frontmatter: FrontmatterBrief;\n checks: BriefCheck[];\n unchecked: string[];\n /** The one instruction that outranks the rest. */\n rule: string;\n}\n\nconst NEVER_OVERWRITE =\n \"Do not overwrite an artifact that already exists. Read it, diff your findings against it, and report what disagrees — a document somebody wrote is evidence, and replacing it destroys the only copy of what they knew.\";\n\n/**\n * Assemble the brief for one service. Reads the docs repo; writes nothing.\n * `at` overrides where the artifacts are briefed to land — `adopt\n * --subsystem`'s target inside a group — and is only ever passed for a\n * service the enumeration does NOT answer: an existing service is briefed\n * where it already lives, because the brief's one rule is to never overwrite,\n * and a second address would brief a duplicate.\n */\nexport async function serviceBrief(\n docsDir: DocsDir,\n service: PathableService,\n at?: ServicePaths,\n): Promise<Brief> {\n const paths = at ?? (await locateServicePaths(docsDir, service));\n const rel = (abs: string): string => relative(docsDir, abs).split(/[\\\\/]/).join(\"/\");\n\n // Read before the artifact loop, not after it: what the fleet already says\n // about this service decides whether it owes an API contract at all. The\n // model path travels with it — located through the enumeration above, never\n // joined at `services/<id>/` — so the edgeless state can name the calls the\n // service's own model attests.\n const landscape = await landscapeContext(docsDir, service, paths);\n const owesApi = apiExpected(landscape);\n\n // The one id the model target's grammar and example are written against,\n // resolved here because `ARTIFACTS` is static on purpose (a target list that\n // varied per repository would be a second thing to keep true) and because this\n // is the only place the fleet map has already been read.\n const extend = extendPoint(landscape);\n\n const targets: BriefTarget[] = [];\n for (const { key, ...artifact } of ARTIFACTS) {\n // `adrs/` is a directory, and an empty one is not an existing artifact —\n // reporting it as present would tell the agent to diff against nothing.\n const dir = key === \"adrsDir\";\n const exists = dir ? await hasMarkdown(paths[key]) : existsSync(paths[key]);\n targets.push({\n ...artifact,\n ...(key === \"openapi\" ? { required: owesApi } : {}),\n // Every `<fqn>` in the model target, replaced by the element this service\n // actually extends. The placeholder is left literal in `targets.ts` — the\n // brief is handed to an agent that will WRITE the file, and\n // `extend <fqn> {` copied verbatim is a document that does not parse.\n shape: artifact.shape.map((rule) => rule.replaceAll(FQN, extend.id)),\n ...(artifact.example === undefined ? {} : { example: exampleFor(artifact.example, extend, service) }),\n path: dir ? `${rel(paths[key])}/` : rel(paths[key]),\n exists,\n action: exists ? \"diff\" : \"create\",\n });\n }\n\n // The eighth target is the fleet map, and it is conditional on TWO facts: an\n // element resolving to the service, and an edge touching it. A service the\n // landscape already draws AND reaches owes it nothing, and briefing an edit\n // there would invite an agent to draw a second box for a service that has\n // one. A service with a box and no edge — what `loam seed` leaves for every\n // service nobody listed under `calls:` — owes it the edges, and until\n // 2026-09-03 element existence alone read as \"the map is complete\" here, so\n // the brief declared the map finished for exactly the services it had not\n // reached. Everything else about the target is unconditional — the file is\n // shared, so `action` is `edit` whenever it exists and `create` only for the\n // very first service of a brand-new docs repo.\n //\n // `modelled === false` and not `!== true`: `null` is \"loam could not read the\n // map\", and it fell through the same branch as \"the map genuinely draws\n // nothing for this service\". The brief then handed over the UNDRAWN target —\n // whose shape rule is \"add one top-level element bound to its directory\" and\n // whose example is a block to splice — for services the map already declares\n // and binds, and an agent that spliced it earned `landscape.binding-duplicate`\n // for doing exactly what the brief asked. With the map unreadable there is no\n // write to ask for; `landscape.instruction` carries the whole answer and names\n // the documents that failed.\n if (landscape.modelled === false || landscape.touched === false) {\n targets.push({\n ...landscapeArtifact({\n service,\n expects: landscape.expects,\n present: landscape.present,\n servicePath: rel(paths.dir),\n // The existing element's id, in the edgeless state only: it switches\n // the example from a block to edges on that element.\n // …and whether it is `#external`, which decides what the shape rule may\n // promise: `landscape.service-isolated` never fires on such an element.\n ...(landscape.touched === false && landscape.elements[0] !== undefined\n ? { elementId: landscape.elements[0].id, external: landscape.elements[0].tags.includes(EXTERNAL_TAG) }\n : {}),\n attested: landscape.attested,\n }),\n path: rel(landscapePath(docsDir)),\n exists: landscape.present,\n action: landscape.present ? \"edit\" : \"create\",\n });\n }\n\n return {\n service,\n docsDir,\n path: rel(paths.dir),\n targets,\n walk: WALK,\n walkClose: WALK_CLOSE,\n landscape,\n frontmatter: FRONTMATTER_BRIEF,\n checks: VALIDATE_CHECKS,\n unchecked: UNCHECKED,\n rule: NEVER_OVERWRITE,\n };\n}\n\nasync function hasMarkdown(dir: string): Promise<boolean> {\n if (!existsSync(dir)) return false;\n const entries = await readdir(dir, { withFileTypes: true });\n // `statSync` for the symlink case: a Dirent never follows one, so `isFile()`\n // is false for a symlinked ADR and the directory reads as empty (repo.ts's\n // `entryIs` is the same rule for `services/` and `features/`).\n return entries.some((e) => {\n if (!e.name.endsWith(\".md\")) return false;\n if (e.isFile()) return true;\n if (!e.isSymbolicLink()) return false;\n try {\n return statSync(join(dir, e.name)).isFile();\n } catch {\n return false;\n }\n });\n}\n\n/**\n * Does the fleet expect an API of this service?\n *\n * The brief marked `openapi.yaml` required unconditionally, and printed the\n * absence as `MISSING` in capitals — while `loam validate` and `loam list`\n * derive the opposite from the same landscape (`service.no-openapi` stays quiet\n * when nothing calls the service, and `list`'s maturity ladder stops demanding\n * a contract). For a UI, a worker or a cron the brief was therefore telling an\n * agent to INVENT an API contract into the source of truth — the one class of\n * guess this whole module exists to refuse, and the one the brief itself names\n * in `openapi.yaml`'s shape rules (`ARTIFACTS`, targets.ts).\n *\n * Positive evidence only, exactly as `list` reads it: a landscape that is\n * absent or unreadable proves nothing about who calls this service, so the\n * contract is still owed. `expects` is the ops on inbound edges, which is\n * `list`'s `called` set seen from one service.\n *\n * `callersKnown` and not `parses`, because since W4 `parses` answers about the\n * whole `architecture/` PROJECT: a broken palette or use case beside a landscape\n * loam reads perfectly made this discard the map and demand an OpenAPI contract\n * of a browser UI — inventing an API into the source of truth, which is the one\n * class of guess this module exists to refuse. `callersKnown` is the narrower\n * fact this question actually needs, and it is the one `list` and `context` read.\n */\nfunction apiExpected(landscape: LandscapeContext): boolean {\n return !landscape.callersKnown || landscape.expects.length > 0;\n}\n"]}
@@ -17,7 +17,35 @@ export const VIA_ALL = "loam validate --all";
17
17
  /** What `loam validate` will run against the result — each check names the invocation that surfaces it. */
18
18
  export const VALIDATE_CHECKS = [
19
19
  { code: "service.no-model", severity: "error", via: VIA_SERVICE, what: "there is no model.likec4 — every other check stops here" },
20
- { code: "c4.invalid", severity: "error", via: VIA_SERVICE, what: "model.likec4 does not parse as LikeC4" },
20
+ {
21
+ code: "c4.invalid",
22
+ severity: "error",
23
+ via: VIA_SERVICE,
24
+ what: "model.likec4 does not parse as LikeC4 — for a model that extends the fleet map, parsed together with `architecture/landscape.likec4`, and every error is this model's, including one the parser blames on the map (the map parses clean on its own)",
25
+ },
26
+ // The two shapes' own grades. A model that extends the map cannot diverge from
27
+ // it — there is one declaration — and a model that stands alone cannot own an
28
+ // element outside itself, because nothing else is in its project. So exactly
29
+ // one of these two can ever fire for a given model, and which one is decided
30
+ // by the file's own grammar (SCHEMA.md, "Two shapes of a service model").
31
+ {
32
+ code: "c4.declaration-diverged",
33
+ severity: "warn",
34
+ via: VIA_SERVICE,
35
+ // "the fleet map (architecture/)", never one file: the map is a project, an
36
+ // element may be declared in any document of it, and the finding's own
37
+ // message and `loam explain` both refuse to name a file for that reason.
38
+ // Naming `architecture/landscape.likec4` here sent a reader of a fleet whose
39
+ // map is spread over several documents to a file that may not declare the
40
+ // element at all (verification 2026-09-04).
41
+ what: "a model that declares its own `specification` (the standalone shape) declares an element the fleet map (`architecture/`) also declares, and the two copies disagree about `kind`, `title`, the tag SET or the `metadata { service }` binding — two documents are two authorities on one element. `description` is deliberately not compared. Copy the map's declaration verbatim, or migrate the model to extend the map",
42
+ },
43
+ {
44
+ code: "c4.element-unowned",
45
+ severity: "warn",
46
+ via: VIA_SERVICE,
47
+ what: "a model that extends the fleet map declares an element OUTSIDE the element that resolves to this service — a system this service reaches belongs in `architecture/landscape.likec4`, declared once, and another service's internals belong in that service's model",
48
+ },
21
49
  {
22
50
  code: "requirements.missing-scenarios",
23
51
  severity: "error",
@@ -216,11 +244,60 @@ export const VALIDATE_CHECKS = [
216
244
  // The gherkin.* staleness chain is absent the same way: it fires only once
217
245
  // `loam gherkin` has generated a suite under <gherkinDir>/loam/ in the
218
246
  // service's own repo, and a fresh baseline has not generated one.
247
+ //
248
+ // The service's OWN use cases: a `dynamic view` in any `.likec4` beside
249
+ // model.likec4 (or inside it) that opts in with `#req-`. The same codes the
250
+ // fleet target grades over architecture/usecases/, reused on the service
251
+ // target because the flow's hops name this service's containers, which only
252
+ // its own project can resolve. `usecase.step-unlinked` was at first left
253
+ // out, on the reasoning that a hop between two containers of one service
254
+ // owes no operationId — true, and exactly the scope of the guard that keeps
255
+ // it quiet there (a service owes none to itself); but a service's model
256
+ // routinely declares a stand-in for a sibling service it calls, and a hop
257
+ // from a service-local flow into that stand-in with no `metadata { op }`
258
+ // warns exactly as it does on the fleet map. The row is listed so the
259
+ // agent's own baseline run never reports a code the brief left unexplained.
260
+ {
261
+ code: "usecase.step-unbacked",
262
+ severity: "error",
263
+ via: VIA_SERVICE,
264
+ what: "a hop of a `#req-`-tagged dynamic view in this service's project that no relationship in model.likec4 backs — LikeC4 reports nothing for it, so only this does",
265
+ },
266
+ {
267
+ code: "usecase.step-contested",
268
+ severity: "warn",
269
+ via: VIA_SERVICE,
270
+ what: "two or more relationships back one hop of a service-local flow and name different operations — loam names the candidates rather than picking one",
271
+ },
272
+ {
273
+ code: "usecase.step-unlinked",
274
+ severity: "warn",
275
+ via: VIA_SERVICE,
276
+ what: "a hop of a service-local flow into ANOTHER service's element — a stand-in this model.likec4 declares for a sibling, bound or titled as that service — backed by an edge carrying no `metadata { op }` and no `publishes`/`consumes`; never on a hop whose caller and provider resolve to the same service, because a service owes no operationId to itself",
277
+ },
278
+ {
279
+ code: "usecase.requirement-unresolved",
280
+ severity: "error",
281
+ via: VIA_SERVICE,
282
+ what: "a `#req-` tag on a service-local flow naming no `Requirement-ID` of this service's spec.md or arch.spec.md — the id is scoped by the directory, never by a capability",
283
+ },
284
+ {
285
+ code: "usecase.capability-unresolved",
286
+ severity: "error",
287
+ via: VIA_SERVICE,
288
+ what: "a `#cap-` tag on a flow inside this service's own project — a capability is claimed on the fleet map, never inside one service; drop the tag and keep the `#req-` one",
289
+ },
290
+ {
291
+ code: "usecase.flow-invalid",
292
+ severity: "error",
293
+ via: VIA_SERVICE,
294
+ what: "a `.likec4` beside model.likec4 that does not parse as part of the service's LikeC4 project — no flow in it was graded; model.likec4 itself is still graded alone",
295
+ },
219
296
  {
220
297
  code: "landscape.service-unmodelled",
221
298
  severity: "error",
222
299
  via: VIA_ALL,
223
- what: "nothing in architecture/landscape.likec4 resolves to services/<id>/ — the fleet map is incomplete until an element exists or an existing one is bound with `metadata { service '<id>' }`",
300
+ what: "nothing in architecture/landscape.likec4 resolves to services/<id>/ — the fleet map is incomplete until an element exists or an existing one is bound with `metadata { service '<id>' }`. On the FLEET target for any service; on the SERVICE target too when that service's model.likec4 EXTENDS the map, where an unbound directory leaves the model with nothing to be inside and nothing in it can be graded",
224
301
  },
225
302
  {
226
303
  code: "landscape.missing",
@@ -232,7 +309,11 @@ export const VALIDATE_CHECKS = [
232
309
  code: "landscape.invalid",
233
310
  severity: "error",
234
311
  via: VIA_ALL,
235
- what: "architecture/landscape.likec4 exists but does not parse — fix it before anything else; the whole cross-service layer is unchecked until it reads",
312
+ // The fleet map is the whole `architecture/` PROJECT, so this code fires for
313
+ // a palette or a use case beside the landscape just as readily as for the
314
+ // landscape itself — and the message names the document that broke. Saying
315
+ // "landscape.likec4" here sent an author to a file with no errors in it.
316
+ what: "the `architecture/` project does not parse — the landscape, or any `.likec4` beside it; the message names the document that broke. Fix it before anything else: the whole cross-service layer is unchecked until the map reads",
236
317
  },
237
318
  {
238
319
  code: "landscape.binding-unknown",
@@ -246,14 +327,59 @@ export const VALIDATE_CHECKS = [
246
327
  via: VIA_ALL,
247
328
  what: "two landscape elements resolve to the same services/<id>/ — every element→service join picks one of them arbitrarily, so the other's edges are filed under a service that does not own them",
248
329
  },
249
- // Fires on a fresh baseline by construction: the adopt protocol's step 3
250
- // writes the model and `subsystem sync` writes the project file, so a
251
- // service is owed the second command from the moment the first file lands.
330
+ // Evidence-gated: the model's own cross-boundary calls are the proof that
331
+ // the map owes an edge, so a service whose model declares none stays silent
332
+ // — that state is the adopt brief's to name (`landscape.touched`), never a
333
+ // fleet finding that cannot tell a hermit from a batch job. And BINARY: one
334
+ // edge closes it, because loam cannot tell a call the map forgot from one the
335
+ // fleet deliberately does not draw at its own altitude, and a set difference
336
+ // reported as an omission is an invented edge with a code beside it.
337
+ { code: "landscape.service-isolated", severity: "warn", via: VIA_ALL, what: "an element resolves to services/<id>/ and no edge anywhere in architecture/ touches it, while model.likec4 declares a call across its boundary — the service is drawn and invisible to every cross-service check; silent when the model declares no such call, and silent once ONE edge is drawn: the check is touched/untouched, not a set difference over the attested calls" },
338
+ // The four renderer-wiring grades, all repaired by one `loam subsystem sync`.
339
+ // None of them fires on a baseline written from this brief: the model it asks
340
+ // for extends the map, so it belongs to the root project and is owed no file
341
+ // and no exclusion. They are listed because an agent adopting into an OLDER
342
+ // repository — one whose root `likec4.config.json` still excludes
343
+ // `services/**`, or that carries a hand-written project file — meets them on
344
+ // the first `--all` run and must not read one as work on the model.
252
345
  {
253
346
  code: "service.likec4-config-missing",
254
347
  severity: "warn",
255
348
  via: VIA_ALL,
256
- what: "model.likec4 exists and services/<id>/likec4.config.json does not — the renderer's per-service project file, which `loam subsystem sync` writes; without it the model is a box on the fleet map with nothing inside it",
349
+ what: "a model that stands alone (declares its own `specification`) renders only as a project of its own, and services/<id>/likec4.config.json is not there — `loam subsystem sync` writes it; without it the model is a box on the fleet map with nothing renderable inside it. Never raised for a model that extends the map",
350
+ },
351
+ {
352
+ code: "service.likec4-config-stray",
353
+ severity: "warn",
354
+ via: VIA_ALL,
355
+ what: "the reverse: model.likec4 extends the fleet map and a likec4.config.json sits beside it — the renderer registers that file as a project of its own, holding nothing, or holding the model alone where it cannot parse. Delete it; loam never writes one beside an extending model",
356
+ },
357
+ {
358
+ code: "service.model-excluded",
359
+ severity: "warn",
360
+ via: VIA_ALL,
361
+ what: "model.likec4 extends the fleet map and the root likec4.config.json's `exclude` hides it, so the renderer never loads it. Two entry shapes reach it: one covering the DIRECTORY, which `loam subsystem sync` rewrites (that command maintains the `services/` entries), and one shaped like a FILE (`services/**/model.likec4`), which sync cannot repair — the message says which one you have, and the file shape has to be removed or narrowed by hand",
362
+ },
363
+ {
364
+ code: "service.model-unexcluded",
365
+ severity: "warn",
366
+ via: VIA_ALL,
367
+ what: "the mirror, and the damaging one: a model that stands alone whose directory the root `exclude` does NOT cover — the renderer merges it into the map, reports every kind and element it declares as a duplicate, and blanks the whole root project, not just this service. Same repair",
368
+ },
369
+ // The same list, one level up: the entry covers the MAP. It is not a
370
+ // per-service wiring grade and no `sync` repairs it — sync recomputes the
371
+ // `services/` entries only — so it is listed separately from the four above.
372
+ {
373
+ code: "landscape.excluded",
374
+ severity: "warn",
375
+ via: VIA_ALL,
376
+ what: "the root likec4.config.json's `exclude` covers architecture/landscape.likec4 itself — the renderer loads no fleet map at all, and every extending model in the root project resolves against nothing, while loam keeps grading the map it reads from disk. `c4.fleet-project-invalid` is not graded while the entry stands, because that check reads the renderer's project and would report this one line once per model. The finding quotes the entry verbatim; remove or narrow it by hand, because `loam subsystem sync` maintains only the `services/` entries in that list",
377
+ },
378
+ {
379
+ code: "c4.fleet-project-invalid",
380
+ severity: "warn",
381
+ via: VIA_ALL,
382
+ what: "every document reads clean where loam grades it, and the ONE project the renderer builds out of them — the map, every extending model, and every `.likec4` beside one — does not parse. A tag or an element declared in two of those documents is declared twice there; declare it once, in the map or in the single service that owns it. Not graded at all while `landscape.excluded` holds: with the map out of that project every `extend` is unresolvable, so the entry is the finding",
257
383
  },
258
384
  ];
259
385
  //# sourceMappingURL=checks.js.map