@ybotok/loam 0.0.0 → 0.1.0-beta.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (126) hide show
  1. package/CHANGELOG.md +138 -0
  2. package/COMPARISON.md +63 -0
  3. package/LICENSE +21 -0
  4. package/MIGRATING-from-OpenSpec.md +202 -0
  5. package/README.md +246 -9
  6. package/SCHEMA.md +425 -0
  7. package/SECURITY.md +9 -0
  8. package/dist/cli.js +142 -0
  9. package/dist/cli.js.map +1 -0
  10. package/dist/commands/adopt.js +240 -0
  11. package/dist/commands/adopt.js.map +1 -0
  12. package/dist/commands/archive.js +1313 -0
  13. package/dist/commands/archive.js.map +1 -0
  14. package/dist/commands/delta.js +388 -0
  15. package/dist/commands/delta.js.map +1 -0
  16. package/dist/commands/dependencies.js +83 -0
  17. package/dist/commands/dependencies.js.map +1 -0
  18. package/dist/commands/docs-repo-gate.js +104 -0
  19. package/dist/commands/docs-repo-gate.js.map +1 -0
  20. package/dist/commands/doctor.js +85 -0
  21. package/dist/commands/doctor.js.map +1 -0
  22. package/dist/commands/explore.js +195 -0
  23. package/dist/commands/explore.js.map +1 -0
  24. package/dist/commands/format.js +17 -0
  25. package/dist/commands/format.js.map +1 -0
  26. package/dist/commands/gherkin.js +309 -0
  27. package/dist/commands/gherkin.js.map +1 -0
  28. package/dist/commands/init.js +250 -0
  29. package/dist/commands/init.js.map +1 -0
  30. package/dist/commands/instructions.js +70 -0
  31. package/dist/commands/instructions.js.map +1 -0
  32. package/dist/commands/list.js +291 -0
  33. package/dist/commands/list.js.map +1 -0
  34. package/dist/commands/migrate-openspec.js +950 -0
  35. package/dist/commands/migrate-openspec.js.map +1 -0
  36. package/dist/commands/new.js +413 -0
  37. package/dist/commands/new.js.map +1 -0
  38. package/dist/commands/rebase.js +356 -0
  39. package/dist/commands/rebase.js.map +1 -0
  40. package/dist/commands/show.js +320 -0
  41. package/dist/commands/show.js.map +1 -0
  42. package/dist/commands/status.js +254 -0
  43. package/dist/commands/status.js.map +1 -0
  44. package/dist/commands/unarchive.js +317 -0
  45. package/dist/commands/unarchive.js.map +1 -0
  46. package/dist/commands/validate.js +1591 -0
  47. package/dist/commands/validate.js.map +1 -0
  48. package/dist/commands/verify.js +775 -0
  49. package/dist/commands/verify.js.map +1 -0
  50. package/dist/commands/vouch.js +343 -0
  51. package/dist/commands/vouch.js.map +1 -0
  52. package/dist/core/agent.js +2065 -0
  53. package/dist/core/agent.js.map +1 -0
  54. package/dist/core/agents-stamp.js +68 -0
  55. package/dist/core/agents-stamp.js.map +1 -0
  56. package/dist/core/arch.js +130 -0
  57. package/dist/core/arch.js.map +1 -0
  58. package/dist/core/brief.js +733 -0
  59. package/dist/core/brief.js.map +1 -0
  60. package/dist/core/coherence.js +577 -0
  61. package/dist/core/coherence.js.map +1 -0
  62. package/dist/core/concurrency.js +51 -0
  63. package/dist/core/concurrency.js.map +1 -0
  64. package/dist/core/config.js +180 -0
  65. package/dist/core/config.js.map +1 -0
  66. package/dist/core/delta.js +476 -0
  67. package/dist/core/delta.js.map +1 -0
  68. package/dist/core/dependencies.js +356 -0
  69. package/dist/core/dependencies.js.map +1 -0
  70. package/dist/core/docs.js +144 -0
  71. package/dist/core/docs.js.map +1 -0
  72. package/dist/core/doctor.js +559 -0
  73. package/dist/core/doctor.js.map +1 -0
  74. package/dist/core/document-bytes.js +59 -0
  75. package/dist/core/document-bytes.js.map +1 -0
  76. package/dist/core/explore.js +225 -0
  77. package/dist/core/explore.js.map +1 -0
  78. package/dist/core/fleet-context.js +225 -0
  79. package/dist/core/fleet-context.js.map +1 -0
  80. package/dist/core/frontmatter.js +199 -0
  81. package/dist/core/frontmatter.js.map +1 -0
  82. package/dist/core/gherkin.js +463 -0
  83. package/dist/core/gherkin.js.map +1 -0
  84. package/dist/core/health.js +67 -0
  85. package/dist/core/health.js.map +1 -0
  86. package/dist/core/ids.js +122 -0
  87. package/dist/core/ids.js.map +1 -0
  88. package/dist/core/issue.js +9 -0
  89. package/dist/core/issue.js.map +1 -0
  90. package/dist/core/json.js +74 -0
  91. package/dist/core/json.js.map +1 -0
  92. package/dist/core/likec4.js +479 -0
  93. package/dist/core/likec4.js.map +1 -0
  94. package/dist/core/maturity.js +66 -0
  95. package/dist/core/maturity.js.map +1 -0
  96. package/dist/core/openapi-merge.js +552 -0
  97. package/dist/core/openapi-merge.js.map +1 -0
  98. package/dist/core/openapi.js +255 -0
  99. package/dist/core/openapi.js.map +1 -0
  100. package/dist/core/openspec-inventory.js +1440 -0
  101. package/dist/core/openspec-inventory.js.map +1 -0
  102. package/dist/core/path-safety.js +168 -0
  103. package/dist/core/path-safety.js.map +1 -0
  104. package/dist/core/provenance.js +720 -0
  105. package/dist/core/provenance.js.map +1 -0
  106. package/dist/core/records.js +33 -0
  107. package/dist/core/records.js.map +1 -0
  108. package/dist/core/repo.js +436 -0
  109. package/dist/core/repo.js.map +1 -0
  110. package/dist/core/report.js +47 -0
  111. package/dist/core/report.js.map +1 -0
  112. package/dist/core/results.js +199 -0
  113. package/dist/core/results.js.map +1 -0
  114. package/dist/core/spec.js +481 -0
  115. package/dist/core/spec.js.map +1 -0
  116. package/dist/core/staging.js +890 -0
  117. package/dist/core/staging.js.map +1 -0
  118. package/dist/core/status.js +1104 -0
  119. package/dist/core/status.js.map +1 -0
  120. package/dist/core/steps.js +72 -0
  121. package/dist/core/steps.js.map +1 -0
  122. package/dist/core/verify.js +807 -0
  123. package/dist/core/verify.js.map +1 -0
  124. package/dist/core/version.js +24 -0
  125. package/dist/core/version.js.map +1 -0
  126. package/package.json +63 -4
