@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.
- package/CHANGELOG.md +89 -0
- package/COMPARISON.md +63 -0
- package/LICENSE +21 -0
- package/MIGRATING-from-OpenSpec.md +202 -0
- package/README.md +241 -9
- package/SCHEMA.md +425 -0
- package/SECURITY.md +9 -0
- package/dist/cli.js +138 -0
- package/dist/cli.js.map +1 -0
- package/dist/commands/adopt.js +226 -0
- package/dist/commands/adopt.js.map +1 -0
- package/dist/commands/archive.js +1297 -0
- package/dist/commands/archive.js.map +1 -0
- package/dist/commands/delta.js +361 -0
- package/dist/commands/delta.js.map +1 -0
- package/dist/commands/dependencies.js +78 -0
- package/dist/commands/dependencies.js.map +1 -0
- package/dist/commands/doctor.js +85 -0
- package/dist/commands/doctor.js.map +1 -0
- package/dist/commands/gherkin.js +270 -0
- package/dist/commands/gherkin.js.map +1 -0
- package/dist/commands/init.js +250 -0
- package/dist/commands/init.js.map +1 -0
- package/dist/commands/list.js +361 -0
- package/dist/commands/list.js.map +1 -0
- package/dist/commands/migrate-openspec.js +933 -0
- package/dist/commands/migrate-openspec.js.map +1 -0
- package/dist/commands/new.js +412 -0
- package/dist/commands/new.js.map +1 -0
- package/dist/commands/rebase.js +358 -0
- package/dist/commands/rebase.js.map +1 -0
- package/dist/commands/show.js +325 -0
- package/dist/commands/show.js.map +1 -0
- package/dist/commands/status.js +270 -0
- package/dist/commands/status.js.map +1 -0
- package/dist/commands/unarchive.js +310 -0
- package/dist/commands/unarchive.js.map +1 -0
- package/dist/commands/validate.js +1609 -0
- package/dist/commands/validate.js.map +1 -0
- package/dist/commands/verify.js +759 -0
- package/dist/commands/verify.js.map +1 -0
- package/dist/commands/vouch.js +325 -0
- package/dist/commands/vouch.js.map +1 -0
- package/dist/core/agent.js +1823 -0
- package/dist/core/agent.js.map +1 -0
- package/dist/core/agents-stamp.js +68 -0
- package/dist/core/agents-stamp.js.map +1 -0
- package/dist/core/arch.js +104 -0
- package/dist/core/arch.js.map +1 -0
- package/dist/core/brief.js +733 -0
- package/dist/core/brief.js.map +1 -0
- package/dist/core/coherence.js +553 -0
- package/dist/core/coherence.js.map +1 -0
- package/dist/core/config.js +172 -0
- package/dist/core/config.js.map +1 -0
- package/dist/core/delta.js +457 -0
- package/dist/core/delta.js.map +1 -0
- package/dist/core/dependencies.js +352 -0
- package/dist/core/dependencies.js.map +1 -0
- package/dist/core/docs.js +143 -0
- package/dist/core/docs.js.map +1 -0
- package/dist/core/doctor.js +544 -0
- package/dist/core/doctor.js.map +1 -0
- package/dist/core/fleet-context.js +272 -0
- package/dist/core/fleet-context.js.map +1 -0
- package/dist/core/frontmatter.js +203 -0
- package/dist/core/frontmatter.js.map +1 -0
- package/dist/core/gherkin.js +521 -0
- package/dist/core/gherkin.js.map +1 -0
- package/dist/core/health.js +71 -0
- package/dist/core/health.js.map +1 -0
- package/dist/core/ids.js +93 -0
- package/dist/core/ids.js.map +1 -0
- package/dist/core/issue.js +9 -0
- package/dist/core/issue.js.map +1 -0
- package/dist/core/json.js +67 -0
- package/dist/core/json.js.map +1 -0
- package/dist/core/likec4.js +474 -0
- package/dist/core/likec4.js.map +1 -0
- package/dist/core/openapi-merge.js +519 -0
- package/dist/core/openapi-merge.js.map +1 -0
- package/dist/core/openapi.js +243 -0
- package/dist/core/openapi.js.map +1 -0
- package/dist/core/openspec-inventory.js +1403 -0
- package/dist/core/openspec-inventory.js.map +1 -0
- package/dist/core/path-safety.js +109 -0
- package/dist/core/path-safety.js.map +1 -0
- package/dist/core/provenance.js +695 -0
- package/dist/core/provenance.js.map +1 -0
- package/dist/core/repo.js +362 -0
- package/dist/core/repo.js.map +1 -0
- package/dist/core/report.js +38 -0
- package/dist/core/report.js.map +1 -0
- package/dist/core/results.js +201 -0
- package/dist/core/results.js.map +1 -0
- package/dist/core/spec.js +481 -0
- package/dist/core/spec.js.map +1 -0
- package/dist/core/staging.js +783 -0
- package/dist/core/staging.js.map +1 -0
- package/dist/core/status.js +1015 -0
- package/dist/core/status.js.map +1 -0
- package/dist/core/verify.js +793 -0
- package/dist/core/verify.js.map +1 -0
- package/dist/core/version.js +8 -0
- package/dist/core/version.js.map +1 -0
- package/package.json +63 -4
|
@@ -0,0 +1,325 @@
|
|
|
1
|
+
import { existsSync } from "node:fs";
|
|
2
|
+
import { readFile, readdir } from "node:fs/promises";
|
|
3
|
+
import { loadConfig } from "../core/config.js";
|
|
4
|
+
import { emitJson, fail, repoPath, reportNoConfig } from "../core/json.js";
|
|
5
|
+
import { listField, readFrontmatter, stringField } from "../core/frontmatter.js";
|
|
6
|
+
import { loadFile, serviceResolver } from "../core/likec4.js";
|
|
7
|
+
import { readOpenapi } from "../core/openapi.js";
|
|
8
|
+
import { DocsRepoUnavailableError, featurePaths, featureSpecPaths, landscapePath, listServices, resolveFeature, servicePaths, } from "../core/repo.js";
|
|
9
|
+
import { parseRequirements } from "../core/spec.js";
|
|
10
|
+
import { docsRepoReady, reportDocsRepoError } from "./validate.js";
|
|
11
|
+
export function registerShow(program) {
|
|
12
|
+
program
|
|
13
|
+
.command("show")
|
|
14
|
+
.argument("<target>", "service id or feature id")
|
|
15
|
+
.description("Show everything loam knows about a service or a feature")
|
|
16
|
+
.option("--json", "emit the machine contract instead of the human view")
|
|
17
|
+
.option("--type <kind>", "force the reading: service | feature")
|
|
18
|
+
.action(async (target, opts) => {
|
|
19
|
+
const json = opts.json === true;
|
|
20
|
+
if (opts.type !== undefined && opts.type !== "service" && opts.type !== "feature") {
|
|
21
|
+
fail(json, "invalid-option", `Unknown --type '${opts.type}'. Expected: service | feature.`);
|
|
22
|
+
return;
|
|
23
|
+
}
|
|
24
|
+
const forced = opts.type;
|
|
25
|
+
const config = await loadConfig();
|
|
26
|
+
if (!config) {
|
|
27
|
+
reportNoConfig(json);
|
|
28
|
+
return;
|
|
29
|
+
}
|
|
30
|
+
const { docsDir } = config;
|
|
31
|
+
// "No service or feature 'x' in <dir>" is a lie when <dir> is not a docs
|
|
32
|
+
// repo at all — the same refusal validate and list owe (docsRepoReady).
|
|
33
|
+
// `docs`, not `services`: a feature is readable from a repo whose
|
|
34
|
+
// services/ is missing, and if the target turns out to be a service the
|
|
35
|
+
// enumeration below refuses with `services-missing` anyway.
|
|
36
|
+
if (!docsRepoReady(json, docsDir, "docs"))
|
|
37
|
+
return;
|
|
38
|
+
try {
|
|
39
|
+
// A feature id is distinctive (FEAT-101); a service name is arbitrary. When
|
|
40
|
+
// both could match, the feature wins and --type forces the other reading.
|
|
41
|
+
const feature = forced === "service" ? null : await resolveFeature(docsDir, target, "include");
|
|
42
|
+
if (feature) {
|
|
43
|
+
await showFeature(docsDir, feature, json);
|
|
44
|
+
return;
|
|
45
|
+
}
|
|
46
|
+
const isService = forced !== "feature" && (await listServices(docsDir)).some((s) => s.id === target);
|
|
47
|
+
if (isService) {
|
|
48
|
+
await showService(docsDir, target, json);
|
|
49
|
+
return;
|
|
50
|
+
}
|
|
51
|
+
const looked = forced ? forced : "service or feature";
|
|
52
|
+
fail(json, "unknown-target", `No ${looked} '${target}' in ${docsDir}.`);
|
|
53
|
+
}
|
|
54
|
+
catch (err) {
|
|
55
|
+
if (err instanceof DocsRepoUnavailableError) {
|
|
56
|
+
reportDocsRepoError(json, err);
|
|
57
|
+
return;
|
|
58
|
+
}
|
|
59
|
+
// An artifact that exists but cannot be read used to escape as a stack
|
|
60
|
+
// trace (`internal` in --json), naming nothing. `show` reads one target,
|
|
61
|
+
// so it has nothing to fall back on — but it can at least say WHICH file.
|
|
62
|
+
const path = err.path;
|
|
63
|
+
if (path === undefined)
|
|
64
|
+
throw err;
|
|
65
|
+
fail(json, "repository-unavailable", `${path} could not be read, so '${target}' cannot be shown. ${err instanceof Error ? err.message : String(err)}`);
|
|
66
|
+
}
|
|
67
|
+
});
|
|
68
|
+
}
|
|
69
|
+
async function showService(docsDir, id, json) {
|
|
70
|
+
const paths = servicePaths(docsDir, id);
|
|
71
|
+
const has = {
|
|
72
|
+
model: existsSync(paths.model),
|
|
73
|
+
spec: existsSync(paths.spec),
|
|
74
|
+
// The architecture-obligations axis. It was invisible here and in `list`,
|
|
75
|
+
// which made the whole axis unnavigable: a service could carry twenty arch
|
|
76
|
+
// requirements — the outbox, the retries, the alerts — and every command a
|
|
77
|
+
// reader uses to find out what a service documents showed none of them.
|
|
78
|
+
archSpec: existsSync(paths.archSpec),
|
|
79
|
+
openapi: existsSync(paths.openapi),
|
|
80
|
+
runbook: existsSync(paths.runbook),
|
|
81
|
+
health: existsSync(paths.health),
|
|
82
|
+
};
|
|
83
|
+
const model = has.model
|
|
84
|
+
? await loadFile(paths.model)
|
|
85
|
+
: { errors: [], elements: [], relationships: [] };
|
|
86
|
+
const reqs = has.spec ? parseRequirements(await readFile(paths.spec, "utf8")) : [];
|
|
87
|
+
const archReqs = has.archSpec ? parseRequirements(await readFile(paths.archSpec, "utf8")) : [];
|
|
88
|
+
const adrs = await countAdrs(paths.adrsDir);
|
|
89
|
+
// `readOpenapi`, not `operationIds`: a contract that exists but does not parse
|
|
90
|
+
// came back from operationIds as an EMPTY operation list, indistinguishable
|
|
91
|
+
// from a service with no endpoints. `show` is what a reader opens to find out
|
|
92
|
+
// what a service offers, and answering "nothing" over a YAML error is the
|
|
93
|
+
// worst possible lie for that question. `x-loam-remove` entries are filtered
|
|
94
|
+
// out for the same reason validate filters them: they are deletion markers
|
|
95
|
+
// from a delta, not operations anybody can call.
|
|
96
|
+
const api = await readOpenapi(paths.openapi);
|
|
97
|
+
const ops = api.ops.filter((o) => !o.remove).map((o) => o.id);
|
|
98
|
+
const fm = await readFrontmatter(paths.spec);
|
|
99
|
+
const provenance = {
|
|
100
|
+
status: stringField(fm, "status") ?? null,
|
|
101
|
+
owner: stringField(fm, "owner") ?? null,
|
|
102
|
+
last_verified: stringField(fm, "last_verified") ?? null,
|
|
103
|
+
sources: listField(fm, "sources"),
|
|
104
|
+
};
|
|
105
|
+
const governs = (op) => reqs.filter((r) => r.operations.includes(op)).map((r) => r.name);
|
|
106
|
+
const { inbound, outbound } = await landscapeEdges(docsDir, id);
|
|
107
|
+
const requirementJson = (r) => ({
|
|
108
|
+
id: r.id,
|
|
109
|
+
name: r.name,
|
|
110
|
+
kind: r.kind,
|
|
111
|
+
scenarios: r.scenarios.length,
|
|
112
|
+
operations: r.operations,
|
|
113
|
+
covers: r.covers,
|
|
114
|
+
});
|
|
115
|
+
if (json) {
|
|
116
|
+
emitJson({
|
|
117
|
+
type: "service",
|
|
118
|
+
id,
|
|
119
|
+
path: repoPath(docsDir, paths.dir),
|
|
120
|
+
has,
|
|
121
|
+
adrs,
|
|
122
|
+
frontmatter: provenance,
|
|
123
|
+
model: {
|
|
124
|
+
elements: model.elements.length,
|
|
125
|
+
relationships: model.relationships.length,
|
|
126
|
+
errors: model.errors.map(errorText),
|
|
127
|
+
},
|
|
128
|
+
requirements: reqs.map(requirementJson),
|
|
129
|
+
archSpec: {
|
|
130
|
+
requirements: archReqs.length,
|
|
131
|
+
scenarios: scenarioCount(archReqs),
|
|
132
|
+
entries: archReqs.map(requirementJson),
|
|
133
|
+
},
|
|
134
|
+
openapi: {
|
|
135
|
+
unreadable: api.unreadable,
|
|
136
|
+
...(api.error === undefined ? {} : { error: api.error }),
|
|
137
|
+
},
|
|
138
|
+
operations: ops.map((op) => ({ id: op, governedBy: governs(op) })),
|
|
139
|
+
landscape: { inbound, outbound },
|
|
140
|
+
});
|
|
141
|
+
return;
|
|
142
|
+
}
|
|
143
|
+
const badge = [provenance.status, provenance.owner].filter((s) => s !== null).join(" · ");
|
|
144
|
+
console.log(`${id} ${repoPath(docsDir, paths.dir)}${badge ? ` ${badge}` : ""}\n`);
|
|
145
|
+
if (provenance.sources.length > 0 || provenance.last_verified !== null) {
|
|
146
|
+
console.log(" provenance");
|
|
147
|
+
if (provenance.last_verified !== null)
|
|
148
|
+
console.log(` verified ${provenance.last_verified}`);
|
|
149
|
+
for (const s of provenance.sources)
|
|
150
|
+
console.log(` sources ${s}`);
|
|
151
|
+
console.log("");
|
|
152
|
+
}
|
|
153
|
+
console.log(" artifacts");
|
|
154
|
+
const modelNote = model.errors.length > 0
|
|
155
|
+
? `${model.errors.length} error(s)`
|
|
156
|
+
: `${count(model.elements.length, "element")} · ${count(model.relationships.length, "relationship")}`;
|
|
157
|
+
console.log(` ${mark(has.model)} model.likec4 ${has.model ? modelNote : ""}`.trimEnd());
|
|
158
|
+
const specNote = (rs) => `${count(rs.length, "requirement")} · ${count(scenarioCount(rs), "scenario")}`;
|
|
159
|
+
console.log(` ${mark(has.spec)} spec.md ${has.spec ? specNote(reqs) : ""}`.trimEnd());
|
|
160
|
+
console.log(` ${mark(has.archSpec)} arch.spec.md ${has.archSpec ? specNote(archReqs) : ""}`.trimEnd());
|
|
161
|
+
const apiNote = api.unreadable ? "does not parse" : count(ops.length, "operation");
|
|
162
|
+
console.log(` ${mark(has.openapi)} openapi.yaml ${has.openapi ? apiNote : ""}`.trimEnd());
|
|
163
|
+
console.log(` ${mark(has.runbook)} runbook.md`);
|
|
164
|
+
console.log(` ${mark(has.health)} health.yaml`);
|
|
165
|
+
console.log(` ${mark(adrs > 0)} adrs/ ${adrs > 0 ? count(adrs, "decision") : ""}`.trimEnd());
|
|
166
|
+
const requirementLine = (r) => {
|
|
167
|
+
const govern = r.operations.length > 0 ? ` → ${r.operations.join(", ")}` : "";
|
|
168
|
+
const covers = r.covers.length > 0 ? ` covers ${r.covers.join(", ")}` : "";
|
|
169
|
+
return ` ${r.name} (${count(r.scenarios.length, "scenario")})${govern}${covers}`;
|
|
170
|
+
};
|
|
171
|
+
if (reqs.length > 0) {
|
|
172
|
+
console.log("\n requirements");
|
|
173
|
+
for (const r of reqs)
|
|
174
|
+
console.log(requirementLine(r));
|
|
175
|
+
}
|
|
176
|
+
if (archReqs.length > 0) {
|
|
177
|
+
console.log("\n arch requirements");
|
|
178
|
+
for (const r of archReqs)
|
|
179
|
+
console.log(requirementLine(r));
|
|
180
|
+
}
|
|
181
|
+
if (api.unreadable) {
|
|
182
|
+
console.log("\n operations");
|
|
183
|
+
console.log(` ✗ openapi.yaml does not parse — nothing here lists what ${id} offers`);
|
|
184
|
+
if (api.error !== undefined)
|
|
185
|
+
console.log(` ${api.error}`);
|
|
186
|
+
}
|
|
187
|
+
else if (ops.length > 0) {
|
|
188
|
+
console.log("\n operations");
|
|
189
|
+
for (const op of ops) {
|
|
190
|
+
const by = governs(op);
|
|
191
|
+
if (by.length > 0)
|
|
192
|
+
console.log(` ✓ ${op} governed by ${by.map((n) => `"${n}"`).join(", ")}`);
|
|
193
|
+
else
|
|
194
|
+
console.log(` ⚠ ${op} not governed by any requirement`);
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
if (inbound.length > 0 || outbound.length > 0) {
|
|
198
|
+
console.log("\n landscape");
|
|
199
|
+
for (const e of inbound)
|
|
200
|
+
console.log(` ← ${e.service}${edgeNote(e)}`);
|
|
201
|
+
for (const e of outbound)
|
|
202
|
+
console.log(` → ${e.service}${edgeNote(e)}`);
|
|
203
|
+
}
|
|
204
|
+
if (model.errors.length > 0) {
|
|
205
|
+
console.log("\n model errors");
|
|
206
|
+
for (const e of model.errors)
|
|
207
|
+
console.log(` ✗ ${errorText(e)}`);
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
/** Inbound/outbound edges for a service, read off the living landscape. */
|
|
211
|
+
async function landscapeEdges(docsDir, service) {
|
|
212
|
+
const path = landscapePath(docsDir);
|
|
213
|
+
if (!existsSync(path))
|
|
214
|
+
return { inbound: [], outbound: [] };
|
|
215
|
+
const land = await loadFile(path);
|
|
216
|
+
if (land.errors.length > 0)
|
|
217
|
+
return { inbound: [], outbound: [] };
|
|
218
|
+
// Edges are filed under the service an element is BOUND to, not under what the
|
|
219
|
+
// box is titled — otherwise renaming a box empties this list without a word —
|
|
220
|
+
// and an edge into a modelled container belongs to the service that owns it.
|
|
221
|
+
const known = new Set((await listServices(docsDir)).map((s) => s.id));
|
|
222
|
+
const svcOf = serviceResolver(land.elements, known);
|
|
223
|
+
const inbound = [];
|
|
224
|
+
const outbound = [];
|
|
225
|
+
for (const r of land.relationships) {
|
|
226
|
+
const edge = (service) => ({
|
|
227
|
+
service,
|
|
228
|
+
op: r.op ?? null,
|
|
229
|
+
title: r.title ?? null,
|
|
230
|
+
});
|
|
231
|
+
if (svcOf(r.target) === service)
|
|
232
|
+
inbound.push(edge(svcOf(r.source)));
|
|
233
|
+
else if (svcOf(r.source) === service)
|
|
234
|
+
outbound.push(edge(svcOf(r.target)));
|
|
235
|
+
}
|
|
236
|
+
return { inbound, outbound };
|
|
237
|
+
}
|
|
238
|
+
/* ------------------------------------------------------------------ */
|
|
239
|
+
/* Feature */
|
|
240
|
+
/* ------------------------------------------------------------------ */
|
|
241
|
+
async function showFeature(docsDir, feature, json) {
|
|
242
|
+
const paths = featurePaths(feature.dir);
|
|
243
|
+
const delta = feature.has.delta
|
|
244
|
+
? await loadFile(paths.delta)
|
|
245
|
+
: { errors: [], elements: [], relationships: [] };
|
|
246
|
+
const taggedEls = delta.elements.filter((e) => e.tags.includes(feature.id));
|
|
247
|
+
const taggedRels = delta.relationships.filter((r) => r.tags.includes(feature.id));
|
|
248
|
+
const services = [];
|
|
249
|
+
for (const svc of feature.services) {
|
|
250
|
+
const specPath = featureSpecPaths(feature.dir, svc).spec;
|
|
251
|
+
const reqs = existsSync(specPath) ? parseRequirements(await readFile(specPath, "utf8")) : [];
|
|
252
|
+
services.push({
|
|
253
|
+
id: svc,
|
|
254
|
+
added: reqs.filter((r) => r.kind === "ADDED").length,
|
|
255
|
+
modified: reqs.filter((r) => r.kind === "MODIFIED").length,
|
|
256
|
+
removed: reqs.filter((r) => r.kind === "REMOVED").length,
|
|
257
|
+
operations: [...new Set(reqs.flatMap((r) => r.operations))],
|
|
258
|
+
});
|
|
259
|
+
}
|
|
260
|
+
if (json) {
|
|
261
|
+
emitJson({
|
|
262
|
+
type: "feature",
|
|
263
|
+
id: feature.id,
|
|
264
|
+
dirName: feature.dirName,
|
|
265
|
+
path: repoPath(docsDir, feature.dir),
|
|
266
|
+
archived: feature.archived,
|
|
267
|
+
has: feature.has,
|
|
268
|
+
delta: {
|
|
269
|
+
elements: taggedEls.length,
|
|
270
|
+
relationships: taggedRels.length,
|
|
271
|
+
errors: delta.errors.map(errorText),
|
|
272
|
+
},
|
|
273
|
+
services,
|
|
274
|
+
});
|
|
275
|
+
return;
|
|
276
|
+
}
|
|
277
|
+
const state = feature.archived ? "archived" : "active";
|
|
278
|
+
console.log(`${feature.id} ${repoPath(docsDir, feature.dir)} ${state}\n`);
|
|
279
|
+
console.log(" artifacts");
|
|
280
|
+
console.log(` ${mark(feature.has.intent)} intent.md`);
|
|
281
|
+
const deltaNote = delta.errors.length > 0
|
|
282
|
+
? `${delta.errors.length} error(s)`
|
|
283
|
+
: `${count(taggedEls.length, "element")} · ${count(taggedRels.length, "relationship")} tagged ${feature.id}`;
|
|
284
|
+
console.log(` ${mark(feature.has.delta)} delta.likec4 ${feature.has.delta ? deltaNote : ""}`.trimEnd());
|
|
285
|
+
if (services.length > 0) {
|
|
286
|
+
console.log("\n services");
|
|
287
|
+
const width = Math.max(...services.map((s) => s.id.length));
|
|
288
|
+
for (const s of services) {
|
|
289
|
+
const ops = s.operations.length > 0 ? ` · ${s.operations.join(", ")}` : "";
|
|
290
|
+
console.log(` ${s.id.padEnd(width)} +${s.added} ~${s.modified} -${s.removed} requirements${ops}`);
|
|
291
|
+
}
|
|
292
|
+
}
|
|
293
|
+
if (delta.errors.length > 0) {
|
|
294
|
+
console.log("\n delta errors");
|
|
295
|
+
for (const e of delta.errors)
|
|
296
|
+
console.log(` ✗ ${errorText(e)}`);
|
|
297
|
+
}
|
|
298
|
+
}
|
|
299
|
+
/* ------------------------------------------------------------------ */
|
|
300
|
+
/** "-" for absent, matching `list`: ✗ is the error glyph here, and a missing runbook is not an error. */
|
|
301
|
+
function mark(present) {
|
|
302
|
+
return present ? "✓" : "-";
|
|
303
|
+
}
|
|
304
|
+
function count(n, noun) {
|
|
305
|
+
return `${n} ${noun}${n === 1 ? "" : "s"}`;
|
|
306
|
+
}
|
|
307
|
+
/** ADRs are markdown files under adrs/ — the same rule `list` counts by. */
|
|
308
|
+
async function countAdrs(dir) {
|
|
309
|
+
if (!existsSync(dir))
|
|
310
|
+
return 0;
|
|
311
|
+
const entries = await readdir(dir, { withFileTypes: true });
|
|
312
|
+
return entries.filter((e) => e.isFile() && e.name.endsWith(".md")).length;
|
|
313
|
+
}
|
|
314
|
+
function scenarioCount(reqs) {
|
|
315
|
+
return reqs.reduce((n, r) => n + r.scenarios.length, 0);
|
|
316
|
+
}
|
|
317
|
+
function edgeNote(e) {
|
|
318
|
+
if (e.op)
|
|
319
|
+
return ` ${e.op}`;
|
|
320
|
+
return e.title ? ` "${e.title}"` : "";
|
|
321
|
+
}
|
|
322
|
+
function errorText(e) {
|
|
323
|
+
return typeof e.line === "number" ? `L${e.line}: ${e.message}` : e.message;
|
|
324
|
+
}
|
|
325
|
+
//# sourceMappingURL=show.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"show.js","sourceRoot":"","sources":["../../src/commands/show.ts"],"names":[],"mappings":"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","sourcesContent":["import type { Command } from \"commander\";\nimport { existsSync } from \"node:fs\";\nimport { readFile, readdir } from \"node:fs/promises\";\nimport { loadConfig } from \"../core/config.js\";\nimport { emitJson, fail, repoPath, reportNoConfig } from \"../core/json.js\";\nimport { listField, readFrontmatter, stringField } from \"../core/frontmatter.js\";\nimport { loadFile, serviceResolver, type Elem } from \"../core/likec4.js\";\nimport { readOpenapi } from \"../core/openapi.js\";\nimport {\n DocsRepoUnavailableError,\n featurePaths,\n featureSpecPaths,\n landscapePath,\n listServices,\n resolveFeature,\n servicePaths,\n type FeatureEntry,\n} from \"../core/repo.js\";\nimport { parseRequirements, type Requirement } from \"../core/spec.js\";\nimport { docsRepoReady, reportDocsRepoError } from \"./validate.js\";\n\ntype TargetType = \"service\" | \"feature\";\n\ninterface ShowOptions {\n json?: boolean;\n type?: string;\n}\n\nexport function registerShow(program: Command): void {\n program\n .command(\"show\")\n .argument(\"<target>\", \"service id or feature id\")\n .description(\"Show everything loam knows about a service or a feature\")\n .option(\"--json\", \"emit the machine contract instead of the human view\")\n .option(\"--type <kind>\", \"force the reading: service | feature\")\n .action(async (target: string, opts: ShowOptions) => {\n const json = opts.json === true;\n if (opts.type !== undefined && opts.type !== \"service\" && opts.type !== \"feature\") {\n fail(json, \"invalid-option\", `Unknown --type '${opts.type}'. Expected: service | feature.`);\n return;\n }\n const forced = opts.type as TargetType | undefined;\n\n const config = await loadConfig();\n if (!config) {\n reportNoConfig(json);\n return;\n }\n const { docsDir } = config;\n // \"No service or feature 'x' in <dir>\" is a lie when <dir> is not a docs\n // repo at all — the same refusal validate and list owe (docsRepoReady).\n // `docs`, not `services`: a feature is readable from a repo whose\n // services/ is missing, and if the target turns out to be a service the\n // enumeration below refuses with `services-missing` anyway.\n if (!docsRepoReady(json, docsDir, \"docs\")) return;\n\n try {\n // A feature id is distinctive (FEAT-101); a service name is arbitrary. When\n // both could match, the feature wins and --type forces the other reading.\n const feature =\n forced === \"service\" ? null : await resolveFeature(docsDir, target, \"include\");\n if (feature) {\n await showFeature(docsDir, feature, json);\n return;\n }\n const isService =\n forced !== \"feature\" && (await listServices(docsDir)).some((s) => s.id === target);\n if (isService) {\n await showService(docsDir, target, json);\n return;\n }\n\n const looked = forced ? forced : \"service or feature\";\n fail(json, \"unknown-target\", `No ${looked} '${target}' in ${docsDir}.`);\n } catch (err) {\n if (err instanceof DocsRepoUnavailableError) {\n reportDocsRepoError(json, err);\n return;\n }\n // An artifact that exists but cannot be read used to escape as a stack\n // trace (`internal` in --json), naming nothing. `show` reads one target,\n // so it has nothing to fall back on — but it can at least say WHICH file.\n const path = (err as NodeJS.ErrnoException).path;\n if (path === undefined) throw err;\n fail(\n json,\n \"repository-unavailable\",\n `${path} could not be read, so '${target}' cannot be shown. ${\n err instanceof Error ? err.message : String(err)\n }`,\n );\n }\n });\n}\n\n/* ------------------------------------------------------------------ */\n/* Service */\n/* ------------------------------------------------------------------ */\n\ninterface Edge {\n service: string;\n op: string | null;\n title: string | null;\n}\n\nasync function showService(docsDir: string, id: string, json: boolean): Promise<void> {\n const paths = servicePaths(docsDir, id);\n const has = {\n model: existsSync(paths.model),\n spec: existsSync(paths.spec),\n // The architecture-obligations axis. It was invisible here and in `list`,\n // which made the whole axis unnavigable: a service could carry twenty arch\n // requirements — the outbox, the retries, the alerts — and every command a\n // reader uses to find out what a service documents showed none of them.\n archSpec: existsSync(paths.archSpec),\n openapi: existsSync(paths.openapi),\n runbook: existsSync(paths.runbook),\n health: existsSync(paths.health),\n };\n\n const model = has.model\n ? await loadFile(paths.model)\n : { errors: [], elements: [], relationships: [] };\n const reqs = has.spec ? parseRequirements(await readFile(paths.spec, \"utf8\")) : [];\n const archReqs = has.archSpec ? parseRequirements(await readFile(paths.archSpec, \"utf8\")) : [];\n const adrs = await countAdrs(paths.adrsDir);\n // `readOpenapi`, not `operationIds`: a contract that exists but does not parse\n // came back from operationIds as an EMPTY operation list, indistinguishable\n // from a service with no endpoints. `show` is what a reader opens to find out\n // what a service offers, and answering \"nothing\" over a YAML error is the\n // worst possible lie for that question. `x-loam-remove` entries are filtered\n // out for the same reason validate filters them: they are deletion markers\n // from a delta, not operations anybody can call.\n const api = await readOpenapi(paths.openapi);\n const ops = api.ops.filter((o) => !o.remove).map((o) => o.id);\n\n const fm = await readFrontmatter(paths.spec);\n const provenance = {\n status: stringField(fm, \"status\") ?? null,\n owner: stringField(fm, \"owner\") ?? null,\n last_verified: stringField(fm, \"last_verified\") ?? null,\n sources: listField(fm, \"sources\"),\n };\n\n const governs = (op: string): string[] =>\n reqs.filter((r) => r.operations.includes(op)).map((r) => r.name);\n\n const { inbound, outbound } = await landscapeEdges(docsDir, id);\n\n const requirementJson = (r: Requirement): Record<string, unknown> => ({\n id: r.id,\n name: r.name,\n kind: r.kind,\n scenarios: r.scenarios.length,\n operations: r.operations,\n covers: r.covers,\n });\n\n if (json) {\n emitJson({\n type: \"service\",\n id,\n path: repoPath(docsDir, paths.dir),\n has,\n adrs,\n frontmatter: provenance,\n model: {\n elements: model.elements.length,\n relationships: model.relationships.length,\n errors: model.errors.map(errorText),\n },\n requirements: reqs.map(requirementJson),\n archSpec: {\n requirements: archReqs.length,\n scenarios: scenarioCount(archReqs),\n entries: archReqs.map(requirementJson),\n },\n openapi: {\n unreadable: api.unreadable,\n ...(api.error === undefined ? {} : { error: api.error }),\n },\n operations: ops.map((op) => ({ id: op, governedBy: governs(op) })),\n landscape: { inbound, outbound },\n });\n return;\n }\n\n const badge = [provenance.status, provenance.owner].filter((s) => s !== null).join(\" · \");\n console.log(`${id} ${repoPath(docsDir, paths.dir)}${badge ? ` ${badge}` : \"\"}\\n`);\n\n if (provenance.sources.length > 0 || provenance.last_verified !== null) {\n console.log(\" provenance\");\n if (provenance.last_verified !== null) console.log(` verified ${provenance.last_verified}`);\n for (const s of provenance.sources) console.log(` sources ${s}`);\n console.log(\"\");\n }\n\n console.log(\" artifacts\");\n const modelNote =\n model.errors.length > 0\n ? `${model.errors.length} error(s)`\n : `${count(model.elements.length, \"element\")} · ${count(model.relationships.length, \"relationship\")}`;\n console.log(` ${mark(has.model)} model.likec4 ${has.model ? modelNote : \"\"}`.trimEnd());\n const specNote = (rs: Requirement[]): string =>\n `${count(rs.length, \"requirement\")} · ${count(scenarioCount(rs), \"scenario\")}`;\n console.log(` ${mark(has.spec)} spec.md ${has.spec ? specNote(reqs) : \"\"}`.trimEnd());\n console.log(\n ` ${mark(has.archSpec)} arch.spec.md ${has.archSpec ? specNote(archReqs) : \"\"}`.trimEnd(),\n );\n const apiNote = api.unreadable ? \"does not parse\" : count(ops.length, \"operation\");\n console.log(` ${mark(has.openapi)} openapi.yaml ${has.openapi ? apiNote : \"\"}`.trimEnd());\n console.log(` ${mark(has.runbook)} runbook.md`);\n console.log(` ${mark(has.health)} health.yaml`);\n console.log(` ${mark(adrs > 0)} adrs/ ${adrs > 0 ? count(adrs, \"decision\") : \"\"}`.trimEnd());\n\n const requirementLine = (r: Requirement): string => {\n const govern = r.operations.length > 0 ? ` → ${r.operations.join(\", \")}` : \"\";\n const covers = r.covers.length > 0 ? ` covers ${r.covers.join(\", \")}` : \"\";\n return ` ${r.name} (${count(r.scenarios.length, \"scenario\")})${govern}${covers}`;\n };\n if (reqs.length > 0) {\n console.log(\"\\n requirements\");\n for (const r of reqs) console.log(requirementLine(r));\n }\n if (archReqs.length > 0) {\n console.log(\"\\n arch requirements\");\n for (const r of archReqs) console.log(requirementLine(r));\n }\n\n if (api.unreadable) {\n console.log(\"\\n operations\");\n console.log(` ✗ openapi.yaml does not parse — nothing here lists what ${id} offers`);\n if (api.error !== undefined) console.log(` ${api.error}`);\n } else if (ops.length > 0) {\n console.log(\"\\n operations\");\n for (const op of ops) {\n const by = governs(op);\n if (by.length > 0) console.log(` ✓ ${op} governed by ${by.map((n) => `\"${n}\"`).join(\", \")}`);\n else console.log(` ⚠ ${op} not governed by any requirement`);\n }\n }\n\n if (inbound.length > 0 || outbound.length > 0) {\n console.log(\"\\n landscape\");\n for (const e of inbound) console.log(` ← ${e.service}${edgeNote(e)}`);\n for (const e of outbound) console.log(` → ${e.service}${edgeNote(e)}`);\n }\n\n if (model.errors.length > 0) {\n console.log(\"\\n model errors\");\n for (const e of model.errors) console.log(` ✗ ${errorText(e)}`);\n }\n}\n\n/** Inbound/outbound edges for a service, read off the living landscape. */\nasync function landscapeEdges(\n docsDir: string,\n service: string,\n): Promise<{ inbound: Edge[]; outbound: Edge[] }> {\n const path = landscapePath(docsDir);\n if (!existsSync(path)) return { inbound: [], outbound: [] };\n const land = await loadFile(path);\n if (land.errors.length > 0) return { inbound: [], outbound: [] };\n // Edges are filed under the service an element is BOUND to, not under what the\n // box is titled — otherwise renaming a box empties this list without a word —\n // and an edge into a modelled container belongs to the service that owns it.\n const known = new Set((await listServices(docsDir)).map((s) => s.id));\n const svcOf = serviceResolver(land.elements, known);\n\n const inbound: Edge[] = [];\n const outbound: Edge[] = [];\n for (const r of land.relationships) {\n const edge = (service: string): Edge => ({\n service,\n op: r.op ?? null,\n title: r.title ?? null,\n });\n if (svcOf(r.target) === service) inbound.push(edge(svcOf(r.source)));\n else if (svcOf(r.source) === service) outbound.push(edge(svcOf(r.target)));\n }\n return { inbound, outbound };\n}\n\n/* ------------------------------------------------------------------ */\n/* Feature */\n/* ------------------------------------------------------------------ */\n\nasync function showFeature(docsDir: string, feature: FeatureEntry, json: boolean): Promise<void> {\n const paths = featurePaths(feature.dir);\n const delta = feature.has.delta\n ? await loadFile(paths.delta)\n : { errors: [], elements: [] as Elem[], relationships: [] };\n const taggedEls = delta.elements.filter((e) => e.tags.includes(feature.id));\n const taggedRels = delta.relationships.filter((r) => r.tags.includes(feature.id));\n\n const services = [];\n for (const svc of feature.services) {\n const specPath = featureSpecPaths(feature.dir, svc).spec;\n const reqs = existsSync(specPath) ? parseRequirements(await readFile(specPath, \"utf8\")) : [];\n services.push({\n id: svc,\n added: reqs.filter((r) => r.kind === \"ADDED\").length,\n modified: reqs.filter((r) => r.kind === \"MODIFIED\").length,\n removed: reqs.filter((r) => r.kind === \"REMOVED\").length,\n operations: [...new Set(reqs.flatMap((r) => r.operations))],\n });\n }\n\n if (json) {\n emitJson({\n type: \"feature\",\n id: feature.id,\n dirName: feature.dirName,\n path: repoPath(docsDir, feature.dir),\n archived: feature.archived,\n has: feature.has,\n delta: {\n elements: taggedEls.length,\n relationships: taggedRels.length,\n errors: delta.errors.map(errorText),\n },\n services,\n });\n return;\n }\n\n const state = feature.archived ? \"archived\" : \"active\";\n console.log(`${feature.id} ${repoPath(docsDir, feature.dir)} ${state}\\n`);\n\n console.log(\" artifacts\");\n console.log(` ${mark(feature.has.intent)} intent.md`);\n const deltaNote =\n delta.errors.length > 0\n ? `${delta.errors.length} error(s)`\n : `${count(taggedEls.length, \"element\")} · ${count(taggedRels.length, \"relationship\")} tagged ${feature.id}`;\n console.log(` ${mark(feature.has.delta)} delta.likec4 ${feature.has.delta ? deltaNote : \"\"}`.trimEnd());\n\n if (services.length > 0) {\n console.log(\"\\n services\");\n const width = Math.max(...services.map((s) => s.id.length));\n for (const s of services) {\n const ops = s.operations.length > 0 ? ` · ${s.operations.join(\", \")}` : \"\";\n console.log(\n ` ${s.id.padEnd(width)} +${s.added} ~${s.modified} -${s.removed} requirements${ops}`,\n );\n }\n }\n\n if (delta.errors.length > 0) {\n console.log(\"\\n delta errors\");\n for (const e of delta.errors) console.log(` ✗ ${errorText(e)}`);\n }\n}\n\n/* ------------------------------------------------------------------ */\n\n/** \"-\" for absent, matching `list`: ✗ is the error glyph here, and a missing runbook is not an error. */\nfunction mark(present: boolean): string {\n return present ? \"✓\" : \"-\";\n}\n\nfunction count(n: number, noun: string): string {\n return `${n} ${noun}${n === 1 ? \"\" : \"s\"}`;\n}\n\n/** ADRs are markdown files under adrs/ — the same rule `list` counts by. */\nasync function countAdrs(dir: string): Promise<number> {\n if (!existsSync(dir)) return 0;\n const entries = await readdir(dir, { withFileTypes: true });\n return entries.filter((e) => e.isFile() && e.name.endsWith(\".md\")).length;\n}\n\nfunction scenarioCount(reqs: Requirement[]): number {\n return reqs.reduce((n, r) => n + r.scenarios.length, 0);\n}\n\nfunction edgeNote(e: Edge): string {\n if (e.op) return ` ${e.op}`;\n return e.title ? ` \"${e.title}\"` : \"\";\n}\n\nfunction errorText(e: { message: string; line?: number }): string {\n return typeof e.line === \"number\" ? `L${e.line}: ${e.message}` : e.message;\n}\n"]}
|