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

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 (67) hide show
  1. package/CHANGELOG.md +124 -0
  2. package/MIGRATING-from-OpenSpec.md +4 -1
  3. package/README.md +11 -10
  4. package/ROADMAP.md +103 -17
  5. package/SCHEMA.md +86 -7
  6. package/dist/commands/archive/run.js +46 -2
  7. package/dist/commands/archive/run.js.map +1 -1
  8. package/dist/commands/seed/plan.js +1 -1
  9. package/dist/commands/seed/plan.js.map +1 -1
  10. package/dist/commands/subsystem/sync.js +111 -20
  11. package/dist/commands/subsystem/sync.js.map +1 -1
  12. package/dist/commands/subsystem/txn/txn.js +20 -1
  13. package/dist/commands/subsystem/txn/txn.js.map +1 -1
  14. package/dist/commands/subsystem/txn/views.js +23 -11
  15. package/dist/commands/subsystem/txn/views.js.map +1 -1
  16. package/dist/commands/validate/fleet/landscape.js +6 -0
  17. package/dist/commands/validate/fleet/landscape.js.map +1 -1
  18. package/dist/commands/validate/fleet/views/ids.js +6 -1
  19. package/dist/commands/validate/fleet/views/ids.js.map +1 -1
  20. package/dist/commands/validate/fleet/views/projects.js +90 -0
  21. package/dist/commands/validate/fleet/views/projects.js.map +1 -0
  22. package/dist/commands/validate/fleet/views/stale.js +15 -39
  23. package/dist/commands/validate/fleet/views/stale.js.map +1 -1
  24. package/dist/core/agent/agents-md/command-map.js +3 -1
  25. package/dist/core/agent/agents-md/command-map.js.map +1 -1
  26. package/dist/core/agent/agents-md/map/subsystem.js +18 -2
  27. package/dist/core/agent/agents-md/map/subsystem.js.map +1 -1
  28. package/dist/core/agent/agents-md.js +4 -0
  29. package/dist/core/agent/agents-md.js.map +1 -1
  30. package/dist/core/agent/workflows/adopt.js +13 -3
  31. package/dist/core/agent/workflows/adopt.js.map +1 -1
  32. package/dist/core/agent/workflows/check.js +1 -0
  33. package/dist/core/agent/workflows/check.js.map +1 -1
  34. package/dist/core/brief/checks.js +9 -0
  35. package/dist/core/brief/checks.js.map +1 -1
  36. package/dist/core/brief/targets.js +7 -4
  37. package/dist/core/brief/targets.js.map +1 -1
  38. package/dist/core/brief/unchecked.js +1 -1
  39. package/dist/core/brief/unchecked.js.map +1 -1
  40. package/dist/core/docs.js +11 -7
  41. package/dist/core/docs.js.map +1 -1
  42. package/dist/core/doctor/doctor.js +17 -6
  43. package/dist/core/doctor/doctor.js.map +1 -1
  44. package/dist/core/explain/families.js +1 -1
  45. package/dist/core/explain/families.js.map +1 -1
  46. package/dist/core/repo/paths.js +39 -1
  47. package/dist/core/repo/paths.js.map +1 -1
  48. package/dist/core/repo/tree/render/body.js +219 -0
  49. package/dist/core/repo/tree/render/body.js.map +1 -0
  50. package/dist/core/repo/tree/render/projects.js +124 -0
  51. package/dist/core/repo/tree/render/projects.js.map +1 -0
  52. package/dist/core/repo/tree/render/stale.js +26 -0
  53. package/dist/core/repo/tree/render/stale.js.map +1 -0
  54. package/dist/core/repo/tree/render/text.js +84 -0
  55. package/dist/core/repo/tree/render/text.js.map +1 -0
  56. package/dist/core/repo/tree/{views.js → render/views.js} +38 -34
  57. package/dist/core/repo/tree/render/views.js.map +1 -0
  58. package/examples/README.md +6 -1
  59. package/examples/docs/architecture/subsystems.likec4 +4 -0
  60. package/examples/docs/services/checkout-web/likec4.config.json +4 -0
  61. package/examples/docs/services/order-service/likec4.config.json +4 -0
  62. package/examples/docs/services/payment-service/likec4.config.json +4 -0
  63. package/examples/docs/services/payment-service/model.likec4 +4 -2
  64. package/examples/docs/services/platform/identity-service/likec4.config.json +4 -0
  65. package/examples/docs/services/platform/notification-service/likec4.config.json +4 -0
  66. package/package.json +1 -1
  67. package/dist/core/repo/tree/views.js.map +0 -1
