@ybotok/loam 0.0.0 → 0.1.0-beta.1

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 (106) hide show
  1. package/CHANGELOG.md +89 -0
  2. package/COMPARISON.md +63 -0
  3. package/LICENSE +21 -0
  4. package/MIGRATING-from-OpenSpec.md +202 -0
  5. package/README.md +241 -9
  6. package/SCHEMA.md +425 -0
  7. package/SECURITY.md +9 -0
  8. package/dist/cli.js +138 -0
  9. package/dist/cli.js.map +1 -0
  10. package/dist/commands/adopt.js +226 -0
  11. package/dist/commands/adopt.js.map +1 -0
  12. package/dist/commands/archive.js +1297 -0
  13. package/dist/commands/archive.js.map +1 -0
  14. package/dist/commands/delta.js +361 -0
  15. package/dist/commands/delta.js.map +1 -0
  16. package/dist/commands/dependencies.js +78 -0
  17. package/dist/commands/dependencies.js.map +1 -0
  18. package/dist/commands/doctor.js +85 -0
  19. package/dist/commands/doctor.js.map +1 -0
  20. package/dist/commands/gherkin.js +270 -0
  21. package/dist/commands/gherkin.js.map +1 -0
  22. package/dist/commands/init.js +250 -0
  23. package/dist/commands/init.js.map +1 -0
  24. package/dist/commands/list.js +361 -0
  25. package/dist/commands/list.js.map +1 -0
  26. package/dist/commands/migrate-openspec.js +933 -0
  27. package/dist/commands/migrate-openspec.js.map +1 -0
  28. package/dist/commands/new.js +412 -0
  29. package/dist/commands/new.js.map +1 -0
  30. package/dist/commands/rebase.js +358 -0
  31. package/dist/commands/rebase.js.map +1 -0
  32. package/dist/commands/show.js +325 -0
  33. package/dist/commands/show.js.map +1 -0
  34. package/dist/commands/status.js +270 -0
  35. package/dist/commands/status.js.map +1 -0
  36. package/dist/commands/unarchive.js +310 -0
  37. package/dist/commands/unarchive.js.map +1 -0
  38. package/dist/commands/validate.js +1609 -0
  39. package/dist/commands/validate.js.map +1 -0
  40. package/dist/commands/verify.js +759 -0
  41. package/dist/commands/verify.js.map +1 -0
  42. package/dist/commands/vouch.js +325 -0
  43. package/dist/commands/vouch.js.map +1 -0
  44. package/dist/core/agent.js +1823 -0
  45. package/dist/core/agent.js.map +1 -0
  46. package/dist/core/agents-stamp.js +68 -0
  47. package/dist/core/agents-stamp.js.map +1 -0
  48. package/dist/core/arch.js +104 -0
  49. package/dist/core/arch.js.map +1 -0
  50. package/dist/core/brief.js +733 -0
  51. package/dist/core/brief.js.map +1 -0
  52. package/dist/core/coherence.js +553 -0
  53. package/dist/core/coherence.js.map +1 -0
  54. package/dist/core/config.js +172 -0
  55. package/dist/core/config.js.map +1 -0
  56. package/dist/core/delta.js +457 -0
  57. package/dist/core/delta.js.map +1 -0
  58. package/dist/core/dependencies.js +352 -0
  59. package/dist/core/dependencies.js.map +1 -0
  60. package/dist/core/docs.js +143 -0
  61. package/dist/core/docs.js.map +1 -0
  62. package/dist/core/doctor.js +544 -0
  63. package/dist/core/doctor.js.map +1 -0
  64. package/dist/core/fleet-context.js +272 -0
  65. package/dist/core/fleet-context.js.map +1 -0
  66. package/dist/core/frontmatter.js +203 -0
  67. package/dist/core/frontmatter.js.map +1 -0
  68. package/dist/core/gherkin.js +521 -0
  69. package/dist/core/gherkin.js.map +1 -0
  70. package/dist/core/health.js +71 -0
  71. package/dist/core/health.js.map +1 -0
  72. package/dist/core/ids.js +93 -0
  73. package/dist/core/ids.js.map +1 -0
  74. package/dist/core/issue.js +9 -0
  75. package/dist/core/issue.js.map +1 -0
  76. package/dist/core/json.js +67 -0
  77. package/dist/core/json.js.map +1 -0
  78. package/dist/core/likec4.js +474 -0
  79. package/dist/core/likec4.js.map +1 -0
  80. package/dist/core/openapi-merge.js +519 -0
  81. package/dist/core/openapi-merge.js.map +1 -0
  82. package/dist/core/openapi.js +243 -0
  83. package/dist/core/openapi.js.map +1 -0
  84. package/dist/core/openspec-inventory.js +1403 -0
  85. package/dist/core/openspec-inventory.js.map +1 -0
  86. package/dist/core/path-safety.js +109 -0
  87. package/dist/core/path-safety.js.map +1 -0
  88. package/dist/core/provenance.js +695 -0
  89. package/dist/core/provenance.js.map +1 -0
  90. package/dist/core/repo.js +362 -0
  91. package/dist/core/repo.js.map +1 -0
  92. package/dist/core/report.js +38 -0
  93. package/dist/core/report.js.map +1 -0
  94. package/dist/core/results.js +201 -0
  95. package/dist/core/results.js.map +1 -0
  96. package/dist/core/spec.js +481 -0
  97. package/dist/core/spec.js.map +1 -0
  98. package/dist/core/staging.js +783 -0
  99. package/dist/core/staging.js.map +1 -0
  100. package/dist/core/status.js +1015 -0
  101. package/dist/core/status.js.map +1 -0
  102. package/dist/core/verify.js +793 -0
  103. package/dist/core/verify.js.map +1 -0
  104. package/dist/core/version.js +8 -0
  105. package/dist/core/version.js.map +1 -0
  106. package/package.json +63 -4
