@ybotok/loam 0.2.0-alpha.2 → 0.2.0-alpha.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (35) hide show
  1. package/CHANGELOG.md +78 -0
  2. package/README.md +3 -3
  3. package/ROADMAP.md +37 -5
  4. package/SCHEMA.md +33 -1
  5. package/dist/commands/archive/run.js +46 -2
  6. package/dist/commands/archive/run.js.map +1 -1
  7. package/dist/commands/seed/plan.js +1 -1
  8. package/dist/commands/seed/plan.js.map +1 -1
  9. package/dist/commands/subsystem/sync.js +1 -1
  10. package/dist/commands/subsystem/sync.js.map +1 -1
  11. package/dist/commands/subsystem/txn/txn.js +1 -1
  12. package/dist/commands/subsystem/txn/txn.js.map +1 -1
  13. package/dist/commands/subsystem/txn/views.js +23 -11
  14. package/dist/commands/subsystem/txn/views.js.map +1 -1
  15. package/dist/commands/validate/fleet/views/ids.js +6 -1
  16. package/dist/commands/validate/fleet/views/ids.js.map +1 -1
  17. package/dist/commands/validate/fleet/views/stale.js +15 -39
  18. package/dist/commands/validate/fleet/views/stale.js.map +1 -1
  19. package/dist/core/agent/agents-md/map/subsystem.js +8 -1
  20. package/dist/core/agent/agents-md/map/subsystem.js.map +1 -1
  21. package/dist/core/agent/agents-md.js +4 -0
  22. package/dist/core/agent/agents-md.js.map +1 -1
  23. package/dist/core/repo/paths.js +1 -1
  24. package/dist/core/repo/paths.js.map +1 -1
  25. package/dist/core/repo/tree/render/body.js +219 -0
  26. package/dist/core/repo/tree/render/body.js.map +1 -0
  27. package/dist/core/repo/tree/render/stale.js +26 -0
  28. package/dist/core/repo/tree/render/stale.js.map +1 -0
  29. package/dist/core/repo/tree/render/text.js +84 -0
  30. package/dist/core/repo/tree/render/text.js.map +1 -0
  31. package/dist/core/repo/tree/{views.js → render/views.js} +38 -34
  32. package/dist/core/repo/tree/render/views.js.map +1 -0
  33. package/examples/docs/architecture/subsystems.likec4 +4 -0
  34. package/package.json +1 -1
  35. package/dist/core/repo/tree/views.js.map +0 -1
package/CHANGELOG.md CHANGED
@@ -10,6 +10,84 @@ case for a change — the alternative that was rejected, the defect it came from
10
10
  generalises — lives where it is maintained: [SCHEMA.md](SCHEMA.md) for the rule,
11
11
  [ROADMAP.md](ROADMAP.md) for the priority and its exit criteria, and the commit that landed it._
12
12
 
