@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,356 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* A read-only dependency/conflict index for active features.
|
|
3
|
+
*
|
|
4
|
+
* This is deliberately derived from parsed artifacts rather than validation
|
|
5
|
+
* messages: issue prose is for people and is not an API. The graph answers a
|
|
6
|
+
* narrower question — which in-flight change must land before another because
|
|
7
|
+
* it introduces an identity or operation the latter already refers to.
|
|
8
|
+
*/
|
|
9
|
+
import { existsSync } from "node:fs";
|
|
10
|
+
import { inOrder } from "./concurrency.js";
|
|
11
|
+
import { FleetContext } from "./fleet-context.js";
|
|
12
|
+
import { serviceResolver } from "./likec4.js";
|
|
13
|
+
import { operations } from "./openapi.js";
|
|
14
|
+
import { SPEC_AXES, compareIds, featurePaths, featureSpecPaths, listFeatures, servicePaths, } from "./repo.js";
|
|
15
|
+
function reqKeys(requirement) {
|
|
16
|
+
return [
|
|
17
|
+
...(requirement.id === undefined ? [] : [`id:${requirement.id}`]),
|
|
18
|
+
`name:${requirement.name}`,
|
|
19
|
+
];
|
|
20
|
+
}
|
|
21
|
+
function reqIndexKey(at) {
|
|
22
|
+
return `${at.service}\0${at.axis}\0${at.identity}`;
|
|
23
|
+
}
|
|
24
|
+
function opIndexKey(at) {
|
|
25
|
+
return `${at.service}\0${at.operationId}`;
|
|
26
|
+
}
|
|
27
|
+
function addOwner(index, key, at, feature) {
|
|
28
|
+
const owners = index.get(key) ?? { at, features: new Set() };
|
|
29
|
+
owners.features.add(feature);
|
|
30
|
+
index.set(key, owners);
|
|
31
|
+
}
|
|
32
|
+
async function readFacts(docsDir, feature, context) {
|
|
33
|
+
const addedRequirements = [];
|
|
34
|
+
const changedRequirements = [];
|
|
35
|
+
const introducedOperations = [];
|
|
36
|
+
const requiredOperations = [];
|
|
37
|
+
/**
|
|
38
|
+
* The operationIds a service ALREADY has, per service.
|
|
39
|
+
*
|
|
40
|
+
* This is the whole difference between a dependency graph and a rumour mill.
|
|
41
|
+
* A feature's `openapi.yaml` is a COMPLETE document, not a patch — authors
|
|
42
|
+
* restate the living API around the slot they are changing — so every feature
|
|
43
|
+
* that so much as mentions `authorizePayment` used to be recorded as
|
|
44
|
+
* INTRODUCING it, and every other feature governing it as REQUIRING it. Two
|
|
45
|
+
* features quoting the same living operation therefore depended on each other
|
|
46
|
+
* in both directions: an invented cycle, an invented conflict, and an ordering
|
|
47
|
+
* for work that had none. `loam validate`'s coherence pass already subtracts
|
|
48
|
+
* the living contract this way (core/coherence.ts); this is the same
|
|
49
|
+
* subtraction, so the two cannot disagree about what "new" means.
|
|
50
|
+
*
|
|
51
|
+
* Cached per service inside the call and, through the FleetContext, across
|
|
52
|
+
* features: a fleet-wide graph asks about the same ten services N times.
|
|
53
|
+
*/
|
|
54
|
+
const livingByService = new Map();
|
|
55
|
+
const living = async (service) => {
|
|
56
|
+
let ids = livingByService.get(service);
|
|
57
|
+
if (ids === undefined) {
|
|
58
|
+
ids = new Set((await operations(servicePaths(docsDir, service).openapi, context))
|
|
59
|
+
.filter((op) => !op.remove)
|
|
60
|
+
.map((op) => op.id));
|
|
61
|
+
livingByService.set(service, ids);
|
|
62
|
+
}
|
|
63
|
+
return ids;
|
|
64
|
+
};
|
|
65
|
+
for (const service of feature.services) {
|
|
66
|
+
const paths = featureSpecPaths(feature.dir, service);
|
|
67
|
+
const livingIds = await living(service);
|
|
68
|
+
for (const axis of SPEC_AXES) {
|
|
69
|
+
const path = paths[axis.key];
|
|
70
|
+
if (!existsSync(path))
|
|
71
|
+
continue;
|
|
72
|
+
const requirements = await context.readRequirements(path);
|
|
73
|
+
for (const requirement of requirements) {
|
|
74
|
+
const fact = { service, axis: axis.file, requirement };
|
|
75
|
+
if (requirement.kind === "ADDED")
|
|
76
|
+
addedRequirements.push(fact);
|
|
77
|
+
if (requirement.kind === "MODIFIED" || requirement.kind === "REMOVED") {
|
|
78
|
+
changedRequirements.push(fact);
|
|
79
|
+
}
|
|
80
|
+
for (const operationId of requirement.operations) {
|
|
81
|
+
// Governing an operation the service already provides is not a
|
|
82
|
+
// dependency on anybody: the contract it needs is already merged.
|
|
83
|
+
if (livingIds.has(operationId))
|
|
84
|
+
continue;
|
|
85
|
+
requiredOperations.push({ service, operationId });
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
if (existsSync(paths.openapi)) {
|
|
90
|
+
const featureOperations = await operations(paths.openapi, context);
|
|
91
|
+
for (const operation of featureOperations) {
|
|
92
|
+
if (livingIds.has(operation.id))
|
|
93
|
+
continue;
|
|
94
|
+
if (operation.remove)
|
|
95
|
+
requiredOperations.push({ service, operationId: operation.id });
|
|
96
|
+
else
|
|
97
|
+
introducedOperations.push({ service, operationId: operation.id });
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
// The C4 delta is the FOURTH place a feature names an operation, and it was
|
|
102
|
+
// the one the graph could not see: an edge carrying `metadata { op 'x' }` is
|
|
103
|
+
// a call this feature builds on `x`, which is exactly the "must land first"
|
|
104
|
+
// relation this module exists to compute. Validate already says so — it
|
|
105
|
+
// reports `c4-api.op-pending` when the op is defined by another feature in
|
|
106
|
+
// flight — so the graph disagreeing with the validator about the same pair of
|
|
107
|
+
// features was the graph being wrong, not quiet.
|
|
108
|
+
//
|
|
109
|
+
// Endpoints resolve through `serviceResolver` — the element's
|
|
110
|
+
// `metadata { service }` binding, title as fallback — the same join every
|
|
111
|
+
// other check uses, so renaming a box in a diagram cannot silently unhook it
|
|
112
|
+
// here either.
|
|
113
|
+
const deltaPath = featurePaths(feature.dir).delta;
|
|
114
|
+
if (existsSync(deltaPath)) {
|
|
115
|
+
const doc = await context.loadLikeC4(deltaPath);
|
|
116
|
+
// A delta that does not parse proves nothing about who depends on whom.
|
|
117
|
+
// `loam validate` owns that diagnosis (`delta.invalid`); inventing edges out
|
|
118
|
+
// of a half-read document would be worse than the silence.
|
|
119
|
+
if (doc.errors.length === 0) {
|
|
120
|
+
// One resolver for the whole delta: `serviceOf` rebuilds its id map on
|
|
121
|
+
// every call, and this loop asks once per edge.
|
|
122
|
+
const svcOf = serviceResolver(doc.elements);
|
|
123
|
+
for (const rel of doc.relationships) {
|
|
124
|
+
if (rel.op === undefined)
|
|
125
|
+
continue;
|
|
126
|
+
const service = svcOf(rel.target);
|
|
127
|
+
if ((await living(service)).has(rel.op))
|
|
128
|
+
continue;
|
|
129
|
+
requiredOperations.push({ service, operationId: rel.op });
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
return {
|
|
134
|
+
feature,
|
|
135
|
+
addedRequirements,
|
|
136
|
+
changedRequirements,
|
|
137
|
+
introducedOperations,
|
|
138
|
+
requiredOperations,
|
|
139
|
+
};
|
|
140
|
+
}
|
|
141
|
+
function compareReasons(a, b) {
|
|
142
|
+
const ak = a.kind === "requirement"
|
|
143
|
+
? `${a.kind}\0${a.service}\0${a.axis}\0${a.identity}`
|
|
144
|
+
: `${a.kind}\0${a.service}\0${a.operationId}`;
|
|
145
|
+
const bk = b.kind === "requirement"
|
|
146
|
+
? `${b.kind}\0${b.service}\0${b.axis}\0${b.identity}`
|
|
147
|
+
: `${b.kind}\0${b.service}\0${b.operationId}`;
|
|
148
|
+
return compareIds(ak, bk);
|
|
149
|
+
}
|
|
150
|
+
function edgeKey(from, to) {
|
|
151
|
+
return `${from}\0${to}`;
|
|
152
|
+
}
|
|
153
|
+
function appendReason(edges, from, to, reason) {
|
|
154
|
+
if (from === to)
|
|
155
|
+
return;
|
|
156
|
+
const key = edgeKey(from, to);
|
|
157
|
+
const edge = edges.get(key) ?? { from, to, reasons: [] };
|
|
158
|
+
const encoded = JSON.stringify(reason);
|
|
159
|
+
if (!edge.reasons.some((item) => JSON.stringify(item) === encoded))
|
|
160
|
+
edge.reasons.push(reason);
|
|
161
|
+
edges.set(key, edge);
|
|
162
|
+
}
|
|
163
|
+
function stronglyConnected(ids, edges) {
|
|
164
|
+
const adjacency = new Map(ids.map((id) => [id, []]));
|
|
165
|
+
for (const edge of edges)
|
|
166
|
+
adjacency.get(edge.from)?.push(edge.to);
|
|
167
|
+
for (const targets of adjacency.values())
|
|
168
|
+
targets.sort(compareIds);
|
|
169
|
+
let nextIndex = 0;
|
|
170
|
+
const indices = new Map();
|
|
171
|
+
const low = new Map();
|
|
172
|
+
const stack = [];
|
|
173
|
+
const onStack = new Set();
|
|
174
|
+
const components = [];
|
|
175
|
+
const visit = (id) => {
|
|
176
|
+
indices.set(id, nextIndex);
|
|
177
|
+
low.set(id, nextIndex);
|
|
178
|
+
nextIndex += 1;
|
|
179
|
+
stack.push(id);
|
|
180
|
+
onStack.add(id);
|
|
181
|
+
for (const target of adjacency.get(id) ?? []) {
|
|
182
|
+
if (!indices.has(target)) {
|
|
183
|
+
visit(target);
|
|
184
|
+
low.set(id, Math.min(low.get(id), low.get(target)));
|
|
185
|
+
}
|
|
186
|
+
else if (onStack.has(target)) {
|
|
187
|
+
low.set(id, Math.min(low.get(id), indices.get(target)));
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
if (low.get(id) !== indices.get(id))
|
|
191
|
+
return;
|
|
192
|
+
const component = [];
|
|
193
|
+
while (stack.length > 0) {
|
|
194
|
+
const member = stack.pop();
|
|
195
|
+
onStack.delete(member);
|
|
196
|
+
component.push(member);
|
|
197
|
+
if (member === id)
|
|
198
|
+
break;
|
|
199
|
+
}
|
|
200
|
+
component.sort(compareIds);
|
|
201
|
+
const selfLoop = component.length === 1
|
|
202
|
+
&& (adjacency.get(component[0]) ?? []).includes(component[0]);
|
|
203
|
+
if (component.length > 1 || selfLoop)
|
|
204
|
+
components.push(component);
|
|
205
|
+
};
|
|
206
|
+
for (const id of [...ids].sort(compareIds))
|
|
207
|
+
if (!indices.has(id))
|
|
208
|
+
visit(id);
|
|
209
|
+
return components.sort((a, b) => compareIds(a[0], b[0]));
|
|
210
|
+
}
|
|
211
|
+
function dependencyFirstOrder(ids, edges) {
|
|
212
|
+
const deps = new Map(ids.map((id) => [id, []]));
|
|
213
|
+
for (const edge of edges)
|
|
214
|
+
deps.get(edge.from)?.push(edge.to);
|
|
215
|
+
for (const values of deps.values())
|
|
216
|
+
values.sort(compareIds);
|
|
217
|
+
const visited = new Set();
|
|
218
|
+
const visiting = new Set();
|
|
219
|
+
const order = [];
|
|
220
|
+
const visit = (id) => {
|
|
221
|
+
if (visited.has(id) || visiting.has(id))
|
|
222
|
+
return;
|
|
223
|
+
visiting.add(id);
|
|
224
|
+
for (const dependency of deps.get(id) ?? [])
|
|
225
|
+
visit(dependency);
|
|
226
|
+
visiting.delete(id);
|
|
227
|
+
visited.add(id);
|
|
228
|
+
order.push(id);
|
|
229
|
+
};
|
|
230
|
+
for (const id of [...ids].sort(compareIds))
|
|
231
|
+
visit(id);
|
|
232
|
+
return order;
|
|
233
|
+
}
|
|
234
|
+
/** Derive the complete active-feature graph, or one feature and its transitive prerequisites. */
|
|
235
|
+
export async function analyzeDependencies(docsDir, featureId, context = new FleetContext()) {
|
|
236
|
+
const features = await listFeatures(docsDir, {}, context);
|
|
237
|
+
// Bounded, not `Promise.all`: `readFacts` loads the feature's delta.likec4,
|
|
238
|
+
// and each of those is a whole Langium workspace held open until the read
|
|
239
|
+
// finishes — the same resource `validate --all` rations for the same measured
|
|
240
|
+
// reason (see core/concurrency.ts). This module fanned out over every active
|
|
241
|
+
// feature at once, and it is not only `loam dependencies` that pays: the fleet
|
|
242
|
+
// form of `loam status` calls in here too. `inOrder` returns results in input
|
|
243
|
+
// order, so the graph, the conflict list and their ordering are unchanged.
|
|
244
|
+
const facts = await inOrder(features, (feature) => readFacts(docsDir, feature, context));
|
|
245
|
+
const requirementOwners = new Map();
|
|
246
|
+
// The second index over the SAME identity: who MODIFIES or REMOVES it. Only
|
|
247
|
+
// the first index existed, so the graph could see "two features add the same
|
|
248
|
+
// requirement" and was blind to "two features rewrite the same requirement" —
|
|
249
|
+
// the collision that actually happens on a fleet, and the one whose loser
|
|
250
|
+
// loses their whole authored text without a word from any command.
|
|
251
|
+
const changedOwners = new Map();
|
|
252
|
+
const operationOwners = new Map();
|
|
253
|
+
for (const fact of facts) {
|
|
254
|
+
for (const added of fact.addedRequirements) {
|
|
255
|
+
for (const identity of reqKeys(added.requirement)) {
|
|
256
|
+
const at = { service: added.service, axis: added.axis, identity };
|
|
257
|
+
addOwner(requirementOwners, reqIndexKey(at), at, fact.feature.id);
|
|
258
|
+
}
|
|
259
|
+
}
|
|
260
|
+
for (const changed of fact.changedRequirements) {
|
|
261
|
+
for (const identity of reqKeys(changed.requirement)) {
|
|
262
|
+
const at = { service: changed.service, axis: changed.axis, identity };
|
|
263
|
+
addOwner(changedOwners, reqIndexKey(at), at, fact.feature.id);
|
|
264
|
+
}
|
|
265
|
+
}
|
|
266
|
+
for (const operation of fact.introducedOperations) {
|
|
267
|
+
addOwner(operationOwners, opIndexKey(operation), operation, fact.feature.id);
|
|
268
|
+
}
|
|
269
|
+
}
|
|
270
|
+
const edgeMap = new Map();
|
|
271
|
+
for (const fact of facts) {
|
|
272
|
+
for (const changed of fact.changedRequirements) {
|
|
273
|
+
for (const identity of reqKeys(changed.requirement)) {
|
|
274
|
+
const at = { service: changed.service, axis: changed.axis, identity };
|
|
275
|
+
for (const owner of requirementOwners.get(reqIndexKey(at))?.features ?? []) {
|
|
276
|
+
appendReason(edgeMap, fact.feature.id, owner, { kind: "requirement", ...at });
|
|
277
|
+
}
|
|
278
|
+
}
|
|
279
|
+
}
|
|
280
|
+
for (const required of fact.requiredOperations) {
|
|
281
|
+
for (const owner of operationOwners.get(opIndexKey(required))?.features ?? []) {
|
|
282
|
+
appendReason(edgeMap, fact.feature.id, owner, { kind: "operation", ...required });
|
|
283
|
+
}
|
|
284
|
+
}
|
|
285
|
+
}
|
|
286
|
+
let edges = [...edgeMap.values()]
|
|
287
|
+
.map((edge) => ({ ...edge, reasons: edge.reasons.sort(compareReasons) }))
|
|
288
|
+
.sort((a, b) => compareIds(a.from, b.from) || compareIds(a.to, b.to));
|
|
289
|
+
const conflicts = [];
|
|
290
|
+
for (const [index, change] of [
|
|
291
|
+
[requirementOwners, "added"],
|
|
292
|
+
[changedOwners, "changed"],
|
|
293
|
+
]) {
|
|
294
|
+
for (const { at, features } of index.values()) {
|
|
295
|
+
if (features.size < 2)
|
|
296
|
+
continue;
|
|
297
|
+
conflicts.push({
|
|
298
|
+
kind: "requirement",
|
|
299
|
+
change,
|
|
300
|
+
service: at.service,
|
|
301
|
+
axis: at.axis,
|
|
302
|
+
identity: at.identity,
|
|
303
|
+
features: [...features].sort(compareIds),
|
|
304
|
+
});
|
|
305
|
+
}
|
|
306
|
+
}
|
|
307
|
+
for (const { at, features } of operationOwners.values()) {
|
|
308
|
+
if (features.size < 2)
|
|
309
|
+
continue;
|
|
310
|
+
conflicts.push({
|
|
311
|
+
kind: "operation",
|
|
312
|
+
// Two features DEFINING the same operationId — the only way an operation
|
|
313
|
+
// collides, since removals address a living slot rather than claim one.
|
|
314
|
+
change: "added",
|
|
315
|
+
service: at.service,
|
|
316
|
+
identity: at.operationId,
|
|
317
|
+
features: [...features].sort(compareIds),
|
|
318
|
+
});
|
|
319
|
+
}
|
|
320
|
+
conflicts.sort((a, b) => compareIds(a.kind, b.kind)
|
|
321
|
+
|| compareIds(a.change, b.change)
|
|
322
|
+
|| compareIds(a.service, b.service)
|
|
323
|
+
|| compareIds(a.axis ?? "", b.axis ?? "")
|
|
324
|
+
|| compareIds(a.identity, b.identity));
|
|
325
|
+
let selected = new Set(features.map((feature) => feature.id));
|
|
326
|
+
if (featureId !== undefined) {
|
|
327
|
+
selected = new Set([featureId]);
|
|
328
|
+
let changed = true;
|
|
329
|
+
while (changed) {
|
|
330
|
+
changed = false;
|
|
331
|
+
for (const edge of edges) {
|
|
332
|
+
if (selected.has(edge.from) && !selected.has(edge.to)) {
|
|
333
|
+
selected.add(edge.to);
|
|
334
|
+
changed = true;
|
|
335
|
+
}
|
|
336
|
+
}
|
|
337
|
+
}
|
|
338
|
+
edges = edges.filter((edge) => selected.has(edge.from) && selected.has(edge.to));
|
|
339
|
+
}
|
|
340
|
+
const selectedFeatures = features.filter((feature) => selected.has(feature.id));
|
|
341
|
+
const ids = selectedFeatures.map((feature) => feature.id);
|
|
342
|
+
const nodes = selectedFeatures.map((feature) => ({
|
|
343
|
+
id: feature.id,
|
|
344
|
+
dirName: feature.dirName,
|
|
345
|
+
services: [...feature.services],
|
|
346
|
+
dependsOn: edges.filter((edge) => edge.from === feature.id).map((edge) => edge.to).sort(compareIds),
|
|
347
|
+
}));
|
|
348
|
+
return {
|
|
349
|
+
nodes,
|
|
350
|
+
edges,
|
|
351
|
+
conflicts: conflicts.filter((conflict) => conflict.features.some((id) => selected.has(id))),
|
|
352
|
+
order: dependencyFirstOrder(ids, edges),
|
|
353
|
+
cycles: stronglyConnected(ids, edges),
|
|
354
|
+
};
|
|
355
|
+
}
|
|
356
|
+
//# sourceMappingURL=dependencies.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"dependencies.js","sourceRoot":"","sources":["../../src/core/dependencies.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * A read-only dependency/conflict index for active features.\n *\n * This is deliberately derived from parsed artifacts rather than validation\n * messages: issue prose is for people and is not an API. The graph answers a\n * narrower question — which in-flight change must land before another because\n * it introduces an identity or operation the latter already refers to.\n */\nimport { existsSync } from \"node:fs\";\nimport { inOrder } from \"./concurrency.js\";\nimport { FleetContext } from \"./fleet-context.js\";\nimport { serviceResolver } from \"./likec4.js\";\nimport { operations } from \"./openapi.js\";\nimport {\n SPEC_AXES,\n compareIds,\n featurePaths,\n featureSpecPaths,\n listFeatures,\n servicePaths,\n type FeatureEntry,\n type SpecAxis,\n} from \"./repo.js\";\nimport type { Requirement } from \"./spec.js\";\n\nexport type DependencyReason =\n | {\n kind: \"requirement\";\n service: string;\n axis: SpecAxis[\"file\"];\n identity: string;\n }\n | { kind: \"operation\"; service: string; operationId: string };\n\nexport interface DependencyEdge {\n /** The feature that consumes/modifies something. */\n from: string;\n /** The feature that introduces it and therefore has to land first. */\n to: string;\n reasons: DependencyReason[];\n}\n\nexport interface FeatureConflict {\n kind: \"requirement\" | \"operation\";\n /**\n * How the owners collide.\n *\n * `added` — two features ADD the same identity (or define the same\n * operationId): whichever archives second replaces the first outright.\n *\n * `changed` — two features MODIFY or REMOVE the same living requirement.\n * That collision was invisible until now, and it is the more common one on a\n * fleet: nobody adds \"Cancel an order\" twice, but two teams editing its text\n * in the same week is a Tuesday. A MODIFIED requirement carries its FULL new\n * text, so the second archive does not merge the first one's wording — it\n * overwrites it, silently and completely.\n */\n change: \"added\" | \"changed\";\n service: string;\n axis?: SpecAxis[\"file\"];\n identity: string;\n features: string[];\n}\n\nexport interface DependencyNode {\n id: string;\n dirName: string;\n services: string[];\n dependsOn: string[];\n}\n\nexport interface DependencyGraph {\n nodes: DependencyNode[];\n edges: DependencyEdge[];\n conflicts: FeatureConflict[];\n /** Dependencies precede their consumers; cyclic members remain deterministic. */\n order: string[];\n /** Strongly connected components with more than one node (or a self-loop). */\n cycles: string[][];\n}\n\ninterface FeatureFacts {\n feature: FeatureEntry;\n addedRequirements: Array<{\n service: string;\n axis: SpecAxis[\"file\"];\n requirement: Requirement;\n }>;\n changedRequirements: Array<{\n service: string;\n axis: SpecAxis[\"file\"];\n requirement: Requirement;\n }>;\n introducedOperations: Array<{ service: string; operationId: string }>;\n requiredOperations: Array<{ service: string; operationId: string }>;\n}\n\nfunction reqKeys(requirement: Pick<Requirement, \"id\" | \"name\">): string[] {\n return [\n ...(requirement.id === undefined ? [] : [`id:${requirement.id}`]),\n `name:${requirement.name}`,\n ];\n}\n\n/**\n * WHERE a collision happens, kept as fields rather than as a string.\n *\n * An index needs one scalar per entry to hash on, so the fields get NUL-joined\n * into a key — but the conflict records want those fields back, and splitting\n * the key apart again means asserting the pieces back into their types. That\n * assertion is a claim about data loam does not own: a service directory or a\n * requirement name containing a NUL would hand `axis` a fragment of an identity\n * with nothing to notice. Carrying the structured value beside the owner set\n * means the key is only ever a key, and the record is read, not reconstructed.\n */\ninterface ReqAt {\n service: string;\n axis: SpecAxis[\"file\"];\n identity: string;\n}\n\ninterface OpAt {\n service: string;\n operationId: string;\n}\n\ninterface Owners<T> {\n readonly at: T;\n readonly features: Set<string>;\n}\n\nfunction reqIndexKey(at: ReqAt): string {\n return `${at.service}\\0${at.axis}\\0${at.identity}`;\n}\n\nfunction opIndexKey(at: OpAt): string {\n return `${at.service}\\0${at.operationId}`;\n}\n\nfunction addOwner<T>(index: Map<string, Owners<T>>, key: string, at: T, feature: string): void {\n const owners = index.get(key) ?? { at, features: new Set<string>() };\n owners.features.add(feature);\n index.set(key, owners);\n}\n\nasync function readFacts(\n docsDir: string,\n feature: FeatureEntry,\n context: FleetContext,\n): Promise<FeatureFacts> {\n const addedRequirements: FeatureFacts[\"addedRequirements\"] = [];\n const changedRequirements: FeatureFacts[\"changedRequirements\"] = [];\n const introducedOperations: FeatureFacts[\"introducedOperations\"] = [];\n const requiredOperations: FeatureFacts[\"requiredOperations\"] = [];\n\n /**\n * The operationIds a service ALREADY has, per service.\n *\n * This is the whole difference between a dependency graph and a rumour mill.\n * A feature's `openapi.yaml` is a COMPLETE document, not a patch — authors\n * restate the living API around the slot they are changing — so every feature\n * that so much as mentions `authorizePayment` used to be recorded as\n * INTRODUCING it, and every other feature governing it as REQUIRING it. Two\n * features quoting the same living operation therefore depended on each other\n * in both directions: an invented cycle, an invented conflict, and an ordering\n * for work that had none. `loam validate`'s coherence pass already subtracts\n * the living contract this way (core/coherence.ts); this is the same\n * subtraction, so the two cannot disagree about what \"new\" means.\n *\n * Cached per service inside the call and, through the FleetContext, across\n * features: a fleet-wide graph asks about the same ten services N times.\n */\n const livingByService = new Map<string, Set<string>>();\n const living = async (service: string): Promise<Set<string>> => {\n let ids = livingByService.get(service);\n if (ids === undefined) {\n ids = new Set(\n (await operations(servicePaths(docsDir, service).openapi, context))\n .filter((op) => !op.remove)\n .map((op) => op.id),\n );\n livingByService.set(service, ids);\n }\n return ids;\n };\n\n for (const service of feature.services) {\n const paths = featureSpecPaths(feature.dir, service);\n const livingIds = await living(service);\n for (const axis of SPEC_AXES) {\n const path = paths[axis.key];\n if (!existsSync(path)) continue;\n const requirements = await context.readRequirements(path);\n for (const requirement of requirements) {\n const fact = { service, axis: axis.file, requirement };\n if (requirement.kind === \"ADDED\") addedRequirements.push(fact);\n if (requirement.kind === \"MODIFIED\" || requirement.kind === \"REMOVED\") {\n changedRequirements.push(fact);\n }\n for (const operationId of requirement.operations) {\n // Governing an operation the service already provides is not a\n // dependency on anybody: the contract it needs is already merged.\n if (livingIds.has(operationId)) continue;\n requiredOperations.push({ service, operationId });\n }\n }\n }\n\n if (existsSync(paths.openapi)) {\n const featureOperations = await operations(paths.openapi, context);\n for (const operation of featureOperations) {\n if (livingIds.has(operation.id)) continue;\n if (operation.remove) requiredOperations.push({ service, operationId: operation.id });\n else introducedOperations.push({ service, operationId: operation.id });\n }\n }\n }\n\n // The C4 delta is the FOURTH place a feature names an operation, and it was\n // the one the graph could not see: an edge carrying `metadata { op 'x' }` is\n // a call this feature builds on `x`, which is exactly the \"must land first\"\n // relation this module exists to compute. Validate already says so — it\n // reports `c4-api.op-pending` when the op is defined by another feature in\n // flight — so the graph disagreeing with the validator about the same pair of\n // features was the graph being wrong, not quiet.\n //\n // Endpoints resolve through `serviceResolver` — the element's\n // `metadata { service }` binding, title as fallback — the same join every\n // other check uses, so renaming a box in a diagram cannot silently unhook it\n // here either.\n const deltaPath = featurePaths(feature.dir).delta;\n if (existsSync(deltaPath)) {\n const doc = await context.loadLikeC4(deltaPath);\n // A delta that does not parse proves nothing about who depends on whom.\n // `loam validate` owns that diagnosis (`delta.invalid`); inventing edges out\n // of a half-read document would be worse than the silence.\n if (doc.errors.length === 0) {\n // One resolver for the whole delta: `serviceOf` rebuilds its id map on\n // every call, and this loop asks once per edge.\n const svcOf = serviceResolver(doc.elements);\n for (const rel of doc.relationships) {\n if (rel.op === undefined) continue;\n const service = svcOf(rel.target);\n if ((await living(service)).has(rel.op)) continue;\n requiredOperations.push({ service, operationId: rel.op });\n }\n }\n }\n\n return {\n feature,\n addedRequirements,\n changedRequirements,\n introducedOperations,\n requiredOperations,\n };\n}\n\nfunction compareReasons(a: DependencyReason, b: DependencyReason): number {\n const ak = a.kind === \"requirement\"\n ? `${a.kind}\\0${a.service}\\0${a.axis}\\0${a.identity}`\n : `${a.kind}\\0${a.service}\\0${a.operationId}`;\n const bk = b.kind === \"requirement\"\n ? `${b.kind}\\0${b.service}\\0${b.axis}\\0${b.identity}`\n : `${b.kind}\\0${b.service}\\0${b.operationId}`;\n return compareIds(ak, bk);\n}\n\nfunction edgeKey(from: string, to: string): string {\n return `${from}\\0${to}`;\n}\n\nfunction appendReason(\n edges: Map<string, DependencyEdge>,\n from: string,\n to: string,\n reason: DependencyReason,\n): void {\n if (from === to) return;\n const key = edgeKey(from, to);\n const edge = edges.get(key) ?? { from, to, reasons: [] };\n const encoded = JSON.stringify(reason);\n if (!edge.reasons.some((item) => JSON.stringify(item) === encoded)) edge.reasons.push(reason);\n edges.set(key, edge);\n}\n\nfunction stronglyConnected(ids: string[], edges: DependencyEdge[]): string[][] {\n const adjacency = new Map(ids.map((id) => [id, [] as string[]]));\n for (const edge of edges) adjacency.get(edge.from)?.push(edge.to);\n for (const targets of adjacency.values()) targets.sort(compareIds);\n\n let nextIndex = 0;\n const indices = new Map<string, number>();\n const low = new Map<string, number>();\n const stack: string[] = [];\n const onStack = new Set<string>();\n const components: string[][] = [];\n\n const visit = (id: string): void => {\n indices.set(id, nextIndex);\n low.set(id, nextIndex);\n nextIndex += 1;\n stack.push(id);\n onStack.add(id);\n\n for (const target of adjacency.get(id) ?? []) {\n if (!indices.has(target)) {\n visit(target);\n low.set(id, Math.min(low.get(id)!, low.get(target)!));\n } else if (onStack.has(target)) {\n low.set(id, Math.min(low.get(id)!, indices.get(target)!));\n }\n }\n if (low.get(id) !== indices.get(id)) return;\n const component: string[] = [];\n while (stack.length > 0) {\n const member = stack.pop()!;\n onStack.delete(member);\n component.push(member);\n if (member === id) break;\n }\n component.sort(compareIds);\n const selfLoop = component.length === 1\n && (adjacency.get(component[0]!) ?? []).includes(component[0]!);\n if (component.length > 1 || selfLoop) components.push(component);\n };\n\n for (const id of [...ids].sort(compareIds)) if (!indices.has(id)) visit(id);\n return components.sort((a, b) => compareIds(a[0]!, b[0]!));\n}\n\nfunction dependencyFirstOrder(ids: string[], edges: DependencyEdge[]): string[] {\n const deps = new Map(ids.map((id) => [id, [] as string[]]));\n for (const edge of edges) deps.get(edge.from)?.push(edge.to);\n for (const values of deps.values()) values.sort(compareIds);\n const visited = new Set<string>();\n const visiting = new Set<string>();\n const order: string[] = [];\n const visit = (id: string): void => {\n if (visited.has(id) || visiting.has(id)) return;\n visiting.add(id);\n for (const dependency of deps.get(id) ?? []) visit(dependency);\n visiting.delete(id);\n visited.add(id);\n order.push(id);\n };\n for (const id of [...ids].sort(compareIds)) visit(id);\n return order;\n}\n\n/** Derive the complete active-feature graph, or one feature and its transitive prerequisites. */\nexport async function analyzeDependencies(\n docsDir: string,\n featureId?: string,\n context = new FleetContext(),\n): Promise<DependencyGraph> {\n const features = await listFeatures(docsDir, {}, context);\n // Bounded, not `Promise.all`: `readFacts` loads the feature's delta.likec4,\n // and each of those is a whole Langium workspace held open until the read\n // finishes — the same resource `validate --all` rations for the same measured\n // reason (see core/concurrency.ts). This module fanned out over every active\n // feature at once, and it is not only `loam dependencies` that pays: the fleet\n // form of `loam status` calls in here too. `inOrder` returns results in input\n // order, so the graph, the conflict list and their ordering are unchanged.\n const facts = await inOrder(features, (feature) => readFacts(docsDir, feature, context));\n const requirementOwners = new Map<string, Owners<ReqAt>>();\n // The second index over the SAME identity: who MODIFIES or REMOVES it. Only\n // the first index existed, so the graph could see \"two features add the same\n // requirement\" and was blind to \"two features rewrite the same requirement\" —\n // the collision that actually happens on a fleet, and the one whose loser\n // loses their whole authored text without a word from any command.\n const changedOwners = new Map<string, Owners<ReqAt>>();\n const operationOwners = new Map<string, Owners<OpAt>>();\n\n for (const fact of facts) {\n for (const added of fact.addedRequirements) {\n for (const identity of reqKeys(added.requirement)) {\n const at: ReqAt = { service: added.service, axis: added.axis, identity };\n addOwner(requirementOwners, reqIndexKey(at), at, fact.feature.id);\n }\n }\n for (const changed of fact.changedRequirements) {\n for (const identity of reqKeys(changed.requirement)) {\n const at: ReqAt = { service: changed.service, axis: changed.axis, identity };\n addOwner(changedOwners, reqIndexKey(at), at, fact.feature.id);\n }\n }\n for (const operation of fact.introducedOperations) {\n addOwner(operationOwners, opIndexKey(operation), operation, fact.feature.id);\n }\n }\n\n const edgeMap = new Map<string, DependencyEdge>();\n for (const fact of facts) {\n for (const changed of fact.changedRequirements) {\n for (const identity of reqKeys(changed.requirement)) {\n const at: ReqAt = { service: changed.service, axis: changed.axis, identity };\n for (const owner of requirementOwners.get(reqIndexKey(at))?.features ?? []) {\n appendReason(edgeMap, fact.feature.id, owner, { kind: \"requirement\", ...at });\n }\n }\n }\n for (const required of fact.requiredOperations) {\n for (const owner of operationOwners.get(opIndexKey(required))?.features ?? []) {\n appendReason(edgeMap, fact.feature.id, owner, { kind: \"operation\", ...required });\n }\n }\n }\n\n let edges = [...edgeMap.values()]\n .map((edge) => ({ ...edge, reasons: edge.reasons.sort(compareReasons) }))\n .sort((a, b) => compareIds(a.from, b.from) || compareIds(a.to, b.to));\n\n const conflicts: FeatureConflict[] = [];\n for (const [index, change] of [\n [requirementOwners, \"added\"],\n [changedOwners, \"changed\"],\n ] as const) {\n for (const { at, features } of index.values()) {\n if (features.size < 2) continue;\n conflicts.push({\n kind: \"requirement\",\n change,\n service: at.service,\n axis: at.axis,\n identity: at.identity,\n features: [...features].sort(compareIds),\n });\n }\n }\n for (const { at, features } of operationOwners.values()) {\n if (features.size < 2) continue;\n conflicts.push({\n kind: \"operation\",\n // Two features DEFINING the same operationId — the only way an operation\n // collides, since removals address a living slot rather than claim one.\n change: \"added\",\n service: at.service,\n identity: at.operationId,\n features: [...features].sort(compareIds),\n });\n }\n conflicts.sort((a, b) =>\n compareIds(a.kind, b.kind)\n || compareIds(a.change, b.change)\n || compareIds(a.service, b.service)\n || compareIds(a.axis ?? \"\", b.axis ?? \"\")\n || compareIds(a.identity, b.identity));\n\n let selected = new Set(features.map((feature) => feature.id));\n if (featureId !== undefined) {\n selected = new Set<string>([featureId]);\n let changed = true;\n while (changed) {\n changed = false;\n for (const edge of edges) {\n if (selected.has(edge.from) && !selected.has(edge.to)) {\n selected.add(edge.to);\n changed = true;\n }\n }\n }\n edges = edges.filter((edge) => selected.has(edge.from) && selected.has(edge.to));\n }\n\n const selectedFeatures = features.filter((feature) => selected.has(feature.id));\n const ids = selectedFeatures.map((feature) => feature.id);\n const nodes = selectedFeatures.map((feature): DependencyNode => ({\n id: feature.id,\n dirName: feature.dirName,\n services: [...feature.services],\n dependsOn: edges.filter((edge) => edge.from === feature.id).map((edge) => edge.to).sort(compareIds),\n }));\n\n return {\n nodes,\n edges,\n conflicts: conflicts.filter((conflict) => conflict.features.some((id) => selected.has(id))),\n order: dependencyFirstOrder(ids, edges),\n cycles: stronglyConnected(ids, edges),\n };\n}\n"]}
|
|
@@ -0,0 +1,144 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Scaffolding for the shared docs repo.
|
|
3
|
+
*
|
|
4
|
+
* There is deliberately no manifest. `init` used to write a `loam.docs.json`
|
|
5
|
+
* listing the repo's services; nothing ever read it — `repo.ts` enumerates from
|
|
6
|
+
* the filesystem, because files are the source of truth — and nothing ever
|
|
7
|
+
* updated it, so it named an empty fleet forever. A second list of services is
|
|
8
|
+
* exactly the drift `loam validate` now cross-checks the landscape for; the
|
|
9
|
+
* cheapest way to keep it honest is not to have it.
|
|
10
|
+
*/
|
|
11
|
+
import { mkdir, writeFile } from "node:fs/promises";
|
|
12
|
+
import { existsSync } from "node:fs";
|
|
13
|
+
import { join, resolve } from "node:path";
|
|
14
|
+
import { AGENTS_MD } from "./agent.js";
|
|
15
|
+
import { AGENTS_FILENAME } from "./repo.js";
|
|
16
|
+
/**
|
|
17
|
+
* Top-level layout of the shared docs repo, and part of its identity rather
|
|
18
|
+
* than a convenience: `services/` is what makes a directory a docs repo —
|
|
19
|
+
* `init` will not join one without it, and every enumerating command refuses a
|
|
20
|
+
* `docsDir` that has none. So it is laid down even when nothing is in it yet.
|
|
21
|
+
*/
|
|
22
|
+
export const DOCS_SUBDIRS = ["architecture", "services", "features"];
|
|
23
|
+
/**
|
|
24
|
+
* The fleet map, empty but valid.
|
|
25
|
+
*
|
|
26
|
+
* Nobody writes this file automatically — `adopt` deliberately does not touch
|
|
27
|
+
* it, because who calls whom is a human judgement and a generated landscape
|
|
28
|
+
* would be a guess presented as the map. But "nobody writes it" used to mean
|
|
29
|
+
* "it is simply absent", and an absent landscape is the one artifact whose
|
|
30
|
+
* absence silences the fleet-wide checks entirely: every cross-service breach
|
|
31
|
+
* `validate` exists to find is invisible on a repo with no landscape.
|
|
32
|
+
*
|
|
33
|
+
* So the scaffold lays down the empty map instead: the four element kinds a
|
|
34
|
+
* fleet actually uses are declared, the model is empty, and the comments say
|
|
35
|
+
* what to add and why. The first `loam adopt` then has somewhere to be drawn.
|
|
36
|
+
*
|
|
37
|
+
* No `views` block, deliberately. loam reads elements and relationships and
|
|
38
|
+
* never a view, so the scaffolded `view index { include * }` was a diagram loam
|
|
39
|
+
* planted on every repo and then never drew — and views are the expensive half
|
|
40
|
+
* of LikeC4: computing them is superlinear in edge count (120 services / 300
|
|
41
|
+
* calls does not finish in three minutes; parsing the same file takes 11 ms).
|
|
42
|
+
* loam stopped computing views, so the block is free for loam itself now, but
|
|
43
|
+
* scaffolding one still hands the user a fleet-sized bill for the first tool
|
|
44
|
+
* that does compute it. Teams who want diagrams declare their own.
|
|
45
|
+
*/
|
|
46
|
+
const LANDSCAPE_STUB = `// The fleet map: every service in services/ appears here, and every call
|
|
47
|
+
// between two of them is an edge. This file is written by hand — loam never
|
|
48
|
+
// guesses it, because "who calls whom" is the one fact no generator can read
|
|
49
|
+
// off a repository.
|
|
50
|
+
//
|
|
51
|
+
// After \`loam adopt --service <id>\`, add the service here:
|
|
52
|
+
//
|
|
53
|
+
// paymentService = softwareSystem 'payment-service' {
|
|
54
|
+
// description 'Owns payment authorization/capture'
|
|
55
|
+
// metadata { service 'payment-service' } // binds the box to services/<id>/
|
|
56
|
+
// }
|
|
57
|
+
//
|
|
58
|
+
// and give each call the operationId it uses, so requirements, C4 and OpenAPI
|
|
59
|
+
// can be cross-checked:
|
|
60
|
+
//
|
|
61
|
+
// checkoutWeb -> paymentService 'Authorizes' {
|
|
62
|
+
// metadata { op 'authorizePayment' }
|
|
63
|
+
// }
|
|
64
|
+
//
|
|
65
|
+
// There is no \`views\` block, on purpose: loam reads the model and never a
|
|
66
|
+
// view, so it would draw nothing. Add views here if you want diagrams, and
|
|
67
|
+
// render them with LikeC4's own tooling (\`npx likec4 start\`) — but scope them,
|
|
68
|
+
// because computing a view is superlinear in the number of edges and an
|
|
69
|
+
// \`include *\` over a whole fleet takes minutes.
|
|
70
|
+
|
|
71
|
+
specification {
|
|
72
|
+
element person
|
|
73
|
+
element softwareSystem
|
|
74
|
+
element container
|
|
75
|
+
element database
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
model {
|
|
79
|
+
}
|
|
80
|
+
`;
|
|
81
|
+
/**
|
|
82
|
+
* The docs repo's own loam.json. It makes the docs repo self-describing: a
|
|
83
|
+
* command run from inside it (or from any directory under it) finds this file
|
|
84
|
+
* first and resolves the fleet to the repo it is standing in, instead of
|
|
85
|
+
* walking out to whatever service repo happens to be above.
|
|
86
|
+
*
|
|
87
|
+
* `"."` and not an absolute path for the same reason every other docsDir is
|
|
88
|
+
* stored as written: this file is committed and cloned to machines whose
|
|
89
|
+
* directory layout nobody here can predict.
|
|
90
|
+
*/
|
|
91
|
+
const DOCS_SELF_CONFIG = `${JSON.stringify({ docsDir: "." }, null, 2)}\n`;
|
|
92
|
+
/** Idempotently create the docs-repo skeleton. Existing files/dirs are left untouched. */
|
|
93
|
+
export async function scaffoldDocs(docsDir) {
|
|
94
|
+
const root = resolve(docsDir);
|
|
95
|
+
const created = [];
|
|
96
|
+
await mkdir(root, { recursive: true });
|
|
97
|
+
for (const dir of DOCS_SUBDIRS) {
|
|
98
|
+
const p = join(root, dir);
|
|
99
|
+
if (!existsSync(p)) {
|
|
100
|
+
await mkdir(p, { recursive: true });
|
|
101
|
+
created.push(p);
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
// Never overwritten, all three of them — a team's own house rules, their own
|
|
105
|
+
// map and their own config outrank the template. The order here IS the order
|
|
106
|
+
// `init` probes for what it will skip; keep the two lists in step.
|
|
107
|
+
for (const [rel, content] of docsRepoFiles()) {
|
|
108
|
+
const path = join(root, rel);
|
|
109
|
+
if (!existsSync(path)) {
|
|
110
|
+
await writeFile(path, content, "utf8");
|
|
111
|
+
created.push(path);
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
return { root, created };
|
|
115
|
+
}
|
|
116
|
+
/**
|
|
117
|
+
* The files every docs repo starts with, relative to its root, in creation
|
|
118
|
+
* order. `init` scaffolds them directly; `migrate-openspec` stages the same
|
|
119
|
+
* bytes into its target, because the whole point of migrating into a docs repo
|
|
120
|
+
* is that it be the docs repo `loam init` makes.
|
|
121
|
+
*
|
|
122
|
+
* Exported as paths only (via `plannedDocsFiles`) where `init` reports what it
|
|
123
|
+
* will skip — a duplicate list is exactly the drift that made `created +
|
|
124
|
+
* skipped` disagree with reality.
|
|
125
|
+
*/
|
|
126
|
+
export function docsRepoFiles(opts = {}) {
|
|
127
|
+
const preamble = opts.landscapePreamble === undefined ? "" : `${opts.landscapePreamble.trimEnd()}\n//\n`;
|
|
128
|
+
return [
|
|
129
|
+
// The process contract lives with the docs it describes, so an agent handed
|
|
130
|
+
// only the docs repo still knows the cycle.
|
|
131
|
+
[AGENTS_FILENAME, AGENTS_MD],
|
|
132
|
+
[join("architecture", "landscape.likec4"), `${preamble}${LANDSCAPE_STUB}`],
|
|
133
|
+
["loam.json", DOCS_SELF_CONFIG],
|
|
134
|
+
];
|
|
135
|
+
}
|
|
136
|
+
/** Everything `scaffoldDocs(docsDir)` would create, in the order it creates it. */
|
|
137
|
+
export function plannedDocsFiles(docsDir) {
|
|
138
|
+
const root = resolve(docsDir);
|
|
139
|
+
return [
|
|
140
|
+
...DOCS_SUBDIRS.map((d) => join(root, d)),
|
|
141
|
+
...docsRepoFiles().map(([rel]) => join(root, rel)),
|
|
142
|
+
];
|
|
143
|
+
}
|
|
144
|
+
//# sourceMappingURL=docs.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"docs.js","sourceRoot":"","sources":["../../src/core/docs.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * Scaffolding for the shared docs repo.\n *\n * There is deliberately no manifest. `init` used to write a `loam.docs.json`\n * listing the repo's services; nothing ever read it — `repo.ts` enumerates from\n * the filesystem, because files are the source of truth — and nothing ever\n * updated it, so it named an empty fleet forever. A second list of services is\n * exactly the drift `loam validate` now cross-checks the landscape for; the\n * cheapest way to keep it honest is not to have it.\n */\nimport { mkdir, writeFile } from \"node:fs/promises\";\nimport { existsSync } from \"node:fs\";\nimport { join, resolve } from \"node:path\";\nimport { AGENTS_MD } from \"./agent.js\";\nimport { AGENTS_FILENAME } from \"./repo.js\";\n\n/**\n * Top-level layout of the shared docs repo, and part of its identity rather\n * than a convenience: `services/` is what makes a directory a docs repo —\n * `init` will not join one without it, and every enumerating command refuses a\n * `docsDir` that has none. So it is laid down even when nothing is in it yet.\n */\nexport const DOCS_SUBDIRS = [\"architecture\", \"services\", \"features\"] as const;\n\n/**\n * The fleet map, empty but valid.\n *\n * Nobody writes this file automatically — `adopt` deliberately does not touch\n * it, because who calls whom is a human judgement and a generated landscape\n * would be a guess presented as the map. But \"nobody writes it\" used to mean\n * \"it is simply absent\", and an absent landscape is the one artifact whose\n * absence silences the fleet-wide checks entirely: every cross-service breach\n * `validate` exists to find is invisible on a repo with no landscape.\n *\n * So the scaffold lays down the empty map instead: the four element kinds a\n * fleet actually uses are declared, the model is empty, and the comments say\n * what to add and why. The first `loam adopt` then has somewhere to be drawn.\n *\n * No `views` block, deliberately. loam reads elements and relationships and\n * never a view, so the scaffolded `view index { include * }` was a diagram loam\n * planted on every repo and then never drew — and views are the expensive half\n * of LikeC4: computing them is superlinear in edge count (120 services / 300\n * calls does not finish in three minutes; parsing the same file takes 11 ms).\n * loam stopped computing views, so the block is free for loam itself now, but\n * scaffolding one still hands the user a fleet-sized bill for the first tool\n * that does compute it. Teams who want diagrams declare their own.\n */\nconst LANDSCAPE_STUB = `// The fleet map: every service in services/ appears here, and every call\n// between two of them is an edge. This file is written by hand — loam never\n// guesses it, because \"who calls whom\" is the one fact no generator can read\n// off a repository.\n//\n// After \\`loam adopt --service <id>\\`, add the service here:\n//\n// paymentService = softwareSystem 'payment-service' {\n// description 'Owns payment authorization/capture'\n// metadata { service 'payment-service' } // binds the box to services/<id>/\n// }\n//\n// and give each call the operationId it uses, so requirements, C4 and OpenAPI\n// can be cross-checked:\n//\n// checkoutWeb -> paymentService 'Authorizes' {\n// metadata { op 'authorizePayment' }\n// }\n//\n// There is no \\`views\\` block, on purpose: loam reads the model and never a\n// view, so it would draw nothing. Add views here if you want diagrams, and\n// render them with LikeC4's own tooling (\\`npx likec4 start\\`) — but scope them,\n// because computing a view is superlinear in the number of edges and an\n// \\`include *\\` over a whole fleet takes minutes.\n\nspecification {\n element person\n element softwareSystem\n element container\n element database\n}\n\nmodel {\n}\n`;\n\n/**\n * The docs repo's own loam.json. It makes the docs repo self-describing: a\n * command run from inside it (or from any directory under it) finds this file\n * first and resolves the fleet to the repo it is standing in, instead of\n * walking out to whatever service repo happens to be above.\n *\n * `\".\"` and not an absolute path for the same reason every other docsDir is\n * stored as written: this file is committed and cloned to machines whose\n * directory layout nobody here can predict.\n */\nconst DOCS_SELF_CONFIG = `${JSON.stringify({ docsDir: \".\" }, null, 2)}\\n`;\n\nexport interface ScaffoldResult {\n root: string;\n created: string[];\n}\n\n/** Idempotently create the docs-repo skeleton. Existing files/dirs are left untouched. */\nexport async function scaffoldDocs(docsDir: string): Promise<ScaffoldResult> {\n const root = resolve(docsDir);\n const created: string[] = [];\n\n await mkdir(root, { recursive: true });\n\n for (const dir of DOCS_SUBDIRS) {\n const p = join(root, dir);\n if (!existsSync(p)) {\n await mkdir(p, { recursive: true });\n created.push(p);\n }\n }\n\n // Never overwritten, all three of them — a team's own house rules, their own\n // map and their own config outrank the template. The order here IS the order\n // `init` probes for what it will skip; keep the two lists in step.\n for (const [rel, content] of docsRepoFiles()) {\n const path = join(root, rel);\n if (!existsSync(path)) {\n await writeFile(path, content, \"utf8\");\n created.push(path);\n }\n }\n\n return { root, created };\n}\n\nexport interface DocsRepoFileOptions {\n /**\n * Comment lines prepended to the landscape stub, for a scaffold with a reason\n * of its own to hand over an empty map — `migrate-openspec` stages a docs repo\n * out of a corpus that HAS no topology, and says so where the migrator reads.\n *\n * A parameter and not a second template on purpose. The rest of the stub —\n * how to declare a service, how to bind an edge to an operationId, why there\n * is no `views` block — is the same advice however the repo came to exist, and\n * the migration used to carry its own shorter copy of it: two files that were\n * both correct on the day they were written and could only diverge after.\n */\n landscapePreamble?: string;\n}\n\n/**\n * The files every docs repo starts with, relative to its root, in creation\n * order. `init` scaffolds them directly; `migrate-openspec` stages the same\n * bytes into its target, because the whole point of migrating into a docs repo\n * is that it be the docs repo `loam init` makes.\n *\n * Exported as paths only (via `plannedDocsFiles`) where `init` reports what it\n * will skip — a duplicate list is exactly the drift that made `created +\n * skipped` disagree with reality.\n */\nexport function docsRepoFiles(opts: DocsRepoFileOptions = {}): Array<[string, string]> {\n const preamble = opts.landscapePreamble === undefined ? \"\" : `${opts.landscapePreamble.trimEnd()}\\n//\\n`;\n return [\n // The process contract lives with the docs it describes, so an agent handed\n // only the docs repo still knows the cycle.\n [AGENTS_FILENAME, AGENTS_MD],\n [join(\"architecture\", \"landscape.likec4\"), `${preamble}${LANDSCAPE_STUB}`],\n [\"loam.json\", DOCS_SELF_CONFIG],\n ];\n}\n\n/** Everything `scaffoldDocs(docsDir)` would create, in the order it creates it. */\nexport function plannedDocsFiles(docsDir: string): string[] {\n const root = resolve(docsDir);\n return [\n ...DOCS_SUBDIRS.map((d) => join(root, d)),\n ...docsRepoFiles().map(([rel]) => join(root, rel)),\n ];\n}\n"]}
|