@ybotok/loam 0.2.0-alpha.6 → 0.2.0-alpha.7

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 (105) hide show
  1. package/CHANGELOG.md +162 -0
  2. package/README.md +4 -4
  3. package/ROADMAP.md +27 -6
  4. package/SCHEMA.md +152 -32
  5. package/dist/commands/archive/run.js +49 -22
  6. package/dist/commands/archive/run.js.map +1 -1
  7. package/dist/commands/seed/commit.js +34 -1
  8. package/dist/commands/seed/commit.js.map +1 -1
  9. package/dist/commands/seed/plan.js +64 -13
  10. package/dist/commands/seed/plan.js.map +1 -1
  11. package/dist/commands/subsystem/edit.js +4 -2
  12. package/dist/commands/subsystem/edit.js.map +1 -1
  13. package/dist/commands/subsystem/move.js +55 -17
  14. package/dist/commands/subsystem/move.js.map +1 -1
  15. package/dist/commands/subsystem/sync.js +142 -45
  16. package/dist/commands/subsystem/sync.js.map +1 -1
  17. package/dist/commands/subsystem/txn/report.js +55 -5
  18. package/dist/commands/subsystem/txn/report.js.map +1 -1
  19. package/dist/commands/subsystem/txn/txn.js +118 -21
  20. package/dist/commands/subsystem/txn/txn.js.map +1 -1
  21. package/dist/commands/subsystem/txn/views.js +51 -8
  22. package/dist/commands/subsystem/txn/views.js.map +1 -1
  23. package/dist/commands/validate/checks/health-deps.js +9 -6
  24. package/dist/commands/validate/checks/health-deps.js.map +1 -1
  25. package/dist/commands/validate/fleet/census.js +26 -3
  26. package/dist/commands/validate/fleet/census.js.map +1 -1
  27. package/dist/commands/validate/fleet/landscape.js +12 -9
  28. package/dist/commands/validate/fleet/landscape.js.map +1 -1
  29. package/dist/commands/validate/fleet/map/attest.js +4 -5
  30. package/dist/commands/validate/fleet/map/attest.js.map +1 -1
  31. package/dist/commands/validate/fleet/map/bindings.js +15 -2
  32. package/dist/commands/validate/fleet/map/bindings.js.map +1 -1
  33. package/dist/commands/validate/fleet/map/consumers.js +43 -122
  34. package/dist/commands/validate/fleet/map/consumers.js.map +1 -1
  35. package/dist/commands/validate/fleet/scorecard/scorecard.js.map +1 -1
  36. package/dist/commands/validate/fleet/views/fleet-project.js +1 -1
  37. package/dist/commands/validate/fleet/views/fleet-project.js.map +1 -1
  38. package/dist/commands/validate/fleet/views/ids.js +33 -7
  39. package/dist/commands/validate/fleet/views/ids.js.map +1 -1
  40. package/dist/commands/validate/fleet/views/root-documents.js +9 -7
  41. package/dist/commands/validate/fleet/views/root-documents.js.map +1 -1
  42. package/dist/commands/validate/fleet/views/stale.js +31 -14
  43. package/dist/commands/validate/fleet/views/stale.js.map +1 -1
  44. package/dist/core/agent/agents-md/command-map.js +1 -1
  45. package/dist/core/agent/agents-md/command-map.js.map +1 -1
  46. package/dist/core/agent/agents-md/map/subsystem.js +27 -14
  47. package/dist/core/agent/agents-md/map/subsystem.js.map +1 -1
  48. package/dist/core/agent/agents-md/refusals.js +5 -3
  49. package/dist/core/agent/agents-md/refusals.js.map +1 -1
  50. package/dist/core/agent/agents-md.js +16 -7
  51. package/dist/core/agent/agents-md.js.map +1 -1
  52. package/dist/core/agent/workflows/check.js +5 -6
  53. package/dist/core/agent/workflows/check.js.map +1 -1
  54. package/dist/core/agent/workflows/reference/spine.js +12 -6
  55. package/dist/core/agent/workflows/reference/spine.js.map +1 -1
  56. package/dist/core/brief/brief.js +25 -8
  57. package/dist/core/brief/brief.js.map +1 -1
  58. package/dist/core/brief/targets.js +64 -0
  59. package/dist/core/brief/targets.js.map +1 -1
  60. package/dist/core/brief/unchecked.js +1 -1
  61. package/dist/core/brief/unchecked.js.map +1 -1
  62. package/dist/core/c4/project/architecture.js +31 -9
  63. package/dist/core/c4/project/architecture.js.map +1 -1
  64. package/dist/core/c4/project/documents.js +5 -4
  65. package/dist/core/c4/project/documents.js.map +1 -1
  66. package/dist/core/c4/project/load.js +8 -4
  67. package/dist/core/c4/project/load.js.map +1 -1
  68. package/dist/core/c4/project/staged.js +7 -3
  69. package/dist/core/c4/project/staged.js.map +1 -1
  70. package/dist/core/deployment/delta.js +12 -10
  71. package/dist/core/deployment/delta.js.map +1 -1
  72. package/dist/core/gate/checks.js +5 -1
  73. package/dist/core/gate/checks.js.map +1 -1
  74. package/dist/core/gate/partners.js +60 -17
  75. package/dist/core/gate/partners.js.map +1 -1
  76. package/dist/core/repo/paths.js +34 -29
  77. package/dist/core/repo/paths.js.map +1 -1
  78. package/dist/core/repo/tree/render/body.js +45 -6
  79. package/dist/core/repo/tree/render/body.js.map +1 -1
  80. package/dist/core/repo/tree/render/service/header.js +125 -0
  81. package/dist/core/repo/tree/render/service/header.js.map +1 -0
  82. package/dist/core/repo/tree/render/service/peers.js +28 -0
  83. package/dist/core/repo/tree/render/service/peers.js.map +1 -0
  84. package/dist/core/repo/tree/render/service/view-id.js +67 -0
  85. package/dist/core/repo/tree/render/service/view-id.js.map +1 -0
  86. package/dist/core/repo/tree/render/service/views.js +78 -0
  87. package/dist/core/repo/tree/render/service/views.js.map +1 -0
  88. package/dist/core/repo/tree/render/stale.js +19 -2
  89. package/dist/core/repo/tree/render/stale.js.map +1 -1
  90. package/dist/core/repo/tree/render/views.js +102 -33
  91. package/dist/core/repo/tree/render/views.js.map +1 -1
  92. package/dist/core/verify/checklist.js +42 -10
  93. package/dist/core/verify/checklist.js.map +1 -1
  94. package/dist/core/vocabulary/issue.js.map +1 -1
  95. package/dist/core/vocabulary/maturity.js +41 -1
  96. package/dist/core/vocabulary/maturity.js.map +1 -1
  97. package/examples/README.md +7 -3
  98. package/examples/docs/architecture/landscape.likec4 +13 -7
  99. package/examples/docs/architecture/services.likec4 +70 -0
  100. package/examples/docs/architecture/subsystems.likec4 +3 -0
  101. package/examples/docs/services/order-service/model.likec4 +10 -8
  102. package/examples/docs/services/payment-service/model.likec4 +6 -4
  103. package/examples/docs/services/platform/identity-service/model.likec4 +9 -7
  104. package/examples/docs/services/platform/notification-service/model.likec4 +4 -5
  105. package/package.json +1 -1
@@ -1,12 +1,15 @@
1
1
  /**
2
2
  * The shared commit window every subsystem WRITE verb goes through — `new`,
3
3
  * `rm`, `move` and `rename` all land here: lock, recover a predecessor's
4
- * journal, plan against the walked tree, regenerate the views file from the
5
- * PLANNED post-state, commit through the journaled transaction (directory
6
- * renames included), report. One window, because the views file must land in
7
- * the same commit as whatever changed the tree — a marker without its view,
8
- * or renames without theirs, is exactly the `subsystem.views-stale` state
9
- * the generated file's contract refuses to leave between two commits.
4
+ * journal, plan against the walked tree, regenerate BOTH generated views files
5
+ * from the PLANNED post-state, commit through the journaled transaction
6
+ * (directory renames included), report. One window, because those files must
7
+ * land in the same commit as whatever changed the tree — a marker without its
8
+ * view, or renames without theirs, is exactly the `subsystem.views-stale` state
9
+ * the generated files' contract refuses to leave between two commits. The
10
+ * per-service file is in that rule and not beside it: its titles are
11
+ * `<subsystem path> / <service id>`, so the placement a verb changes is written
12
+ * into it too.
10
13
  *
11
14
  * This window does NOT write the per-service project files
12
15
  * (`services/<…>/<id>/likec4.config.json`); only `../sync.ts` does, and the
@@ -26,11 +29,19 @@
26
29
  * model with no file beside it, and that is the state `validate --all` names
27
30
  * as `service.likec4-config-missing`, with `loam subsystem sync` as the
28
31
  * repair — one writer, so the create-only rule has one place to hold.
32
+ *
33
+ * NEITHER generated file is written over a document loam did not write, and
34
+ * that guard is checked HERE as well as in `../sync.ts` — see the comment at
35
+ * the check. Both are filenames loam RESERVES inside a directory the team
36
+ * owns, both are rewritten whole, and until the check below existed all four
37
+ * verbs took an authored file at either path without a word. Unlike sync,
38
+ * which reports the one file `refused` and completes the rest, a verb refuses
39
+ * WHOLE: the file has to land in this commit, so half of this commit is not a
40
+ * lesser version of it.
29
41
  */
30
42
  import { existsSync } from "node:fs";
31
43
  import { readFile } from "node:fs/promises";
32
- import { fail } from "../../../core/envelope/json.js";
33
- import { subsystemViewsPath } from "../../../core/repo/paths.js";
44
+ import { fail, repoPath } from "../../../core/envelope/json.js";
34
45
  import { listFleetTree } from "../../../core/repo/repo.js";
35
46
  import { stageWrites } from "../../../core/staging/commit.js";
36
47
  import { InterruptedCommitError } from "../../../core/staging/interrupted.js";
@@ -38,11 +49,21 @@ import { acquireDocsLock, DocsBusyError } from "../../../core/staging/lock.js";
38
49
  import { recoverInterruptedCommit } from "../../../core/staging/recovery/recover.js";
39
50
  import { commitStaged } from "../../../core/staging/txn/transaction.js";
40
51
  import {} from "../../../core/staging/writes.js";
52
+ import { authoredGeneratedPaths, authoredRefusal } from "../../../core/repo/tree/render/service/header.js";
41
53
  import { viewsAgree } from "../../../core/repo/tree/render/views.js";
42
54
  import { expectedViews, LANDSCAPE_UNREADABLE_NOTE } from "./views.js";
43
- /** The text tail a verb's success line carries about the generated file. */
55
+ /** The text tail a verb's success line carries about the generated files — one clause per file that moved. */
44
56
  export function reportViews(views) {
45
- return views === "current" ? "" : ` — architecture/subsystems.likec4 ${views}`;
57
+ return ([
58
+ ["architecture/subsystems.likec4", views.subsystems],
59
+ ["architecture/services.likec4", views.services],
60
+ ]
61
+ // `current` says nothing: the tail hangs off a success line, and a verb
62
+ // that renamed one directory must not print two clauses about files that
63
+ // did not move.
64
+ .filter(([, action]) => action !== "current")
65
+ .map(([file, action]) => ` — ${file} ${action}`)
66
+ .join(""));
46
67
  }