@@ -0,0 +1 @@
1
+ {"version":3,"file":"body.js","sourceRoot":"","sources":["../../../../../src/core/repo/tree/render/body.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * What one subsystem's generated view SHOWS: which boxes it draws around the\n * services beneath a directory, and the one thing it may say about the map.\n *\n * Split from `../views.ts` when that file crossed the 400-line limit: the file\n * render (header, view ids, staleness comparison) and the body render (the\n * boxes) are two subjects, and the seam is exactly where the second one\n * consults the LANDSCAPE. Everything here reads element ids and nothing else —\n * no view is ever parsed (docs/DESIGN.md rule 26), and the generated file\n * stays a pure function of the tree plus the committed map.\n */\nimport { type Elem } from \"../../../c4/likec4.js\";\nimport { serviceResolver } from \"../../../c4/resolve/service.js\";\nimport { servicesUnder, withinSubsystem } from \"../find.js\";\nimport type { FleetTree, SubsystemEntry, WalkedService } from \"../walk.js\";\nimport { subsystemLabel } from \"./text.js\";\n\n/**\n * Everything a view's body needs from the landscape, resolved ONCE: the\n * element standing for each service, every element id the map declares, and\n * whether an element is a service's own box. A record rather than four\n * threaded parameters because the body render recurses down the subsystem\n * subtree and would otherwise sit on the parameter limit at every hop\n * (`docs/CODE-STYLE.md`'s own prescription).\n */\nexport interface RenderContext {\n tree: FleetTree;\n elementOf: Map<string, string>;\n elementIds: ReadonlySet<string>;\n isServiceElement: (id: string) => boolean;\n}\n\nexport function renderContext(tree: FleetTree, elements: Elem[]): RenderContext {\n const known: ReadonlySet<string> = new Set(tree.services.map((s) => s.id));\n const resolve = serviceResolver(elements, known);\n return {\n tree,\n elementOf: elementByService(tree, elements, resolve),\n elementIds: new Set(elements.map((e) => e.id)),\n isServiceElement: (id: string): boolean => known.has(resolve(id)),\n };\n}\n\n/** A view's body lines, and the one comment the render may write above it. */\nexport interface ViewBody {\n lines: string[];\n disclosure: string | null;\n}\n\n/**\n * Three shapes, and which one applies is a fact about the landscape rather\n * than a setting:\n *\n * 1. **The model draws this group.** Some ancestor element holds exactly this\n * subsystem's members and nothing else, so the view includes it beside the\n * leaves and the renderer nests them inside the box the AUTHOR drew, with\n * the author's own title. Measured: including the ancestor alone renders\n * one empty box and the leaves alone render flat — both lines are needed.\n * 2. **The model is flat.** No member element is nested at all, so there is\n * no authored box to agree with, and the view wraps the members in a\n * `group '<title>'` of its own — a real named box in the renderer, derived\n * from the DIRECTORY the services already sit in, mirroring the subtree so\n * a parent subsystem shows its children as boxes rather than as one pile.\n * Nothing is invented: the grouping is the tree, and the tree is what this\n * file has always mirrored.\n * 3. **The model nests them, differently.** The members ARE nested but no\n * ancestor owns exactly them — the directory tree and the map disagree\n * about this group. loam draws no box (a synthetic one would silently\n * replace the author's boundary with the folder's, and the picture would\n * look the same whether or not the two agree) and writes ONE comment line\n * saying so, outside the view body. That comment is the whole of loam's\n * answer to the drift: it grades nothing, it fails nothing, and it appears\n * in the diff of the very commit that caused it — placement stays a fact\n * nothing branches on.\n */\nexport function viewBody(ctx: RenderContext, sub: SubsystemEntry): ViewBody {\n const members = memberElements(ctx, sub);\n if (members.length === 0) return { lines: [], disclosure: null };\n if (members.every((id) => !id.includes(\".\"))) return { lines: groupedBody(ctx, sub, \" \"), disclosure: null };\n const lines = [...ownedAncestors(ctx, sub), ...members].map((id) => ` include ${id}`);\n if (owningAncestor(ctx, sub) !== null) return { lines, disclosure: null };\n const shared = sharedAncestor(ctx, members);\n return {\n lines,\n disclosure:\n shared === null\n ? \"model: no boundary — these members do not share a containing element.\"\n : `model: no boundary — ${shared} also holds services outside this subsystem.`,\n };\n}\n\n/**\n * The flat-model body: this subsystem as a box, its child subsystems as boxes\n * inside it, its own services as leaves. Recursive, and the recursion is the\n * point — a parent subsystem's view is otherwise one undifferentiated pile of\n * every service beneath it.\n *\n * An empty child renders an empty named box, which is what the tree says: the\n * group exists and holds nothing yet.\n */\nfunction groupedBody(ctx: RenderContext, sub: SubsystemEntry, indent: string): string[] {\n const inner = `${indent} `;\n const out = [`${indent}group '${subsystemLabel(sub)}' {`];\n for (const child of childSubsystems(ctx, sub)) out.push(...groupedBody(ctx, child, inner));\n for (const id of directMemberElements(ctx, sub)) out.push(`${inner}include ${id}`);\n out.push(`${indent}}`);\n return out;\n}\n\n/** The child subsystems one level down, ordered by name. */\nfunction childSubsystems(ctx: RenderContext, sub: SubsystemEntry): SubsystemEntry[] {\n return ctx.tree.subsystems\n .filter((c) => c.path.length === sub.path.length + 1 && withinSubsystem(sub.path, c.path))\n .sort((a, b) => (a.name < b.name ? -1 : 1));\n}\n\n/** The drawn elements of the services filed in THIS directory, not in a child. */\nfunction directMemberElements(ctx: RenderContext, sub: SubsystemEntry): string[] {\n return drawnElements(\n ctx,\n membersOf(ctx.tree, sub).filter((s) => s.subsystem.length === sub.path.length),\n );\n}\n\n/** The drawn elements of every service beneath this subsystem, ordered by service id. */\nfunction memberElements(ctx: RenderContext, sub: SubsystemEntry): string[] {\n return drawnElements(ctx, membersOf(ctx.tree, sub));\n}\n\n/**\n * The element ids these services resolve to. A service the map does not draw\n * is omitted rather than invented: the omission is already an error\n * (`landscape.service-unmodelled`), and rendering only what the committed\n * documents resolve keeps the output a function of committed bytes alone.\n */\nfunction drawnElements(ctx: RenderContext, services: WalkedService[]): string[] {\n return services.map((s) => ctx.elementOf.get(s.id)).filter((id): id is string => id !== undefined);\n}\n\n/**\n * The boxes a nested-model view draws: this subsystem's own owning ancestor\n * when it has one, plus the owning ancestor of every subsystem beneath it — so\n * a parent view keeps its children's boundaries instead of flattening them\n * into one box. Sorted and de-duplicated, because two subsystems resolve to\n * one element only in a tree that already reports a collision.\n */\nfunction ownedAncestors(ctx: RenderContext, sub: SubsystemEntry): string[] {\n const found = new Set<string>();\n for (const candidate of ctx.tree.subsystems) {\n if (!withinSubsystem(sub.path, candidate.path)) continue;\n const owner = owningAncestor(ctx, candidate);\n if (owner !== null) found.add(owner);\n }\n return [...found].sort();\n}\n\n/**\n * The deepest ancestor element that holds exactly this subsystem's members —\n * or null when no element does.\n *\n * \"Exactly\" is the whole guard, and dropping it was measured on loam's own\n * `examples/docs`: the members' longest common ancestor there is a group of\n * five services while the subsystem holds two, so an unguarded include would\n * generate a box labelled for the group with two of its five children drawn\n * inside it — a diagram asserting something false, in a file the reader is\n * told not to edit. Tightness is judged over ELEMENTS rather than over\n * documented services: mid-adoption a group commonly holds systems no\n * `services/<id>/` exists for yet, and counting only the documented ones would\n * draw that same false box for the whole of an adoption.\n *\n * An ancestor that is itself a service's element is refused separately: one\n * service's box containing another's is a picture of nothing.\n */\nfunction owningAncestor(ctx: RenderContext, sub: SubsystemEntry): string | null {\n const members = memberElements(ctx, sub);\n if (members.length === 0) return null;\n const owned = new Set(members);\n for (const candidate of ancestorChain(members)) {\n if (!ctx.elementIds.has(candidate) || ctx.isServiceElement(candidate)) continue;\n if (ownsExactly(ctx, candidate, owned)) return candidate;\n }\n return null;\n}\n\n/** The deepest common ancestor the map declares at all — the disclosure's subject. */\nfunction sharedAncestor(ctx: RenderContext, members: string[]): string | null {\n return ancestorChain(members).find((id) => ctx.elementIds.has(id)) ?? null;\n}\n\n/** The common proper ancestors of every member id, deepest first. */\nfunction ancestorChain(members: string[]): string[] {\n const segments = members.map((id) => id.split(\".\"));\n let common = Math.min(...segments.map((s) => s.length - 1));\n for (let i = 0; i < common; i++) {\n if (!segments.every((s) => s[i] === segments[0]![i])) {\n common = i;\n break;\n }\n }\n const out: string[] = [];\n for (let depth = common; depth > 0; depth--) out.push(segments[0]!.slice(0, depth).join(\".\"));\n return out;\n}\n\n/**\n * Does every element beneath `ancestor` belong to this subsystem — as a\n * member, as one of a member's own containers, or as a container of one?\n */\nfunction ownsExactly(ctx: RenderContext, ancestor: string, members: ReadonlySet<string>): boolean {\n const prefix = `${ancestor}.`;\n for (const id of ctx.elementIds) {\n if (!id.startsWith(prefix)) continue;\n const claimed = [...members].some((m) => id === m || id.startsWith(`${m}.`) || m.startsWith(`${id}.`));\n if (!claimed) return false;\n }\n return true;\n}\n\n/**\n * The landscape element standing for each service id: for every element,\n * resolve it through the shared join, and let the SHALLOWEST element win\n * (document order breaking ties). Shallowest, because `serviceResolver`\n * resolves a service's own containers to the service too, and a view should\n * include the service-level box, not whichever nested container happened to be\n * declared first.\n */\nfunction elementByService(\n tree: FleetTree,\n elements: Elem[],\n resolve: (id: string) => string,\n): Map<string, string> {\n const known: ReadonlySet<string> = new Set(tree.services.map((s) => s.id));\n const depth = (id: string): number => id.split(\".\").length;\n const out = new Map<string, string>();\n for (const e of elements) {\n const service = resolve(e.id);\n if (!known.has(service)) continue;\n const held = out.get(service);\n if (held === undefined || depth(e.id) < depth(held)) out.set(service, e.id);\n }\n return out;\n}\n\n/** Services beneath this subsystem at any depth, ordered by id — the transitive membership. */\nfunction membersOf(tree: FleetTree, sub: SubsystemEntry): WalkedService[] {\n return [...servicesUnder(tree, sub)].sort((a, b) => (a.id < b.id ? -1 : 1));\n}\n"]}
@@ -0,0 +1,124 @@
1
+ /**
2
+ * The renderer's project file for ONE service — `services/<…>/<id>/likec4.config.json`
3
+ * — the second thing under `services/` that exists only to be rendered.
4
+ *
5
+ * Why it exists. The root `likec4.config.json` scopes the fleet project to
6
+ * `architecture/` and excludes `services/**`, and must: loam parses every
7
+ * `.likec4` file ALONE, so each `model.likec4` declares its own `specification`
8
+ * block, and a renderer merging the tree reports every declaration as a
9
+ * duplicate. The cost of that exclusion went unstated for a release: an
10
+ * adopted service's model — the artifact `loam adopt` spends the most effort
11
+ * on — was a box on the fleet map with nothing renderable inside it from the
12
+ * docs root, and the documented way to see it was one renderer invocation per
13
+ * service directory. Measured at the 1.59.2 pin: a `likec4.config.json` INSIDE
14
+ * a service directory is registered as a project of its own even though the
15
+ * root excludes the directory (the root's `exclude` does not hide a nested
16
+ * project file), the workspace then loads `fleet` plus one project per service
17
+ * with zero errors, and each service project carries LikeC4's own `index` view
18
+ * of the containers beside whatever view the model authors. One two-key file
19
+ * per service directory is therefore what makes the model renderable from the
20
+ * same root, in the same renderer, beside the map.
21
+ *
22
+ * CREATE-ONLY, never compared, never rewritten, never removed — the root
23
+ * config's ownership rule (`core/docs.ts`: written once, never re-read, the
24
+ * team owns it), NOT the generated views file's. The bytes are a function of
25
+ * the service id alone, which SCHEMA.md defines as the identity no verb ever
26
+ * changes, so nothing can make the file stale; a compare would only fight the
27
+ * per-project keys LikeC4 lets a team add (`title`, styles, a contact), and
28
+ * would restale every service on any change to this rule. Presence is the one
29
+ * question loam owes the file, and presence needs no parse: rule 26 stands,
30
+ * and loam still reads none of it. A leftover file after a model is deleted is
31
+ * the team's, and harmless — measured, a nested project holding no `.likec4`
32
+ * file is silently absent from the renderer's project list.
33
+ *
34
+ * THE NAME. LikeC4's project-name grammar (measured at the pin) refuses an
35
+ * empty name, the word `default`, and the bytes `.`, `@` and `#`; loam's
36
+ * service-id grammar admits `.`. So every `.` becomes `-`, and a name that
37
+ * would then equal the ROOT project's (`fleet` — a nested project of that name
38
+ * is silently registered as `fleet-1`) or LikeC4's reserved `default` is
39
+ * prefixed `service-`. Readable and typeable after `--project`, and NOT
40
+ * injective: an id spelled a.b beside a service literally named a-b, or
41
+ * `service-fleet` beside `fleet`, share a name and the renderer suffixes the
42
+ * later one `-1`. That is disclosed rather than escaped away, because the
43
+ * picker shows the `title` (measured: `title ?? id`), which is ALWAYS the id
44
+ * verbatim, and a collision costs one suffixed label rather than the blanked
45
+ * workspace that made `subsystemViewId` injective. The root's name arrives as
46
+ * a parameter, like every spelling this package needs from `repo/`: `render/`
47
+ * cannot import `repo/paths.ts` (`stale.ts` records the cycle), and a second
48
+ * copy of the word `fleet` here would be the drift the injection prevents.
49
+ *
50
+ * A DIRECTORY THAT IS NOT A LEGAL SERVICE ID IS OWED NO FILE. The tree walk
51
+ * enumerates every directory on purpose — the fleet is never reported smaller
52
+ * than it is — and `service.id-invalid` names the illegal ones. Folding only
53
+ * `.` is right for every LEGAL id, whose remaining bytes LikeC4 accepts; a
54
+ * name like `pay@1` would produce a project file the renderer silently drops
55
+ * (measured: zero errors, the project simply absent from the list), and
56
+ * because the file is create-only the grade below would then be silent
57
+ * forever while the model stayed unrendered — the exact state this file
58
+ * exists to remove, with a label saying it was fixed. So the survey skips
59
+ * such a service: the writer creates nothing, the grader says nothing, and
60
+ * `service.id-invalid`'s own repair — the rename — is what earns the file on
61
+ * the first sync after it.
62
+ *
63
+ * NO MODEL, NO FILE: a service directory without `model.likec4` — one an
64
+ * archive materialised, a spec-only baseline — is owed nothing, and gets its
65
+ * file on the first `loam subsystem sync` after the model lands. A feature's
66
+ * `delta.likec4` is deliberately out of scope: the directory is transient and
67
+ * `loam archive` moves it where a project file would still be registered, so
68
+ * every shipped feature would keep a dead project in the list forever.
69
+ */
70
+ import { existsSync } from "node:fs";
71
+ import { dirNameHazard } from "../../../kernel/ids/service.js";
72
+ /** LikeC4's own reserved project name; a file naming it is refused outright. */
73
+ const LIKEC4_RESERVED_PROJECT = "default";
74
+ /**
75
+ * The project name for a service id — the id itself for every id made of
76
+ * letters, digits, `_` and `-`, which is nearly all of them.
77
+ */
78
+ export function serviceProjectName(id, rootProject) {
79
+ const folded = id.replace(/\./g, "-");
80
+ return folded === rootProject || folded === LIKEC4_RESERVED_PROJECT ? `service-${folded}` : folded;
81
+ }
82
+ /**
83
+ * The exact bytes of a service's project file: two keys, two-space JSON, LF,
84
+ * one trailing newline — the root file's own serialisation. `title` is the id
85
+ * verbatim, always, so whatever the renderer labels a project with is the
86
+ * service's real identity even where `name` had to bend.
87
+ */
88
+ export function renderServiceProject(id, rootProject) {
89
+ return `${JSON.stringify({ name: serviceProjectName(id, rootProject), title: id }, null, 2)}\n`;
90
+ }
91
+ /**
92
+ * ONE predicate for the writer (`subsystem sync`) and the grader
93
+ * (`validate --all`), on `viewsState`'s precedent — two private spellings of
94
+ * "is this model a project" would be two chances to answer differently about
95
+ * one file, and a grader naming a gap the writer would not fill is a loop no
96
+ * command can clear. Both the gaps AND the count live here for the same
97
+ * reason: a caller re-deriving "has a model" beside this function is the
98
+ * second spelling in another coat.
99
+ *
100
+ * Structural over `{ id, dir }` so a walked service and a full entry both fit,
101
+ * and the two file spellings arrive injected (`repo/paths.ts`'s
102
+ * `serviceRenderPaths`) for the package reason the banner gives. Sorted by id,
103
+ * plain lexicographic like every list this package emits, so the writer's
104
+ * report and the grader's findings come out in one stable order.
105
+ */
106
+ export function surveyProjects(services, pathsOf) {
107
+ const gaps = [];
108
+ let modelled = 0;
109
+ for (const service of services) {
110
+ // The banner's rule: a name the renderer would refuse earns no file, and
111
+ // `service.id-invalid` — not this survey — is what names the repair.
112
+ if (dirNameHazard(service.id) !== null)
113
+ continue;
114
+ const paths = pathsOf(service.dir);
115
+ if (!existsSync(paths.model))
116
+ continue;
117
+ modelled += 1;
118
+ if (!existsSync(paths.project))
119
+ gaps.push({ service, path: paths.project });
120
+ }
121
+ gaps.sort((a, b) => (a.service.id < b.service.id ? -1 : a.service.id > b.service.id ? 1 : 0));
122
+ return { gaps, modelled };
123
+ }
124
+ //# sourceMappingURL=projects.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"projects.js","sourceRoot":"","sources":["../../../../../src/core/repo/tree/render/projects.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAoEG;AACH,OAAO,EAAE,UAAU,EAAE,MAAM,SAAS,CAAC;AAErC,OAAO,EAAE,aAAa,EAAE,MAAM,gCAAgC,CAAC;AAE/D,gFAAgF;AAChF,MAAM,uBAAuB,GAAG,SAAS,CAAC;AAE1C;;;GAGG;AACH,MAAM,UAAU,kBAAkB,CAAC,EAAU,EAAE,WAAmB;IAChE,MAAM,MAAM,GAAG,EAAE,CAAC,OAAO,CAAC,KAAK,EAAE,GAAG,CAAC,CAAC;IACtC,OAAO,MAAM,KAAK,WAAW,IAAI,MAAM,KAAK,uBAAuB,CAAC,CAAC,CAAC,WAAW,MAAM,EAAE,CAAC,CAAC,CAAC,MAAM,CAAC;AACrG,CAAC;AAED;;;;;GAKG;AACH,MAAM,UAAU,oBAAoB,CAAC,EAAU,EAAE,WAAmB;IAClE,OAAO,GAAG,IAAI,CAAC,SAAS,CAAC,EAAE,IAAI,EAAE,kBAAkB,CAAC,EAAE,EAAE,WAAW,CAAC,EAAE,KAAK,EAAE,EAAE,EAAE,EAAE,IAAI,EAAE,CAAC,CAAC,IAAI,CAAC;AAClG,CAAC;AA6BD;;;;;;;;;;;;;;GAcG;AACH,MAAM,UAAU,cAAc,CAC5B,QAAsB,EACtB,OAAgE;IAEhE,MAAM,IAAI,GAAoB,EAAE,CAAC;IACjC,IAAI,QAAQ,GAAG,CAAC,CAAC;IACjB,KAAK,MAAM,OAAO,IAAI,QAAQ,EAAE,CAAC;QAC/B,yEAAyE;QACzE,qEAAqE;QACrE,IAAI,aAAa,CAAC,OAAO,CAAC,EAAE,CAAC,KAAK,IAAI;YAAE,SAAS;QACjD,MAAM,KAAK,GAAG,OAAO,CAAC,OAAO,CAAC,GAAG,CAAC,CAAC;QACnC,IAAI,CAAC,UAAU,CAAC,KAAK,CAAC,KAAK,CAAC;YAAE,SAAS;QACvC,QAAQ,IAAI,CAAC,CAAC;QACd,IAAI,CAAC,UAAU,CAAC,KAAK,CAAC,OAAO,CAAC;YAAE,IAAI,CAAC,IAAI,CAAC,EAAE,OAAO,EAAE,IAAI,EAAE,KAAK,CAAC,OAAO,EAAE,CAAC,CAAC;IAC9E,CAAC;IACD,IAAI,CAAC,IAAI,CAAC,CAAC,CAAC,EAAE,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,CAAC,OAAO,CAAC,EAAE,GAAG,CAAC,CAAC,OAAO,CAAC,EAAE,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,OAAO,CAAC,EAAE,GAAG,CAAC,CAAC,OAAO,CAAC,EAAE,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC;IAC9F,OAAO,EAAE,IAAI,EAAE,QAAQ,EAAE,CAAC;AAC5B,CAAC","sourcesContent":["/**\n * The renderer's project file for ONE service — `services/<…>/<id>/likec4.config.json`\n * — the second thing under `services/` that exists only to be rendered.\n *\n * Why it exists. The root `likec4.config.json` scopes the fleet project to\n * `architecture/` and excludes `services/**`, and must: loam parses every\n * `.likec4` file ALONE, so each `model.likec4` declares its own `specification`\n * block, and a renderer merging the tree reports every declaration as a\n * duplicate. The cost of that exclusion went unstated for a release: an\n * adopted service's model — the artifact `loam adopt` spends the most effort\n * on — was a box on the fleet map with nothing renderable inside it from the\n * docs root, and the documented way to see it was one renderer invocation per\n * service directory. Measured at the 1.59.2 pin: a `likec4.config.json` INSIDE\n * a service directory is registered as a project of its own even though the\n * root excludes the directory (the root's `exclude` does not hide a nested\n * project file), the workspace then loads `fleet` plus one project per service\n * with zero errors, and each service project carries LikeC4's own `index` view\n * of the containers beside whatever view the model authors. One two-key file\n * per service directory is therefore what makes the model renderable from the\n * same root, in the same renderer, beside the map.\n *\n * CREATE-ONLY, never compared, never rewritten, never removed — the root\n * config's ownership rule (`core/docs.ts`: written once, never re-read, the\n * team owns it), NOT the generated views file's. The bytes are a function of\n * the service id alone, which SCHEMA.md defines as the identity no verb ever\n * changes, so nothing can make the file stale; a compare would only fight the\n * per-project keys LikeC4 lets a team add (`title`, styles, a contact), and\n * would restale every service on any change to this rule. Presence is the one\n * question loam owes the file, and presence needs no parse: rule 26 stands,\n * and loam still reads none of it. A leftover file after a model is deleted is\n * the team's, and harmless — measured, a nested project holding no `.likec4`\n * file is silently absent from the renderer's project list.\n *\n * THE NAME. LikeC4's project-name grammar (measured at the pin) refuses an\n * empty name, the word `default`, and the bytes `.`, `@` and `#`; loam's\n * service-id grammar admits `.`. So every `.` becomes `-`, and a name that\n * would then equal the ROOT project's (`fleet` — a nested project of that name\n * is silently registered as `fleet-1`) or LikeC4's reserved `default` is\n * prefixed `service-`. Readable and typeable after `--project`, and NOT\n * injective: an id spelled a.b beside a service literally named a-b, or\n * `service-fleet` beside `fleet`, share a name and the renderer suffixes the\n * later one `-1`. That is disclosed rather than escaped away, because the\n * picker shows the `title` (measured: `title ?? id`), which is ALWAYS the id\n * verbatim, and a collision costs one suffixed label rather than the blanked\n * workspace that made `subsystemViewId` injective. The root's name arrives as\n * a parameter, like every spelling this package needs from `repo/`: `render/`\n * cannot import `repo/paths.ts` (`stale.ts` records the cycle), and a second\n * copy of the word `fleet` here would be the drift the injection prevents.\n *\n * A DIRECTORY THAT IS NOT A LEGAL SERVICE ID IS OWED NO FILE. The tree walk\n * enumerates every directory on purpose — the fleet is never reported smaller\n * than it is — and `service.id-invalid` names the illegal ones. Folding only\n * `.` is right for every LEGAL id, whose remaining bytes LikeC4 accepts; a\n * name like `pay@1` would produce a project file the renderer silently drops\n * (measured: zero errors, the project simply absent from the list), and\n * because the file is create-only the grade below would then be silent\n * forever while the model stayed unrendered — the exact state this file\n * exists to remove, with a label saying it was fixed. So the survey skips\n * such a service: the writer creates nothing, the grader says nothing, and\n * `service.id-invalid`'s own repair — the rename — is what earns the file on\n * the first sync after it.\n *\n * NO MODEL, NO FILE: a service directory without `model.likec4` — one an\n * archive materialised, a spec-only baseline — is owed nothing, and gets its\n * file on the first `loam subsystem sync` after the model lands. A feature's\n * `delta.likec4` is deliberately out of scope: the directory is transient and\n * `loam archive` moves it where a project file would still be registered, so\n * every shipped feature would keep a dead project in the list forever.\n */\nimport { existsSync } from \"node:fs\";\nimport type { ServiceDir } from \"../../../kernel/ids/dirs.js\";\nimport { dirNameHazard } from \"../../../kernel/ids/service.js\";\n\n/** LikeC4's own reserved project name; a file naming it is refused outright. */\nconst LIKEC4_RESERVED_PROJECT = \"default\";\n\n/**\n * The project name for a service id — the id itself for every id made of\n * letters, digits, `_` and `-`, which is nearly all of them.\n */\nexport function serviceProjectName(id: string, rootProject: string): string {\n const folded = id.replace(/\\./g, \"-\");\n return folded === rootProject || folded === LIKEC4_RESERVED_PROJECT ? `service-${folded}` : folded;\n}\n\n/**\n * The exact bytes of a service's project file: two keys, two-space JSON, LF,\n * one trailing newline — the root file's own serialisation. `title` is the id\n * verbatim, always, so whatever the renderer labels a project with is the\n * service's real identity even where `name` had to bend.\n */\nexport function renderServiceProject(id: string, rootProject: string): string {\n return `${JSON.stringify({ name: serviceProjectName(id, rootProject), title: id }, null, 2)}\\n`;\n}\n\n/**\n * A service whose model has no project file beside it, and where the file\n * goes. The SERVICE travels whole — a walked service or a full entry,\n * whichever the caller surveyed — rather than its id: the grader spells the\n * finding's path from the entry's own placement, and a join back by id would\n * name the wrong directory the moment two services share a leaf name under\n * different subsystems (`subsystem.name-collision` — a broken tree the walk\n * still enumerates in full, and one this grade is emitted on by design).\n */\nexport interface ProjectGap<T> {\n service: T;\n /** Absolute path of the file `loam subsystem sync` will create. */\n path: string;\n}\n\n/** What one survey of the tree found: the gaps, and the domain they were counted against. */\nexport interface ProjectSurvey<T> {\n /** Every service owed a file and lacking one, sorted by id. */\n gaps: ProjectGap<T>[];\n /**\n * Every service the survey considered at all — a model on disk, a legal id\n * — file or no file. `modelled - gaps.length` is therefore \"already had\n * one\", derived from the same walk rather than counted a second way.\n */\n modelled: number;\n}\n\n/**\n * ONE predicate for the writer (`subsystem sync`) and the grader\n * (`validate --all`), on `viewsState`'s precedent — two private spellings of\n * \"is this model a project\" would be two chances to answer differently about\n * one file, and a grader naming a gap the writer would not fill is a loop no\n * command can clear. Both the gaps AND the count live here for the same\n * reason: a caller re-deriving \"has a model\" beside this function is the\n * second spelling in another coat.\n *\n * Structural over `{ id, dir }` so a walked service and a full entry both fit,\n * and the two file spellings arrive injected (`repo/paths.ts`'s\n * `serviceRenderPaths`) for the package reason the banner gives. Sorted by id,\n * plain lexicographic like every list this package emits, so the writer's\n * report and the grader's findings come out in one stable order.\n */\nexport function surveyProjects<T extends { id: string; dir: ServiceDir }>(\n services: readonly T[],\n pathsOf: (dir: ServiceDir) => { model: string; project: string },\n): ProjectSurvey<T> {\n const gaps: ProjectGap<T>[] = [];\n let modelled = 0;\n for (const service of services) {\n // The banner's rule: a name the renderer would refuse earns no file, and\n // `service.id-invalid` — not this survey — is what names the repair.\n if (dirNameHazard(service.id) !== null) continue;\n const paths = pathsOf(service.dir);\n if (!existsSync(paths.model)) continue;\n modelled += 1;\n if (!existsSync(paths.project)) gaps.push({ service, path: paths.project });\n }\n gaps.sort((a, b) => (a.service.id < b.service.id ? -1 : a.service.id > b.service.id ? 1 : 0));\n return { gaps, modelled };\n}\n"]}
@@ -0,0 +1,26 @@
1
+ /**
2
+ * Is the generated file on disk what this tree and this map render to?
3
+ *
4
+ * One function, because the question now has three askers — `validate --all`
5
+ * (which turns a disagreement into `subsystem.views-stale`), `loam subsystem
6
+ * sync` and the write verbs (which repair it), and `loam archive` (which must
7
+ * not report a repo current when it has just made the file stale). Three
8
+ * private compares would be three chances to answer differently about one
9
+ * file, and the module that mints the bytes is the one that owes the answer.
10
+ *
11
+ * The PATH arrives as a parameter rather than a `DocsDir`: `repo/paths.ts`
12
+ * owns the spelling of every file in a docs repo, and importing it here would
13
+ * close the package cycle `repo → repo/tree → repo/tree/render → repo` that
14
+ * `scripts/package-graph.mjs` exists to refuse. `walk.ts` pays the same price
15
+ * for the same reason and says so.
16
+ */
17
+ import { existsSync } from "node:fs";
18
+ import { readFile } from "node:fs/promises";
19
+ import {} from "../../../c4/likec4.js";
20
+ import { renderSubsystemViews, viewsAgree } from "./views.js";
21
+ export async function viewsState(path, tree, elements) {
22
+ const expected = renderSubsystemViews(tree, elements);
23
+ const actual = existsSync(path) ? await readFile(path, "utf8") : null;
24
+ return { actual, expected, agrees: viewsAgree(actual, expected) };
25
+ }
26
+ //# sourceMappingURL=stale.js.map
@@ -0,0 +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;AAC5C,OAAO,EAAa,MAAM,uBAAuB,CAAC;AAElD,OAAO,EAAE,oBAAoB,EAAE,UAAU,EAAE,MAAM,YAAY,CAAC;AAgB9D,MAAM,CAAC,KAAK,UAAU,UAAU,CAAC,IAAY,EAAE,IAAe,EAAE,QAAgB;IAC9E,MAAM,QAAQ,GAAG,oBAAoB,CAAC,IAAI,EAAE,QAAQ,CAAC,CAAC;IACtD,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 Elem } from \"../../../c4/likec4.js\";\nimport type { FleetTree } from \"../walk.js\";\nimport { 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, elements: Elem[]): Promise<ViewsState> {\n const expected = renderSubsystemViews(tree, elements);\n const actual = existsSync(path) ? await readFile(path, \"utf8\") : null;\n return { actual, expected, agrees: viewsAgree(actual, expected) };\n}\n"]}
@@ -0,0 +1,84 @@
1
+ /**
2
+ * One string, safely spelled as a LikeC4 single-quoted literal.
3
+ *
4
+ * The marker reader validates that a `title` is a string and nothing else
5
+ * (`../marker.ts`), so every measurement below is reachable from a
6
+ * `subsystem.yaml` a human may legally write, against the pinned likec4:
7
+ *
8
+ * - a raw `'` is 2 parse errors, and a parse error in ONE document of the
9
+ * `architecture/` project blanks the model for every document in it — a
10
+ * fleet would lose its whole map over one apostrophe in a marker;
11
+ * - a trailing lone `\` is 3 errors (it escapes the closing quote);
12
+ * - `C:\path` written raw is 0 errors and a quietly wrong `C:path` label;
13
+ * - `Ops' } view evil { include x` is 0 errors and INJECTS a view — and an
14
+ * injected id equal to an authored one silently replaces that author's
15
+ * view in the rendered project. That one is the reason this function is
16
+ * not optional.
17
+ *
18
+ * Normalize, then escape, and in that order: every run of control or space
19
+ * characters collapses to one space, because the file's contract is
20
+ * line-oriented (`../views.ts`'s header states the git-merge property in
21
+ * lines) and a literal newline inside an emitted string would split
22
+ * `title '…'` in two. Then `\` before `'`, because doubling the backslashes
23
+ * afterwards would double the ones this function itself inserted.
24
+ */
25
+ export function likec4String(text) {
26
+ return text
27
+ .replace(/[\p{Cc}\p{Zs}\p{Zl}\p{Zp}]+/gu, " ")
28
+ .trim()
29
+ .split("\\")
30
+ .join("\\\\")
31
+ .split("'")
32
+ .join("\\'");
33
+ }
34
+ /**
35
+ * ONE subsystem's label: the marker's `title`, or the DIRECTORY NAME when the
36
+ * marker carries none.
37
+ *
38
+ * The fallback is not a nicety. Without a title LikeC4 shows the view id, and
39
+ * the id is hex-escaped for injectivity (`subsystemViewId` says why), so every
40
+ * subsystem whose directory name holds a hyphen — the common case — rendered
41
+ * as `subsystem_platform__order_2dflow`. A directory name is something a human
42
+ * chose; the escaped id never is.
43
+ *
44
+ * A blank or whitespace-only title counts as absent: measured, `title ''`
45
+ * collapses to null at the model layer, so emitting one would put back exactly
46
+ * the unlabelled view this exists to remove.
47
+ *
48
+ * A `/` inside the label becomes `∕` (U+2215), and that substitution is the
49
+ * price of `viewTitle` below composing a path out of these. LikeC4 splits a
50
+ * view title on `/` into browser folders and displays only the last segment —
51
+ * measured: `Payments / refunds` renders as `refunds` with `Payments` demoted
52
+ * to a folder — so an authored slash left alone would silently hide
53
+ * half of what somebody wrote AND invent a folder level the tree does not
54
+ * have. Since loam is the one choosing to compose with `/`, loam owns that
55
+ * consequence rather than passing it on. The whole title stays visible and the
56
+ * folder path stays exactly the subsystem tree. A directory name cannot
57
+ * contain `/`, so the fallback never needs it.
58
+ */
59
+ export function subsystemLabel(sub) {
60
+ const segment = (text) => likec4String(text).split("/").join("∕");
61
+ const authored = segment(sub.meta.title ?? "");
62
+ return authored === "" ? segment(sub.name) : authored;
63
+ }
64
+ /**
65
+ * The title a view carries: every marked ancestor's label and its own, joined
66
+ * with ` / ` — `Platform / Order flow` for `services/platform/order-flow`.
67
+ *
68
+ * LikeC4 reads that as a path: the browser shows one folder per ancestor and
69
+ * the leaf's own label as the title, so the view list mirrors the directory
70
+ * tree a human already navigates, and two subsystems whose leaf labels are
71
+ * equal are told apart by their parents instead of appearing twice under one
72
+ * name. Nothing is invented — the chain is the tree — and a subsystem directly
73
+ * under `services/` has a chain of one and reads exactly as its own label.
74
+ *
75
+ * The cost, stated because it is real: renaming a PARENT's title rewrites the
76
+ * bytes of every view beneath it, so one marker edit restales more of the file
77
+ * than it used to. The alternative — a fixed `loam subsystems /` prefix —
78
+ * buys the same folder without the churn and puts loam's own name in a
79
+ * reader's UI; the tree's own words won.
80
+ */
81
+ export function viewTitle(chain) {
82
+ return chain.map(subsystemLabel).join(" / ");
83
+ }
84
+ //# sourceMappingURL=text.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"text.js","sourceRoot":"","sources":["../../../../../src/core/repo/tree/render/text.ts"],"names":[],"mappings":"AAcA;;;;;;;;;;;;;;;;;;;;;;;GAuBG;AACH,MAAM,UAAU,YAAY,CAAC,IAAY;IACvC,OAAO,IAAI;SACR,OAAO,CAAC,+BAA+B,EAAE,GAAG,CAAC;SAC7C,IAAI,EAAE;SACN,KAAK,CAAC,IAAI,CAAC;SACX,IAAI,CAAC,MAAM,CAAC;SACZ,KAAK,CAAC,GAAG,CAAC;SACV,IAAI,CAAC,KAAK,CAAC,CAAC;AACjB,CAAC;AAED;;;;;;;;;;;;;;;;;;;;;;;;GAwBG;AACH,MAAM,UAAU,cAAc,CAAC,GAAmB;IAChD,MAAM,OAAO,GAAG,CAAC,IAAY,EAAU,EAAE,CAAC,YAAY,CAAC,IAAI,CAAC,CAAC,KAAK,CAAC,GAAG,CAAC,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC;IAClF,MAAM,QAAQ,GAAG,OAAO,CAAC,GAAG,CAAC,IAAI,CAAC,KAAK,IAAI,EAAE,CAAC,CAAC;IAC/C,OAAO,QAAQ,KAAK,EAAE,CAAC,CAAC,CAAC,OAAO,CAAC,GAAG,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC,QAAQ,CAAC;AACxD,CAAC;AAED;;;;;;;;;;;;;;;;GAgBG;AACH,MAAM,UAAU,SAAS,CAAC,KAAgC;IACxD,OAAO,KAAK,CAAC,GAAG,CAAC,cAAc,CAAC,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC;AAC/C,CAAC","sourcesContent":["/**\n * Somebody else's words, made safe to write into a `.likec4` file.\n *\n * The generated views file carries FREE TEXT for the first time — a\n * subsystem's `title` and `description`, straight out of `subsystem.yaml` —\n * and this module is the only place in loam where a person's prose becomes\n * syntax. It sits in its own module beside the body renderer because both the\n * file-level render (`../views.ts`, the view's own `title` line) and the box\n * render (`./body.ts`, a group's label) must escape the same way; two\n * spellings would be two chances to differ, and the one that forgot would ship\n * a fleet-wide parse error.\n */\nimport type { SubsystemEntry } from \"../walk.js\";\n\n/**\n * One string, safely spelled as a LikeC4 single-quoted literal.\n *\n * The marker reader validates that a `title` is a string and nothing else\n * (`../marker.ts`), so every measurement below is reachable from a\n * `subsystem.yaml` a human may legally write, against the pinned likec4:\n *\n * - a raw `'` is 2 parse errors, and a parse error in ONE document of the\n * `architecture/` project blanks the model for every document in it — a\n * fleet would lose its whole map over one apostrophe in a marker;\n * - a trailing lone `\\` is 3 errors (it escapes the closing quote);\n * - `C:\\path` written raw is 0 errors and a quietly wrong `C:path` label;\n * - `Ops' } view evil { include x` is 0 errors and INJECTS a view — and an\n * injected id equal to an authored one silently replaces that author's\n * view in the rendered project. That one is the reason this function is\n * not optional.\n *\n * Normalize, then escape, and in that order: every run of control or space\n * characters collapses to one space, because the file's contract is\n * line-oriented (`../views.ts`'s header states the git-merge property in\n * lines) and a literal newline inside an emitted string would split\n * `title '…'` in two. Then `\\` before `'`, because doubling the backslashes\n * afterwards would double the ones this function itself inserted.\n */\nexport function likec4String(text: string): string {\n return text\n .replace(/[\\p{Cc}\\p{Zs}\\p{Zl}\\p{Zp}]+/gu, \" \")\n .trim()\n .split(\"\\\\\")\n .join(\"\\\\\\\\\")\n .split(\"'\")\n .join(\"\\\\'\");\n}\n\n/**\n * ONE subsystem's label: the marker's `title`, or the DIRECTORY NAME when the\n * marker carries none.\n *\n * The fallback is not a nicety. Without a title LikeC4 shows the view id, and\n * the id is hex-escaped for injectivity (`subsystemViewId` says why), so every\n * subsystem whose directory name holds a hyphen — the common case — rendered\n * as `subsystem_platform__order_2dflow`. A directory name is something a human\n * chose; the escaped id never is.\n *\n * A blank or whitespace-only title counts as absent: measured, `title ''`\n * collapses to null at the model layer, so emitting one would put back exactly\n * the unlabelled view this exists to remove.\n *\n * A `/` inside the label becomes `∕` (U+2215), and that substitution is the\n * price of `viewTitle` below composing a path out of these. LikeC4 splits a\n * view title on `/` into browser folders and displays only the last segment —\n * measured: `Payments / refunds` renders as `refunds` with `Payments` demoted\n * to a folder — so an authored slash left alone would silently hide\n * half of what somebody wrote AND invent a folder level the tree does not\n * have. Since loam is the one choosing to compose with `/`, loam owns that\n * consequence rather than passing it on. The whole title stays visible and the\n * folder path stays exactly the subsystem tree. A directory name cannot\n * contain `/`, so the fallback never needs it.\n */\nexport function subsystemLabel(sub: SubsystemEntry): string {\n const segment = (text: string): string => likec4String(text).split(\"/\").join(\"∕\");\n const authored = segment(sub.meta.title ?? \"\");\n return authored === \"\" ? segment(sub.name) : authored;\n}\n\n/**\n * The title a view carries: every marked ancestor's label and its own, joined\n * with ` / ` — `Platform / Order flow` for `services/platform/order-flow`.\n *\n * LikeC4 reads that as a path: the browser shows one folder per ancestor and\n * the leaf's own label as the title, so the view list mirrors the directory\n * tree a human already navigates, and two subsystems whose leaf labels are\n * equal are told apart by their parents instead of appearing twice under one\n * name. Nothing is invented — the chain is the tree — and a subsystem directly\n * under `services/` has a chain of one and reads exactly as its own label.\n *\n * The cost, stated because it is real: renaming a PARENT's title rewrites the\n * bytes of every view beneath it, so one marker edit restales more of the file\n * than it used to. The alternative — a fixed `loam subsystems /` prefix —\n * buys the same folder without the churn and puts loam's own name in a\n * reader's UI; the tree's own words won.\n */\nexport function viewTitle(chain: readonly SubsystemEntry[]): string {\n return chain.map(subsystemLabel).join(\" / \");\n}\n"]}
@@ -12,6 +12,13 @@
12
12
  * the LikeC4 renderer, which merges the whole `architecture/` project per
