@ybotok/loam 0.0.0 → 0.1.0-beta.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +138 -0
- package/COMPARISON.md +63 -0
- package/LICENSE +21 -0
- package/MIGRATING-from-OpenSpec.md +202 -0
- package/README.md +246 -9
- package/SCHEMA.md +425 -0
- package/SECURITY.md +9 -0
- package/dist/cli.js +142 -0
- package/dist/cli.js.map +1 -0
- package/dist/commands/adopt.js +240 -0
- package/dist/commands/adopt.js.map +1 -0
- package/dist/commands/archive.js +1313 -0
- package/dist/commands/archive.js.map +1 -0
- package/dist/commands/delta.js +388 -0
- package/dist/commands/delta.js.map +1 -0
- package/dist/commands/dependencies.js +83 -0
- package/dist/commands/dependencies.js.map +1 -0
- package/dist/commands/docs-repo-gate.js +104 -0
- package/dist/commands/docs-repo-gate.js.map +1 -0
- package/dist/commands/doctor.js +85 -0
- package/dist/commands/doctor.js.map +1 -0
- package/dist/commands/explore.js +195 -0
- package/dist/commands/explore.js.map +1 -0
- package/dist/commands/format.js +17 -0
- package/dist/commands/format.js.map +1 -0
- package/dist/commands/gherkin.js +309 -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/instructions.js +70 -0
- package/dist/commands/instructions.js.map +1 -0
- package/dist/commands/list.js +291 -0
- package/dist/commands/list.js.map +1 -0
- package/dist/commands/migrate-openspec.js +950 -0
- package/dist/commands/migrate-openspec.js.map +1 -0
- package/dist/commands/new.js +413 -0
- package/dist/commands/new.js.map +1 -0
- package/dist/commands/rebase.js +356 -0
- package/dist/commands/rebase.js.map +1 -0
- package/dist/commands/show.js +320 -0
- package/dist/commands/show.js.map +1 -0
- package/dist/commands/status.js +254 -0
- package/dist/commands/status.js.map +1 -0
- package/dist/commands/unarchive.js +317 -0
- package/dist/commands/unarchive.js.map +1 -0
- package/dist/commands/validate.js +1591 -0
- package/dist/commands/validate.js.map +1 -0
- package/dist/commands/verify.js +775 -0
- package/dist/commands/verify.js.map +1 -0
- package/dist/commands/vouch.js +343 -0
- package/dist/commands/vouch.js.map +1 -0
- package/dist/core/agent.js +2065 -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 +130 -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 +577 -0
- package/dist/core/coherence.js.map +1 -0
- package/dist/core/concurrency.js +51 -0
- package/dist/core/concurrency.js.map +1 -0
- package/dist/core/config.js +180 -0
- package/dist/core/config.js.map +1 -0
- package/dist/core/delta.js +476 -0
- package/dist/core/delta.js.map +1 -0
- package/dist/core/dependencies.js +356 -0
- package/dist/core/dependencies.js.map +1 -0
- package/dist/core/docs.js +144 -0
- package/dist/core/docs.js.map +1 -0
- package/dist/core/doctor.js +559 -0
- package/dist/core/doctor.js.map +1 -0
- package/dist/core/document-bytes.js +59 -0
- package/dist/core/document-bytes.js.map +1 -0
- package/dist/core/explore.js +225 -0
- package/dist/core/explore.js.map +1 -0
- package/dist/core/fleet-context.js +225 -0
- package/dist/core/fleet-context.js.map +1 -0
- package/dist/core/frontmatter.js +199 -0
- package/dist/core/frontmatter.js.map +1 -0
- package/dist/core/gherkin.js +463 -0
- package/dist/core/gherkin.js.map +1 -0
- package/dist/core/health.js +67 -0
- package/dist/core/health.js.map +1 -0
- package/dist/core/ids.js +122 -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 +74 -0
- package/dist/core/json.js.map +1 -0
- package/dist/core/likec4.js +479 -0
- package/dist/core/likec4.js.map +1 -0
- package/dist/core/maturity.js +66 -0
- package/dist/core/maturity.js.map +1 -0
- package/dist/core/openapi-merge.js +552 -0
- package/dist/core/openapi-merge.js.map +1 -0
- package/dist/core/openapi.js +255 -0
- package/dist/core/openapi.js.map +1 -0
- package/dist/core/openspec-inventory.js +1440 -0
- package/dist/core/openspec-inventory.js.map +1 -0
- package/dist/core/path-safety.js +168 -0
- package/dist/core/path-safety.js.map +1 -0
- package/dist/core/provenance.js +720 -0
- package/dist/core/provenance.js.map +1 -0
- package/dist/core/records.js +33 -0
- package/dist/core/records.js.map +1 -0
- package/dist/core/repo.js +436 -0
- package/dist/core/repo.js.map +1 -0
- package/dist/core/report.js +47 -0
- package/dist/core/report.js.map +1 -0
- package/dist/core/results.js +199 -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 +890 -0
- package/dist/core/staging.js.map +1 -0
- package/dist/core/status.js +1104 -0
- package/dist/core/status.js.map +1 -0
- package/dist/core/steps.js +72 -0
- package/dist/core/steps.js.map +1 -0
- package/dist/core/verify.js +807 -0
- package/dist/core/verify.js.map +1 -0
- package/dist/core/version.js +24 -0
- package/dist/core/version.js.map +1 -0
- package/package.json +63 -4
|
@@ -0,0 +1,320 @@
|
|
|
1
|
+
import { existsSync } from "node:fs";
|
|
2
|
+
import { readFile } from "node:fs/promises";
|
|
3
|
+
import { loadConfig } from "../core/config.js";
|
|
4
|
+
import { FleetContext } from "../core/fleet-context.js";
|
|
5
|
+
import { emitJson, fail, repoPath, reportNoConfig } from "../core/json.js";
|
|
6
|
+
import { listField, readFrontmatter, stringField } from "../core/frontmatter.js";
|
|
7
|
+
import { loadFile, serviceResolver } from "../core/likec4.js";
|
|
8
|
+
import { readOpenapi } from "../core/openapi.js";
|
|
9
|
+
import { DocsRepoUnavailableError, countMarkdown, featurePaths, featureSpecPaths, landscapePath, listServices, resolveFeature, servicePaths, } from "../core/repo.js";
|
|
10
|
+
import { parseRequirements } from "../core/spec.js";
|
|
11
|
+
import { docsRepoReady, reportDocsRepoError, reportRepositoryUnavailable } from "./docs-repo-gate.js";
|
|
12
|
+
import { plural } from "./format.js";
|
|
13
|
+
export function registerShow(program) {
|
|
14
|
+
program
|
|
15
|
+
.command("show")
|
|
16
|
+
.argument("<target>", "service id or feature id")
|
|
17
|
+
.description("Show everything loam knows about a service or a feature")
|
|
18
|
+
.option("--json", "emit the machine contract instead of the human view")
|
|
19
|
+
.option("--type <kind>", "force the reading: service | feature")
|
|
20
|
+
.action(async (target, opts) => {
|
|
21
|
+
const json = opts.json === true;
|
|
22
|
+
if (opts.type !== undefined && opts.type !== "service" && opts.type !== "feature") {
|
|
23
|
+
fail(json, "invalid-option", `Unknown --type '${opts.type}'. Expected: service | feature.`);
|
|
24
|
+
return;
|
|
25
|
+
}
|
|
26
|
+
const forced = opts.type;
|
|
27
|
+
const config = await loadConfig();
|
|
28
|
+
if (!config) {
|
|
29
|
+
reportNoConfig(json);
|
|
30
|
+
return;
|
|
31
|
+
}
|
|
32
|
+
const { docsDir } = config;
|
|
33
|
+
// "No service or feature 'x' in <dir>" is a lie when <dir> is not a docs
|
|
34
|
+
// repo at all — the same refusal validate and list owe (docsRepoReady).
|
|
35
|
+
// `docs`, not `services`: a feature is readable from a repo whose
|
|
36
|
+
// services/ is missing, and if the target turns out to be a service the
|
|
37
|
+
// enumeration below refuses with `services-missing` anyway.
|
|
38
|
+
if (!docsRepoReady(json, docsDir, "docs"))
|
|
39
|
+
return;
|
|
40
|
+
// One read index for the invocation. Deciding the target is a service
|
|
41
|
+
// enumerates the fleet, and so does resolving the landscape's edges to the
|
|
42
|
+
// services that own them — `loam show <service>` walked services/ twice for
|
|
43
|
+
// one answer. The context memoises only within this invocation, so a later
|
|
44
|
+
// run still sees whatever is on disk then.
|
|
45
|
+
const context = new FleetContext();
|
|
46
|
+
try {
|
|
47
|
+
// A feature id is distinctive (FEAT-101); a service name is arbitrary. When
|
|
48
|
+
// both could match, the feature wins and --type forces the other reading.
|
|
49
|
+
const feature = forced === "service" ? null : await resolveFeature(docsDir, target, "include", context);
|
|
50
|
+
if (feature) {
|
|
51
|
+
await showFeature(docsDir, feature, json);
|
|
52
|
+
return;
|
|
53
|
+
}
|
|
54
|
+
const isService = forced !== "feature" && (await listServices(docsDir, context)).some((s) => s.id === target);
|
|
55
|
+
if (isService) {
|
|
56
|
+
await showService(docsDir, target, json, context);
|
|
57
|
+
return;
|
|
58
|
+
}
|
|
59
|
+
const looked = forced ? forced : "service or feature";
|
|
60
|
+
fail(json, "unknown-target", `No ${looked} '${target}' in ${docsDir}.`);
|
|
61
|
+
}
|
|
62
|
+
catch (err) {
|
|
63
|
+
if (err instanceof DocsRepoUnavailableError) {
|
|
64
|
+
reportDocsRepoError(json, err);
|
|
65
|
+
return;
|
|
66
|
+
}
|
|
67
|
+
// An artifact that exists but cannot be read used to escape as a stack
|
|
68
|
+
// trace (`internal` in --json), naming nothing. `show` reads one target,
|
|
69
|
+
// so it has nothing to fall back on — but it can at least say WHICH file.
|
|
70
|
+
reportRepositoryUnavailable(json, err, `'${target}' cannot be shown`, docsDir);
|
|
71
|
+
}
|
|
72
|
+
});
|
|
73
|
+
}
|
|
74
|
+
async function showService(docsDir, id, json, context) {
|
|
75
|
+
const paths = servicePaths(docsDir, id);
|
|
76
|
+
const has = {
|
|
77
|
+
model: existsSync(paths.model),
|
|
78
|
+
spec: existsSync(paths.spec),
|
|
79
|
+
// The architecture-obligations axis. It was invisible here and in `list`,
|
|
80
|
+
// which made the whole axis unnavigable: a service could carry twenty arch
|
|
81
|
+
// requirements — the outbox, the retries, the alerts — and every command a
|
|
82
|
+
// reader uses to find out what a service documents showed none of them.
|
|
83
|
+
archSpec: existsSync(paths.archSpec),
|
|
84
|
+
openapi: existsSync(paths.openapi),
|
|
85
|
+
runbook: existsSync(paths.runbook),
|
|
86
|
+
health: existsSync(paths.health),
|
|
87
|
+
};
|
|
88
|
+
const model = has.model
|
|
89
|
+
? await loadFile(paths.model)
|
|
90
|
+
: { errors: [], elements: [], relationships: [] };
|
|
91
|
+
const reqs = has.spec ? parseRequirements(await readFile(paths.spec, "utf8")) : [];
|
|
92
|
+
const archReqs = has.archSpec ? parseRequirements(await readFile(paths.archSpec, "utf8")) : [];
|
|
93
|
+
const adrs = await countMarkdown(paths.adrsDir);
|
|
94
|
+
// `readOpenapi`, not `operationIds`: a contract that exists but does not parse
|
|
95
|
+
// came back from operationIds as an EMPTY operation list, indistinguishable
|
|
96
|
+
// from a service with no endpoints. `show` is what a reader opens to find out
|
|
97
|
+
// what a service offers, and answering "nothing" over a YAML error is the
|
|
98
|
+
// worst possible lie for that question. `x-loam-remove` entries are filtered
|
|
99
|
+
// out for the same reason validate filters them: they are deletion markers
|
|
100
|
+
// from a delta, not operations anybody can call.
|
|
101
|
+
const api = await readOpenapi(paths.openapi);
|
|
102
|
+
const ops = api.ops.filter((o) => !o.remove).map((o) => o.id);
|
|
103
|
+
const fm = await readFrontmatter(paths.spec);
|
|
104
|
+
const provenance = {
|
|
105
|
+
status: stringField(fm, "status") ?? null,
|
|
106
|
+
owner: stringField(fm, "owner") ?? null,
|
|
107
|
+
last_verified: stringField(fm, "last_verified") ?? null,
|
|
108
|
+
sources: listField(fm, "sources"),
|
|
109
|
+
};
|
|
110
|
+
const governs = (op) => reqs.filter((r) => r.operations.includes(op)).map((r) => r.name);
|
|
111
|
+
const { inbound, outbound } = await landscapeEdges(docsDir, id, context);
|
|
112
|
+
const requirementJson = (r) => ({
|
|
113
|
+
id: r.id,
|
|
114
|
+
name: r.name,
|
|
115
|
+
kind: r.kind,
|
|
116
|
+
scenarios: r.scenarios.length,
|
|
117
|
+
operations: r.operations,
|
|
118
|
+
covers: r.covers,
|
|
119
|
+
});
|
|
120
|
+
if (json) {
|
|
121
|
+
emitJson({
|
|
122
|
+
type: "service",
|
|
123
|
+
id,
|
|
124
|
+
path: repoPath(docsDir, paths.dir),
|
|
125
|
+
has,
|
|
126
|
+
adrs,
|
|
127
|
+
frontmatter: provenance,
|
|
128
|
+
model: {
|
|
129
|
+
elements: model.elements.length,
|
|
130
|
+
relationships: model.relationships.length,
|
|
131
|
+
errors: model.errors.map(errorText),
|
|
132
|
+
},
|
|
133
|
+
requirements: reqs.map(requirementJson),
|
|
134
|
+
archSpec: {
|
|
135
|
+
requirements: archReqs.length,
|
|
136
|
+
scenarios: scenarioCount(archReqs),
|
|
137
|
+
entries: archReqs.map(requirementJson),
|
|
138
|
+
},
|
|
139
|
+
openapi: {
|
|
140
|
+
unreadable: api.unreadable,
|
|
141
|
+
...(api.error === undefined ? {} : { error: api.error }),
|
|
142
|
+
},
|
|
143
|
+
operations: ops.map((op) => ({ id: op, governedBy: governs(op) })),
|
|
144
|
+
landscape: { inbound, outbound },
|
|
145
|
+
});
|
|
146
|
+
return;
|
|
147
|
+
}
|
|
148
|
+
const badge = [provenance.status, provenance.owner].filter((s) => s !== null).join(" · ");
|
|
149
|
+
console.log(`${id} ${repoPath(docsDir, paths.dir)}${badge ? ` ${badge}` : ""}\n`);
|
|
150
|
+
if (provenance.sources.length > 0 || provenance.last_verified !== null) {
|
|
151
|
+
console.log(" provenance");
|
|
152
|
+
if (provenance.last_verified !== null)
|
|
153
|
+
console.log(` verified ${provenance.last_verified}`);
|
|
154
|
+
for (const s of provenance.sources)
|
|
155
|
+
console.log(` sources ${s}`);
|
|
156
|
+
console.log("");
|
|
157
|
+
}
|
|
158
|
+
console.log(" artifacts");
|
|
159
|
+
const modelNote = model.errors.length > 0
|
|
160
|
+
? `${model.errors.length} error(s)`
|
|
161
|
+
: `${plural(model.elements.length, "element")} · ${plural(model.relationships.length, "relationship")}`;
|
|
162
|
+
console.log(` ${mark(has.model)} model.likec4 ${has.model ? modelNote : ""}`.trimEnd());
|
|
163
|
+
const specNote = (rs) => `${plural(rs.length, "requirement")} · ${plural(scenarioCount(rs), "scenario")}`;
|
|
164
|
+
console.log(` ${mark(has.spec)} spec.md ${has.spec ? specNote(reqs) : ""}`.trimEnd());
|
|
165
|
+
console.log(` ${mark(has.archSpec)} arch.spec.md ${has.archSpec ? specNote(archReqs) : ""}`.trimEnd());
|
|
166
|
+
const apiNote = api.unreadable ? "does not parse" : plural(ops.length, "operation");
|
|
167
|
+
console.log(` ${mark(has.openapi)} openapi.yaml ${has.openapi ? apiNote : ""}`.trimEnd());
|
|
168
|
+
console.log(` ${mark(has.runbook)} runbook.md`);
|
|
169
|
+
console.log(` ${mark(has.health)} health.yaml`);
|
|
170
|
+
console.log(` ${mark(adrs > 0)} adrs/ ${adrs > 0 ? plural(adrs, "decision") : ""}`.trimEnd());
|
|
171
|
+
const requirementLine = (r) => {
|
|
172
|
+
const govern = r.operations.length > 0 ? ` → ${r.operations.join(", ")}` : "";
|
|
173
|
+
const covers = r.covers.length > 0 ? ` covers ${r.covers.join(", ")}` : "";
|
|
174
|
+
return ` ${r.name} (${plural(r.scenarios.length, "scenario")})${govern}${covers}`;
|
|
175
|
+
};
|
|
176
|
+
if (reqs.length > 0) {
|
|
177
|
+
console.log("\n requirements");
|
|
178
|
+
for (const r of reqs)
|
|
179
|
+
console.log(requirementLine(r));
|
|
180
|
+
}
|
|
181
|
+
if (archReqs.length > 0) {
|
|
182
|
+
console.log("\n arch requirements");
|
|
183
|
+
for (const r of archReqs)
|
|
184
|
+
console.log(requirementLine(r));
|
|
185
|
+
}
|
|
186
|
+
if (api.unreadable) {
|
|
187
|
+
console.log("\n operations");
|
|
188
|
+
console.log(` ✗ openapi.yaml does not parse — nothing here lists what ${id} offers`);
|
|
189
|
+
if (api.error !== undefined)
|
|
190
|
+
console.log(` ${api.error}`);
|
|
191
|
+
}
|
|
192
|
+
else if (ops.length > 0) {
|
|
193
|
+
console.log("\n operations");
|
|
194
|
+
for (const op of ops) {
|
|
195
|
+
const by = governs(op);
|
|
196
|
+
if (by.length > 0)
|
|
197
|
+
console.log(` ✓ ${op} governed by ${by.map((n) => `"${n}"`).join(", ")}`);
|
|
198
|
+
else
|
|
199
|
+
console.log(` ⚠ ${op} not governed by any requirement`);
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
if (inbound.length > 0 || outbound.length > 0) {
|
|
203
|
+
console.log("\n landscape");
|
|
204
|
+
for (const e of inbound)
|
|
205
|
+
console.log(` ← ${e.service}${edgeNote(e)}`);
|
|
206
|
+
for (const e of outbound)
|
|
207
|
+
console.log(` → ${e.service}${edgeNote(e)}`);
|
|
208
|
+
}
|
|
209
|
+
if (model.errors.length > 0) {
|
|
210
|
+
console.log("\n model errors");
|
|
211
|
+
for (const e of model.errors)
|
|
212
|
+
console.log(` ✗ ${errorText(e)}`);
|
|
213
|
+
}
|
|
214
|
+
}
|
|
215
|
+
/** Inbound/outbound edges for a service, read off the living landscape. */
|
|
216
|
+
async function landscapeEdges(docsDir, service, context) {
|
|
217
|
+
const path = landscapePath(docsDir);
|
|
218
|
+
if (!existsSync(path))
|
|
219
|
+
return { inbound: [], outbound: [] };
|
|
220
|
+
const land = await loadFile(path);
|
|
221
|
+
if (land.errors.length > 0)
|
|
222
|
+
return { inbound: [], outbound: [] };
|
|
223
|
+
// Edges are filed under the service an element is BOUND to, not under what the
|
|
224
|
+
// box is titled — otherwise renaming a box empties this list without a word —
|
|
225
|
+
// and an edge into a modelled container belongs to the service that owns it.
|
|
226
|
+
const known = new Set((await listServices(docsDir, context)).map((s) => s.id));
|
|
227
|
+
const svcOf = serviceResolver(land.elements, known);
|
|
228
|
+
const inbound = [];
|
|
229
|
+
const outbound = [];
|
|
230
|
+
for (const r of land.relationships) {
|
|
231
|
+
const edge = (service) => ({
|
|
232
|
+
service,
|
|
233
|
+
op: r.op ?? null,
|
|
234
|
+
title: r.title ?? null,
|
|
235
|
+
});
|
|
236
|
+
if (svcOf(r.target) === service)
|
|
237
|
+
inbound.push(edge(svcOf(r.source)));
|
|
238
|
+
else if (svcOf(r.source) === service)
|
|
239
|
+
outbound.push(edge(svcOf(r.target)));
|
|
240
|
+
}
|
|
241
|
+
return { inbound, outbound };
|
|
242
|
+
}
|
|
243
|
+
/* ------------------------------------------------------------------ */
|
|
244
|
+
/* Feature */
|
|
245
|
+
/* ------------------------------------------------------------------ */
|
|
246
|
+
async function showFeature(docsDir, feature, json) {
|
|
247
|
+
const paths = featurePaths(feature.dir);
|
|
248
|
+
const delta = feature.has.delta
|
|
249
|
+
? await loadFile(paths.delta)
|
|
250
|
+
: { errors: [], elements: [], relationships: [] };
|
|
251
|
+
const taggedEls = delta.elements.filter((e) => e.tags.includes(feature.id));
|
|
252
|
+
const taggedRels = delta.relationships.filter((r) => r.tags.includes(feature.id));
|
|
253
|
+
const services = [];
|
|
254
|
+
for (const svc of feature.services) {
|
|
255
|
+
const specPath = featureSpecPaths(feature.dir, svc).spec;
|
|
256
|
+
const reqs = existsSync(specPath) ? parseRequirements(await readFile(specPath, "utf8")) : [];
|
|
257
|
+
services.push({
|
|
258
|
+
id: svc,
|
|
259
|
+
added: reqs.filter((r) => r.kind === "ADDED").length,
|
|
260
|
+
modified: reqs.filter((r) => r.kind === "MODIFIED").length,
|
|
261
|
+
removed: reqs.filter((r) => r.kind === "REMOVED").length,
|
|
262
|
+
operations: [...new Set(reqs.flatMap((r) => r.operations))],
|
|
263
|
+
});
|
|
264
|
+
}
|
|
265
|
+
if (json) {
|
|
266
|
+
emitJson({
|
|
267
|
+
type: "feature",
|
|
268
|
+
id: feature.id,
|
|
269
|
+
dirName: feature.dirName,
|
|
270
|
+
path: repoPath(docsDir, feature.dir),
|
|
271
|
+
archived: feature.archived,
|
|
272
|
+
has: feature.has,
|
|
273
|
+
delta: {
|
|
274
|
+
elements: taggedEls.length,
|
|
275
|
+
relationships: taggedRels.length,
|
|
276
|
+
errors: delta.errors.map(errorText),
|
|
277
|
+
},
|
|
278
|
+
services,
|
|
279
|
+
});
|
|
280
|
+
return;
|
|
281
|
+
}
|
|
282
|
+
const state = feature.archived ? "archived" : "active";
|
|
283
|
+
console.log(`${feature.id} ${repoPath(docsDir, feature.dir)} ${state}\n`);
|
|
284
|
+
console.log(" artifacts");
|
|
285
|
+
console.log(` ${mark(feature.has.intent)} intent.md`);
|
|
286
|
+
const deltaNote = delta.errors.length > 0
|
|
287
|
+
? `${delta.errors.length} error(s)`
|
|
288
|
+
: `${plural(taggedEls.length, "element")} · ${plural(taggedRels.length, "relationship")} tagged ${feature.id}`;
|
|
289
|
+
console.log(` ${mark(feature.has.delta)} delta.likec4 ${feature.has.delta ? deltaNote : ""}`.trimEnd());
|
|
290
|
+
if (services.length > 0) {
|
|
291
|
+
console.log("\n services");
|
|
292
|
+
const width = Math.max(...services.map((s) => s.id.length));
|
|
293
|
+
for (const s of services) {
|
|
294
|
+
const ops = s.operations.length > 0 ? ` · ${s.operations.join(", ")}` : "";
|
|
295
|
+
console.log(` ${s.id.padEnd(width)} +${s.added} ~${s.modified} -${s.removed} requirements${ops}`);
|
|
296
|
+
}
|
|
297
|
+
}
|
|
298
|
+
if (delta.errors.length > 0) {
|
|
299
|
+
console.log("\n delta errors");
|
|
300
|
+
for (const e of delta.errors)
|
|
301
|
+
console.log(` ✗ ${errorText(e)}`);
|
|
302
|
+
}
|
|
303
|
+
}
|
|
304
|
+
/* ------------------------------------------------------------------ */
|
|
305
|
+
/** "-" for absent, matching `list`: ✗ is the error glyph here, and a missing runbook is not an error. */
|
|
306
|
+
function mark(present) {
|
|
307
|
+
return present ? "✓" : "-";
|
|
308
|
+
}
|
|
309
|
+
function scenarioCount(reqs) {
|
|
310
|
+
return reqs.reduce((n, r) => n + r.scenarios.length, 0);
|
|
311
|
+
}
|
|
312
|
+
function edgeNote(e) {
|
|
313
|
+
if (e.op)
|
|
314
|
+
return ` ${e.op}`;
|
|
315
|
+
return e.title ? ` "${e.title}"` : "";
|
|
316
|
+
}
|
|
317
|
+
function errorText(e) {
|
|
318
|
+
return typeof e.line === "number" ? `L${e.line}: ${e.message}` : e.message;
|
|
319
|
+
}
|
|
320
|
+
//# 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 } from \"node:fs/promises\";\nimport { loadConfig } from \"../core/config.js\";\nimport { FleetContext } from \"../core/fleet-context.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 countMarkdown,\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, reportRepositoryUnavailable } from \"./docs-repo-gate.js\";\nimport { plural } from \"./format.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 // One read index for the invocation. Deciding the target is a service\n // enumerates the fleet, and so does resolving the landscape's edges to the\n // services that own them — `loam show <service>` walked services/ twice for\n // one answer. The context memoises only within this invocation, so a later\n // run still sees whatever is on disk then.\n const context = new FleetContext();\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\", context);\n if (feature) {\n await showFeature(docsDir, feature, json);\n return;\n }\n const isService =\n forced !== \"feature\" && (await listServices(docsDir, context)).some((s) => s.id === target);\n if (isService) {\n await showService(docsDir, target, json, context);\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 reportRepositoryUnavailable(json, err, `'${target}' cannot be shown`, docsDir);\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(\n docsDir: string,\n id: string,\n json: boolean,\n context: FleetContext,\n): 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 countMarkdown(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, context);\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 : `${plural(model.elements.length, \"element\")} · ${plural(model.relationships.length, \"relationship\")}`;\n console.log(` ${mark(has.model)} model.likec4 ${has.model ? modelNote : \"\"}`.trimEnd());\n const specNote = (rs: Requirement[]): string =>\n `${plural(rs.length, \"requirement\")} · ${plural(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\" : plural(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 ? plural(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} (${plural(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 context: FleetContext,\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, context)).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 : `${plural(taggedEls.length, \"element\")} · ${plural(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 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"]}
|
|
@@ -0,0 +1,254 @@
|
|
|
1
|
+
import { loadConfig } from "../core/config.js";
|
|
2
|
+
import { FleetContext } from "../core/fleet-context.js";
|
|
3
|
+
import { emitJson, fail, reportNoConfig } from "../core/json.js";
|
|
4
|
+
import { findingJson, SEVERITY_MARK } from "../core/report.js";
|
|
5
|
+
import { DocsRepoUnavailableError, featureCandidates, missingFeatureMessage, resolveFeature, } from "../core/repo.js";
|
|
6
|
+
import { featureStatus, fleetStatus, } from "../core/status.js";
|
|
7
|
+
import { docsRepoReady, reportDocsRepoError, reportRepositoryUnavailable } from "./docs-repo-gate.js";
|
|
8
|
+
export function registerStatus(program) {
|
|
9
|
+
program
|
|
10
|
+
.command("status")
|
|
11
|
+
.argument("[feature]", "feature id or directory name; omit for the whole repository")
|
|
12
|
+
.description("Report where the work stands and what to do next, derived from the files")
|
|
13
|
+
.option("--json", "emit the machine contract instead of the human view")
|
|
14
|
+
.option("--service <id>", "narrow the per-service view to one service")
|
|
15
|
+
.action(async (featureArg, opts) => {
|
|
16
|
+
const json = opts.json === true;
|
|
17
|
+
const config = await loadConfig();
|
|
18
|
+
if (!config) {
|
|
19
|
+
reportNoConfig(json);
|
|
20
|
+
return;
|
|
21
|
+
}
|
|
22
|
+
const { docsDir } = config;
|
|
23
|
+
// A docsDir that is not a docs repo is refused, never rendered as a repo
|
|
24
|
+
// with nothing in it — the doctrine docs-repo-gate.ts's docsRepoReady exists
|
|
25
|
+
// for. `status` counts services, so it needs services/ in both modes.
|
|
26
|
+
if (!docsRepoReady(json, docsDir, "services"))
|
|
27
|
+
return;
|
|
28
|
+
const context = new FleetContext();
|
|
29
|
+
// What the refusal below says it was answering when it gave up.
|
|
30
|
+
let scope = "this repository";
|
|
31
|
+
try {
|
|
32
|
+
if (featureArg === undefined) {
|
|
33
|
+
const report = await fleetStatus(docsDir, { service: opts.service, context });
|
|
34
|
+
// A `--service` that names nothing is refused rather than answered
|
|
35
|
+
// with an empty fleet: "no services and no features" and "you
|
|
36
|
+
// misspelled it" are opposite facts, and only one of them is worth
|
|
37
|
+
// acting on.
|
|
38
|
+
if (opts.service !== undefined && report.services.total === 0) {
|
|
39
|
+
fail(json, "unknown-service", `No service '${opts.service}' under ${docsDir}/services/.`);
|
|
40
|
+
return;
|
|
41
|
+
}
|
|
42
|
+
if (json)
|
|
43
|
+
emitJson({ command: "status", docsDir, scope: "fleet", ...fleetJson(report) });
|
|
44
|
+
else
|
|
45
|
+
printFleet(report);
|
|
46
|
+
return;
|
|
47
|
+
}
|
|
48
|
+
// Archived features are IN scope: an agent asking where FEAT-101 stands
|
|
49
|
+
// after it shipped deserves "it shipped", not "no such feature" — the
|
|
50
|
+
// same reading `show` takes. An argument naming two directories is
|
|
51
|
+
// still answered (status writes nothing), with the tie broken the way
|
|
52
|
+
// resolveFeature always breaks it.
|
|
53
|
+
const feature = await resolveFeature(docsDir, featureArg, "include", context);
|
|
54
|
+
if (!feature) {
|
|
55
|
+
fail(json, "unknown-target", await missingFeatureMessage(docsDir, featureArg, context));
|
|
56
|
+
return;
|
|
57
|
+
}
|
|
58
|
+
scope = feature.id;
|
|
59
|
+
// `config.service` is not a lens like `--service`: it says which
|
|
60
|
+
// repository this IS, and three of the steps (`loam gherkin`, `verify
|
|
61
|
+
// --record --service`) refuse anywhere else. Without it status handed a
|
|
62
|
+
// docs-repo reader commands only a service repo can run, and never told
|
|
63
|
+
// a service repo that the work in front of it was its own.
|
|
64
|
+
const report = await featureStatus(docsDir, feature, {
|
|
65
|
+
service: opts.service,
|
|
66
|
+
boundService: config.service,
|
|
67
|
+
context,
|
|
68
|
+
});
|
|
69
|
+
if (opts.service !== undefined && !report.feature.services.includes(opts.service)) {
|
|
70
|
+
fail(json, "unknown-service", `${feature.id} carries no delta for '${opts.service}'. It touches: ${report.feature.services.length > 0 ? report.feature.services.join(", ") : "(no services)"}.`);
|
|
71
|
+
return;
|
|
72
|
+
}
|
|
73
|
+
const ambiguous = await featureCandidates(docsDir, featureArg, "include", context);
|
|
74
|
+
if (json) {
|
|
75
|
+
emitJson({ command: "status", docsDir, scope: "feature", ...featureJson(report) });
|
|
76
|
+
}
|
|
77
|
+
else {
|
|
78
|
+
printFeature(report, ambiguous.length > 1 ? ambiguous.map((c) => c.dirName) : []);
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
catch (err) {
|
|
82
|
+
if (err instanceof DocsRepoUnavailableError) {
|
|
83
|
+
reportDocsRepoError(json, err);
|
|
84
|
+
return;
|
|
85
|
+
}
|
|
86
|
+
// One unreadable artifact must not escape as a stack trace and an
|
|
87
|
+
// `internal` envelope that names nothing: the projection is
|
|
88
|
+
// all-or-nothing (a status missing one artifact reads as a feature that
|
|
89
|
+
// does not owe it), so the honest answer is a refusal naming the path.
|
|
90
|
+
// The errno-vs-path reading this branch depends on — the reason one
|
|
91
|
+
// directory where a file belongs used to kill BOTH forms of status —
|
|
92
|
+
// now lives once, with its reasoning, on the shared helper.
|
|
93
|
+
reportRepositoryUnavailable(json, err, `the status of ${scope} would be missing an artifact`, docsDir);
|
|
94
|
+
}
|
|
95
|
+
// No exit code is set on any path above that succeeded, deliberately.
|
|
96
|
+
// `doctor` exits 1 because a broken installation makes every later
|
|
97
|
+
// command lie, and `validate` exits 1 because it IS the fleet gate. This
|
|
98
|
+
// command is a question, and its answer is equally true whether the
|
|
99
|
+
// feature is finished or not — an agent runs it precisely BECAUSE work is
|
|
100
|
+
// outstanding, and exiting 1 there would make every `set -e` script and
|
|
101
|
+
// every CI step read "there is work to do" as a failure.
|
|
102
|
+
});
|
|
103
|
+
}
|
|
104
|
+
/* ------------------------------------------------------------------ */
|
|
105
|
+
/* JSON */
|
|
106
|
+
/* ------------------------------------------------------------------ */
|
|
107
|
+
/**
|
|
108
|
+
* `issues` go out through `findingJson`, the same serializer `validate` uses:
|
|
109
|
+
* an `Issue` is a `Finding` without details, and one breach reported by two
|
|
110
|
+
* commands must arrive in one shape or a consumer needs two parsers.
|
|
111
|
+
*/
|
|
112
|
+
function featureJson(r) {
|
|
113
|
+
return {
|
|
114
|
+
interrupted: r.interrupted,
|
|
115
|
+
feature: r.feature,
|
|
116
|
+
service: r.service,
|
|
117
|
+
artifacts: r.artifacts,
|
|
118
|
+
checks: { ...r.checks, issues: r.checks.issues.map(findingJson) },
|
|
119
|
+
verification: r.verification,
|
|
120
|
+
next: r.next,
|
|
121
|
+
};
|
|
122
|
+
}
|
|
123
|
+
function fleetJson(r) {
|
|
124
|
+
return {
|
|
125
|
+
interrupted: r.interrupted,
|
|
126
|
+
service: r.service,
|
|
127
|
+
services: r.services,
|
|
128
|
+
features: r.features,
|
|
129
|
+
order: r.order,
|
|
130
|
+
next: r.next,
|
|
131
|
+
};
|
|
132
|
+
}
|
|
133
|
+
/* ------------------------------------------------------------------ */
|
|
134
|
+
/* Text */
|
|
135
|
+
/* ------------------------------------------------------------------ */
|
|
136
|
+
function pad(values) {
|
|
137
|
+
return Math.max(0, ...values.map((v) => v.length));
|
|
138
|
+
}
|
|
139
|
+
/**
|
|
140
|
+
* The half-merged-repo banner, printed FIRST and on stderr.
|
|
141
|
+
*
|
|
142
|
+
* stderr, alone among this command's output, because it is not part of the
|
|
143
|
+
* answer: it is the reason the answer may be wrong. A reader piping `status`
|
|
144
|
+
* into a pager still sees it, and the human view stays the shape every other
|
|
145
|
+
* run has. `--json` carries the same fact in `interrupted`.
|
|
146
|
+
*/
|
|
147
|
+
function printInterrupted(i) {
|
|
148
|
+
console.error(i.unreadable
|
|
149
|
+
? "⚠ this docs repo holds a .loam-commit that cannot be read — a commit was interrupted and nothing here can be trusted until a human compares the living docs against version control."
|
|
150
|
+
: `⚠ a \`${i.recover}\` was killed mid-commit (${i.host}, pid ${i.pid}, ${i.at}) — ${i.files.length} file(s) may be half-written: ${i.files.join(", ")}. Everything below is derived from them.`);
|
|
151
|
+
console.error("");
|
|
152
|
+
}
|
|
153
|
+
/** `spec (payment-service)` — the artifact id plus the service, when it has one. */
|
|
154
|
+
function artifactLabel(a) {
|
|
155
|
+
return a.service === null ? a.id : `${a.id} (${a.service})`;
|
|
156
|
+
}
|
|
157
|
+
/**
|
|
158
|
+
* The counts come from the record's `claims:` array (verify's `tallyRecord`),
|
|
159
|
+
* never from its `summary:` block. The verdict is printed beside the state
|
|
160
|
+
* because they answer different questions and only together are they honest:
|
|
161
|
+
* `recorded 4/4 confirmed` used to be the whole line for a feature `verify`
|
|
162
|
+
* calls `attested` — complete, and resting on somebody's word about a test.
|
|
163
|
+
*/
|
|
164
|
+
function verificationLine(v) {
|
|
165
|
+
if (v.state === "absent")
|
|
166
|
+
return "none";
|
|
167
|
+
if (v.state === "unreadable")
|
|
168
|
+
return "unreadable";
|
|
169
|
+
return (`${v.state} · ${v.verdict} ${v.confirmed}/${v.claims} confirmed` +
|
|
170
|
+
(v.unanswered > 0 ? ` · ${v.unanswered} unanswered` : "") +
|
|
171
|
+
(v.attested > 0 ? ` · ${v.attested} scenario(s) on an agent's word, not a test run` : "") +
|
|
172
|
+
` (recorded ${v.recorded ?? "?"})`);
|
|
173
|
+
}
|
|
174
|
+
/**
|
|
175
|
+
* The steps, numbered, with the command on its own indented line — doctor's
|
|
176
|
+
* layout, for doctor's reason: what to do and what to type are two different
|
|
177
|
+
* sentences, and someone scanning a half-built feature reads down the `run:`
|
|
178
|
+
* column.
|
|
179
|
+
*/
|
|
180
|
+
function printNext(steps) {
|
|
181
|
+
console.log("\n next");
|
|
182
|
+
for (const [i, step] of steps.entries()) {
|
|
183
|
+
console.log(` ${i + 1}. ${step.code}: ${step.statement}`);
|
|
184
|
+
console.log(` run: ${step.command}`);
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
function printFeature(r, ambiguous) {
|
|
188
|
+
if (r.interrupted !== null)
|
|
189
|
+
printInterrupted(r.interrupted);
|
|
190
|
+
const f = r.feature;
|
|
191
|
+
console.log(`loam status ${f.id} — ${f.stage}${f.archived ? " (archived)" : ""}`);
|
|
192
|
+
console.log(` feature ${f.dirName} ${f.path}`);
|
|
193
|
+
console.log(` services ${f.services.length > 0 ? f.services.join(", ") : "(none)"}`);
|
|
194
|
+
if (r.service !== null)
|
|
195
|
+
console.log(` narrowed to ${r.service}`);
|
|
196
|
+
if (f.blockedBy.length > 0)
|
|
197
|
+
console.log(` waiting on ${f.blockedBy.join(", ")}`);
|
|
198
|
+
console.log(` coherence ${r.checks.ran
|
|
199
|
+
? `${r.checks.errors} error(s) · ${r.checks.warnings} warning(s) · ${r.checks.gating} gate archive`
|
|
200
|
+
: "not run (archived)"}`);
|
|
201
|
+
console.log(` verification ${verificationLine(r.verification)}`);
|
|
202
|
+
if (ambiguous.length > 0) {
|
|
203
|
+
console.log(` note '${f.id}' also names ${ambiguous.filter((d) => d !== f.dirName).join(", ")}`);
|
|
204
|
+
}
|
|
205
|
+
console.log("\n artifacts");
|
|
206
|
+
const labels = r.artifacts.map(artifactLabel);
|
|
207
|
+
const labelWidth = pad(labels);
|
|
208
|
+
const statusWidth = pad(r.artifacts.map((a) => a.status));
|
|
209
|
+
for (const [i, a] of r.artifacts.entries()) {
|
|
210
|
+
// `done` on a file that is not there is the one row a reader misreads: it
|
|
211
|
+
// means the feature owes no such artifact, not that one was written. The
|
|
212
|
+
// JSON contract says it with `exists`; the human view has to say it in
|
|
213
|
+
// words, or the table quietly claims files that do not exist.
|
|
214
|
+
const note = a.blockedBy.length > 0
|
|
215
|
+
? ` (needs ${a.blockedBy.join(", ")})`
|
|
216
|
+
: !a.exists && a.status === "done"
|
|
217
|
+
? " (not written — none owed)"
|
|
218
|
+
: "";
|
|
219
|
+
console.log(` ${a.status.padEnd(statusWidth)} ${labels[i].padEnd(labelWidth)} ${a.path}${note}`.trimEnd());
|
|
220
|
+
}
|
|
221
|
+
if (r.checks.issues.length > 0) {
|
|
222
|
+
console.log("\n findings");
|
|
223
|
+
for (const issue of r.checks.issues) {
|
|
224
|
+
console.log(` ${SEVERITY_MARK[issue.severity]} ${issue.code}: ${issue.message}`);
|
|
225
|
+
}
|
|
226
|
+
}
|
|
227
|
+
printNext(r.next);
|
|
228
|
+
}
|
|
229
|
+
function printFleet(r) {
|
|
230
|
+
if (r.interrupted !== null)
|
|
231
|
+
printInterrupted(r.interrupted);
|
|
232
|
+
const s = r.services;
|
|
233
|
+
console.log(`loam status — ${s.total} service(s) · ${r.features.length} feature(s) in flight`);
|
|
234
|
+
console.log(` services ${s.undocumented} undocumented · ${s.draft} draft · ${s.vouched} vouched`);
|
|
235
|
+
if (r.service !== null)
|
|
236
|
+
console.log(` narrowed to ${r.service}`);
|
|
237
|
+
if (r.order.length > 1)
|
|
238
|
+
console.log(` order ${r.order.join(" → ")}`);
|
|
239
|
+
if (r.features.length > 0) {
|
|
240
|
+
console.log("\n features");
|
|
241
|
+
const ids = pad(r.features.map((f) => f.id));
|
|
242
|
+
const stages = pad(r.features.map((f) => f.stage));
|
|
243
|
+
for (const f of r.features) {
|
|
244
|
+
const detail = f.blockedBy.length > 0
|
|
245
|
+
? `waiting on ${f.blockedBy.join(", ")}`
|
|
246
|
+
: f.missing.length > 0
|
|
247
|
+
? `missing ${f.missing.join(", ")}`
|
|
248
|
+
: f.services.join(", ");
|
|
249
|
+
console.log(` ${f.stage.padEnd(stages)} ${f.id.padEnd(ids)} ${detail}`.trimEnd());
|
|
250
|
+
}
|
|
251
|
+
}
|
|
252
|
+
printNext(r.next);
|
|
253
|
+
}
|
|
254
|
+
//# sourceMappingURL=status.js.map
|