47
68
  /**
48
69
  * Lock → recover → plan → stage (views included) → journaled commit → report.
@@ -74,19 +95,69 @@ export async function commitWindow(docsDir, json, plan) {
74
95
  const txn = await plan(await listFleetTree(docsDir));
75
96
  if (txn === null)
76
97
  return;
77
- const path = subsystemViewsPath(docsDir);
78
98
  const expected = await expectedViews(docsDir, txn.tree);
79
- const bytes = expected.content === null ? null : Buffer.from(expected.content, "utf8");
80
- // Content, not bytes — `viewsAgree` carries the reason. Without it every
81
- // subsystem move on a Windows clone folded a rewrite of this file into its
82
- // transaction that changed not one fact, and journalled it.
83
- const current = existsSync(path) ? await readFile(path, "utf8") : null;
84
- let views = "current";
85
- const writes = [...txn.writes];
86
- if (!viewsAgree(current, expected.content)) {
87
- views = current === null ? "created" : bytes === null ? "removed" : "updated";
88
- writes.push({ path, content: bytes });
99
+ // OWNERSHIP BEFORE ANYTHING IS STAGED, and the WHOLE verb is refused.
100
+ //
101
+ // The guard first shipped in `../sync.ts` alone, and this window is the
102
+ // other writer of exactly the same two reserved paths — for `subsystem
103
+ // new`, `rm`, `move` and `rename` at once. Reproduced before this call
104
+ // existed: with a hand-authored `architecture/services.likec4` in the tree,
105
+ // `loam subsystem new platform` printed "architecture/services.likec4
106
+ // updated" and destroyed it, first line and all. One predicate, one
107
+ // sentence, shared from beneath every writer
108
+ // (`core/repo/tree/render/service/header.ts`).
109
+ //
110
+ // WHY THE WHOLE OPERATION AND NOT JUST THE FILE. `sync` can decline one file
111
+ // and still do the rest, because nothing else it writes depends on that file
112
+ // and every part of it is idempotent. This window cannot: its entire reason
113
+ // for existing is that the generated files land in the SAME commit as the
114
+ // change to the tree, so completing the structural half and reporting the
115
+ // file as refused would MANUFACTURE the `subsystem.views-stale` state the
116
+ // banner above says this window refuses to leave between two commits — and
117
+ // it would do so irreversibly, since `move` and `rename` also stage their
118
+ // renames in git. A half-done move nobody asked for is worse than a refusal
119
+ // a person can act on, and a refusal is re-runnable the moment the authored
120
+ // file is out of the reserved path.
121
+ //
122
+ // It runs AFTER `plan`, which writes nothing and has already answered the
123
+ // verb-specific refusals (`unknown-target`, `subsystem-not-empty`,
124
+ // `move-uncommitted`): a caller whose name is wrong AND whose tree holds an
125
+ // authored views file is better told about the name.
126
+ const authored = await authoredGeneratedPaths([expected.subsystems.path, expected.services.path]);
127
+ if (authored.length > 0) {
128
+ fail(json,
129
+ // The code is `already-exists` and not a new one because it is exactly
130
+ // what that code has always meant here — "refusing to create over
131
+ // something that already exists, because overwriting would destroy work
132
+ // nobody asked to lose" — and it is already this command's documented
133
+ // refusal for the flat namespace. A second code for the same sentence
134
+ // would be a second branch an agent has to learn for no new decision.
135
+ "already-exists", authored.map((path) => authoredRefusal(repoPath(docsDir, path))).join(" ") +
136
+ " NOTHING was written and nothing was renamed: this command lands the generated views file in the same " +
137
+ "commit as the change to the tree, so it refuses whole rather than leaving the tree changed — and, for " +
138
+ "`move` and `rename`, renames staged in git — with a views file it may not write.");
139
+ return;
89
140
  }
141
+ const writes = [...txn.writes];
142
+ // BOTH generated files, in the ONE commit that changes the tree. The
143
+ // per-service file is not a bystander here: its view titles are
144
+ // `<subsystem path> / <service id>`, so every `move` and `rename` restales
145
+ // it exactly as it restales the subsystem views, and leaving it for a later
146
+ // `sync` is the failing-between-two-commits state this window exists to
147
+ // refuse.
148
+ const views = {
149
+ subsystems: await stageGenerated(expected.subsystems, writes, false),
150
+ // PROTECTED when the map did not parse as a project, and only this file
151
+ // is: `renderSubsystemViews` answers null off the TREE, which is readable
152
+ // either way, while `renderServiceOverviews` answers null whenever no
153
+ // service resolves to an element — which is what an unreadable map looks
154
+ // like from here. Under `known: false` a null render is therefore not
155
+ // evidence of absence, and writing it would delete the whole file over a
156
+ // sibling document that does not parse. A verb still rewrites a file it
157
+ // CAN render (the tolerant arm this window has always taken), and the
158
+ // person is told by the note at the foot of this function.
159
+ services: await stageGenerated(expected.services, writes, !expected.known),
160
+ };
90
161
  const staged = await stageWrites(writes);
91
162
  const committed = await commitStaged({
92
163
  root: docsDir,
@@ -117,4 +188,30 @@ export async function commitWindow(docsDir, json, plan) {
117
188
  await releaseLock();
118
189
  }
119
190
  }
191
+ /**
192
+ * Compare ONE generated file against what it must say, append the write when
193
+ * they differ, and name what happened.
194
+ *
195
+ * The compare is over CONTENT, not bytes — `viewsAgree` carries the reason,
196
+ * beside the generator that mints them: an ordinary Windows clone hands a
197
+ * generated file back with CRLF and not one fact in it changed, and without
198
+ * this every subsystem move on such a clone folded a rewrite that changed
199
+ * nothing into its transaction and journalled it. What gets WRITTEN is still
200
+ * the LF render as a Buffer, so the decode here is a comparison only.
201
+ *
202
+ * `protect` is the caller's statement that a NULL render is not evidence: the
203
+ * file is left exactly as it is rather than deleted. It is never a licence to
204
+ * skip a real rewrite — a file loam can render is rendered, which is the
205
+ * difference between this window and `../sync.ts`'s `blocked`.
206
+ */
207
+ async function stageGenerated(file, writes, protect) {
208
+ const current = existsSync(file.path) ? await readFile(file.path, "utf8") : null;
209
+ if (viewsAgree(current, file.content))
210
+ return "current";
211
+ if (protect && file.content === null)
212
+ return "current";
213
+ const bytes = file.content === null ? null : Buffer.from(file.content, "utf8");
214
+ writes.push({ path: file.path, content: bytes });
215
+ return current === null ? "created" : bytes === null ? "removed" : "updated";
216
+ }
120
217
  //# sourceMappingURL=txn.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"txn.js","sourceRoot":"","sources":["../../../../src/commands/subsystem/txn/txn.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA4BG;AACH,OAAO,EAAE,UAAU,EAAE,MAAM,SAAS,CAAC;AACrC,OAAO,EAAE,QAAQ,EAAE,MAAM,kBAAkB,CAAC;AAE5C,OAAO,EAAE,IAAI,EAAkB,MAAM,gCAAgC,CAAC;AACtE,OAAO,EAAE,kBAAkB,EAAE,MAAM,6BAA6B,CAAC;AACjE,OAAO,EAAE,aAAa,EAAE,MAAM,4BAA4B,CAAC;AAE3D,OAAO,EAAE,WAAW,EAAE,MAAM,iCAAiC,CAAC;AAC9D,OAAO,EAAuB,sBAAsB,EAAE,MAAM,sCAAsC,CAAC;AACnG,OAAO,EAAE,eAAe,EAAE,aAAa,EAAE,MAAM,+BAA+B,CAAC;AAC/E,OAAO,EAAE,wBAAwB,EAAE,MAAM,2CAA2C,CAAC;AACrF,OAAO,EAAE,YAAY,EAAE,MAAM,0CAA0C,CAAC;AACxE,OAAO,EAAqB,MAAM,iCAAiC,CAAC;AACpE,OAAO,EAAE,UAAU,EAAE,MAAM,yCAAyC,CAAC;AACrE,OAAO,EAAE,aAAa,EAAE,yBAAyB,EAAE,MAAM,YAAY,CAAC;AAItE,4EAA4E;AAC5E,MAAM,UAAU,WAAW,CAAC,KAAkB;IAC5C,OAAO,KAAK,KAAK,SAAS,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,qCAAqC,KAAK,EAAE,CAAC;AACjF,CAAC;AAqBD;;;;GAIG;AACH,MAAM,CAAC,KAAK,UAAU,YAAY,CAChC,OAAgB,EAChB,IAAa,EACb,IAAuD;IAEvD,IAAI,WAAgC,CAAC;IACrC,IAAI,CAAC;QACH,WAAW,GAAG,MAAM,eAAe,CAAC,OAAO,CAAC,CAAC;IAC/C,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,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;QACD,MAAM,GAAG,GAAG,MAAM,IAAI,CAAC,MAAM,aAAa,CAAC,OAAO,CAAC,CAAC,CAAC;QACrD,IAAI,GAAG,KAAK,IAAI;YAAE,OAAO;QAEzB,MAAM,IAAI,GAAG,kBAAkB,CAAC,OAAO,CAAC,CAAC;QACzC,MAAM,QAAQ,GAAG,MAAM,aAAa,CAAC,OAAO,EAAE,GAAG,CAAC,IAAI,CAAC,CAAC;QACxD,MAAM,KAAK,GAAG,QAAQ,CAAC,OAAO,KAAK,IAAI,CAAC,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC,MAAM,CAAC,IAAI,CAAC,QAAQ,CAAC,OAAO,EAAE,MAAM,CAAC,CAAC;QACvF,yEAAyE;QACzE,2EAA2E;QAC3E,4DAA4D;QAC5D,MAAM,OAAO,GAAG,UAAU,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC,MAAM,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC,CAAC,CAAC,CAAC,IAAI,CAAC;QACvE,IAAI,KAAK,GAAgB,SAAS,CAAC;QACnC,MAAM,MAAM,GAAG,CAAC,GAAG,GAAG,CAAC,MAAM,CAAC,CAAC;QAC/B,IAAI,CAAC,UAAU,CAAC,OAAO,EAAE,QAAQ,CAAC,OAAO,CAAC,EAAE,CAAC;YAC3C,KAAK,GAAG,OAAO,KAAK,IAAI,CAAC,CAAC,CAAC,SAAS,CAAC,CAAC,CAAC,KAAK,KAAK,IAAI,CAAC,CAAC,CAAC,SAAS,CAAC,CAAC,CAAC,SAAS,CAAC;YAC9E,MAAM,CAAC,IAAI,CAAC,EAAE,IAAI,EAAE,OAAO,EAAE,KAAK,EAAE,CAAC,CAAC;QACxC,CAAC;QACD,MAAM,MAAM,GAAG,MAAM,WAAW,CAAC,MAAM,CAAC,CAAC;QACzC,MAAM,SAAS,GAAG,MAAM,YAAY,CAClC;YACE,IAAI,EAAE,OAAO;YACb,OAAO,EAAE,WAAW;YACpB,KAAK,EAAE,qBAAqB;YAC5B,MAAM,EAAE,GAAG,CAAC,MAAM;YAClB,GAAG,CAAC,GAAG,CAAC,KAAK,KAAK,SAAS,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,EAAE,KAAK,EAAE,GAAG,CAAC,KAAK,EAAE,CAAC;SACzD,EACD,MAAM,EACN,GAAG,CAAC,IAAI,CACT,CAAC;QACF,IAAI,CAAC,SAAS,CAAC,EAAE,EAAE,CAAC;YAClB,MAAM,IAAI,GACR,SAAS,CAAC,IAAI,KAAK,qBAAqB;gBACtC,CAAC,CAAC,SAAS,CAAC,IAAI;gBAChB,CAAC,CAAC,SAAS,CAAC,KAAK;oBACf,CAAC,CAAC,CAAC,GAAG,CAAC,SAAS,IAAI,GAAG,CAAC,UAAU,IAAI,SAAS,CAAC,IAAI,CAAC;oBACrD,CAAC,CAAC,CAAC,GAAG,CAAC,UAAU,IAAI,SAAS,CAAC,IAAI,CAAC,CAAC;YAC3C,IAAI,CAAC,IAAI,EAAE,IAAI,EAAE,SAAS,CAAC,OAAO,CAAC,CAAC;YACpC,OAAO;QACT,CAAC;QACD,MAAM,GAAG,CAAC,MAAM,CAAC,KAAK,EAAE,SAAS,CAAC,CAAC;QACnC,0EAA0E;QAC1E,8EAA8E;QAC9E,6EAA6E;QAC7E,uEAAuE;QACvE,wEAAwE;QACxE,IAAI,CAAC,IAAI,IAAI,CAAC,QAAQ,CAAC,KAAK;YAAE,OAAO,CAAC,GAAG,CAAC,yBAAyB,CAAC,CAAC;IACvE,CAAC;YAAS,CAAC;QACT,MAAM,WAAW,EAAE,CAAC;IACtB,CAAC;AACH,CAAC","sourcesContent":["/**\n * The shared commit window every subsystem WRITE verb goes through — `new`,\n * `rm`, `move` and `rename` all land here: lock, recover a predecessor's\n * journal, plan against the walked tree, regenerate the views file from the\n * PLANNED post-state, commit through the journaled transaction (directory\n * renames included), report. One window, because the views file must land in\n * the same commit as whatever changed the tree — a marker without its view,\n * or renames without theirs, is exactly the `subsystem.views-stale` state\n * the generated file's contract refuses to leave between two commits.\n *\n * This window does NOT write the per-service project files\n * (`services/<…>/<id>/likec4.config.json`); only `../sync.ts` does, and the\n * asymmetry is deliberate rather than an omission. The views file must ride\n * in the verb's commit because its bytes depend on the PLACEMENT the verb is\n * changing. A project file's bytes depend on the service id alone, which no\n * verb here changes — and the file lives INSIDE the service directory, so a\n * `move` carries it to the new location as part of the rename it already\n * journals (in a git-backed repo that move REFUSES first, `move-uncommitted`,\n * until the files `sync` created are committed — the refusal names them, and\n * `sync`'s own output says to commit them), and `rm` never reaches one,\n * because it refuses any subsystem that still holds a member\n * (`subsystem-not-empty`, `../edit.ts`). No verb can\n * therefore leave a project file stale or orphaned, and a verb that filled\n * gaps in passing would silently write files a `--json` reader of `new` or\n * `rename` was never told to expect. The one state a verb can leave is a\n * model with no file beside it, and that is the state `validate --all` names\n * as `service.likec4-config-missing`, with `loam subsystem sync` as the\n * repair — one writer, so the create-only rule has one place to hold.\n */\nimport { existsSync } from \"node:fs\";\nimport { readFile } from \"node:fs/promises\";\nimport type { DocsDir } from \"../../../core/kernel/ids/dirs.js\";\nimport { fail, type ErrorCode } from \"../../../core/envelope/json.js\";\nimport { subsystemViewsPath } from \"../../../core/repo/paths.js\";\nimport { listFleetTree } from \"../../../core/repo/repo.js\";\nimport type { FleetTree } from \"../../../core/repo/tree/walk.js\";\nimport { stageWrites } from \"../../../core/staging/commit.js\";\nimport { type CommitRecovery, InterruptedCommitError } from \"../../../core/staging/interrupted.js\";\nimport { acquireDocsLock, DocsBusyError } from \"../../../core/staging/lock.js\";\nimport { recoverInterruptedCommit } from \"../../../core/staging/recovery/recover.js\";\nimport { commitStaged } from \"../../../core/staging/txn/transaction.js\";\nimport { type PlannedWrite } from \"../../../core/staging/writes.js\";\nimport { viewsAgree } from \"../../../core/repo/tree/render/views.js\";\nimport { expectedViews, LANDSCAPE_UNREADABLE_NOTE } from \"./views.js\";\n\nexport type ViewsAction = \"created\" | \"updated\" | \"removed\" | \"current\";\n\n/** The text tail a verb's success line carries about the generated file. */\nexport function reportViews(views: ViewsAction): string {\n return views === \"current\" ? \"\" : ` — architecture/subsystems.likec4 ${views}`;\n}\n\n/** What one subsystem verb asks the shared commit window to land. */\nexport interface SubsystemTxn {\n /** The journal's `target` — what this commit is FOR. */\n target: string;\n /** Marker writes (create/delete); the views write is derived and appended. */\n writes: PlannedWrite[];\n /** The tree AS THE COMMIT LEAVES IT — the views file is rendered from this. */\n tree: FleetTree;\n /** Directory renames, performed after the swaps, in this order. */\n moves?: { from: string; to: string }[];\n /** Remap of a RACED commit failure (new: a lost marker create is `already-exists`). */\n racedCode?: ErrorCode;\n /** Remap of a rolled-back commit failure (move: `move-failed` — no merge was computed). */\n failedCode?: ErrorCode;\n /** The rollback sentence's verb for half-state files. */\n what: string;\n report: (views: ViewsAction, recovered: CommitRecovery | null) => void | Promise<void>;\n}\n\n/**\n * Lock → recover → plan → stage (views included) → journaled commit → report.\n * `plan` returning null means it already reported a refusal; nothing was\n * staged and nothing is written.\n */\nexport async function commitWindow(\n docsDir: DocsDir,\n json: boolean,\n plan: (tree: FleetTree) => Promise<SubsystemTxn | null>,\n): Promise<void> {\n let releaseLock: () => Promise<void>;\n try {\n releaseLock = await acquireDocsLock(docsDir);\n } catch (err) {\n if (!(err instanceof DocsBusyError)) throw err;\n fail(json, \"docs-busy\", err.message);\n return;\n }\n try {\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 const txn = await plan(await listFleetTree(docsDir));\n if (txn === null) return;\n\n const path = subsystemViewsPath(docsDir);\n const expected = await expectedViews(docsDir, txn.tree);\n const bytes = expected.content === null ? null : Buffer.from(expected.content, \"utf8\");\n // Content, not bytes — `viewsAgree` carries the reason. Without it every\n // subsystem move on a Windows clone folded a rewrite of this file into its\n // transaction that changed not one fact, and journalled it.\n const current = existsSync(path) ? await readFile(path, \"utf8\") : null;\n let views: ViewsAction = \"current\";\n const writes = [...txn.writes];\n if (!viewsAgree(current, expected.content)) {\n views = current === null ? \"created\" : bytes === null ? \"removed\" : \"updated\";\n writes.push({ path, content: bytes });\n }\n const staged = await stageWrites(writes);\n const committed = await commitStaged(\n {\n root: docsDir,\n command: \"subsystem\",\n rerun: \"loam subsystem sync\",\n target: txn.target,\n ...(txn.moves === undefined ? {} : { moves: txn.moves }),\n },\n staged,\n txn.what,\n );\n if (!committed.ok) {\n const code =\n committed.code === \"rollback-incomplete\"\n ? committed.code\n : committed.raced\n ? (txn.racedCode ?? txn.failedCode ?? committed.code)\n : (txn.failedCode ?? committed.code);\n fail(json, code, committed.message);\n return;\n }\n await txn.report(views, recovered);\n // A verb CANNOT leave this file behind the tree — its bytes depend on the\n // placement the verb just changed, so the tolerant render lands even when the\n // map is unreadable and the file loses its includes. `sync` may decline that\n // rewrite (`action: \"blocked\"`); a verb may not, so the person is told\n // instead, and the same note names the run that puts the includes back.\n if (!json && !expected.known) console.log(LANDSCAPE_UNREADABLE_NOTE);\n } finally {\n await releaseLock();\n }\n}\n"]}
