@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.
- package/CHANGELOG.md +162 -0
- package/README.md +4 -4
- package/ROADMAP.md +27 -6
- package/SCHEMA.md +152 -32
- package/dist/commands/archive/run.js +49 -22
- package/dist/commands/archive/run.js.map +1 -1
- package/dist/commands/seed/commit.js +34 -1
- package/dist/commands/seed/commit.js.map +1 -1
- package/dist/commands/seed/plan.js +64 -13
- package/dist/commands/seed/plan.js.map +1 -1
- package/dist/commands/subsystem/edit.js +4 -2
- package/dist/commands/subsystem/edit.js.map +1 -1
- package/dist/commands/subsystem/move.js +55 -17
- package/dist/commands/subsystem/move.js.map +1 -1
- package/dist/commands/subsystem/sync.js +142 -45
- package/dist/commands/subsystem/sync.js.map +1 -1
- package/dist/commands/subsystem/txn/report.js +55 -5
- package/dist/commands/subsystem/txn/report.js.map +1 -1
- package/dist/commands/subsystem/txn/txn.js +118 -21
- package/dist/commands/subsystem/txn/txn.js.map +1 -1
- package/dist/commands/subsystem/txn/views.js +51 -8
- package/dist/commands/subsystem/txn/views.js.map +1 -1
- package/dist/commands/validate/checks/health-deps.js +9 -6
- package/dist/commands/validate/checks/health-deps.js.map +1 -1
- package/dist/commands/validate/fleet/census.js +26 -3
- package/dist/commands/validate/fleet/census.js.map +1 -1
- package/dist/commands/validate/fleet/landscape.js +12 -9
- package/dist/commands/validate/fleet/landscape.js.map +1 -1
- package/dist/commands/validate/fleet/map/attest.js +4 -5
- package/dist/commands/validate/fleet/map/attest.js.map +1 -1
- package/dist/commands/validate/fleet/map/bindings.js +15 -2
- package/dist/commands/validate/fleet/map/bindings.js.map +1 -1
- package/dist/commands/validate/fleet/map/consumers.js +43 -122
- package/dist/commands/validate/fleet/map/consumers.js.map +1 -1
- package/dist/commands/validate/fleet/scorecard/scorecard.js.map +1 -1
- package/dist/commands/validate/fleet/views/fleet-project.js +1 -1
- package/dist/commands/validate/fleet/views/fleet-project.js.map +1 -1
- package/dist/commands/validate/fleet/views/ids.js +33 -7
- package/dist/commands/validate/fleet/views/ids.js.map +1 -1
- package/dist/commands/validate/fleet/views/root-documents.js +9 -7
- package/dist/commands/validate/fleet/views/root-documents.js.map +1 -1
- package/dist/commands/validate/fleet/views/stale.js +31 -14
- package/dist/commands/validate/fleet/views/stale.js.map +1 -1
- package/dist/core/agent/agents-md/command-map.js +1 -1
- package/dist/core/agent/agents-md/command-map.js.map +1 -1
- package/dist/core/agent/agents-md/map/subsystem.js +27 -14
- package/dist/core/agent/agents-md/map/subsystem.js.map +1 -1
- package/dist/core/agent/agents-md/refusals.js +5 -3
- package/dist/core/agent/agents-md/refusals.js.map +1 -1
- package/dist/core/agent/agents-md.js +16 -7
- package/dist/core/agent/agents-md.js.map +1 -1
- package/dist/core/agent/workflows/check.js +5 -6
- package/dist/core/agent/workflows/check.js.map +1 -1
- package/dist/core/agent/workflows/reference/spine.js +12 -6
- package/dist/core/agent/workflows/reference/spine.js.map +1 -1
- package/dist/core/brief/brief.js +25 -8
- package/dist/core/brief/brief.js.map +1 -1
- package/dist/core/brief/targets.js +64 -0
- package/dist/core/brief/targets.js.map +1 -1
- package/dist/core/brief/unchecked.js +1 -1
- package/dist/core/brief/unchecked.js.map +1 -1
- package/dist/core/c4/project/architecture.js +31 -9
- package/dist/core/c4/project/architecture.js.map +1 -1
- package/dist/core/c4/project/documents.js +5 -4
- package/dist/core/c4/project/documents.js.map +1 -1
- package/dist/core/c4/project/load.js +8 -4
- package/dist/core/c4/project/load.js.map +1 -1
- package/dist/core/c4/project/staged.js +7 -3
- package/dist/core/c4/project/staged.js.map +1 -1
- package/dist/core/deployment/delta.js +12 -10
- package/dist/core/deployment/delta.js.map +1 -1
- package/dist/core/gate/checks.js +5 -1
- package/dist/core/gate/checks.js.map +1 -1
- package/dist/core/gate/partners.js +60 -17
- package/dist/core/gate/partners.js.map +1 -1
- package/dist/core/repo/paths.js +34 -29
- package/dist/core/repo/paths.js.map +1 -1
- package/dist/core/repo/tree/render/body.js +45 -6
- package/dist/core/repo/tree/render/body.js.map +1 -1
- package/dist/core/repo/tree/render/service/header.js +125 -0
- package/dist/core/repo/tree/render/service/header.js.map +1 -0
- package/dist/core/repo/tree/render/service/peers.js +28 -0
- package/dist/core/repo/tree/render/service/peers.js.map +1 -0
- package/dist/core/repo/tree/render/service/view-id.js +67 -0
- package/dist/core/repo/tree/render/service/view-id.js.map +1 -0
- package/dist/core/repo/tree/render/service/views.js +78 -0
- package/dist/core/repo/tree/render/service/views.js.map +1 -0
- package/dist/core/repo/tree/render/stale.js +19 -2
- package/dist/core/repo/tree/render/stale.js.map +1 -1
- package/dist/core/repo/tree/render/views.js +102 -33
- package/dist/core/repo/tree/render/views.js.map +1 -1
- package/dist/core/verify/checklist.js +42 -10
- package/dist/core/verify/checklist.js.map +1 -1
- package/dist/core/vocabulary/issue.js.map +1 -1
- package/dist/core/vocabulary/maturity.js +41 -1
- package/dist/core/vocabulary/maturity.js.map +1 -1
- package/examples/README.md +7 -3
- package/examples/docs/architecture/landscape.likec4 +13 -7
- package/examples/docs/architecture/services.likec4 +70 -0
- package/examples/docs/architecture/subsystems.likec4 +3 -0
- package/examples/docs/services/order-service/model.likec4 +10 -8
- package/examples/docs/services/payment-service/model.likec4 +6 -4
- package/examples/docs/services/platform/identity-service/model.likec4 +9 -7
- package/examples/docs/services/platform/notification-service/model.likec4 +4 -5
- package/package.json +1 -1
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
import { GENERATED_MARK } from "./header.js";
|
|
2
|
+
import { peersOf } from "./peers.js";
|
|
3
|
+
import { serviceViewId } from "./view-id.js";
|
|
4
|
+
/**
|
|
5
|
+
* The global style group these views reference when the map declares one, the
|
|
6
|
+
* exact counterpart of `SUBSYSTEM_STYLE_GROUP` and reserved on the same terms:
|
|
7
|
+
* presence of the DECLARATION is the whole opt-in, because referencing an
|
|
8
|
+
* undeclared style id is a parse error that blanks the entire `architecture/`
|
|
9
|
+
* project in the renderer. The name mirrors the file it styles.
|
|
10
|
+
*/
|
|
11
|
+
export const SERVICE_STYLE_GROUP = "services";
|
|
12
|
+
/**
|
|
13
|
+
* The header every generated file opens with — stable bytes, part of the
|
|
14
|
+
* content-compare contract, naming the regenerator so a reader who found the
|
|
15
|
+
* file before the docs knows what owns it. `loam subsystem sync` regenerates
|
|
16
|
+
* BOTH generated files, which is why one command is named here and why one
|
|
17
|
+
* finding code (`subsystem.views-stale`) honestly covers both.
|
|
18
|
+
*
|
|
19
|
+
* It OPENS with the shared `GENERATED_MARK` rather than spelling it, because
|
|
20
|
+
* the same bytes are what `loam subsystem sync` reads back off disk to decide
|
|
21
|
+
* whether the file at this reserved path is loam's to rewrite at all
|
|
22
|
+
* (`./header.js`). A second literal here is how a file loam wrote stops being
|
|
23
|
+
* recognised as loam's.
|
|
24
|
+
*/
|
|
25
|
+
const HEADER = `${GENERATED_MARK} — one overview view per service under services/, drawn from the fleet map.\n` +
|
|
26
|
+
"// Do not edit: `loam subsystem sync` regenerates this file from the directories and the map,\n" +
|
|
27
|
+
"// and `loam validate --all` reports `subsystem.views-stale` when it drifts.\n" +
|
|
28
|
+
"// A navigation aid, not the fleet's architecture: author your own diagrams elsewhere.\n";
|
|
29
|
+
/**
|
|
30
|
+
* Render the whole file, or null when no service resolves to a map element —
|
|
31
|
+
* the file must then be ABSENT, so a fleet whose map draws nothing never
|
|
32
|
+
* carries a generated artifact that says nothing.
|
|
33
|
+
*
|
|
34
|
+
* Services sorted by id and peers sorted by id, one `include` per line: the
|
|
35
|
+
* output is byte-reproducible on any machine from the same committed bytes (no
|
|
36
|
+
* timestamps, no absolute paths, no readdir-order dependence), and two
|
|
37
|
+
* concurrent changes to different services touch different lines, so git merges
|
|
38
|
+
* the file without intervention.
|
|
39
|
+
*/
|
|
40
|
+
export function renderServiceViews(input) {
|
|
41
|
+
if (input.subjects.length === 0)
|
|
42
|
+
return null;
|
|
43
|
+
const lines = [HEADER, ...(input.disclosure === null ? [] : [input.disclosure]), "views {"];
|
|
44
|
+
const subjects = [...input.subjects].sort((a, b) => (a.id < b.id ? -1 : 1));
|
|
45
|
+
for (const [i, subject] of subjects.entries()) {
|
|
46
|
+
if (i > 0)
|
|
47
|
+
lines.push("");
|
|
48
|
+
lines.push(...viewLines(subject, peersOf(subject.fqn, input.relationships), input.styled));
|
|
49
|
+
}
|
|
50
|
+
lines.push("}");
|
|
51
|
+
return lines.join("\n") + "\n";
|
|
52
|
+
}
|
|
53
|
+
/**
|
|
54
|
+
* One view, in the order the parser demands: metadata FIRST, and that is a hard
|
|
55
|
+
* constraint rather than a taste — measured, an `include` line followed by
|
|
56
|
+
* `title` is a parse error ("Expecting token of type '}' but found `title`"),
|
|
57
|
+
* and one parse error in this file blanks the whole `architecture/` project in
|
|
58
|
+
* the renderer. `global style` sits after `title` and before anything that
|
|
59
|
+
* includes, where the same pin measured it parsing with zero errors.
|
|
60
|
+
*/
|
|
61
|
+
function viewLines(subject, peers, styled) {
|
|
62
|
+
const { fqn } = subject;
|
|
63
|
+
const out = [` view ${serviceViewId(subject.id)} of ${fqn} {`, ` title '${subject.title}'`];
|
|
64
|
+
if (styled)
|
|
65
|
+
out.push(` global style ${SERVICE_STYLE_GROUP}`);
|
|
66
|
+
out.push(" include *");
|
|
67
|
+
for (const peer of peers)
|
|
68
|
+
out.push(` include ${peer}`);
|
|
69
|
+
out.push(" exclude * -> *", ` include -> ${fqn}`, ` include ${fqn} ->`);
|
|
70
|
+
// The interior wiring, last. Measured: `include <fqn>.* -> <fqn>.*` on a
|
|
71
|
+
// CHILDLESS element is legal — `likec4 validate` reports valid and the export
|
|
72
|
+
// carries zero errors — so one generated line serves both model shapes: an
|
|
73
|
+
// EXTENDING model's containers live in the root project and are drawn, a
|
|
74
|
+
// STANDALONE model's are not in this project and the line matches nothing.
|
|
75
|
+
out.push(` include ${fqn}.* -> ${fqn}.*`, " }");
|
|
76
|
+
return out;
|
|
77
|
+
}
|
|
78
|
+
//# sourceMappingURL=views.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"views.js","sourceRoot":"","sources":["../../../../../../src/core/repo/tree/render/service/views.ts"],"names":[],"mappings":"AAkEA,OAAO,EAAE,cAAc,EAAE,MAAM,aAAa,CAAC;AAC7C,OAAO,EAAE,OAAO,EAAE,MAAM,YAAY,CAAC;AACrC,OAAO,EAAE,aAAa,EAAE,MAAM,cAAc,CAAC;AAE7C;;;;;;GAMG;AACH,MAAM,CAAC,MAAM,mBAAmB,GAAG,UAAU,CAAC;AAuB9C;;;;;;;;;;;;GAYG;AACH,MAAM,MAAM,GACV,GAAG,cAAc,+EAA+E;IAChG,iGAAiG;IACjG,gFAAgF;IAChF,0FAA0F,CAAC;AAE7F;;;;;;;;;;GAUG;AACH,MAAM,UAAU,kBAAkB,CAAC,KAAwB;IACzD,IAAI,KAAK,CAAC,QAAQ,CAAC,MAAM,KAAK,CAAC;QAAE,OAAO,IAAI,CAAC;IAC7C,MAAM,KAAK,GAAa,CAAC,MAAM,EAAE,GAAG,CAAC,KAAK,CAAC,UAAU,KAAK,IAAI,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,CAAC,KAAK,CAAC,UAAU,CAAC,CAAC,EAAE,SAAS,CAAC,CAAC;IACtG,MAAM,QAAQ,GAAG,CAAC,GAAG,KAAK,CAAC,QAAQ,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC,EAAE,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,CAAC,EAAE,GAAG,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC;IAC5E,KAAK,MAAM,CAAC,CAAC,EAAE,OAAO,CAAC,IAAI,QAAQ,CAAC,OAAO,EAAE,EAAE,CAAC;QAC9C,IAAI,CAAC,GAAG,CAAC;YAAE,KAAK,CAAC,IAAI,CAAC,EAAE,CAAC,CAAC;QAC1B,KAAK,CAAC,IAAI,CAAC,GAAG,SAAS,CAAC,OAAO,EAAE,OAAO,CAAC,OAAO,CAAC,GAAG,EAAE,KAAK,CAAC,aAAa,CAAC,EAAE,KAAK,CAAC,MAAM,CAAC,CAAC,CAAC;IAC7F,CAAC;IACD,KAAK,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC;IAChB,OAAO,KAAK,CAAC,IAAI,CAAC,IAAI,CAAC,GAAG,IAAI,CAAC;AACjC,CAAC;AAED;;;;;;;GAOG;AACH,SAAS,SAAS,CAAC,OAA2B,EAAE,KAAwB,EAAE,MAAe;IACvF,MAAM,EAAE,GAAG,EAAE,GAAG,OAAO,CAAC;IACxB,MAAM,GAAG,GAAG,CAAC,UAAU,aAAa,CAAC,OAAO,CAAC,EAAE,CAAC,OAAO,GAAG,IAAI,EAAE,cAAc,OAAO,CAAC,KAAK,GAAG,CAAC,CAAC;IAChG,IAAI,MAAM;QAAE,GAAG,CAAC,IAAI,CAAC,oBAAoB,mBAAmB,EAAE,CAAC,CAAC;IAChE,GAAG,CAAC,IAAI,CAAC,eAAe,CAAC,CAAC;IAC1B,KAAK,MAAM,IAAI,IAAI,KAAK;QAAE,GAAG,CAAC,IAAI,CAAC,eAAe,IAAI,EAAE,CAAC,CAAC;IAC1D,GAAG,CAAC,IAAI,CAAC,oBAAoB,EAAE,kBAAkB,GAAG,EAAE,EAAE,eAAe,GAAG,KAAK,CAAC,CAAC;IACjF,yEAAyE;IACzE,8EAA8E;IAC9E,2EAA2E;IAC3E,yEAAyE;IACzE,2EAA2E;IAC3E,GAAG,CAAC,IAAI,CAAC,eAAe,GAAG,SAAS,GAAG,IAAI,EAAE,KAAK,CAAC,CAAC;IACpD,OAAO,GAAG,CAAC;AACb,CAAC","sourcesContent":["/**\n * `architecture/services.likec4` — one generated LikeC4 view per service,\n * showing that service, its own interior, and its links to everything else the\n * fleet map draws.\n *\n * GENERATED, never authored, exactly like the subsystem views beside it:\n * authored bytes stay authored (archive's verbatim-splice promise), `loam\n * subsystem sync` owns this file outright, and staleness against the tree is a\n * content compare and never a parse — a views-only document does not parse\n * standalone, so nothing in loam ever loads it. docs/DESIGN.md rule 26 does not\n * change with a second file: what a view SHOWS stays unread in every document,\n * this one included.\n *\n * ## The recipe, and why the obvious one is wrong\n *\n * Measured at likec4 1.59.2 on a 120-service and a 58-service fleet. Each view\n * is written as\n *\n * view service_<id> of <fqn> {\n * title '<subsystem path> / <service id>'\n * global style services // only when the map declares the group\n * include * // the service and its own interior\n * include <peer> // one line per peer, id-sorted\n * exclude * -> * // drop the peer-to-peer cross traffic\n * include -> <fqn> // its inbound edges\n * include <fqn> -> // its outbound edges\n * include <fqn>.* -> <fqn>.* // its internal wiring\n * }\n *\n * Three things in that shape are measurements rather than taste, and each one\n * fails SILENTLY — zero errors, a wrong picture — if it is tidied away:\n *\n * 1. **The peers are named, not wildcarded.** `./peers.ts` carries the\n * measurement: `include <fqn> -> *` collapses every counterpart to its\n * GROUP box and yields zero drill-down on any map that nests services.\n * 2. **The edge predicates are UNARY.** `include -> <fqn>` and `include <fqn>\n * ->`, never the binary `include * -> <fqn>` / `include <fqn> -> *`.\n * Measured on `payments.svc0` of the 58-service fleet, swapping the two\n * unary lines for their binary forms takes the view from 11 nodes to 15:\n * the four parent GROUP boxes of the named peers come back as nodes of\n * their own, so the view regrows exactly what rule 1 removed. Zero errors\n * either way, which is what makes it worth a comment.\n * 3. **`exclude * -> *` sits between the includes and the edge lines.** Without\n * it the named peers bring their own cross traffic with them: on the same\n * subject, 24 edges instead of 9, on a view about one service.\n *\n * `of <fqn>` is the navigation half and costs one keyword: LikeC4 computes\n * `navigateTo` into a view declared `of` an element from every OTHER view that\n * draws that element, with no authored links. That is what makes the fleet map,\n * every subsystem view and every other service view click-through into a\n * service. Two views of one element are legal — the first document\n * alphabetically wins the default target and both stay reachable, because the\n * subject's own node cycles to the other — so a team's authored element view in\n * `landscape.likec4` keeps precedence over this file, which sorts later.\n *\n * ## What arrives from outside\n *\n * Everything about the TREE — which services exist, what each one's element is,\n * what its title reads — arrives composed, in `ServiceViewSubject`. This package\n * may not import `../views.ts` or `../text.ts`: the parent imports it, and the\n * reverse edge would be the package cycle `scripts/package-graph.mjs` refuses.\n * So the title arrives already through `likec4String` rather than escaped a\n * second time here; a second escaper is how one of the two forgets an\n * apostrophe and ships a fleet-wide parse error.\n */\nimport type { Rel } from \"../../../../c4/likec4.js\";\nimport { GENERATED_MARK } from \"./header.js\";\nimport { peersOf } from \"./peers.js\";\nimport { serviceViewId } from \"./view-id.js\";\n\n/**\n * The global style group these views reference when the map declares one, the\n * exact counterpart of `SUBSYSTEM_STYLE_GROUP` and reserved on the same terms:\n * presence of the DECLARATION is the whole opt-in, because referencing an\n * undeclared style id is a parse error that blanks the entire `architecture/`\n * project in the renderer. The name mirrors the file it styles.\n */\nexport const SERVICE_STYLE_GROUP = \"services\";\n\n/** One service this file draws — resolved against the map, and already escaped. */\nexport interface ServiceViewSubject {\n /** The `services/<id>/` directory id: the sort key and the view id's source. */\n id: string;\n /** The landscape element it resolves to — the view's `of`, and its subject. */\n fqn: string;\n /** `<subsystem path> / <service id>`, already through `likec4String`. */\n title: string;\n}\n\n/** Everything the file render reads, as one record — see the banner on why it arrives composed. */\nexport interface ServiceViewsInput {\n subjects: readonly ServiceViewSubject[];\n /** The map's edges, from which each subject's peer set is computed. */\n relationships: readonly Rel[];\n /** Emit `global style services` — true only when the map DECLARES that group. */\n styled: boolean;\n /** The one file-level comment the render may write, or null. */\n disclosure: string | null;\n}\n\n/**\n * The header every generated file opens with — stable bytes, part of the\n * content-compare contract, naming the regenerator so a reader who found the\n * file before the docs knows what owns it. `loam subsystem sync` regenerates\n * BOTH generated files, which is why one command is named here and why one\n * finding code (`subsystem.views-stale`) honestly covers both.\n *\n * It OPENS with the shared `GENERATED_MARK` rather than spelling it, because\n * the same bytes are what `loam subsystem sync` reads back off disk to decide\n * whether the file at this reserved path is loam's to rewrite at all\n * (`./header.js`). A second literal here is how a file loam wrote stops being\n * recognised as loam's.\n */\nconst HEADER =\n `${GENERATED_MARK} — one overview view per service under services/, drawn from the fleet map.\\n` +\n \"// Do not edit: `loam subsystem sync` regenerates this file from the directories and the map,\\n\" +\n \"// and `loam validate --all` reports `subsystem.views-stale` when it drifts.\\n\" +\n \"// A navigation aid, not the fleet's architecture: author your own diagrams elsewhere.\\n\";\n\n/**\n * Render the whole file, or null when no service resolves to a map element —\n * the file must then be ABSENT, so a fleet whose map draws nothing never\n * carries a generated artifact that says nothing.\n *\n * Services sorted by id and peers sorted by id, one `include` per line: the\n * output is byte-reproducible on any machine from the same committed bytes (no\n * timestamps, no absolute paths, no readdir-order dependence), and two\n * concurrent changes to different services touch different lines, so git merges\n * the file without intervention.\n */\nexport function renderServiceViews(input: ServiceViewsInput): string | null {\n if (input.subjects.length === 0) return null;\n const lines: string[] = [HEADER, ...(input.disclosure === null ? [] : [input.disclosure]), \"views {\"];\n const subjects = [...input.subjects].sort((a, b) => (a.id < b.id ? -1 : 1));\n for (const [i, subject] of subjects.entries()) {\n if (i > 0) lines.push(\"\");\n lines.push(...viewLines(subject, peersOf(subject.fqn, input.relationships), input.styled));\n }\n lines.push(\"}\");\n return lines.join(\"\\n\") + \"\\n\";\n}\n\n/**\n * One view, in the order the parser demands: metadata FIRST, and that is a hard\n * constraint rather than a taste — measured, an `include` line followed by\n * `title` is a parse error (\"Expecting token of type '}' but found `title`\"),\n * and one parse error in this file blanks the whole `architecture/` project in\n * the renderer. `global style` sits after `title` and before anything that\n * includes, where the same pin measured it parsing with zero errors.\n */\nfunction viewLines(subject: ServiceViewSubject, peers: readonly string[], styled: boolean): string[] {\n const { fqn } = subject;\n const out = [` view ${serviceViewId(subject.id)} of ${fqn} {`, ` title '${subject.title}'`];\n if (styled) out.push(` global style ${SERVICE_STYLE_GROUP}`);\n out.push(\" include *\");\n for (const peer of peers) out.push(` include ${peer}`);\n out.push(\" exclude * -> *\", ` include -> ${fqn}`, ` include ${fqn} ->`);\n // The interior wiring, last. Measured: `include <fqn>.* -> <fqn>.*` on a\n // CHILDLESS element is legal — `likec4 validate` reports valid and the export\n // carries zero errors — so one generated line serves both model shapes: an\n // EXTENDING model's containers live in the root project and are drawn, a\n // STANDALONE model's are not in this project and the line matches nothing.\n out.push(` include ${fqn}.* -> ${fqn}.*`, \" }\");\n return out;\n}\n"]}
|
|
@@ -16,9 +16,26 @@
|
|
|
16
16
|
*/
|
|
17
17
|
import { existsSync } from "node:fs";
|
|
18
18
|
import { readFile } from "node:fs/promises";
|
|
19
|
-
import { renderSubsystemViews, viewsAgree } from "./views.js";
|
|
19
|
+
import { renderServiceOverviews, renderSubsystemViews, viewsAgree } from "./views.js";
|
|
20
20
|
export async function viewsState(path, tree, map) {
|
|
21
|
-
|
|
21
|
+
return stateOf(path, renderSubsystemViews(tree, map));
|
|
22
|
+
}
|
|
23
|
+
/**
|
|
24
|
+
* The same question about the OTHER generated file, `architecture/services.likec4`.
|
|
25
|
+
*
|
|
26
|
+
* A second exported function rather than a `which` parameter, and a second call
|
|
27
|
+
* at every asker rather than one that answers about both: the two files go stale
|
|
28
|
+
* for different reasons and are repaired in one command, so a caller that wants
|
|
29
|
+
* both asks twice and names each file in its own finding —
|
|
30
|
+
* `subsystem.views-stale` carries the path precisely so one code can honestly
|
|
31
|
+
* cover two files. Folding them into one state would lose which one drifted,
|
|
32
|
+
* which is the only thing the message has to say.
|
|
33
|
+
*/
|
|
34
|
+
export async function serviceViewsState(path, tree, map) {
|
|
35
|
+
return stateOf(path, renderServiceOverviews(tree, map));
|
|
36
|
+
}
|
|
37
|
+
/** Read the file, or record its absence, and compare — the half both askers share. */
|
|
38
|
+
async function stateOf(path, expected) {
|
|
22
39
|
const actual = existsSync(path) ? await readFile(path, "utf8") : null;
|
|
23
40
|
return { actual, expected, agrees: viewsAgree(actual, expected) };
|
|
24
41
|
}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"stale.js","sourceRoot":"","sources":["../../../../../src/core/repo/tree/render/stale.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;GAeG;AACH,OAAO,EAAE,UAAU,EAAE,MAAM,SAAS,CAAC;AACrC,OAAO,EAAE,QAAQ,EAAE,MAAM,kBAAkB,CAAC;AAE5C,OAAO,EAAiB,oBAAoB,EAAE,UAAU,EAAE,MAAM,YAAY,CAAC;
|
|
1
|
+
{"version":3,"file":"stale.js","sourceRoot":"","sources":["../../../../../src/core/repo/tree/render/stale.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;GAeG;AACH,OAAO,EAAE,UAAU,EAAE,MAAM,SAAS,CAAC;AACrC,OAAO,EAAE,QAAQ,EAAE,MAAM,kBAAkB,CAAC;AAE5C,OAAO,EAAiB,sBAAsB,EAAE,oBAAoB,EAAE,UAAU,EAAE,MAAM,YAAY,CAAC;AAgBrG,MAAM,CAAC,KAAK,UAAU,UAAU,CAAC,IAAY,EAAE,IAAe,EAAE,GAAa;IAC3E,OAAO,OAAO,CAAC,IAAI,EAAE,oBAAoB,CAAC,IAAI,EAAE,GAAG,CAAC,CAAC,CAAC;AACxD,CAAC;AAED;;;;;;;;;;GAUG;AACH,MAAM,CAAC,KAAK,UAAU,iBAAiB,CAAC,IAAY,EAAE,IAAe,EAAE,GAAa;IAClF,OAAO,OAAO,CAAC,IAAI,EAAE,sBAAsB,CAAC,IAAI,EAAE,GAAG,CAAC,CAAC,CAAC;AAC1D,CAAC;AAED,sFAAsF;AACtF,KAAK,UAAU,OAAO,CAAC,IAAY,EAAE,QAAuB;IAC1D,MAAM,MAAM,GAAG,UAAU,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC,MAAM,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC,CAAC,CAAC,CAAC,IAAI,CAAC;IACtE,OAAO,EAAE,MAAM,EAAE,QAAQ,EAAE,MAAM,EAAE,UAAU,CAAC,MAAM,EAAE,QAAQ,CAAC,EAAE,CAAC;AACpE,CAAC","sourcesContent":["/**\n * Is the generated file on disk what this tree and this map render to?\n *\n * One function, because the question now has three askers — `validate --all`\n * (which turns a disagreement into `subsystem.views-stale`), `loam subsystem\n * sync` and the write verbs (which repair it), and `loam archive` (which must\n * not report a repo current when it has just made the file stale). Three\n * private compares would be three chances to answer differently about one\n * file, and the module that mints the bytes is the one that owes the answer.\n *\n * The PATH arrives as a parameter rather than a `DocsDir`: `repo/paths.ts`\n * owns the spelling of every file in a docs repo, and importing it here would\n * close the package cycle `repo → repo/tree → repo/tree/render → repo` that\n * `scripts/package-graph.mjs` exists to refuse. `walk.ts` pays the same price\n * for the same reason and says so.\n */\nimport { existsSync } from \"node:fs\";\nimport { readFile } from \"node:fs/promises\";\nimport type { FleetTree } from \"../walk.js\";\nimport { type MapFacts, renderServiceOverviews, renderSubsystemViews, viewsAgree } from \"./views.js\";\n\n/**\n * The three states, kept apart: absent, must-be-absent, and\n * present-but-different are three different sentences, and a fleet whose views\n * file does not exist is not a fleet whose views file is blank.\n */\nexport interface ViewsState {\n /** The bytes on disk, or null when the file is not there. */\n actual: string | null;\n /** The bytes the tree renders to, or null when the file must be absent. */\n expected: string | null;\n /** Do the two say the same thing? Content, not bytes — `viewsAgree` says why. */\n agrees: boolean;\n}\n\nexport async function viewsState(path: string, tree: FleetTree, map: MapFacts): Promise<ViewsState> {\n return stateOf(path, renderSubsystemViews(tree, map));\n}\n\n/**\n * The same question about the OTHER generated file, `architecture/services.likec4`.\n *\n * A second exported function rather than a `which` parameter, and a second call\n * at every asker rather than one that answers about both: the two files go stale\n * for different reasons and are repaired in one command, so a caller that wants\n * both asks twice and names each file in its own finding —\n * `subsystem.views-stale` carries the path precisely so one code can honestly\n * cover two files. Folding them into one state would lose which one drifted,\n * which is the only thing the message has to say.\n */\nexport async function serviceViewsState(path: string, tree: FleetTree, map: MapFacts): Promise<ViewsState> {\n return stateOf(path, renderServiceOverviews(tree, map));\n}\n\n/** Read the file, or record its absence, and compare — the half both askers share. */\nasync function stateOf(path: string, expected: string | null): Promise<ViewsState> {\n const actual = existsSync(path) ? await readFile(path, \"utf8\") : null;\n return { actual, expected, agrees: viewsAgree(actual, expected) };\n}\n"]}
|
|
@@ -49,6 +49,9 @@
|
|
|
49
49
|
*/
|
|
50
50
|
import {} from "../../../c4/likec4.js";
|
|
51
51
|
import { renderContext, viewBody } from "./body.js";
|
|
52
|
+
import { GENERATED_MARK } from "./service/header.js";
|
|
53
|
+
import { escapeViewIdSegment } from "./service/view-id.js";
|
|
54
|
+
import { renderServiceViews, SERVICE_STYLE_GROUP, } from "./service/views.js";
|
|
52
55
|
import { likec4String, viewTitle } from "./text.js";
|
|
53
56
|
/**
|
|
54
57
|
* The global style group a generated view references when the map declares
|
|
@@ -75,8 +78,14 @@ export const SUBSYSTEM_STYLE_GROUP = "subsystems";
|
|
|
75
78
|
* The header every generated file opens with — stable bytes, part of the
|
|
76
79
|
* byte-compare contract, naming the regenerator so a reader who found the
|
|
77
80
|
* file first knows what owns it.
|
|
81
|
+
*
|
|
82
|
+
* It OPENS with the shared `GENERATED_MARK` rather than spelling it, because
|
|
83
|
+
* the same bytes are what `loam subsystem sync` reads back off disk to decide
|
|
84
|
+
* whether the file standing at this reserved path is loam's to rewrite at all
|
|
85
|
+
* (`./service/header.ts` carries the whole reason). A second literal here is
|
|
86
|
+
* how a file loam wrote stops being recognised as loam's.
|
|
78
87
|
*/
|
|
79
|
-
const HEADER =
|
|
88
|
+
const HEADER = `${GENERATED_MARK} — the subsystem tree under services/, one view per subsystem.\n` +
|
|
80
89
|
"// Do not edit: `loam subsystem sync` regenerates this file from the directories,\n" +
|
|
81
90
|
"// and `loam validate --all` reports `subsystem.views-stale` when it drifts.\n" +
|
|
82
91
|
"// A scoping aid, not the fleet's architecture: author your own diagrams elsewhere.\n";
|
|
@@ -89,12 +98,16 @@ const HEADER = "// generated by loam — the subsystem tree under services/, one
|
|
|
89
98
|
* generated views ignore it. Silent when the map declares no group at all: a
|
|
90
99
|
* fleet with no palette is owed no sentence about one. Ids go unescaped
|
|
91
100
|
* because LikeC4's grammar admits only identifier bytes in a style id.
|
|
101
|
+
*
|
|
102
|
+
* The group is a PARAMETER because there are two generated files now and each
|
|
103
|
+
* reserves its own name (`subsystems`, `services`); a map declaring one and not
|
|
104
|
+
* the other must get the sentence in exactly the file that went unstyled.
|
|
92
105
|
*/
|
|
93
|
-
function stylesDisclosure(declared) {
|
|
94
|
-
if (declared.length === 0 || declared.includes(
|
|
106
|
+
function stylesDisclosure(declared, group) {
|
|
107
|
+
if (declared.length === 0 || declared.includes(group))
|
|
95
108
|
return null;
|
|
96
109
|
return (`// styles: the project declares global style group(s) ${[...declared].sort().join(", ")}, ` +
|
|
97
|
-
`and none named \`${
|
|
110
|
+
`and none named \`${group}\` — these views carry the renderer's defaults until one is.`);
|
|
98
111
|
}
|
|
99
112
|
/**
|
|
100
113
|
* Render the views file for a walked tree, or null when the tree has no
|
|
@@ -107,7 +120,7 @@ export function renderSubsystemViews(tree, map) {
|
|
|
107
120
|
const ctx = renderContext(tree, map.elements);
|
|
108
121
|
const declared = map.globalStyles ?? [];
|
|
109
122
|
const styled = declared.includes(SUBSYSTEM_STYLE_GROUP);
|
|
110
|
-
const disclosure = stylesDisclosure(declared);
|
|
123
|
+
const disclosure = stylesDisclosure(declared, SUBSYSTEM_STYLE_GROUP);
|
|
111
124
|
const lines = [HEADER, ...(disclosure === null ? [] : [disclosure]), "views {"];
|
|
112
125
|
const subsystems = [...tree.subsystems].sort((a, b) => (pathKey(a) < pathKey(b) ? -1 : 1));
|
|
113
126
|
for (const [i, sub] of subsystems.entries()) {
|
|
@@ -117,13 +130,23 @@ export function renderSubsystemViews(tree, map) {
|
|
|
117
130
|
const body = viewBody(ctx, sub);
|
|
118
131
|
if (body.disclosure !== null)
|
|
119
132
|
lines.push(` // ${body.disclosure}`);
|
|
120
|
-
|
|
133
|
+
// `of <ancestor>` when — and only when — the map holds an element that owns
|
|
134
|
+
// exactly this subsystem's members (`../render/body.ts` argues the
|
|
135
|
+
// "exactly"). It is the same one keyword the per-service views spend, and it
|
|
136
|
+
// buys the same thing: LikeC4 computes `navigateTo` into this view from
|
|
137
|
+
// every other view that draws that element, so the fleet map's group boxes
|
|
138
|
+
// become click-through and a neighbouring group box inside one subsystem
|
|
139
|
+
// view opens THAT subsystem's view. Measured at 58 services in 11 groups,
|
|
140
|
+
// 12 of 14 nodes navigable. With no owning ancestor there is no element to
|
|
141
|
+
// be `of`, so there is no navigation — honest, and one more reason to nest
|
|
142
|
+
// the map that loam already discloses one line above.
|
|
143
|
+
lines.push(` view ${subsystemViewId(sub)}${body.of === null ? "" : ` of ${body.of}`} {`);
|
|
121
144
|
// Metadata FIRST, and that ordering is a hard constraint rather than a
|
|
122
145
|
// taste: measured, an `include` OR a `group { … }` line followed by
|
|
123
146
|
// `title` is a parse error ("Expecting token of type '}' but found
|
|
124
147
|
// `title`"), and one parse error in this file blanks the whole
|
|
125
148
|
// `architecture/` project in the renderer.
|
|
126
|
-
lines.push(` title '${viewTitle(titleChain(tree, sub))}'`);
|
|
149
|
+
lines.push(` title '${viewTitle(titleChain(tree, sub.path))}'`);
|
|
127
150
|
const description = likec4String(sub.meta.description ?? "");
|
|
128
151
|
if (description !== "")
|
|
129
152
|
lines.push(` description '${description}'`);
|
|
@@ -142,8 +165,62 @@ export function renderSubsystemViews(tree, map) {
|
|
|
142
165
|
return lines.join("\n") + "\n";
|
|
143
166
|
}
|
|
144
167
|
/**
|
|
145
|
-
*
|
|
146
|
-
*
|
|
168
|
+
* The GENERATED per-service overviews — `architecture/services.likec4`, one
|
|
169
|
+
* view per service the map resolves. Null when it resolves none, so the file is
|
|
170
|
+
* then absent exactly as `subsystems.likec4` is for a tree with no subsystems.
|
|
171
|
+
*
|
|
172
|
+
* This function is the composer and nothing else: `./service/` owns the view
|
|
173
|
+
* shape and its measurements, and everything about the TREE is resolved here,
|
|
174
|
+
* because that package may not import `./body.ts` or `./text.ts` (the edge runs
|
|
175
|
+
* the other way — `./service/views.ts` says why) and duplicating either would
|
|
176
|
+
* put a second element→service join and a second string escaper in the tree.
|
|
177
|
+
*
|
|
178
|
+
* Emission is gated on the SAME join the subsystem views use: a service the map
|
|
179
|
+
* does not draw is omitted rather than invented, because that omission is
|
|
180
|
+
* already `landscape.service-unmodelled` and rendering only what the committed
|
|
181
|
+
* map resolves keeps the output a function of committed bytes alone.
|
|
182
|
+
*/
|
|
183
|
+
export function renderServiceOverviews(tree, map) {
|
|
184
|
+
const ctx = renderContext(tree, map.elements);
|
|
185
|
+
const declared = map.globalStyles ?? [];
|
|
186
|
+
const subjects = [];
|
|
187
|
+
for (const service of tree.services) {
|
|
188
|
+
const fqn = ctx.elementOf.get(service.id);
|
|
189
|
+
if (fqn === undefined)
|
|
190
|
+
continue;
|
|
191
|
+
subjects.push({ id: service.id, fqn, title: serviceViewTitle(tree, service.subsystem, service.id) });
|
|
192
|
+
}
|
|
193
|
+
return renderServiceViews({
|
|
194
|
+
subjects,
|
|
195
|
+
relationships: map.relationships,
|
|
196
|
+
styled: declared.includes(SERVICE_STYLE_GROUP),
|
|
197
|
+
disclosure: stylesDisclosure(declared, SERVICE_STYLE_GROUP),
|
|
198
|
+
});
|
|
199
|
+
}
|
|
200
|
+
/**
|
|
201
|
+
* A per-service view's title: its subsystem path and then its own id, joined
|
|
202
|
+
* with ` / ` — `Platform / order-flow / checkout-web`, or bare `checkout-web`
|
|
203
|
+
* for a service filed directly under `services/`.
|
|
204
|
+
*
|
|
205
|
+
* LikeC4 reads that as a folder path (`./text.ts` measured it), so a service's
|
|
206
|
+
* overview lands in the SAME browser folder as its subsystem's view rather than
|
|
207
|
+
* in a flat list of fifty-eight titles — which is the whole readability answer
|
|
208
|
+
* at fleet scale, and it costs the same helpers the subsystem views already use.
|
|
209
|
+
*
|
|
210
|
+
* The leaf is the service's `services/<id>/` DIRECTORY id and deliberately not
|
|
211
|
+
* the map element's title: it is the name every loam command addresses the
|
|
212
|
+
* service by, it cannot drift when somebody renames a box on the map, and a
|
|
213
|
+
* directory name cannot contain `/`, so it needs no folder-separator
|
|
214
|
+
* substitution of its own. It still goes through `likec4String` — an id is a
|
|
215
|
+
* directory name, and a directory name is somebody else's word.
|
|
216
|
+
*/
|
|
217
|
+
function serviceViewTitle(tree, subsystem, id) {
|
|
218
|
+
return [viewTitle(titleChain(tree, subsystem)), likec4String(id)].filter((part) => part !== "").join(" / ");
|
|
219
|
+
}
|
|
220
|
+
/**
|
|
221
|
+
* The MARKED subsystems along a path, outermost first — the chain `viewTitle`
|
|
222
|
+
* composes a folder path out of, for a subsystem's own view and for the view of
|
|
223
|
+
* a service filed beneath it alike.
|
|
147
224
|
*
|
|
148
225
|
* Marked, because an intermediate directory holding no `subsystem.yaml` is
|
|
149
226
|
* `subsystem.unmarked`, an error the walk already names: it is not a group,
|
|
@@ -151,9 +228,9 @@ export function renderSubsystemViews(tree, map) {
|
|
|
151
228
|
* rather than off the path segments, so what the title says and what the
|
|
152
229
|
* finding says about the same directory cannot disagree.
|
|
153
230
|
*/
|
|
154
|
-
function titleChain(tree,
|
|
231
|
+
function titleChain(tree, path) {
|
|
155
232
|
return tree.subsystems
|
|
156
|
-
.filter((s) => s.path.length <=
|
|
233
|
+
.filter((s) => s.path.length <= path.length && s.path.every((name, i) => path[i] === name))
|
|
157
234
|
.sort((a, b) => a.path.length - b.path.length);
|
|
158
235
|
}
|
|
159
236
|
/** The sort key and the comment spelling: the subsystem's path from services/, joined. */
|
|
@@ -164,31 +241,23 @@ function pathKey(sub) {
|
|
|
164
241
|
* A LikeC4 identifier for the view, derived from the subsystem's path so
|
|
165
242
|
* nesting reads in the name — and INJECTIVE over every path the name grammar
|
|
166
243
|
* admits, because the renderer refuses the whole `architecture/` project over
|
|
167
|
-
* a duplicate view id.
|
|
168
|
-
*
|
|
169
|
-
*
|
|
170
|
-
*
|
|
171
|
-
*
|
|
172
|
-
*
|
|
173
|
-
*
|
|
174
|
-
*
|
|
175
|
-
*
|
|
176
|
-
*
|
|
177
|
-
*
|
|
178
|
-
*
|
|
179
|
-
*
|
|
180
|
-
* whose longer hex could in principle re-collide; it is deterministic still,
|
|
181
|
-
* and that directory is already an error finding by name.
|
|
182
|
-
*
|
|
183
|
-
* EXPORTED, with its prefix, because `subsystem.view-id-collision` has to ask
|
|
184
|
-
* the generator what it mints rather than re-deriving it. A check carrying its
|
|
185
|
-
* own copy of the escaping above would go quiet the day the encoding changed,
|
|
186
|
-
* and going quiet is the failure this whole family exists to prevent.
|
|
244
|
+
* a duplicate view id.
|
|
245
|
+
*
|
|
246
|
+
* The per-BYTE encoding, the collision it was written against, and the reason
|
|
247
|
+
* segments join on `__` all live with the encoder in `./service/view-id.ts` —
|
|
248
|
+
* which is also where the per-service ids are minted, so the two generated
|
|
249
|
+
* files cannot drift into two spellings of the same escape. That module sits
|
|
250
|
+
* BELOW this one on purpose: it is the leaf, and the import may only run
|
|
251
|
+
* downward (`./service/views.ts`'s banner records the package direction).
|
|
252
|
+
*
|
|
253
|
+
* EXPORTED here still, with its prefix, because `subsystem.view-id-collision`
|
|
254
|
+
* has to ask the generator what it mints rather than re-deriving it. A check
|
|
255
|
+
* carrying its own copy of the escaping would go quiet the day the encoding
|
|
256
|
+
* changed, and going quiet is the failure this whole family exists to prevent.
|
|
187
257
|
*/
|
|
188
258
|
export const SUBSYSTEM_VIEW_PREFIX = "subsystem_";
|
|
189
259
|
export function subsystemViewId(sub) {
|
|
190
|
-
|
|
191
|
-
return `subsystem_${sub.path.map(segment).join("__")}`;
|
|
260
|
+
return `${SUBSYSTEM_VIEW_PREFIX}${sub.path.map(escapeViewIdSegment).join("__")}`;
|
|
192
261
|
}
|
|
193
262
|
/**
|
|
194
263
|
* Does the file on disk already SAY what `renderSubsystemViews` renders?
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"views.js","sourceRoot":"","sources":["../../../../../src/core/repo/tree/render/views.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * The generated views file: `architecture/subsystems.likec4`, one LikeC4 view\n * per subsystem enumerating the services beneath it. GENERATED, never\n * authored — authored bytes stay authored (archive's verbatim-splice promise),\n * so the tree is mirrored into a file of its own that `loam subsystem sync`\n * owns outright, and staleness against the tree is a byte compare\n * (`subsystem.views-stale` in `validate --all`), never a parse: a spike\n * confirmed a views-only document does NOT parse standalone (its `include`\n * lines reference elements the landscape defines), so nothing in loam ever\n * loads this file (and rule 26 does not change that: this file is GENERATED and\n * holds STATIC views, whose contents loam may not read in any document) — only\n * the LikeC4 renderer, which merges the whole `architecture/` project per\n * likec4.config.json, resolves it.\n *\n * Each view opens with the LABEL a human reads — `title`, and `description`\n * when the marker carries one — because this file exists only to be rendered\n * and an unlabelled view is shown by its id, which is hex-escaped for\n * injectivity and reads as `subsystem_platform__order_2dflow`. Both\n * strings are somebody else's words and go through `likec4String`, which\n * records what a raw one does.\n *\n * The output is deterministic and line-oriented, and both halves are the\n * contract: subsystems sorted by path, members sorted by id, ONE `include`\n * per line — so two concurrent moves into different groups touch different\n * lines and git merges the file without intervention, exactly as the\n * service-grouped landscape splice already achieves. Byte-reproducible on any\n * machine: no timestamps, no absolute paths, no readdir-order dependence (the\n * walk sorts, and every list is re-sorted here). Plain lexicographic sorts on\n * purpose — the ids' own grammar is ASCII, an INVALID name is still ordered\n * deterministically, and `compareIds`' numeric tokenizing lives in `repo/`,\n * which this package must not import (`walk.ts` says why the edge only points\n * the other way).\n *\n * Membership is TRANSITIVE: a subsystem's view includes every service beneath\n * it at any depth, so a parent's view is the whole group's map rather than an\n * empty frame around its children. An empty view body is legal LikeC4 (same\n * spike), so an empty subsystem renders as one — `subsystem new` must be\n * usable before anything moves in.\n *\n * Include lines name the landscape ELEMENT id, resolved through the same\n * element→service join `validate` uses (`serviceResolver`: explicit\n * `metadata { service }` binding first, then a title naming a real service) —\n * because element ids in real landscapes are not service ids, and a view of\n * raw service ids would silently render empty for every such fleet. A member\n * with no landscape element is omitted from the includes: the omission is\n * already an error (`landscape.service-unmodelled`), and rendering only what\n * the committed landscape resolves keeps the output a function of committed\n * bytes alone.\n */\nimport { type LoadedDoc } from \"../../../c4/likec4.js\";\nimport { renderContext, viewBody } from \"./body.js\";\nimport { likec4String, viewTitle } from \"./text.js\";\nimport type { FleetTree, SubsystemEntry } from \"../walk.js\";\n\n/**\n * The global style group a generated view references when the map declares\n * one: `global { styleGroup subsystems { … } }` (or the single-rule form\n * `global { style subsystems … }` — LikeC4 files both under one id table).\n * Presence is the opt-in and it is read off a DECLARATION, never a view: a\n * generated view cannot carry an inline `style` rule of its own (the file is\n * regenerated wholesale), and the one mechanism LikeC4 offers a view for\n * borrowing a repository's palette is a reference to a global style by id.\n * Referencing one that is not declared is a parse error that blanks the whole\n * `architecture/` project in the renderer, which is why the line is written\n * only when the id is declared and why its absence leaves the file exactly\n * what it was before this constant existed.\n *\n * One conventional name rather than \"the single declared group\": a second\n * group somebody declares for a hand-drawn view would otherwise silently\n * switch the generated views' styling OFF and restale the file on a change\n * that touched no subsystem — behaviour flipping on an unrelated declaration\n * is the shape `body.ts` calls \"a picture that looks the same whether or not\n * the two agree\". The name mirrors the file it styles.\n */\nexport const SUBSYSTEM_STYLE_GROUP = \"subsystems\";\n\n/**\n * What the render reads off the parsed map: the elements the member join\n * resolves, and the global style ids a view may reference. A record rather\n * than a third parameter because the two come out of ONE parsed document, and\n * passing them apart would make an inconsistent pair representable. Every\n * `LoadedDoc` is structurally a `MapFacts`, so a caller holding the document\n * passes the document.\n */\nexport type MapFacts = Pick<LoadedDoc, \"elements\" | \"globalStyles\">;\n\n/**\n * The header every generated file opens with — stable bytes, part of the\n * byte-compare contract, naming the regenerator so a reader who found the\n * file first knows what owns it.\n */\nconst HEADER =\n \"// generated by loam — the subsystem tree under services/, one view per subsystem.\\n\" +\n \"// Do not edit: `loam subsystem sync` regenerates this file from the directories,\\n\" +\n \"// and `loam validate --all` reports `subsystem.views-stale` when it drifts.\\n\" +\n \"// A scoping aid, not the fleet's architecture: author your own diagrams elsewhere.\\n\";\n\n/**\n * The one file-level comment the render may write: the map declares global\n * style groups, and none is the conventional one. Written between `HEADER`\n * and `views {` — outside every view body, graded by nothing — so a team\n * that already declared a palette under another name reads the fix in the\n * diff of the sync that regenerated the file, instead of wondering why the\n * generated views ignore it. Silent when the map declares no group at all: a\n * fleet with no palette is owed no sentence about one. Ids go unescaped\n * because LikeC4's grammar admits only identifier bytes in a style id.\n */\nfunction stylesDisclosure(declared: readonly string[]): string | null {\n if (declared.length === 0 || declared.includes(SUBSYSTEM_STYLE_GROUP)) return null;\n return (\n `// styles: the project declares global style group(s) ${[...declared].sort().join(\", \")}, ` +\n `and none named \\`${SUBSYSTEM_STYLE_GROUP}\\` — these views carry the renderer's defaults until one is.`\n );\n}\n\n/**\n * Render the views file for a walked tree, or null when the tree has no\n * subsystems — the file must then be ABSENT, so a flat fleet never carries a\n * generated artifact that says nothing.\n */\nexport function renderSubsystemViews(tree: FleetTree, map: MapFacts): string | null {\n if (tree.subsystems.length === 0) return null;\n const ctx = renderContext(tree, map.elements);\n const declared = map.globalStyles ?? [];\n const styled = declared.includes(SUBSYSTEM_STYLE_GROUP);\n const disclosure = stylesDisclosure(declared);\n const lines: string[] = [HEADER, ...(disclosure === null ? [] : [disclosure]), \"views {\"];\n const subsystems = [...tree.subsystems].sort((a, b) => (pathKey(a) < pathKey(b) ? -1 : 1));\n for (const [i, sub] of subsystems.entries()) {\n if (i > 0) lines.push(\"\");\n lines.push(` // services/${pathKey(sub)}`);\n const body = viewBody(ctx, sub);\n if (body.disclosure !== null) lines.push(` // ${body.disclosure}`);\n lines.push(` view ${subsystemViewId(sub)} {`);\n // Metadata FIRST, and that ordering is a hard constraint rather than a\n // taste: measured, an `include` OR a `group { … }` line followed by\n // `title` is a parse error (\"Expecting token of type '}' but found\n // `title`\"), and one parse error in this file blanks the whole\n // `architecture/` project in the renderer.\n lines.push(` title '${viewTitle(titleChain(tree, sub))}'`);\n const description = likec4String(sub.meta.description ?? \"\");\n if (description !== \"\") lines.push(` description '${description}'`);\n // After the label, before anything that includes: measured at the pin\n // (test/likec4-view-shape.test.ts), a `global style <id>` line here\n // parses with zero errors, while the same line before `title` is the\n // parse error the comment above records. The palette is the map's own —\n // the group is declared in the same project this file renders into — so\n // the reference resolves exactly when the presence test says it does.\n if (styled) lines.push(` global style ${SUBSYSTEM_STYLE_GROUP}`);\n lines.push(...body.lines);\n lines.push(\" }\");\n }\n lines.push(\"}\");\n return lines.join(\"\\n\") + \"\\n\";\n}\n\n/**\n * This subsystem and every MARKED ancestor above it, outermost first — the\n * chain `viewTitle` composes a path out of.\n *\n * Marked, because an intermediate directory holding no `subsystem.yaml` is\n * `subsystem.unmarked`, an error the walk already names: it is not a group,\n * so it contributes no folder level here either. Read off the walked tree\n * rather than off the path segments, so what the title says and what the\n * finding says about the same directory cannot disagree.\n */\nfunction titleChain(tree: FleetTree, sub: SubsystemEntry): SubsystemEntry[] {\n return tree.subsystems\n .filter((s) => s.path.length <= sub.path.length && s.path.every((name, i) => sub.path[i] === name))\n .sort((a, b) => a.path.length - b.path.length);\n}\n\n/** The sort key and the comment spelling: the subsystem's path from services/, joined. */\nfunction pathKey(sub: SubsystemEntry): string {\n return sub.path.join(\"/\");\n}\n\n/**\n * A LikeC4 identifier for the view, derived from the subsystem's path so\n * nesting reads in the name — and INJECTIVE over every path the name grammar\n * admits, because the renderer refuses the whole `architecture/` project over\n * a duplicate view id. The naive folding (every non-identifier byte and the\n * join separator alike becoming `_`) collided three DISTINCT healthy names\n * onto one identifier — `a_b`, and `b` under `a`, both rendered\n * `subsystem_a_b` — so `validate --all` was green while the renderer refused\n * the fleet map; `subsystem.name-collision` never fires because the flat\n * namespace really is intact. Hence the encoding: an identifier byte\n * (`[A-Za-z0-9]`) passes through, any other byte becomes `_` + two lowercase\n * hex digits (`.` → `_2e`, `-` → `_2d`, `_` itself → `_5f` — the underscore\n * must be escaped precisely because it is BOTH legal in a name and the\n * separator), and segments join on `__`. Underscore runs then decode\n * uniquely — one is an escape, two a separator, three a separator followed\n * by an escape — so no two distinct legal paths share an identifier. A name\n * `subsystem.name-invalid` already flagged may hold a code point above 0xff\n * whose longer hex could in principle re-collide; it is deterministic still,\n * and that directory is already an error finding by name.\n *\n * EXPORTED, with its prefix, because `subsystem.view-id-collision` has to ask\n * the generator what it mints rather than re-deriving it. A check carrying its\n * own copy of the escaping above would go quiet the day the encoding changed,\n * and going quiet is the failure this whole family exists to prevent.\n */\nexport const SUBSYSTEM_VIEW_PREFIX = \"subsystem_\";\n\nexport function subsystemViewId(sub: SubsystemEntry): string {\n const segment = (name: string): string =>\n [...name].map((ch) => (/[A-Za-z0-9]/.test(ch) ? ch : `_${ch.codePointAt(0)!.toString(16).padStart(2, \"0\")}`)).join(\"\");\n return `subsystem_${sub.path.map(segment).join(\"__\")}`;\n}\n\n/**\n * Does the file on disk already SAY what `renderSubsystemViews` renders?\n *\n * The generator above only ever emits LF. `core.autocrlf=true` is Git for\n * Windows' installer default and a docs repo ships no `.gitattributes`, so an\n * ordinary Windows clone hands the file back with CRLF and not one fact\n * changed. Four call sites asked this question with a byte equality and all\n * four answered \"stale\" on such a clone: `loam validate` reported an error,\n * `loam subsystem sync` reported `updated` and rewrote with LF, git then showed\n * the file permanently modified, and the next checkout restored the CRLF. That\n * is a loop rather than a diagnosis, which is why the comparison lives here\n * now — the module that mints the bytes is the one that owes an answer about\n * them, and three near-copies of this rule would go stale one at a time.\n *\n * Still never a parse: a views-only document does not parse standalone, and no\n * check anywhere reads this file's content. The narrowing is from BYTES to\n * CONTENT, which is what `subsystem.views-stale`'s own message (\"does not match\n * the tree\") already claims. The symmetric cost is accepted and is the one\n * `core/verify/pins/pin.ts` documents for `pinnedDigest`: a pure line-ending\n * change is invisible, correctly, because it is not a change to what the file\n * says.\n *\n * `null` is carried through as itself rather than collapsing into `\"\"`: absent,\n * must-be-absent and present-but-different are three distinct states with three\n * distinct messages, and a fleet whose views file does not exist is not a fleet\n * whose views file is blank.\n */\nexport function viewsAgree(actual: string | null, expected: string | null): boolean {\n const lf = (text: string | null): string | null => (text === null ? null : text.replace(/\\r\\n|\\r/g, \"\\n\"));\n return lf(actual) === lf(expected);\n}\n"]}
|
|
1
|
+
{"version":3,"file":"views.js","sourceRoot":"","sources":["../../../../../src/core/repo/tree/render/views.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * The generated views file: `architecture/subsystems.likec4`, one LikeC4 view\n * per subsystem enumerating the services beneath it. GENERATED, never\n * authored — authored bytes stay authored (archive's verbatim-splice promise),\n * so the tree is mirrored into a file of its own that `loam subsystem sync`\n * owns outright, and staleness against the tree is a byte compare\n * (`subsystem.views-stale` in `validate --all`), never a parse: a spike\n * confirmed a views-only document does NOT parse standalone (its `include`\n * lines reference elements the landscape defines), so nothing in loam ever\n * loads this file (and rule 26 does not change that: this file is GENERATED and\n * holds STATIC views, whose contents loam may not read in any document) — only\n * the LikeC4 renderer, which merges the whole `architecture/` project per\n * likec4.config.json, resolves it.\n *\n * Each view opens with the LABEL a human reads — `title`, and `description`\n * when the marker carries one — because this file exists only to be rendered\n * and an unlabelled view is shown by its id, which is hex-escaped for\n * injectivity and reads as `subsystem_platform__order_2dflow`. Both\n * strings are somebody else's words and go through `likec4String`, which\n * records what a raw one does.\n *\n * The output is deterministic and line-oriented, and both halves are the\n * contract: subsystems sorted by path, members sorted by id, ONE `include`\n * per line — so two concurrent moves into different groups touch different\n * lines and git merges the file without intervention, exactly as the\n * service-grouped landscape splice already achieves. Byte-reproducible on any\n * machine: no timestamps, no absolute paths, no readdir-order dependence (the\n * walk sorts, and every list is re-sorted here). Plain lexicographic sorts on\n * purpose — the ids' own grammar is ASCII, an INVALID name is still ordered\n * deterministically, and `compareIds`' numeric tokenizing lives in `repo/`,\n * which this package must not import (`walk.ts` says why the edge only points\n * the other way).\n *\n * Membership is TRANSITIVE: a subsystem's view includes every service beneath\n * it at any depth, so a parent's view is the whole group's map rather than an\n * empty frame around its children. An empty view body is legal LikeC4 (same\n * spike), so an empty subsystem renders as one — `subsystem new` must be\n * usable before anything moves in.\n *\n * Include lines name the landscape ELEMENT id, resolved through the same\n * element→service join `validate` uses (`serviceResolver`: explicit\n * `metadata { service }` binding first, then a title naming a real service) —\n * because element ids in real landscapes are not service ids, and a view of\n * raw service ids would silently render empty for every such fleet. A member\n * with no landscape element is omitted from the includes: the omission is\n * already an error (`landscape.service-unmodelled`), and rendering only what\n * the committed landscape resolves keeps the output a function of committed\n * bytes alone.\n */\nimport { type LoadedDoc } from \"../../../c4/likec4.js\";\nimport { renderContext, viewBody } from \"./body.js\";\nimport { GENERATED_MARK } from \"./service/header.js\";\nimport { escapeViewIdSegment } from \"./service/view-id.js\";\nimport {\n renderServiceViews,\n SERVICE_STYLE_GROUP,\n type ServiceViewSubject,\n} from \"./service/views.js\";\nimport { likec4String, viewTitle } from \"./text.js\";\nimport type { FleetTree, SubsystemEntry } from \"../walk.js\";\n\n/**\n * The global style group a generated view references when the map declares\n * one: `global { styleGroup subsystems { … } }` (or the single-rule form\n * `global { style subsystems … }` — LikeC4 files both under one id table).\n * Presence is the opt-in and it is read off a DECLARATION, never a view: a\n * generated view cannot carry an inline `style` rule of its own (the file is\n * regenerated wholesale), and the one mechanism LikeC4 offers a view for\n * borrowing a repository's palette is a reference to a global style by id.\n * Referencing one that is not declared is a parse error that blanks the whole\n * `architecture/` project in the renderer, which is why the line is written\n * only when the id is declared and why its absence leaves the file exactly\n * what it was before this constant existed.\n *\n * One conventional name rather than \"the single declared group\": a second\n * group somebody declares for a hand-drawn view would otherwise silently\n * switch the generated views' styling OFF and restale the file on a change\n * that touched no subsystem — behaviour flipping on an unrelated declaration\n * is the shape `body.ts` calls \"a picture that looks the same whether or not\n * the two agree\". The name mirrors the file it styles.\n */\nexport const SUBSYSTEM_STYLE_GROUP = \"subsystems\";\n\n/**\n * What the render reads off the parsed map: the elements the member join\n * resolves, the RELATIONSHIPS a per-service view computes its peer set from,\n * and the global style ids a view may reference. A record rather than three\n * threaded parameters because all three come out of ONE parsed document, and\n * passing them apart would make an inconsistent pair representable. Every\n * `LoadedDoc` is structurally a `MapFacts`, so a caller holding the document\n * passes the document.\n *\n * `relationships` is REQUIRED, and that is the type doing work rather than a\n * shape falling out of `LoadedDoc`. It was optional once, on the reasoning that\n * the `LoadedDoc` literals standing in for a map that is absent or did not parse\n * have no edges to offer — and the consequence of omitting it is SILENT: every\n * per-service view renders with NO peers, the service and its own interior and\n * nothing it touches, with no error anywhere. Two independent readers found the\n * same caller composing a literal without the field beside a document that\n * carried one (`commands/subsystem/txn/views.ts`'s landscape-only fallback), so\n * documenting the hazard was measurably not enough: a caller with no edges must\n * now say `relationships: []` and mean it, and a caller holding a parsed\n * document passes the document.\n *\n * `globalStyles` stays optional because `LoadedDoc` declares it so and its\n * absence is not silent in the same way: a missing palette costs one `global\n * style` line, which the file-level disclosure comment then names.\n */\nexport type MapFacts = Pick<LoadedDoc, \"elements\" | \"relationships\" | \"globalStyles\">;\n\n/**\n * The header every generated file opens with — stable bytes, part of the\n * byte-compare contract, naming the regenerator so a reader who found the\n * file first knows what owns it.\n *\n * It OPENS with the shared `GENERATED_MARK` rather than spelling it, because\n * the same bytes are what `loam subsystem sync` reads back off disk to decide\n * whether the file standing at this reserved path is loam's to rewrite at all\n * (`./service/header.ts` carries the whole reason). A second literal here is\n * how a file loam wrote stops being recognised as loam's.\n */\nconst HEADER =\n `${GENERATED_MARK} — the subsystem tree under services/, one view per subsystem.\\n` +\n \"// Do not edit: `loam subsystem sync` regenerates this file from the directories,\\n\" +\n \"// and `loam validate --all` reports `subsystem.views-stale` when it drifts.\\n\" +\n \"// A scoping aid, not the fleet's architecture: author your own diagrams elsewhere.\\n\";\n\n/**\n * The one file-level comment the render may write: the map declares global\n * style groups, and none is the conventional one. Written between `HEADER`\n * and `views {` — outside every view body, graded by nothing — so a team\n * that already declared a palette under another name reads the fix in the\n * diff of the sync that regenerated the file, instead of wondering why the\n * generated views ignore it. Silent when the map declares no group at all: a\n * fleet with no palette is owed no sentence about one. Ids go unescaped\n * because LikeC4's grammar admits only identifier bytes in a style id.\n *\n * The group is a PARAMETER because there are two generated files now and each\n * reserves its own name (`subsystems`, `services`); a map declaring one and not\n * the other must get the sentence in exactly the file that went unstyled.\n */\nfunction stylesDisclosure(declared: readonly string[], group: string): string | null {\n if (declared.length === 0 || declared.includes(group)) return null;\n return (\n `// styles: the project declares global style group(s) ${[...declared].sort().join(\", \")}, ` +\n `and none named \\`${group}\\` — these views carry the renderer's defaults until one is.`\n );\n}\n\n/**\n * Render the views file for a walked tree, or null when the tree has no\n * subsystems — the file must then be ABSENT, so a flat fleet never carries a\n * generated artifact that says nothing.\n */\nexport function renderSubsystemViews(tree: FleetTree, map: MapFacts): string | null {\n if (tree.subsystems.length === 0) return null;\n const ctx = renderContext(tree, map.elements);\n const declared = map.globalStyles ?? [];\n const styled = declared.includes(SUBSYSTEM_STYLE_GROUP);\n const disclosure = stylesDisclosure(declared, SUBSYSTEM_STYLE_GROUP);\n const lines: string[] = [HEADER, ...(disclosure === null ? [] : [disclosure]), \"views {\"];\n const subsystems = [...tree.subsystems].sort((a, b) => (pathKey(a) < pathKey(b) ? -1 : 1));\n for (const [i, sub] of subsystems.entries()) {\n if (i > 0) lines.push(\"\");\n lines.push(` // services/${pathKey(sub)}`);\n const body = viewBody(ctx, sub);\n if (body.disclosure !== null) lines.push(` // ${body.disclosure}`);\n // `of <ancestor>` when — and only when — the map holds an element that owns\n // exactly this subsystem's members (`../render/body.ts` argues the\n // \"exactly\"). It is the same one keyword the per-service views spend, and it\n // buys the same thing: LikeC4 computes `navigateTo` into this view from\n // every other view that draws that element, so the fleet map's group boxes\n // become click-through and a neighbouring group box inside one subsystem\n // view opens THAT subsystem's view. Measured at 58 services in 11 groups,\n // 12 of 14 nodes navigable. With no owning ancestor there is no element to\n // be `of`, so there is no navigation — honest, and one more reason to nest\n // the map that loam already discloses one line above.\n lines.push(` view ${subsystemViewId(sub)}${body.of === null ? \"\" : ` of ${body.of}`} {`);\n // Metadata FIRST, and that ordering is a hard constraint rather than a\n // taste: measured, an `include` OR a `group { … }` line followed by\n // `title` is a parse error (\"Expecting token of type '}' but found\n // `title`\"), and one parse error in this file blanks the whole\n // `architecture/` project in the renderer.\n lines.push(` title '${viewTitle(titleChain(tree, sub.path))}'`);\n const description = likec4String(sub.meta.description ?? \"\");\n if (description !== \"\") lines.push(` description '${description}'`);\n // After the label, before anything that includes: measured at the pin\n // (test/likec4-view-shape.test.ts), a `global style <id>` line here\n // parses with zero errors, while the same line before `title` is the\n // parse error the comment above records. The palette is the map's own —\n // the group is declared in the same project this file renders into — so\n // the reference resolves exactly when the presence test says it does.\n if (styled) lines.push(` global style ${SUBSYSTEM_STYLE_GROUP}`);\n lines.push(...body.lines);\n lines.push(\" }\");\n }\n lines.push(\"}\");\n return lines.join(\"\\n\") + \"\\n\";\n}\n\n/**\n * The GENERATED per-service overviews — `architecture/services.likec4`, one\n * view per service the map resolves. Null when it resolves none, so the file is\n * then absent exactly as `subsystems.likec4` is for a tree with no subsystems.\n *\n * This function is the composer and nothing else: `./service/` owns the view\n * shape and its measurements, and everything about the TREE is resolved here,\n * because that package may not import `./body.ts` or `./text.ts` (the edge runs\n * the other way — `./service/views.ts` says why) and duplicating either would\n * put a second element→service join and a second string escaper in the tree.\n *\n * Emission is gated on the SAME join the subsystem views use: a service the map\n * does not draw is omitted rather than invented, because that omission is\n * already `landscape.service-unmodelled` and rendering only what the committed\n * map resolves keeps the output a function of committed bytes alone.\n */\nexport function renderServiceOverviews(tree: FleetTree, map: MapFacts): string | null {\n const ctx = renderContext(tree, map.elements);\n const declared = map.globalStyles ?? [];\n const subjects: ServiceViewSubject[] = [];\n for (const service of tree.services) {\n const fqn = ctx.elementOf.get(service.id);\n if (fqn === undefined) continue;\n subjects.push({ id: service.id, fqn, title: serviceViewTitle(tree, service.subsystem, service.id) });\n }\n return renderServiceViews({\n subjects,\n relationships: map.relationships,\n styled: declared.includes(SERVICE_STYLE_GROUP),\n disclosure: stylesDisclosure(declared, SERVICE_STYLE_GROUP),\n });\n}\n\n/**\n * A per-service view's title: its subsystem path and then its own id, joined\n * with ` / ` — `Platform / order-flow / checkout-web`, or bare `checkout-web`\n * for a service filed directly under `services/`.\n *\n * LikeC4 reads that as a folder path (`./text.ts` measured it), so a service's\n * overview lands in the SAME browser folder as its subsystem's view rather than\n * in a flat list of fifty-eight titles — which is the whole readability answer\n * at fleet scale, and it costs the same helpers the subsystem views already use.\n *\n * The leaf is the service's `services/<id>/` DIRECTORY id and deliberately not\n * the map element's title: it is the name every loam command addresses the\n * service by, it cannot drift when somebody renames a box on the map, and a\n * directory name cannot contain `/`, so it needs no folder-separator\n * substitution of its own. It still goes through `likec4String` — an id is a\n * directory name, and a directory name is somebody else's word.\n */\nfunction serviceViewTitle(tree: FleetTree, subsystem: string[], id: string): string {\n return [viewTitle(titleChain(tree, subsystem)), likec4String(id)].filter((part) => part !== \"\").join(\" / \");\n}\n\n/**\n * The MARKED subsystems along a path, outermost first — the chain `viewTitle`\n * composes a folder path out of, for a subsystem's own view and for the view of\n * a service filed beneath it alike.\n *\n * Marked, because an intermediate directory holding no `subsystem.yaml` is\n * `subsystem.unmarked`, an error the walk already names: it is not a group,\n * so it contributes no folder level here either. Read off the walked tree\n * rather than off the path segments, so what the title says and what the\n * finding says about the same directory cannot disagree.\n */\nfunction titleChain(tree: FleetTree, path: readonly string[]): SubsystemEntry[] {\n return tree.subsystems\n .filter((s) => s.path.length <= path.length && s.path.every((name, i) => path[i] === name))\n .sort((a, b) => a.path.length - b.path.length);\n}\n\n/** The sort key and the comment spelling: the subsystem's path from services/, joined. */\nfunction pathKey(sub: SubsystemEntry): string {\n return sub.path.join(\"/\");\n}\n\n/**\n * A LikeC4 identifier for the view, derived from the subsystem's path so\n * nesting reads in the name — and INJECTIVE over every path the name grammar\n * admits, because the renderer refuses the whole `architecture/` project over\n * a duplicate view id.\n *\n * The per-BYTE encoding, the collision it was written against, and the reason\n * segments join on `__` all live with the encoder in `./service/view-id.ts` —\n * which is also where the per-service ids are minted, so the two generated\n * files cannot drift into two spellings of the same escape. That module sits\n * BELOW this one on purpose: it is the leaf, and the import may only run\n * downward (`./service/views.ts`'s banner records the package direction).\n *\n * EXPORTED here still, with its prefix, because `subsystem.view-id-collision`\n * has to ask the generator what it mints rather than re-deriving it. A check\n * carrying its own copy of the escaping would go quiet the day the encoding\n * changed, and going quiet is the failure this whole family exists to prevent.\n */\nexport const SUBSYSTEM_VIEW_PREFIX = \"subsystem_\";\n\nexport function subsystemViewId(sub: SubsystemEntry): string {\n return `${SUBSYSTEM_VIEW_PREFIX}${sub.path.map(escapeViewIdSegment).join(\"__\")}`;\n}\n\n/**\n * Does the file on disk already SAY what `renderSubsystemViews` renders?\n *\n * The generator above only ever emits LF. `core.autocrlf=true` is Git for\n * Windows' installer default and a docs repo ships no `.gitattributes`, so an\n * ordinary Windows clone hands the file back with CRLF and not one fact\n * changed. Four call sites asked this question with a byte equality and all\n * four answered \"stale\" on such a clone: `loam validate` reported an error,\n * `loam subsystem sync` reported `updated` and rewrote with LF, git then showed\n * the file permanently modified, and the next checkout restored the CRLF. That\n * is a loop rather than a diagnosis, which is why the comparison lives here\n * now — the module that mints the bytes is the one that owes an answer about\n * them, and three near-copies of this rule would go stale one at a time.\n *\n * Still never a parse: a views-only document does not parse standalone, and no\n * check anywhere reads this file's content. The narrowing is from BYTES to\n * CONTENT, which is what `subsystem.views-stale`'s own message (\"does not match\n * the tree\") already claims. The symmetric cost is accepted and is the one\n * `core/verify/pins/pin.ts` documents for `pinnedDigest`: a pure line-ending\n * change is invisible, correctly, because it is not a change to what the file\n * says.\n *\n * `null` is carried through as itself rather than collapsing into `\"\"`: absent,\n * must-be-absent and present-but-different are three distinct states with three\n * distinct messages, and a fleet whose views file does not exist is not a fleet\n * whose views file is blank.\n */\nexport function viewsAgree(actual: string | null, expected: string | null): boolean {\n const lf = (text: string | null): string | null => (text === null ? null : text.replace(/\\r\\n|\\r/g, \"\\n\"));\n return lf(actual) === lf(expected);\n}\n"]}
|
|
@@ -28,7 +28,7 @@ import { readFile } from "node:fs/promises";
|
|
|
28
28
|
import { readAsyncapi } from "../asyncapi/read.js";
|
|
29
29
|
import {} from "../c4/likec4.js";
|
|
30
30
|
import { elementService, serviceResolver } from "../c4/resolve/service.js";
|
|
31
|
-
import { ACTOR_KINDS } from "../vocabulary/maturity.js";
|
|
31
|
+
import { ACTOR_KINDS, INFRA_KINDS } from "../vocabulary/maturity.js";
|
|
32
32
|
import { operationIds, operations } from "../openapi/doc.js";
|
|
33
33
|
import { featurePaths, featureSpecPaths } from "../repo/paths.js";
|
|
34
34
|
import { locateServicePaths } from "../repo/service-target.js";
|
|
@@ -78,25 +78,57 @@ export async function featureChecklist(docsDir, featureDir, featureId, fleet) {
|
|
|
78
78
|
// reports it; inventing claims out of a broken document would only add noise.
|
|
79
79
|
if (res.errors.length === 0) {
|
|
80
80
|
const elements = res.elements;
|
|
81
|
+
// The enumerated fleet — `services/` plus this feature's own `specs/`
|
|
82
|
+
// directories, the only place a service the feature INTRODUCES has a
|
|
83
|
+
// directory at all. Built once, above both readers below, because they
|
|
84
|
+
// must not disagree: the skip loop asks whether an element STANDS FOR a
|
|
85
|
+
// service, and the resolver asks WHICH service an endpoint belongs to,
|
|
86
|
+
// and a set that answered the second question only left the first one
|
|
87
|
+
// reading a title as unknown that the very next line resolved.
|
|
88
|
+
const known = new Set([
|
|
89
|
+
...(await enumeratedServiceIds(docsDir, ctx)),
|
|
90
|
+
...specServices,
|
|
91
|
+
]);
|
|
81
92
|
for (const e of elements) {
|
|
82
93
|
if (!e.tags.includes(featureId))
|
|
83
94
|
continue;
|
|
84
|
-
// A dotted id is a container INSIDE a service,
|
|
85
|
-
// repository
|
|
86
|
-
|
|
95
|
+
// A dotted id is a container INSIDE a service, an actor is nobody's
|
|
96
|
+
// repository, and the fleet's own infrastructure — a `database`,
|
|
97
|
+
// `queue`, `topic`, `cache` or `bucket` — has no repository either.
|
|
98
|
+
// None of the three is a service that has to exist.
|
|
99
|
+
//
|
|
100
|
+
// The infra skip closes a claim no repository could ever answer: a
|
|
101
|
+
// delta that draws a fleet-level `database` minted `service.exists` for
|
|
102
|
+
// it, and `loam verify` then asked somebody to point at the code that
|
|
103
|
+
// proves a Postgres has a services/ directory. It is the SAME shape as
|
|
104
|
+
// the container-and-actor skips beside it — an element that is not a
|
|
105
|
+
// team's system — not a new kind of exemption.
|
|
106
|
+
//
|
|
107
|
+
// STANDING FOR A SERVICE still wins, because that is a claim about
|
|
108
|
+
// this repo rather than a guess at one, and it is the same predicate
|
|
109
|
+
// every other surface applies — `commands/validate/fleet/census.ts`'s
|
|
110
|
+
// `standsForService`, spelled here rather than imported because core
|
|
111
|
+
// may not reach into commands. Both of its rungs matter: a `database`
|
|
112
|
+
// carrying `metadata { service '<id>' }` names a directory the author
|
|
113
|
+
// says exists, and one whose TITLE names a real `services/<id>/` names
|
|
114
|
+
// a directory the enumeration just read. Testing the binding alone made
|
|
115
|
+
// an adopted store a service on every other surface and infrastructure
|
|
116
|
+
// here, so its `service.exists` claim silently went missing from the
|
|
117
|
+
// checklist that is supposed to hold every promise the feature made.
|
|
118
|
+
const standsFor = e.service !== undefined || known.has(e.title);
|
|
119
|
+
const infra = !standsFor && INFRA_KINDS.has(e.kind.toLowerCase());
|
|
120
|
+
if (e.id.includes(".") || ACTOR_KINDS.has(e.kind.toLowerCase()) || infra)
|
|
87
121
|
continue;
|
|
88
122
|
const svc = elementService(e);
|
|
89
123
|
exists.push(claim("service.exists", svc, [svc], `service '${svc}' exists`));
|
|
90
124
|
}
|
|
91
|
-
//
|
|
92
|
-
//
|
|
93
|
-
//
|
|
94
|
-
// drawn into a modelled container names the service that owns it. A
|
|
95
|
-
// claim against a service called "api" that has never existed is one no
|
|
125
|
+
// That same set rides into the resolver so a claim about an edge drawn
|
|
126
|
+
// into a modelled container names the service that owns it. A claim
|
|
127
|
+
// against a service called "api" that has never existed is one no
|
|
96
128
|
// evidence in any repository could ever answer, and the fleet half alone
|
|
97
129
|
// still derived exactly that claim for an introduced service's own
|
|
98
130
|
// container. Unenumerable services/ degrades to the feature's names.
|
|
99
|
-
const svcOf = serviceResolver(elements,
|
|
131
|
+
const svcOf = serviceResolver(elements, known);
|
|
100
132
|
for (const r of res.relationships) {
|
|
101
133
|
// Untagged edges are context for the diagram; an edge with no operation
|
|
102
134
|
// names nothing specific enough to look for in the code.
|