13
13
  * likec4.config.json, resolves it.
14
14
  *
15
+ * Each view opens with the LABEL a human reads — `title`, and `description`
16
+ * when the marker carries one — because this file exists only to be rendered
17
+ * and an unlabelled view is shown by its id, which is hex-escaped for
18
+ * injectivity and reads as `subsystem_platform__order_2dflow`. Both
19
+ * strings are somebody else's words and go through `likec4String`, which
20
+ * records what a raw one does.
21
+ *
15
22
  * The output is deterministic and line-oriented, and both halves are the
16
23
  * contract: subsystems sorted by path, members sorted by id, ONE `include`
17
24
  * per line — so two concurrent moves into different groups touch different
@@ -40,9 +47,9 @@
40
47
  * the committed landscape resolves keeps the output a function of committed
41
48
  * bytes alone.
42
49
  */
43
- import {} from "../../c4/likec4.js";
44
- import { serviceResolver } from "../../c4/resolve/service.js";
45
- import { servicesUnder } from "./find.js";
50
+ import {} from "../../../c4/likec4.js";
51
+ import { renderContext, viewBody } from "./body.js";
52
+ import { likec4String, viewTitle } from "./text.js";
46
53
  /**
47
54
  * The header every generated file opens with — stable bytes, part of the
48
55
  * byte-compare contract, naming the regenerator so a reader who found the
@@ -50,7 +57,8 @@ import { servicesUnder } from "./find.js";
50
57
  */