1
+ {"version":3,"file":"txn.js","sourceRoot":"","sources":["../../../../src/commands/subsystem/txn/txn.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * The shared commit window every subsystem WRITE verb goes through — `new`,\n * `rm`, `move` and `rename` all land here: lock, recover a predecessor's\n * journal, plan against the walked tree, regenerate BOTH generated views files\n * from the PLANNED post-state, commit through the journaled transaction\n * (directory renames included), report. One window, because those files must\n * land in the same commit as whatever changed the tree — a marker without its\n * view, or renames without theirs, is exactly the `subsystem.views-stale` state\n * the generated files' contract refuses to leave between two commits. The\n * per-service file is in that rule and not beside it: its titles are\n * `<subsystem path> / <service id>`, so the placement a verb changes is written\n * into it too.\n *\n * This window does NOT write the per-service project files\n * (`services/<…>/<id>/likec4.config.json`); only `../sync.ts` does, and the\n * asymmetry is deliberate rather than an omission. The views file must ride\n * in the verb's commit because its bytes depend on the PLACEMENT the verb is\n * changing. A project file's bytes depend on the service id alone, which no\n * verb here changes — and the file lives INSIDE the service directory, so a\n * `move` carries it to the new location as part of the rename it already\n * journals (in a git-backed repo that move REFUSES first, `move-uncommitted`,\n * until the files `sync` created are committed — the refusal names them, and\n * `sync`'s own output says to commit them), and `rm` never reaches one,\n * because it refuses any subsystem that still holds a member\n * (`subsystem-not-empty`, `../edit.ts`). No verb can\n * therefore leave a project file stale or orphaned, and a verb that filled\n * gaps in passing would silently write files a `--json` reader of `new` or\n * `rename` was never told to expect. The one state a verb can leave is a\n * model with no file beside it, and that is the state `validate --all` names\n * as `service.likec4-config-missing`, with `loam subsystem sync` as the\n * repair — one writer, so the create-only rule has one place to hold.\n *\n * NEITHER generated file is written over a document loam did not write, and\n * that guard is checked HERE as well as in `../sync.ts` — see the comment at\n * the check. Both are filenames loam RESERVES inside a directory the team\n * owns, both are rewritten whole, and until the check below existed all four\n * verbs took an authored file at either path without a word. Unlike sync,\n * which reports the one file `refused` and completes the rest, a verb refuses\n * WHOLE: the file has to land in this commit, so half of this commit is not a\n * lesser version of it.\n */\nimport { existsSync } from \"node:fs\";\nimport { readFile } from \"node:fs/promises\";\nimport type { DocsDir } from \"../../../core/kernel/ids/dirs.js\";\nimport { fail, repoPath, type ErrorCode } from \"../../../core/envelope/json.js\";\nimport { listFleetTree } from \"../../../core/repo/repo.js\";\nimport type { FleetTree } from \"../../../core/repo/tree/walk.js\";\nimport { stageWrites } from \"../../../core/staging/commit.js\";\nimport { type CommitRecovery, InterruptedCommitError } from \"../../../core/staging/interrupted.js\";\nimport { acquireDocsLock, DocsBusyError } from \"../../../core/staging/lock.js\";\nimport { recoverInterruptedCommit } from \"../../../core/staging/recovery/recover.js\";\nimport { commitStaged } from \"../../../core/staging/txn/transaction.js\";\nimport { type PlannedWrite } from \"../../../core/staging/writes.js\";\nimport { authoredGeneratedPaths, authoredRefusal } from \"../../../core/repo/tree/render/service/header.js\";\nimport { viewsAgree } from \"../../../core/repo/tree/render/views.js\";\nimport { type ExpectedFile, expectedViews, LANDSCAPE_UNREADABLE_NOTE } from \"./views.js\";\n\nexport type ViewsAction = \"created\" | \"updated\" | \"removed\" | \"current\";\n\n/**\n * What one commit did to EACH generated file, kept apart.\n *\n * One action per file rather than one folded answer, for the reason\n * `./views.ts` gives about the expected bytes: the two files change for\n * different reasons and are named separately everywhere a person or a `--json`\n * reader meets them. The `views` key a verb has always emitted still carries\n * `subsystems` and nothing else — a payload key's meaning is frozen, so the\n * second file arrives as a key of its own beside it.\n */\nexport interface ViewsActions {\n /** `architecture/subsystems.likec4` — the verb's `--json` `views` key, unchanged. */\n subsystems: ViewsAction;\n /** `architecture/services.likec4` — the per-service overviews, one per service the map draws. */\n services: ViewsAction;\n}\n\n/** The text tail a verb's success line carries about the generated files — one clause per file that moved. */\nexport function reportViews(views: ViewsActions): string {\n return (\n [\n [\"architecture/subsystems.likec4\", views.subsystems] as const,\n [\"architecture/services.likec4\", views.services] as const,\n ]\n // `current` says nothing: the tail hangs off a success line, and a verb\n // that renamed one directory must not print two clauses about files that\n // did not move.\n .filter(([, action]) => action !== \"current\")\n .map(([file, action]) => ` — ${file} ${action}`)\n .join(\"\")\n );\n}\n\n/** What one subsystem verb asks the shared commit window to land. */\nexport interface SubsystemTxn {\n /** The journal's `target` — what this commit is FOR. */\n target: string;\n /** Marker writes (create/delete); the views write is derived and appended. */\n writes: PlannedWrite[];\n /** The tree AS THE COMMIT LEAVES IT — the views file is rendered from this. */\n tree: FleetTree;\n /** Directory renames, performed after the swaps, in this order. */\n moves?: { from: string; to: string }[];\n /** Remap of a RACED commit failure (new: a lost marker create is `already-exists`). */\n racedCode?: ErrorCode;\n /** Remap of a rolled-back commit failure (move: `move-failed` — no merge was computed). */\n failedCode?: ErrorCode;\n /** The rollback sentence's verb for half-state files. */\n what: string;\n report: (views: ViewsActions, recovered: CommitRecovery | null) => void | Promise<void>;\n}\n\n/**\n * Lock → recover → plan → stage (views included) → journaled commit → report.\n * `plan` returning null means it already reported a refusal; nothing was\n * staged and nothing is written.\n */\nexport async function commitWindow(\n docsDir: DocsDir,\n json: boolean,\n plan: (tree: FleetTree) => Promise<SubsystemTxn | null>,\n): Promise<void> {\n let releaseLock: () => Promise<void>;\n try {\n releaseLock = await acquireDocsLock(docsDir);\n } catch (err) {\n if (!(err instanceof DocsBusyError)) throw err;\n fail(json, \"docs-busy\", err.message);\n return;\n }\n try {\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 const txn = await plan(await listFleetTree(docsDir));\n if (txn === null) return;\n\n const expected = await expectedViews(docsDir, txn.tree);\n // OWNERSHIP BEFORE ANYTHING IS STAGED, and the WHOLE verb is refused.\n //\n // The guard first shipped in `../sync.ts` alone, and this window is the\n // other writer of exactly the same two reserved paths — for `subsystem\n // new`, `rm`, `move` and `rename` at once. Reproduced before this call\n // existed: with a hand-authored `architecture/services.likec4` in the tree,\n // `loam subsystem new platform` printed \"architecture/services.likec4\n // updated\" and destroyed it, first line and all. One predicate, one\n // sentence, shared from beneath every writer\n // (`core/repo/tree/render/service/header.ts`).\n //\n // WHY THE WHOLE OPERATION AND NOT JUST THE FILE. `sync` can decline one file\n // and still do the rest, because nothing else it writes depends on that file\n // and every part of it is idempotent. This window cannot: its entire reason\n // for existing is that the generated files land in the SAME commit as the\n // change to the tree, so completing the structural half and reporting the\n // file as refused would MANUFACTURE the `subsystem.views-stale` state the\n // banner above says this window refuses to leave between two commits — and\n // it would do so irreversibly, since `move` and `rename` also stage their\n // renames in git. A half-done move nobody asked for is worse than a refusal\n // a person can act on, and a refusal is re-runnable the moment the authored\n // file is out of the reserved path.\n //\n // It runs AFTER `plan`, which writes nothing and has already answered the\n // verb-specific refusals (`unknown-target`, `subsystem-not-empty`,\n // `move-uncommitted`): a caller whose name is wrong AND whose tree holds an\n // authored views file is better told about the name.\n const authored = await authoredGeneratedPaths([expected.subsystems.path, expected.services.path]);\n if (authored.length > 0) {\n fail(\n json,\n // The code is `already-exists` and not a new one because it is exactly\n // what that code has always meant here — \"refusing to create over\n // something that already exists, because overwriting would destroy work\n // nobody asked to lose\" — and it is already this command's documented\n // refusal for the flat namespace. A second code for the same sentence\n // would be a second branch an agent has to learn for no new decision.\n \"already-exists\",\n authored.map((path) => authoredRefusal(repoPath(docsDir, path))).join(\" \") +\n \" NOTHING was written and nothing was renamed: this command lands the generated views file in the same \" +\n \"commit as the change to the tree, so it refuses whole rather than leaving the tree changed — and, for \" +\n \"`move` and `rename`, renames staged in git — with a views file it may not write.\",\n );\n return;\n }\n const writes = [...txn.writes];\n // BOTH generated files, in the ONE commit that changes the tree. The\n // per-service file is not a bystander here: its view titles are\n // `<subsystem path> / <service id>`, so every `move` and `rename` restales\n // it exactly as it restales the subsystem views, and leaving it for a later\n // `sync` is the failing-between-two-commits state this window exists to\n // refuse.\n const views: ViewsActions = {\n subsystems: await stageGenerated(expected.subsystems, writes, false),\n // PROTECTED when the map did not parse as a project, and only this file\n // is: `renderSubsystemViews` answers null off the TREE, which is readable\n // either way, while `renderServiceOverviews` answers null whenever no\n // service resolves to an element — which is what an unreadable map looks\n // like from here. Under `known: false` a null render is therefore not\n // evidence of absence, and writing it would delete the whole file over a\n // sibling document that does not parse. A verb still rewrites a file it\n // CAN render (the tolerant arm this window has always taken), and the\n // person is told by the note at the foot of this function.\n services: await stageGenerated(expected.services, writes, !expected.known),\n };\n const staged = await stageWrites(writes);\n const committed = await commitStaged(\n {\n root: docsDir,\n command: \"subsystem\",\n rerun: \"loam subsystem sync\",\n target: txn.target,\n ...(txn.moves === undefined ? {} : { moves: txn.moves }),\n },\n staged,\n txn.what,\n );\n if (!committed.ok) {\n const code =\n committed.code === \"rollback-incomplete\"\n ? committed.code\n : committed.raced\n ? (txn.racedCode ?? txn.failedCode ?? committed.code)\n : (txn.failedCode ?? committed.code);\n fail(json, code, committed.message);\n return;\n }\n await txn.report(views, recovered);\n // A verb CANNOT leave this file behind the tree — its bytes depend on the\n // placement the verb just changed, so the tolerant render lands even when the\n // map is unreadable and the file loses its includes. `sync` may decline that\n // rewrite (`action: \"blocked\"`); a verb may not, so the person is told\n // instead, and the same note names the run that puts the includes back.\n if (!json && !expected.known) console.log(LANDSCAPE_UNREADABLE_NOTE);\n } finally {\n await releaseLock();\n }\n}\n\n/**\n * Compare ONE generated file against what it must say, append the write when\n * they differ, and name what happened.\n *\n * The compare is over CONTENT, not bytes — `viewsAgree` carries the reason,\n * beside the generator that mints them: an ordinary Windows clone hands a\n * generated file back with CRLF and not one fact in it changed, and without\n * this every subsystem move on such a clone folded a rewrite that changed\n * nothing into its transaction and journalled it. What gets WRITTEN is still\n * the LF render as a Buffer, so the decode here is a comparison only.\n *\n * `protect` is the caller's statement that a NULL render is not evidence: the\n * file is left exactly as it is rather than deleted. It is never a licence to\n * skip a real rewrite — a file loam can render is rendered, which is the\n * difference between this window and `../sync.ts`'s `blocked`.\n */\nasync function stageGenerated(file: ExpectedFile, writes: PlannedWrite[], protect: boolean): Promise<ViewsAction> {\n const current = existsSync(file.path) ? await readFile(file.path, \"utf8\") : null;\n if (viewsAgree(current, file.content)) return \"current\";\n if (protect && file.content === null) return \"current\";\n const bytes = file.content === null ? null : Buffer.from(file.content, \"utf8\");\n writes.push({ path: file.path, content: bytes });\n return current === null ? \"created\" : bytes === null ? \"removed\" : \"updated\";\n}\n"]}
@@ -1,20 +1,31 @@
1
1
  /**
2
- * What the generated views file MUST say — the shared answer every subsystem
2
+ * What the generated views files MUST say — the shared answer every subsystem
3
3
  * writer and `subsystem sync` render from. A module beneath the verbs (in the
4
4
  * txn/ subpackage) because the commit window and sync both consume it, and a
5
5
  * second spelling of "what should the views say" would be a second chance to
6
6
  * disagree with `validate`'s staleness check.
7
+ *
8
+ * BOTH generated files come out of ONE read of the map, and that is the reason
9
+ * this module answers about the pair rather than about one file: they are
10
+ * rendered from the same parsed `architecture/` project, they are repaired by
11
+ * the same command, and they must land in the same commit — a verb that changed
12
+ * a service's placement changed a subsystem view's members AND that service's
13
+ * overview TITLE (`<subsystem path> / <service id>`), so shipping one without
14
+ * the other is the "stale between two commits" state the generated files'
15
+ * contract exists to refuse. Reading the project twice would also parse the
16
+ * whole fleet map twice per run for one answer.
7
17
  */