@@ -0,0 +1,361 @@
1
+ import { existsSync } from "node:fs";
2
+ import { readFile } from "node:fs/promises";
3
+ import { parse as parseYaml } from "yaml";
4
+ import { loadConfig } from "../core/config.js";
5
+ import { InvalidIdError, assertServiceId } from "../core/ids.js";
6
+ import { emitJson, emitJsonError, fail, repoPath, reportNoConfig } from "../core/json.js";
7
+ import { elementService, loadFile, serviceOf } from "../core/likec4.js";
8
+ import { operations } from "../core/openapi.js";
9
+ import { DocsRepoUnavailableError, compareIds, featurePaths, featureSpecPaths, listServices, missingFeatureMessage, resolveFeature, } from "../core/repo.js";
10
+ import { parseRequirements } from "../core/spec.js";
11
+ export function registerDelta(program) {
12
+ program
13
+ .command("delta")
14
+ .argument("<featureId>", "feature id, e.g. FEAT-101")
15
+ .description("Project a feature onto a service: why + requirement delta + C4 changes")
16
+ .option("--service <id>", "service to project onto (defaults to the configured service)")
17
+ .option("--json", "emit the machine contract instead of the human view")
18
+ .action(async (featureId, opts) => {
19
+ const json = opts.json === true;
20
+ const config = await loadConfig();
21
+ if (!config) {
22
+ reportNoConfig(json);
23
+ return;
24
+ }
25
+ // The id grammar first, and on the RAW argument: `--service ../../etc`
26
+ // reaches `featureSpecPaths` and reads from outside the feature directory.
27
+ // One grammar for the whole tool (core/ids.ts).
28
+ if (opts.service !== undefined) {
29
+ try {
30
+ assertServiceId(opts.service, "--service");
31
+ }
32
+ catch (err) {
33
+ if (!(err instanceof InvalidIdError))
34
+ throw err;
35
+ fail(json, "invalid-option", err.message);
36
+ return;
37
+ }
38
+ }
39
+ // The feature is resolved BEFORE the service is chosen, because every
40
+ // sentence about the service ("which ones does this feature touch?") is
41
+ // only answerable once the feature is known.
42
+ const feature = await resolveFeature(config.docsDir, featureId, "exclude");
43
+ if (!feature) {
44
+ fail(json, "unknown-target", await missingFeatureMessage(config.docsDir, featureId));
45
+ return;
46
+ }
47
+ // Canonical id from here on — the delta's tags carry `#FEAT-5`, so a raw
48
+ // `FEAT-5-slug` argument used to empty the architecture slice silently.
49
+ const { id } = feature;
50
+ const paths = featurePaths(feature.dir);
51
+ const deltaDoc = existsSync(paths.delta) ? await loadFile(paths.delta) : null;
52
+ // Services the feature speaks about at all: one with a requirement delta,
53
+ // or one its C4 delta introduces. This is the list a refusal quotes.
54
+ const featureServices = [
55
+ ...new Set([...feature.services, ...introducedServices(deltaDoc, id)]),
56
+ ].sort(compareIds);
57
+ const service = opts.service ?? config.service;
58
+ if (!service) {
59
+ // The refusal NAMES the choices. "No service. Pass --service <id>" told
60
+ // the reader the shape of the flag and nothing about the answer, so the
61
+ // next step was always to go list the feature's specs/ by hand — and an
62
+ // agent had no way to recover at all. The list rides the JSON envelope
63
+ // for the same reason.
64
+ const message = `No service to project ${id} onto. Pass --service <id> or set 'service' in loam.json.` +
65
+ (featureServices.length === 0
66
+ ? ` ${id} carries no per-service delta yet.`
67
+ : ` ${id} touches: ${featureServices.join(", ")}.`);
68
+ if (json)
69
+ emitJsonError("invalid-option", message, { feature: id, services: featureServices });
70
+ else {
71
+ console.error(message);
72
+ process.exitCode = 1;
73
+ }
74
+ return;
75
+ }
76
+ // A service nobody has heard of produces a perfectly plausible EMPTY
77
+ // report — no requirements, no edges, "no architecture change" — which an
78
+ // agent consuming this as a task brief reads as "nothing to do here".
79
+ // A typo must never be indistinguishable from a finished service.
80
+ const living = await livingServices(config.docsDir);
81
+ if (!featureServices.includes(service) && !living.includes(service)) {
82
+ const near = nearestIds(service, [...new Set([...featureServices, ...living])]);
83
+ fail(json, "unknown-target", `No service '${service}': it is neither a services/${service}/ in the docs repo nor a service ${id} touches` +
84
+ (featureServices.length === 0 ? "" : ` (${featureServices.join(", ")})`) +
85
+ (near.length === 0 ? "." : ` — did you mean ${near.map((n) => `'${n}'`).join(" or ")}?`));
86
+ return;
87
+ }
88
+ // Why — business intent
89
+ const intent = existsSync(paths.intent)
90
+ ? stripFrontmatter(await readFile(paths.intent, "utf8")).trim()
91
+ : null;
92
+ // Requirement delta for this service (OpenSpec style), business and arch:
93
+ // the same projection covers both axes, in the same shape, so the payload
94
+ // stays one task — the arch requirements are the integration/ops half the
95
+ // business ones never mention.
96
+ const specPaths = featureSpecPaths(feature.dir, service);
97
+ const reqs = existsSync(specPaths.spec)
98
+ ? parseRequirements(await readFile(specPaths.spec, "utf8"))
99
+ : [];
100
+ const archReqs = existsSync(specPaths.archSpec)
101
+ ? parseRequirements(await readFile(specPaths.archSpec, "utf8"))
102
+ : [];
103
+ // The contract axis. Until now no command surfaced it at all: the feature's
104
+ // openapi.yaml was read by validate and by archive, and the one command
105
+ // whose output IS the implementation task never mentioned it — so "add the
106
+ // endpoint" was the half of the work the brief left out.
107
+ const api = await apiChanges(specPaths.openapi);
108
+ // C4 architecture slice
109
+ const arch = archSlice(deltaDoc, service, id);
110
+ // An unparseable delta.likec4 empties the C4 slice, and a consumer
111
+ // reading this projection as a task brief — the JSON payload and the
112
+ // printed view alike — would take that as "no architecture change": the
113
+ // vacuously-green pattern. The output stays as informative as ever (and
114
+ // `ok` stays true under --json: the command ran); the exit code is what
115
+ // stops a pipeline from building on it, so it is set BEFORE the format
116
+ // fork — the guard is about the delta, not about how it is rendered.
117
+ if (arch.errors.length > 0)
118
+ process.exitCode = 1;
119
+ if (json) {
120
+ const reqJson = (r) => ({
121
+ kind: r.kind,
122
+ id: r.id,
123
+ name: r.name,
124
+ text: r.text.join("\n").trim(),
125
+ operations: r.operations,
126
+ covers: r.covers,
127
+ // Scenarios go out verbatim: they are the acceptance criteria and the
128
+ // source for the tests whoever picks this up is expected to write.
129
+ scenarios: r.scenarios.map((s) => ({ name: s.name, lines: s.lines })),
130
+ });
131
+ emitJson({
132
+ feature: id,
133
+ service,
134
+ services: featureServices,
135
+ path: repoPath(config.docsDir, feature.dir),
136
+ intent,
137
+ requirements: reqs.map(reqJson),
138
+ // Same shape, separate section: an arch requirement's scenarios are
139
+ // integration/ops tests, and a consumer must not have to parse prose
140
+ // to tell the two apart.
141
+ archRequirements: archReqs.map(reqJson),
142
+ api,
143
+ architecture: arch,
144
+ });
145
+ return;
146
+ }
147
+ console.log(`${id} · ${service}\n`);
148
+ if (intent) {
149
+ console.log(indent(intent, " "));
150
+ console.log();
151
+ }
152
+ if (existsSync(specPaths.spec))
153
+ printRequirements(reqs, "Requirements");
154
+ else
155
+ console.log("Requirements: (none for this service)\n");
156
+ if (existsSync(specPaths.archSpec))
157
+ printRequirements(archReqs, "Arch requirements");
158
+ if (existsSync(specPaths.openapi))
159
+ printApi(api);
160
+ if (existsSync(paths.delta))
161
+ printArchSlice(arch, service);
162
+ });
163
+ }
164
+ /** Every service in the docs repo, or none when there is no docs repo to ask. */
165
+ async function livingServices(docsDir) {
166
+ try {
167
+ return (await listServices(docsDir)).map((s) => s.id);
168
+ }
169
+ catch (err) {
170
+ // "There is no docs repo at all" is a different diagnosis with its own
171
+ // command (`loam doctor`); turning it into "unknown service" here would
172
+ // send the reader after a typo that is not there.
173
+ if (!(err instanceof DocsRepoUnavailableError))
174
+ throw err;
175
+ return [];
176
+ }
177
+ }
178
+ /** Services the feature's C4 delta introduces — its own tagged top-level elements. */
179
+ function introducedServices(doc, featureId) {
180
+ if (doc === null || doc.errors.length > 0)
181
+ return [];
182
+ return doc.elements.filter((e) => e.tags.includes(featureId)).map(elementService);
183
+ }
184
+ /** Known ids within a small edit distance of `id`, closest first, at most three. */
185
+ function nearestIds(id, known) {
186
+ const budget = Math.max(1, Math.floor(id.length / 4));
187
+ return known
188
+ .map((candidate) => ({ candidate, distance: editDistance(id.toLowerCase(), candidate.toLowerCase()) }))
189
+ .filter((scored) => scored.distance <= budget)
190
+ .sort((a, b) => a.distance - b.distance || compareIds(a.candidate, b.candidate))
191
+ .slice(0, 3)
192
+ .map((scored) => scored.candidate);
193
+ }
194
+ /** Plain Levenshtein — ids are short, and the row-at-a-time form keeps it obvious. */
195
+ function editDistance(a, b) {
196
+ let previous = Array.from({ length: b.length + 1 }, (_, i) => i);
197
+ for (let i = 1; i <= a.length; i += 1) {
198
+ const current = [i];
199
+ for (let j = 1; j <= b.length; j += 1) {
200
+ const substitution = previous[j - 1] + (a[i - 1] === b[j - 1] ? 0 : 1);
201
+ current.push(Math.min(previous[j] + 1, current[j - 1] + 1, substitution));
202
+ }
203
+ previous = current;
204
+ }
205
+ return previous[b.length];
206
+ }
207
+ /**
208
+ * The feature's OpenAPI delta for one service, as slots rather than names.
209
+ *
210
+ * The operation set comes from core/openapi.ts — the same structure-aware reader
211
+ * every other check uses, so this view cannot see an operation validate does not.
212
+ * `summary` is the one field that reader does not carry, and it is the sentence
213
+ * an implementer most wants, so it is looked up by the path+method the reader
214
+ * already resolved: a second read of the same document, never a second opinion
215
+ * about what an operation IS.
216
+ */
217
+ async function apiChanges(openapiPath) {
218
+ if (!existsSync(openapiPath))
219
+ return [];
220
+ const ops = await operations(openapiPath);
221
+ if (ops.length === 0)
222
+ return [];
223
+ let doc;
224
+ try {
225
+ doc = parseYaml(await readFile(openapiPath, "utf8"));
226
+ }
227
+ catch {
228
+ // Unreadable is impossible here (ops came back non-empty), but a summary is
229
+ // decoration and must never be the reason a task brief fails to print.
230
+ doc = null;
231
+ }
232
+ const summaryOf = (op) => {
233
+ const paths = doc?.paths;
234
+ const item = paths?.[op.path];
235
+ const entry = item?.[op.method];
236
+ const summary = entry?.["summary"];
237
+ return typeof summary === "string" && summary.length > 0 ? summary : null;
238
+ };
239
+ return ops
240
+ .map((op) => ({
241
+ path: op.path,
242
+ method: op.method.toUpperCase(),
243
+ operationId: op.id,
244
+ summary: summaryOf(op),
245
+ remove: op.remove,
246
+ }))
247
+ .sort((a, b) => compareIds(a.path, b.path) || compareIds(a.method, b.method));
248
+ }
249
+ /** The feature's tagged edges around one service, plus whether the service is new. */
250
+ function archSlice(doc, service, featureId) {
251
+ const empty = { isNew: false, inbound: [], outbound: [], errors: [] };
252
+ if (doc === null)
253
+ return empty;
254
+ const { errors, elements, relationships } = doc;
255
+ if (errors.length > 0) {
256
+ return { ...empty, errors: errors.map((e) => (typeof e.line === "number" ? `L${e.line}: ${e.message}` : e.message)) };
257
+ }
258
+ // Which service an element stands for is the binding's call, not the title's.
259
+ const svcOf = (id) => serviceOf(elements, id);
260
+ const edge = (r, other) => ({
261
+ service: other,
262
+ op: r.op ?? null,
263
+ title: r.title ?? null,
264
+ });
265
+ const featRels = relationships.filter((r) => r.tags.includes(featureId));
266
+ return {
267
+ isNew: elements.some((e) => elementService(e) === service && e.tags.includes(featureId)),
268
+ inbound: featRels.filter((r) => svcOf(r.target) === service).map((r) => edge(r, svcOf(r.source))),
269
+ outbound: featRels.filter((r) => svcOf(r.source) === service).map((r) => edge(r, svcOf(r.target))),
270
+ errors: [],
271
+ };
272
+ }
273
+ function stripFrontmatter(md) {
274
+ if (!md.startsWith("---"))
275
+ return md;
276
+ const close = md.indexOf("\n---", 3);
277
+ if (close === -1)
278
+ return md;
279
+ const nl = md.indexOf("\n", close + 1);
280
+ return nl === -1 ? "" : md.slice(nl + 1).trimStart();
281
+ }
282
+ /**
283
+ * The requirement delta, in full.
284
+ *
285
+ * The text view used to print headings and scenario NAMES only, which made it a
286
+ * table of contents for a file the reader then had to open — while `--json`
287
+ * carried the requirement body and the Given/When/Then lines verbatim. The two
288
+ * are the same briefing, so they carry the same content: a person reading this
289
+ * in a terminal is being asked to implement it, exactly like the agent reading
290
+ * the payload.
291
+ */
292
+ function printRequirements(reqs, label) {
293
+ if (reqs.length === 0) {
294
+ console.log(`${label}: (none)\n`);
295
+ return;
296
+ }
297
+ console.log(`${label}:`);
298
+ for (const r of reqs) {
299
+ const tag = r.kind === "BASE" ? "" : `[${r.kind}] `;
300
+ const n = r.scenarios.length;
301
+ console.log(` ${tag}${r.name} (${n} scenario${n === 1 ? "" : "s"})`);
302
+ const body = r.text.join("\n").trim();
303
+ if (body.length > 0)
304
+ console.log(indent(body, " "));
305
+ if (r.operations.length > 0)
306
+ console.log(` Operations: ${r.operations.join(", ")}`);
307
+ if (r.covers.length > 0)
308
+ console.log(` Covers: ${r.covers.join(", ")}`);
309
+ for (const s of r.scenarios) {
310
+ console.log(`\n Scenario: ${s.name}`);
311
+ // Verbatim, bullets and markdown emphasis included: these lines are the
312
+ // acceptance criteria, and paraphrasing them here would put a second
313
+ // wording of the contract in front of whoever implements it.
314
+ for (const line of s.lines)
315
+ console.log(` ${line}`);
316
+ }
317
+ console.log();
318
+ }
319
+ }
320
+ function printApi(api) {
321
+ if (api.length === 0) {
322
+ console.log("API: (the openapi delta defines no operations)\n");
323
+ return;
324
+ }
325
+ console.log("API (this feature's openapi.yaml for the service):");
326
+ for (const op of api) {
327
+ const marker = op.remove ? "REMOVE " : "";
328
+ console.log(` ${marker}${op.method} ${op.path} ${op.operationId}${op.summary === null ? "" : ` — ${op.summary}`}`);
329
+ }
330
+ console.log();
331
+ }
332
+ function printArchSlice(arch, service) {
333
+ if (arch.errors.length > 0) {
334
+ console.log("Architecture: delta.likec4 has errors — run `loam validate`.");
335
+ return;
336
+ }
337
+ console.log("Architecture:");
338
+ if (arch.isNew)
339
+ console.log(` NEW service — create ${service}`);
340
+ if (arch.outbound.length > 0) {
341
+ console.log(" Outbound (new calls from this service):");
342
+ for (const e of arch.outbound)
343
+ console.log(` → ${e.service} "${e.title ?? ""}"`);
344
+ }
345
+ if (arch.inbound.length > 0) {
346
+ console.log(" Inbound (this service is newly called):");
347
+ for (const e of arch.inbound)
348
+ console.log(` ← ${e.service} "${e.title ?? ""}"`);
349
+ }
350
+ if (!arch.isNew && arch.outbound.length === 0 && arch.inbound.length === 0) {
351
+ console.log(" (no architecture change for this service)");
352
+ }
353
+ console.log();
354
+ }
355
+ function indent(text, pad) {
356
+ return text
357
+ .split("\n")
358
+ .map((l) => (l.length > 0 ? pad + l : l))
359
+ .join("\n");
360
+ }
361
+ //# sourceMappingURL=delta.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"delta.js","sourceRoot":"","sources":["../../src/commands/delta.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,UAAU,EAAE,MAAM,SAAS,CAAC;AACrC,OAAO,EAAE,QAAQ,EAAE,MAAM,kBAAkB,CAAC;AAC5C,OAAO,EAAE,KAAK,IAAI,SAAS,EAAE,MAAM,MAAM,CAAC;AAC1C,OAAO,EAAE,UAAU,EAAE,MAAM,mBAAmB,CAAC;AAC/C,OAAO,EAAE,cAAc,EAAE,eAAe,EAAE,MAAM,gBAAgB,CAAC;AACjE,OAAO,EAAE,QAAQ,EAAE,aAAa,EAAE,IAAI,EAAE,QAAQ,EAAE,cAAc,EAAE,MAAM,iBAAiB,CAAC;AAC1F,OAAO,EAAE,cAAc,EAAE,QAAQ,EAAE,SAAS,EAAuC,MAAM,mBAAmB,CAAC;AAC7G,OAAO,EAAE,UAAU,EAAkB,MAAM,oBAAoB,CAAC;AAChE,OAAO,EACL,wBAAwB,EACxB,UAAU,EACV,YAAY,EACZ,gBAAgB,EAChB,YAAY,EACZ,qBAAqB,EACrB,cAAc,GACf,MAAM,iBAAiB,CAAC;AACzB,OAAO,EAAE,iBAAiB,EAAoB,MAAM,iBAAiB,CAAC;AAoCtE,MAAM,UAAU,aAAa,CAAC,OAAgB;IAC5C,OAAO;SACJ,OAAO,CAAC,OAAO,CAAC;SAChB,QAAQ,CAAC,aAAa,EAAE,2BAA2B,CAAC;SACpD,WAAW,CAAC,wEAAwE,CAAC;SACrF,MAAM,CAAC,gBAAgB,EAAE,8DAA8D,CAAC;SACxF,MAAM,CAAC,QAAQ,EAAE,qDAAqD,CAAC;SACvE,MAAM,CAAC,KAAK,EAAE,SAAiB,EAAE,IAAkB,EAAE,EAAE;QACtD,MAAM,IAAI,GAAG,IAAI,CAAC,IAAI,KAAK,IAAI,CAAC;QAChC,MAAM,MAAM,GAAG,MAAM,UAAU,EAAE,CAAC;QAClC,IAAI,CAAC,MAAM,EAAE,CAAC;YACZ,cAAc,CAAC,IAAI,CAAC,CAAC;YACrB,OAAO;QACT,CAAC;QAED,uEAAuE;QACvE,2EAA2E;QAC3E,gDAAgD;QAChD,IAAI,IAAI,CAAC,OAAO,KAAK,SAAS,EAAE,CAAC;YAC/B,IAAI,CAAC;gBACH,eAAe,CAAC,IAAI,CAAC,OAAO,EAAE,WAAW,CAAC,CAAC;YAC7C,CAAC;YAAC,OAAO,GAAG,EAAE,CAAC;gBACb,IAAI,CAAC,CAAC,GAAG,YAAY,cAAc,CAAC;oBAAE,MAAM,GAAG,CAAC;gBAChD,IAAI,CAAC,IAAI,EAAE,gBAAgB,EAAE,GAAG,CAAC,OAAO,CAAC,CAAC;gBAC1C,OAAO;YACT,CAAC;QACH,CAAC;QAED,sEAAsE;QACtE,wEAAwE;QACxE,6CAA6C;QAC7C,MAAM,OAAO,GAAG,MAAM,cAAc,CAAC,MAAM,CAAC,OAAO,EAAE,SAAS,EAAE,SAAS,CAAC,CAAC;QAC3E,IAAI,CAAC,OAAO,EAAE,CAAC;YACb,IAAI,CAAC,IAAI,EAAE,gBAAgB,EAAE,MAAM,qBAAqB,CAAC,MAAM,CAAC,OAAO,EAAE,SAAS,CAAC,CAAC,CAAC;YACrF,OAAO;QACT,CAAC;QACD,yEAAyE;QACzE,wEAAwE;QACxE,MAAM,EAAE,EAAE,EAAE,GAAG,OAAO,CAAC;QACvB,MAAM,KAAK,GAAG,YAAY,CAAC,OAAO,CAAC,GAAG,CAAC,CAAC;QAExC,MAAM,QAAQ,GAAG,UAAU,CAAC,KAAK,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC,MAAM,QAAQ,CAAC,KAAK,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC,IAAI,CAAC;QAC9E,0EAA0E;QAC1E,qEAAqE;QACrE,MAAM,eAAe,GAAG;YACtB,GAAG,IAAI,GAAG,CAAC,CAAC,GAAG,OAAO,CAAC,QAAQ,EAAE,GAAG,kBAAkB,CAAC,QAAQ,EAAE,EAAE,CAAC,CAAC,CAAC;SACvE,CAAC,IAAI,CAAC,UAAU,CAAC,CAAC;QAEnB,MAAM,OAAO,GAAG,IAAI,CAAC,OAAO,IAAI,MAAM,CAAC,OAAO,CAAC;QAC/C,IAAI,CAAC,OAAO,EAAE,CAAC;YACb,wEAAwE;YACxE,wEAAwE;YACxE,wEAAwE;YACxE,uEAAuE;YACvE,uBAAuB;YACvB,MAAM,OAAO,GACX,yBAAyB,EAAE,2DAA2D;gBACtF,CAAC,eAAe,CAAC,MAAM,KAAK,CAAC;oBAC3B,CAAC,CAAC,IAAI,EAAE,oCAAoC;oBAC5C,CAAC,CAAC,IAAI,EAAE,aAAa,eAAe,CAAC,IAAI,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC;YACxD,IAAI,IAAI;gBAAE,aAAa,CAAC,gBAAgB,EAAE,OAAO,EAAE,EAAE,OAAO,EAAE,EAAE,EAAE,QAAQ,EAAE,eAAe,EAAE,CAAC,CAAC;iBAC1F,CAAC;gBACJ,OAAO,CAAC,KAAK,CAAC,OAAO,CAAC,CAAC;gBACvB,OAAO,CAAC,QAAQ,GAAG,CAAC,CAAC;YACvB,CAAC;YACD,OAAO;QACT,CAAC;QAED,qEAAqE;QACrE,0EAA0E;QAC1E,sEAAsE;QACtE,kEAAkE;QAClE,MAAM,MAAM,GAAG,MAAM,cAAc,CAAC,MAAM,CAAC,OAAO,CAAC,CAAC;QACpD,IAAI,CAAC,eAAe,CAAC,QAAQ,CAAC,OAAO,CAAC,IAAI,CAAC,MAAM,CAAC,QAAQ,CAAC,OAAO,CAAC,EAAE,CAAC;YACpE,MAAM,IAAI,GAAG,UAAU,CAAC,OAAO,EAAE,CAAC,GAAG,IAAI,GAAG,CAAC,CAAC,GAAG,eAAe,EAAE,GAAG,MAAM,CAAC,CAAC,CAAC,CAAC,CAAC;YAChF,IAAI,CACF,IAAI,EACJ,gBAAgB,EAChB,eAAe,OAAO,+BAA+B,OAAO,oCAAoC,EAAE,UAAU;gBAC1G,CAAC,eAAe,CAAC,MAAM,KAAK,CAAC,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,KAAK,eAAe,CAAC,IAAI,CAAC,IAAI,CAAC,GAAG,CAAC;gBACxE,CAAC,IAAI,CAAC,MAAM,KAAK,CAAC,CAAC,CAAC,CAAC,GAAG,CAAC,CAAC,CAAC,mBAAmB,IAAI,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC,IAAI,CAAC,MAAM,CAAC,GAAG,CAAC,CAC3F,CAAC;YACF,OAAO;QACT,CAAC;QAED,wBAAwB;QACxB,MAAM,MAAM,GAAG,UAAU,CAAC,KAAK,CAAC,MAAM,CAAC;YACrC,CAAC,CAAC,gBAAgB,CAAC,MAAM,QAAQ,CAAC,KAAK,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC,CAAC,IAAI,EAAE;YAC/D,CAAC,CAAC,IAAI,CAAC;QAET,0EAA0E;QAC1E,0EAA0E;QAC1E,0EAA0E;QAC1E,+BAA+B;QAC/B,MAAM,SAAS,GAAG,gBAAgB,CAAC,OAAO,CAAC,GAAG,EAAE,OAAO,CAAC,CAAC;QACzD,MAAM,IAAI,GAAG,UAAU,CAAC,SAAS,CAAC,IAAI,CAAC;YACrC,CAAC,CAAC,iBAAiB,CAAC,MAAM,QAAQ,CAAC,SAAS,CAAC,IAAI,EAAE,MAAM,CAAC,CAAC;YAC3D,CAAC,CAAC,EAAE,CAAC;QACP,MAAM,QAAQ,GAAG,UAAU,CAAC,SAAS,CAAC,QAAQ,CAAC;YAC7C,CAAC,CAAC,iBAAiB,CAAC,MAAM,QAAQ,CAAC,SAAS,CAAC,QAAQ,EAAE,MAAM,CAAC,CAAC;YAC/D,CAAC,CAAC,EAAE,CAAC;QAEP,4EAA4E;QAC5E,wEAAwE;QACxE,2EAA2E;QAC3E,yDAAyD;QACzD,MAAM,GAAG,GAAG,MAAM,UAAU,CAAC,SAAS,CAAC,OAAO,CAAC,CAAC;QAEhD,wBAAwB;QACxB,MAAM,IAAI,GAAG,SAAS,CAAC,QAAQ,EAAE,OAAO,EAAE,EAAE,CAAC,CAAC;QAE9C,mEAAmE;QACnE,qEAAqE;QACrE,wEAAwE;QACxE,wEAAwE;QACxE,wEAAwE;QACxE,uEAAuE;QACvE,qEAAqE;QACrE,IAAI,IAAI,CAAC,MAAM,CAAC,MAAM,GAAG,CAAC;YAAE,OAAO,CAAC,QAAQ,GAAG,CAAC,CAAC;QAEjD,IAAI,IAAI,EAAE,CAAC;YACT,MAAM,OAAO,GAAG,CAAC,CAAc,EAA2B,EAAE,CAAC,CAAC;gBAC5D,IAAI,EAAE,CAAC,CAAC,IAAI;gBACZ,EAAE,EAAE,CAAC,CAAC,EAAE;gBACR,IAAI,EAAE,CAAC,CAAC,IAAI;gBACZ,IAAI,EAAE,CAAC,CAAC,IAAI,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC,IAAI,EAAE;gBAC9B,UAAU,EAAE,CAAC,CAAC,UAAU;gBACxB,MAAM,EAAE,CAAC,CAAC,MAAM;gBAChB,sEAAsE;gBACtE,mEAAmE;gBACnE,SAAS,EAAE,CAAC,CAAC,SAAS,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,EAAE,IAAI,EAAE,CAAC,CAAC,IAAI,EAAE,KAAK,EAAE,CAAC,CAAC,KAAK,EAAE,CAAC,CAAC;aACtE,CAAC,CAAC;YACH,QAAQ,CAAC;gBACP,OAAO,EAAE,EAAE;gBACX,OAAO;gBACP,QAAQ,EAAE,eAAe;gBACzB,IAAI,EAAE,QAAQ,CAAC,MAAM,CAAC,OAAO,EAAE,OAAO,CAAC,GAAG,CAAC;gBAC3C,MAAM;gBACN,YAAY,EAAE,IAAI,CAAC,GAAG,CAAC,OAAO,CAAC;gBAC/B,oEAAoE;gBACpE,qEAAqE;gBACrE,yBAAyB;gBACzB,gBAAgB,EAAE,QAAQ,CAAC,GAAG,CAAC,OAAO,CAAC;gBACvC,GAAG;gBACH,YAAY,EAAE,IAAI;aACnB,CAAC,CAAC;YACH,OAAO;QACT,CAAC;QAED,OAAO,CAAC,GAAG,CAAC,GAAG,EAAE,MAAM,OAAO,IAAI,CAAC,CAAC;QACpC,IAAI,MAAM,EAAE,CAAC;YACX,OAAO,CAAC,GAAG,CAAC,MAAM,CAAC,MAAM,EAAE,IAAI,CAAC,CAAC,CAAC;YAClC,OAAO,CAAC,GAAG,EAAE,CAAC;QAChB,CAAC;QACD,IAAI,UAAU,CAAC,SAAS,CAAC,IAAI,CAAC;YAAE,iBAAiB,CAAC,IAAI,EAAE,cAAc,CAAC,CAAC;;YACnE,OAAO,CAAC,GAAG,CAAC,yCAAyC,CAAC,CAAC;QAC5D,IAAI,UAAU,CAAC,SAAS,CAAC,QAAQ,CAAC;YAAE,iBAAiB,CAAC,QAAQ,EAAE,mBAAmB,CAAC,CAAC;QACrF,IAAI,UAAU,CAAC,SAAS,CAAC,OAAO,CAAC;YAAE,QAAQ,CAAC,GAAG,CAAC,CAAC;QACjD,IAAI,UAAU,CAAC,KAAK,CAAC,KAAK,CAAC;YAAE,cAAc,CAAC,IAAI,EAAE,OAAO,CAAC,CAAC;IAC7D,CAAC,CAAC,CAAC;AACP,CAAC;AAED,iFAAiF;AACjF,KAAK,UAAU,cAAc,CAAC,OAAe;IAC3C,IAAI,CAAC;QACH,OAAO,CAAC,MAAM,YAAY,CAAC,OAAO,CAAC,CAAC,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC;IACxD,CAAC;IAAC,OAAO,GAAG,EAAE,CAAC;QACb,uEAAuE;QACvE,wEAAwE;QACxE,kDAAkD;QAClD,IAAI,CAAC,CAAC,GAAG,YAAY,wBAAwB,CAAC;YAAE,MAAM,GAAG,CAAC;QAC1D,OAAO,EAAE,CAAC;IACZ,CAAC;AACH,CAAC;AAED,sFAAsF;AACtF,SAAS,kBAAkB,CAAC,GAAqB,EAAE,SAAiB;IAClE,IAAI,GAAG,KAAK,IAAI,IAAI,GAAG,CAAC,MAAM,CAAC,MAAM,GAAG,CAAC;QAAE,OAAO,EAAE,CAAC;IACrD,OAAO,GAAG,CAAC,QAAQ,CAAC,MAAM,CAAC,CAAC,CAAO,EAAE,EAAE,CAAC,CAAC,CAAC,IAAI,CAAC,QAAQ,CAAC,SAAS,CAAC,CAAC,CAAC,GAAG,CAAC,cAAc,CAAC,CAAC;AAC1F,CAAC;AAED,oFAAoF;AACpF,SAAS,UAAU,CAAC,EAAU,EAAE,KAAe;IAC7C,MAAM,MAAM,GAAG,IAAI,CAAC,GAAG,CAAC,CAAC,EAAE,IAAI,CAAC,KAAK,CAAC,EAAE,CAAC,MAAM,GAAG,CAAC,CAAC,CAAC,CAAC;IACtD,OAAO,KAAK;SACT,GAAG,CAAC,CAAC,SAAS,EAAE,EAAE,CAAC,CAAC,EAAE,SAAS,EAAE,QAAQ,EAAE,YAAY,CAAC,EAAE,CAAC,WAAW,EAAE,EAAE,SAAS,CAAC,WAAW,EAAE,CAAC,EAAE,CAAC,CAAC;SACtG,MAAM,CAAC,CAAC,MAAM,EAAE,EAAE,CAAC,MAAM,CAAC,QAAQ,IAAI,MAAM,CAAC;SAC7C,IAAI,CAAC,CAAC,CAAC,EAAE,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,QAAQ,GAAG,CAAC,CAAC,QAAQ,IAAI,UAAU,CAAC,CAAC,CAAC,SAAS,EAAE,CAAC,CAAC,SAAS,CAAC,CAAC;SAC/E,KAAK,CAAC,CAAC,EAAE,CAAC,CAAC;SACX,GAAG,CAAC,CAAC,MAAM,EAAE,EAAE,CAAC,MAAM,CAAC,SAAS,CAAC,CAAC;AACvC,CAAC;AAED,sFAAsF;AACtF,SAAS,YAAY,CAAC,CAAS,EAAE,CAAS;IACxC,IAAI,QAAQ,GAAG,KAAK,CAAC,IAAI,CAAC,EAAE,MAAM,EAAE,CAAC,CAAC,MAAM,GAAG,CAAC,EAAE,EAAE,CAAC,CAAC,EAAE,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,CAAC;IACjE,KAAK,IAAI,CAAC,GAAG,CAAC,EAAE,CAAC,IAAI,CAAC,CAAC,MAAM,EAAE,CAAC,IAAI,CAAC,EAAE,CAAC;QACtC,MAAM,OAAO,GAAG,CAAC,CAAC,CAAC,CAAC;QACpB,KAAK,IAAI,CAAC,GAAG,CAAC,EAAE,CAAC,IAAI,CAAC,CAAC,MAAM,EAAE,CAAC,IAAI,CAAC,EAAE,CAAC;YACtC,MAAM,YAAY,GAAG,QAAQ,CAAC,CAAC,GAAG,CAAC,CAAE,GAAG,CAAC,CAAC,CAAC,CAAC,GAAG,CAAC,CAAC,KAAK,CAAC,CAAC,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC;YACxE,OAAO,CAAC,IAAI,CAAC,IAAI,CAAC,GAAG,CAAC,QAAQ,CAAC,CAAC,CAAE,GAAG,CAAC,EAAE,OAAO,CAAC,CAAC,GAAG,CAAC,CAAE,GAAG,CAAC,EAAE,YAAY,CAAC,CAAC,CAAC;QAC9E,CAAC;QACD,QAAQ,GAAG,OAAO,CAAC;IACrB,CAAC;IACD,OAAO,QAAQ,CAAC,CAAC,CAAC,MAAM,CAAE,CAAC;AAC7B,CAAC;AAED;;;;;;;;;GASG;AACH,KAAK,UAAU,UAAU,CAAC,WAAmB;IAC3C,IAAI,CAAC,UAAU,CAAC,WAAW,CAAC;QAAE,OAAO,EAAE,CAAC;IACxC,MAAM,GAAG,GAAG,MAAM,UAAU,CAAC,WAAW,CAAC,CAAC;IAC1C,IAAI,GAAG,CAAC,MAAM,KAAK,CAAC;QAAE,OAAO,EAAE,CAAC;IAChC,IAAI,GAAY,CAAC;IACjB,IAAI,CAAC;QACH,GAAG,GAAG,SAAS,CAAC,MAAM,QAAQ,CAAC,WAAW,EAAE,MAAM,CAAC,CAAC,CAAC;IACvD,CAAC;IAAC,MAAM,CAAC;QACP,4EAA4E;QAC5E,uEAAuE;QACvE,GAAG,GAAG,IAAI,CAAC;IACb,CAAC;IACD,MAAM,SAAS,GAAG,CAAC,EAAa,EAAiB,EAAE;QACjD,MAAM,KAAK,GAAI,GAAkD,EAAE,KAAK,CAAC;QACzE,MAAM,IAAI,GAAG,KAAK,EAAE,CAAC,EAAE,CAAC,IAAI,CAAwC,CAAC;QACrE,MAAM,KAAK,GAAG,IAAI,EAAE,CAAC,EAAE,CAAC,MAAM,CAAwC,CAAC;QACvE,MAAM,OAAO,GAAG,KAAK,EAAE,CAAC,SAAS,CAAC,CAAC;QACnC,OAAO,OAAO,OAAO,KAAK,QAAQ,IAAI,OAAO,CAAC,MAAM,GAAG,CAAC,CAAC,CAAC,CAAC,OAAO,CAAC,CAAC,CAAC,IAAI,CAAC;IAC5E,CAAC,CAAC;IACF,OAAO,GAAG;SACP,GAAG,CAAC,CAAC,EAAE,EAAa,EAAE,CAAC,CAAC;QACvB,IAAI,EAAE,EAAE,CAAC,IAAI;QACb,MAAM,EAAE,EAAE,CAAC,MAAM,CAAC,WAAW,EAAE;QAC/B,WAAW,EAAE,EAAE,CAAC,EAAE;QAClB,OAAO,EAAE,SAAS,CAAC,EAAE,CAAC;QACtB,MAAM,EAAE,EAAE,CAAC,MAAM;KAClB,CAAC,CAAC;SACF,IAAI,CAAC,CAAC,CAAC,EAAE,CAAC,EAAE,EAAE,CAAC,UAAU,CAAC,CAAC,CAAC,IAAI,EAAE,CAAC,CAAC,IAAI,CAAC,IAAI,UAAU,CAAC,CAAC,CAAC,MAAM,EAAE,CAAC,CAAC,MAAM,CAAC,CAAC,CAAC;AAClF,CAAC;AAED,sFAAsF;AACtF,SAAS,SAAS,CAAC,GAAqB,EAAE,OAAe,EAAE,SAAiB;IAC1E,MAAM,KAAK,GAAc,EAAE,KAAK,EAAE,KAAK,EAAE,OAAO,EAAE,EAAE,EAAE,QAAQ,EAAE,EAAE,EAAE,MAAM,EAAE,EAAE,EAAE,CAAC;IACjF,IAAI,GAAG,KAAK,IAAI;QAAE,OAAO,KAAK,CAAC;IAE/B,MAAM,EAAE,MAAM,EAAE,QAAQ,EAAE,aAAa,EAAE,GAAG,GAAG,CAAC;IAChD,IAAI,MAAM,CAAC,MAAM,GAAG,CAAC,EAAE,CAAC;QACtB,OAAO,EAAE,GAAG,KAAK,EAAE,MAAM,EAAE,MAAM,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,OAAO,CAAC,CAAC,IAAI,KAAK,QAAQ,CAAC,CAAC,CAAC,IAAI,CAAC,CAAC,IAAI,KAAK,CAAC,CAAC,OAAO,EAAE,CAAC,CAAC,CAAC,CAAC,CAAC,OAAO,CAAC,CAAC,EAAE,CAAC;IACxH,CAAC;IAED,8EAA8E;IAC9E,MAAM,KAAK,GAAG,CAAC,EAAU,EAAU,EAAE,CAAC,SAAS,CAAC,QAAQ,EAAE,EAAE,CAAC,CAAC;IAC9D,MAAM,IAAI,GAAG,CAAC,CAAM,EAAE,KAAa,EAAQ,EAAE,CAAC,CAAC;QAC7C,OAAO,EAAE,KAAK;QACd,EAAE,EAAE,CAAC,CAAC,EAAE,IAAI,IAAI;QAChB,KAAK,EAAE,CAAC,CAAC,KAAK,IAAI,IAAI;KACvB,CAAC,CAAC;IACH,MAAM,QAAQ,GAAG,aAAa,CAAC,MAAM,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,IAAI,CAAC,QAAQ,CAAC,SAAS,CAAC,CAAC,CAAC;IAEzE,OAAO;QACL,KAAK,EAAE,QAAQ,CAAC,IAAI,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,cAAc,CAAC,CAAC,CAAC,KAAK,OAAO,IAAI,CAAC,CAAC,IAAI,CAAC,QAAQ,CAAC,SAAS,CAAC,CAAC;QACxF,OAAO,EAAE,QAAQ,CAAC,MAAM,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,KAAK,CAAC,CAAC,CAAC,MAAM,CAAC,KAAK,OAAO,CAAC,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,IAAI,CAAC,CAAC,EAAE,KAAK,CAAC,CAAC,CAAC,MAAM,CAAC,CAAC,CAAC;QACjG,QAAQ,EAAE,QAAQ,CAAC,MAAM,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,KAAK,CAAC,CAAC,CAAC,MAAM,CAAC,KAAK,OAAO,CAAC,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,IAAI,CAAC,CAAC,EAAE,KAAK,CAAC,CAAC,CAAC,MAAM,CAAC,CAAC,CAAC;QAClG,MAAM,EAAE,EAAE;KACX,CAAC;AACJ,CAAC;AAED,SAAS,gBAAgB,CAAC,EAAU;IAClC,IAAI,CAAC,EAAE,CAAC,UAAU,CAAC,KAAK,CAAC;QAAE,OAAO,EAAE,CAAC;IACrC,MAAM,KAAK,GAAG,EAAE,CAAC,OAAO,CAAC,OAAO,EAAE,CAAC,CAAC,CAAC;IACrC,IAAI,KAAK,KAAK,CAAC,CAAC;QAAE,OAAO,EAAE,CAAC;IAC5B,MAAM,EAAE,GAAG,EAAE,CAAC,OAAO,CAAC,IAAI,EAAE,KAAK,GAAG,CAAC,CAAC,CAAC;IACvC,OAAO,EAAE,KAAK,CAAC,CAAC,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC,KAAK,CAAC,EAAE,GAAG,CAAC,CAAC,CAAC,SAAS,EAAE,CAAC;AACvD,CAAC;AAED;;;;;;;;;GASG;AACH,SAAS,iBAAiB,CAAC,IAAmB,EAAE,KAAa;IAC3D,IAAI,IAAI,CAAC,MAAM,KAAK,CAAC,EAAE,CAAC;QACtB,OAAO,CAAC,GAAG,CAAC,GAAG,KAAK,YAAY,CAAC,CAAC;QAClC,OAAO;IACT,CAAC;IACD,OAAO,CAAC,GAAG,CAAC,GAAG,KAAK,GAAG,CAAC,CAAC;IACzB,KAAK,MAAM,CAAC,IAAI,IAAI,EAAE,CAAC;QACrB,MAAM,GAAG,GAAG,CAAC,CAAC,IAAI,KAAK,MAAM,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,IAAI,CAAC,CAAC,IAAI,IAAI,CAAC;QACpD,MAAM,CAAC,GAAG,CAAC,CAAC,SAAS,CAAC,MAAM,CAAC;QAC7B,OAAO,CAAC,GAAG,CAAC,KAAK,GAAG,GAAG,CAAC,CAAC,IAAI,MAAM,CAAC,YAAY,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,GAAG,GAAG,CAAC,CAAC;QACvE,MAAM,IAAI,GAAG,CAAC,CAAC,IAAI,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC,IAAI,EAAE,CAAC;QACtC,IAAI,IAAI,CAAC,MAAM,GAAG,CAAC;YAAE,OAAO,CAAC,GAAG,CAAC,MAAM,CAAC,IAAI,EAAE,MAAM,CAAC,CAAC,CAAC;QACvD,IAAI,CAAC,CAAC,UAAU,CAAC,MAAM,GAAG,CAAC;YAAE,OAAO,CAAC,GAAG,CAAC,mBAAmB,CAAC,CAAC,UAAU,CAAC,IAAI,CAAC,IAAI,CAAC,EAAE,CAAC,CAAC;QACvF,IAAI,CAAC,CAAC,MAAM,CAAC,MAAM,GAAG,CAAC;YAAE,OAAO,CAAC,GAAG,CAAC,eAAe,CAAC,CAAC,MAAM,CAAC,IAAI,CAAC,IAAI,CAAC,EAAE,CAAC,CAAC;QAC3E,KAAK,MAAM,CAAC,IAAI,CAAC,CAAC,SAAS,EAAE,CAAC;YAC5B,OAAO,CAAC,GAAG,CAAC,mBAAmB,CAAC,CAAC,IAAI,EAAE,CAAC,CAAC;YACzC,wEAAwE;YACxE,qEAAqE;YACrE,6DAA6D;YAC7D,KAAK,MAAM,IAAI,IAAI,CAAC,CAAC,KAAK;gBAAE,OAAO,CAAC,GAAG,CAAC,SAAS,IAAI,EAAE,CAAC,CAAC;QAC3D,CAAC;QACD,OAAO,CAAC,GAAG,EAAE,CAAC;IAChB,CAAC;AACH,CAAC;AAED,SAAS,QAAQ,CAAC,GAAgB;IAChC,IAAI,GAAG,CAAC,MAAM,KAAK,CAAC,EAAE,CAAC;QACrB,OAAO,CAAC,GAAG,CAAC,kDAAkD,CAAC,CAAC;QAChE,OAAO;IACT,CAAC;IACD,OAAO,CAAC,GAAG,CAAC,oDAAoD,CAAC,CAAC;IAClE,KAAK,MAAM,EAAE,IAAI,GAAG,EAAE,CAAC;QACrB,MAAM,MAAM,GAAG,EAAE,CAAC,MAAM,CAAC,CAAC,CAAC,SAAS,CAAC,CAAC,CAAC,EAAE,CAAC;QAC1C,OAAO,CAAC,GAAG,CACT,KAAK,MAAM,GAAG,EAAE,CAAC,MAAM,IAAI,EAAE,CAAC,IAAI,KAAK,EAAE,CAAC,WAAW,GAAG,EAAE,CAAC,OAAO,KAAK,IAAI,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,MAAM,EAAE,CAAC,OAAO,EAAE,EAAE,CACxG,CAAC;IACJ,CAAC;IACD,OAAO,CAAC,GAAG,EAAE,CAAC;AAChB,CAAC;AAED,SAAS,cAAc,CAAC,IAAe,EAAE,OAAe;IACtD,IAAI,IAAI,CAAC,MAAM,CAAC,MAAM,GAAG,CAAC,EAAE,CAAC;QAC3B,OAAO,CAAC,GAAG,CAAC,8DAA8D,CAAC,CAAC;QAC5E,OAAO;IACT,CAAC;IACD,OAAO,CAAC,GAAG,CAAC,eAAe,CAAC,CAAC;IAC7B,IAAI,IAAI,CAAC,KAAK;QAAE,OAAO,CAAC,GAAG,CAAC,0BAA0B,OAAO,EAAE,CAAC,CAAC;IACjE,IAAI,IAAI,CAAC,QAAQ,CAAC,MAAM,GAAG,CAAC,EAAE,CAAC;QAC7B,OAAO,CAAC,GAAG,CAAC,2CAA2C,CAAC,CAAC;QACzD,KAAK,MAAM,CAAC,IAAI,IAAI,CAAC,QAAQ;YAAE,OAAO,CAAC,GAAG,CAAC,SAAS,CAAC,CAAC,OAAO,MAAM,CAAC,CAAC,KAAK,IAAI,EAAE,GAAG,CAAC,CAAC;IACvF,CAAC;IACD,IAAI,IAAI,CAAC,OAAO,CAAC,MAAM,GAAG,CAAC,EAAE,CAAC;QAC5B,OAAO,CAAC,GAAG,CAAC,2CAA2C,CAAC,CAAC;QACzD,KAAK,MAAM,CAAC,IAAI,IAAI,CAAC,OAAO;YAAE,OAAO,CAAC,GAAG,CAAC,SAAS,CAAC,CAAC,OAAO,MAAM,CAAC,CAAC,KAAK,IAAI,EAAE,GAAG,CAAC,CAAC;IACtF,CAAC;IACD,IAAI,CAAC,IAAI,CAAC,KAAK,IAAI,IAAI,CAAC,QAAQ,CAAC,MAAM,KAAK,CAAC,IAAI,IAAI,CAAC,OAAO,CAAC,MAAM,KAAK,CAAC,EAAE,CAAC;QAC3E,OAAO,CAAC,GAAG,CAAC,6CAA6C,CAAC,CAAC;IAC7D,CAAC;IACD,OAAO,CAAC,GAAG,EAAE,CAAC;AAChB,CAAC;AAED,SAAS,MAAM,CAAC,IAAY,EAAE,GAAW;IACvC,OAAO,IAAI;SACR,KAAK,CAAC,IAAI,CAAC;SACX,GAAG,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,CAAC,MAAM,GAAG,CAAC,CAAC,CAAC,CAAC,GAAG,GAAG,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC;SACxC,IAAI,CAAC,IAAI,CAAC,CAAC;AAChB,CAAC","sourcesContent":["import type { Command } from \"commander\";\nimport { existsSync } from \"node:fs\";\nimport { readFile } from \"node:fs/promises\";\nimport { parse as parseYaml } from \"yaml\";\nimport { loadConfig } from \"../core/config.js\";\nimport { InvalidIdError, assertServiceId } from \"../core/ids.js\";\nimport { emitJson, emitJsonError, fail, repoPath, reportNoConfig } from \"../core/json.js\";\nimport { elementService, loadFile, serviceOf, type Elem, type LoadedDoc, type Rel } from \"../core/likec4.js\";\nimport { operations, type Operation } from \"../core/openapi.js\";\nimport {\n DocsRepoUnavailableError,\n compareIds,\n featurePaths,\n featureSpecPaths,\n listServices,\n missingFeatureMessage,\n resolveFeature,\n} from \"../core/repo.js\";\nimport { parseRequirements, type Requirement } from \"../core/spec.js\";\n\ninterface DeltaOptions {\n service?: string;\n json?: boolean;\n}\n\n/** One end of a feature edge, as seen from the projected service. */\ninterface Edge {\n service: string;\n op: string | null;\n title: string | null;\n}\n\ninterface ArchSlice {\n isNew: boolean;\n inbound: Edge[];\n outbound: Edge[];\n errors: string[];\n}\n\n/**\n * One operation the feature's OpenAPI delta defines for this service. The\n * projection is a task brief, and \"implement createSplit\" without the path and\n * the method is not a task — it is a name to go look up in a file the reader\n * was not told about.\n */\ninterface ApiChange {\n path: string;\n method: string;\n operationId: string;\n summary: string | null;\n /** `x-loam-remove: true` — this operation is being retired, not added. */\n remove: boolean;\n}\n\nexport function registerDelta(program: Command): void {\n program\n .command(\"delta\")\n .argument(\"<featureId>\", \"feature id, e.g. FEAT-101\")\n .description(\"Project a feature onto a service: why + requirement delta + C4 changes\")\n .option(\"--service <id>\", \"service to project onto (defaults to the configured service)\")\n .option(\"--json\", \"emit the machine contract instead of the human view\")\n .action(async (featureId: string, opts: DeltaOptions) => {\n const json = opts.json === true;\n const config = await loadConfig();\n if (!config) {\n reportNoConfig(json);\n return;\n }\n\n // The id grammar first, and on the RAW argument: `--service ../../etc`\n // reaches `featureSpecPaths` and reads from outside the feature directory.\n // One grammar for the whole tool (core/ids.ts).\n if (opts.service !== undefined) {\n try {\n assertServiceId(opts.service, \"--service\");\n } catch (err) {\n if (!(err instanceof InvalidIdError)) throw err;\n fail(json, \"invalid-option\", err.message);\n return;\n }\n }\n\n // The feature is resolved BEFORE the service is chosen, because every\n // sentence about the service (\"which ones does this feature touch?\") is\n // only answerable once the feature is known.\n const feature = await resolveFeature(config.docsDir, featureId, \"exclude\");\n if (!feature) {\n fail(json, \"unknown-target\", await missingFeatureMessage(config.docsDir, featureId));\n return;\n }\n // Canonical id from here on — the delta's tags carry `#FEAT-5`, so a raw\n // `FEAT-5-slug` argument used to empty the architecture slice silently.\n const { id } = feature;\n const paths = featurePaths(feature.dir);\n\n const deltaDoc = existsSync(paths.delta) ? await loadFile(paths.delta) : null;\n // Services the feature speaks about at all: one with a requirement delta,\n // or one its C4 delta introduces. This is the list a refusal quotes.\n const featureServices = [\n ...new Set([...feature.services, ...introducedServices(deltaDoc, id)]),\n ].sort(compareIds);\n\n const service = opts.service ?? config.service;\n if (!service) {\n // The refusal NAMES the choices. \"No service. Pass --service <id>\" told\n // the reader the shape of the flag and nothing about the answer, so the\n // next step was always to go list the feature's specs/ by hand — and an\n // agent had no way to recover at all. The list rides the JSON envelope\n // for the same reason.\n const message =\n `No service to project ${id} onto. Pass --service <id> or set 'service' in loam.json.` +\n (featureServices.length === 0\n ? ` ${id} carries no per-service delta yet.`\n : ` ${id} touches: ${featureServices.join(\", \")}.`);\n if (json) emitJsonError(\"invalid-option\", message, { feature: id, services: featureServices });\n else {\n console.error(message);\n process.exitCode = 1;\n }\n return;\n }\n\n // A service nobody has heard of produces a perfectly plausible EMPTY\n // report — no requirements, no edges, \"no architecture change\" — which an\n // agent consuming this as a task brief reads as \"nothing to do here\".\n // A typo must never be indistinguishable from a finished service.\n const living = await livingServices(config.docsDir);\n if (!featureServices.includes(service) && !living.includes(service)) {\n const near = nearestIds(service, [...new Set([...featureServices, ...living])]);\n fail(\n json,\n \"unknown-target\",\n `No service '${service}': it is neither a services/${service}/ in the docs repo nor a service ${id} touches` +\n (featureServices.length === 0 ? \"\" : ` (${featureServices.join(\", \")})`) +\n (near.length === 0 ? \".\" : ` — did you mean ${near.map((n) => `'${n}'`).join(\" or \")}?`),\n );\n return;\n }\n\n // Why — business intent\n const intent = existsSync(paths.intent)\n ? stripFrontmatter(await readFile(paths.intent, \"utf8\")).trim()\n : null;\n\n // Requirement delta for this service (OpenSpec style), business and arch:\n // the same projection covers both axes, in the same shape, so the payload\n // stays one task — the arch requirements are the integration/ops half the\n // business ones never mention.\n const specPaths = featureSpecPaths(feature.dir, service);\n const reqs = existsSync(specPaths.spec)\n ? parseRequirements(await readFile(specPaths.spec, \"utf8\"))\n : [];\n const archReqs = existsSync(specPaths.archSpec)\n ? parseRequirements(await readFile(specPaths.archSpec, \"utf8\"))\n : [];\n\n // The contract axis. Until now no command surfaced it at all: the feature's\n // openapi.yaml was read by validate and by archive, and the one command\n // whose output IS the implementation task never mentioned it — so \"add the\n // endpoint\" was the half of the work the brief left out.\n const api = await apiChanges(specPaths.openapi);\n\n // C4 architecture slice\n const arch = archSlice(deltaDoc, service, id);\n\n // An unparseable delta.likec4 empties the C4 slice, and a consumer\n // reading this projection as a task brief — the JSON payload and the\n // printed view alike — would take that as \"no architecture change\": the\n // vacuously-green pattern. The output stays as informative as ever (and\n // `ok` stays true under --json: the command ran); the exit code is what\n // stops a pipeline from building on it, so it is set BEFORE the format\n // fork — the guard is about the delta, not about how it is rendered.\n if (arch.errors.length > 0) process.exitCode = 1;\n\n if (json) {\n const reqJson = (r: Requirement): Record<string, unknown> => ({\n kind: r.kind,\n id: r.id,\n name: r.name,\n text: r.text.join(\"\\n\").trim(),\n operations: r.operations,\n covers: r.covers,\n // Scenarios go out verbatim: they are the acceptance criteria and the\n // source for the tests whoever picks this up is expected to write.\n scenarios: r.scenarios.map((s) => ({ name: s.name, lines: s.lines })),\n });\n emitJson({\n feature: id,\n service,\n services: featureServices,\n path: repoPath(config.docsDir, feature.dir),\n intent,\n requirements: reqs.map(reqJson),\n // Same shape, separate section: an arch requirement's scenarios are\n // integration/ops tests, and a consumer must not have to parse prose\n // to tell the two apart.\n archRequirements: archReqs.map(reqJson),\n api,\n architecture: arch,\n });\n return;\n }\n\n console.log(`${id} · ${service}\\n`);\n if (intent) {\n console.log(indent(intent, \" \"));\n console.log();\n }\n if (existsSync(specPaths.spec)) printRequirements(reqs, \"Requirements\");\n else console.log(\"Requirements: (none for this service)\\n\");\n if (existsSync(specPaths.archSpec)) printRequirements(archReqs, \"Arch requirements\");\n if (existsSync(specPaths.openapi)) printApi(api);\n if (existsSync(paths.delta)) printArchSlice(arch, service);\n });\n}\n\n/** Every service in the docs repo, or none when there is no docs repo to ask. */\nasync function livingServices(docsDir: string): Promise<string[]> {\n try {\n return (await listServices(docsDir)).map((s) => s.id);\n } catch (err) {\n // \"There is no docs repo at all\" is a different diagnosis with its own\n // command (`loam doctor`); turning it into \"unknown service\" here would\n // send the reader after a typo that is not there.\n if (!(err instanceof DocsRepoUnavailableError)) throw err;\n return [];\n }\n}\n\n/** Services the feature's C4 delta introduces — its own tagged top-level elements. */\nfunction introducedServices(doc: LoadedDoc | null, featureId: string): string[] {\n if (doc === null || doc.errors.length > 0) return [];\n return doc.elements.filter((e: Elem) => e.tags.includes(featureId)).map(elementService);\n}\n\n/** Known ids within a small edit distance of `id`, closest first, at most three. */\nfunction nearestIds(id: string, known: string[]): string[] {\n const budget = Math.max(1, Math.floor(id.length / 4));\n return known\n .map((candidate) => ({ candidate, distance: editDistance(id.toLowerCase(), candidate.toLowerCase()) }))\n .filter((scored) => scored.distance <= budget)\n .sort((a, b) => a.distance - b.distance || compareIds(a.candidate, b.candidate))\n .slice(0, 3)\n .map((scored) => scored.candidate);\n}\n\n/** Plain Levenshtein — ids are short, and the row-at-a-time form keeps it obvious. */\nfunction editDistance(a: string, b: string): number {\n let previous = Array.from({ length: b.length + 1 }, (_, i) => i);\n for (let i = 1; i <= a.length; i += 1) {\n const current = [i];\n for (let j = 1; j <= b.length; j += 1) {\n const substitution = previous[j - 1]! + (a[i - 1] === b[j - 1] ? 0 : 1);\n current.push(Math.min(previous[j]! + 1, current[j - 1]! + 1, substitution));\n }\n previous = current;\n }\n return previous[b.length]!;\n}\n\n/**\n * The feature's OpenAPI delta for one service, as slots rather than names.\n *\n * The operation set comes from core/openapi.ts — the same structure-aware reader\n * every other check uses, so this view cannot see an operation validate does not.\n * `summary` is the one field that reader does not carry, and it is the sentence\n * an implementer most wants, so it is looked up by the path+method the reader\n * already resolved: a second read of the same document, never a second opinion\n * about what an operation IS.\n */\nasync function apiChanges(openapiPath: string): Promise<ApiChange[]> {\n if (!existsSync(openapiPath)) return [];\n const ops = await operations(openapiPath);\n if (ops.length === 0) return [];\n let doc: unknown;\n try {\n doc = parseYaml(await readFile(openapiPath, \"utf8\"));\n } catch {\n // Unreadable is impossible here (ops came back non-empty), but a summary is\n // decoration and must never be the reason a task brief fails to print.\n doc = null;\n }\n const summaryOf = (op: Operation): string | null => {\n const paths = (doc as { paths?: Record<string, unknown> } | null)?.paths;\n const item = paths?.[op.path] as Record<string, unknown> | undefined;\n const entry = item?.[op.method] as Record<string, unknown> | undefined;\n const summary = entry?.[\"summary\"];\n return typeof summary === \"string\" && summary.length > 0 ? summary : null;\n };\n return ops\n .map((op): ApiChange => ({\n path: op.path,\n method: op.method.toUpperCase(),\n operationId: op.id,\n summary: summaryOf(op),\n remove: op.remove,\n }))\n .sort((a, b) => compareIds(a.path, b.path) || compareIds(a.method, b.method));\n}\n\n/** The feature's tagged edges around one service, plus whether the service is new. */\nfunction archSlice(doc: LoadedDoc | null, service: string, featureId: string): ArchSlice {\n const empty: ArchSlice = { isNew: false, inbound: [], outbound: [], errors: [] };\n if (doc === null) return empty;\n\n const { errors, elements, relationships } = doc;\n if (errors.length > 0) {\n return { ...empty, errors: errors.map((e) => (typeof e.line === \"number\" ? `L${e.line}: ${e.message}` : e.message)) };\n }\n\n // Which service an element stands for is the binding's call, not the title's.\n const svcOf = (id: string): string => serviceOf(elements, id);\n const edge = (r: Rel, other: string): Edge => ({\n service: other,\n op: r.op ?? null,\n title: r.title ?? null,\n });\n const featRels = relationships.filter((r) => r.tags.includes(featureId));\n\n return {\n isNew: elements.some((e) => elementService(e) === service && e.tags.includes(featureId)),\n inbound: featRels.filter((r) => svcOf(r.target) === service).map((r) => edge(r, svcOf(r.source))),\n outbound: featRels.filter((r) => svcOf(r.source) === service).map((r) => edge(r, svcOf(r.target))),\n errors: [],\n };\n}\n\nfunction stripFrontmatter(md: string): string {\n if (!md.startsWith(\"---\")) return md;\n const close = md.indexOf(\"\\n---\", 3);\n if (close === -1) return md;\n const nl = md.indexOf(\"\\n\", close + 1);\n return nl === -1 ? \"\" : md.slice(nl + 1).trimStart();\n}\n\n/**\n * The requirement delta, in full.\n *\n * The text view used to print headings and scenario NAMES only, which made it a\n * table of contents for a file the reader then had to open — while `--json`\n * carried the requirement body and the Given/When/Then lines verbatim. The two\n * are the same briefing, so they carry the same content: a person reading this\n * in a terminal is being asked to implement it, exactly like the agent reading\n * the payload.\n */\nfunction printRequirements(reqs: Requirement[], label: string): void {\n if (reqs.length === 0) {\n console.log(`${label}: (none)\\n`);\n return;\n }\n console.log(`${label}:`);\n for (const r of reqs) {\n const tag = r.kind === \"BASE\" ? \"\" : `[${r.kind}] `;\n const n = r.scenarios.length;\n console.log(` ${tag}${r.name} (${n} scenario${n === 1 ? \"\" : \"s\"})`);\n const body = r.text.join(\"\\n\").trim();\n if (body.length > 0) console.log(indent(body, \" \"));\n if (r.operations.length > 0) console.log(` Operations: ${r.operations.join(\", \")}`);\n if (r.covers.length > 0) console.log(` Covers: ${r.covers.join(\", \")}`);\n for (const s of r.scenarios) {\n console.log(`\\n Scenario: ${s.name}`);\n // Verbatim, bullets and markdown emphasis included: these lines are the\n // acceptance criteria, and paraphrasing them here would put a second\n // wording of the contract in front of whoever implements it.\n for (const line of s.lines) console.log(` ${line}`);\n }\n console.log();\n }\n}\n\nfunction printApi(api: ApiChange[]): void {\n if (api.length === 0) {\n console.log(\"API: (the openapi delta defines no operations)\\n\");\n return;\n }\n console.log(\"API (this feature's openapi.yaml for the service):\");\n for (const op of api) {\n const marker = op.remove ? \"REMOVE \" : \"\";\n console.log(\n ` ${marker}${op.method} ${op.path} ${op.operationId}${op.summary === null ? \"\" : ` — ${op.summary}`}`,\n );\n }\n console.log();\n}\n\nfunction printArchSlice(arch: ArchSlice, service: string): void {\n if (arch.errors.length > 0) {\n console.log(\"Architecture: delta.likec4 has errors — run `loam validate`.\");\n return;\n }\n console.log(\"Architecture:\");\n if (arch.isNew) console.log(` NEW service — create ${service}`);\n if (arch.outbound.length > 0) {\n console.log(\" Outbound (new calls from this service):\");\n for (const e of arch.outbound) console.log(` → ${e.service} \"${e.title ?? \"\"}\"`);\n }\n if (arch.inbound.length > 0) {\n console.log(\" Inbound (this service is newly called):\");\n for (const e of arch.inbound) console.log(` ← ${e.service} \"${e.title ?? \"\"}\"`);\n }\n if (!arch.isNew && arch.outbound.length === 0 && arch.inbound.length === 0) {\n console.log(\" (no architecture change for this service)\");\n }\n console.log();\n}\n\nfunction indent(text: string, pad: string): string {\n return text\n .split(\"\\n\")\n .map((l) => (l.length > 0 ? pad + l : l))\n .join(\"\\n\");\n}\n"]}
@@ -0,0 +1,78 @@
1
+ import { loadConfig } from "../core/config.js";
2
+ import { analyzeDependencies, } from "../core/dependencies.js";
3
+ import { FleetContext } from "../core/fleet-context.js";
4
+ import { emitJson, fail, reportNoConfig } from "../core/json.js";
5
+ import { resolveFeature } from "../core/repo.js";
6
+ export function registerDependencies(program) {
7
+ program
8
+ .command("dependencies")
9
+ .argument("[featureId]", "active feature id; omit for the whole in-flight graph")
10
+ .description("Show dependencies and conflicts derived from active feature artifacts")
11
+ .option("--json", "emit the machine contract instead of the human view")
12
+ .action(async (featureArg, opts) => {
13
+ const json = opts.json === true;
14
+ const config = await loadConfig();
15
+ if (!config) {
16
+ reportNoConfig(json);
17
+ return;
18
+ }
19
+ const context = new FleetContext();
20
+ const feature = featureArg === undefined
21
+ ? null
22
+ : await resolveFeature(config.docsDir, featureArg, "exclude", context);
23
+ if (featureArg !== undefined && feature === null) {
24
+ fail(json, "unknown-target", `No active feature '${featureArg}' in ${config.docsDir}.`);
25
+ return;
26
+ }
27
+ const graph = await analyzeDependencies(config.docsDir, feature?.id, context);
28
+ if (json) {
29
+ emitJson({
30
+ command: "dependencies",
31
+ docsDir: config.docsDir,
32
+ feature: feature?.id ?? null,
33
+ ...graph,
34
+ });
35
+ return;
36
+ }
37
+ printGraph(graph, feature?.id);
38
+ });
39
+ }
40
+ function reasonText(reason) {
41
+ return reason.kind === "operation"
42
+ ? `${reason.service} operation ${reason.operationId}`
43
+ : `${reason.service}/${reason.axis} ${reason.identity}`;
44
+ }
45
+ function printGraph(graph, featureId) {
46
+ const scope = featureId === undefined ? "active features" : `${featureId} and prerequisites`;
47
+ console.log(`dependencies — ${scope} (${graph.nodes.length})`);
48
+ console.log(` order: ${graph.order.length === 0 ? "(none)" : graph.order.join(" → ")}`);
49
+ for (const node of graph.nodes) {
50
+ console.log(`\n ${node.id}${node.dependsOn.length === 0 ? " (independent)" : ""}`);
51
+ for (const edge of graph.edges.filter((candidate) => candidate.from === node.id)) {
52
+ console.log(` depends on ${edge.to}`);
53
+ for (const reason of edge.reasons)
54
+ console.log(` - ${reasonText(reason)}`);
55
+ }
56
+ }
57
+ if (graph.conflicts.length > 0) {
58
+ console.log("\n conflicts");
59
+ for (const conflict of graph.conflicts) {
60
+ const where = conflict.kind === "requirement"
61
+ ? `${conflict.service}/${conflict.axis}`
62
+ : conflict.service;
63
+ // "added by" vs "changed by": the two collisions want different fixes.
64
+ // Two features ADDING the same identity is usually duplicated work that
65
+ // should become one feature; two features CHANGING it is usually genuine
66
+ // parallel work whose second archive would silently overwrite the first's
67
+ // full text, so the losers need to be re-based, not merged.
68
+ const how = conflict.change === "added" ? "added by" : "changed by";
69
+ console.log(` ! ${where} ${conflict.identity} — ${how} ${conflict.features.join(", ")}`);
70
+ }
71
+ }
72
+ if (graph.cycles.length > 0) {
73
+ console.log("\n cycles");
74
+ for (const cycle of graph.cycles)
75
+ console.log(` ! ${cycle.join(" ↔ ")}`);
76
+ }
77
+ }
78
+ //# sourceMappingURL=dependencies.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"dependencies.js","sourceRoot":"","sources":["../../src/commands/dependencies.ts"],"names":[],"mappings":"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","sourcesContent":["import type { Command } from \"commander\";\nimport { loadConfig } from \"../core/config.js\";\nimport {\n analyzeDependencies,\n type DependencyGraph,\n type DependencyReason,\n} from \"../core/dependencies.js\";\nimport { FleetContext } from \"../core/fleet-context.js\";\nimport { emitJson, fail, reportNoConfig } from \"../core/json.js\";\nimport { resolveFeature } from \"../core/repo.js\";\n\ninterface DependenciesOptions {\n json?: boolean;\n}\n\nexport function registerDependencies(program: Command): void {\n program\n .command(\"dependencies\")\n .argument(\"[featureId]\", \"active feature id; omit for the whole in-flight graph\")\n .description(\"Show dependencies and conflicts derived from active feature artifacts\")\n .option(\"--json\", \"emit the machine contract instead of the human view\")\n .action(async (featureArg: string | undefined, opts: DependenciesOptions) => {\n const json = opts.json === true;\n const config = await loadConfig();\n if (!config) {\n reportNoConfig(json);\n return;\n }\n\n const context = new FleetContext();\n const feature = featureArg === undefined\n ? null\n : await resolveFeature(config.docsDir, featureArg, \"exclude\", context);\n if (featureArg !== undefined && feature === null) {\n fail(json, \"unknown-target\", `No active feature '${featureArg}' in ${config.docsDir}.`);\n return;\n }\n\n const graph = await analyzeDependencies(config.docsDir, feature?.id, context);\n if (json) {\n emitJson({\n command: \"dependencies\",\n docsDir: config.docsDir,\n feature: feature?.id ?? null,\n ...graph,\n });\n return;\n }\n printGraph(graph, feature?.id);\n });\n}\n\nfunction reasonText(reason: DependencyReason): string {\n return reason.kind === \"operation\"\n ? `${reason.service} operation ${reason.operationId}`\n : `${reason.service}/${reason.axis} ${reason.identity}`;\n}\n\nfunction printGraph(graph: DependencyGraph, featureId?: string): void {\n const scope = featureId === undefined ? \"active features\" : `${featureId} and prerequisites`;\n console.log(`dependencies — ${scope} (${graph.nodes.length})`);\n console.log(` order: ${graph.order.length === 0 ? \"(none)\" : graph.order.join(\" → \")}`);\n\n for (const node of graph.nodes) {\n console.log(`\\n ${node.id}${node.dependsOn.length === 0 ? \" (independent)\" : \"\"}`);\n for (const edge of graph.edges.filter((candidate) => candidate.from === node.id)) {\n console.log(` depends on ${edge.to}`);\n for (const reason of edge.reasons) console.log(` - ${reasonText(reason)}`);\n }\n }\n\n if (graph.conflicts.length > 0) {\n console.log(\"\\n conflicts\");\n for (const conflict of graph.conflicts) {\n const where = conflict.kind === \"requirement\"\n ? `${conflict.service}/${conflict.axis}`\n : conflict.service;\n // \"added by\" vs \"changed by\": the two collisions want different fixes.\n // Two features ADDING the same identity is usually duplicated work that\n // should become one feature; two features CHANGING it is usually genuine\n // parallel work whose second archive would silently overwrite the first's\n // full text, so the losers need to be re-based, not merged.\n const how = conflict.change === \"added\" ? \"added by\" : \"changed by\";\n console.log(` ! ${where} ${conflict.identity} — ${how} ${conflict.features.join(\", \")}`);\n }\n }\n\n if (graph.cycles.length > 0) {\n console.log(\"\\n cycles\");\n for (const cycle of graph.cycles) console.log(` ! ${cycle.join(\" ↔ \")}`);\n }\n}\n"]}
@@ -0,0 +1,85 @@
1
+ import { diagnose } from "../core/doctor.js";
2
+ import { emitJson } from "../core/json.js";
3
+ export function registerDoctor(program) {
4
+ program
5
+ .command("doctor")
6
+ .description("Diagnose local loam configuration and docs-repo accessibility without writing")
7
+ .option("--json", "emit the machine contract instead of the human view")
8
+ .action(async (opts) => {
9
+ const report = await diagnose();
10
+ if (opts.json === true)
11
+ emitJson({ command: "doctor", ...report });
12
+ else
13
+ printDoctor(report);
14
+ if (!report.healthy)
15
+ process.exitCode = 1;
16
+ });
17
+ }
18
+ function yesNo(value) {
19
+ return value ? "yes" : "no";
20
+ }
21
+ /**
22
+ * The stamp as one readable token. "absent" and "unstamped" are kept apart
23
+ * because they are different repos: the first has no AGENTS.md at all, the
24
+ * second has one nobody has claimed a version for.
25
+ */
26
+ function stampLabel(stamp) {
27
+ if (!stamp.present)
28
+ return "absent";
29
+ if (stamp.version === null)
30
+ return "unstamped";
31
+ return `v${stamp.version}${stamp.stale ? " (stale)" : ""}`;
32
+ }
33
+ /**
34
+ * What a killed writer left in the docs repo, as one line. "clean" is the
35
+ * answer on every healthy repo, and "(unresolved)" keeps it apart from clean:
36
+ * a docsDir that never resolved was never looked at.
37
+ */
38
+ function writePathLabel(residue) {
39
+ if (residue === null)
40
+ return "(unresolved)";
41
+ const parts = [
42
+ ...(residue.lock === null ? [] : [`lock held${residue.lock.stale ? " (stale)" : ""}`]),
43
+ ...(residue.intentUnreadable
44
+ ? ["interrupted commit (unreadable)"]
45
+ : residue.intent === null
46
+ ? []
47
+ : [`interrupted ${residue.intent.command} of ${residue.intent.feature}`]),
48
+ ...(residue.temps.length === 0 ? [] : [`${residue.temps.length} orphaned temp file(s)`]),
49
+ ];
50
+ return parts.length === 0 ? "clean" : parts.join(" · ");
51
+ }
52
+ function printDoctor(report) {
53
+ console.log(`loam doctor — ${report.healthy ? "healthy" : "blocked"}`);
54
+ console.log(` runtime ${report.runtime.package}@${report.runtime.version}`);
55
+ console.log(` node ${report.runtime.node} ${report.runtime.platform}/${report.runtime.arch}`);
56
+ console.log(` config ${report.config.status} ${report.config.path}`);
57
+ if (report.config.error !== null)
58
+ console.log(` ${report.config.error}`);
59
+ console.log(` docsDir ${report.docs.path ?? "(unresolved)"}`);
60
+ console.log(` access exists ${yesNo(report.docs.exists)} · read ${yesNo(report.docs.readable)} · write ${yesNo(report.docs.writable)}`);
61
+ console.log(` fleet services/ ${yesNo(report.docs.servicesDir)} · landscape ${yesNo(report.docs.landscape)} · ${report.counts.services} services · ${report.counts.activeFeatures} active features`);
62
+ console.log(` binding ${report.currentService.configured ?? "(none)"} · ${report.currentService.status}`);
63
+ // The agent surface is reported whether or not it has drifted — like every
64
+ // line above it, this is state, not a complaint. Only the findings block is
65
+ // silent when there is nothing to say.
66
+ const agents = report.agents;
67
+ console.log(` agents ${agents.tools.length === 0 ? "(none)" : agents.tools.join(", ")}`
68
+ + ` (${agents.toolsSource}) · ${agents.plannedFiles} files · ${agents.missingFiles.length} missing`
69
+ + ` · ${agents.staleFiles.length} stale · AGENTS.md ${stampLabel(agents.stamp)}`);
70
+ // The write path is state too, and its clean answer is the one worth printing
71
+ // most often: a reader who has just been told the docs are half-written needs
72
+ // to see it go away.
73
+ console.log(` write path ${writePathLabel(report.writePath)}`);
74
+ if (report.findings.length > 0) {
75
+ console.log("\n findings");
76
+ for (const finding of report.findings) {
77
+ console.log(` ${finding.severity === "blocker" ? "✗" : "⚠"} ${finding.code}: ${finding.message}`);
78
+ // The fix is printed on its own indented line rather than appended to the
79
+ // message: what is wrong and what to type are two different sentences,
80
+ // and someone scanning a blocked repo reads down the `fix:` column.
81
+ console.log(` fix: ${finding.fix}`);
82
+ }
83
+ }
84
+ }
85
+ //# sourceMappingURL=doctor.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"doctor.js","sourceRoot":"","sources":["../../src/commands/doctor.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,QAAQ,EAAqB,MAAM,mBAAmB,CAAC;AAChE,OAAO,EAAE,QAAQ,EAAE,MAAM,iBAAiB,CAAC;AAM3C,MAAM,UAAU,cAAc,CAAC,OAAgB;IAC7C,OAAO;SACJ,OAAO,CAAC,QAAQ,CAAC;SACjB,WAAW,CAAC,+EAA+E,CAAC;SAC5F,MAAM,CAAC,QAAQ,EAAE,qDAAqD,CAAC;SACvE,MAAM,CAAC,KAAK,EAAE,IAAmB,EAAE,EAAE;QACpC,MAAM,MAAM,GAAG,MAAM,QAAQ,EAAE,CAAC;QAChC,IAAI,IAAI,CAAC,IAAI,KAAK,IAAI;YAAE,QAAQ,CAAC,EAAE,OAAO,EAAE,QAAQ,EAAE,GAAG,MAAM,EAAE,CAAC,CAAC;;YAC9D,WAAW,CAAC,MAAM,CAAC,CAAC;QACzB,IAAI,CAAC,MAAM,CAAC,OAAO;YAAE,OAAO,CAAC,QAAQ,GAAG,CAAC,CAAC;IAC5C,CAAC,CAAC,CAAC;AACP,CAAC;AAED,SAAS,KAAK,CAAC,KAAc;IAC3B,OAAO,KAAK,CAAC,CAAC,CAAC,KAAK,CAAC,CAAC,CAAC,IAAI,CAAC;AAC9B,CAAC;AAED;;;;GAIG;AACH,SAAS,UAAU,CAAC,KAAsC;IACxD,IAAI,CAAC,KAAK,CAAC,OAAO;QAAE,OAAO,QAAQ,CAAC;IACpC,IAAI,KAAK,CAAC,OAAO,KAAK,IAAI;QAAE,OAAO,WAAW,CAAC;IAC/C,OAAO,IAAI,KAAK,CAAC,OAAO,GAAG,KAAK,CAAC,KAAK,CAAC,CAAC,CAAC,UAAU,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC;AAC7D,CAAC;AAED;;;;GAIG;AACH,SAAS,cAAc,CAAC,OAAkC;IACxD,IAAI,OAAO,KAAK,IAAI;QAAE,OAAO,cAAc,CAAC;IAC5C,MAAM,KAAK,GAAG;QACZ,GAAG,CAAC,OAAO,CAAC,IAAI,KAAK,IAAI,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,CAAC,YAAY,OAAO,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC,CAAC,UAAU,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC;QACtF,GAAG,CAAC,OAAO,CAAC,gBAAgB;YAC1B,CAAC,CAAC,CAAC,iCAAiC,CAAC;YACrC,CAAC,CAAC,OAAO,CAAC,MAAM,KAAK,IAAI;gBACvB,CAAC,CAAC,EAAE;gBACJ,CAAC,CAAC,CAAC,eAAe,OAAO,CAAC,MAAM,CAAC,OAAO,OAAO,OAAO,CAAC,MAAM,CAAC,OAAO,EAAE,CAAC,CAAC;QAC7E,GAAG,CAAC,OAAO,CAAC,KAAK,CAAC,MAAM,KAAK,CAAC,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,CAAC,GAAG,OAAO,CAAC,KAAK,CAAC,MAAM,wBAAwB,CAAC,CAAC;KACzF,CAAC;IACF,OAAO,KAAK,CAAC,MAAM,KAAK,CAAC,CAAC,CAAC,CAAC,OAAO,CAAC,CAAC,CAAC,KAAK,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC;AAC1D,CAAC;AAED,SAAS,WAAW,CAAC,MAAoB;IACvC,OAAO,CAAC,GAAG,CAAC,iBAAiB,MAAM,CAAC,OAAO,CAAC,CAAC,CAAC,SAAS,CAAC,CAAC,CAAC,SAAS,EAAE,CAAC,CAAC;IACvE,OAAO,CAAC,GAAG,CAAC,mBAAmB,MAAM,CAAC,OAAO,CAAC,OAAO,IAAI,MAAM,CAAC,OAAO,CAAC,OAAO,EAAE,CAAC,CAAC;IACnF,OAAO,CAAC,GAAG,CAAC,mBAAmB,MAAM,CAAC,OAAO,CAAC,IAAI,IAAI,MAAM,CAAC,OAAO,CAAC,QAAQ,IAAI,MAAM,CAAC,OAAO,CAAC,IAAI,EAAE,CAAC,CAAC;IACxG,OAAO,CAAC,GAAG,CAAC,mBAAmB,MAAM,CAAC,MAAM,CAAC,MAAM,KAAK,MAAM,CAAC,MAAM,CAAC,IAAI,EAAE,CAAC,CAAC;IAC9E,IAAI,MAAM,CAAC,MAAM,CAAC,KAAK,KAAK,IAAI;QAAE,OAAO,CAAC,GAAG,CAAC,mBAAmB,MAAM,CAAC,MAAM,CAAC,KAAK,EAAE,CAAC,CAAC;IACxF,OAAO,CAAC,GAAG,CAAC,mBAAmB,MAAM,CAAC,IAAI,CAAC,IAAI,IAAI,cAAc,EAAE,CAAC,CAAC;IACrE,OAAO,CAAC,GAAG,CACT,0BAA0B,KAAK,CAAC,MAAM,CAAC,IAAI,CAAC,MAAM,CAAC,WAAW,KAAK,CAAC,MAAM,CAAC,IAAI,CAAC,QAAQ,CAAC,YAAY,KAAK,CAAC,MAAM,CAAC,IAAI,CAAC,QAAQ,CAAC,EAAE,CACnI,CAAC;IACF,OAAO,CAAC,GAAG,CACT,6BAA6B,KAAK,CAAC,MAAM,CAAC,IAAI,CAAC,WAAW,CAAC,gBAAgB,KAAK,CAAC,MAAM,CAAC,IAAI,CAAC,SAAS,CAAC,MAAM,MAAM,CAAC,MAAM,CAAC,QAAQ,eAAe,MAAM,CAAC,MAAM,CAAC,cAAc,kBAAkB,CACjM,CAAC;IACF,OAAO,CAAC,GAAG,CACT,mBAAmB,MAAM,CAAC,cAAc,CAAC,UAAU,IAAI,QAAQ,MAAM,MAAM,CAAC,cAAc,CAAC,MAAM,EAAE,CACpG,CAAC;IACF,2EAA2E;IAC3E,4EAA4E;IAC5E,uCAAuC;IACvC,MAAM,MAAM,GAAG,MAAM,CAAC,MAAM,CAAC;IAC7B,OAAO,CAAC,GAAG,CACT,mBAAmB,MAAM,CAAC,KAAK,CAAC,MAAM,KAAK,CAAC,CAAC,CAAC,CAAC,QAAQ,CAAC,CAAC,CAAC,MAAM,CAAC,KAAK,CAAC,IAAI,CAAC,IAAI,CAAC,EAAE;UACjF,KAAK,MAAM,CAAC,WAAW,OAAO,MAAM,CAAC,YAAY,YAAY,MAAM,CAAC,YAAY,CAAC,MAAM,UAAU;UACjG,MAAM,MAAM,CAAC,UAAU,CAAC,MAAM,sBAAsB,UAAU,CAAC,MAAM,CAAC,KAAK,CAAC,EAAE,CACjF,CAAC;IACF,8EAA8E;IAC9E,8EAA8E;IAC9E,qBAAqB;IACrB,OAAO,CAAC,GAAG,CAAC,mBAAmB,cAAc,CAAC,MAAM,CAAC,SAAS,CAAC,EAAE,CAAC,CAAC;IACnE,IAAI,MAAM,CAAC,QAAQ,CAAC,MAAM,GAAG,CAAC,EAAE,CAAC;QAC/B,OAAO,CAAC,GAAG,CAAC,cAAc,CAAC,CAAC;QAC5B,KAAK,MAAM,OAAO,IAAI,MAAM,CAAC,QAAQ,EAAE,CAAC;YACtC,OAAO,CAAC,GAAG,CAAC,OAAO,OAAO,CAAC,QAAQ,KAAK,SAAS,CAAC,CAAC,CAAC,GAAG,CAAC,CAAC,CAAC,GAAG,IAAI,OAAO,CAAC,IAAI,KAAK,OAAO,CAAC,OAAO,EAAE,CAAC,CAAC;YACrG,0EAA0E;YAC1E,uEAAuE;YACvE,oEAAoE;YACpE,OAAO,CAAC,GAAG,CAAC,cAAc,OAAO,CAAC,GAAG,EAAE,CAAC,CAAC;QAC3C,CAAC;IACH,CAAC;AACH,CAAC","sourcesContent":["import type { Command } from \"commander\";\nimport { diagnose, type DoctorReport } from \"../core/doctor.js\";\nimport { emitJson } from \"../core/json.js\";\n\ninterface DoctorOptions {\n json?: boolean;\n}\n\nexport function registerDoctor(program: Command): void {\n program\n .command(\"doctor\")\n .description(\"Diagnose local loam configuration and docs-repo accessibility without writing\")\n .option(\"--json\", \"emit the machine contract instead of the human view\")\n .action(async (opts: DoctorOptions) => {\n const report = await diagnose();\n if (opts.json === true) emitJson({ command: \"doctor\", ...report });\n else printDoctor(report);\n if (!report.healthy) process.exitCode = 1;\n });\n}\n\nfunction yesNo(value: boolean): string {\n return value ? \"yes\" : \"no\";\n}\n\n/**\n * The stamp as one readable token. \"absent\" and \"unstamped\" are kept apart\n * because they are different repos: the first has no AGENTS.md at all, the\n * second has one nobody has claimed a version for.\n */\nfunction stampLabel(stamp: DoctorReport[\"agents\"][\"stamp\"]): string {\n if (!stamp.present) return \"absent\";\n if (stamp.version === null) return \"unstamped\";\n return `v${stamp.version}${stamp.stale ? \" (stale)\" : \"\"}`;\n}\n\n/**\n * What a killed writer left in the docs repo, as one line. \"clean\" is the\n * answer on every healthy repo, and \"(unresolved)\" keeps it apart from clean:\n * a docsDir that never resolved was never looked at.\n */\nfunction writePathLabel(residue: DoctorReport[\"writePath\"]): string {\n if (residue === null) return \"(unresolved)\";\n const parts = [\n ...(residue.lock === null ? [] : [`lock held${residue.lock.stale ? \" (stale)\" : \"\"}`]),\n ...(residue.intentUnreadable\n ? [\"interrupted commit (unreadable)\"]\n : residue.intent === null\n ? []\n : [`interrupted ${residue.intent.command} of ${residue.intent.feature}`]),\n ...(residue.temps.length === 0 ? [] : [`${residue.temps.length} orphaned temp file(s)`]),\n ];\n return parts.length === 0 ? \"clean\" : parts.join(\" · \");\n}\n\nfunction printDoctor(report: DoctorReport): void {\n console.log(`loam doctor — ${report.healthy ? \"healthy\" : \"blocked\"}`);\n console.log(` runtime ${report.runtime.package}@${report.runtime.version}`);\n console.log(` node ${report.runtime.node} ${report.runtime.platform}/${report.runtime.arch}`);\n console.log(` config ${report.config.status} ${report.config.path}`);\n if (report.config.error !== null) console.log(` ${report.config.error}`);\n console.log(` docsDir ${report.docs.path ?? \"(unresolved)\"}`);\n console.log(\n ` access exists ${yesNo(report.docs.exists)} · read ${yesNo(report.docs.readable)} · write ${yesNo(report.docs.writable)}`,\n );\n console.log(\n ` fleet services/ ${yesNo(report.docs.servicesDir)} · landscape ${yesNo(report.docs.landscape)} · ${report.counts.services} services · ${report.counts.activeFeatures} active features`,\n );\n console.log(\n ` binding ${report.currentService.configured ?? \"(none)\"} · ${report.currentService.status}`,\n );\n // The agent surface is reported whether or not it has drifted — like every\n // line above it, this is state, not a complaint. Only the findings block is\n // silent when there is nothing to say.\n const agents = report.agents;\n console.log(\n ` agents ${agents.tools.length === 0 ? \"(none)\" : agents.tools.join(\", \")}`\n + ` (${agents.toolsSource}) · ${agents.plannedFiles} files · ${agents.missingFiles.length} missing`\n + ` · ${agents.staleFiles.length} stale · AGENTS.md ${stampLabel(agents.stamp)}`,\n );\n // The write path is state too, and its clean answer is the one worth printing\n // most often: a reader who has just been told the docs are half-written needs\n // to see it go away.\n console.log(` write path ${writePathLabel(report.writePath)}`);\n if (report.findings.length > 0) {\n console.log(\"\\n findings\");\n for (const finding of report.findings) {\n console.log(` ${finding.severity === \"blocker\" ? \"✗\" : \"⚠\"} ${finding.code}: ${finding.message}`);\n // The fix is printed on its own indented line rather than appended to the\n // message: what is wrong and what to type are two different sentences,\n // and someone scanning a blocked repo reads down the `fix:` column.\n console.log(` fix: ${finding.fix}`);\n }\n }\n}\n"]}