@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,479 @@
1
+ import { readFile } from "node:fs/promises";
2
+ import { LikeC4 } from "likec4";
3
+ /**
4
+ * The service directory an element stands for: an explicit
5
+ * `metadata { service '<id>' }` binding wins, the title is the fallback.
6
+ *
7
+ * The fallback is what every docs repo written before the binding existed relies
8
+ * on, and it is also the trap the binding exists to close: matching on the title
9
+ * means renaming a box in a diagram silently unlinks it from its service, and
10
+ * every check that joined the two just stops finding anything.
11
+ */
12
+ export function elementService(e) {
13
+ return e.service ?? e.title;
14
+ }
15
+ /**
16
+ * Every id an endpoint may be filed under, nearest first: `a.b.c`, `a.b`, `a`.
17
+ *
18
+ * A landscape does not have to draw a service as one opaque box. The moment
19
+ * somebody models its containers — `paymentService.api`, `paymentService.worker`
20
+ * — an edge drawn INTO a container is still an edge into the service, and every
21
+ * join that groups by service has to know that. Resolving only the exact id is
22
+ * what made those edges invisible: the spine check silently skipped them, and
23
+ * the no-openapi grace treated a service with a dozen inbound container calls as
24
+ * one nobody calls at all.
25
+ */
26
+ function ancestorIds(id) {
27
+ const out = [id];
28
+ for (let dot = id.lastIndexOf("."); dot !== -1; dot = id.lastIndexOf(".", dot - 1)) {
29
+ out.push(id.slice(0, dot));
30
+ }
31
+ return out;
32
+ }
33
+ /**
34
+ * A memoized `id -> service` resolver over one document.
35
+ *
36
+ * Resolution order, and why it is this order:
37
+ *
38
+ * 1. the nearest ancestor (the id itself first) carrying an explicit
39
+ * `metadata { service '<id>' }` — a binding is a claim somebody wrote down,
40
+ * so it outranks every guess;
41
+ * 2. the nearest ancestor whose title names a REAL `services/<id>/` directory —
42
+ * positive evidence from the filesystem, available only to callers that
43
+ * hand in `known`;
44
+ * 3. the element's own title, and finally the raw id — today's fallback, kept
45
+ * LAST because it is the one that lies: it happily resolves
46
+ * `paymentService.api` to "api", a service that has never existed.
47
+ *
48
+ * Built once per document and shared, because it is called inside loops over
49
+ * every relationship and the walk up the ancestor chain is not free.
50
+ */
51
+ export function serviceResolver(elements, known) {
52
+ const byId = new Map(elements.map((e) => [e.id, e]));
53
+ const memo = new Map();
54
+ return (id) => {
55
+ const hit = memo.get(id);
56
+ if (hit !== undefined)
57
+ return hit;
58
+ let answer;
59
+ for (const candidate of ancestorIds(id)) {
60
+ const e = byId.get(candidate);
61
+ if (e?.service !== undefined) {
62
+ answer = e.service;
63
+ break;
64
+ }
65
+ }
66
+ if (answer === undefined && known !== undefined) {
67
+ for (const candidate of ancestorIds(id)) {
68
+ const e = byId.get(candidate);
69
+ if (e !== undefined && known.has(e.title)) {
70
+ answer = e.title;
71
+ break;
72
+ }
73
+ }
74
+ }
75
+ if (answer === undefined) {
76
+ const self = byId.get(id);
77
+ answer = self ? elementService(self) : id;
78
+ }
79
+ memo.set(id, answer);
80
+ return answer;
81
+ };
82
+ }
83
+ /**
84
+ * The service a relationship endpoint belongs to. An id that names no element
85
+ * resolves to itself, so a partial document degrades to the id rather than
86
+ * throwing.
87
+ *
88
+ * `known` is the set of service directories that actually exist; pass it
89
+ * wherever the caller has enumerated `services/`, so an edge into a modelled
90
+ * container resolves to the service that owns it instead of to the container's
91
+ * own title. Callers with a single document and no repository in hand omit it
92
+ * and keep the pre-existing behaviour.
93
+ */
94
+ export function serviceOf(elements, id, known) {
95
+ return serviceResolver(elements, known)(id);
96
+ }
97
+ /** Flatten a LikeC4 model into loam's neutral `Elem`/`Rel` view. */
98
+ export function flattenModel(model) {
99
+ const elements = [...model.elements()].map((e) => ({
100
+ id: e.id,
101
+ kind: e.kind,
102
+ title: e.title,
103
+ description: descText(e.description),
104
+ service: metaKey(e.metadata, "service"),
105
+ tags: [...(e.tags ?? [])],
106
+ }));
107
+ const relationships = [...model.relationships()].map((r) => ({
108
+ source: r.source.id,
109
+ target: r.target.id,
110
+ // LikeC4 reports an untitled edge as title: null — normalize to the declared `title?: string`.
111
+ title: r.title ?? undefined,
112
+ op: metaKey(r.metadata, "op"),
113
+ tags: [...(r.tags ?? [])],
114
+ }));
115
+ return { elements, relationships };
116
+ }
117
+ /**
118
+ * Load and validate a single self-contained `.likec4` document, in-process
119
+ * (no external tool, no JVM). Returns validation errors and, if clean, the
120
+ * flattened elements + relationships.
121
+ */
122
+ export async function loadFile(path) {
123
+ return loadSource(await readFile(path, "utf8"));
124
+ }
125
+ /**
126
+ * `loadFile` for text that is not (yet) on disk — what the archive merge uses
127
+ * to prove a computed landscape parses BEFORE anything is written.
128
+ */
129
+ export async function loadSource(src) {
130
+ // LikeC4 owns a Langium workspace and registers a process-exit listener for
131
+ // it. Validation creates many short-lived instances, so every acquired
132
+ // instance must be disposed even on an invalid document or a failed model
133
+ // computation. Its logger is disabled because diagnostics are returned in
134
+ // LoadedDoc instead of being written out-of-band to stderr.
135
+ const likec4 = await LikeC4.fromSource(src, { logger: false });
136
+ try {
137
+ // Assigned, not cast: LikeC4 declares `getErrors()` as a non-nullable array
138
+ // whose element type is structurally wider than ours, so the compiler can
139
+ // check the shape here. The cast was the only thing standing between a
140
+ // dependency renaming `message` and a build that still succeeded — and the
141
+ // `?? []` it carried could never fire.
142
+ const errors = likec4.getErrors();
143
+ if (errors.length > 0) {
144
+ return { errors, elements: [], relationships: [] };
145
+ }
146
+ // `parsedModel`, not `computedModel`: the computed stage additionally
147
+ // computes every VIEW the document declares (and a default one when it
148
+ // declares none), which loam never reads — it renders nothing. That work is
149
+ // superlinear in the number of RELATIONSHIPS, so a landscape at fleet shape
150
+ // turned `loam list` from under a second into minutes. The elements and
151
+ // relationships below are identical either way.
152
+ const model = (await likec4.parsedModel());
153
+ return { errors, ...flattenModel(model) };
154
+ }
155
+ finally {
156
+ await likec4.dispose();
157
+ }
158
+ }
159
+ /**
160
+ * Read one string key out of a LikeC4 `metadata { ... }` block. Two keys carry
161
+ * loam's spines: `op` on a relationship (the OpenAPI operationId it calls) and
162
+ * `service` on an element (the services/<id> directory it stands for). Elements
163
+ * with no metadata come back as `{}`, so a missing key is indistinguishable from
164
+ * a missing block — both mean "not bound".
165
+ */
166
+ function metaKey(m, key) {
167
+ if (m && typeof m === "object") {
168
+ const v = m[key];
169
+ if (typeof v === "string")
170
+ return v;
171
+ }
172
+ return undefined;
173
+ }
174
+ /** LikeC4 descriptions can be a string or a rich-text object ({ txt } / { text } / { md }). */
175
+ function descText(d) {
176
+ if (typeof d === "string")
177
+ return d;
178
+ if (d && typeof d === "object") {
179
+ const o = d;
180
+ for (const key of ["txt", "text", "md", "value"]) {
181
+ const v = o[key];
182
+ if (typeof v === "string")
183
+ return v;
184
+ }
185
+ }
186
+ return undefined;
187
+ }
188
+ export function maskSource(text) {
189
+ const out = text.split("");
190
+ const literals = [];
191
+ let state = "code";
192
+ let litStart = -1;
193
+ let lit = "";
194
+ for (let i = 0; i < text.length; i += 1) {
195
+ const ch = text[i];
196
+ const next = text[i + 1];
197
+ switch (state) {
198
+ case "code":
199
+ if (ch === "'" || ch === '"') {
200
+ state = ch === "'" ? "squote" : "dquote";
201
+ litStart = i;
202
+ lit = "";
203
+ out[i] = " ";
204
+ }
205
+ else if (ch === "/" && next === "/") {
206
+ state = "lineComment";
207
+ out[i] = " ";
208
+ }
209
+ else if (ch === "/" && next === "*") {
210
+ state = "blockComment";
211
+ out[i] = " ";
212
+ }
213
+ break;
214
+ case "squote":
215
+ case "dquote":
216
+ if (ch === "\\" && i + 1 < text.length) {
217
+ lit += text[i + 1];
218
+ out[i] = " ";
219
+ out[i + 1] = " ";
220
+ i += 1;
221
+ }
222
+ else if (ch === (state === "squote" ? "'" : '"')) {
223
+ literals.push({ start: litStart, end: i + 1, value: lit });
224
+ state = "code";
225
+ out[i] = " ";
226
+ }
227
+ else {
228
+ lit += ch;
229
+ if (ch !== "\n")
230
+ out[i] = " ";
231
+ }
232
+ break;
233
+ case "lineComment":
234
+ if (ch === "\n")
235
+ state = "code";
236
+ else
237
+ out[i] = " ";
238
+ break;
239
+ case "blockComment":
240
+ if (ch === "*" && next === "/") {
241
+ state = "code";
242
+ out[i] = " ";
243
+ out[i + 1] = " ";
244
+ i += 1;
245
+ }
246
+ else if (ch !== "\n") {
247
+ out[i] = " ";
248
+ }
249
+ break;
250
+ }
251
+ }
252
+ return { code: out.join(""), literals };
253
+ }
254
+ /** Index of the `}` matching the `{` at `open`, over MASKED code; -1 when unbalanced. */
255
+ export function matchBrace(code, open) {
256
+ let depth = 0;
257
+ for (let i = open; i < code.length; i += 1) {
258
+ if (code[i] === "{")
259
+ depth += 1;
260
+ else if (code[i] === "}") {
261
+ depth -= 1;
262
+ if (depth === 0)
263
+ return i;
264
+ }
265
+ }
266
+ return -1;
267
+ }
268
+ /**
269
+ * Locate every element and relationship declaration inside the top-level
270
+ * `model { ... }` block of a LikeC4 source, with the byte range each one
271
+ * occupies. This is the splice map `loam archive` merges the landscape with:
272
+ * the parsed model says WHAT exists, this says WHERE its authored text is —
273
+ * verbatim, so nothing the parser does not model (technology, style, links)
274
+ * can be lost by rebuilding it.
275
+ *
276
+ * Both declaration forms are recognised — `name = kind ...` always, and
277
+ * `kind name ...` for any kind the document's own `specification` block
278
+ * declares. Relationship endpoints are resolved the way LikeC4 reads them:
279
+ * innermost enclosing scope outward, then the top level, then a unique leaf
280
+ * name anywhere; `it`/`this` (and an omitted source) mean the enclosing
281
+ * element. A name that resolves nowhere is left as written — the caller's
282
+ * match against the parsed model then fails loudly instead of guessing.
283
+ *
284
+ * Returns null when there is no model block. Callers only hand in documents
285
+ * LikeC4 has already parsed, so the scan can trust balanced braces.
286
+ */
287
+ export function scanModel(text) {
288
+ const { code, literals } = maskSource(text);
289
+ const m = /\bmodel\s*\{/.exec(code);
290
+ if (!m)
291
+ return null;
292
+ const open = m.index + m[0].length - 1;
293
+ const close = matchBrace(code, open);
294
+ if (close === -1)
295
+ return null;
296
+ // Element kinds from the specification block(s): the `<kind> <name>`
297
+ // declaration form is recognisable only by its first token being a kind.
298
+ const kinds = new Set();
299
+ for (const sm of code.matchAll(/\bspecification\s*\{/g)) {
300
+ const sClose = matchBrace(code, sm.index + sm[0].length - 1);
301
+ if (sClose === -1)
302
+ continue;
303
+ for (const em of code.slice(sm.index, sClose).matchAll(/\belement\s+([A-Za-z_][\w-]*)/g)) {
304
+ kinds.add(em[1]);
305
+ }
306
+ }
307
+ const indentOf = (at) => {
308
+ const nl = text.lastIndexOf("\n", at - 1);
309
+ const ws = text.slice(nl + 1, at);
310
+ return /^[ \t]*$/.test(ws) ? ws : "";
311
+ };
312
+ const literalIn = (from, to) => literals.find((l) => l.start >= from && l.end <= to);
313
+ const keyedLiteral = (key, from, to) => {
314
+ if (from === -1)
315
+ return undefined;
316
+ const re = new RegExp(`\\b${key}\\b`, "g");
317
+ re.lastIndex = from;
318
+ const km = re.exec(code);
319
+ if (km === null || km.index >= to)
320
+ return undefined;
321
+ return literalIn(km.index, to)?.value;
322
+ };
323
+ const trimmedEnd = (from, to) => {
324
+ let e = to;
325
+ while (e > from && /\s/.test(text[e - 1]))
326
+ e -= 1;
327
+ return e;
328
+ };
329
+ const elements = [];
330
+ const raw = [];
331
+ const stack = [];
332
+ let i = open + 1;
333
+ while (i < close) {
334
+ const ch = code[i];
335
+ if (/\s/.test(ch)) {
336
+ i += 1;
337
+ continue;
338
+ }
339
+ if (ch === "}") {
340
+ const frame = stack.pop();
341
+ if (frame !== undefined) {
342
+ frame.el.bodyClose = i;
343
+ frame.el.end = i + 1;
344
+ elements.push(frame.el);
345
+ }
346
+ i += 1;
347
+ continue;
348
+ }
349
+ // A statement's head: up to its own `{`, the end of the line, or the
350
+ // enclosing block's `}` on the same line — whichever comes first.
351
+ const stmtStart = i;
352
+ let headEnd = close;
353
+ let opener = -1;
354
+ for (let j = i; j < close; j += 1) {
355
+ const c = code[j];
356
+ if (c === "\n" || c === "}") {
357
+ headEnd = j;
358
+ break;
359
+ }
360
+ if (c === "{") {
361
+ opener = j;
362
+ headEnd = j;
363
+ break;
364
+ }
365
+ }
366
+ const head = code.slice(stmtStart, headEnd);
367
+ const parentId = stack.length > 0 ? stack[stack.length - 1].id : "";
368
+ // Classify: `name = kind` first, then a relationship (the arrow cannot hide
369
+ // in a string — the head is masked), then the `kind name` form, else a
370
+ // property/tag line that is consumed whole, nested block included.
371
+ let localName = null;
372
+ const assign = /^([A-Za-z_]\w*)\s*=\s*[A-Za-z_]/.exec(head);
373
+ if (assign !== null) {
374
+ localName = assign[1];
375
+ }
376
+ else if (!head.includes("->")) {
377
+ const toks = head.trim().split(/\s+/);
378
+ if (toks.length >= 2 && kinds.has(toks[0]) && /^[A-Za-z_]\w*$/.test(toks[1])) {
379
+ localName = toks[1];
380
+ }
381
+ }
382
+ if (localName !== null) {
383
+ const id = parentId === "" ? localName : `${parentId}.${localName}`;
384
+ const el = {
385
+ id,
386
+ start: stmtStart,
387
+ end: headEnd,
388
+ bodyOpen: opener,
389
+ bodyClose: -1,
390
+ indent: indentOf(stmtStart),
391
+ };
392
+ if (opener !== -1) {
393
+ stack.push({ id, el });
394
+ i = opener + 1;
395
+ }
396
+ else {
397
+ el.end = trimmedEnd(stmtStart, headEnd);
398
+ elements.push(el);
399
+ i = headEnd;
400
+ }
401
+ continue;
402
+ }
403
+ if (head.includes("->")) {
404
+ const bClose = opener === -1 ? -1 : matchBrace(code, opener);
405
+ const stmtEnd = opener === -1 ? trimmedEnd(stmtStart, headEnd) : bClose === -1 ? close : bClose + 1;
406
+ const am = /^([A-Za-z_][\w.]*)?\s*(?:-\[[^\]]*\])?->\s*([A-Za-z_][\w.]*)/.exec(head);
407
+ if (am !== null) {
408
+ const arrowEnd = stmtStart + am[0].length;
409
+ const bodyFrom = opener === -1 ? -1 : opener + 1;
410
+ const bodyTo = opener === -1 ? -1 : stmtEnd - 1;
411
+ const title = literalIn(arrowEnd, headEnd)?.value ?? keyedLiteral("title", bodyFrom, bodyTo);
412
+ const tags = [];
413
+ for (const tm of code.slice(stmtStart, stmtEnd).matchAll(/#([\w-]+)/g))
414
+ tags.push(tm[1]);
415
+ raw.push({
416
+ srcLit: am[1],
417
+ tgtLit: am[2],
418
+ parent: parentId,
419
+ start: stmtStart,
420
+ end: stmtEnd,
421
+ indent: indentOf(stmtStart),
422
+ title,
423
+ op: keyedLiteral("op", bodyFrom, bodyTo),
424
+ tags,
425
+ });
426
+ }
427
+ i = Math.max(stmtEnd, headEnd);
428
+ continue;
429
+ }
430
+ if (opener !== -1) {
431
+ const bClose = matchBrace(code, opener);
432
+ i = bClose === -1 ? close : bClose + 1;
433
+ }
434
+ else {
435
+ i = headEnd;
436
+ }
437
+ }
438
+ // Resolve relationship endpoints against everything declared anywhere in the
439
+ // model — LikeC4 resolution is not order-dependent, so neither is this.
440
+ const ids = new Set(elements.map((e) => e.id));
441
+ const byLeaf = new Map();
442
+ for (const e of elements) {
443
+ const leaf = e.id.slice(e.id.lastIndexOf(".") + 1);
444
+ byLeaf.set(leaf, [...(byLeaf.get(leaf) ?? []), e.id]);
445
+ }
446
+ const resolve = (lit, enclosing) => {
447
+ if (lit === undefined || lit === "it" || lit === "this")
448
+ return enclosing;
449
+ const segs = lit.split(".");
450
+ let base;
451
+ for (let scope = enclosing; scope !== "" && base === undefined;) {
452
+ if (ids.has(`${scope}.${segs[0]}`))
453
+ base = `${scope}.${segs[0]}`;
454
+ const dot = scope.lastIndexOf(".");
455
+ scope = dot === -1 ? "" : scope.slice(0, dot);
456
+ }
457
+ if (base === undefined && ids.has(segs[0]))
458
+ base = segs[0];
459
+ if (base === undefined) {
460
+ const leafed = byLeaf.get(segs[0]);
461
+ if (leafed !== undefined && leafed.length === 1)
462
+ base = leafed[0];
463
+ }
464
+ return [base ?? segs[0], ...segs.slice(1)].join(".");
465
+ };
466
+ const rels = raw.map((r) => ({
467
+ source: resolve(r.srcLit, r.parent),
468
+ target: resolve(r.tgtLit, r.parent),
469
+ title: r.title,
470
+ op: r.op,
471
+ tags: r.tags,
472
+ start: r.start,
473
+ end: r.end,
474
+ indent: r.indent,
475
+ }));
476
+ elements.sort((a, b) => a.start - b.start);
477
+ return { open, close, elements, rels };
478
+ }
479
+ //# sourceMappingURL=likec4.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"likec4.js","sourceRoot":"","sources":["../../src/core/likec4.ts"],"names":[],"mappings":"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","sourcesContent":["import { readFile } from \"node:fs/promises\";\nimport { LikeC4 } from \"likec4\";\n\n/** A parse/validation issue reported by LikeC4. */\nexport interface LikeC4Error {\n message: string;\n line?: number;\n sourceFsPath?: string;\n}\n\n/** loam-neutral element view (flattened from the LikeC4 parsed model). */\nexport interface Elem {\n id: string;\n kind: string;\n title: string;\n description?: string;\n /**\n * The `services/<id>` directory this element stands for, from the element's\n * `metadata { service '...' }`. Absent on the elements nobody has bound —\n * see `elementService` for the fallback.\n */\n service?: string;\n tags: string[];\n}\n\n/**\n * The service directory an element stands for: an explicit\n * `metadata { service '<id>' }` binding wins, the title is the fallback.\n *\n * The fallback is what every docs repo written before the binding existed relies\n * on, and it is also the trap the binding exists to close: matching on the title\n * means renaming a box in a diagram silently unlinks it from its service, and\n * every check that joined the two just stops finding anything.\n */\nexport function elementService(e: Elem): string {\n return e.service ?? e.title;\n}\n\n/**\n * Every id an endpoint may be filed under, nearest first: `a.b.c`, `a.b`, `a`.\n *\n * A landscape does not have to draw a service as one opaque box. The moment\n * somebody models its containers — `paymentService.api`, `paymentService.worker`\n * — an edge drawn INTO a container is still an edge into the service, and every\n * join that groups by service has to know that. Resolving only the exact id is\n * what made those edges invisible: the spine check silently skipped them, and\n * the no-openapi grace treated a service with a dozen inbound container calls as\n * one nobody calls at all.\n */\nfunction ancestorIds(id: string): string[] {\n const out = [id];\n for (let dot = id.lastIndexOf(\".\"); dot !== -1; dot = id.lastIndexOf(\".\", dot - 1)) {\n out.push(id.slice(0, dot));\n }\n return out;\n}\n\n/**\n * A memoized `id -> service` resolver over one document.\n *\n * Resolution order, and why it is this order:\n *\n * 1. the nearest ancestor (the id itself first) carrying an explicit\n * `metadata { service '<id>' }` — a binding is a claim somebody wrote down,\n * so it outranks every guess;\n * 2. the nearest ancestor whose title names a REAL `services/<id>/` directory —\n * positive evidence from the filesystem, available only to callers that\n * hand in `known`;\n * 3. the element's own title, and finally the raw id — today's fallback, kept\n * LAST because it is the one that lies: it happily resolves\n * `paymentService.api` to \"api\", a service that has never existed.\n *\n * Built once per document and shared, because it is called inside loops over\n * every relationship and the walk up the ancestor chain is not free.\n */\nexport function serviceResolver(\n elements: Elem[],\n known?: ReadonlySet<string>,\n): (id: string) => string {\n const byId = new Map(elements.map((e) => [e.id, e]));\n const memo = new Map<string, string>();\n return (id: string): string => {\n const hit = memo.get(id);\n if (hit !== undefined) return hit;\n let answer: string | undefined;\n for (const candidate of ancestorIds(id)) {\n const e = byId.get(candidate);\n if (e?.service !== undefined) {\n answer = e.service;\n break;\n }\n }\n if (answer === undefined && known !== undefined) {\n for (const candidate of ancestorIds(id)) {\n const e = byId.get(candidate);\n if (e !== undefined && known.has(e.title)) {\n answer = e.title;\n break;\n }\n }\n }\n if (answer === undefined) {\n const self = byId.get(id);\n answer = self ? elementService(self) : id;\n }\n memo.set(id, answer);\n return answer;\n };\n}\n\n/**\n * The service a relationship endpoint belongs to. An id that names no element\n * resolves to itself, so a partial document degrades to the id rather than\n * throwing.\n *\n * `known` is the set of service directories that actually exist; pass it\n * wherever the caller has enumerated `services/`, so an edge into a modelled\n * container resolves to the service that owns it instead of to the container's\n * own title. Callers with a single document and no repository in hand omit it\n * and keep the pre-existing behaviour.\n */\nexport function serviceOf(elements: Elem[], id: string, known?: ReadonlySet<string>): string {\n return serviceResolver(elements, known)(id);\n}\n\n/** loam-neutral relationship view. */\nexport interface Rel {\n source: string;\n target: string;\n title?: string;\n /** OpenAPI operationId this call uses, from the relationship's `metadata { op '...' }`. */\n op?: string;\n tags: string[];\n}\n\nexport interface LoadedDoc {\n errors: LikeC4Error[];\n elements: Elem[];\n relationships: Rel[];\n}\n\n/**\n * The slice of a LikeC4 model loam reads: elements and relationships, and on\n * each of those only id/kind/title/description/tags/metadata.\n *\n * It is a type of its own, rather than an inline cast at the one call site,\n * because BOTH of LikeC4's model stages expose exactly this slice — which is\n * what makes the cheap stage a safe substitute for the expensive one. The\n * substitution is pinned in test/likec4-model-parity.test.ts, which flattens\n * both stages through `flattenModel` below and compares the results.\n */\nexport interface ReadableModel {\n elements: () => Iterable<{\n id: string;\n kind: string;\n title: string;\n description?: unknown;\n tags?: readonly string[];\n metadata?: unknown;\n }>;\n relationships: () => Iterable<{\n source: { id: string };\n target: { id: string };\n title?: string | null;\n tags?: readonly string[];\n metadata?: unknown;\n }>;\n}\n\n/** Flatten a LikeC4 model into loam's neutral `Elem`/`Rel` view. */\nexport function flattenModel(model: ReadableModel): { elements: Elem[]; relationships: Rel[] } {\n const elements: Elem[] = [...model.elements()].map((e) => ({\n id: e.id,\n kind: e.kind,\n title: e.title,\n description: descText(e.description),\n service: metaKey(e.metadata, \"service\"),\n tags: [...(e.tags ?? [])],\n }));\n const relationships: Rel[] = [...model.relationships()].map((r) => ({\n source: r.source.id,\n target: r.target.id,\n // LikeC4 reports an untitled edge as title: null — normalize to the declared `title?: string`.\n title: r.title ?? undefined,\n op: metaKey(r.metadata, \"op\"),\n tags: [...(r.tags ?? [])],\n }));\n return { elements, relationships };\n}\n\n/**\n * Load and validate a single self-contained `.likec4` document, in-process\n * (no external tool, no JVM). Returns validation errors and, if clean, the\n * flattened elements + relationships.\n */\nexport async function loadFile(path: string): Promise<LoadedDoc> {\n return loadSource(await readFile(path, \"utf8\"));\n}\n\n/**\n * `loadFile` for text that is not (yet) on disk — what the archive merge uses\n * to prove a computed landscape parses BEFORE anything is written.\n */\nexport async function loadSource(src: string): Promise<LoadedDoc> {\n // LikeC4 owns a Langium workspace and registers a process-exit listener for\n // it. Validation creates many short-lived instances, so every acquired\n // instance must be disposed even on an invalid document or a failed model\n // computation. Its logger is disabled because diagnostics are returned in\n // LoadedDoc instead of being written out-of-band to stderr.\n const likec4 = await LikeC4.fromSource(src, { logger: false });\n try {\n // Assigned, not cast: LikeC4 declares `getErrors()` as a non-nullable array\n // whose element type is structurally wider than ours, so the compiler can\n // check the shape here. The cast was the only thing standing between a\n // dependency renaming `message` and a build that still succeeded — and the\n // `?? []` it carried could never fire.\n const errors: LikeC4Error[] = likec4.getErrors();\n if (errors.length > 0) {\n return { errors, elements: [], relationships: [] };\n }\n\n // `parsedModel`, not `computedModel`: the computed stage additionally\n // computes every VIEW the document declares (and a default one when it\n // declares none), which loam never reads — it renders nothing. That work is\n // superlinear in the number of RELATIONSHIPS, so a landscape at fleet shape\n // turned `loam list` from under a second into minutes. The elements and\n // relationships below are identical either way.\n const model = (await likec4.parsedModel()) as ReadableModel;\n return { errors, ...flattenModel(model) };\n } finally {\n await likec4.dispose();\n }\n}\n\n/**\n * Read one string key out of a LikeC4 `metadata { ... }` block. Two keys carry\n * loam's spines: `op` on a relationship (the OpenAPI operationId it calls) and\n * `service` on an element (the services/<id> directory it stands for). Elements\n * with no metadata come back as `{}`, so a missing key is indistinguishable from\n * a missing block — both mean \"not bound\".\n */\nfunction metaKey(m: unknown, key: string): string | undefined {\n if (m && typeof m === \"object\") {\n const v = (m as Record<string, unknown>)[key];\n if (typeof v === \"string\") return v;\n }\n return undefined;\n}\n\n/** LikeC4 descriptions can be a string or a rich-text object ({ txt } / { text } / { md }). */\nfunction descText(d: unknown): string | undefined {\n if (typeof d === \"string\") return d;\n if (d && typeof d === \"object\") {\n const o = d as Record<string, unknown>;\n for (const key of [\"txt\", \"text\", \"md\", \"value\"]) {\n const v = o[key];\n if (typeof v === \"string\") return v;\n }\n }\n return undefined;\n}\n\n/* ------------------------------------------------------------------ */\n/* Source scanning — the text-level view the archive merge splices from */\n/* ------------------------------------------------------------------ */\n\n/** A string literal in LikeC4 source: its span in the text, and its unescaped value. */\nexport interface SourceLiteral {\n start: number;\n end: number;\n value: string;\n}\n\n/**\n * A LikeC4 source with everything that is not code blanked out: string\n * interiors and comments become spaces, newlines survive, and the result is\n * exactly as long as the input — so structure (braces, arrows, `#tags`) can be\n * read with plain string operations at offsets that are valid in the original.\n */\nexport interface MaskedSource {\n code: string;\n literals: SourceLiteral[];\n}\n\nexport function maskSource(text: string): MaskedSource {\n const out = text.split(\"\");\n const literals: SourceLiteral[] = [];\n type State = \"code\" | \"squote\" | \"dquote\" | \"lineComment\" | \"blockComment\";\n let state: State = \"code\";\n let litStart = -1;\n let lit = \"\";\n for (let i = 0; i < text.length; i += 1) {\n const ch = text[i]!;\n const next = text[i + 1];\n switch (state) {\n case \"code\":\n if (ch === \"'\" || ch === '\"') {\n state = ch === \"'\" ? \"squote\" : \"dquote\";\n litStart = i;\n lit = \"\";\n out[i] = \" \";\n } else if (ch === \"/\" && next === \"/\") {\n state = \"lineComment\";\n out[i] = \" \";\n } else if (ch === \"/\" && next === \"*\") {\n state = \"blockComment\";\n out[i] = \" \";\n }\n break;\n case \"squote\":\n case \"dquote\":\n if (ch === \"\\\\\" && i + 1 < text.length) {\n lit += text[i + 1]!;\n out[i] = \" \";\n out[i + 1] = \" \";\n i += 1;\n } else if (ch === (state === \"squote\" ? \"'\" : '\"')) {\n literals.push({ start: litStart, end: i + 1, value: lit });\n state = \"code\";\n out[i] = \" \";\n } else {\n lit += ch;\n if (ch !== \"\\n\") out[i] = \" \";\n }\n break;\n case \"lineComment\":\n if (ch === \"\\n\") state = \"code\";\n else out[i] = \" \";\n break;\n case \"blockComment\":\n if (ch === \"*\" && next === \"/\") {\n state = \"code\";\n out[i] = \" \";\n out[i + 1] = \" \";\n i += 1;\n } else if (ch !== \"\\n\") {\n out[i] = \" \";\n }\n break;\n }\n }\n return { code: out.join(\"\"), literals };\n}\n\n/** Index of the `}` matching the `{` at `open`, over MASKED code; -1 when unbalanced. */\nexport function matchBrace(code: string, open: number): number {\n let depth = 0;\n for (let i = open; i < code.length; i += 1) {\n if (code[i] === \"{\") depth += 1;\n else if (code[i] === \"}\") {\n depth -= 1;\n if (depth === 0) return i;\n }\n }\n return -1;\n}\n\n/** An element declaration located in source, under the id the computed model gives it. */\nexport interface ScannedElement {\n /** Full dotted id — `parent.child` for a nested declaration. */\n id: string;\n /** Span of the whole declaration: first head character to one past its end. */\n start: number;\n end: number;\n /** Its own body braces; -1 when the declaration has no body. */\n bodyOpen: number;\n bodyClose: number;\n /** Leading whitespace of the declaration's first line. */\n indent: string;\n}\n\n/** A relationship statement located in source, endpoints resolved to full ids (best effort). */\nexport interface ScannedRel {\n source: string;\n target: string;\n title?: string;\n op?: string;\n tags: string[];\n start: number;\n end: number;\n indent: string;\n}\n\nexport interface ScannedModel {\n /** The `model` block's own braces. */\n open: number;\n close: number;\n /** In document order. A nested declaration's span lies inside its parent's. */\n elements: ScannedElement[];\n rels: ScannedRel[];\n}\n\n/**\n * Locate every element and relationship declaration inside the top-level\n * `model { ... }` block of a LikeC4 source, with the byte range each one\n * occupies. This is the splice map `loam archive` merges the landscape with:\n * the parsed model says WHAT exists, this says WHERE its authored text is —\n * verbatim, so nothing the parser does not model (technology, style, links)\n * can be lost by rebuilding it.\n *\n * Both declaration forms are recognised — `name = kind ...` always, and\n * `kind name ...` for any kind the document's own `specification` block\n * declares. Relationship endpoints are resolved the way LikeC4 reads them:\n * innermost enclosing scope outward, then the top level, then a unique leaf\n * name anywhere; `it`/`this` (and an omitted source) mean the enclosing\n * element. A name that resolves nowhere is left as written — the caller's\n * match against the parsed model then fails loudly instead of guessing.\n *\n * Returns null when there is no model block. Callers only hand in documents\n * LikeC4 has already parsed, so the scan can trust balanced braces.\n */\nexport function scanModel(text: string): ScannedModel | null {\n const { code, literals } = maskSource(text);\n const m = /\\bmodel\\s*\\{/.exec(code);\n if (!m) return null;\n const open = m.index + m[0].length - 1;\n const close = matchBrace(code, open);\n if (close === -1) return null;\n\n // Element kinds from the specification block(s): the `<kind> <name>`\n // declaration form is recognisable only by its first token being a kind.\n const kinds = new Set<string>();\n for (const sm of code.matchAll(/\\bspecification\\s*\\{/g)) {\n const sClose = matchBrace(code, sm.index + sm[0].length - 1);\n if (sClose === -1) continue;\n for (const em of code.slice(sm.index, sClose).matchAll(/\\belement\\s+([A-Za-z_][\\w-]*)/g)) {\n kinds.add(em[1]!);\n }\n }\n\n const indentOf = (at: number): string => {\n const nl = text.lastIndexOf(\"\\n\", at - 1);\n const ws = text.slice(nl + 1, at);\n return /^[ \\t]*$/.test(ws) ? ws : \"\";\n };\n const literalIn = (from: number, to: number): SourceLiteral | undefined =>\n literals.find((l) => l.start >= from && l.end <= to);\n const keyedLiteral = (key: string, from: number, to: number): string | undefined => {\n if (from === -1) return undefined;\n const re = new RegExp(`\\\\b${key}\\\\b`, \"g\");\n re.lastIndex = from;\n const km = re.exec(code);\n if (km === null || km.index >= to) return undefined;\n return literalIn(km.index, to)?.value;\n };\n const trimmedEnd = (from: number, to: number): number => {\n let e = to;\n while (e > from && /\\s/.test(text[e - 1]!)) e -= 1;\n return e;\n };\n\n const elements: ScannedElement[] = [];\n interface RawRel {\n srcLit?: string;\n tgtLit: string;\n parent: string;\n start: number;\n end: number;\n indent: string;\n title?: string;\n op?: string;\n tags: string[];\n }\n const raw: RawRel[] = [];\n const stack: Array<{ id: string; el: ScannedElement }> = [];\n\n let i = open + 1;\n while (i < close) {\n const ch = code[i]!;\n if (/\\s/.test(ch)) {\n i += 1;\n continue;\n }\n if (ch === \"}\") {\n const frame = stack.pop();\n if (frame !== undefined) {\n frame.el.bodyClose = i;\n frame.el.end = i + 1;\n elements.push(frame.el);\n }\n i += 1;\n continue;\n }\n\n // A statement's head: up to its own `{`, the end of the line, or the\n // enclosing block's `}` on the same line — whichever comes first.\n const stmtStart = i;\n let headEnd = close;\n let opener = -1;\n for (let j = i; j < close; j += 1) {\n const c = code[j]!;\n if (c === \"\\n\" || c === \"}\") {\n headEnd = j;\n break;\n }\n if (c === \"{\") {\n opener = j;\n headEnd = j;\n break;\n }\n }\n const head = code.slice(stmtStart, headEnd);\n const parentId = stack.length > 0 ? stack[stack.length - 1]!.id : \"\";\n\n // Classify: `name = kind` first, then a relationship (the arrow cannot hide\n // in a string — the head is masked), then the `kind name` form, else a\n // property/tag line that is consumed whole, nested block included.\n let localName: string | null = null;\n const assign = /^([A-Za-z_]\\w*)\\s*=\\s*[A-Za-z_]/.exec(head);\n if (assign !== null) {\n localName = assign[1]!;\n } else if (!head.includes(\"->\")) {\n const toks = head.trim().split(/\\s+/);\n if (toks.length >= 2 && kinds.has(toks[0]!) && /^[A-Za-z_]\\w*$/.test(toks[1]!)) {\n localName = toks[1]!;\n }\n }\n\n if (localName !== null) {\n const id = parentId === \"\" ? localName : `${parentId}.${localName}`;\n const el: ScannedElement = {\n id,\n start: stmtStart,\n end: headEnd,\n bodyOpen: opener,\n bodyClose: -1,\n indent: indentOf(stmtStart),\n };\n if (opener !== -1) {\n stack.push({ id, el });\n i = opener + 1;\n } else {\n el.end = trimmedEnd(stmtStart, headEnd);\n elements.push(el);\n i = headEnd;\n }\n continue;\n }\n\n if (head.includes(\"->\")) {\n const bClose = opener === -1 ? -1 : matchBrace(code, opener);\n const stmtEnd = opener === -1 ? trimmedEnd(stmtStart, headEnd) : bClose === -1 ? close : bClose + 1;\n const am = /^([A-Za-z_][\\w.]*)?\\s*(?:-\\[[^\\]]*\\])?->\\s*([A-Za-z_][\\w.]*)/.exec(head);\n if (am !== null) {\n const arrowEnd = stmtStart + am[0].length;\n const bodyFrom = opener === -1 ? -1 : opener + 1;\n const bodyTo = opener === -1 ? -1 : stmtEnd - 1;\n const title = literalIn(arrowEnd, headEnd)?.value ?? keyedLiteral(\"title\", bodyFrom, bodyTo);\n const tags: string[] = [];\n for (const tm of code.slice(stmtStart, stmtEnd).matchAll(/#([\\w-]+)/g)) tags.push(tm[1]!);\n raw.push({\n srcLit: am[1],\n tgtLit: am[2]!,\n parent: parentId,\n start: stmtStart,\n end: stmtEnd,\n indent: indentOf(stmtStart),\n title,\n op: keyedLiteral(\"op\", bodyFrom, bodyTo),\n tags,\n });\n }\n i = Math.max(stmtEnd, headEnd);\n continue;\n }\n\n if (opener !== -1) {\n const bClose = matchBrace(code, opener);\n i = bClose === -1 ? close : bClose + 1;\n } else {\n i = headEnd;\n }\n }\n\n // Resolve relationship endpoints against everything declared anywhere in the\n // model — LikeC4 resolution is not order-dependent, so neither is this.\n const ids = new Set(elements.map((e) => e.id));\n const byLeaf = new Map<string, string[]>();\n for (const e of elements) {\n const leaf = e.id.slice(e.id.lastIndexOf(\".\") + 1);\n byLeaf.set(leaf, [...(byLeaf.get(leaf) ?? []), e.id]);\n }\n const resolve = (lit: string | undefined, enclosing: string): string => {\n if (lit === undefined || lit === \"it\" || lit === \"this\") return enclosing;\n const segs = lit.split(\".\");\n let base: string | undefined;\n for (let scope = enclosing; scope !== \"\" && base === undefined; ) {\n if (ids.has(`${scope}.${segs[0]!}`)) base = `${scope}.${segs[0]!}`;\n const dot = scope.lastIndexOf(\".\");\n scope = dot === -1 ? \"\" : scope.slice(0, dot);\n }\n if (base === undefined && ids.has(segs[0]!)) base = segs[0]!;\n if (base === undefined) {\n const leafed = byLeaf.get(segs[0]!);\n if (leafed !== undefined && leafed.length === 1) base = leafed[0]!;\n }\n return [base ?? segs[0]!, ...segs.slice(1)].join(\".\");\n };\n\n const rels: ScannedRel[] = raw.map((r) => ({\n source: resolve(r.srcLit, r.parent),\n target: resolve(r.tgtLit, r.parent),\n title: r.title,\n op: r.op,\n tags: r.tags,\n start: r.start,\n end: r.end,\n indent: r.indent,\n }));\n\n elements.sort((a, b) => a.start - b.start);\n return { open, close, elements, rels };\n}\n"]}
@@ -0,0 +1,66 @@
1
+ /** The ladder, in order. Each rung stands on every rung below it. */
2
+ export const MATURITY_LADDER = ["empty", "partial", "documented", "sourced", "vouched"];
3
+ /**
4
+ * How far a service's documentation has got. Derived from artifact PRESENCE and
5
+ * provenance state only — the data an enumeration already holds — never from
6
+ * what the artifacts say. COMPLETENESS of adopted docs is explicitly on the
7
+ * unchecked list (brief.ts): a service with one endpoint documented out of
8
+ * thirty climbs this ladder exactly as fast as a thorough one, which is why no
9
+ * rung is called "adopted".
10
+ *
11
+ * empty services/<id>/ exists, no artifact is in it
12
+ * partial some artifacts, but not the required set
13
+ * documented the artifacts the adopt brief marks required are present
14
+ * sourced the living spec declares `sources` — something ties it to code
15
+ * vouched status: verified with a sources_digest behind it — a person
16
+ * stamped it. `verified` with no digest is a claim with nothing
17
+ * behind it and stays below this rung.
18
+ *
19
+ * `openapi.yaml` is required ONLY where an API is expected. The rung used to
20
+ * demand it of everything, which permanently pinned every worker, cron and
21
+ * consumer in the fleet at `partial` — services that are fully documented and
22
+ * vouched for, reported as unfinished forever, and a rollup that therefore said
23
+ * a correctly-adopted fleet was half-done. The evidence is the same one
24
+ * `validate` uses to keep `service.no-openapi` quiet, so a service cannot be
25
+ * green in one command and unfinished in the other.
26
+ */
27
+ export function serviceMaturity(v) {
28
+ const s = v.entry;
29
+ if (!Object.values(s.has).some(Boolean) && !v.archSpec && s.adrs === 0)
30
+ return "empty";
31
+ if (!(s.has.model && s.has.spec && (!v.apiExpected || s.has.openapi)))
32
+ return "partial";
33
+ if (!s.sources.declared)
34
+ return "documented";
35
+ if (!(s.status === "verified" && s.sources.stamped))
36
+ return "sourced";
37
+ return "vouched";
38
+ }
39
+ /**
40
+ * What stands between this service and `vouched`, named as the thing to go and
41
+ * do. One rung's worth at a time: telling somebody at `empty` that they also
42
+ * need a vouch is noise, and the vouch is not even possible yet.
43
+ */
44
+ export function maturityGaps(v) {
45
+ const s = v.entry;
46
+ const files = [
47
+ ...(s.has.model ? [] : ["model.likec4"]),
48
+ ...(s.has.spec ? [] : ["spec.md"]),
49
+ ...(v.apiExpected && !s.has.openapi ? ["openapi.yaml"] : []),
50
+ ];
51
+ if (files.length > 0)
52
+ return files;
53
+ if (!s.sources.declared)
54
+ return ["sources: in the spec.md frontmatter"];
55
+ if (!(s.status === "verified" && s.sources.stamped))
56
+ return [`\`loam vouch --service ${s.id}\``];
57
+ return [];
58
+ }
59
+ /** Counts per rung, every rung present — a stable shape a fleet dashboard can diff. */
60
+ export function maturityRollup(graded) {
61
+ const out = Object.fromEntries(MATURITY_LADDER.map((m) => [m, 0]));
62
+ for (const v of graded)
63
+ out[v.maturity] += 1;
64
+ return out;
65
+ }
66
+ //# sourceMappingURL=maturity.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"maturity.js","sourceRoot":"","sources":["../../src/core/maturity.ts"],"names":[],"mappings":"AAeA,qEAAqE;AACrE,MAAM,CAAC,MAAM,eAAe,GAAG,CAAC,OAAO,EAAE,SAAS,EAAE,YAAY,EAAE,SAAS,EAAE,SAAS,CAAU,CAAC;AAoBjG;;;;;;;;;;;;;;;;;;;;;;;GAuBG;AACH,MAAM,UAAU,eAAe,CAAC,CAAgB;IAC9C,MAAM,CAAC,GAAG,CAAC,CAAC,KAAK,CAAC;IAClB,IAAI,CAAC,MAAM,CAAC,MAAM,CAAC,CAAC,CAAC,GAAG,CAAC,CAAC,IAAI,CAAC,OAAO,CAAC,IAAI,CAAC,CAAC,CAAC,QAAQ,IAAI,CAAC,CAAC,IAAI,KAAK,CAAC;QAAE,OAAO,OAAO,CAAC;IACvF,IAAI,CAAC,CAAC,CAAC,CAAC,GAAG,CAAC,KAAK,IAAI,CAAC,CAAC,GAAG,CAAC,IAAI,IAAI,CAAC,CAAC,CAAC,CAAC,WAAW,IAAI,CAAC,CAAC,GAAG,CAAC,OAAO,CAAC,CAAC;QAAE,OAAO,SAAS,CAAC;IACxF,IAAI,CAAC,CAAC,CAAC,OAAO,CAAC,QAAQ;QAAE,OAAO,YAAY,CAAC;IAC7C,IAAI,CAAC,CAAC,CAAC,CAAC,MAAM,KAAK,UAAU,IAAI,CAAC,CAAC,OAAO,CAAC,OAAO,CAAC;QAAE,OAAO,SAAS,CAAC;IACtE,OAAO,SAAS,CAAC;AACnB,CAAC;AAED;;;;GAIG;AACH,MAAM,UAAU,YAAY,CAAC,CAAgB;IAC3C,MAAM,CAAC,GAAG,CAAC,CAAC,KAAK,CAAC;IAClB,MAAM,KAAK,GAAG;QACZ,GAAG,CAAC,CAAC,CAAC,GAAG,CAAC,KAAK,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,CAAC,cAAc,CAAC,CAAC;QACxC,GAAG,CAAC,CAAC,CAAC,GAAG,CAAC,IAAI,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,CAAC,SAAS,CAAC,CAAC;QAClC,GAAG,CAAC,CAAC,CAAC,WAAW,IAAI,CAAC,CAAC,CAAC,GAAG,CAAC,OAAO,CAAC,CAAC,CAAC,CAAC,cAAc,CAAC,CAAC,CAAC,CAAC,EAAE,CAAC;KAC7D,CAAC;IACF,IAAI,KAAK,CAAC,MAAM,GAAG,CAAC;QAAE,OAAO,KAAK,CAAC;IACnC,IAAI,CAAC,CAAC,CAAC,OAAO,CAAC,QAAQ;QAAE,OAAO,CAAC,qCAAqC,CAAC,CAAC;IACxE,IAAI,CAAC,CAAC,CAAC,CAAC,MAAM,KAAK,UAAU,IAAI,CAAC,CAAC,OAAO,CAAC,OAAO,CAAC;QAAE,OAAO,CAAC,0BAA0B,CAAC,CAAC,EAAE,IAAI,CAAC,CAAC;IACjG,OAAO,EAAE,CAAC;AACZ,CAAC;AAED,uFAAuF;AACvF,MAAM,UAAU,cAAc,CAAC,MAAyC;IACtE,MAAM,GAAG,GAAG,MAAM,CAAC,WAAW,CAAC,eAAe,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,CAA6B,CAAC;IAC/F,KAAK,MAAM,CAAC,IAAI,MAAM;QAAE,GAAG,CAAC,CAAC,CAAC,QAAQ,CAAC,IAAI,CAAC,CAAC;IAC7C,OAAO,GAAG,CAAC;AACb,CAAC","sourcesContent":["/**\n * The adoption-maturity ladder: one monotone word for how far a service's\n * documentation has got, and what stands between it and the next rung.\n *\n * This lived inside `commands/list.ts` while `list` was the only caller. It\n * moved here when `explore` needed the same answer, because the alternative is\n * the failure `core/ids.ts` already records for the service-id grammar: a\n * second, subtly different copy, so two commands grade the same directory\n * differently and nobody can tell which one is lying. The rung is a fleet dial\n * — `list` prints it per service and in a rollup, `explore` prints it for a\n * service somebody is about to build against — and a dial with two readings is\n * not a dial.\n */\nimport type { ServiceEntry } from \"./repo.js\";\n\n/** The ladder, in order. Each rung stands on every rung below it. */\nexport const MATURITY_LADDER = [\"empty\", \"partial\", \"documented\", \"sourced\", \"vouched\"] as const;\nexport type Maturity = (typeof MATURITY_LADDER)[number];\n\n/**\n * A service entry plus the two facts the directory enumeration cannot know on\n * its own. Both change what a rung MEANS, and both are fleet-level: they need\n * the landscape, not `services/<id>/`.\n */\nexport interface MaturityInput {\n entry: ServiceEntry;\n /** `arch.spec.md` — the architecture-obligations axis, absent from ServiceEntry.has. */\n archSpec: boolean;\n /**\n * Does anything in the fleet call an operation on this service? True unless\n * the landscape PROVES otherwise, exactly as `validate`'s no-openapi grace\n * reads it: a missing or unparseable landscape proves nothing.\n */\n apiExpected: boolean;\n}\n\n/**\n * How far a service's documentation has got. Derived from artifact PRESENCE and\n * provenance state only — the data an enumeration already holds — never from\n * what the artifacts say. COMPLETENESS of adopted docs is explicitly on the\n * unchecked list (brief.ts): a service with one endpoint documented out of\n * thirty climbs this ladder exactly as fast as a thorough one, which is why no\n * rung is called \"adopted\".\n *\n * empty services/<id>/ exists, no artifact is in it\n * partial some artifacts, but not the required set\n * documented the artifacts the adopt brief marks required are present\n * sourced the living spec declares `sources` — something ties it to code\n * vouched status: verified with a sources_digest behind it — a person\n * stamped it. `verified` with no digest is a claim with nothing\n * behind it and stays below this rung.\n *\n * `openapi.yaml` is required ONLY where an API is expected. The rung used to\n * demand it of everything, which permanently pinned every worker, cron and\n * consumer in the fleet at `partial` — services that are fully documented and\n * vouched for, reported as unfinished forever, and a rollup that therefore said\n * a correctly-adopted fleet was half-done. The evidence is the same one\n * `validate` uses to keep `service.no-openapi` quiet, so a service cannot be\n * green in one command and unfinished in the other.\n */\nexport function serviceMaturity(v: MaturityInput): Maturity {\n const s = v.entry;\n if (!Object.values(s.has).some(Boolean) && !v.archSpec && s.adrs === 0) return \"empty\";\n if (!(s.has.model && s.has.spec && (!v.apiExpected || s.has.openapi))) return \"partial\";\n if (!s.sources.declared) return \"documented\";\n if (!(s.status === \"verified\" && s.sources.stamped)) return \"sourced\";\n return \"vouched\";\n}\n\n/**\n * What stands between this service and `vouched`, named as the thing to go and\n * do. One rung's worth at a time: telling somebody at `empty` that they also\n * need a vouch is noise, and the vouch is not even possible yet.\n */\nexport function maturityGaps(v: MaturityInput): string[] {\n const s = v.entry;\n const files = [\n ...(s.has.model ? [] : [\"model.likec4\"]),\n ...(s.has.spec ? [] : [\"spec.md\"]),\n ...(v.apiExpected && !s.has.openapi ? [\"openapi.yaml\"] : []),\n ];\n if (files.length > 0) return files;\n if (!s.sources.declared) return [\"sources: in the spec.md frontmatter\"];\n if (!(s.status === \"verified\" && s.sources.stamped)) return [`\\`loam vouch --service ${s.id}\\``];\n return [];\n}\n\n/** Counts per rung, every rung present — a stable shape a fleet dashboard can diff. */\nexport function maturityRollup(graded: readonly { maturity: Maturity }[]): Record<Maturity, number> {\n const out = Object.fromEntries(MATURITY_LADDER.map((m) => [m, 0])) as Record<Maturity, number>;\n for (const v of graded) out[v.maturity] += 1;\n return out;\n}\n"]}