8
18
  import { existsSync } from "node:fs";
9
19
  import { relative, resolve, sep } from "node:path";
10
20
  import { loadFile } from "../../../core/c4/likec4.js";
11
21
  import { architectureProjectDocuments, loadArchitecture } from "../../../core/c4/project/architecture.js";
12
22
  import { serviceDirOf } from "../../../core/kernel/ids/dirs.js";
13
- import { landscapePath } from "../../../core/repo/paths.js";
14
- import { renderSubsystemViews } from "../../../core/repo/tree/render/views.js";
23
+ import { landscapePath, serviceViewsPath, subsystemViewsPath } from "../../../core/repo/paths.js";
24
+ import { renderServiceOverviews, renderSubsystemViews, } from "../../../core/repo/tree/render/views.js";
15
25
  /**
16
- * The elements and declared global style ids of the whole `architecture/`
17
- * PROJECT — the member join, and the one line a view may borrow a palette
26
+ * The elements, the relationships and the declared global style ids of the whole
27
+ * `architecture/` PROJECT — the member join, the edges every per-service view
28
+ * computes its peer set from, and the one line a view may borrow a palette
18
29
  * with — with the provenance of that answer beside it.
19
30
  *
20
31
  * The PROJECT, not the landscape file alone, and that is a correctness rule
@@ -54,8 +65,11 @@ import { renderSubsystemViews } from "../../../core/repo/tree/render/views.js";
54
65
  */