51
58
  const HEADER = "// generated by loam — the subsystem tree under services/, one view per subsystem.\n" +
52
59
  "// Do not edit: `loam subsystem sync` regenerates this file from the directories,\n" +
53
- "// and `loam validate --all` reports `subsystem.views-stale` when it drifts.\n";
60
+ "// and `loam validate --all` reports `subsystem.views-stale` when it drifts.\n" +
61
+ "// A scoping aid, not the fleet's architecture: author your own diagrams elsewhere.\n";
54
62
  /**
55
63
  * Render the views file for a walked tree, or null when the tree has no
56
64
  * subsystems — the file must then be ABSENT, so a flat fleet never carries a
@@ -59,50 +67,46 @@ const HEADER = "// generated by loam — the subsystem tree under services/, one
59
67
  export function renderSubsystemViews(tree, elements) {
60
68
  if (tree.subsystems.length === 0)
61
69
  return null;
62
- const elementOf = elementByService(tree, elements);
70
+ const ctx = renderContext(tree, elements);
63
71
  const lines = [HEADER, "views {"];
64
72
  const subsystems = [...tree.subsystems].sort((a, b) => (pathKey(a) < pathKey(b) ? -1 : 1));
65
73
  for (const [i, sub] of subsystems.entries()) {
66
74
  if (i > 0)
67
75
  lines.push("");
68
76
  lines.push(` // services/${pathKey(sub)}`);
77
+ const body = viewBody(ctx, sub);
78
+ if (body.disclosure !== null)
79
+ lines.push(` // ${body.disclosure}`);
69
80
  lines.push(` view ${subsystemViewId(sub)} {`);
70
- for (const member of membersOf(tree, sub)) {
71
- const element = elementOf.get(member.id);
72
- if (element !== undefined)
73
- lines.push(` include ${element}`);
74
- }
81
+ // Metadata FIRST, and that ordering is a hard constraint rather than a
82
+ // taste: measured, an `include` OR a `group { … }` line followed by
83
+ // `title` is a parse error ("Expecting token of type '}' but found
84
+ // `title`"), and one parse error in this file blanks the whole
85
+ // `architecture/` project in the renderer.
86
+ lines.push(` title '${viewTitle(titleChain(tree, sub))}'`);
87
+ const description = likec4String(sub.meta.description ?? "");
88
+ if (description !== "")
89
+ lines.push(` description '${description}'`);
90
+ lines.push(...body.lines);
75
91
  lines.push(" }");
76
92
  }
77
93
  lines.push("}");
78
94
  return lines.join("\n") + "\n";
79
95
  }
80
96
  /**
81
- * The landscape element standing for each service id: for every element,
82
- * resolve it through the shared join, and let the SHALLOWEST element win
83
- * (document order breaking ties). Shallowest, because `serviceResolver`
84
- * resolves a service's own containers to the service too, and a view should
85
- * include the service-level box, not whichever nested container happened to
86
- * be declared first.
97
+ * This subsystem and every MARKED ancestor above it, outermost first — the
98
+ * chain `viewTitle` composes a path out of.
99
+ *
100
+ * Marked, because an intermediate directory holding no `subsystem.yaml` is
101
+ * `subsystem.unmarked`, an error the walk already names: it is not a group,
102
+ * so it contributes no folder level here either. Read off the walked tree
103
+ * rather than off the path segments, so what the title says and what the
104
+ * finding says about the same directory cannot disagree.
87
105
  */