13
+ ## [0.2.0-alpha.4] - 2026-09-03
14
+
15
+ The view title becomes the subsystem's path. `0.2.0-alpha.3` was tagged and **never published** — it
16
+ was withdrawn before its publish step for a reason unrelated to the code — so this is the release
17
+ that carries both it and the entry below, and `npm i @ybotok/loam@alpha` moves from `0.2.0-alpha.2`
18
+ straight to this one. A repo upgrading from either reports one `subsystem.views-stale` until one
19
+ `loam subsystem sync`.
20
+
21
+ ### Changed — a generated subsystem view is titled by its path
22
+
23
+ `0.2.0-alpha.3` gave each view the subsystem's own label. It now carries the whole path — every
24
+ marked ancestor's label and its own, joined with ` / `, so `services/platform/order-flow` reads
25
+ `Platform / Order flow`. Measured against the pinned likec4: the renderer reads that as a folder
26
+ path, so the view list mirrors the directory tree a human already navigates — one folder per
27
+ ancestor, the leaf's own label as the title — and two subsystems whose labels are equal are told
28
+ apart by their parents instead of appearing twice under one name. The box a view draws keeps the
29
+ subsystem's own label, unprefixed.
30
+
31
+ Because loam is now the one composing with `/`, it owns what a `/` inside an authored label would
32
+ do: measured, LikeC4 would show only the last segment and add a folder level the tree does not have,
33
+ so a slash in a marker's `title` becomes `∕` (U+2215) and the whole authored label stays visible as
34
+ one folder name. The alternative — a fixed `loam subsystems /` prefix — bought the same folder
35
+ without the churn and put loam's own name in a reader's UI; the tree's own words won.
36
+
37
+ Two costs, both real: the generated bytes move again, so a repo upgrading from `0.2.0-alpha.3`
38
+ reports one `subsystem.views-stale` until one `loam subsystem sync`; and renaming a PARENT's title
39
+ now rewrites the bytes of every view beneath it.
40
+
41
+ ## [0.2.0-alpha.3] - 2026-09-03
42
+
43
+ The generated subsystem views, made readable — and the defect that hunt found underneath them.
44
+ Still a prerelease on the `alpha` line: `npm i @ybotok/loam@alpha` installs it. Nothing below is
45
+ breaking, and one entry is a change every repo NOTICES: the generated file's bytes move, so the
46
+ first `loam validate --all` after upgrading asks for one `loam subsystem sync`.
47
+
48
+ ### Fixed — a `subsystem.views-stale` no command could clear
49
+
50
+ `loam validate --all` grades `architecture/subsystems.likec4` against the whole `architecture/`
51
+ project — the landscape merged with every `architecture/usecases/*.likec4` — while `loam subsystem
52
+ sync` and the write verbs rendered it from `landscape.likec4` alone. One use-case document declaring
53
+ an element bound to a filed service was therefore enough to make `sync` answer `current` while
54
+ `validate --all` exited 1 with `subsystem.views-stale`, whose message names `sync` as the repair.
55
+ Reproduced on `examples/docs`; the two now read the same documents. No code, flag or payload key
56
+ changes.
57
+
58
+ ### Changed — the generated subsystem views carry a label and a boundary
59
+
60
+ `architecture/subsystems.likec4` exists only to be rendered, and in the renderer its views were the
61
+ least readable objects in a docs repo: no title, so LikeC4 showed the hex-escaped id
62
+ (`subsystem_platform__order_2dflow`), and leaf includes only, so a fleet drawn as ordinary
63
+ grouped C4 rendered flat. Every view now carries `title` (the marker's, or the directory name) and
64
+ `description` when the marker authors one, and draws a boundary: the model's own containing element
65
+ when one holds exactly the subsystem's members, a `group '<title>'` mirroring the directory subtree
66
+ when the landscape nests nothing, and — when the members are nested but no element owns exactly them
67
+ — no box plus one comment line, `// model: no boundary — …`, which is loam's whole answer to a tree
68
+ and a map that disagree. Nothing grades that comment: placement still carries no policy.
69
+
70
+ Marker text is normalized to one line and escaped before it is written (measured: a raw `'` in a
71
+ title is two parse errors and blanks the fleet's whole map in the renderer; `Ops' } view evil {` is
72
+ zero errors and injects a view). [SCHEMA.md](SCHEMA.md) states the rules.
73
+
74
+ What a repo notices, and what it costs:
75
+
76
+ - the first `loam validate --all` after upgrading reports exactly one `subsystem.views-stale` error
77
+ (exit 1) until one `loam subsystem sync` runs. `sync` does not `git add`, so the repair leaves one
78
+ unstaged one-file diff to commit;
79
+ - the boundary depends on which OTHER services sit under an element, so an unrelated adoption or
80
+ archive can flip a view from boxed to flat and restale the file. `loam archive` now withholds its
81
+ "living spec + landscape are now complete + current" line, prints one warning naming
82
+ `loam subsystem sync`, and adds `subsystemViewsStale: true` to its `--json` payload when it left
83
+ the file behind;
84
+ - `subsystem.views-stale` and `subsystem.view-id-collision` now carry `locations`: the generated
85
+ file (or `architecture/` when it does not exist yet) and the claimant document respectively. Both
86
+ used to fall back to the landscape, sending an editor to a file with nothing to fix in it.
87
+
88
+ No command, flag, exit code, envelope key or stable code string changes; `subsystem.views-stale` is
89
+ not in the `IssueCode` union and carries no `gates` key, so `loam archive` remains unaffected by it.
90
+
13
91
  ## [0.2.0-alpha.2] - 2026-09-02
14
92
 
15
93
  The deployment axis, and the two defects the axis surfaced on its way in. Still a prerelease on the
package/README.md CHANGED
@@ -7,7 +7,7 @@
7
7
  [![CI](https://github.com/ybotok/loam/actions/workflows/ci.yml/badge.svg)](https://github.com/ybotok/loam/actions/workflows/ci.yml)
8
8
  [![npm](https://img.shields.io/npm/v/@ybotok/loam.svg)](https://www.npmjs.com/package/@ybotok/loam)
9
9
 
10
- > **Pre-release: `0.2.0-alpha.2`**, the second release of the 0.2 line and the newest published
10
+ > **Pre-release: `0.2.0-alpha.4`**, the third published release of the 0.2 line and the newest published
11
11
  > version of [`@ybotok/loam`](https://www.npmjs.com/package/@ybotok/loam). It publishes under the
12
12
  > `alpha` npm dist-tag: `npm i @ybotok/loam@alpha`, or the exact version, always installs this
13
13
  > line. `latest` is moved onto a release by hand once it has published, so a plain
@@ -486,7 +486,7 @@ always preserved byte for byte, with the entry printed for manual merge.
486
486
  maps, what is lost, what must be added.
487
487
  - [ROADMAP.md](ROADMAP.md) — the evidence-backed improvement plan, priorities, non-goals and exit
488
488
  criteria.
489
- - [CHANGELOG.md](CHANGELOG.md) — released beta.1–alpha.2 plus the changes on `main` under
489
+ - [CHANGELOG.md](CHANGELOG.md) — released beta.1–alpha.4 plus the changes on `main` under
490
490
  `[Unreleased]`.
491
491
  - [docs/BENCHMARKS.md](https://github.com/ybotok/loam/blob/main/docs/BENCHMARKS.md) — what a
492
492
  fleet-sized run costs, with the method, the machine and the committed runs behind every number.
@@ -527,7 +527,7 @@ against a live readdir by `test/docs-facts.test.ts`, so this sentence fails the
527
527
  trails the tree.
528
528
 
529
529
  The package is published as [`@ybotok/loam`](https://www.npmjs.com/package/@ybotok/loam), currently
530
- `0.2.0-alpha.2`: the unscoped `loam` name is taken by an unrelated GDAL wrapper, so the package ships
530
+ `0.2.0-alpha.4`: the unscoped `loam` name is taken by an unrelated GDAL wrapper, so the package ships
531
531
  under the maintainer's own npm user scope. Releases are tag-driven and maintainer-only
532
532
  ([CONTRIBUTING.md](CONTRIBUTING.md)); the version this line names is pinned to `package.json` by a
533
533
  drift test, so it cannot silently trail a release.
package/ROADMAP.md CHANGED
@@ -1,6 +1,6 @@
1
1
  # Roadmap
2
2
 
3
- _Assessed 2026-09-02._
3
+ _Assessed 2026-09-03._
4
4
 
5
5
  This is loam's canonical improvement sequence. It records priorities and exit criteria, not delivery
6
6
  dates. An item is complete only when its exit criteria are supported by repeatable evidence; moving
@@ -18,7 +18,7 @@ acceptance tests, and implementation evidence remain ordinary files in repositor
18
18
  state inside a service. The CLI is small enough to audit, and every writer — not only archive — now
19
19
  commits through a locked, journaled transaction that a crash cannot leave half-applied.
20
20
 
21
- The tree contains **424 TypeScript modules in 129 source packages**, with an acyclic package graph
21
+ The tree contains **427 TypeScript modules in 130 source packages**, with an acyclic package graph
22
22
  checked by
23
23
  [scripts/package-graph.mjs](https://github.com/ybotok/loam/blob/main/scripts/package-graph.mjs). The
24
24
  CLI exposes **29 commands** from [src/cli.ts](https://github.com/ybotok/loam/blob/main/src/cli.ts),
@@ -27,10 +27,10 @@ dated snapshot below: each derives from the tree in one readdir, so
27
27
  [test/docs-facts.test.ts](https://github.com/ybotok/loam/blob/main/test/docs-facts.test.ts) grades
28
28
  them live and this paragraph cannot quietly trail the code the way its predecessor did.
29
29
 
30
- _Measured 2026-09-02 at `0.2.0-alpha.2`, and dated because neither number has a cheap derivation:_
30
+ _Measured 2026-09-03 at `0.2.0-alpha.4`, and dated because neither number has a cheap derivation:_
31
31
  lint, typecheck, `npm run arch:check` and `npm run meta:check` green; `npm test` passing
32
- **3,399/3,399 tests**, with two `skipIf(asRoot)` cases the root gate container cannot run; the
33
- coverage gate passing with **92.25% statements, 84.04% branches, 96.62% functions, and 94.18%
32
+ **3,410/3,410 tests**, with two `skipIf(asRoot)` cases the root gate container cannot run; the
33
+ coverage gate passing with **92.26% statements, 84.06% branches, 96.62% functions, and 94.20%
34
34
  lines** against its thresholds of 91 / 82 / 95 / 93; and `npm run release:check` plus
35
35
  `npm run test:package` green against the real tarball.
36
36
 
@@ -125,6 +125,26 @@ changes, exit criteria, and what each review surfaced — are in this file's his
125
125
  working tree and not committed, and the range goes in with the commit rather than being guessed at
126
126
  here.
127
127
 
128
+ #### The generated subsystem views — a label, a boundary, and one reader of the map
129
+
130
+ **Landed 2026-09-03.** Three problem reports from a fleet running loam, one axis. The generated
131
+ `architecture/subsystems.likec4` exists only to be rendered and was the least readable object in a
132
+ docs repo: no `title`, so the renderer showed the hex-escaped view id, and leaf includes only, so a
133
+ fleet drawn as grouped C4 rendered flat. Each view now carries the marker's title (the directory
134
+ name when it has none) and description, and draws a boundary the LANDSCAPE decides: the model's own
135
+ containing element when one holds exactly the members, a `group` mirroring the directory subtree
136
+ when the map nests nothing, and one `// model: no boundary — …` comment when the two disagree.
137
+ That comment is the whole answer to the drift the third report asked about — disclosure in a
138
+ generated artifact, never a finding, because grading placement against the map is the policy the
139
+ tree is defined not to carry. The graded alternative is written down under
140
+ [Later](#later--promote-only-from-evidence) with the evidence bar it has to clear.
141
+
142
+ The hunt found what neither report did: `validate --all` graded that file against the whole
143
+ `architecture/` project while every writer rendered it from `landscape.likec4` alone, so a
144
+ use-case document binding one element could produce a `subsystem.views-stale` that
145
+ `loam subsystem sync` reported as `current` — reproduced on `examples/docs`, and fixed by pointing
146
+ both sides at the same read.
147
+
128
148
  #### The deployment axis — topology joined to requirements
129
149
 
130
150
  **Promoted 2026-09-02, and authored as a feature in loam's own docs repo** rather than as an item
@@ -349,6 +369,18 @@ item gets promoted — it went from here to `## Now` and from there to
349
369
  - **UI generation:** begin with a disposable projection over the stable JSON contract only after CLI
350
370
  consumers demonstrate a repeated navigation problem. It must not introduce a second mutable state,
351
371
  hidden workflow state, or a required service.
372
+ - **A graded subsystem/model correspondence:** a fleet that draws its landscape as grouped C4 states
373
+ its grouping twice — once as directories under `services/`, once as the nesting of the elements —
374
+ and `loam subsystem move` changes the first while the second stays put. Since 2026-09-02 the
375
+ generated views file DISCLOSES that (the `// model: no boundary — …` line; see
376
+ [SCHEMA.md](SCHEMA.md)), and nothing grades it, because grading placement against the map is the
377
+ policy the tree is defined not to carry. The shape a graded version would take is known and
378
+ measured: an author's opt-in `metadata { subsystem '<name>' }` on the grouping element, read
379
+ through the existing metadata reader, the NAME rather than the path (names are unique in one flat
380
+ namespace and survive a move, which a path binding would not), with the check structurally silent
381
+ for every fleet that never writes one. Promote only on evidence from a second fleet, after living
382
+ with the disclosure — the first report classified itself `inconclusive`, and one fleet is not
383
+ enough to qualify an invariant that says placement is never part of any identity.
352
384
  - **Landscape scaling:** retain one landscape while conflicts are exceptional. If same-service
353
385
  conflicts become routine — the current trigger in [SCHEMA.md](SCHEMA.md) is weekly rather than
354
386
  monthly — evaluate service-owned model files plus a thin global cross-service map. Migration must
package/SCHEMA.md CHANGED
@@ -164,9 +164,41 @@ check uses; a member nothing models is omitted, which `landscape.service-unmodel
164
164
  reports). The output is deterministic and line-oriented — subsystems sorted by path, members by id,
165
165
  one `include` per line, no timestamps, no absolute paths — so it is byte-reproducible on any machine
166
166
  and two concurrent moves into different groups merge in git without intervention.
167
+
168
+ Each view carries the **label a human reads**: `title`, and `description` when the marker authors
169
+ one. The title is the subsystem's **path** — every marked ancestor's label and its own, joined with
170
+ ` / `, so `services/platform/order-flow` reads `Platform / Order flow` — and each label is the
171
+ marker's `title`, falling back to the directory name (an unlabelled view is shown by its id, which
172
+ is hex-escaped for injectivity and reads as `subsystem_platform__order_2dflow`). LikeC4
173
+ reads that path as folders: the view list mirrors the directory tree, showing one folder per
174
+ ancestor and the leaf's own label as the title. A `/` **inside** a label becomes `∕` (U+2215), so an
175
+ authored slash can neither hide half of what somebody wrote nor invent a folder level the tree does
176
+ not have. Both strings are somebody's prose written into syntax, so both are normalized to one line
177
+ and escaped. Titles are not unique — only ids are — and renaming a parent's title rewrites every
178
+ view beneath it.
179
+
180
+ The **boundary** each view draws is a fact about the landscape, never a setting:
181
+
182
+ 1. an element that holds *exactly* the subsystem's members and nothing else is included beside the
183
+ leaves, so the renderer nests them in the box the author drew, under the author's title;
184
+ 2. a landscape that nests nothing puts the services in a `group '<title>'` of loam's own, mirroring
185
+ the directory subtree, so a parent subsystem shows its children as boxes rather than one pile;
186
+ 3. members that ARE nested with no element owning exactly them get no box and one comment line —
187
+ `// model: no boundary — <id> also holds services outside this subsystem.` — because a synthetic
188
+ box would look identical whether or not the tree and the map agree. That comment is the whole of
189
+ what loam says about the two disagreeing: nothing grades it, nothing fails on it, and it lands in
190
+ the diff of the commit that caused it. **The tree and any `group` nesting in the landscape stay
191
+ independent**; `loam subsystem move` never rewrites the map, and no finding compares them.
192
+
193
+ Because the boundary depends on which *other* services sit under an element, an unrelated adoption
194
+ or archive can flip a view from boxed to flat — a new reason to see `subsystem.views-stale`, and why
195
+ `loam archive` withholds its "complete + current" line and names `loam subsystem sync` when it left
196
+ the file behind.
197
+
167
198
  `loam subsystem sync` is the one regenerator (it also *removes* the file when the tree has no
168
199
  subsystems), every `loam subsystem` write verb regenerates it inside the same transaction, and
169
- `validate --all` grades staleness by **byte comparison** — exactly one error,
200
+ `validate --all` grades staleness by **content comparison** (line endings normalized; a CRLF
201
+ checkout changes no fact) — exactly one error,
170
202
  `subsystem.views-stale`, on exactly that file; nothing in loam ever parses it: a views-only document
171
203
  does not parse standalone (its `include` lines reference elements the landscape defines), and it
172
204
  holds **static** views, whose contents loam never reads in any document. Only the LikeC4 renderer,
@@ -15,6 +15,10 @@ import { message, quietPruneEmptyParents, quietRm, rollbackError, rollbackStaged
15
15
  import { clearCommitIntent, writeCommitIntent } from "../../core/staging/recovery/intent.js";
16
16
  import { SNAPSHOT_DIR, SnapshotClobberError, snapshotDir, writeSnapshot, } from "../../core/staging/snapshot.js";
17
17
  import { enumeratedServices } from "../../core/repo/service-target.js";
18
+ import { listFleetTree } from "../../core/repo/repo.js";
19
+ import { subsystemViewsPath } from "../../core/repo/paths.js";
20
+ import { viewsState } from "../../core/repo/tree/render/stale.js";
21
+ import { loadArchitecture } from "../../core/c4/project/architecture.js";
18
22
  import { FleetContext } from "../../core/fleet-context.js";
19
23
  import { gate } from "./plan/gate.js";
20
24
  import {} from "./plan/refusal.js";
@@ -227,8 +231,19 @@ export async function archiveLocked(config, featureId, opts) {
227
231
  : "";
228
232
  throw new ArchiveFailure(failures.length > 0 ? "rollback-incomplete" : "merge-failed", wrapped.message + kept);
229
233
  }
234
+ // This merge just changed the map, and the generated views file is a
235
+ // function of the tree AND the map: a delta whose element lands inside a
236
+ // group that exactly covered a subsystem takes that subsystem's boundary
237
+ // line away, and a delta binding an element to a filed service adds an
238
+ // include. Archive does not rewrite the file — `loam subsystem sync` owns it
239
+ // outright, and folding a second generated artifact into this transaction
240
+ // would mean archive computing the post-merge tree AND the post-merge map,
241
+ // which is how the two writers disagree — but it must not report a repo as
242
+ // current when the next `validate --all` will call it stale. Graded through
243
+ // validate's own function, so the two cannot answer differently.
244
+ const staleViews = await subsystemViewsStale(config.docsDir);
230
245
  if (json) {
231
- emitJson(payload(true));
246
+ emitJson({ ...payload(true), ...(staleViews ? { subsystemViewsStale: true } : {}) });
232
247
  return;
233
248
  }
234
249
  console.log(`\n archived: features/${dirName} → features/archive/${dirName}`);
@@ -239,14 +254,43 @@ export async function archiveLocked(config, featureId, opts) {
239
254
  // caller told it to merge past. Printing it over either is how a red fleet
240
255
  // gets reported as a finished one.
241
256
  const incomplete = planWarns.filter((w) => w.code === "service.no-model");
242
- if (incomplete.length === 0 && overridden.length === 0) {
257
+ if (incomplete.length === 0 && overridden.length === 0 && !staleViews) {
243
258
  console.log(" living spec + landscape are now complete + current.");
244
259
  return;
245
260
  }
261
+ if (staleViews) {
262
+ console.log(" ⚠ architecture/subsystems.likec4 no longer matches the tree and this map — run `loam subsystem sync`.");
263
+ }
246
264
  for (const w of incomplete)
247
265
  console.log(` ⚠ ${w.message}`);
248
266
  if (overridden.length > 0) {
249
267
  console.log(` ⚠ merged past ${overridden.length} gating issue(s) with --approve — the living docs carry them now; \`loam validate --all\` says what they cost.`);
250
268
  }
251
269
  }
270
+ /**
271
+ * Would the next `validate --all` report `subsystem.views-stale`?
272
+ *
273
+ * Asked through validate's OWN function rather than a second compare, so the
274
+ * warning archive prints and the error validate raises can never disagree —
275
+ * including about the gate validate holds: a map that does not parse is
276
+ * `landscape.invalid`'s business, and grading a generated file against a map
277
+ * nobody can read would cascade a second complaint behind the first. An
278
+ * unreadable map therefore answers "no claim", exactly as validate does.
279
+ */
280
+ async function subsystemViewsStale(docsDir) {
281
+ try {
282
+ const doc = await loadArchitecture(docsDir);
283
+ if (doc.errors.length > 0)
284
+ return false;
285
+ const state = await viewsState(subsystemViewsPath(docsDir), await listFleetTree(docsDir), doc.elements);
286
+ return !state.agrees;
287
+ }
288
+ catch {
289
+ // Tolerant for the reason the snapshot resolver above is: a docs repo with
290
+ // no `services/` at all still archives a landscape-only feature, and this
291
+ // question is an ADVISORY printed after a commit that already succeeded.
292
+ // Nothing here may turn a landed archive into a non-zero exit.
293
+ return false;
294
+ }
295
+ }
252
296
  //# sourceMappingURL=run.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"run.js","sourceRoot":"","sources":["../../../src/commands/archive/run.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * The archive itself, under the docs lock: gate, plan, commit.\n *\n * The order is the command's whole safety property. Every refusal happens before\n * a byte is planned (`./plan/gate.ts`), the plan is computed entirely in memory\n * so a failure on any axis leaves the living docs untouched (`./plan/specs.ts`,\n * `./plan/contracts/`, `./plan/landscape.ts`), and only a plan that succeeded\n * on every axis is staged\n * and swapped. `--dry-run` returns between the second and the third.\n */\nimport { existsSync } from \"node:fs\";\nimport { mkdir, rename } from \"node:fs/promises\";\nimport { emitJson, repoPath, sayExplain } from \"../../core/envelope/json.js\";\nimport {\n message,\n quietPruneEmptyParents,\n quietRm,\n rollbackError,\n rollbackStaged,\n stageWrites,\n swapStaged,\n} from \"../../core/staging/commit.js\";\nimport { clearCommitIntent, writeCommitIntent } from \"../../core/staging/recovery/intent.js\";\nimport {\n SNAPSHOT_DIR,\n SnapshotClobberError,\n snapshotDir,\n writeSnapshot,\n type ServiceKey,\n} from \"../../core/staging/snapshot.js\";\nimport { enumeratedServices } from \"../../core/repo/service-target.js\";\nimport { FleetContext } from \"../../core/fleet-context.js\";\nimport { gate } from \"./plan/gate.js\";\nimport { type ArchiveOptions } from \"./plan/refusal.js\";\nimport { planCapabilities, planGlossary, planSpecs } from \"./plan/specs.js\";\nimport { planOpenapiContracts } from \"./plan/contracts/openapi.js\";\nimport { planAsyncapiContracts } from \"./plan/contracts/asyncapi.js\";\nimport { planDeployments, planFlows, planLandscape } from \"./plan/landscape.js\";\nimport { emptyPlan } from \"./plan/state.js\";\nimport { issueJson, refuseJson } from \"./plan/refusal.js\";\nimport { ArchiveFailure } from \"./plan/refusal.js\";\nimport { printPlan } from \"./plan/refusal.js\";\nimport type { DocsDir } from \"../../core/kernel/ids/dirs.js\";\n\nexport async function archiveLocked(\n config: { docsDir: DocsDir },\n featureId: string,\n opts: ArchiveOptions,\n): Promise<void> {\n const dryRun = opts.dryRun === true;\n const json = opts.json === true;\n // All prose goes through here so `--json` keeps stdout a single JSON document.\n const say = (line = \"\"): void => {\n if (!json) console.log(line);\n };\n\n // One read index for the whole run, threaded through every planner's\n // config record: gate, the spec merge and both contract merges each hold\n // per-service loops that resolve living paths through the enumeration, and\n // without a shared context every one of those calls re-walks the fleet —\n // O(services²) work under a lock that guarantees the tree is not moving.\n // The snapshot resolver below reads through the same context, so it pays\n // for that enumeration too.\n const read = { docsDir: config.docsDir, fleet: new FleetContext() };\n const gated = await gate(read, featureId, opts, say);\n if (gated === null) return;\n const { id, dirName, featureDir, gating, advisory, archiveDir, archiveDest, recovered, issues } = gated;\n\n say(`archive ${id}${dryRun ? \" (dry run)\" : \"\"}\\n`);\n\n // PLAN — compute every merge in memory. Nothing is written until the whole plan\n // succeeds, so a failure on any axis leaves the living docs untouched.\n const planned = emptyPlan();\n await planSpecs(read, gated, planned, say);\n // The business corpus, beside the service one and before the contracts: both\n // are requirement merges, and reading them together is how a plan stays\n // legible as \"what this feature promises\" followed by \"how it is built\".\n await planCapabilities(read, gated, planned, say);\n // The vocabulary the change brings with it, beside the promises it makes:\n // a create-only whole-file copy, refused at the gate if the word already\n // lives (`core/glossary/delta.ts`).\n await planGlossary(read, gated, planned, say);\n await planOpenapiContracts(read, gated, planned, say);\n await planAsyncapiContracts(read, gated, planned, say);\n await planLandscape(config, gated, planned, say);\n // The flows this feature brings, after the elements they draw hops over: the\n // copy is create-only and refused at the gate if the living tree already holds\n // one (`core/usecases/delta/flows.ts`), and it is ordered after the landscape\n // merge so a reader of the plan sees the map change before the sequence drawn\n // on it.\n await planFlows(config, gated, planned, say);\n await planDeployments(config, gated, planned, say);\n const { writes, planWarns, planGates, openapiRemovals, asyncapiRemovals } = planned;\n\n // Gate on what only the plan could see: a merged operation pointing at a\n // component that exists nowhere, a removal marker addressing no operation.\n // Same doctrine as the coherence gate — a judgment call --approve overrides\n // (unlike the mechanical merge-failed refusals), and a dry run is gated too.\n // Checked after the whole plan so the refusal costs nothing: no write has\n // happened yet either way.\n if (planGates.length > 0 && !opts.approve) {\n const msg = `archive ${id} — BLOCKED: ${planGates.length} issue(s) in the contract merge`;\n if (json) {\n refuseJson(\n \"not-coherent\",\n msg,\n [...issues, ...planWarns, ...planGates],\n `features/${dirName}`,\n );\n return;\n }\n console.error(`${msg}:`);\n for (const i of planGates) console.error(` ✗ ${i.message}`);\n console.error(`\\nFix them in the feature's openapi.yaml / asyncapi.yaml — or re-run with --approve to merge anyway.`);\n sayExplain(\"not-coherent\");\n process.exitCode = 1;\n return;\n }\n if (planGates.length > 0) {\n say(`\\n ⚠ archiving despite ${planGates.length} contract merge issue(s) (--approve):`);\n for (const i of planGates) say(` ✗ ${i.message}`);\n }\n\n // The plan as data, verb decided now — after the commit everything would read\n // as an update. The `--json` payload is identical for a dry run and the real\n // thing except for `archived`; what WOULD happen is what DOES happen.\n const warnings = [...advisory, ...planWarns];\n const overridden = opts.approve === true ? [...gating, ...planGates] : [];\n const plan: Array<Record<string, unknown>> = writes.map((w) => ({\n path: repoPath(config.docsDir, w.path),\n action: existsSync(w.path) ? \"update\" : \"create\",\n }));\n plan.push({ path: `features/${dirName}`, action: \"move\", to: `features/archive/${dirName}` });\n const payload = (archived: boolean): Record<string, unknown> => ({\n command: \"archive\",\n feature: id,\n archived,\n path: repoPath(config.docsDir, archiveDest),\n plan,\n warnings: warnings.map((issue) => issueJson(issue, repoPath(config.docsDir, archiveDest))),\n overridden: overridden.map((issue) => issueJson(issue, repoPath(config.docsDir, archiveDest))),\n openapiRemovals,\n asyncapiRemovals,\n // Present only when this run found an interrupted commit and dealt with it:\n // an agent that sees the docs change under it deserves to be told why, and\n // the absent field is the ordinary case.\n ...(recovered === null ? {} : { recovered }),\n });\n\n if (dryRun) {\n if (json) emitJson(payload(false));\n else printPlan(config.docsDir, writes, dirName);\n return;\n }\n\n // COMMIT — the whole plan computed cleanly. Stage every new version beside its\n // target, snapshot what is about to be overwritten, then swap them into place.\n // Staging touches the filesystem — a read-only `architecture/`, a full disk,\n // a target whose pre-image cannot be read. None of that is a bug in loam, and\n // reporting `internal` sends the reader looking for one; it is the same\n // answer as any other merge that could not be computed: nothing was written.\n let staged;\n try {\n staged = await stageWrites(writes);\n } catch (err) {\n throw new ArchiveFailure(\"merge-failed\", `${message(err)} — nothing was written`);\n }\n let snapshot = false;\n let createdArchiveDir: string | undefined;\n try {\n // The snapshot's resolver pair: how a docs-relative path decomposes into a\n // service identity, and where that identity lives NOW. Built here — the\n // command layer's view of the tree — and injected, so the snapshot module\n // never learns the repo layout. The seam the flat wave left is now\n // threaded: both answers come from the enumeration, so a service filed\n // into a subsystem keys its snapshot rows by the directory it actually\n // lives in, and the clobber guard grades a leftover claim at that same\n // address. The root-level parse stays as the fallback for the one path\n // shape the enumeration cannot know yet — a service this archive CREATES,\n // which always materialises unfiled at services/<id>/.\n // Tolerant on purpose: a docs repo whose services/ is absent still\n // archives a landscape-only feature exactly as before — the enumeration\n // is empty, every row keys by the flat fallback, and the missing\n // directory stays doctor's diagnosis rather than a merge-failed here.\n const dirOf = new Map(\n (await enumeratedServices(config.docsDir, read.fleet)).map((s) => [s.id as string, repoPath(config.docsDir, s.dir)]),\n );\n const serviceKeyOf = (rel: string): ServiceKey | null => {\n for (const [service, dir] of dirOf) {\n if (rel.startsWith(`${dir}/`)) return { service, artifact: rel.slice(dir.length + 1) };\n }\n const m = /^services\\/([^/]+)\\/(.+)$/.exec(rel);\n return m ? { service: m[1]!, artifact: m[2]! } : null;\n };\n const serviceDirOf = (service: string): string | null => dirOf.get(service) ?? null;\n await writeSnapshot({\n featureDir,\n docsDir: config.docsDir,\n feature: { featureId: id, dirName },\n staged,\n serviceKeyOf,\n serviceDirOf,\n });\n snapshot = true;\n // The journal, fsynced, BEFORE the first rename: swapStaged is N renames and\n // only each one of them is atomic, so a kill between two used to leave a\n // half-merged repo that nothing could name — `doctor` and `status` called it\n // healthy and `validate --all` blamed the delta. Written from `staged`, so\n // it records the same digests the swaps are about to produce.\n await writeCommitIntent(\n config.docsDir,\n { command: \"archive\", restore: \"before\", feature: id, moveFrom: featureDir, moveTo: archiveDest },\n staged,\n );\n await swapStaged(staged);\n createdArchiveDir = await mkdir(archiveDir, { recursive: true });\n await rename(featureDir, archiveDest);\n // Last: while it exists, the commit is in flight.\n await clearCommitIntent(config.docsDir);\n } catch (err) {\n if (err instanceof SnapshotClobberError) {\n // Nothing swapped — the refusal happens before the journal is even\n // written — so this only takes the temp files away. Its own code, because\n // the answer is not \"re-run\": a previous archive of this feature is still\n // sitting in the living docs.\n await rollbackStaged(staged);\n throw new ArchiveFailure(\"commit-interrupted\", err.message);\n }\n // Everything this run made, unmade: the swapped files, the snapshot inside the\n // feature that is staying put, and features/archive/ if we are the ones who\n // created it (mkdir reports nothing when it was already there).\n const failures = await rollbackStaged(staged);\n // The rollback decided the outcome, so the journal has nothing left to\n // describe — including on rollback-incomplete, where the files it names are\n // the ones the message tells a human to look at.\n await clearCommitIntent(config.docsDir);\n // The snapshot goes only when the rollback HELD. On rollback-incomplete it\n // holds the only on-disk pre-images of the very files the message tells the\n // reader to repair by hand — a MODIFIED requirement's previous text appears\n // nowhere else. Retaining it is safe: the next archive of this feature reads\n // it before it would replace it, and refuses if the living docs have moved.\n if (snapshot && failures.length === 0) await quietRm(snapshotDir(featureDir));\n // `features/archive/` is shared the moment it exists: another archive can\n // have moved a whole feature into it between our mkdir and this rollback,\n // and a recursive remove of \"the directory we created\" took that feature —\n // snapshot and all — with it, on OUR failure path, in silence. Empty\n // directories only, stopping at the first that is not.\n if (createdArchiveDir !== undefined) await quietPruneEmptyParents(archiveDir, createdArchiveDir);\n // The code is a caller's answer to \"can I trust the repo?\": merge-failed\n // means yes (rolled back), rollback-incomplete means look at it by hand.\n const wrapped = rollbackError(err, failures);\n const kept =\n snapshot && failures.length > 0\n ? ` Pre-images of the overwritten files are kept in features/${dirName}/${SNAPSHOT_DIR}/files/.`\n : \"\";\n throw new ArchiveFailure(failures.length > 0 ? \"rollback-incomplete\" : \"merge-failed\", wrapped.message + kept);\n }\n\n if (json) {\n emitJson(payload(true));\n return;\n }\n console.log(`\\n archived: features/${dirName} → features/archive/${dirName}`);\n console.log(` snapshot: features/archive/${dirName}/${SNAPSHOT_DIR}/ — \\`loam unarchive ${id}\\` puts it back`);\n // The closing line is a claim about the whole docs repo, and it is the line a\n // reader stops at. It may be printed only when this archive left nothing the\n // next `validate --all` will fail on — a service with no model, or a gate the\n // caller told it to merge past. Printing it over either is how a red fleet\n // gets reported as a finished one.\n const incomplete = planWarns.filter((w) => w.code === \"service.no-model\");\n if (incomplete.length === 0 && overridden.length === 0) {\n console.log(\" living spec + landscape are now complete + current.\");\n return;\n }\n for (const w of incomplete) console.log(` ⚠ ${w.message}`);\n if (overridden.length > 0) {\n console.log(\n ` ⚠ merged past ${overridden.length} gating issue(s) with --approve — the living docs carry them now; \\`loam validate --all\\` says what they cost.`,\n );\n }\n}\n"]}
1
+ {"version":3,"file":"run.js","sourceRoot":"","sources":["../../../src/commands/archive/run.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * The archive itself, under the docs lock: gate, plan, commit.\n *\n * The order is the command's whole safety property. Every refusal happens before\n * a byte is planned (`./plan/gate.ts`), the plan is computed entirely in memory\n * so a failure on any axis leaves the living docs untouched (`./plan/specs.ts`,\n * `./plan/contracts/`, `./plan/landscape.ts`), and only a plan that succeeded\n * on every axis is staged\n * and swapped. `--dry-run` returns between the second and the third.\n */\nimport { existsSync } from \"node:fs\";\nimport { mkdir, rename } from \"node:fs/promises\";\nimport { emitJson, repoPath, sayExplain } from \"../../core/envelope/json.js\";\nimport {\n message,\n quietPruneEmptyParents,\n quietRm,\n rollbackError,\n rollbackStaged,\n stageWrites,\n swapStaged,\n} from \"../../core/staging/commit.js\";\nimport { clearCommitIntent, writeCommitIntent } from \"../../core/staging/recovery/intent.js\";\nimport {\n SNAPSHOT_DIR,\n SnapshotClobberError,\n snapshotDir,\n writeSnapshot,\n type ServiceKey,\n} from \"../../core/staging/snapshot.js\";\nimport { enumeratedServices } from \"../../core/repo/service-target.js\";\nimport { listFleetTree } from \"../../core/repo/repo.js\";\nimport { subsystemViewsPath } from \"../../core/repo/paths.js\";\nimport { viewsState } from \"../../core/repo/tree/render/stale.js\";\nimport { loadArchitecture } from \"../../core/c4/project/architecture.js\";\nimport { FleetContext } from \"../../core/fleet-context.js\";\nimport { gate } from \"./plan/gate.js\";\nimport { type ArchiveOptions } from \"./plan/refusal.js\";\nimport { planCapabilities, planGlossary, planSpecs } from \"./plan/specs.js\";\nimport { planOpenapiContracts } from \"./plan/contracts/openapi.js\";\nimport { planAsyncapiContracts } from \"./plan/contracts/asyncapi.js\";\nimport { planDeployments, planFlows, planLandscape } from \"./plan/landscape.js\";\nimport { emptyPlan } from \"./plan/state.js\";\nimport { issueJson, refuseJson } from \"./plan/refusal.js\";\nimport { ArchiveFailure } from \"./plan/refusal.js\";\nimport { printPlan } from \"./plan/refusal.js\";\nimport type { DocsDir } from \"../../core/kernel/ids/dirs.js\";\n\nexport async function archiveLocked(\n config: { docsDir: DocsDir },\n featureId: string,\n opts: ArchiveOptions,\n): Promise<void> {\n const dryRun = opts.dryRun === true;\n const json = opts.json === true;\n // All prose goes through here so `--json` keeps stdout a single JSON document.\n const say = (line = \"\"): void => {\n if (!json) console.log(line);\n };\n\n // One read index for the whole run, threaded through every planner's\n // config record: gate, the spec merge and both contract merges each hold\n // per-service loops that resolve living paths through the enumeration, and\n // without a shared context every one of those calls re-walks the fleet —\n // O(services²) work under a lock that guarantees the tree is not moving.\n // The snapshot resolver below reads through the same context, so it pays\n // for that enumeration too.\n const read = { docsDir: config.docsDir, fleet: new FleetContext() };\n const gated = await gate(read, featureId, opts, say);\n if (gated === null) return;\n const { id, dirName, featureDir, gating, advisory, archiveDir, archiveDest, recovered, issues } = gated;\n\n say(`archive ${id}${dryRun ? \" (dry run)\" : \"\"}\\n`);\n\n // PLAN — compute every merge in memory. Nothing is written until the whole plan\n // succeeds, so a failure on any axis leaves the living docs untouched.\n const planned = emptyPlan();\n await planSpecs(read, gated, planned, say);\n // The business corpus, beside the service one and before the contracts: both\n // are requirement merges, and reading them together is how a plan stays\n // legible as \"what this feature promises\" followed by \"how it is built\".\n await planCapabilities(read, gated, planned, say);\n // The vocabulary the change brings with it, beside the promises it makes:\n // a create-only whole-file copy, refused at the gate if the word already\n // lives (`core/glossary/delta.ts`).\n await planGlossary(read, gated, planned, say);\n await planOpenapiContracts(read, gated, planned, say);\n await planAsyncapiContracts(read, gated, planned, say);\n await planLandscape(config, gated, planned, say);\n // The flows this feature brings, after the elements they draw hops over: the\n // copy is create-only and refused at the gate if the living tree already holds\n // one (`core/usecases/delta/flows.ts`), and it is ordered after the landscape\n // merge so a reader of the plan sees the map change before the sequence drawn\n // on it.\n await planFlows(config, gated, planned, say);\n await planDeployments(config, gated, planned, say);\n const { writes, planWarns, planGates, openapiRemovals, asyncapiRemovals } = planned;\n\n // Gate on what only the plan could see: a merged operation pointing at a\n // component that exists nowhere, a removal marker addressing no operation.\n // Same doctrine as the coherence gate — a judgment call --approve overrides\n // (unlike the mechanical merge-failed refusals), and a dry run is gated too.\n // Checked after the whole plan so the refusal costs nothing: no write has\n // happened yet either way.\n if (planGates.length > 0 && !opts.approve) {\n const msg = `archive ${id} — BLOCKED: ${planGates.length} issue(s) in the contract merge`;\n if (json) {\n refuseJson(\n \"not-coherent\",\n msg,\n [...issues, ...planWarns, ...planGates],\n `features/${dirName}`,\n );\n return;\n }\n console.error(`${msg}:`);\n for (const i of planGates) console.error(` ✗ ${i.message}`);\n console.error(`\\nFix them in the feature's openapi.yaml / asyncapi.yaml — or re-run with --approve to merge anyway.`);\n sayExplain(\"not-coherent\");\n process.exitCode = 1;\n return;\n }\n if (planGates.length > 0) {\n say(`\\n ⚠ archiving despite ${planGates.length} contract merge issue(s) (--approve):`);\n for (const i of planGates) say(` ✗ ${i.message}`);\n }\n\n // The plan as data, verb decided now — after the commit everything would read\n // as an update. The `--json` payload is identical for a dry run and the real\n // thing except for `archived`; what WOULD happen is what DOES happen.\n const warnings = [...advisory, ...planWarns];\n const overridden = opts.approve === true ? [...gating, ...planGates] : [];\n const plan: Array<Record<string, unknown>> = writes.map((w) => ({\n path: repoPath(config.docsDir, w.path),\n action: existsSync(w.path) ? \"update\" : \"create\",\n }));\n plan.push({ path: `features/${dirName}`, action: \"move\", to: `features/archive/${dirName}` });\n const payload = (archived: boolean): Record<string, unknown> => ({\n command: \"archive\",\n feature: id,\n archived,\n path: repoPath(config.docsDir, archiveDest),\n plan,\n warnings: warnings.map((issue) => issueJson(issue, repoPath(config.docsDir, archiveDest))),\n overridden: overridden.map((issue) => issueJson(issue, repoPath(config.docsDir, archiveDest))),\n openapiRemovals,\n asyncapiRemovals,\n // Present only when this run found an interrupted commit and dealt with it:\n // an agent that sees the docs change under it deserves to be told why, and\n // the absent field is the ordinary case.\n ...(recovered === null ? {} : { recovered }),\n });\n\n if (dryRun) {\n if (json) emitJson(payload(false));\n else printPlan(config.docsDir, writes, dirName);\n return;\n }\n\n // COMMIT — the whole plan computed cleanly. Stage every new version beside its\n // target, snapshot what is about to be overwritten, then swap them into place.\n // Staging touches the filesystem — a read-only `architecture/`, a full disk,\n // a target whose pre-image cannot be read. None of that is a bug in loam, and\n // reporting `internal` sends the reader looking for one; it is the same\n // answer as any other merge that could not be computed: nothing was written.\n let staged;\n try {\n staged = await stageWrites(writes);\n } catch (err) {\n throw new ArchiveFailure(\"merge-failed\", `${message(err)} — nothing was written`);\n }\n let snapshot = false;\n let createdArchiveDir: string | undefined;\n try {\n // The snapshot's resolver pair: how a docs-relative path decomposes into a\n // service identity, and where that identity lives NOW. Built here — the\n // command layer's view of the tree — and injected, so the snapshot module\n // never learns the repo layout. The seam the flat wave left is now\n // threaded: both answers come from the enumeration, so a service filed\n // into a subsystem keys its snapshot rows by the directory it actually\n // lives in, and the clobber guard grades a leftover claim at that same\n // address. The root-level parse stays as the fallback for the one path\n // shape the enumeration cannot know yet — a service this archive CREATES,\n // which always materialises unfiled at services/<id>/.\n // Tolerant on purpose: a docs repo whose services/ is absent still\n // archives a landscape-only feature exactly as before — the enumeration\n // is empty, every row keys by the flat fallback, and the missing\n // directory stays doctor's diagnosis rather than a merge-failed here.\n const dirOf = new Map(\n (await enumeratedServices(config.docsDir, read.fleet)).map((s) => [s.id as string, repoPath(config.docsDir, s.dir)]),\n );\n const serviceKeyOf = (rel: string): ServiceKey | null => {\n for (const [service, dir] of dirOf) {\n if (rel.startsWith(`${dir}/`)) return { service, artifact: rel.slice(dir.length + 1) };\n }\n const m = /^services\\/([^/]+)\\/(.+)$/.exec(rel);\n return m ? { service: m[1]!, artifact: m[2]! } : null;\n };\n const serviceDirOf = (service: string): string | null => dirOf.get(service) ?? null;\n await writeSnapshot({\n featureDir,\n docsDir: config.docsDir,\n feature: { featureId: id, dirName },\n staged,\n serviceKeyOf,\n serviceDirOf,\n });\n snapshot = true;\n // The journal, fsynced, BEFORE the first rename: swapStaged is N renames and\n // only each one of them is atomic, so a kill between two used to leave a\n // half-merged repo that nothing could name — `doctor` and `status` called it\n // healthy and `validate --all` blamed the delta. Written from `staged`, so\n // it records the same digests the swaps are about to produce.\n await writeCommitIntent(\n config.docsDir,\n { command: \"archive\", restore: \"before\", feature: id, moveFrom: featureDir, moveTo: archiveDest },\n staged,\n );\n await swapStaged(staged);\n createdArchiveDir = await mkdir(archiveDir, { recursive: true });\n await rename(featureDir, archiveDest);\n // Last: while it exists, the commit is in flight.\n await clearCommitIntent(config.docsDir);\n } catch (err) {\n if (err instanceof SnapshotClobberError) {\n // Nothing swapped — the refusal happens before the journal is even\n // written — so this only takes the temp files away. Its own code, because\n // the answer is not \"re-run\": a previous archive of this feature is still\n // sitting in the living docs.\n await rollbackStaged(staged);\n throw new ArchiveFailure(\"commit-interrupted\", err.message);\n }\n // Everything this run made, unmade: the swapped files, the snapshot inside the\n // feature that is staying put, and features/archive/ if we are the ones who\n // created it (mkdir reports nothing when it was already there).\n const failures = await rollbackStaged(staged);\n // The rollback decided the outcome, so the journal has nothing left to\n // describe — including on rollback-incomplete, where the files it names are\n // the ones the message tells a human to look at.\n await clearCommitIntent(config.docsDir);\n // The snapshot goes only when the rollback HELD. On rollback-incomplete it\n // holds the only on-disk pre-images of the very files the message tells the\n // reader to repair by hand — a MODIFIED requirement's previous text appears\n // nowhere else. Retaining it is safe: the next archive of this feature reads\n // it before it would replace it, and refuses if the living docs have moved.\n if (snapshot && failures.length === 0) await quietRm(snapshotDir(featureDir));\n // `features/archive/` is shared the moment it exists: another archive can\n // have moved a whole feature into it between our mkdir and this rollback,\n // and a recursive remove of \"the directory we created\" took that feature —\n // snapshot and all — with it, on OUR failure path, in silence. Empty\n // directories only, stopping at the first that is not.\n if (createdArchiveDir !== undefined) await quietPruneEmptyParents(archiveDir, createdArchiveDir);\n // The code is a caller's answer to \"can I trust the repo?\": merge-failed\n // means yes (rolled back), rollback-incomplete means look at it by hand.\n const wrapped = rollbackError(err, failures);\n const kept =\n snapshot && failures.length > 0\n ? ` Pre-images of the overwritten files are kept in features/${dirName}/${SNAPSHOT_DIR}/files/.`\n : \"\";\n throw new ArchiveFailure(failures.length > 0 ? \"rollback-incomplete\" : \"merge-failed\", wrapped.message + kept);\n }\n\n // This merge just changed the map, and the generated views file is a\n // function of the tree AND the map: a delta whose element lands inside a\n // group that exactly covered a subsystem takes that subsystem's boundary\n // line away, and a delta binding an element to a filed service adds an\n // include. Archive does not rewrite the file — `loam subsystem sync` owns it\n // outright, and folding a second generated artifact into this transaction\n // would mean archive computing the post-merge tree AND the post-merge map,\n // which is how the two writers disagree — but it must not report a repo as\n // current when the next `validate --all` will call it stale. Graded through\n // validate's own function, so the two cannot answer differently.\n const staleViews = await subsystemViewsStale(config.docsDir);\n\n if (json) {\n emitJson({ ...payload(true), ...(staleViews ? { subsystemViewsStale: true } : {}) });\n return;\n }\n console.log(`\\n archived: features/${dirName} → features/archive/${dirName}`);\n console.log(` snapshot: features/archive/${dirName}/${SNAPSHOT_DIR}/ — \\`loam unarchive ${id}\\` puts it back`);\n // The closing line is a claim about the whole docs repo, and it is the line a\n // reader stops at. It may be printed only when this archive left nothing the\n // next `validate --all` will fail on — a service with no model, or a gate the\n // caller told it to merge past. Printing it over either is how a red fleet\n // gets reported as a finished one.\n const incomplete = planWarns.filter((w) => w.code === \"service.no-model\");\n if (incomplete.length === 0 && overridden.length === 0 && !staleViews) {\n console.log(\" living spec + landscape are now complete + current.\");\n return;\n }\n if (staleViews) {\n console.log(\n \" ⚠ architecture/subsystems.likec4 no longer matches the tree and this map — run `loam subsystem sync`.\",\n );\n }\n for (const w of incomplete) console.log(` ⚠ ${w.message}`);\n if (overridden.length > 0) {\n console.log(\n ` ⚠ merged past ${overridden.length} gating issue(s) with --approve — the living docs carry them now; \\`loam validate --all\\` says what they cost.`,\n );\n }\n}\n\n/**\n * Would the next `validate --all` report `subsystem.views-stale`?\n *\n * Asked through validate's OWN function rather than a second compare, so the\n * warning archive prints and the error validate raises can never disagree —\n * including about the gate validate holds: a map that does not parse is\n * `landscape.invalid`'s business, and grading a generated file against a map\n * nobody can read would cascade a second complaint behind the first. An\n * unreadable map therefore answers \"no claim\", exactly as validate does.\n */\nasync function subsystemViewsStale(docsDir: DocsDir): Promise<boolean> {\n try {\n const doc = await loadArchitecture(docsDir);\n if (doc.errors.length > 0) return false;\n const state = await viewsState(subsystemViewsPath(docsDir), await listFleetTree(docsDir), doc.elements);\n return !state.agrees;\n } catch {\n // Tolerant for the reason the snapshot resolver above is: a docs repo with\n // no `services/` at all still archives a landscape-only feature, and this\n // question is an ADVISORY printed after a commit that already succeeded.\n // Nothing here may turn a landed archive into a non-zero exit.\n return false;\n }\n}\n"]}
@@ -13,7 +13,7 @@ import { join } from "node:path";
13
13
  import { serviceDirOf } from "../../core/kernel/ids/dirs.js";
14
14
  import { landscapePath, servicePathsUnder, servicesDir, subsystemPathUnder, subsystemViewsPath, unfiledServicePaths, } from "../../core/repo/paths.js";
15
15
  import { SUBSYSTEM_MARKER } from "../../core/repo/tree/marker.js";
16
- import { renderSubsystemViews, viewsAgree } from "../../core/repo/tree/views.js";
16
+ import { renderSubsystemViews, viewsAgree } from "../../core/repo/tree/render/views.js";
17
17
  import { TEMP_FILE_RE } from "../../core/staging/commit.js";
18
18
  import { planWrite } from "../../core/staging/writes.js";
19
19
  /**
@@ -1 +1 @@
1
- {"version":3,"file":"plan.js","sourceRoot":"","sources":["../../../src/commands/seed/plan.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * What a seed run will write, decided in full before a byte lands. Split from\n * `./commit.ts` on the phase seam: that module owns the WINDOW — the lock, the\n * interrupted-commit recovery, the two refusals and the transaction — while\n * this one answers the single question \"given the tree as it is and the fleet\n * file as it reads, which files change and how\". Nothing here acquires\n * anything, and nothing here writes: it is called under the lock and hands\n * back a plan the transaction executes or rolls back whole.\n */\nimport { existsSync } from \"node:fs\";\nimport { readdir, readFile } from \"node:fs/promises\";\nimport { join } from \"node:path\";\nimport type { Elem } from \"../../core/c4/likec4.js\";\nimport type { FleetSeed } from \"../../core/c4/seed/fleet-file.js\";\nimport { serviceDirOf, type DocsDir } from \"../../core/kernel/ids/dirs.js\";\nimport {\n landscapePath,\n servicePathsUnder,\n servicesDir,\n subsystemPathUnder,\n subsystemViewsPath,\n unfiledServicePaths,\n} from \"../../core/repo/paths.js\";\nimport { SUBSYSTEM_MARKER } from \"../../core/repo/tree/marker.js\";\nimport { renderSubsystemViews, viewsAgree } from \"../../core/repo/tree/views.js\";\nimport type { FleetTree, SubsystemEntry, WalkedService } from \"../../core/repo/tree/walk.js\";\nimport { TEMP_FILE_RE } from \"../../core/staging/commit.js\";\nimport { planWrite, type PlannedWrite } from \"../../core/staging/writes.js\";\n\n/** Everything the plan reads. The commit request extends this and adds only `rerun`. */\nexport interface SeedPlanInput {\n docsDir: DocsDir;\n seed: FleetSeed;\n /** The sealed landscape text (stamp on line 1), already proven to parse. */\n landscape: string;\n /**\n * That same text's parsed elements — the loadSource self-check's output,\n * reused for the views join so the generated views file agrees byte for byte\n * with what `validate --all` will recompute from the committed landscape.\n */\n elements: Elem[];\n}\n\nexport interface SeedPlan {\n writes: PlannedWrite[];\n /** Service ids split by whether this run creates the directory. */\n services: { created: string[]; existing: string[] };\n}\n\n/**\n * Does this existing service directory hold nothing git would keep? An empty\n * directory is a service to the walk but nothing at all to git, so a landscape\n * declaring it clones into a repo where `validate --all` fails on a service\n * that exists only on the author's disk. Seed's own crash window produces\n * exactly this tree — `stageWrites` creates directories before the journal is\n * written, so a kill in between leaves directories no recovery knows about —\n * and so does a plain `mkdir`. Staging temps do not count as content: they are\n * that same crash's litter, not somebody's file.\n */\nasync function keepsNothing(dir: string): Promise<boolean> {\n try {\n return (await readdir(dir)).every((entry) => TEMP_FILE_RE.test(entry));\n } catch {\n // Unreadable is not empty: the walk found this directory a moment ago, and\n // inventing a write into a directory we cannot list is the wrong guess.\n return false;\n }\n}\n\n/**\n * The complete plan, in memory, before a byte lands: the sealed landscape, one\n * marker per NEW subsystem, one `.gitkeep` per service directory this commit\n * creates — and per existing one holding nothing git would keep (see\n * `keepsNothing`) — and the generated views file rendered from the tree AS\n * THIS COMMIT LEAVES IT, never from a re-walk between two states\n * (commands/subsystem/txn/views.ts's `movedTree` doctrine). That last one is\n * the only write that can be a DELETE: a tree with no subsystems owes no views\n * file, and a leftover is a finding.\n *\n * Existing directories are never moved: fleet.yaml's placement is ignored for\n * a service that already exists — moving is `loam subsystem move <id> --into\n * <name>`'s job, under its own guards — and nothing anywhere is deleted except\n * that one generated file.\n */\nexport async function planSeedWrites(req: SeedPlanInput, tree: FleetTree): Promise<SeedPlan> {\n const { docsDir, seed } = req;\n const writes: PlannedWrite[] = [planWrite(landscapePath(docsDir), req.landscape)];\n\n const existingSubs = new Map(tree.subsystems.map((s) => [s.name, s]));\n const newSubs: SubsystemEntry[] = [];\n for (const sub of seed.subsystems) {\n if (existingSubs.has(sub as string)) continue;\n const dir = subsystemPathUnder(servicesDir(docsDir), sub);\n // A comment line parses to null — a valid empty marker (tree/marker.ts).\n writes.push(planWrite(join(dir, SUBSYSTEM_MARKER), \"# subsystem marker\\n\"));\n newSubs.push({ name: sub as string, path: [sub as string], dir, meta: {} });\n }\n\n const treeDirs = new Map(tree.services.map((s) => [s.id as string, s.dir]));\n // Every existing directory inspected at once — independent reads, so the\n // loop below stays synchronous.\n const invisible = new Map(\n await Promise.all([...treeDirs].map(async ([id, dir]) => [id, await keepsNothing(dir)] as const)),\n );\n const created: string[] = [];\n const existing: string[] = [];\n const newWalked: WalkedService[] = [];\n for (const s of seed.services) {\n const already = treeDirs.get(s.id as string);\n if (already !== undefined) {\n existing.push(s.id);\n // Existing, but git-invisible: give it the `.gitkeep` a created service\n // gets, or the landscape about to be written declares a directory the\n // next clone will not have. `planWrite` makes this a no-op when the file\n // is already there, so a real service is never touched.\n if (invisible.get(s.id as string) === true) writes.push(planWrite(join(already, \".gitkeep\"), \"\"));\n continue;\n }\n created.push(s.id);\n const parent = s.subsystem === null ? null : existingSubs.get(s.subsystem as string) ?? null;\n const dir =\n s.subsystem === null\n ? unfiledServicePaths(docsDir, s.id).dir\n : parent !== null\n ? servicePathsUnder(parent.dir, s.id).dir\n : // A subsystem this same commit creates: the creation spelling,\n // through the same branded joins the subsystem verbs use.\n serviceDirOf(join(subsystemPathUnder(servicesDir(docsDir), s.subsystem), s.id));\n writes.push(planWrite(join(dir, \".gitkeep\"), \"\"));\n newWalked.push({\n // A ServiceId IS a RawServiceId (the checked subtype), so no re-cast.\n id: s.id,\n dir,\n subsystem: s.subsystem === null ? [] : parent !== null ? parent.path : [s.subsystem as string],\n });\n }\n\n const plannedTree: FleetTree = {\n findings: [],\n services: [...tree.services, ...newWalked],\n subsystems: [...tree.subsystems, ...newSubs],\n };\n // The generated views file, including its ABSENCE. `renderSubsystemViews`\n // returning null means the tree has no subsystems, and its contract is that\n // the file must then not exist — `validate` grades a leftover as\n // `subsystem.views-stale`, so a seed that ignored the file would exit 0 and\n // send the caller, via its own `next` list, into a failing gate over a file\n // it had in hand. `content: null` is the delete the transaction understands\n // (commands/subsystem/txn/txn.ts's `\"removed\"`), and a file that already\n // SAYS this is left out of the journal entirely rather than written back\n // over itself.\n //\n // \"Already says this\" is content, not bytes: `viewsAgree` performs the\n // comparison and records the Windows reason for it, beside the generator\n // that mints the bytes. What gets WRITTEN is unchanged — the LF render, as a\n // Buffer — so this decode is a comparison only, never the string round trip\n // to disk that writes.ts forbids; a views file whose bytes are not UTF-8\n // therefore fails the compare and is regenerated, which is the right answer\n // for it anyway.\n const viewsPath = subsystemViewsPath(docsDir);\n const views = renderSubsystemViews(plannedTree, req.elements);\n const wanted = views === null ? null : Buffer.from(views, \"utf8\");\n const onDisk = existsSync(viewsPath) ? await readFile(viewsPath, \"utf8\") : null;\n if (!viewsAgree(onDisk, views)) writes.push({ path: viewsPath, content: wanted });\n\n return { writes, services: { created: created.sort(), existing: existing.sort() } };\n}\n"]}
1
+ {"version":3,"file":"plan.js","sourceRoot":"","sources":["../../../src/commands/seed/plan.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * What a seed run will write, decided in full before a byte lands. Split from\n * `./commit.ts` on the phase seam: that module owns the WINDOW — the lock, the\n * interrupted-commit recovery, the two refusals and the transaction — while\n * this one answers the single question \"given the tree as it is and the fleet\n * file as it reads, which files change and how\". Nothing here acquires\n * anything, and nothing here writes: it is called under the lock and hands\n * back a plan the transaction executes or rolls back whole.\n */\nimport { existsSync } from \"node:fs\";\nimport { readdir, readFile } from \"node:fs/promises\";\nimport { join } from \"node:path\";\nimport type { Elem } from \"../../core/c4/likec4.js\";\nimport type { FleetSeed } from \"../../core/c4/seed/fleet-file.js\";\nimport { serviceDirOf, type DocsDir } from \"../../core/kernel/ids/dirs.js\";\nimport {\n landscapePath,\n servicePathsUnder,\n servicesDir,\n subsystemPathUnder,\n subsystemViewsPath,\n unfiledServicePaths,\n} from \"../../core/repo/paths.js\";\nimport { SUBSYSTEM_MARKER } from \"../../core/repo/tree/marker.js\";\nimport { renderSubsystemViews, viewsAgree } from \"../../core/repo/tree/render/views.js\";\nimport type { FleetTree, SubsystemEntry, WalkedService } from \"../../core/repo/tree/walk.js\";\nimport { TEMP_FILE_RE } from \"../../core/staging/commit.js\";\nimport { planWrite, type PlannedWrite } from \"../../core/staging/writes.js\";\n\n/** Everything the plan reads. The commit request extends this and adds only `rerun`. */\nexport interface SeedPlanInput {\n docsDir: DocsDir;\n seed: FleetSeed;\n /** The sealed landscape text (stamp on line 1), already proven to parse. */\n landscape: string;\n /**\n * That same text's parsed elements — the loadSource self-check's output,\n * reused for the views join so the generated views file agrees byte for byte\n * with what `validate --all` will recompute from the committed landscape.\n */\n elements: Elem[];\n}\n\nexport interface SeedPlan {\n writes: PlannedWrite[];\n /** Service ids split by whether this run creates the directory. */\n services: { created: string[]; existing: string[] };\n}\n\n/**\n * Does this existing service directory hold nothing git would keep? An empty\n * directory is a service to the walk but nothing at all to git, so a landscape\n * declaring it clones into a repo where `validate --all` fails on a service\n * that exists only on the author's disk. Seed's own crash window produces\n * exactly this tree — `stageWrites` creates directories before the journal is\n * written, so a kill in between leaves directories no recovery knows about —\n * and so does a plain `mkdir`. Staging temps do not count as content: they are\n * that same crash's litter, not somebody's file.\n */\nasync function keepsNothing(dir: string): Promise<boolean> {\n try {\n return (await readdir(dir)).every((entry) => TEMP_FILE_RE.test(entry));\n } catch {\n // Unreadable is not empty: the walk found this directory a moment ago, and\n // inventing a write into a directory we cannot list is the wrong guess.\n return false;\n }\n}\n\n/**\n * The complete plan, in memory, before a byte lands: the sealed landscape, one\n * marker per NEW subsystem, one `.gitkeep` per service directory this commit\n * creates — and per existing one holding nothing git would keep (see\n * `keepsNothing`) — and the generated views file rendered from the tree AS\n * THIS COMMIT LEAVES IT, never from a re-walk between two states\n * (commands/subsystem/txn/views.ts's `movedTree` doctrine). That last one is\n * the only write that can be a DELETE: a tree with no subsystems owes no views\n * file, and a leftover is a finding.\n *\n * Existing directories are never moved: fleet.yaml's placement is ignored for\n * a service that already exists — moving is `loam subsystem move <id> --into\n * <name>`'s job, under its own guards — and nothing anywhere is deleted except\n * that one generated file.\n */\nexport async function planSeedWrites(req: SeedPlanInput, tree: FleetTree): Promise<SeedPlan> {\n const { docsDir, seed } = req;\n const writes: PlannedWrite[] = [planWrite(landscapePath(docsDir), req.landscape)];\n\n const existingSubs = new Map(tree.subsystems.map((s) => [s.name, s]));\n const newSubs: SubsystemEntry[] = [];\n for (const sub of seed.subsystems) {\n if (existingSubs.has(sub as string)) continue;\n const dir = subsystemPathUnder(servicesDir(docsDir), sub);\n // A comment line parses to null — a valid empty marker (tree/marker.ts).\n writes.push(planWrite(join(dir, SUBSYSTEM_MARKER), \"# subsystem marker\\n\"));\n newSubs.push({ name: sub as string, path: [sub as string], dir, meta: {} });\n }\n\n const treeDirs = new Map(tree.services.map((s) => [s.id as string, s.dir]));\n // Every existing directory inspected at once — independent reads, so the\n // loop below stays synchronous.\n const invisible = new Map(\n await Promise.all([...treeDirs].map(async ([id, dir]) => [id, await keepsNothing(dir)] as const)),\n );\n const created: string[] = [];\n const existing: string[] = [];\n const newWalked: WalkedService[] = [];\n for (const s of seed.services) {\n const already = treeDirs.get(s.id as string);\n if (already !== undefined) {\n existing.push(s.id);\n // Existing, but git-invisible: give it the `.gitkeep` a created service\n // gets, or the landscape about to be written declares a directory the\n // next clone will not have. `planWrite` makes this a no-op when the file\n // is already there, so a real service is never touched.\n if (invisible.get(s.id as string) === true) writes.push(planWrite(join(already, \".gitkeep\"), \"\"));\n continue;\n }\n created.push(s.id);\n const parent = s.subsystem === null ? null : existingSubs.get(s.subsystem as string) ?? null;\n const dir =\n s.subsystem === null\n ? unfiledServicePaths(docsDir, s.id).dir\n : parent !== null\n ? servicePathsUnder(parent.dir, s.id).dir\n : // A subsystem this same commit creates: the creation spelling,\n // through the same branded joins the subsystem verbs use.\n serviceDirOf(join(subsystemPathUnder(servicesDir(docsDir), s.subsystem), s.id));\n writes.push(planWrite(join(dir, \".gitkeep\"), \"\"));\n newWalked.push({\n // A ServiceId IS a RawServiceId (the checked subtype), so no re-cast.\n id: s.id,\n dir,\n subsystem: s.subsystem === null ? [] : parent !== null ? parent.path : [s.subsystem as string],\n });\n }\n\n const plannedTree: FleetTree = {\n findings: [],\n services: [...tree.services, ...newWalked],\n subsystems: [...tree.subsystems, ...newSubs],\n };\n // The generated views file, including its ABSENCE. `renderSubsystemViews`\n // returning null means the tree has no subsystems, and its contract is that\n // the file must then not exist — `validate` grades a leftover as\n // `subsystem.views-stale`, so a seed that ignored the file would exit 0 and\n // send the caller, via its own `next` list, into a failing gate over a file\n // it had in hand. `content: null` is the delete the transaction understands\n // (commands/subsystem/txn/txn.ts's `\"removed\"`), and a file that already\n // SAYS this is left out of the journal entirely rather than written back\n // over itself.\n //\n // \"Already says this\" is content, not bytes: `viewsAgree` performs the\n // comparison and records the Windows reason for it, beside the generator\n // that mints the bytes. What gets WRITTEN is unchanged — the LF render, as a\n // Buffer — so this decode is a comparison only, never the string round trip\n // to disk that writes.ts forbids; a views file whose bytes are not UTF-8\n // therefore fails the compare and is regenerated, which is the right answer\n // for it anyway.\n const viewsPath = subsystemViewsPath(docsDir);\n const views = renderSubsystemViews(plannedTree, req.elements);\n const wanted = views === null ? null : Buffer.from(views, \"utf8\");\n const onDisk = existsSync(viewsPath) ? await readFile(viewsPath, \"utf8\") : null;\n if (!viewsAgree(onDisk, views)) writes.push({ path: viewsPath, content: wanted });\n\n return { writes, services: { created: created.sort(), existing: existing.sort() } };\n}\n"]}
@@ -24,7 +24,7 @@ import { InterruptedCommitError } from "../../core/staging/interrupted.js";
24
24
  import { acquireDocsLockWaiting, DocsBusyError, LOCK_WAIT_MS } from "../../core/staging/lock.js";
25
25
  import { recoverInterruptedCommit } from "../../core/staging/recovery/recover.js";
26
26
  import { commitStaged } from "../../core/staging/txn/transaction.js";
27
- import { viewsAgree } from "../../core/repo/tree/views.js";
27
+ import { viewsAgree } from "../../core/repo/tree/render/views.js";
28
28
  import { expectedViews } from "./txn/views.js";
29
29
  export async function runSync(docsDir, json) {
30
30
  let releaseLock;
@@ -1 +1 @@
1
- {"version":3,"file":"sync.js","sourceRoot":"","sources":["../../../src/commands/subsystem/sync.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;GAeG;AACH,OAAO,EAAE,UAAU,EAAE,MAAM,SAAS,CAAC;AACrC,OAAO,EAAE,QAAQ,EAAE,MAAM,kBAAkB,CAAC;AAE5C,OAAO,EAAE,QAAQ,EAAE,IAAI,EAAE,MAAM,6BAA6B,CAAC;AAC7D,OAAO,EAAE,kBAAkB,EAAE,MAAM,0BAA0B,CAAC;AAC9D,OAAO,EAAE,aAAa,EAAE,MAAM,yBAAyB,CAAC;AACxD,OAAO,EAAE,WAAW,EAAE,MAAM,8BAA8B,CAAC;AAC3D,OAAO,EAAuB,sBAAsB,EAAE,MAAM,mCAAmC,CAAC;AAChG,OAAO,EAAE,sBAAsB,EAAE,aAAa,EAAE,YAAY,EAAE,MAAM,4BAA4B,CAAC;AACjG,OAAO,EAAE,wBAAwB,EAAE,MAAM,wCAAwC,CAAC;AAClF,OAAO,EAAE,YAAY,EAAE,MAAM,uCAAuC,CAAC;AACrE,OAAO,EAAE,UAAU,EAAE,MAAM,+BAA+B,CAAC;AAC3D,OAAO,EAAE,aAAa,EAAE,MAAM,gBAAgB,CAAC;AAK/C,MAAM,CAAC,KAAK,UAAU,OAAO,CAAC,OAAgB,EAAE,IAAa;IAC3D,IAAI,WAAgC,CAAC;IACrC,IAAI,CAAC;QACH,WAAW,GAAG,MAAM,sBAAsB,CAAC,OAAO,EAAE,YAAY,CAAC,CAAC;IACpE,CAAC;IAAC,OAAO,GAAG,EAAE,CAAC;QACb,IAAI,CAAC,CAAC,GAAG,YAAY,aAAa,CAAC;YAAE,MAAM,GAAG,CAAC;QAC/C,IAAI,CAAC,IAAI,EAAE,WAAW,EAAE,GAAG,CAAC,OAAO,CAAC,CAAC;QACrC,OAAO;IACT,CAAC;IACD,IAAI,CAAC;QACH,uEAAuE;QACvE,2DAA2D;QAC3D,IAAI,SAAgC,CAAC;QACrC,IAAI,CAAC;YACH,SAAS,GAAG,MAAM,wBAAwB,CAAC,OAAO,CAAC,CAAC;QACtD,CAAC;QAAC,OAAO,GAAG,EAAE,CAAC;YACb,IAAI,CAAC,CAAC,GAAG,YAAY,sBAAsB,CAAC;gBAAE,MAAM,GAAG,CAAC;YACxD,IAAI,CAAC,IAAI,EAAE,oBAAoB,EAAE,GAAG,CAAC,OAAO,CAAC,CAAC;YAC9C,OAAO;QACT,CAAC;QAED,MAAM,IAAI,GAAG,MAAM,aAAa,CAAC,OAAO,CAAC,CAAC;QAC1C,MAAM,QAAQ,GAAG,MAAM,aAAa,CAAC,OAAO,EAAE,IAAI,CAAC,CAAC;QACpD,MAAM,IAAI,GAAG,kBAAkB,CAAC,OAAO,CAAC,CAAC;QACzC,MAAM,aAAa,GAAG,QAAQ,KAAK,IAAI,CAAC,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC,MAAM,CAAC,IAAI,CAAC,QAAQ,EAAE,MAAM,CAAC,CAAC;QAC/E,wEAAwE;QACxE,2EAA2E;QAC3E,4EAA4E;QAC5E,yEAAyE;QACzE,yEAAyE;QACzE,wDAAwD;QACxD,MAAM,MAAM,GAAG,UAAU,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC,MAAM,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC,CAAC,CAAC,CAAC,IAAI,CAAC;QACtE,IAAI,UAAU,CAAC,MAAM,EAAE,QAAQ,CAAC,EAAE,CAAC;YACjC,MAAM,CAAC,IAAI,EAAE,EAAE,MAAM,EAAE,SAAS,EAAE,UAAU,EAAE,IAAI,CAAC,UAAU,CAAC,MAAM,EAAE,SAAS,EAAE,CAAC,CAAC;YACnF,OAAO;QACT,CAAC;QACD,MAAM,MAAM,GAAe,MAAM,KAAK,IAAI,CAAC,CAAC,CAAC,SAAS,CAAC,CAAC,CAAC,aAAa,KAAK,IAAI,CAAC,CAAC,CAAC,SAAS,CAAC,CAAC,CAAC,SAAS,CAAC;QACxG,MAAM,MAAM,GAAG,MAAM,WAAW,CAAC,CAAC,EAAE,IAAI,EAAE,OAAO,EAAE,aAAa,EAAE,CAAC,CAAC,CAAC;QACrE,MAAM,SAAS,GAAG,MAAM,YAAY,CAClC,EAAE,IAAI,EAAE,OAAO,EAAE,OAAO,EAAE,WAAW,EAAE,KAAK,EAAE,qBAAqB,EAAE,MAAM,EAAE,YAAY,EAAE,EAC3F,MAAM,EACN,QAAQ,CACT,CAAC;QACF,IAAI,CAAC,SAAS,CAAC,EAAE,EAAE,CAAC;YAClB,IAAI,CAAC,IAAI,EAAE,SAAS,CAAC,IAAI,EAAE,SAAS,CAAC,OAAO,CAAC,CAAC;YAC9C,OAAO;QACT,CAAC;QACD,MAAM,CAAC,IAAI,EAAE,EAAE,MAAM,EAAE,UAAU,EAAE,IAAI,CAAC,UAAU,CAAC,MAAM,EAAE,SAAS,EAAE,CAAC,CAAC;IAC1E,CAAC;YAAS,CAAC;QACT,MAAM,WAAW,EAAE,CAAC;IACtB,CAAC;AACH,CAAC;AAED,SAAS,MAAM,CACb,IAAa,EACb,GAAiF;IAEjF,IAAI,IAAI,EAAE,CAAC;QACT,QAAQ,CAAC;YACP,OAAO,EAAE,WAAW;YACpB,IAAI,EAAE,gCAAgC;YACtC,MAAM,EAAE,GAAG,CAAC,MAAM;YAClB,UAAU,EAAE,GAAG,CAAC,UAAU;YAC1B,GAAG,CAAC,GAAG,CAAC,SAAS,KAAK,IAAI,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,EAAE,SAAS,EAAE,GAAG,CAAC,SAAS,EAAE,CAAC;SAChE,CAAC,CAAC;QACH,OAAO;IACT,CAAC;IACD,IAAI,GAAG,CAAC,SAAS,KAAK,IAAI,IAAI,GAAG,CAAC,SAAS,CAAC,OAAO,KAAK,YAAY,EAAE,CAAC;QACrE,OAAO,CAAC,GAAG,CACT,yCAAyC,GAAG,CAAC,SAAS,CAAC,OAAO,oBAAoB,GAAG,CAAC,SAAS,CAAC,OAAO,IAAI,CAC5G,CAAC;IACJ,CAAC;IACD,MAAM,QAAQ,GAA+B;QAC3C,OAAO,EAAE,8CAA8C,GAAG,CAAC,UAAU,oCAAoC;QACzG,OAAO,EAAE,0CAA0C,GAAG,CAAC,UAAU,qBAAqB;QACtF,OAAO,EAAE,4CAA4C,GAAG,CAAC,UAAU,qBAAqB;QACxF,OAAO,EAAE,4GAA4G;KACtH,CAAC;IACF,OAAO,CAAC,GAAG,CAAC,QAAQ,CAAC,GAAG,CAAC,MAAM,CAAC,CAAC,CAAC;AACpC,CAAC","sourcesContent":["/**\n * `loam subsystem sync` — the ONE repair for `subsystem.views-stale`:\n * recompute the generated views file from the tree, write it when it\n * differs, remove it when the tree has no subsystems, and say which.\n *\n * The write goes through the same lock + journaled transaction every docs\n * writer uses (`commitStaged`), so a predecessor's interrupted commit is\n * recovered first and a sync killed mid-swap is rolled forward by the next\n * writer. Idempotent by construction: the expected bytes are a pure function\n * of the tree and the committed landscape, so a second run reports `current`\n * and writes nothing.\n *\n * The expected bytes come from `./txn/views.ts` — the same module every\n * writer verb renders from, so sync and the transactions cannot disagree\n * about what the file should say.\n */\nimport { existsSync } from \"node:fs\";\nimport { readFile } from \"node:fs/promises\";\nimport type { DocsDir } from \"../../core/kernel/ids/dirs.js\";\nimport { emitJson, fail } from \"../../core/envelope/json.js\";\nimport { subsystemViewsPath } from \"../../core/repo/paths.js\";\nimport { listFleetTree } from \"../../core/repo/repo.js\";\nimport { stageWrites } from \"../../core/staging/commit.js\";\nimport { type CommitRecovery, InterruptedCommitError } from \"../../core/staging/interrupted.js\";\nimport { acquireDocsLockWaiting, DocsBusyError, LOCK_WAIT_MS } from \"../../core/staging/lock.js\";\nimport { recoverInterruptedCommit } from \"../../core/staging/recovery/recover.js\";\nimport { commitStaged } from \"../../core/staging/txn/transaction.js\";\nimport { viewsAgree } from \"../../core/repo/tree/views.js\";\nimport { expectedViews } from \"./txn/views.js\";\n\n/** What one sync did to the file. */\ntype SyncAction = \"current\" | \"created\" | \"updated\" | \"removed\";\n\nexport async function runSync(docsDir: DocsDir, json: boolean): Promise<void> {\n let releaseLock: () => Promise<void>;\n try {\n releaseLock = await acquireDocsLockWaiting(docsDir, LOCK_WAIT_MS);\n } catch (err) {\n if (!(err instanceof DocsBusyError)) throw err;\n fail(json, \"docs-busy\", err.message);\n return;\n }\n try {\n // A predecessor's journal is recovered (or refused) before the tree is\n // read: the views file itself may be the half-written one.\n let recovered: CommitRecovery | null;\n try {\n recovered = await recoverInterruptedCommit(docsDir);\n } catch (err) {\n if (!(err instanceof InterruptedCommitError)) throw err;\n fail(json, \"commit-interrupted\", err.message);\n return;\n }\n\n const tree = await listFleetTree(docsDir);\n const expected = await expectedViews(docsDir, tree);\n const path = subsystemViewsPath(docsDir);\n const expectedBytes = expected === null ? null : Buffer.from(expected, \"utf8\");\n // Content, not bytes: `viewsAgree` records why, beside the generator. A\n // Windows clone hands this file back CRLF with not one fact changed, and a\n // byte compare made `sync` answer `updated` and rewrite it LF — after which\n // git shows the file permanently modified and the next checkout puts the\n // CRLF back. `validate` stopped demanding that rewrite; `sync` must stop\n // offering it, or the two disagree about the same file.\n const actual = existsSync(path) ? await readFile(path, \"utf8\") : null;\n if (viewsAgree(actual, expected)) {\n report(json, { action: \"current\", subsystems: tree.subsystems.length, recovered });\n return;\n }\n const action: SyncAction = actual === null ? \"created\" : expectedBytes === null ? \"removed\" : \"updated\";\n const staged = await stageWrites([{ path, content: expectedBytes }]);\n const committed = await commitStaged(\n { root: docsDir, command: \"subsystem\", rerun: \"loam subsystem sync\", target: \"subsystems\" },\n staged,\n \"synced\",\n );\n if (!committed.ok) {\n fail(json, committed.code, committed.message);\n return;\n }\n report(json, { action, subsystems: tree.subsystems.length, recovered });\n } finally {\n await releaseLock();\n }\n}\n\nfunction report(\n json: boolean,\n out: { action: SyncAction; subsystems: number; recovered: CommitRecovery | null },\n): void {\n if (json) {\n emitJson({\n command: \"subsystem\",\n path: \"architecture/subsystems.likec4\",\n action: out.action,\n subsystems: out.subsystems,\n ...(out.recovered === null ? {} : { recovered: out.recovered }),\n });\n return;\n }\n if (out.recovered !== null && out.recovered.outcome !== \"consistent\") {\n console.log(\n `note: recovered an interrupted \\`loam ${out.recovered.command}\\` commit first (${out.recovered.outcome}).`,\n );\n }\n const sentence: Record<SyncAction, string> = {\n current: `architecture/subsystems.likec4 is current (${out.subsystems} subsystem(s)) — nothing to write.`,\n created: `wrote architecture/subsystems.likec4 — ${out.subsystems} subsystem view(s).`,\n updated: `updated architecture/subsystems.likec4 — ${out.subsystems} subsystem view(s).`,\n removed: `removed architecture/subsystems.likec4 — the tree has no subsystems, so the generated file must be absent.`,\n };\n console.log(sentence[out.action]);\n}\n"]}
1
+ {"version":3,"file":"sync.js","sourceRoot":"","sources":["../../../src/commands/subsystem/sync.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * `loam subsystem sync` — the ONE repair for `subsystem.views-stale`:\n * recompute the generated views file from the tree, write it when it\n * differs, remove it when the tree has no subsystems, and say which.\n *\n * The write goes through the same lock + journaled transaction every docs\n * writer uses (`commitStaged`), so a predecessor's interrupted commit is\n * recovered first and a sync killed mid-swap is rolled forward by the next\n * writer. Idempotent by construction: the expected bytes are a pure function\n * of the tree and the committed landscape, so a second run reports `current`\n * and writes nothing.\n *\n * The expected bytes come from `./txn/views.ts` — the same module every\n * writer verb renders from, so sync and the transactions cannot disagree\n * about what the file should say.\n */\nimport { existsSync } from \"node:fs\";\nimport { readFile } from \"node:fs/promises\";\nimport type { DocsDir } from \"../../core/kernel/ids/dirs.js\";\nimport { emitJson, fail } from \"../../core/envelope/json.js\";\nimport { subsystemViewsPath } from \"../../core/repo/paths.js\";\nimport { listFleetTree } from \"../../core/repo/repo.js\";\nimport { stageWrites } from \"../../core/staging/commit.js\";\nimport { type CommitRecovery, InterruptedCommitError } from \"../../core/staging/interrupted.js\";\nimport { acquireDocsLockWaiting, DocsBusyError, LOCK_WAIT_MS } from \"../../core/staging/lock.js\";\nimport { recoverInterruptedCommit } from \"../../core/staging/recovery/recover.js\";\nimport { commitStaged } from \"../../core/staging/txn/transaction.js\";\nimport { viewsAgree } from \"../../core/repo/tree/render/views.js\";\nimport { expectedViews } from \"./txn/views.js\";\n\n/** What one sync did to the file. */\ntype SyncAction = \"current\" | \"created\" | \"updated\" | \"removed\";\n\nexport async function runSync(docsDir: DocsDir, json: boolean): Promise<void> {\n let releaseLock: () => Promise<void>;\n try {\n releaseLock = await acquireDocsLockWaiting(docsDir, LOCK_WAIT_MS);\n } catch (err) {\n if (!(err instanceof DocsBusyError)) throw err;\n fail(json, \"docs-busy\", err.message);\n return;\n }\n try {\n // A predecessor's journal is recovered (or refused) before the tree is\n // read: the views file itself may be the half-written one.\n let recovered: CommitRecovery | null;\n try {\n recovered = await recoverInterruptedCommit(docsDir);\n } catch (err) {\n if (!(err instanceof InterruptedCommitError)) throw err;\n fail(json, \"commit-interrupted\", err.message);\n return;\n }\n\n const tree = await listFleetTree(docsDir);\n const expected = await expectedViews(docsDir, tree);\n const path = subsystemViewsPath(docsDir);\n const expectedBytes = expected === null ? null : Buffer.from(expected, \"utf8\");\n // Content, not bytes: `viewsAgree` records why, beside the generator. A\n // Windows clone hands this file back CRLF with not one fact changed, and a\n // byte compare made `sync` answer `updated` and rewrite it LF — after which\n // git shows the file permanently modified and the next checkout puts the\n // CRLF back. `validate` stopped demanding that rewrite; `sync` must stop\n // offering it, or the two disagree about the same file.\n const actual = existsSync(path) ? await readFile(path, \"utf8\") : null;\n if (viewsAgree(actual, expected)) {\n report(json, { action: \"current\", subsystems: tree.subsystems.length, recovered });\n return;\n }\n const action: SyncAction = actual === null ? \"created\" : expectedBytes === null ? \"removed\" : \"updated\";\n const staged = await stageWrites([{ path, content: expectedBytes }]);\n const committed = await commitStaged(\n { root: docsDir, command: \"subsystem\", rerun: \"loam subsystem sync\", target: \"subsystems\" },\n staged,\n \"synced\",\n );\n if (!committed.ok) {\n fail(json, committed.code, committed.message);\n return;\n }\n report(json, { action, subsystems: tree.subsystems.length, recovered });\n } finally {\n await releaseLock();\n }\n}\n\nfunction report(\n json: boolean,\n out: { action: SyncAction; subsystems: number; recovered: CommitRecovery | null },\n): void {\n if (json) {\n emitJson({\n command: \"subsystem\",\n path: \"architecture/subsystems.likec4\",\n action: out.action,\n subsystems: out.subsystems,\n ...(out.recovered === null ? {} : { recovered: out.recovered }),\n });\n return;\n }\n if (out.recovered !== null && out.recovered.outcome !== \"consistent\") {\n console.log(\n `note: recovered an interrupted \\`loam ${out.recovered.command}\\` commit first (${out.recovered.outcome}).`,\n );\n }\n const sentence: Record<SyncAction, string> = {\n current: `architecture/subsystems.likec4 is current (${out.subsystems} subsystem(s)) — nothing to write.`,\n created: `wrote architecture/subsystems.likec4 — ${out.subsystems} subsystem view(s).`,\n updated: `updated architecture/subsystems.likec4 — ${out.subsystems} subsystem view(s).`,\n removed: `removed architecture/subsystems.likec4 — the tree has no subsystems, so the generated file must be absent.`,\n };\n console.log(sentence[out.action]);\n}\n"]}
@@ -19,7 +19,7 @@ import { acquireDocsLock, DocsBusyError } from "../../../core/staging/lock.js";
19
19
  import { recoverInterruptedCommit } from "../../../core/staging/recovery/recover.js";
20
20
  import { commitStaged } from "../../../core/staging/txn/transaction.js";
21
21
  import {} from "../../../core/staging/writes.js";
22
- import { viewsAgree } from "../../../core/repo/tree/views.js";
22
+ import { viewsAgree } from "../../../core/repo/tree/render/views.js";
23
23
  import { expectedViews } from "./views.js";
24
24
  /** The text tail a verb's success line carries about the generated file. */
25
25
  export function reportViews(views) {