55
66
  export async function landscapeFacts(docsDir) {
56
67
  const path = landscapePath(docsDir);
68
+ // No map at all: no elements and NO EDGES, said outright. `relationships` is
69
+ // required of every `MapFacts` precisely so an empty answer has to be written
70
+ // down rather than dropped — see the type.
57
71
  if (!existsSync(path))
58
- return { facts: { elements: [] }, known: true };
72
+ return { facts: { elements: [], relationships: [] }, known: true };
59
73
  try {
60
74
  const doc = await loadArchitecture(docsDir);
61
75
  // The whole document on success, so the render reads the same record
@@ -81,7 +95,23 @@ export async function landscapeFacts(docsDir) {
81
95
  // cannot merge them (that is what failed one line up), but a document that
82
96
  // parses alone certainly declares the style groups it declares.
83
97
  if (file.errors.length === 0) {
84
- return { facts: { elements: file.elements, globalStyles: await declaredStyles(docsDir, file) }, known: false };
98
+ // THE LANDSCAPE'S OWN RELATIONSHIPS TRAVEL WITH ITS ELEMENTS. This arm
99
+ // composes a record rather than handing the document across, because the
100
+ // style census below is widened past what this one document declares — and
101
+ // the first version of it listed the fields it happened to be thinking
102
+ // about and dropped the edges the document was carrying all along. That is
103
+ // silent: every per-service view then renders with no peers, the service
104
+ // and its own interior and nothing it touches, and no check anywhere says
105
+ // so. `MapFacts` now REQUIRES the field for exactly this reason; the copy
106
+ // stays explicit so the widening above it stays visible.
107
+ return {
108
+ facts: {
109
+ elements: file.elements,
110
+ relationships: file.relationships,
111
+ globalStyles: await declaredStyles(docsDir, file),
112
+ },
113
+ known: false,
114
+ };
85
115
  }
86
116
  }
87
117
  catch {
@@ -129,7 +159,20 @@ async function declaredStyles(docsDir, landscape) {
129
159
  }
130
160
  export async function expectedViews(docsDir, tree) {
131
161
  const read = await landscapeFacts(docsDir);
132
- return { known: read.known, content: renderSubsystemViews(tree, read.facts ?? { elements: [] }) };
162
+ // The parsed DOCUMENT goes across, never a literal composed here: `MapFacts`
163
+ // carries the relationships the per-service peer sets are computed from, and a
164
+ // caller that dropped them would render every service view peerless — the
165
+ // service and its interior, and nothing it touches — with no error anywhere.
166
+ // The type now REQUIRES the field so that mistake cannot be made silently
167
+ // (`render/views.ts` records what it cost); this last-resort literal is the one
168
+ // place an empty answer is the TRUE one — loam could not read the map at
169
+ // all — and it says so rather than leaving the field off.
170
+ const map = read.facts ?? { elements: [], relationships: [] };
171
+ return {
172
+ known: read.known,
173
+ subsystems: { path: subsystemViewsPath(docsDir), content: renderSubsystemViews(tree, map) },
174
+ services: { path: serviceViewsPath(docsDir), content: renderServiceOverviews(tree, map) },
175
+ };
133
176
  }
134
177
  /**
135
178
  * The one line a person is owed when the map cannot be read — the same sentence
@@ -1 +1 @@
1
- {"version":3,"file":"views.js","sourceRoot":"","sources":["../../../../src/commands/subsystem/txn/views.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * What the generated views file MUST say — the shared answer every subsystem\n * writer and `subsystem sync` render from. A module beneath the verbs (in the\n * txn/ subpackage) because the commit window and sync both consume it, and a\n * second spelling of \"what should the views say\" would be a second chance to\n * disagree with `validate`'s staleness check.\n */\nimport { existsSync } from \"node:fs\";\nimport { relative, resolve, sep } from \"node:path\";\nimport { loadFile } from \"../../../core/c4/likec4.js\";\nimport { architectureProjectDocuments, loadArchitecture } from \"../../../core/c4/project/architecture.js\";\nimport { serviceDirOf, type DocsDir } from \"../../../core/kernel/ids/dirs.js\";\nimport { landscapePath } from \"../../../core/repo/paths.js\";\nimport { type MapFacts, renderSubsystemViews } from \"../../../core/repo/tree/render/views.js\";\nimport type { FleetTree } from \"../../../core/repo/tree/walk.js\";\n\n/** What one read of the map yielded, and whether it came from the whole project. */\nexport interface LandscapeRead {\n /** The element join and the style census, or null when neither reading was available. */\n facts: MapFacts | null;\n /**\n * The `architecture/` PROJECT parsed. FALSE means the facts below, if there\n * are any, were assembled document by document — the member join from the\n * landscape file alone (so an element a use-case document binds is missing\n * from it), the style census from every document that parses on its own.\n */\n known: boolean;\n}\n\n/**\n * The elements and declared global style ids of the whole `architecture/`\n * PROJECT — the member join, and the one line a view may borrow a palette\n * with — with the provenance of that answer beside it.\n *\n * The PROJECT, not the landscape file alone, and that is a correctness rule\n * rather than a preference: `validate` grades this file against\n * `loadArchitecture` — the landscape merged with every\n * `architecture/usecases/*.likec4`, which may `extend` the model and bind\n * elements of its own. While this side read the landscape alone, a use-case\n * document declaring one element bound to a filed service was enough to make\n * `sync` answer `current` while `validate --all` reported\n * `subsystem.views-stale` and named `sync` as the repair — a loop no command\n * could clear, reproduced on `examples/docs` before this call changed. Two\n * readers of \"what should the views say\" must read the same documents, or the\n * shared render in `core/repo/tree/render/views.ts` is shared in name only.\n *\n * WHY THE FALLBACK AND THE FLAG ARE TWO ANSWERS RATHER THAN ONE. The fallback\n * exists because one broken use-case document must not cost the render its\n * member join: the landscape's own elements are still the truth about who\n * belongs in which subsystem, and re-rendering the file without them takes every\n * `include` line off it (verification 2026-09-04, W5). But the STYLE CENSUS is\n * not the landscape file's alone — SCHEMA blesses a palette in any `.likec4`\n * under `architecture/` — so the fallback takes it over EVERY document that\n * parses on its own (`declaredStyles`). Reading it off the landscape alone was\n * enough to drop `global style subsystems` from a good generated file whenever\n * the palette sat in a sibling and an unrelated sibling was broken\n * (re-verification 2026-09-04, area C item 8).\n *\n * `known` still means the PROJECT parsed, and nothing weaker, because the MEMBER\n * JOIN is what it licences: a use-case document that binds an element of its own\n * contributes to the join and cannot be read document by document. It is\n * `../sync.ts`'s licence to leave an existing file alone.\n *\n * An ABSENT map is not a failure: there is nothing to read, the empty facts are\n * the truth, and the tolerant render is what a fleet being scaffolded needs. An\n * EMPTY project is no longer reachable here — `architectureProjectDocuments`\n * keeps the landscape whatever the root `exclude` says, and its banner records\n * what an empty one cost.\n */\nexport async function landscapeFacts(docsDir: DocsDir): Promise<LandscapeRead> {\n const path = landscapePath(docsDir);\n if (!existsSync(path)) return { facts: { elements: [] }, known: true };\n try {\n const doc = await loadArchitecture(docsDir);\n // The whole document on success, so the render reads the same record\n // `validate --all` grades against; `globalStyles` rides along without\n // being named here, and a field the loaders add later does too.\n if (doc.errors.length === 0) return { facts: doc, known: true };\n } catch {\n // A throw and a parse error are the same evidence: fall through to the file.\n }\n try {\n const file = await loadFile(path);\n // THE STYLE CENSUS IS TAKEN OVER EVERY DOCUMENT, not over the landscape, and\n // that is what makes the fallback safe for a verb that has to write. A\n // palette declared in a SIBLING under `architecture/` is invisible to a read\n // of the landscape alone, so a `subsystem new` run standing beside one\n // unrelated broken sibling silently dropped `global style subsystems` from a\n // good generated file — the landscape parsed, the element join survived, and\n // nothing graded the loss because `sync` answers `blocked` for as long as the\n // broken file stands (re-verification 2026-09-04, area C item 8). Reading\n // each document ON ITS OWN is the widening the fallback is entitled to: it\n // cannot merge them (that is what failed one line up), but a document that\n // parses alone certainly declares the style groups it declares.\n if (file.errors.length === 0) {\n return { facts: { elements: file.elements, globalStyles: await declaredStyles(docsDir, file) }, known: false };\n }\n } catch {\n // Unreadable bytes; the caller is told loam could not say.\n }\n return { facts: null, known: false };\n}\n\n/**\n * Every global style id declared by an `architecture/` document that parses ON\n * ITS OWN, the landscape's included — the union, sorted, so the render is a\n * function of the set rather than of readdir order.\n *\n * The document set is `architectureProjectDocuments`: the same list the PROJECT\n * read uses, so the fallback can never consider a file the merged read would\n * have excluded — the generated views file itself, or one the root `exclude`\n * hides from the renderer. A document that does not parse alone contributes\n * nothing and is not an error here: it is named by `loam validate --all`, and\n * the note this module carries already tells the reader the census came from\n * outside a project read.\n */\nasync function declaredStyles(docsDir: DocsDir, landscape: MapFacts): Promise<string[]> {\n const ids = new Set(landscape.globalStyles ?? []);\n let paths: string[];\n try {\n paths = await architectureProjectDocuments(docsDir);\n } catch {\n return [...ids].sort();\n }\n const map = resolve(landscapePath(docsDir));\n for (const path of paths) {\n if (resolve(path) === map) continue;\n try {\n const doc = await loadFile(path);\n if (doc.errors.length === 0) for (const id of doc.globalStyles ?? []) ids.add(id);\n } catch {\n // One unreadable sibling costs its own styles and nothing else.\n }\n }\n return [...ids].sort();\n}\n\n/**\n * The bytes the views file must hold for this tree, and whether the map they\n * were computed from was read as a PROJECT.\n *\n * The content is always renderable — from the landscape file alone, or from\n * nothing — because a fleet with no generated file yet must still get one, and\n * the tree-changing verbs must still land theirs in the commit that changes the\n * tree. `known: false` is the caller's licence to leave an EXISTING file alone\n * instead (`../sync.ts`, `action: \"blocked\"`).\n */\nexport interface ExpectedViews {\n /**\n * The `architecture/` PROJECT was read. False means the content was rendered\n * from the landscape file alone — or from nothing at all — so its style census\n * is not one an existing file may be rewritten from.\n */\n known: boolean;\n /** The bytes the file must hold, or null for \"must be absent\". */\n content: string | null;\n}\n\nexport async function expectedViews(docsDir: DocsDir, tree: FleetTree): Promise<ExpectedViews> {\n const read = await landscapeFacts(docsDir);\n return { known: read.known, content: renderSubsystemViews(tree, read.facts ?? { elements: [] }) };\n}\n\n/**\n * The one line a person is owed when the map cannot be read — the same sentence\n * from `../sync.ts` (which then leaves an existing file alone) and from a\n * tree-changing verb (which renders the tolerant shape anyway, because its own\n * contract is that the file lands in the verb's commit). It names the gating\n * surface rather than repeating its findings: `loam validate --all` is where the\n * broken document is named. It lives here, beside the facts it is about, because\n * both callers are on this side of the `txn/` boundary and a constant in\n * `../sync.ts` would point the package edge back up.\n */\nexport const LANDSCAPE_UNREADABLE_NOTE =\n \"note: architecture/ does not parse as one project, so the element join is whatever \" +\n \"architecture/landscape.likec4 alone says and the style census is the union of the documents that parse on \" +\n \"their own — fix the map (`loam validate --all` names the file), then re-run `loam subsystem sync`.\";\n\n/**\n * The tree AS THE RENAMES LEAVE IT, computed in memory: every dir under a\n * moved root is remapped by prefix, and each entry's placement chain is\n * re-derived from its remapped dir. The views file is rendered from this, so\n * the commit lands file and renames agreeing — never from a re-walk that\n * would have to happen between two states. Beside `expectedViews` because the\n * two are one promise: what the commit renames and what the file says must\n * come from the same in-memory answer.\n */\nexport function movedTree(tree: FleetTree, renames: { from: string; to: string }[], servicesRoot: string): FleetTree {\n const remap = (dir: string): string => {\n for (const r of renames) {\n if (dir === r.from || dir.startsWith(r.from + sep)) return r.to + dir.slice(r.from.length);\n }\n return dir;\n };\n const chainOf = (dir: string): string[] => relative(servicesRoot, dir).split(sep);\n return {\n findings: tree.findings,\n services: tree.services.map((s) => {\n const dir = remap(s.dir);\n // The constructor call records provenance: a readdir-established\n // directory carried through a rename this same commit performs.\n return { ...s, dir: serviceDirOf(dir), subsystem: chainOf(dir).slice(0, -1) };\n }),\n subsystems: tree.subsystems.map((s) => {\n const dir = remap(s.dir);\n return { ...s, dir, path: chainOf(dir) };\n }),\n };\n}\n"]}
1
+ {"version":3,"file":"views.js","sourceRoot":"","sources":["../../../../src/commands/subsystem/txn/views.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * What the generated views files MUST say — the shared answer every subsystem\n * writer and `subsystem sync` render from. A module beneath the verbs (in the\n * txn/ subpackage) because the commit window and sync both consume it, and a\n * second spelling of \"what should the views say\" would be a second chance to\n * disagree with `validate`'s staleness check.\n *\n * BOTH generated files come out of ONE read of the map, and that is the reason\n * this module answers about the pair rather than about one file: they are\n * rendered from the same parsed `architecture/` project, they are repaired by\n * the same command, and they must land in the same commit — a verb that changed\n * a service's placement changed a subsystem view's members AND that service's\n * overview TITLE (`<subsystem path> / <service id>`), so shipping one without\n * the other is the \"stale between two commits\" state the generated files'\n * contract exists to refuse. Reading the project twice would also parse the\n * whole fleet map twice per run for one answer.\n */\nimport { existsSync } from \"node:fs\";\nimport { relative, resolve, sep } from \"node:path\";\nimport { loadFile } from \"../../../core/c4/likec4.js\";\nimport { architectureProjectDocuments, loadArchitecture } from \"../../../core/c4/project/architecture.js\";\nimport { serviceDirOf, type DocsDir } from \"../../../core/kernel/ids/dirs.js\";\nimport { landscapePath, serviceViewsPath, subsystemViewsPath } from \"../../../core/repo/paths.js\";\nimport {\n type MapFacts,\n renderServiceOverviews,\n renderSubsystemViews,\n} from \"../../../core/repo/tree/render/views.js\";\nimport type { FleetTree } from \"../../../core/repo/tree/walk.js\";\n\n/** What one read of the map yielded, and whether it came from the whole project. */\nexport interface LandscapeRead {\n /** The element join and the style census, or null when neither reading was available. */\n facts: MapFacts | null;\n /**\n * The `architecture/` PROJECT parsed. FALSE means the facts below, if there\n * are any, were assembled document by document — the member join from the\n * landscape file alone (so an element a use-case document binds is missing\n * from it), the style census from every document that parses on its own.\n */\n known: boolean;\n}\n\n/**\n * The elements, the relationships and the declared global style ids of the whole\n * `architecture/` PROJECT — the member join, the edges every per-service view\n * computes its peer set from, and the one line a view may borrow a palette\n * with — with the provenance of that answer beside it.\n *\n * The PROJECT, not the landscape file alone, and that is a correctness rule\n * rather than a preference: `validate` grades this file against\n * `loadArchitecture` — the landscape merged with every\n * `architecture/usecases/*.likec4`, which may `extend` the model and bind\n * elements of its own. While this side read the landscape alone, a use-case\n * document declaring one element bound to a filed service was enough to make\n * `sync` answer `current` while `validate --all` reported\n * `subsystem.views-stale` and named `sync` as the repair — a loop no command\n * could clear, reproduced on `examples/docs` before this call changed. Two\n * readers of \"what should the views say\" must read the same documents, or the\n * shared render in `core/repo/tree/render/views.ts` is shared in name only.\n *\n * WHY THE FALLBACK AND THE FLAG ARE TWO ANSWERS RATHER THAN ONE. The fallback\n * exists because one broken use-case document must not cost the render its\n * member join: the landscape's own elements are still the truth about who\n * belongs in which subsystem, and re-rendering the file without them takes every\n * `include` line off it (verification 2026-09-04, W5). But the STYLE CENSUS is\n * not the landscape file's alone — SCHEMA blesses a palette in any `.likec4`\n * under `architecture/` — so the fallback takes it over EVERY document that\n * parses on its own (`declaredStyles`). Reading it off the landscape alone was\n * enough to drop `global style subsystems` from a good generated file whenever\n * the palette sat in a sibling and an unrelated sibling was broken\n * (re-verification 2026-09-04, area C item 8).\n *\n * `known` still means the PROJECT parsed, and nothing weaker, because the MEMBER\n * JOIN is what it licences: a use-case document that binds an element of its own\n * contributes to the join and cannot be read document by document. It is\n * `../sync.ts`'s licence to leave an existing file alone.\n *\n * An ABSENT map is not a failure: there is nothing to read, the empty facts are\n * the truth, and the tolerant render is what a fleet being scaffolded needs. An\n * EMPTY project is no longer reachable here — `architectureProjectDocuments`\n * keeps the landscape whatever the root `exclude` says, and its banner records\n * what an empty one cost.\n */\nexport async function landscapeFacts(docsDir: DocsDir): Promise<LandscapeRead> {\n const path = landscapePath(docsDir);\n // No map at all: no elements and NO EDGES, said outright. `relationships` is\n // required of every `MapFacts` precisely so an empty answer has to be written\n // down rather than dropped — see the type.\n if (!existsSync(path)) return { facts: { elements: [], relationships: [] }, known: true };\n try {\n const doc = await loadArchitecture(docsDir);\n // The whole document on success, so the render reads the same record\n // `validate --all` grades against; `globalStyles` rides along without\n // being named here, and a field the loaders add later does too.\n if (doc.errors.length === 0) return { facts: doc, known: true };\n } catch {\n // A throw and a parse error are the same evidence: fall through to the file.\n }\n try {\n const file = await loadFile(path);\n // THE STYLE CENSUS IS TAKEN OVER EVERY DOCUMENT, not over the landscape, and\n // that is what makes the fallback safe for a verb that has to write. A\n // palette declared in a SIBLING under `architecture/` is invisible to a read\n // of the landscape alone, so a `subsystem new` run standing beside one\n // unrelated broken sibling silently dropped `global style subsystems` from a\n // good generated file — the landscape parsed, the element join survived, and\n // nothing graded the loss because `sync` answers `blocked` for as long as the\n // broken file stands (re-verification 2026-09-04, area C item 8). Reading\n // each document ON ITS OWN is the widening the fallback is entitled to: it\n // cannot merge them (that is what failed one line up), but a document that\n // parses alone certainly declares the style groups it declares.\n if (file.errors.length === 0) {\n // THE LANDSCAPE'S OWN RELATIONSHIPS TRAVEL WITH ITS ELEMENTS. This arm\n // composes a record rather than handing the document across, because the\n // style census below is widened past what this one document declares — and\n // the first version of it listed the fields it happened to be thinking\n // about and dropped the edges the document was carrying all along. That is\n // silent: every per-service view then renders with no peers, the service\n // and its own interior and nothing it touches, and no check anywhere says\n // so. `MapFacts` now REQUIRES the field for exactly this reason; the copy\n // stays explicit so the widening above it stays visible.\n return {\n facts: {\n elements: file.elements,\n relationships: file.relationships,\n globalStyles: await declaredStyles(docsDir, file),\n },\n known: false,\n };\n }\n } catch {\n // Unreadable bytes; the caller is told loam could not say.\n }\n return { facts: null, known: false };\n}\n\n/**\n * Every global style id declared by an `architecture/` document that parses ON\n * ITS OWN, the landscape's included — the union, sorted, so the render is a\n * function of the set rather than of readdir order.\n *\n * The document set is `architectureProjectDocuments`: the same list the PROJECT\n * read uses, so the fallback can never consider a file the merged read would\n * have excluded — the generated views file itself, or one the root `exclude`\n * hides from the renderer. A document that does not parse alone contributes\n * nothing and is not an error here: it is named by `loam validate --all`, and\n * the note this module carries already tells the reader the census came from\n * outside a project read.\n */\nasync function declaredStyles(docsDir: DocsDir, landscape: MapFacts): Promise<string[]> {\n const ids = new Set(landscape.globalStyles ?? []);\n let paths: string[];\n try {\n paths = await architectureProjectDocuments(docsDir);\n } catch {\n return [...ids].sort();\n }\n const map = resolve(landscapePath(docsDir));\n for (const path of paths) {\n if (resolve(path) === map) continue;\n try {\n const doc = await loadFile(path);\n if (doc.errors.length === 0) for (const id of doc.globalStyles ?? []) ids.add(id);\n } catch {\n // One unreadable sibling costs its own styles and nothing else.\n }\n }\n return [...ids].sort();\n}\n\n/**\n * ONE generated file: where it lives, and the bytes it must hold — `null` for\n * \"this file must not exist\", which is a state of its own and not a blank file\n * (`render/stale.ts` keeps the same three apart on the reading side).\n */\nexport interface ExpectedFile {\n path: string;\n content: string | null;\n}\n\n/**\n * The bytes BOTH generated files must hold for this tree, and whether the map\n * they were computed from was read as a PROJECT.\n *\n * The content is always renderable — from the landscape file alone, or from\n * nothing — because a fleet with no generated file yet must still get one, and\n * the tree-changing verbs must still land theirs in the commit that changes the\n * tree. `known: false` is the caller's licence to leave an EXISTING file alone\n * instead (`../sync.ts`, `action: \"blocked\"`).\n *\n * NAMED FIELDS, never an array, and never one folded answer: the two files go\n * stale for different reasons and each is named in its own finding and its own\n * report line, so a caller that could not tell them apart would have nothing to\n * say. `subsystem.views-stale` carries the path precisely so one code covers\n * both honestly.\n *\n * THE SERVICE FILE IS THE ONE THE MAP CAN EMPTY. `renderSubsystemViews` answers\n * null only when the TREE has no subsystems — a fact no map can change — while\n * `renderServiceOverviews` answers null whenever no service resolves to an\n * element, which is exactly what an unreadable map looks like from here (`facts`\n * falls back to an empty element list and an empty edge list alike). So for that\n * file \"must be absent\" and \"loam could not read the map\" are the same bytes,\n * and only `known` tells them apart:\n * a writer must not treat a null service render under `known: false` as evidence\n * that the file should go. Both callers hold that line, and say so.\n */\nexport interface ExpectedViews {\n /**\n * The `architecture/` PROJECT was read. False means the content was rendered\n * from the landscape file alone — or from nothing at all — so its style census\n * is not one an existing file may be rewritten from, and its element join is\n * not one the per-service file may be deleted on.\n */\n known: boolean;\n /** `architecture/subsystems.likec4` — one view per subsystem, from the tree. */\n subsystems: ExpectedFile;\n /** `architecture/services.likec4` — one overview view per service the map draws. */\n services: ExpectedFile;\n}\n\nexport async function expectedViews(docsDir: DocsDir, tree: FleetTree): Promise<ExpectedViews> {\n const read = await landscapeFacts(docsDir);\n // The parsed DOCUMENT goes across, never a literal composed here: `MapFacts`\n // carries the relationships the per-service peer sets are computed from, and a\n // caller that dropped them would render every service view peerless — the\n // service and its interior, and nothing it touches — with no error anywhere.\n // The type now REQUIRES the field so that mistake cannot be made silently\n // (`render/views.ts` records what it cost); this last-resort literal is the one\n // place an empty answer is the TRUE one — loam could not read the map at\n // all — and it says so rather than leaving the field off.\n const map = read.facts ?? { elements: [], relationships: [] };\n return {\n known: read.known,\n subsystems: { path: subsystemViewsPath(docsDir), content: renderSubsystemViews(tree, map) },\n services: { path: serviceViewsPath(docsDir), content: renderServiceOverviews(tree, map) },\n };\n}\n\n/**\n * The one line a person is owed when the map cannot be read — the same sentence\n * from `../sync.ts` (which then leaves an existing file alone) and from a\n * tree-changing verb (which renders the tolerant shape anyway, because its own\n * contract is that the file lands in the verb's commit). It names the gating\n * surface rather than repeating its findings: `loam validate --all` is where the\n * broken document is named. It lives here, beside the facts it is about, because\n * both callers are on this side of the `txn/` boundary and a constant in\n * `../sync.ts` would point the package edge back up.\n */\nexport const LANDSCAPE_UNREADABLE_NOTE =\n \"note: architecture/ does not parse as one project, so the element join is whatever \" +\n \"architecture/landscape.likec4 alone says and the style census is the union of the documents that parse on \" +\n \"their own — fix the map (`loam validate --all` names the file), then re-run `loam subsystem sync`.\";\n\n/**\n * The tree AS THE RENAMES LEAVE IT, computed in memory: every dir under a\n * moved root is remapped by prefix, and each entry's placement chain is\n * re-derived from its remapped dir. The views file is rendered from this, so\n * the commit lands file and renames agreeing — never from a re-walk that\n * would have to happen between two states. Beside `expectedViews` because the\n * two are one promise: what the commit renames and what the file says must\n * come from the same in-memory answer.\n */\nexport function movedTree(tree: FleetTree, renames: { from: string; to: string }[], servicesRoot: string): FleetTree {\n const remap = (dir: string): string => {\n for (const r of renames) {\n if (dir === r.from || dir.startsWith(r.from + sep)) return r.to + dir.slice(r.from.length);\n }\n return dir;\n };\n const chainOf = (dir: string): string[] => relative(servicesRoot, dir).split(sep);\n return {\n findings: tree.findings,\n services: tree.services.map((s) => {\n const dir = remap(s.dir);\n // The constructor call records provenance: a readdir-established\n // directory carried through a rename this same commit performs.\n return { ...s, dir: serviceDirOf(dir), subsystem: chainOf(dir).slice(0, -1) };\n }),\n subsystems: tree.subsystems.map((s) => {\n const dir = remap(s.dir);\n return { ...s, dir, path: chainOf(dir) };\n }),\n };\n}\n"]}
@@ -10,11 +10,13 @@
10
10
  * concerned.
11
11
  *
12
12
  * Resolution is against the SERVICE'S OWN model — deliberately never the
13
- * landscape. A private datastore lives nested inside the service element
14
- * (`landscape.datastore-private` is the check pushing it there), so the
15
- * landscape carries only what crosses the service boundary, while the model
16
- * carries everything the service touches — and that is the set the on-call
17
- * file has to agree with. An id resolves by the same names every other join
13
+ * landscape. A store one service owns may be nested inside that service's
14
+ * element or drawn as a fleet-level peer, whichever the team chose (loam grades
15
+ * neither: `landscape.datastore-private`, which used to push it into the first,
16
+ * was retired 2026-09-07). Only the nested placement puts it in the landscape's
17
+ * interior at all, so the landscape is not a set this check could rely on, while
18
+ * the model carries everything the service touches — and that is the set the
19
+ * on-call file has to agree with. An id resolves by the same names every other join
18
20
  * uses: the element's likec4 id, its `metadata { service }` binding, or its
19
21
  * title. Exact match, no case folding — a fold would be a permanent
20
22
  * equivalence semantic no other loam join has, able to silently join two
@@ -45,7 +47,8 @@ export function healthDependencyFindings(deps) {
45
47
  message: `${service}: health.yaml declares dependency '${dep}' but nothing in ` +
46
48
  `${modelPath} answers to that name — either a dependency nobody ` +
47
49
  `modelled, or one that no longer exists. Model it (an external system as its own element ` +
48
- `with the edge this service actually has; a private store as a nested container), or ` +
50
+ `with the edge this service actually has; a store this service owns as a nested container ` +
51
+ `or as its own element, whichever the map already does), or ` +
49
52
  `delete the entry` +
50
53
  (close.length > 0 ? `. Did you mean: ${close.join(", ")}?` : ""),
51
54
  });