88
- function elementByService(tree, elements) {
89
- const known = new Set(tree.services.map((s) => s.id));
90
- const resolve = serviceResolver(elements, known);
91
- const depth = (id) => id.split(".").length;
92
- const out = new Map();
93
- for (const e of elements) {
94
- const service = resolve(e.id);
95
- if (!known.has(service))
96
- continue;
97
- const held = out.get(service);
98
- if (held === undefined || depth(e.id) < depth(held))
99
- out.set(service, e.id);
100
- }
101
- return out;
102
- }
103
- /** Services beneath this subsystem at any depth, ordered by id — the transitive membership. */
104
- function membersOf(tree, sub) {
105
- return [...servicesUnder(tree, sub)].sort((a, b) => (a.id < b.id ? -1 : 1));
106
+ function titleChain(tree, sub) {
107
+ return tree.subsystems
108
+ .filter((s) => s.path.length <= sub.path.length && s.path.every((name, i) => sub.path[i] === name))
109
+ .sort((a, b) => a.path.length - b.path.length);
106
110
  }
107
111
  /** The sort key and the comment spelling: the subsystem's path from services/, joined. */
108
112
  function pathKey(sub) {
@@ -0,0 +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 Elem } 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 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 * 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, elements: Elem[]): string | null {\n if (tree.subsystems.length === 0) return null;\n const ctx = renderContext(tree, elements);\n const lines: string[] = [HEADER, \"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 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"]}
@@ -145,7 +145,12 @@ contracts and digests are byte-identical to their unfiled days, every command ad
145
145
  id, and `architecture/subsystems.likec4` — GENERATED by `loam subsystem sync`, one view per
