@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,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":"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","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":"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","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"]}
|