@@ -1 +1 @@
1
- {"version":3,"file":"health-deps.js","sourceRoot":"","sources":["../../../../src/commands/validate/checks/health-deps.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;GAsBG;AACH,OAAO,EAAE,QAAQ,EAAE,MAAM,0BAA0B,CAAC;AACpD,OAAO,EAAa,MAAM,4BAA4B,CAAC;AACvD,OAAO,EAAE,cAAc,EAAE,MAAM,qCAAqC,CAAC;AACrE,OAAO,EAAgB,MAAM,oCAAoC,CAAC;AAkBlE,MAAM,UAAU,wBAAwB,CAAC,IAAgB;IACvD,MAAM,EAAE,OAAO,EAAE,YAAY,EAAE,QAAQ,EAAE,SAAS,EAAE,GAAG,IAAI,CAAC;IAC5D,yEAAyE;IACzE,4EAA4E;IAC5E,gCAAgC;IAChC,IAAI,QAAQ,CAAC,MAAM,KAAK,CAAC;QAAE,OAAO,EAAE,CAAC;IACrC,MAAM,QAAQ,GAAc,EAAE,CAAC;IAC/B,KAAK,MAAM,GAAG,IAAI,YAAY,EAAE,CAAC;QAC/B,IAAI,QAAQ,CAAC,IAAI,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,EAAE,KAAK,GAAG,IAAI,cAAc,CAAC,CAAC,CAAC,KAAK,GAAG,CAAC;YAAE,SAAS;QAC9E,MAAM,IAAI,GAAG,CAAC,GAAG,IAAI,GAAG,CAAC,QAAQ,CAAC,OAAO,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,CAAC,EAAE,EAAE,cAAc,CAAC,CAAC,CAAW,CAAC,CAAC,CAAC,CAAC,CAAC;QACxF,MAAM,KAAK,GAAG,QAAQ,CAAC,GAAG,EAAE,IAAI,CAAC,CAAC;QAClC,QAAQ,CAAC,IAAI,CAAC;YACZ,QAAQ,EAAE,MAAM;YAChB,IAAI,EAAE,8BAA8B;YACpC,OAAO,EAAE,OAAO;YAChB,OAAO,EACL,GAAG,OAAO,sCAAsC,GAAG,mBAAmB;gBACtE,GAAG,SAAS,qDAAqD;gBACjE,0FAA0F;gBAC1F,sFAAsF;gBACtF,kBAAkB;gBAClB,CAAC,KAAK,CAAC,MAAM,GAAG,CAAC,CAAC,CAAC,CAAC,mBAAmB,KAAK,CAAC,IAAI,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,CAAC;SACnE,CAAC,CAAC;IACL,CAAC;IACD,OAAO,QAAQ,CAAC;AAClB,CAAC","sourcesContent":["/**\n * The health.yaml → model reconciliation: every id the `dependencies:` block\n * declares must be something this service's OWN model answers to.\n *\n * Without this reconciliation, a disabled optional service registry can leave\n * the model, landscape, arch spec and runbook corrected while health.yaml still\n * declares `serviceRegistry` as a `critical: startup` dependency under a green\n * validator — in the file an on-call engineer reaches for first. Its\n * dependencies block was otherwise free text as far as validation was\n * concerned.\n *\n * Resolution is against the SERVICE'S OWN model — deliberately never the\n * landscape. A private datastore lives nested inside the service element\n * (`landscape.datastore-private` is the check pushing it there), so the\n * landscape carries only what crosses the service boundary, while the model\n * carries everything the service touches — and that is the set the on-call\n * file has to agree with. An id resolves by the same names every other join\n * uses: the element's likec4 id, its `metadata { service }` binding, or its\n * title. Exact match, no case folding — a fold would be a permanent\n * equivalence semantic no other loam join has, able to silently join two\n * genuinely different names; the did-you-mean hint bridges the typo case\n * instead.\n */\nimport { closeIds } from \"../../../core/c4/arch.js\";\nimport { type Elem } from \"../../../core/c4/likec4.js\";\nimport { elementService } from \"../../../core/c4/resolve/service.js\";\nimport { type Finding } from \"../../../core/vocabulary/report.js\";\n\nexport interface HealthDeps {\n service: string;\n /**\n * The resolved path of the service's own `model.likec4` — the file this\n * check reads and the one its message names. Handed in rather than joined\n * from the id: `services/<id>/model.likec4` is right only for an unfiled\n * service, and every sibling finding in this command prints the resolved\n * path of the file it means.\n */\n modelPath: string;\n /** `dependencies:` ids, already muted by the caller when health.yaml is unreadable. */\n dependencies: string[];\n /** The service's own parsed model — empty when absent or invalid, which mutes the check. */\n elements: Elem[];\n}\n\nexport function healthDependencyFindings(deps: HealthDeps): Finding[] {\n const { service, dependencies, elements, modelPath } = deps;\n // An absent or unparsable model already has its own finding, and grading\n // health.yaml against a model nobody could read would manufacture warns out\n // of the wrong file's breakage.\n if (elements.length === 0) return [];\n const findings: Finding[] = [];\n for (const dep of dependencies) {\n if (elements.some((e) => e.id === dep || elementService(e) === dep)) continue;\n const pool = [...new Set(elements.flatMap((e) => [e.id, elementService(e) as string]))];\n const close = closeIds(dep, pool);\n findings.push({\n severity: \"warn\",\n code: \"health.dependency-unmodelled\",\n subject: service,\n message:\n `${service}: health.yaml declares dependency '${dep}' but nothing in ` +\n `${modelPath} answers to that name — either a dependency nobody ` +\n `modelled, or one that no longer exists. Model it (an external system as its own element ` +\n `with the edge this service actually has; a private store as a nested container), or ` +\n `delete the entry` +\n (close.length > 0 ? `. Did you mean: ${close.join(\", \")}?` : \"\"),\n });\n }\n return findings;\n}\n"]}
1
+ {"version":3,"file":"health-deps.js","sourceRoot":"","sources":["../../../../src/commands/validate/checks/health-deps.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;GAwBG;AACH,OAAO,EAAE,QAAQ,EAAE,MAAM,0BAA0B,CAAC;AACpD,OAAO,EAAa,MAAM,4BAA4B,CAAC;AACvD,OAAO,EAAE,cAAc,EAAE,MAAM,qCAAqC,CAAC;AACrE,OAAO,EAAgB,MAAM,oCAAoC,CAAC;AAkBlE,MAAM,UAAU,wBAAwB,CAAC,IAAgB;IACvD,MAAM,EAAE,OAAO,EAAE,YAAY,EAAE,QAAQ,EAAE,SAAS,EAAE,GAAG,IAAI,CAAC;IAC5D,yEAAyE;IACzE,4EAA4E;IAC5E,gCAAgC;IAChC,IAAI,QAAQ,CAAC,MAAM,KAAK,CAAC;QAAE,OAAO,EAAE,CAAC;IACrC,MAAM,QAAQ,GAAc,EAAE,CAAC;IAC/B,KAAK,MAAM,GAAG,IAAI,YAAY,EAAE,CAAC;QAC/B,IAAI,QAAQ,CAAC,IAAI,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,EAAE,KAAK,GAAG,IAAI,cAAc,CAAC,CAAC,CAAC,KAAK,GAAG,CAAC;YAAE,SAAS;QAC9E,MAAM,IAAI,GAAG,CAAC,GAAG,IAAI,GAAG,CAAC,QAAQ,CAAC,OAAO,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,CAAC,EAAE,EAAE,cAAc,CAAC,CAAC,CAAW,CAAC,CAAC,CAAC,CAAC,CAAC;QACxF,MAAM,KAAK,GAAG,QAAQ,CAAC,GAAG,EAAE,IAAI,CAAC,CAAC;QAClC,QAAQ,CAAC,IAAI,CAAC;YACZ,QAAQ,EAAE,MAAM;YAChB,IAAI,EAAE,8BAA8B;YACpC,OAAO,EAAE,OAAO;YAChB,OAAO,EACL,GAAG,OAAO,sCAAsC,GAAG,mBAAmB;gBACtE,GAAG,SAAS,qDAAqD;gBACjE,0FAA0F;gBAC1F,2FAA2F;gBAC3F,6DAA6D;gBAC7D,kBAAkB;gBAClB,CAAC,KAAK,CAAC,MAAM,GAAG,CAAC,CAAC,CAAC,CAAC,mBAAmB,KAAK,CAAC,IAAI,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,CAAC;SACnE,CAAC,CAAC;IACL,CAAC;IACD,OAAO,QAAQ,CAAC;AAClB,CAAC","sourcesContent":["/**\n * The health.yaml → model reconciliation: every id the `dependencies:` block\n * declares must be something this service's OWN model answers to.\n *\n * Without this reconciliation, a disabled optional service registry can leave\n * the model, landscape, arch spec and runbook corrected while health.yaml still\n * declares `serviceRegistry` as a `critical: startup` dependency under a green\n * validator — in the file an on-call engineer reaches for first. Its\n * dependencies block was otherwise free text as far as validation was\n * concerned.\n *\n * Resolution is against the SERVICE'S OWN model — deliberately never the\n * landscape. A store one service owns may be nested inside that service's\n * element or drawn as a fleet-level peer, whichever the team chose (loam grades\n * neither: `landscape.datastore-private`, which used to push it into the first,\n * was retired 2026-09-07). Only the nested placement puts it in the landscape's\n * interior at all, so the landscape is not a set this check could rely on, while\n * the model carries everything the service touches — and that is the set the\n * on-call file has to agree with. An id resolves by the same names every other join\n * uses: the element's likec4 id, its `metadata { service }` binding, or its\n * title. Exact match, no case folding — a fold would be a permanent\n * equivalence semantic no other loam join has, able to silently join two\n * genuinely different names; the did-you-mean hint bridges the typo case\n * instead.\n */\nimport { closeIds } from \"../../../core/c4/arch.js\";\nimport { type Elem } from \"../../../core/c4/likec4.js\";\nimport { elementService } from \"../../../core/c4/resolve/service.js\";\nimport { type Finding } from \"../../../core/vocabulary/report.js\";\n\nexport interface HealthDeps {\n service: string;\n /**\n * The resolved path of the service's own `model.likec4` — the file this\n * check reads and the one its message names. Handed in rather than joined\n * from the id: `services/<id>/model.likec4` is right only for an unfiled\n * service, and every sibling finding in this command prints the resolved\n * path of the file it means.\n */\n modelPath: string;\n /** `dependencies:` ids, already muted by the caller when health.yaml is unreadable. */\n dependencies: string[];\n /** The service's own parsed model — empty when absent or invalid, which mutes the check. */\n elements: Elem[];\n}\n\nexport function healthDependencyFindings(deps: HealthDeps): Finding[] {\n const { service, dependencies, elements, modelPath } = deps;\n // An absent or unparsable model already has its own finding, and grading\n // health.yaml against a model nobody could read would manufacture warns out\n // of the wrong file's breakage.\n if (elements.length === 0) return [];\n const findings: Finding[] = [];\n for (const dep of dependencies) {\n if (elements.some((e) => e.id === dep || elementService(e) === dep)) continue;\n const pool = [...new Set(elements.flatMap((e) => [e.id, elementService(e) as string]))];\n const close = closeIds(dep, pool);\n findings.push({\n severity: \"warn\",\n code: \"health.dependency-unmodelled\",\n subject: service,\n message:\n `${service}: health.yaml declares dependency '${dep}' but nothing in ` +\n `${modelPath} answers to that name — either a dependency nobody ` +\n `modelled, or one that no longer exists. Model it (an external system as its own element ` +\n `with the edge this service actually has; a store this service owns as a nested container ` +\n `or as its own element, whichever the map already does), or ` +\n `delete the entry` +\n (close.length > 0 ? `. Did you mean: ${close.join(\", \")}?` : \"\"),\n });\n }\n return findings;\n}\n"]}
@@ -11,7 +11,7 @@
11
11
  */
12
12
  import {} from "../../../core/c4/likec4.js";
13
13
  import { properAncestorIds } from "../../../core/kernel/ids/fqn/ancestors.js";
14
- import { ACTOR_KINDS, EXTERNAL_TAG } from "../../../core/vocabulary/maturity.js";
14
+ import { ACTOR_KINDS, EXTERNAL_TAG, INFRA_KINDS } from "../../../core/vocabulary/maturity.js";
15
15
  /**
16
16
  * Does this element stand for a `services/<id>/` directory — an explicit
17
17
  * `metadata { service }` binding, or a title naming a real one? The question
@@ -50,7 +50,8 @@ export function serviceLevelElements(elements, services) {
50
50
  }
51
51
  /**
52
52
  * The drawn SYSTEMS: service-level elements minus everything the map itself
53
- * says is not a service. Actor kinds model people; `#external` is somebody
53
+ * says is not a service. Actor kinds model people; INFRA kinds model the
54
+ * datastores, queues and buckets the fleet RUNS; `#external` is somebody
54
55
  * else's system; and an element that CONTAINS a service is a grouping — a
55
56
  * domain, a boundary, an enterprise — not a service nobody adopted. There is
56
57
  * nothing to bind on a grouping and nothing to tag #external, so asking for
@@ -59,12 +60,34 @@ export function serviceLevelElements(elements, services) {
59
60
  * the scorecard's `c4.elements` census counts it — one predicate, two readers,
60
61
  * zero drift.
61
62
  *
63
+ * The INFRA skip is the newest and the one with a measured cost behind it. A
64
+ * `database` the fleet OWNS and draws at fleet level is neither a person nor
65
+ * somebody else's system, so it used to earn `landscape.service-undocumented`
66
+ * — "bind it with `metadata { service '<id>' }`, or tag it `#external`" —
67
+ * whose two remedies are both FALSE of it: there is no directory to bind, and
68
+ * it is ours. Measured on a one-service fixture drawing an owned `database` at
69
+ * fleet level (2026-09-07): that warning plus `landscape.datastore-private`
70
+ * plus `gate.partner-undocumented` on its one consumer, three findings and no
71
+ * correct fix, which is why both `examples/docs` and this repo's own suite
72
+ * tagged an owned Redis `#external` to buy silence.
73
+ *
74
+ * `standsForService` wins over the kind, and that guard is load-bearing rather
75
+ * than defensive: a `database` bound with `metadata { service '<id>' }`, or one
76
+ * whose title names a real `services/<id>/`, IS an adopted service — dropping
77
+ * it here would take it out of the scorecard's `c4.elements` denominator and
78
+ * out of the binding passes at once. The kind is the fallback for an element
79
+ * nothing else answers for, never an override.
80
+ *
62
81
  * The `#external` skip here is exactly what `./kind-tags.ts` guards: read off a
63
- * tag a KIND declared, it can empty this census for a whole fleet at once.
82
+ * tag a KIND declared, it can empty this census for a whole fleet at once. The
83
+ * INFRA skip is NOT guarded there and does not need to be — a kind is what an
84
+ * element already is, not a claim declared over the fleet, and `#infrastructure`
85
+ * is deliberately not a tag loam grades on (`GRADED_TAGS`).
64
86
  */
65
87
  export function drawnSystems(elements, services) {
66
88
  return serviceLevelElements(elements, services).filter((e) => !e.tags.includes(EXTERNAL_TAG) &&
67
89
  !ACTOR_KINDS.has(e.kind.toLowerCase()) &&
90
+ !(INFRA_KINDS.has(e.kind.toLowerCase()) && !standsForService(e, services)) &&
68
91
  !elements.some((c) => c.id.startsWith(`${e.id}.`) && standsForService(c, services)));
69
92
  }
70
93
  //# sourceMappingURL=census.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"census.js","sourceRoot":"","sources":["../../../../src/commands/validate/fleet/census.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;GAUG;AACH,OAAO,EAAa,MAAM,4BAA4B,CAAC;AACvD,OAAO,EAAE,iBAAiB,EAAE,MAAM,2CAA2C,CAAC;AAC9E,OAAO,EAAE,WAAW,EAAE,YAAY,EAAE,MAAM,sCAAsC,CAAC;AAEjF;;;;;GAKG;AACH,MAAM,UAAU,gBAAgB,CAAC,CAAO,EAAE,QAA6B;IACrE,OAAO,CAAC,CAAC,OAAO,KAAK,SAAS,IAAI,QAAQ,CAAC,GAAG,CAAC,CAAC,CAAC,KAAK,CAAC,CAAC;AAC1D,CAAC;AAED;;;;;;;;;;;;GAYG;AACH,MAAM,UAAU,oBAAoB,CAAC,QAAgB,EAAE,QAA6B;IAClF,MAAM,IAAI,GAAG,IAAI,GAAG,CAAC,QAAQ,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,CAAC,CAAC;IACrD;;;;;OAKG;IACH,MAAM,WAAW,GAAG,CAAC,CAAO,EAAU,EAAE,CACtC,iBAAiB,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,OAAO,CAAC,CAAC,EAAE,EAAE,EAAE;QACrC,MAAM,MAAM,GAAG,IAAI,CAAC,GAAG,CAAC,EAAE,CAAC,CAAC;QAC5B,OAAO,MAAM,KAAK,SAAS,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,CAAC,MAAM,CAAC,CAAC;IAC9C,CAAC,CAAC,CAAC;IACL,OAAO,QAAQ,CAAC,MAAM,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,WAAW,CAAC,CAAC,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,gBAAgB,CAAC,CAAC,EAAE,QAAQ,CAAC,CAAC,CAAC,CAAC;AAC5F,CAAC;AAED;;;;;;;;;;;;;GAaG;AACH,MAAM,UAAU,YAAY,CAAC,QAAgB,EAAE,QAA6B;IAC1E,OAAO,oBAAoB,CAAC,QAAQ,EAAE,QAAQ,CAAC,CAAC,MAAM,CACpD,CAAC,CAAC,EAAE,EAAE,CACJ,CAAC,CAAC,CAAC,IAAI,CAAC,QAAQ,CAAC,YAAY,CAAC;QAC9B,CAAC,WAAW,CAAC,GAAG,CAAC,CAAC,CAAC,IAAI,CAAC,WAAW,EAAE,CAAC;QACtC,CAAC,QAAQ,CAAC,IAAI,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC,UAAU,CAAC,GAAG,CAAC,CAAC,EAAE,GAAG,CAAC,IAAI,gBAAgB,CAAC,CAAC,EAAE,QAAQ,CAAC,CAAC,CACtF,CAAC;AACJ,CAAC","sourcesContent":["/**\n * The fleet map's census: which drawn elements answer for a `services/<id>/`.\n *\n * Three predicates, in one place because every exemption in the fleet target,\n * the scorecard's `c4.elements` count and the kind-tag check all have to answer\n * the same question the same way — a second spelling of \"is this element a\n * service\" is how a map and a rollup start disagreeing about the same fleet.\n * They lived in `./landscape.ts` beside their first reader until that file hit\n * its line limit; the seam was already there, since nothing here reads a file,\n * emits a finding, or knows what a target is.\n */\nimport { type Elem } from \"../../../core/c4/likec4.js\";\nimport { properAncestorIds } from \"../../../core/kernel/ids/fqn/ancestors.js\";\nimport { ACTOR_KINDS, EXTERNAL_TAG } from \"../../../core/vocabulary/maturity.js\";\n\n/**\n * Does this element stand for a `services/<id>/` directory — an explicit\n * `metadata { service }` binding, or a title naming a real one? The question\n * every exemption in `./landscape.ts`, the scorecard's census and\n * `./kind-tags.ts` all ask, exported so they cannot answer it differently.\n */\nexport function standsForService(e: Elem, services: ReadonlySet<string>): boolean {\n return e.service !== undefined || services.has(e.title);\n}\n\n/**\n * Service-LEVEL elements: everything not drawn inside something that is\n * already a service.\n *\n * Depth is not a fact about a service. This used to keep only top-level\n * elements (`!e.id.includes(\".\")`), which ordinary C4 breaks the moment it\n * groups services under a parent: every nested element was thrown away, so a\n * bound service read as unmodelled — an ERROR, on every service in the fleet\n * — and a binding written one level down was never checked at all. The tree\n * is walked instead, the way `serviceResolver` walks it for edges: a binding\n * wins at any depth, then a title naming a real services/<id>/, and what sits\n * INSIDE one of those is that service's container, not a service of its own.\n */\nexport function serviceLevelElements(elements: Elem[], services: ReadonlySet<string>): Elem[] {\n const byId = new Map(elements.map((e) => [e.id, e]));\n /**\n * The ancestors this document actually DECLARES, nearest first. The chain\n * itself is `core/kernel/ids/fqn/ancestors.ts`'s — the one spelling of it —\n * and the filter is this function's own: an id may name a level nothing\n * declares, and an undeclared level answers for no service.\n */\n const ancestorsOf = (e: Elem): Elem[] =>\n properAncestorIds(e.id).flatMap((id) => {\n const parent = byId.get(id);\n return parent === undefined ? [] : [parent];\n });\n return elements.filter((e) => !ancestorsOf(e).some((p) => standsForService(p, services)));\n}\n\n/**\n * The drawn SYSTEMS: service-level elements minus everything the map itself\n * says is not a service. Actor kinds model people; `#external` is somebody\n * else's system; and an element that CONTAINS a service is a grouping — a\n * domain, a boundary, an enterprise — not a service nobody adopted. There is\n * nothing to bind on a grouping and nothing to tag #external, so asking for\n * either would be a warning with no correct fix; the services under it answer\n * for themselves. `validateLandscape`'s undocumented pass walks this set, and\n * the scorecard's `c4.elements` census counts it — one predicate, two readers,\n * zero drift.\n *\n * The `#external` skip here is exactly what `./kind-tags.ts` guards: read off a\n * tag a KIND declared, it can empty this census for a whole fleet at once.\n */\nexport function drawnSystems(elements: Elem[], services: ReadonlySet<string>): Elem[] {\n return serviceLevelElements(elements, services).filter(\n (e) =>\n !e.tags.includes(EXTERNAL_TAG) &&\n !ACTOR_KINDS.has(e.kind.toLowerCase()) &&\n !elements.some((c) => c.id.startsWith(`${e.id}.`) && standsForService(c, services)),\n );\n}\n"]}
1
+ {"version":3,"file":"census.js","sourceRoot":"","sources":["../../../../src/commands/validate/fleet/census.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;GAUG;AACH,OAAO,EAAa,MAAM,4BAA4B,CAAC;AACvD,OAAO,EAAE,iBAAiB,EAAE,MAAM,2CAA2C,CAAC;AAC9E,OAAO,EAAE,WAAW,EAAE,YAAY,EAAE,WAAW,EAAE,MAAM,sCAAsC,CAAC;AAE9F;;;;;GAKG;AACH,MAAM,UAAU,gBAAgB,CAAC,CAAO,EAAE,QAA6B;IACrE,OAAO,CAAC,CAAC,OAAO,KAAK,SAAS,IAAI,QAAQ,CAAC,GAAG,CAAC,CAAC,CAAC,KAAK,CAAC,CAAC;AAC1D,CAAC;AAED;;;;;;;;;;;;GAYG;AACH,MAAM,UAAU,oBAAoB,CAAC,QAAgB,EAAE,QAA6B;IAClF,MAAM,IAAI,GAAG,IAAI,GAAG,CAAC,QAAQ,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,CAAC,CAAC;IACrD;;;;;OAKG;IACH,MAAM,WAAW,GAAG,CAAC,CAAO,EAAU,EAAE,CACtC,iBAAiB,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,OAAO,CAAC,CAAC,EAAE,EAAE,EAAE;QACrC,MAAM,MAAM,GAAG,IAAI,CAAC,GAAG,CAAC,EAAE,CAAC,CAAC;QAC5B,OAAO,MAAM,KAAK,SAAS,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,CAAC,MAAM,CAAC,CAAC;IAC9C,CAAC,CAAC,CAAC;IACL,OAAO,QAAQ,CAAC,MAAM,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,WAAW,CAAC,CAAC,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,gBAAgB,CAAC,CAAC,EAAE,QAAQ,CAAC,CAAC,CAAC,CAAC;AAC5F,CAAC;AAED;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAmCG;AACH,MAAM,UAAU,YAAY,CAAC,QAAgB,EAAE,QAA6B;IAC1E,OAAO,oBAAoB,CAAC,QAAQ,EAAE,QAAQ,CAAC,CAAC,MAAM,CACpD,CAAC,CAAC,EAAE,EAAE,CACJ,CAAC,CAAC,CAAC,IAAI,CAAC,QAAQ,CAAC,YAAY,CAAC;QAC9B,CAAC,WAAW,CAAC,GAAG,CAAC,CAAC,CAAC,IAAI,CAAC,WAAW,EAAE,CAAC;QACtC,CAAC,CAAC,WAAW,CAAC,GAAG,CAAC,CAAC,CAAC,IAAI,CAAC,WAAW,EAAE,CAAC,IAAI,CAAC,gBAAgB,CAAC,CAAC,EAAE,QAAQ,CAAC,CAAC;QAC1E,CAAC,QAAQ,CAAC,IAAI,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC,UAAU,CAAC,GAAG,CAAC,CAAC,EAAE,GAAG,CAAC,IAAI,gBAAgB,CAAC,CAAC,EAAE,QAAQ,CAAC,CAAC,CACtF,CAAC;AACJ,CAAC","sourcesContent":["/**\n * The fleet map's census: which drawn elements answer for a `services/<id>/`.\n *\n * Three predicates, in one place because every exemption in the fleet target,\n * the scorecard's `c4.elements` count and the kind-tag check all have to answer\n * the same question the same way — a second spelling of \"is this element a\n * service\" is how a map and a rollup start disagreeing about the same fleet.\n * They lived in `./landscape.ts` beside their first reader until that file hit\n * its line limit; the seam was already there, since nothing here reads a file,\n * emits a finding, or knows what a target is.\n */\nimport { type Elem } from \"../../../core/c4/likec4.js\";\nimport { properAncestorIds } from \"../../../core/kernel/ids/fqn/ancestors.js\";\nimport { ACTOR_KINDS, EXTERNAL_TAG, INFRA_KINDS } from \"../../../core/vocabulary/maturity.js\";\n\n/**\n * Does this element stand for a `services/<id>/` directory — an explicit\n * `metadata { service }` binding, or a title naming a real one? The question\n * every exemption in `./landscape.ts`, the scorecard's census and\n * `./kind-tags.ts` all ask, exported so they cannot answer it differently.\n */\nexport function standsForService(e: Elem, services: ReadonlySet<string>): boolean {\n return e.service !== undefined || services.has(e.title);\n}\n\n/**\n * Service-LEVEL elements: everything not drawn inside something that is\n * already a service.\n *\n * Depth is not a fact about a service. This used to keep only top-level\n * elements (`!e.id.includes(\".\")`), which ordinary C4 breaks the moment it\n * groups services under a parent: every nested element was thrown away, so a\n * bound service read as unmodelled — an ERROR, on every service in the fleet\n * — and a binding written one level down was never checked at all. The tree\n * is walked instead, the way `serviceResolver` walks it for edges: a binding\n * wins at any depth, then a title naming a real services/<id>/, and what sits\n * INSIDE one of those is that service's container, not a service of its own.\n */\nexport function serviceLevelElements(elements: Elem[], services: ReadonlySet<string>): Elem[] {\n const byId = new Map(elements.map((e) => [e.id, e]));\n /**\n * The ancestors this document actually DECLARES, nearest first. The chain\n * itself is `core/kernel/ids/fqn/ancestors.ts`'s — the one spelling of it —\n * and the filter is this function's own: an id may name a level nothing\n * declares, and an undeclared level answers for no service.\n */\n const ancestorsOf = (e: Elem): Elem[] =>\n properAncestorIds(e.id).flatMap((id) => {\n const parent = byId.get(id);\n return parent === undefined ? [] : [parent];\n });\n return elements.filter((e) => !ancestorsOf(e).some((p) => standsForService(p, services)));\n}\n\n/**\n * The drawn SYSTEMS: service-level elements minus everything the map itself\n * says is not a service. Actor kinds model people; INFRA kinds model the\n * datastores, queues and buckets the fleet RUNS; `#external` is somebody\n * else's system; and an element that CONTAINS a service is a grouping — a\n * domain, a boundary, an enterprise — not a service nobody adopted. There is\n * nothing to bind on a grouping and nothing to tag #external, so asking for\n * either would be a warning with no correct fix; the services under it answer\n * for themselves. `validateLandscape`'s undocumented pass walks this set, and\n * the scorecard's `c4.elements` census counts it — one predicate, two readers,\n * zero drift.\n *\n * The INFRA skip is the newest and the one with a measured cost behind it. A\n * `database` the fleet OWNS and draws at fleet level is neither a person nor\n * somebody else's system, so it used to earn `landscape.service-undocumented`\n * — \"bind it with `metadata { service '<id>' }`, or tag it `#external`\" —\n * whose two remedies are both FALSE of it: there is no directory to bind, and\n * it is ours. Measured on a one-service fixture drawing an owned `database` at\n * fleet level (2026-09-07): that warning plus `landscape.datastore-private`\n * plus `gate.partner-undocumented` on its one consumer, three findings and no\n * correct fix, which is why both `examples/docs` and this repo's own suite\n * tagged an owned Redis `#external` to buy silence.\n *\n * `standsForService` wins over the kind, and that guard is load-bearing rather\n * than defensive: a `database` bound with `metadata { service '<id>' }`, or one\n * whose title names a real `services/<id>/`, IS an adopted service — dropping\n * it here would take it out of the scorecard's `c4.elements` denominator and\n * out of the binding passes at once. The kind is the fallback for an element\n * nothing else answers for, never an override.\n *\n * The `#external` skip here is exactly what `./kind-tags.ts` guards: read off a\n * tag a KIND declared, it can empty this census for a whole fleet at once. The\n * INFRA skip is NOT guarded there and does not need to be — a kind is what an\n * element already is, not a claim declared over the fleet, and `#infrastructure`\n * is deliberately not a tag loam grades on (`GRADED_TAGS`).\n */\nexport function drawnSystems(elements: Elem[], services: ReadonlySet<string>): Elem[] {\n return serviceLevelElements(elements, services).filter(\n (e) =>\n !e.tags.includes(EXTERNAL_TAG) &&\n !ACTOR_KINDS.has(e.kind.toLowerCase()) &&\n !(INFRA_KINDS.has(e.kind.toLowerCase()) && !standsForService(e, services)) &&\n !elements.some((c) => c.id.startsWith(`${e.id}.`) && standsForService(c, services)),\n );\n}\n"]}