@@ -0,0 +1,733 @@
1
+ /**
2
+ * The adoption brief: what an agent needs in order to write one service's
3
+ * baseline documentation, and what will be checked when it does.
4
+ *
5
+ * `adopt` was specified for years as "read the code, emit the C4". It is not
6
+ * being built, because it cannot be: nothing deterministic reads a legacy
7
+ * service and says what its architecture MEANS, and a guessed model is worse
8
+ * than no model — everyone downstream has to re-derive it to know whether to
9
+ * believe it.
10
+ *
11
+ * So loam takes the half of the job that IS deterministic. It states the work
12
+ * precisely (which files, in which grammar, bound to which existing elements),
13
+ * it says exactly which checks the result will face, and then it says which
14
+ * checks do not exist — because everything in that last list is honesty no
15
+ * validator will ever supply, and an agent that does not know where the checking
16
+ * stops will assume it never does.
17
+ *
18
+ * Nothing here reads code. `sources` in the frontmatter is the only line that
19
+ * ties the result to a repository, which is why the brief argues for it instead
20
+ * of merely listing it.
21
+ */
22
+ import { existsSync, statSync } from "node:fs";
23
+ import { readdir } from "node:fs/promises";
24
+ import { join, relative } from "node:path";
25
+ import { elementService, loadFile, serviceResolver } from "./likec4.js";
26
+ import { landscapePath, listServices, servicePaths } from "./repo.js";
27
+ /**
28
+ * The LikeC4 identifier the fleet map's element for a service conventionally
29
+ * carries: the directory name, camel-cased. It is a suggestion and nothing
30
+ * more — `metadata { service '<id>' }` is what actually joins the element to
31
+ * the directory — but a brief that hands over a concrete block gets followed,
32
+ * and one that describes a block in prose gets improvised on.
33
+ */
34
+ function elementIdFor(service) {
35
+ const parts = service.split(/[^A-Za-z0-9]+/).filter((p) => p !== "");
36
+ if (parts.length === 0)
37
+ return "service";
38
+ const head = parts[0].toLowerCase();
39
+ const tail = parts.slice(1).map((p) => p[0].toUpperCase() + p.slice(1).toLowerCase());
40
+ const id = head + tail.join("");
41
+ // LikeC4 identifiers cannot open with a digit; a service id can.
42
+ return /^[0-9]/.test(id) ? `svc${id[0].toUpperCase()}${id.slice(1)}` : id;
43
+ }
44
+ /**
45
+ * The block this service still owes `architecture/landscape.likec4`.
46
+ *
47
+ * `expects` is what the fleet ALREADY calls on the service — those edges exist
48
+ * in the map today, so the example draws the outbound side as a comment rather
49
+ * than telling an agent to duplicate an edge that is already there.
50
+ */
51
+ function landscapeBlock(service, expects, present) {
52
+ const id = elementIdFor(service);
53
+ const calls = expects.length > 0
54
+ ? expects.map((op) => `// <caller> -> ${id} 'Calls ${op}' { metadata { op '${op}' } }`)
55
+ : [`// <caller> -> ${id} 'Calls <operationId>' { metadata { op '<operationId>' } }`];
56
+ const model = [
57
+ "model {",
58
+ ...(present ? [" // ... the fleet's other services stay exactly as they are ...", ""] : []),
59
+ ` ${id} = softwareSystem '${service}' {`,
60
+ " description '<one line: what this service is responsible for>'",
61
+ ` metadata { service '${service}' }`,
62
+ " }",
63
+ "",
64
+ " // every call in or out of it, one edge each:",
65
+ ...calls.map((line) => ` ${line}`),
66
+ "}",
67
+ ];
68
+ // When the file does not exist the example is the WHOLE file, so it carries
69
+ // the `specification` block a bare `model` would be rejected without: the
70
+ // brief must never hand over a document `loam validate` refuses. When the file
71
+ // does exist the example is a fragment to splice, and repeating a
72
+ // `specification` block there would invite a second one.
73
+ return present
74
+ ? model.join("\n")
75
+ : ["specification {", " element softwareSystem", "}", "", ...model].join("\n");
76
+ }
77
+ /** The fleet map's own brief — assembled per service, because the block it owes names the service. */
78
+ function landscapeArtifact(service, expects, present) {
79
+ return {
80
+ artifact: "landscape.likec4",
81
+ required: true,
82
+ purpose: "the fleet map — until an element here resolves to this directory, the service is documented and invisible to every cross-service check",
83
+ shape: [
84
+ "This file is the WHOLE FLEET's, not this service's. Add to it; never rewrite it. Everything already in `model { ... }` belongs to the other services, and replacing the file destroys their map along with their edges.",
85
+ `Add one top-level element for the service, bound to its directory: \`metadata { service '${service}' }\`. Until an element resolves to \`services/${service}/\`, \`loam validate --all\` reports \`landscape.service-unmodelled\` (error).`,
86
+ "Bind rather than rename: a binding whose id names no directory is `landscape.binding-unknown` (error), and two elements binding the SAME directory is `landscape.binding-duplicate` (warn) — every element→service join then picks one of them arbitrarily.",
87
+ "Draw every cross-service call as an edge carrying the operation it uses: `a -> b 'Calls createSplit' { metadata { op 'createSplit' } }`. The `op` must be an operationId the TARGET's openapi.yaml defines, or `spine.op-undefined` (error) — a broken contract between services.",
88
+ "If the file does not exist yet, create it with a `specification { ... }` block declaring the kinds you use, then the `model { ... }` block. A landscape that does not parse is `landscape.invalid` (error) and blinds every cross-service check at once.",
89
+ ],
90
+ example: landscapeBlock(service, expects, present),
91
+ };
92
+ }
93
+ /** The artifacts, in the order a baseline is best written in. */
94
+ const ARTIFACTS = [
95
+ {
96
+ artifact: "model.likec4",
97
+ key: "model",
98
+ required: true,
99
+ purpose: "the service's C4 — what it is, what is inside it, what it talks to",
100
+ // Every rule below is one a later check depends on, and the mapping is
101
+ // pinned in test/agent-contract.test.ts. Rules nobody checks — the `views`
102
+ // block, "exactly one top-level element" — used to sit here reading exactly
103
+ // like the enforced ones, which is how a brief teaches an agent that
104
+ // `loam validate` passing means more than it does. They now live in
105
+ // UNCHECKED, where their status is the point.
106
+ shape: [
107
+ "A `specification { ... }` block declaring every element kind you use (`element softwareSystem`, `element container`). LikeC4 rejects an undeclared kind, and `loam validate --service <id>` reports it as `c4.invalid` with line numbers.",
108
+ "A `model { ... }` block holding the service's element, with containers and components nested inside it. The whole file has to parse — `loam validate` runs LikeC4 in-process and reports `c4.invalid` with line numbers.",
109
+ "The service element binds to this directory: `metadata { service '<id>' }`. Without a binding the element's TITLE has to equal the directory name — and then renaming the box silently unlinks every check that joined the two. The same binding is what makes the FLEET map resolve an element to `services/<id>/`; until one does, `loam validate --all` reports `landscape.service-unmodelled`.",
110
+ "A call to another service is an edge that names the operation it uses: `a -> b 'Calls createSplit' { metadata { op 'createSplit' } }`. Cross-service edges belong in `architecture/landscape.likec4` (a target of its own, below) — this file cannot even resolve the other service's element — and there the `op` must be an operationId the TARGET's openapi.yaml defines, or `spine.op-undefined` fires.",
111
+ ],
112
+ example: `specification {
113
+ element softwareSystem
114
+ element container
115
+ }
116
+
117
+ model {
118
+ paymentService = softwareSystem 'payment-service' {
119
+ description 'Owns payment authorization and capture'
120
+ metadata { service 'payment-service' }
121
+
122
+ api = container 'HTTP API'
123
+ ledger = container 'Ledger store'
124
+ }
125
+ }
126
+
127
+ // Views are LikeC4's, not loam's: loam parses this file and computes no view,
128
+ // so nothing below is read by any check. Keep it for 'npx likec4 start', which
129
+ // does render it. Scoped to one service 'include *' is cheap; the same line in
130
+ // architecture/landscape.likec4 walks every call in the fleet and takes minutes.
131
+ views {
132
+ view of paymentService {
133
+ include *
134
+ }
135
+ }
136
+ `,
137
+ },
138
+ {
139
+ artifact: "spec.md",
140
+ key: "spec",
141
+ required: true,
142
+ purpose: "the living requirements — what this service is required to do, as it stands today",
143
+ shape: [
144
+ "Frontmatter (below), then a `## Requirements` heading.",
145
+ "`### Requirement: <name>` — ONE observable behaviour, written with SHALL, testable without reading the code. Add `Requirement-ID: <id>` in its body for stable identity across heading renames; legacy requirements without one continue to match by exact name.",
146
+ "At least one `#### Scenario: <name>` per requirement, with Given/When/Then bullets. This is gated (`requirements.missing-scenarios`) — scenarios are the acceptance criteria and the source for tests.",
147
+ "`Operations: <operationId>[, <operationId>]` in the requirement body names the API operations it governs. Every one must exist in this service's openapi.yaml — a name that does not is `spec-api.op-undefined` (error), reported against the LIVING spec, not only inside a feature delta.",
148
+ "Document what the service DOES today, not what it should do. This is the baseline; changes to it belong in a feature delta.",
149
+ ],
150
+ example: `---
151
+ service: payment-service
152
+ status: draft
153
+ owner: payments-team
154
+ sources:
155
+ - src/main/java/com/shop/payment/
156
+ ---
157
+
158
+ # payment-service
159
+
160
+ ## Requirements
161
+
162
+ ### Requirement: Authorize a payment
163
+ The service SHALL authorize a payment against the issuer before it is captured.
164
+
165
+ Operations: authorizePayment
166
+
167
+ #### Scenario: Successful authorization
168
+ - **Given** a valid card and an amount within its limit
169
+ - **When** authorization is requested
170
+ - **Then** the payment is authorized and an authorization id is returned
171
+ `,
172
+ },
173
+ {
174
+ artifact: "arch.spec.md",
175
+ key: "archSpec",
176
+ required: false,
177
+ purpose: "the living architecture requirements — the obligations the business spec never carries: outbox, retries, idempotency, metrics, alerts",
178
+ shape: [
179
+ "Same grammar and frontmatter as spec.md: `## Requirements`, `### Requirement:` + `#### Scenario:` with Given/When/Then. The same checks read it.",
180
+ "A `Covers:` line per requirement names what its scenarios exercise: a C4 element id (`paymentService.db`), an edge (`paymentService -> kafka`), or a health signal from health.yaml (`alert:<id>` / `sli:<id>`). Every entry must resolve, or `covers.unknown` (warn) flags the typo.",
181
+ "Optional, but expected for a service with real architecture: every alert/SLI health.yaml declares wants a covering requirement here (`health.uncovered`, warn) — a signal nothing tests is dashboard decoration.",
182
+ "Write the obligations no business requirement states — the transactional outbox, what a caller's retry may assume, what pages whom. An arch scenario becomes an integration/ops test, not an acceptance test.",
183
+ ],
184
+ example: `---
185
+ service: payment-service
186
+ status: draft
187
+ owner: payments-team
188
+ sources:
189
+ - src/main/java/com/shop/payment/
190
+ ---
191
+
192
+ # payment-service — architecture
193
+
194
+ ## Requirements
195
+
196
+ ### Requirement: Events leave through the transactional outbox
197
+ The service SHALL write a domain event and its state change in one transaction,
198
+ published by an outbox relay — never a dual write.
199
+
200
+ Covers: paymentService.db, paymentService -> kafka
201
+
202
+ #### Scenario: Broker down at commit time
203
+ - **Given** an event in the outbox
204
+ - **When** the broker is unavailable
205
+ - **Then** the state stays committed and the event is published once it returns
206
+ `,
207
+ },
208
+ {
209
+ artifact: "openapi.yaml",
210
+ key: "openapi",
211
+ // Overridden per service in `serviceBrief`: an API is required only where
212
+ // the fleet expects one. See `apiExpected`.
213
+ required: true,
214
+ purpose: "the API contract — the operations this service exposes",
215
+ shape: [
216
+ "OpenAPI 3.x with a `paths` map.",
217
+ "EVERY operation carries an `operationId`. That token is the spine: the C4 edge's `metadata { op }`, the requirement's `Operations:` line and this `operationId` are one name spelled three times, and they must be identical.",
218
+ "loam reads operationIds and nothing else in this file. The schemas are for humans, codegen and review — they are not checked, so getting them wrong is invisible here and expensive later.",
219
+ "Document the endpoints that exist. An operation the fleet already calls (see `landscape.expects`) must be in here, or the contract breaks the moment this lands.",
220
+ // The instruction the brief was missing, and the one that matters most:
221
+ // it used to mark this artifact required unconditionally, in capitals,
222
+ // which for a UI, a worker or a cron told the agent to invent an API into
223
+ // the source of truth. `required` is derived now; this says so in prose
224
+ // as well, because an agent reading the shape rules must not re-derive
225
+ // "MISSING" as "write something".
226
+ "If this service exposes no HTTP API, there is no file to write. Do not invent one — an empty or imagined contract is a claim the whole fleet then joins against. `required` above is derived from the fleet map: it is false exactly when the landscape parses and no edge calls an operation on this service, which is the same evidence that keeps `service.no-openapi` quiet.",
227
+ ],
228
+ },
229
+ {
230
+ artifact: "adrs/",
231
+ key: "adrsDir",
232
+ required: false,
233
+ purpose: "the decisions behind the shape of the service",
234
+ shape: [
235
+ "One file per decision: `adrs/NNNN-<slug>.md`, MADR-style (context · decision · consequences).",
236
+ "Only decisions whose consequences are still visible in the code. A reconstructed rationale for something nobody decided is fiction, and it is the kind that survives review because it sounds reasonable.",
237
+ "If a decision is evident but its reasoning is not, write the decision and say the reasoning is unknown. That is a fact; a plausible motive is not.",
238
+ ],
239
+ },
240
+ {
241
+ artifact: "runbook.md",
242
+ key: "runbook",
243
+ required: false,
244
+ purpose: "how it is run — deploys, dependencies, what to do when it pages",
245
+ shape: [
246
+ "Frontmatter, then how it is deployed and configured, what it depends on, and what to do about the failures that actually happen.",
247
+ "Written from what is in the repo (pipelines, manifests, alert definitions). Whoever carries the pager verifies it; leave `status: draft` until they have.",
248
+ ],
249
+ },
250
+ {
251
+ artifact: "health.yaml",
252
+ key: "health",
253
+ required: false,
254
+ purpose: "SLIs, SLOs, checks and the dependencies that are critical",
255
+ shape: [
256
+ "SLIs and SLOs as they have been AGREED, plus health checks and critical dependencies.",
257
+ "If no SLO has ever been agreed, say so and leave it out. An invented 99.9% becomes a number other teams plan against.",
258
+ ],
259
+ },
260
+ ];
261
+ const FRONTMATTER_BRIEF = {
262
+ fields: {
263
+ service: "the directory this file lives under, exactly. A mismatch is an error (`frontmatter.field-mismatch`).",
264
+ status: "`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.",
265
+ owner: "the team or person who answers for this service.",
266
+ sources: "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.",
267
+ },
268
+ never: ["last_verified", "sources_digest", "content_digest", "sources_files"],
269
+ why: "`sources` 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 a later `validate` can say the code has moved since anyone read it. Both are worth exactly as much as the list is honest.",
270
+ };
271
+ /* ------------------------------------------------------------------ */
272
+ /* The checks — and the ones that do not exist */
273
+ /* ------------------------------------------------------------------ */
274
+ /**
275
+ * The two invocations a baseline meets. `<id>` is a placeholder for the service
276
+ * id — the JSON contract keeps it symbolic, the text renderer substitutes it.
277
+ */
278
+ const VIA_SERVICE = "loam validate --service <id>";
279
+ export const VIA_ALL = "loam validate --all";
280
+ /** What `loam validate` will run against the result — each check names the invocation that surfaces it. */
281
+ export const VALIDATE_CHECKS = [
282
+ { code: "service.no-model", severity: "error", via: VIA_SERVICE, what: "there is no model.likec4 — every other check stops here" },
283
+ { code: "c4.invalid", severity: "error", via: VIA_SERVICE, what: "model.likec4 does not parse as LikeC4" },
284
+ {
285
+ code: "requirements.missing-scenarios",
286
+ severity: "error",
287
+ via: VIA_SERVICE,
288
+ what: "a requirement in spec.md or arch.spec.md has no `#### Scenario:`",
289
+ },
290
+ {
291
+ code: "spec.duplicate-requirement",
292
+ severity: "error",
293
+ via: VIA_SERVICE,
294
+ what: "one `### Requirement:` name defined twice in one living spec.md or arch.spec.md — a later merge edits only the first, the rest live on as stale copies",
295
+ },
296
+ {
297
+ code: "spec.requirement-id-invalid",
298
+ severity: "error",
299
+ via: VIA_SERVICE,
300
+ what: "an optional `Requirement-ID:` violates `[A-Za-z][A-Za-z0-9._-]{0,127}` — stable identities must be portable and unambiguous",
301
+ },
302
+ {
303
+ code: "spec.requirement-id-repeated",
304
+ severity: "error",
305
+ via: VIA_SERVICE,
306
+ what: "one requirement declares `Requirement-ID:` more than once — keep exactly one identity line",
307
+ },
308
+ {
309
+ code: "spec.requirement-id-duplicate",
310
+ severity: "error",
311
+ via: VIA_SERVICE,
312
+ what: "one stable Requirement-ID identifies multiple requirements in one spec.md or arch.spec.md",
313
+ },
314
+ // The keep-last pair: a second `Operations:` / `Covers:` line in one
315
+ // requirement body REPLACES the first — assignment, not append — so the
316
+ // author's two-line list silently loses its first line. The semantics stay;
317
+ // the loss no longer happens in silence.
318
+ {
319
+ code: "spec.repeated-operations",
320
+ severity: "warn",
321
+ via: VIA_SERVICE,
322
+ what: "a second `Operations:` line in one requirement body — the last replaces the earlier ones, whose list is silently dropped; merge into one comma-separated line",
323
+ },
324
+ {
325
+ code: "spec.repeated-covers",
326
+ severity: "warn",
327
+ via: VIA_SERVICE,
328
+ what: "a second `Covers:` line in one requirement body — same keep-last rule, same silent loss; merge into one comma-separated line",
329
+ },
330
+ // The graded absences: the brief marks spec.md and openapi.yaml required, and
331
+ // validate agrees they belong — as warns, because partial adoption is a
332
+ // supported state and these are its honest progress meter, not a gate.
333
+ {
334
+ code: "service.no-spec",
335
+ severity: "warn",
336
+ via: VIA_SERVICE,
337
+ what: "spec.md does not exist yet — legal mid-adoption, but requirement coverage and API governance are unchecked until it does",
338
+ },
339
+ {
340
+ // `warn` is the floor, not the whole story, and the difference matters to a
341
+ // baseline: the absence is graded an ERROR once something already written
342
+ // down joins into the file — a living requirement's `Operations:` line, or
343
+ // an op-linked landscape edge (`landscape.expects`, above). For a service
344
+ // the fleet already calls, `required: true` on this target and this row are
345
+ // the same fact stated twice.
346
+ code: "service.no-openapi",
347
+ severity: "warn",
348
+ via: VIA_SERVICE,
349
+ what: "openapi.yaml does not exist yet — quiet only when the landscape proves no other service calls an operation on this one, and an ERROR rather than a warning once a living `Operations:` line or a landscape edge already names an operation it would have defined",
350
+ },
351
+ {
352
+ code: "openapi.invalid",
353
+ severity: "error",
354
+ via: VIA_SERVICE,
355
+ what: "openapi.yaml exists but does not parse — an unreadable contract proves nothing, so the `api.*` checks and the spine's op resolution are suspended until it reads",
356
+ },
357
+ {
358
+ code: "api.ungoverned",
359
+ severity: "warn",
360
+ via: VIA_SERVICE,
361
+ what: "an operationId in openapi.yaml that no requirement's `Operations:` line names",
362
+ },
363
+ {
364
+ code: "api.ops-unlinked",
365
+ severity: "warn",
366
+ via: VIA_SERVICE,
367
+ what: "openapi.yaml defines operations and spec.md has requirements, but no `Operations:` line joins them — every cross-axis check is vacuously green",
368
+ },
369
+ // The `Operations:` spine, checked against the LIVING contract. It used to
370
+ // fire only inside a feature delta, so a baseline could ship a requirement
371
+ // governing an operation nobody defines and stay green until the first
372
+ // feature touched it.
373
+ {
374
+ code: "spec-api.op-undefined",
375
+ severity: "error",
376
+ via: VIA_SERVICE,
377
+ what: "a living requirement's `Operations:` line names an operationId this service's openapi.yaml does not define — the two axes disagree about what the service exposes",
378
+ },
379
+ {
380
+ code: "spec.no-requirements",
381
+ severity: "warn",
382
+ via: VIA_SERVICE,
383
+ what: "spec.md exists but holds no `### Requirement:` block at all — every requirement-driven check below it is vacuous rather than passing",
384
+ },
385
+ // The deprecation pair fires on a fresh baseline too — a legacy service is
386
+ // exactly where ops marked `deprecated: true` coexist with their
387
+ // replacements, and documenting that lifecycle honestly is part of the
388
+ // adoption. Mark the flag where the code retires an op; loam reads it, and
389
+ // has no removal semantics beyond it.
390
+ {
391
+ code: "api.requirement-deprecated",
392
+ severity: "warn",
393
+ via: VIA_SERVICE,
394
+ what: "a requirement whose `Operations:` list resolves only to operations openapi.yaml marks `deprecated: true` — the behaviour it governs is being retired",
395
+ },
396
+ {
397
+ code: "spine.op-undefined",
398
+ severity: "error",
399
+ via: VIA_SERVICE,
400
+ what: "a landscape edge into this service calls an operation its openapi.yaml does not define — a broken contract between services",
401
+ },
402
+ {
403
+ code: "spine.op-link-missing",
404
+ severity: "warn",
405
+ via: VIA_SERVICE,
406
+ what: 'a landscape "Calls" edge into this service with no `metadata { op }`',
407
+ },
408
+ {
409
+ code: "spine.op-deprecated",
410
+ severity: "warn",
411
+ via: VIA_SERVICE,
412
+ what: "a landscape edge into this service calls an operation openapi.yaml marks `deprecated: true` — the consumer should be migrating off it",
413
+ },
414
+ {
415
+ code: "covers.unknown",
416
+ severity: "warn",
417
+ via: VIA_SERVICE,
418
+ what: "a `Covers:` entry in arch.spec.md that resolves to no element, edge, alert or SLI — a typo silently costs the coverage it was written for",
419
+ },
420
+ {
421
+ code: "health.invalid",
422
+ severity: "warn",
423
+ via: VIA_SERVICE,
424
+ what: "health.yaml exists but does not parse — its alert/SLI ids are unreadable, so `Covers: alert:/sli:` entries and health coverage go unchecked until it reads (a missing health.yaml stays silent)",
425
+ },
426
+ {
427
+ code: "health.uncovered",
428
+ severity: "warn",
429
+ via: VIA_SERVICE,
430
+ what: "an alert or SLI declared in health.yaml that no arch.spec.md requirement covers — expected until the arch spec is written; each one is a signal nothing tests",
431
+ },
432
+ {
433
+ code: "frontmatter.malformed",
434
+ severity: "error",
435
+ via: VIA_SERVICE,
436
+ what: "the frontmatter block does not parse as YAML — owner, status and sources are unreadable, and the field checks are suspended until the header is fixed",
437
+ },
438
+ {
439
+ code: "frontmatter.field-mismatch",
440
+ severity: "error",
441
+ via: VIA_SERVICE,
442
+ what: "spec.md declares a different `service:` than the directory it lives under",
443
+ },
444
+ {
445
+ code: "frontmatter.status-unknown",
446
+ severity: "error",
447
+ via: VIA_SERVICE,
448
+ what: "a status outside `draft` / `verified` — a typo here reads as unverified forever",
449
+ },
450
+ { code: "frontmatter.field-missing", severity: "warn", via: VIA_SERVICE, what: "no `owner`, `status` or `service`" },
451
+ { code: "sources.absent", severity: "warn", via: VIA_SERVICE, what: "spec.md names no `sources`" },
452
+ {
453
+ code: "sources.path-missing",
454
+ severity: "error",
455
+ via: VIA_SERVICE,
456
+ what: "a listed source does not exist, or is a glob pattern — no longer supported (checked when loam runs inside the service's repo)",
457
+ },
458
+ {
459
+ code: "sources.unvouched",
460
+ severity: "warn",
461
+ via: VIA_SERVICE,
462
+ what: "`sources` with no digest — nobody has vouched for the document yet. Expected on a fresh baseline; only a human closes it",
463
+ },
464
+ // sources.stale, sources.current and content.stale are deliberately absent:
465
+ // all three compare against digests `loam vouch` stamps, and a fresh baseline
466
+ // has no stamp to compare — the table lists what a baseline can actually meet.
467
+ // The gherkin.* staleness chain is absent the same way: it fires only once
468
+ // `loam gherkin` has generated a suite under <gherkinDir>/loam/ in the
469
+ // service's own repo, and a fresh baseline has not generated one.
470
+ {
471
+ code: "landscape.service-unmodelled",
472
+ severity: "error",
473
+ via: VIA_ALL,
474
+ 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>' }`",
475
+ },
476
+ {
477
+ code: "landscape.missing",
478
+ severity: "error",
479
+ via: VIA_ALL,
480
+ what: "architecture/landscape.likec4 does not exist at all — an error as soon as services/ holds one service (a warning only in an empty docs repo): with no fleet map, every cross-service check is blind rather than passing",
481
+ },
482
+ {
483
+ code: "landscape.invalid",
484
+ severity: "error",
485
+ via: VIA_ALL,
486
+ what: "architecture/landscape.likec4 exists but does not parse — fix it before anything else; the whole cross-service layer is unchecked until it reads",
487
+ },
488
+ {
489
+ code: "landscape.binding-unknown",
490
+ severity: "error",
491
+ via: VIA_ALL,
492
+ what: "an element's `metadata { service }` names a directory that does not exist — a binding is a claim about this repo, and this one is false",
493
+ },
494
+ {
495
+ code: "landscape.binding-duplicate",
496
+ severity: "warn",
497
+ via: VIA_ALL,
498
+ 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",
499
+ },
500
+ ];
501
+ /**
502
+ * What no check will ever tell you. Read it as the list of ways to be wrong
503
+ * quietly.
504
+ *
505
+ * The first two entries are here because they used to be `shape` rules on
506
+ * model.likec4, phrased exactly like the enforced ones. A brief that states an
507
+ * unenforced rule beside four enforced ones is not merely useless — it is the
508
+ * way an agent learns that a green `loam validate` means more than it does.
509
+ * Anything nothing checks belongs on this list, where its status IS the point.
510
+ */
511
+ export const UNCHECKED = [
512
+ // Not merely unchecked — unread. This entry used to end "write one", on the
513
+ // theory that a view is what makes a model legible. It is, to LikeC4's
514
+ // renderer; loam computes none. Saying otherwise taught agents that a views
515
+ // block was owed to loam, and an `include *` over the FLEET map is the one
516
+ // shape that costs minutes rather than milliseconds.
517
+ "Whether model.likec4 declares a `views { ... }` block, or any view at all — and nothing in loam ever will. loam reads elements and relationships out of the PARSED model and renders nothing, so it computes no view and a model without one is missing nothing loam wants. Views belong to LikeC4's own renderer: write them if you want diagrams, and read them with `npx likec4 start`. Scope them when you do — computing a view is superlinear in the number of edges, and an `include *` over `architecture/landscape.likec4` is the expensive one, because that file holds every call in the fleet.",
518
+ "Whether the model has exactly ONE top-level element for the service, with its containers nested inside. Five top-level boxes for one service parse, validate and bind exactly as well as one — and then the fleet map, which joins elements to directories, has five candidates for the same service and no way to say which is wrong.",
519
+ "Whether the model is the architecture the code actually has. loam parses model.likec4; it never reads a line of the service.",
520
+ "Whether a requirement is TRUE of the service. `loam validate` checks that a requirement has a scenario, never that either one describes real behaviour.",
521
+ "Whether the scenarios are acceptance criteria somebody could test, or the requirement restated in Given/When/Then.",
522
+ "Whether an operationId is the one the code serves, and whether the request and response schemas resemble what the endpoint really accepts and returns. Only the operationId is read.",
523
+ "Whether the runbook's steps work, or the health.yaml SLOs are numbers anyone agreed to.",
524
+ "Whether an arch scenario really exercises what its `Covers:` line names. The entries are resolved against the model and health.yaml — never against code, tests, dashboards or alert rules.",
525
+ "Whether an ADR records a decision that was made, or one reconstructed afterwards to justify the code.",
526
+ "COMPLETENESS. Forty behaviours documented as one requirement passes every check loam has. So does a service with one endpoint documented out of thirty.",
527
+ "Whether `sources` names the files you read. loam checks those paths exist — not that they are the ones the document came from.",
528
+ ];
529
+ const NEVER_OVERWRITE = "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.";
530
+ /** Assemble the brief for one service. Reads the docs repo; writes nothing. */
531
+ export async function serviceBrief(docsDir, service) {
532
+ const paths = servicePaths(docsDir, service);
533
+ const rel = (abs) => relative(docsDir, abs).split(/[\\/]/).join("/");
534
+ // Read before the artifact loop, not after it: what the fleet already says
535
+ // about this service decides whether it owes an API contract at all.
536
+ const landscape = await landscapeContext(docsDir, service);
537
+ const owesApi = apiExpected(landscape);
538
+ const targets = [];
539
+ for (const { key, ...artifact } of ARTIFACTS) {
540
+ // `adrs/` is a directory, and an empty one is not an existing artifact —
541
+ // reporting it as present would tell the agent to diff against nothing.
542
+ const dir = key === "adrsDir";
543
+ const exists = dir ? await hasMarkdown(paths[key]) : existsSync(paths[key]);
544
+ targets.push({
545
+ ...artifact,
546
+ ...(key === "openapi" ? { required: owesApi } : {}),
547
+ path: dir ? `${rel(paths[key])}/` : rel(paths[key]),
548
+ exists,
549
+ action: exists ? "diff" : "create",
550
+ });
551
+ }
552
+ // The eighth target is the fleet map, and it is conditional: a service the
553
+ // landscape ALREADY resolves an element to owes it nothing, and briefing an
554
+ // edit there would invite an agent to draw a second box for a service that
555
+ // has one. Everything else about the target is unconditional — the file is
556
+ // shared, so `action` is `edit` whenever it exists and `create` only for the
557
+ // very first service of a brand-new docs repo.
558
+ if (landscape.modelled !== true) {
559
+ targets.push({
560
+ ...landscapeArtifact(service, landscape.expects, landscape.present),
561
+ path: rel(landscapePath(docsDir)),
562
+ exists: landscape.present,
563
+ action: landscape.present ? "edit" : "create",
564
+ });
565
+ }
566
+ return {
567
+ service,
568
+ docsDir,
569
+ path: rel(paths.dir),
570
+ targets,
571
+ landscape,
572
+ frontmatter: FRONTMATTER_BRIEF,
573
+ checks: VALIDATE_CHECKS,
574
+ unchecked: UNCHECKED,
575
+ rule: NEVER_OVERWRITE,
576
+ };
577
+ }
578
+ async function hasMarkdown(dir) {
579
+ if (!existsSync(dir))
580
+ return false;
581
+ const entries = await readdir(dir, { withFileTypes: true });
582
+ // `statSync` for the symlink case: a Dirent never follows one, so `isFile()`
583
+ // is false for a symlinked ADR and the directory reads as empty (repo.ts's
584
+ // `entryIs` is the same rule for `services/` and `features/`).
585
+ return entries.some((e) => {
586
+ if (!e.name.endsWith(".md"))
587
+ return false;
588
+ if (e.isFile())
589
+ return true;
590
+ if (!e.isSymbolicLink())
591
+ return false;
592
+ try {
593
+ return statSync(join(dir, e.name)).isFile();
594
+ }
595
+ catch {
596
+ return false;
597
+ }
598
+ });
599
+ }
600
+ /**
601
+ * Does the fleet expect an API of this service?
602
+ *
603
+ * The brief marked `openapi.yaml` required unconditionally, and printed the
604
+ * absence as `MISSING` in capitals — while `loam validate` and `loam list`
605
+ * derive the opposite from the same landscape (`service.no-openapi` stays quiet
606
+ * when nothing calls the service, and `list`'s maturity ladder stops demanding
607
+ * a contract). For a UI, a worker or a cron the brief was therefore telling an
608
+ * agent to INVENT an API contract into the source of truth — the one class of
609
+ * guess this whole module exists to refuse, and the one the brief itself names
610
+ * 219 lines further down.
611
+ *
612
+ * Positive evidence only, exactly as `list` reads it: a landscape that is
613
+ * absent or does not parse proves nothing about who calls this service, so the
614
+ * contract is still owed. `expects` is the ops on inbound edges, which is
615
+ * `list`'s `called` set seen from one service.
616
+ */
617
+ function apiExpected(landscape) {
618
+ return !landscape.parses || landscape.expects.length > 0;
619
+ }
620
+ /**
621
+ * The service ids the resolver may fall back on when an element carries no
622
+ * `metadata { service }` binding — the same positive evidence `validate` and
623
+ * `list` hand it. A docs repo that cannot be enumerated (no `services/` yet:
624
+ * `adopt` runs there) leaves the resolver with bindings alone, which is what it
625
+ * had before.
626
+ */
627
+ async function knownServices(docsDir) {
628
+ try {
629
+ return new Set((await listServices(docsDir)).map((s) => s.id));
630
+ }
631
+ catch {
632
+ return new Set();
633
+ }
634
+ }
635
+ /**
636
+ * What the living landscape already says about this service.
637
+ *
638
+ * This is the half of the brief that stops an agent inventing a parallel model:
639
+ * the fleet has already drawn some of these boxes and edges, and the baseline has
640
+ * to attach to them. A landscape that does not parse yields `modelled: null` —
641
+ * "nothing models it" would be a claim about a document nobody could read.
642
+ */
643
+ async function landscapeContext(docsDir, service) {
644
+ const path = landscapePath(docsDir);
645
+ const empty = {
646
+ present: existsSync(path),
647
+ parses: false,
648
+ modelled: null,
649
+ elements: [],
650
+ inbound: [],
651
+ outbound: [],
652
+ expects: [],
653
+ instruction: null,
654
+ };
655
+ if (!empty.present)
656
+ return { ...empty, modelled: false, instruction: instructionFor(service, "absent", []) };
657
+ const land = await loadFile(path);
658
+ if (land.errors.length > 0)
659
+ return { ...empty, instruction: instructionFor(service, "unparseable", []) };
660
+ const mine = (e) => elementService(e) === service;
661
+ const elements = land.elements.filter(mine).map((e) => ({
662
+ id: e.id,
663
+ title: e.title,
664
+ kind: e.kind,
665
+ tags: e.tags,
666
+ bound: e.service !== undefined,
667
+ }));
668
+ // One resolver for the whole scan, built with the real service ids: an edge
669
+ // drawn into a modelled container (`paymentService.api`) is an edge into the
670
+ // service, and `expects` — which now decides whether this service owes an
671
+ // OpenAPI contract at all — has to see it. Building it once also stops the
672
+ // per-edge rebuild the old `serviceOf(land.elements, id)` call did.
673
+ const svcOf = serviceResolver(land.elements, await knownServices(docsDir));
674
+ const inbound = [];
675
+ const outbound = [];
676
+ for (const r of land.relationships) {
677
+ const edge = { op: r.op ?? null, title: r.title ?? null };
678
+ if (svcOf(r.target) === service)
679
+ inbound.push({ from: svcOf(r.source), ...edge });
680
+ else if (svcOf(r.source) === service)
681
+ outbound.push({ to: svcOf(r.target), ...edge });
682
+ }
683
+ const expects = [...new Set(inbound.map((e) => e.op).filter((op) => op !== null))];
684
+ return {
685
+ present: true,
686
+ parses: true,
687
+ modelled: elements.length > 0,
688
+ elements,
689
+ inbound,
690
+ outbound,
691
+ expects,
692
+ instruction: elements.length > 0 ? null : instructionFor(service, "unmodelled", expects),
693
+ };
694
+ }
695
+ /**
696
+ * The fleet-map instruction, in the three states a landscape can be in.
697
+ *
698
+ * It says the file, the block, and the consequence, in that order. The
699
+ * consequence is the part agents acted on: "add an element" reads as tidiness,
700
+ * "until you do, `loam validate --all` fails and no cross-service check can see
701
+ * this service" reads as work.
702
+ */
703
+ function instructionFor(service, state, expects) {
704
+ const id = elementIdFor(service);
705
+ const bind = `an element bound with \`metadata { service '${service}' }\``;
706
+ const consequence = `Until one resolves, \`loam validate --all\` reports \`landscape.service-unmodelled\` (error) for ` +
707
+ `services/${service}/: the service is documented and invisible — no edge into it can be checked ` +
708
+ `against its openapi.yaml, and no feature can draw a call to it.`;
709
+ // `expects` is reachable only through elements that already resolve to this
710
+ // service, so it is empty in every state that produces an instruction. It is
711
+ // threaded through anyway rather than dropped: the caller computes it once,
712
+ // and the day an unmodelled service can inherit an edge (a container-level
713
+ // binding, say) the sentence is already here instead of being noticed missing.
714
+ const owed = expects.length > 0
715
+ ? ` The fleet already calls ${expects.map((o) => `'${o}'`).join(", ")} on this service, so those edges exist; your element is what they will resolve to.`
716
+ : "";
717
+ if (state === "absent") {
718
+ return (`architecture/landscape.likec4 does not exist yet. Create it with a \`specification { ... }\` block ` +
719
+ `declaring the kinds you use and a \`model { ... }\` block containing ${bind} ` +
720
+ `(conventionally \`${id} = softwareSystem '${service}'\`), plus one edge per cross-service call, each ` +
721
+ `carrying \`metadata { op '<operationId>' }\`. ${consequence}${owed}`);
722
+ }
723
+ if (state === "unparseable") {
724
+ return (`architecture/landscape.likec4 exists but does not parse (\`landscape.invalid\`), so nothing can be said ` +
725
+ `about what it already models. Fix the parse errors first — they blind every cross-service check at once — ` +
726
+ `then make sure it holds ${bind}. ${consequence}`);
727
+ }
728
+ return (`Nothing in architecture/landscape.likec4 resolves to services/${service}/. ADD to that file — do not rewrite ` +
729
+ `it, every other service's map is in there — ${bind}, conventionally ` +
730
+ `\`${id} = softwareSystem '${service}'\`, inside the existing \`model { ... }\` block, and draw each ` +
731
+ `cross-service call as an edge carrying \`metadata { op '<operationId>' }\`. ${consequence}${owed}`);
732
+ }
733
+ //# sourceMappingURL=brief.js.map