146
146
  subsystem, one `include` per line — is the only file the grouping added. Edit the tree with
147
147
  `loam subsystem move|rename|rm` and the views file travels in the same transaction; edit it by hand
148
- and `validate --all` answers `subsystem.views-stale`. (`order-service` and `payment-service` stay
148
+ and `validate --all` answers `subsystem.views-stale`. Each of the five services also carries a
149
+ `likec4.config.json` — written by `loam subsystem sync`, create-only, never graded for content —
150
+ because the root project excludes `services/**` and that file is what registers a service's
151
+ `model.likec4` as a LikeC4 project of its own: `npx likec4 validate examples/docs` names six
152
+ projects, `fleet` and the five services, and with more than one project it needs `--project fleet`
153
+ (or a service's name) to grade one. (`order-service` and `payment-service` stay
149
154
  unfiled deliberately: FEAT-088's committed version-2 snapshot restores them by literal path, and the
150
155
  `loam unarchive FEAT-088` walkthrough must keep working byte-for-byte.)
151
156
 
@@ -1,10 +1,14 @@
1
1
  // generated by loam — the subsystem tree under services/, one view per subsystem.
2
2
  // Do not edit: `loam subsystem sync` regenerates this file from the directories,
3
3
  // and `loam validate --all` reports `subsystem.views-stale` when it drifts.
4
+ // A scoping aid, not the fleet's architecture: author your own diagrams elsewhere.
4
5
 
5
6
  views {
6
7
  // services/platform
8
+ // model: no boundary — marketplace also holds services outside this subsystem.
7
9
  view subsystem_platform {
10
+ title 'Platform'
11
+ description 'Cross-cutting identity and messaging'
8
12
  include marketplace.identityService
9
13
  include marketplace.notificationService
10
14
  }
@@ -0,0 +1,4 @@
1
+ {
2
+ "name": "checkout-web",
3
+ "title": "checkout-web"
4
+ }
@@ -0,0 +1,4 @@
1
+ {
2
+ "name": "order-service",
3
+ "title": "order-service"
4
+ }
@@ -0,0 +1,4 @@
1
+ {
2
+ "name": "payment-service",
3
+ "title": "payment-service"
4
+ }
@@ -2,8 +2,10 @@
2
2
  //
3
3
  // loam parses every `.likec4` file ALONE, so this document declares its own
4
4
  // `specification` block; a renderer handed the whole tree at once would merge
5
- // them and call every declaration a duplicate, which is what
6
- // `likec4.config.json` scopes the root project to `architecture/` for.
5
+ // them and call every declaration a duplicate, which is why the root
6
+ // `likec4.config.json` scopes the fleet project to `architecture/` and stays
7
+ // that way. What renders THIS model beside the map from the docs root is this
8
+ // directory's own `likec4.config.json`, written by `loam subsystem sync`.
7
9
  specification {
8
10
  element softwareSystem
9
11
  element container
@@ -0,0 +1,4 @@
1
+ {
2
+ "name": "identity-service",
3
+ "title": "identity-service"
4
+ }
@@ -0,0 +1,4 @@
1
+ {
2
+ "name": "notification-service",
3
+ "title": "notification-service"
4
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ybotok/loam",
3
- "version": "0.2.0-alpha.2",
3
+ "version": "0.2.0-alpha.5",
4
4
  "description": "Architecture-first semantic integrity and change governance for evolving software systems: connect C4, requirements, contracts, and verification evidence.",
5
5
  "author": "spentsov",
6
6
  "repository": {
@@ -1 +0,0 @@
1
- {"version":3,"file":"views.js","sourceRoot":"","sources":["../../../../src/core/repo/tree/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 * 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 Elem } from \"../../c4/likec4.js\";\nimport { serviceResolver } from \"../../c4/resolve/service.js\";\nimport { servicesUnder } from \"./find.js\";\nimport type { FleetTree, SubsystemEntry, WalkedService } from \"./walk.js\";\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\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, elements: Elem[]): string | null {\n if (tree.subsystems.length === 0) return null;\n const elementOf = elementByService(tree, elements);\n const lines: string[] = [HEADER, \"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 lines.push(` view ${subsystemViewId(sub)} {`);\n for (const member of membersOf(tree, sub)) {\n const element = elementOf.get(member.id);\n if (element !== undefined) lines.push(` include ${element}`);\n }\n lines.push(\" }\");\n }\n lines.push(\"}\");\n return lines.join(\"\\n\") + \"\\n\";\n}\n\n/**\n * The landscape element standing for each service id: for every element,\n * resolve it through the shared join, and let the SHALLOWEST element win\n * (document order breaking ties). Shallowest, because `serviceResolver`\n * resolves a service's own containers to the service too, and a view should\n * include the service-level box, not whichever nested container happened to\n * be declared first.\n */\nfunction elementByService(tree: FleetTree, elements: Elem[]): Map<string, string> {\n const known: ReadonlySet<string> = new Set(tree.services.map((s) => s.id));\n const resolve = serviceResolver(elements, known);\n const depth = (id: string): number => id.split(\".\").length;\n const out = new Map<string, string>();\n for (const e of elements) {\n const service = resolve(e.id);\n if (!known.has(service)) continue;\n const held = out.get(service);\n if (held === undefined || depth(e.id) < depth(held)) out.set(service, e.id);\n }\n return out;\n}\n\n/** Services beneath this subsystem at any depth, ordered by id — the transitive membership. */\nfunction membersOf(tree: FleetTree, sub: SubsystemEntry): WalkedService[] {\n return [...servicesUnder(tree, sub)].sort((a, b) => (a.id < b.id ? -1 : 1));\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"]}