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

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (105) hide show
  1. package/CHANGELOG.md +162 -0
  2. package/README.md +4 -4
  3. package/ROADMAP.md +27 -6
  4. package/SCHEMA.md +152 -32
  5. package/dist/commands/archive/run.js +49 -22
  6. package/dist/commands/archive/run.js.map +1 -1
  7. package/dist/commands/seed/commit.js +34 -1
  8. package/dist/commands/seed/commit.js.map +1 -1
  9. package/dist/commands/seed/plan.js +64 -13
  10. package/dist/commands/seed/plan.js.map +1 -1
  11. package/dist/commands/subsystem/edit.js +4 -2
  12. package/dist/commands/subsystem/edit.js.map +1 -1
  13. package/dist/commands/subsystem/move.js +55 -17
  14. package/dist/commands/subsystem/move.js.map +1 -1
  15. package/dist/commands/subsystem/sync.js +142 -45
  16. package/dist/commands/subsystem/sync.js.map +1 -1
  17. package/dist/commands/subsystem/txn/report.js +55 -5
  18. package/dist/commands/subsystem/txn/report.js.map +1 -1
  19. package/dist/commands/subsystem/txn/txn.js +118 -21
  20. package/dist/commands/subsystem/txn/txn.js.map +1 -1
  21. package/dist/commands/subsystem/txn/views.js +51 -8
  22. package/dist/commands/subsystem/txn/views.js.map +1 -1
  23. package/dist/commands/validate/checks/health-deps.js +9 -6
  24. package/dist/commands/validate/checks/health-deps.js.map +1 -1
  25. package/dist/commands/validate/fleet/census.js +26 -3
  26. package/dist/commands/validate/fleet/census.js.map +1 -1
  27. package/dist/commands/validate/fleet/landscape.js +12 -9
  28. package/dist/commands/validate/fleet/landscape.js.map +1 -1
  29. package/dist/commands/validate/fleet/map/attest.js +4 -5
  30. package/dist/commands/validate/fleet/map/attest.js.map +1 -1
  31. package/dist/commands/validate/fleet/map/bindings.js +15 -2
  32. package/dist/commands/validate/fleet/map/bindings.js.map +1 -1
  33. package/dist/commands/validate/fleet/map/consumers.js +43 -122
  34. package/dist/commands/validate/fleet/map/consumers.js.map +1 -1
  35. package/dist/commands/validate/fleet/scorecard/scorecard.js.map +1 -1
  36. package/dist/commands/validate/fleet/views/fleet-project.js +1 -1
  37. package/dist/commands/validate/fleet/views/fleet-project.js.map +1 -1
  38. package/dist/commands/validate/fleet/views/ids.js +33 -7
  39. package/dist/commands/validate/fleet/views/ids.js.map +1 -1
  40. package/dist/commands/validate/fleet/views/root-documents.js +9 -7
  41. package/dist/commands/validate/fleet/views/root-documents.js.map +1 -1
  42. package/dist/commands/validate/fleet/views/stale.js +31 -14
  43. package/dist/commands/validate/fleet/views/stale.js.map +1 -1
  44. package/dist/core/agent/agents-md/command-map.js +1 -1
  45. package/dist/core/agent/agents-md/command-map.js.map +1 -1
  46. package/dist/core/agent/agents-md/map/subsystem.js +27 -14
  47. package/dist/core/agent/agents-md/map/subsystem.js.map +1 -1
  48. package/dist/core/agent/agents-md/refusals.js +5 -3
  49. package/dist/core/agent/agents-md/refusals.js.map +1 -1
  50. package/dist/core/agent/agents-md.js +16 -7
  51. package/dist/core/agent/agents-md.js.map +1 -1
  52. package/dist/core/agent/workflows/check.js +5 -6
  53. package/dist/core/agent/workflows/check.js.map +1 -1
  54. package/dist/core/agent/workflows/reference/spine.js +12 -6
  55. package/dist/core/agent/workflows/reference/spine.js.map +1 -1
  56. package/dist/core/brief/brief.js +25 -8
  57. package/dist/core/brief/brief.js.map +1 -1
  58. package/dist/core/brief/targets.js +64 -0
  59. package/dist/core/brief/targets.js.map +1 -1
  60. package/dist/core/brief/unchecked.js +1 -1
  61. package/dist/core/brief/unchecked.js.map +1 -1
  62. package/dist/core/c4/project/architecture.js +31 -9
  63. package/dist/core/c4/project/architecture.js.map +1 -1
  64. package/dist/core/c4/project/documents.js +5 -4
  65. package/dist/core/c4/project/documents.js.map +1 -1
  66. package/dist/core/c4/project/load.js +8 -4
  67. package/dist/core/c4/project/load.js.map +1 -1
  68. package/dist/core/c4/project/staged.js +7 -3
  69. package/dist/core/c4/project/staged.js.map +1 -1
  70. package/dist/core/deployment/delta.js +12 -10
  71. package/dist/core/deployment/delta.js.map +1 -1
  72. package/dist/core/gate/checks.js +5 -1
  73. package/dist/core/gate/checks.js.map +1 -1
  74. package/dist/core/gate/partners.js +60 -17
  75. package/dist/core/gate/partners.js.map +1 -1
  76. package/dist/core/repo/paths.js +34 -29
  77. package/dist/core/repo/paths.js.map +1 -1
  78. package/dist/core/repo/tree/render/body.js +45 -6
  79. package/dist/core/repo/tree/render/body.js.map +1 -1
  80. package/dist/core/repo/tree/render/service/header.js +125 -0
  81. package/dist/core/repo/tree/render/service/header.js.map +1 -0
  82. package/dist/core/repo/tree/render/service/peers.js +28 -0
  83. package/dist/core/repo/tree/render/service/peers.js.map +1 -0
  84. package/dist/core/repo/tree/render/service/view-id.js +67 -0
  85. package/dist/core/repo/tree/render/service/view-id.js.map +1 -0
  86. package/dist/core/repo/tree/render/service/views.js +78 -0
  87. package/dist/core/repo/tree/render/service/views.js.map +1 -0
  88. package/dist/core/repo/tree/render/stale.js +19 -2
  89. package/dist/core/repo/tree/render/stale.js.map +1 -1
  90. package/dist/core/repo/tree/render/views.js +102 -33
  91. package/dist/core/repo/tree/render/views.js.map +1 -1
  92. package/dist/core/verify/checklist.js +42 -10
  93. package/dist/core/verify/checklist.js.map +1 -1
  94. package/dist/core/vocabulary/issue.js.map +1 -1
  95. package/dist/core/vocabulary/maturity.js +41 -1
  96. package/dist/core/vocabulary/maturity.js.map +1 -1
  97. package/examples/README.md +7 -3
  98. package/examples/docs/architecture/landscape.likec4 +13 -7
  99. package/examples/docs/architecture/services.likec4 +70 -0
  100. package/examples/docs/architecture/subsystems.likec4 +3 -0
  101. package/examples/docs/services/order-service/model.likec4 +10 -8
  102. package/examples/docs/services/payment-service/model.likec4 +6 -4
  103. package/examples/docs/services/platform/identity-service/model.likec4 +9 -7
  104. package/examples/docs/services/platform/notification-service/model.likec4 +4 -5
  105. package/package.json +1 -1
@@ -1 +1 @@
1
- {"version":3,"file":"unchecked.js","sourceRoot":"","sources":["../../../src/core/brief/unchecked.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;GAWG;AACH,MAAM,CAAC,MAAM,SAAS,GAAa;IACjC,4EAA4E;IAC5E,uEAAuE;IACvE,4EAA4E;IAC5E,2EAA2E;IAC3E,qDAAqD;IACrD,EAAE;IACF,6EAA6E;IAC7E,2EAA2E;IAC3E,sEAAsE;IACtE,sEAAsE;IACtE,gCAAgC;IAChC,gkDAAgkD;IAChkD,6EAA6E;IAC7E,8EAA8E;IAC9E,0EAA0E;IAC1E,6EAA6E;IAC7E,6EAA6E;IAC7E,kDAAkD;IAClD,oqCAAoqC;IACpqC,4EAA4E;IAC5E,8EAA8E;IAC9E,yEAAyE;IACzE,2EAA2E;IAC3E,2EAA2E;IAC3E,uEAAuE;IACvE,2EAA2E;IAC3E,2EAA2E;IAC3E,kDAAkD;IAClD,wmCAAwmC;IACxmC,8oBAA8oB;IAC9oB,8HAA8H;IAC9H,yJAAyJ;IACzJ,oHAAoH;IACpH,wUAAwU;IACxU,yFAAyF;IACzF,6LAA6L;IAC7L,uGAAuG;IACvG,gnBAAgnB;IAChnB,yJAAyJ;IACzJ,mXAAmX;IACnX,2RAA2R;IAC3R,mjBAAmjB;IACnjB,gIAAgI;CACjI,CAAC","sourcesContent":["/**\n * What no check will ever tell you. Read it as the list of ways to be wrong\n * quietly. The checks that do exist — the table an adoption baseline actually\n * meets — live in checks.ts; this module is that table's counterpart, split\n * out so each half can grow without crowding the other.\n *\n * The first two entries are here because they used to be `shape` rules on\n * model.likec4, phrased exactly like the enforced ones. A brief that states an\n * unenforced rule beside four enforced ones is not merely useless — it is the\n * way an agent learns that a green `loam validate` means more than it does.\n * Anything nothing checks belongs on this list, where its status IS the point.\n */\nexport const UNCHECKED: string[] = [\n // Not merely unchecked — unread. This entry used to end \"write one\", on the\n // theory that a view is what makes a model legible. It is, to LikeC4's\n // renderer; loam computes none. Saying otherwise taught agents that a views\n // block was owed to loam, and an `include *` over the FLEET map is the one\n // shape that costs minutes rather than milliseconds.\n //\n // Still exactly true of PRESENCE, which is what this entry is about. Rule 26\n // lets loam read a dynamic view's declared steps WHERE ONE EXISTS; nothing\n // grades their absence, and the failure this comment records — agents\n // learning that a views block is owed to loam — is the one rule 26 is\n // written to prevent recurring.\n \"Whether model.likec4 declares a `views { ... }` block, or any view at all — and nothing in loam ever will. What loam reads, when a `dynamic view` IS present — in the fleet map, or in this service's own project once a view there carries `#req-` — is only what its author declared: the view's tags and its ordered steps. It computes no view and renders nothing, and it never grades a model for lacking one — a fleet that draws no diagrams owes loam no views block, today or ever. Views belong to LikeC4's own renderer: write them if you want diagrams, and read them with `npx likec4 start` at the docs repo root. The root project holds `architecture/` AND every model that EXTENDS the map, so a model written the way this brief asks for it renders there beside the fleet map with no second file to write — one project, one entry in the renderer's picker. The exception is a model that declares its own `specification` block and is therefore parsed alone: its kinds would be duplicates inside the root project, so its directory is excluded there and its own `services/<id>/likec4.config.json` — written by `loam subsystem sync`, loam's file and never yours — makes it a project of its own. A feature's delta.likec4 is the one document always pointed at alone (`npx likec4 start features/<FEAT>`), because it re-declares the landscape's identifiers and carries a `specification` block of its own. Scope your views too — computing a view is superlinear in the number of edges, and an `include *` over `architecture/landscape.likec4` is the expensive one, because that file holds every call in the fleet.\",\n // The readability half of the entry above. They are separate entries because\n // the failures are: one is a view that takes minutes to compute, the other is\n // a view that computes instantly and cannot be read. A fleet acquires the\n // second one the day its second service starts publishing, and every service\n // after that makes it worse by exactly one edge — which is why it arrives as\n // a surprise on a map that was fine last quarter.\n \"Whether the fleet map is LEGIBLE — nothing in loam draws it, so nothing in loam notices. The shape that goes first is a shared broker: Kafka, a bus, a gateway, drawn as ONE element, is the node every service in the fleet points at, and a view over it is a star with sixty spokes. Model the TOPIC rather than the broker — `paymentEvents = topic 'payment.events'` nested inside the broker's element, with the edges pointing at `kafka.paymentEvents` — and one hub of degree sixty becomes twelve of degree five, which is also the truer model: the contract a producer and a consumer share is the topic, never the broker. Declare `element topic { #external, style { shape queue } }` in the `specification` block and give the nested elements that kind: LikeC4 does not inherit tags, so a topic under an `#external` broker is NOT external itself and `loam validate --all` asks for a `services/payment.events/` nobody owes (`landscape.service-undocumented`). Then scope the views, which is where the rest of the legibility lives: `exclude element.tag = #external` keeps the fleet overview to the calls between our own services, and a second view over the broker draws the event spine on its own.\",\n // The third view entry, and the one docs/DESIGN.md rule 26 makes permanent:\n // what a rendered view SHOWS is never computed, so whether an adopted service\n // is on the picture is a fact loam cannot have. It is here rather than a\n // check because each of the three ways to vanish is either the fleet's own\n // vocabulary (a placeholder tag `loam seed` never writes), a static view's\n // `include` predicates (unread by rule), or LikeC4's semantics — and a\n // warning that is right once and noise forty times is the warning somebody\n // silences. The brief NAMES the element's unread tags (landscape.ts); this\n // entry says what nothing will ever do with them.\n \"Whether an adopted service is VISIBLE on the fleet map, which is a different state from being drawn on it. Three things make a documented, bound, edged service disappear, and no check names any of them. A placeholder tag somebody put on the element before adoption — `#provisional`, or whatever your fleet uses to mean drawn-ahead-of-adoption — survives adoption untouched: loam's `seed` writes no such tag, loam reads none, and a fleet view written `exclude element.tag = #<that>` keeps hiding the service until a human removes it, so clearing it is part of the landscape edit. A curated view that lists its members by id (`include paymentService, billingService`) gains nothing by itself. And `include *` shows top-level elements only: once services nest inside a boundary element — the shape loam's own generated subsystem views draw a box around — a fleet view written that way collapses to the boundary and shows no service at all, and needs `include <boundary>.**` (LikeC4's semantics, not loam's). Adopted and visible are two states; only the second is yours to make, and nothing here can tell you which one you are in.\",\n \"Whether the model has exactly ONE top-level element for the service, with its containers nested inside — which is a question only for a model that declares its own `specification` and is parsed alone. Five top-level boxes for one service parse, validate and bind exactly as well as one there, and then the fleet map, which joins elements to directories, has five candidates for the same service and no way to say which is wrong. A model that EXTENDS the map cannot get into that state by accident: it opens one element, the map's, and anything it declares beside that element is `c4.element-unowned` — which is the same rule enforced instead of stated.\",\n \"Whether the model is the architecture the code actually has. loam parses model.likec4; it never reads a line of the service.\",\n \"Whether a requirement is TRUE of the service. `loam validate` checks that a requirement has a scenario, never that either one describes real behaviour.\",\n \"Whether the scenarios are acceptance criteria somebody could test, or the requirement restated in Given/When/Then.\",\n \"Whether an operationId is the one the code serves, and whether the request and response schemas resemble what the endpoint really accepts and returns. Presence is probed — an operation with no response schema at all is warned about — but what a declared schema SAYS is never compared with anything; only the operationId joins.\",\n \"Whether the runbook's steps work, or the health.yaml SLOs are numbers anyone agreed to.\",\n \"Whether an arch scenario really exercises what its `Covers:` line names. The entries are resolved against the model and health.yaml — never against code, tests, dashboards or alert rules.\",\n \"Whether an ADR records a decision that was made, or one reconstructed afterwards to justify the code.\",\n \"Whether the DEPLOYMENT model is the topology the fleet actually runs. Every join over it is between documents: `instanceOf` resolves against the C4 model, a `#obl-` tag resolves against the obligation vocabulary, and a `Covers: node:` entry resolves against the nodes the map declares. Nothing here knows whether the second cluster exists, whether it is reachable, whether replication is configured, or whether it holds the data a requirement claims — and no check counts datacenters, because how many a service needs is an architectural decision the fleet makes and writes down as a requirement, never one loam evaluates.\",\n \"COMPLETENESS. Forty behaviours documented as one requirement passes every check loam has. So does a service with one endpoint documented out of thirty.\",\n \"REPRODUCIBILITY — the bar the artifact set aims at: could a reader answer, without the code, what the service exposes, what it reaches, what shapes it exchanges, how it is run and what pages whom. The depth probes catch an EMPTY contract; nothing measures a shallow one against the service, so green means the files agree with each other, never that they reach the bar.\",\n \"Whether an arch requirement recording effective configuration or a library's semantics ('retries: -1 is unbounded, proven from the bytecode') is still true of the chart and the classpath. The convention names arch.spec.md as the home for such facts; loam never verifies the values.\",\n \"Whether a permission in `architecture/permissions.yaml` is one the system actually has. `Requires:` is checked against that file and the file against nothing: a vocabulary that agrees with every requirement citing it can still name three permissions the identity provider retired last year, and `owned_by` / `enforced_by` are read as prose — neither is resolved against `services/`, and a realm is never opened. The check is a spelling guard between documents, exactly as `Operations:` is; whether the word is real is a question only the running system answers.\",\n \"Whether `sources` names the files you read. loam checks those paths exist — not that they are the ones the document came from.\",\n];\n"]}
1
+ {"version":3,"file":"unchecked.js","sourceRoot":"","sources":["../../../src/core/brief/unchecked.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;GAWG;AACH,MAAM,CAAC,MAAM,SAAS,GAAa;IACjC,4EAA4E;IAC5E,uEAAuE;IACvE,4EAA4E;IAC5E,2EAA2E;IAC3E,qDAAqD;IACrD,EAAE;IACF,6EAA6E;IAC7E,2EAA2E;IAC3E,sEAAsE;IACtE,sEAAsE;IACtE,gCAAgC;IAChC,gkDAAgkD;IAChkD,6EAA6E;IAC7E,8EAA8E;IAC9E,0EAA0E;IAC1E,6EAA6E;IAC7E,6EAA6E;IAC7E,kDAAkD;IAClD,u/CAAu/C;IACv/C,4EAA4E;IAC5E,8EAA8E;IAC9E,yEAAyE;IACzE,2EAA2E;IAC3E,2EAA2E;IAC3E,uEAAuE;IACvE,2EAA2E;IAC3E,2EAA2E;IAC3E,kDAAkD;IAClD,wmCAAwmC;IACxmC,8oBAA8oB;IAC9oB,8HAA8H;IAC9H,yJAAyJ;IACzJ,oHAAoH;IACpH,wUAAwU;IACxU,yFAAyF;IACzF,6LAA6L;IAC7L,uGAAuG;IACvG,gnBAAgnB;IAChnB,yJAAyJ;IACzJ,mXAAmX;IACnX,2RAA2R;IAC3R,mjBAAmjB;IACnjB,gIAAgI;CACjI,CAAC","sourcesContent":["/**\n * What no check will ever tell you. Read it as the list of ways to be wrong\n * quietly. The checks that do exist — the table an adoption baseline actually\n * meets — live in checks.ts; this module is that table's counterpart, split\n * out so each half can grow without crowding the other.\n *\n * The first two entries are here because they used to be `shape` rules on\n * model.likec4, phrased exactly like the enforced ones. A brief that states an\n * unenforced rule beside four enforced ones is not merely useless — it is the\n * way an agent learns that a green `loam validate` means more than it does.\n * Anything nothing checks belongs on this list, where its status IS the point.\n */\nexport const UNCHECKED: string[] = [\n // Not merely unchecked — unread. This entry used to end \"write one\", on the\n // theory that a view is what makes a model legible. It is, to LikeC4's\n // renderer; loam computes none. Saying otherwise taught agents that a views\n // block was owed to loam, and an `include *` over the FLEET map is the one\n // shape that costs minutes rather than milliseconds.\n //\n // Still exactly true of PRESENCE, which is what this entry is about. Rule 26\n // lets loam read a dynamic view's declared steps WHERE ONE EXISTS; nothing\n // grades their absence, and the failure this comment records — agents\n // learning that a views block is owed to loam — is the one rule 26 is\n // written to prevent recurring.\n \"Whether model.likec4 declares a `views { ... }` block, or any view at all — and nothing in loam ever will. What loam reads, when a `dynamic view` IS present — in the fleet map, or in this service's own project once a view there carries `#req-` — is only what its author declared: the view's tags and its ordered steps. It computes no view and renders nothing, and it never grades a model for lacking one — a fleet that draws no diagrams owes loam no views block, today or ever. Views belong to LikeC4's own renderer: write them if you want diagrams, and read them with `npx likec4 start` at the docs repo root. The root project holds `architecture/` AND every model that EXTENDS the map, so a model written the way this brief asks for it renders there beside the fleet map with no second file to write — one project, one entry in the renderer's picker. The exception is a model that declares its own `specification` block and is therefore parsed alone: its kinds would be duplicates inside the root project, so its directory is excluded there and its own `services/<id>/likec4.config.json` — written by `loam subsystem sync`, loam's file and never yours — makes it a project of its own. A feature's delta.likec4 is the one document always pointed at alone (`npx likec4 start features/<FEAT>`), because it re-declares the landscape's identifiers and carries a `specification` block of its own. Scope your views too — computing a view is superlinear in the number of edges, and an `include *` over `architecture/landscape.likec4` is the expensive one, because that file holds every call in the fleet.\",\n // The readability half of the entry above. They are separate entries because\n // the failures are: one is a view that takes minutes to compute, the other is\n // a view that computes instantly and cannot be read. A fleet acquires the\n // second one the day its second service starts publishing, and every service\n // after that makes it worse by exactly one edge — which is why it arrives as\n // a surprise on a map that was fine last quarter.\n \"Whether the fleet map is LEGIBLE — nothing in loam draws it, so nothing in loam notices. The shape that goes first is a shared broker: Kafka, a bus, a gateway, drawn as ONE element, is the node every service in the fleet points at, and a view over it is a star with sixty spokes. Model the TOPIC rather than the broker — `paymentEvents = topic 'payment.events'` nested inside the broker's element, with the edges pointing at `kafka.paymentEvents` — and one hub of degree sixty becomes twelve of degree five, which is also the truer model: the contract a producer and a consumer share is the topic, never the broker. Declare `element topic { style { shape queue } }` in the `specification` block and give the nested elements that kind: loam reads `database`, `queue`, `topic`, `cache` and `bucket` as infrastructure rather than as candidate services, so a topic drawn this way is out of the service census and nobody is asked for a `services/payment.events/` (the same element spelled `container` earns `landscape.service-undocumented`). Adding `#external` to that kind is now a VIEW choice — LikeC4 does not inherit tags, so a topic under an `#external` broker is not external itself, and tagging the kind is what puts the topics behind the `exclude` below — not a way to silence a check. Then scope the views, which is where the rest of the legibility lives: `exclude element.tag = #external` keeps the fleet overview to the calls between our own services, and a second view over the broker draws the event spine on its own.\",\n // The third view entry, and the one docs/DESIGN.md rule 26 makes permanent:\n // what a rendered view SHOWS is never computed, so whether an adopted service\n // is on the picture is a fact loam cannot have. It is here rather than a\n // check because each of the three ways to vanish is either the fleet's own\n // vocabulary (a placeholder tag `loam seed` never writes), a static view's\n // `include` predicates (unread by rule), or LikeC4's semantics — and a\n // warning that is right once and noise forty times is the warning somebody\n // silences. The brief NAMES the element's unread tags (landscape.ts); this\n // entry says what nothing will ever do with them.\n \"Whether an adopted service is VISIBLE on the fleet map, which is a different state from being drawn on it. Three things make a documented, bound, edged service disappear, and no check names any of them. A placeholder tag somebody put on the element before adoption — `#provisional`, or whatever your fleet uses to mean drawn-ahead-of-adoption — survives adoption untouched: loam's `seed` writes no such tag, loam reads none, and a fleet view written `exclude element.tag = #<that>` keeps hiding the service until a human removes it, so clearing it is part of the landscape edit. A curated view that lists its members by id (`include paymentService, billingService`) gains nothing by itself. And `include *` shows top-level elements only: once services nest inside a boundary element — the shape loam's own generated subsystem views draw a box around — a fleet view written that way collapses to the boundary and shows no service at all, and needs `include <boundary>.**` (LikeC4's semantics, not loam's). Adopted and visible are two states; only the second is yours to make, and nothing here can tell you which one you are in.\",\n \"Whether the model has exactly ONE top-level element for the service, with its containers nested inside — which is a question only for a model that declares its own `specification` and is parsed alone. Five top-level boxes for one service parse, validate and bind exactly as well as one there, and then the fleet map, which joins elements to directories, has five candidates for the same service and no way to say which is wrong. A model that EXTENDS the map cannot get into that state by accident: it opens one element, the map's, and anything it declares beside that element is `c4.element-unowned` — which is the same rule enforced instead of stated.\",\n \"Whether the model is the architecture the code actually has. loam parses model.likec4; it never reads a line of the service.\",\n \"Whether a requirement is TRUE of the service. `loam validate` checks that a requirement has a scenario, never that either one describes real behaviour.\",\n \"Whether the scenarios are acceptance criteria somebody could test, or the requirement restated in Given/When/Then.\",\n \"Whether an operationId is the one the code serves, and whether the request and response schemas resemble what the endpoint really accepts and returns. Presence is probed — an operation with no response schema at all is warned about — but what a declared schema SAYS is never compared with anything; only the operationId joins.\",\n \"Whether the runbook's steps work, or the health.yaml SLOs are numbers anyone agreed to.\",\n \"Whether an arch scenario really exercises what its `Covers:` line names. The entries are resolved against the model and health.yaml — never against code, tests, dashboards or alert rules.\",\n \"Whether an ADR records a decision that was made, or one reconstructed afterwards to justify the code.\",\n \"Whether the DEPLOYMENT model is the topology the fleet actually runs. Every join over it is between documents: `instanceOf` resolves against the C4 model, a `#obl-` tag resolves against the obligation vocabulary, and a `Covers: node:` entry resolves against the nodes the map declares. Nothing here knows whether the second cluster exists, whether it is reachable, whether replication is configured, or whether it holds the data a requirement claims — and no check counts datacenters, because how many a service needs is an architectural decision the fleet makes and writes down as a requirement, never one loam evaluates.\",\n \"COMPLETENESS. Forty behaviours documented as one requirement passes every check loam has. So does a service with one endpoint documented out of thirty.\",\n \"REPRODUCIBILITY — the bar the artifact set aims at: could a reader answer, without the code, what the service exposes, what it reaches, what shapes it exchanges, how it is run and what pages whom. The depth probes catch an EMPTY contract; nothing measures a shallow one against the service, so green means the files agree with each other, never that they reach the bar.\",\n \"Whether an arch requirement recording effective configuration or a library's semantics ('retries: -1 is unbounded, proven from the bytecode') is still true of the chart and the classpath. The convention names arch.spec.md as the home for such facts; loam never verifies the values.\",\n \"Whether a permission in `architecture/permissions.yaml` is one the system actually has. `Requires:` is checked against that file and the file against nothing: a vocabulary that agrees with every requirement citing it can still name three permissions the identity provider retired last year, and `owned_by` / `enforced_by` are read as prose — neither is resolved against `services/`, and a realm is never opened. The check is a spelling guard between documents, exactly as `Operations:` is; whether the word is real is a question only the running system answers.\",\n \"Whether `sources` names the files you read. loam checks those paths exist — not that they are the ones the document came from.\",\n];\n"]}
@@ -9,9 +9,9 @@
9
9
  * `core/` may not import from `commands/` (AGENTS.md), so the choice was a second
10
10
  * copy of the two-line composition or one copy one level down. A second copy is
11
11
  * the copy that drifts, and the thing that would drift is which documents are in
12
- * the project: the generated `architecture/subsystems.likec4` exclusion is a
13
- * correctness rule (see `./documents.ts`), not a preference, and a reader that
14
- * forgot it would blank a fleet's whole map over a stale generated file.
12
+ * the project: the GENERATED documents' exclusion is a correctness rule (see
13
+ * `./documents.ts`), not a preference, and a reader that forgot it would blank a
14
+ * fleet's whole map over a stale generated file.
15
15
  *
16
16
  * So the wrapper moved here and `validate`'s own module kept the landscape
17
17
  * CONTAINMENT — `readLandscape` / `unreadableLandscape` — which is genuinely
@@ -22,11 +22,33 @@ import { join, relative, resolve } from "node:path";
22
22
  import { excludingPath, readRootExclude } from "../root-project/exclude.js";
23
23
  import { architectureDocuments } from "./documents.js";
24
24
  import { asLoadedDoc, loadProject } from "./load.js";
25
- import { landscapePath, subsystemViewsPath } from "../../repo/paths.js";
25
+ import { GENERATED_ARCHITECTURE_FILES, landscapePath } from "../../repo/paths.js";
26
26
  /** `<docsDir>/architecture` — the LikeC4 project root, spelled once. */
27
27
  export function architectureDir(docsDir) {
28
28
  return join(docsDir, "architecture");
29
29
  }
30
+ /**
31
+ * Every document inside `architecture/` that LOAM GENERATES, absolute — the ONE
32
+ * answer four project reads have to agree on, and the reason it is a SET rather
33
+ * than the single path it used to be.
34
+ *
35
+ * There are two generated files now (`subsystems.likec4` and `services.likec4`,
36
+ * both written by `loam subsystem sync`), and every one of these readers must
37
+ * drop BOTH: this module's own project list, the merge-preview staging beside it
38
+ * (`./staged.ts`), and the renderer's-eye load `c4.fleet-project-invalid` builds
39
+ * (`commands/validate/fleet/views/root-documents.ts`). A views-only document
40
+ * does not parse standalone — its `include` lines name elements the landscape
41
+ * declares — so a reader that keeps one loads a document whose only fault may be
42
+ * that `sync` has not run yet, and ONE parse error in a project blanks the model
43
+ * for every document in it. A file listed by one reader and forgotten by another
44
+ * fails silently in exactly that direction, which is why the NAMES live in one
45
+ * table (`repo/paths.ts`'s `GENERATED_ARCHITECTURE_FILES`) and the join to a
46
+ * docsDir lives here, once, rather than as one `…ViewsPath(docsDir)` call per
47
+ * reader that somebody has to remember to extend.
48
+ */
49
+ export function generatedArchitectureDocuments(docsDir) {
50
+ return Object.keys(GENERATED_ARCHITECTURE_FILES).map((name) => join(architectureDir(docsDir), name));
51
+ }
30
52
  /**
31
53
  * Exactly the documents `loadArchitecture` puts in the fleet map's project, as
32
54
  * one named answer.
@@ -35,10 +57,10 @@ export function architectureDir(docsDir) {
35
57
  * model that extends the map is parsed beside it (`c4/service-model/`), and the
36
58
  * set it must be staged with is this one plus the model. Two spellings of "the
37
59
  * architecture project's documents" would be two chances to forget the
38
- * generated `subsystems.likec4` exclusion — a correctness rule, not an
39
- * optimisation (`./documents.ts` says what it prevents) — and the reader that
40
- * forgot it would blank a service's whole model over a stale generated file it
41
- * has nothing to do with.
60
+ * generated documents' exclusion — a correctness rule, not an optimisation
61
+ * (`./documents.ts` says what it prevents) — and the reader that forgot it would
62
+ * blank a service's whole model over a stale generated file it has nothing to do
63
+ * with.
42
64
  *
43
65
  * THE ROOT `exclude` IS HONOURED HERE, and that is the second correctness rule
44
66
  * this one list carries. A `.likec4` under `architecture/` that the root project
@@ -68,7 +90,7 @@ export function architectureDir(docsDir) {
68
90
  * renderer drops.
69
91
  */
70
92
  export async function architectureProjectDocuments(docsDir) {
71
- const paths = await architectureDocuments(architectureDir(docsDir), [subsystemViewsPath(docsDir)]);
93
+ const paths = await architectureDocuments(architectureDir(docsDir), generatedArchitectureDocuments(docsDir));
72
94
  const exclude = await readRootExclude(docsDir);
73
95
  if (exclude === null || exclude.length === 0)
74
96
  return paths;
@@ -1 +1 @@
1
- {"version":3,"file":"architecture.js","sourceRoot":"","sources":["../../../../src/core/c4/project/architecture.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;GAmBG;AACH,OAAO,EAAE,IAAI,EAAE,QAAQ,EAAE,OAAO,EAAE,MAAM,WAAW,CAAC;AAEpD,OAAO,EAAE,aAAa,EAAE,eAAe,EAAE,MAAM,4BAA4B,CAAC;AAC5E,OAAO,EAAE,qBAAqB,EAAE,MAAM,gBAAgB,CAAC;AACvD,OAAO,EAAE,WAAW,EAAE,WAAW,EAAE,MAAM,WAAW,CAAC;AAErD,OAAO,EAAE,aAAa,EAAE,kBAAkB,EAAE,MAAM,qBAAqB,CAAC;AAExE,wEAAwE;AACxE,MAAM,UAAU,eAAe,CAAC,OAAgB;IAC9C,OAAO,IAAI,CAAC,OAAO,EAAE,cAAc,CAAC,CAAC;AACvC,CAAC;AAED;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAuCG;AACH,MAAM,CAAC,KAAK,UAAU,4BAA4B,CAAC,OAAgB;IACjE,MAAM,KAAK,GAAG,MAAM,qBAAqB,CAAC,eAAe,CAAC,OAAO,CAAC,EAAE,CAAC,kBAAkB,CAAC,OAAO,CAAC,CAAC,CAAC,CAAC;IACnG,MAAM,OAAO,GAAG,MAAM,eAAe,CAAC,OAAO,CAAC,CAAC;IAC/C,IAAI,OAAO,KAAK,IAAI,IAAI,OAAO,CAAC,MAAM,KAAK,CAAC;QAAE,OAAO,KAAK,CAAC;IAC3D,MAAM,GAAG,GAAG,OAAO,CAAC,aAAa,CAAC,OAAO,CAAC,CAAC,CAAC;IAC5C,OAAO,KAAK,CAAC,MAAM,CAAC,CAAC,IAAI,EAAE,EAAE,CAAC,OAAO,CAAC,IAAI,CAAC,KAAK,GAAG,IAAI,aAAa,CAAC,OAAO,EAAE,QAAQ,CAAC,OAAO,EAAE,IAAI,CAAC,CAAC,KAAK,IAAI,CAAC,CAAC;AACnH,CAAC;AAED;;;;;;;;;;;GAWG;AACH,MAAM,CAAC,KAAK,UAAU,gBAAgB,CAAC,OAAgB;IACrD,OAAO,WAAW,CAAC,MAAM,WAAW,CAAC,eAAe,CAAC,OAAO,CAAC,EAAE,MAAM,4BAA4B,CAAC,OAAO,CAAC,CAAC,CAAC,CAAC;AAC/G,CAAC","sourcesContent":["/**\n * `architecture/` as ONE LikeC4 project — the docsDir-aware wrapper over the two\n * modules beside it.\n *\n * It lived in `commands/validate/fleet/load.ts` until the use-case axis grew a\n * second reader. That was the right home while `validate --all` was the only\n * caller: the command layer is where a `DocsDir` becomes paths. It stopped being\n * the right home the moment `core/usecases/fleet.ts` needed the same load —\n * `core/` may not import from `commands/` (AGENTS.md), so the choice was a second\n * copy of the two-line composition or one copy one level down. A second copy is\n * the copy that drifts, and the thing that would drift is which documents are in\n * the project: the generated `architecture/subsystems.likec4` exclusion is a\n * correctness rule (see `./documents.ts`), not a preference, and a reader that\n * forgot it would blank a fleet's whole map over a stale generated file.\n *\n * So the wrapper moved here and `validate`'s own module kept the landscape\n * CONTAINMENT — `readLandscape` / `unreadableLandscape` — which is genuinely\n * command-layer business: it is about which validate target a failed read is\n * filed against.\n */\nimport { join, relative, resolve } from \"node:path\";\nimport type { LoadedDoc } from \"../likec4.js\";\nimport { excludingPath, readRootExclude } from \"../root-project/exclude.js\";\nimport { architectureDocuments } from \"./documents.js\";\nimport { asLoadedDoc, loadProject } from \"./load.js\";\nimport type { DocsDir } from \"../../kernel/ids/dirs.js\";\nimport { landscapePath, subsystemViewsPath } from \"../../repo/paths.js\";\n\n/** `<docsDir>/architecture` — the LikeC4 project root, spelled once. */\nexport function architectureDir(docsDir: DocsDir): string {\n return join(docsDir, \"architecture\");\n}\n\n/**\n * Exactly the documents `loadArchitecture` puts in the fleet map's project, as\n * one named answer.\n *\n * It exists because a SECOND project now has to contain the same set: a service\n * model that extends the map is parsed beside it (`c4/service-model/`), and the\n * set it must be staged with is this one plus the model. Two spellings of \"the\n * architecture project's documents\" would be two chances to forget the\n * generated `subsystems.likec4` exclusion — a correctness rule, not an\n * optimisation (`./documents.ts` says what it prevents) — and the reader that\n * forgot it would blank a service's whole model over a stale generated file it\n * has nothing to do with.\n *\n * THE ROOT `exclude` IS HONOURED HERE, and that is the second correctness rule\n * this one list carries. A `.likec4` under `architecture/` that the root project\n * excludes is a document the RENDERER never loads, so a global style group\n * declared in one is a name the fleet project cannot resolve — and until this\n * filter existed the generated subsystem views referenced it anyway: `likec4\n * validate` Invalid on every run, `loam validate --all` at 0 errors, `doctor`\n * healthy and `sync` \"current\" (verification 2026-09-04, W5). A root config loam\n * cannot read an `exclude` list out of filters nothing: asserting an exclusion on\n * evidence loam does not have is worse than loading one document too many.\n *\n * THE MAP ITSELF IS THE FLOOR, and it is not a special case for tidiness. The\n * filter had none, so an entry covering `architecture/landscape.likec4` — an\n * `architecture/*.likec4` a team wrote for a palette, say — left this list\n * EMPTY, and an empty project parses with zero errors. Every reader then took\n * that for \"the map declares nothing\": `validate --all` reported\n * `landscape.service-unmodelled` naming the very file that models the service\n * and `c4.invalid` on every extending model, `subsystem sync` rewrote a good\n * generated file down to a title, and nothing in either run mentioned the\n * `exclude` entry that caused it (verification 2026-09-04, review C). Keeping\n * the landscape is the tolerant direction this same banner already chooses for\n * an unreadable config: loam grades one document the renderer may skip, rather\n * than asserting a fleet's whole architecture says nothing. The entry is NAMED\n * to its author one target over — `landscape.excluded`, in\n * `commands/validate/fleet/views/projects.ts`, which reads this same list\n * through this same matcher so the two can never disagree about which file the\n * renderer drops.\n */\nexport async function architectureProjectDocuments(docsDir: DocsDir): Promise<string[]> {\n const paths = await architectureDocuments(architectureDir(docsDir), [subsystemViewsPath(docsDir)]);\n const exclude = await readRootExclude(docsDir);\n if (exclude === null || exclude.length === 0) return paths;\n const map = resolve(landscapePath(docsDir));\n return paths.filter((path) => resolve(path) === map || excludingPath(exclude, relative(docsDir, path)) === null);\n}\n\n/**\n * The fleet map, read as the PROJECT it actually is.\n *\n * `architecture/landscape.likec4` plus every `architecture/usecases/*.likec4`,\n * merged the way the renderer merges them — because a use case declares views\n * over the landscape's elements and does not parse standalone (measured: five\n * errors). `architectureDocuments` owns which files are in and why the\n * generated one is not.\n *\n * A fleet with no use cases loads exactly the landscape and behaves as it\n * always did, which is what keeps this a widening rather than a change.\n */\nexport async function loadArchitecture(docsDir: DocsDir): Promise<LoadedDoc> {\n return asLoadedDoc(await loadProject(architectureDir(docsDir), await architectureProjectDocuments(docsDir)));\n}\n"]}
1
+ {"version":3,"file":"architecture.js","sourceRoot":"","sources":["../../../../src/core/c4/project/architecture.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;GAmBG;AACH,OAAO,EAAE,IAAI,EAAE,QAAQ,EAAE,OAAO,EAAE,MAAM,WAAW,CAAC;AAEpD,OAAO,EAAE,aAAa,EAAE,eAAe,EAAE,MAAM,4BAA4B,CAAC;AAC5E,OAAO,EAAE,qBAAqB,EAAE,MAAM,gBAAgB,CAAC;AACvD,OAAO,EAAE,WAAW,EAAE,WAAW,EAAE,MAAM,WAAW,CAAC;AAErD,OAAO,EAAE,4BAA4B,EAAE,aAAa,EAAE,MAAM,qBAAqB,CAAC;AAElF,wEAAwE;AACxE,MAAM,UAAU,eAAe,CAAC,OAAgB;IAC9C,OAAO,IAAI,CAAC,OAAO,EAAE,cAAc,CAAC,CAAC;AACvC,CAAC;AAED;;;;;;;;;;;;;;;;;;GAkBG;AACH,MAAM,UAAU,8BAA8B,CAAC,OAAgB;IAC7D,OAAO,MAAM,CAAC,IAAI,CAAC,4BAA4B,CAAC,CAAC,GAAG,CAAC,CAAC,IAAI,EAAE,EAAE,CAAC,IAAI,CAAC,eAAe,CAAC,OAAO,CAAC,EAAE,IAAI,CAAC,CAAC,CAAC;AACvG,CAAC;AAED;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAuCG;AACH,MAAM,CAAC,KAAK,UAAU,4BAA4B,CAAC,OAAgB;IACjE,MAAM,KAAK,GAAG,MAAM,qBAAqB,CAAC,eAAe,CAAC,OAAO,CAAC,EAAE,8BAA8B,CAAC,OAAO,CAAC,CAAC,CAAC;IAC7G,MAAM,OAAO,GAAG,MAAM,eAAe,CAAC,OAAO,CAAC,CAAC;IAC/C,IAAI,OAAO,KAAK,IAAI,IAAI,OAAO,CAAC,MAAM,KAAK,CAAC;QAAE,OAAO,KAAK,CAAC;IAC3D,MAAM,GAAG,GAAG,OAAO,CAAC,aAAa,CAAC,OAAO,CAAC,CAAC,CAAC;IAC5C,OAAO,KAAK,CAAC,MAAM,CAAC,CAAC,IAAI,EAAE,EAAE,CAAC,OAAO,CAAC,IAAI,CAAC,KAAK,GAAG,IAAI,aAAa,CAAC,OAAO,EAAE,QAAQ,CAAC,OAAO,EAAE,IAAI,CAAC,CAAC,KAAK,IAAI,CAAC,CAAC;AACnH,CAAC;AAED;;;;;;;;;;;GAWG;AACH,MAAM,CAAC,KAAK,UAAU,gBAAgB,CAAC,OAAgB;IACrD,OAAO,WAAW,CAAC,MAAM,WAAW,CAAC,eAAe,CAAC,OAAO,CAAC,EAAE,MAAM,4BAA4B,CAAC,OAAO,CAAC,CAAC,CAAC,CAAC;AAC/G,CAAC","sourcesContent":["/**\n * `architecture/` as ONE LikeC4 project — the docsDir-aware wrapper over the two\n * modules beside it.\n *\n * It lived in `commands/validate/fleet/load.ts` until the use-case axis grew a\n * second reader. That was the right home while `validate --all` was the only\n * caller: the command layer is where a `DocsDir` becomes paths. It stopped being\n * the right home the moment `core/usecases/fleet.ts` needed the same load —\n * `core/` may not import from `commands/` (AGENTS.md), so the choice was a second\n * copy of the two-line composition or one copy one level down. A second copy is\n * the copy that drifts, and the thing that would drift is which documents are in\n * the project: the GENERATED documents' exclusion is a correctness rule (see\n * `./documents.ts`), not a preference, and a reader that forgot it would blank a\n * fleet's whole map over a stale generated file.\n *\n * So the wrapper moved here and `validate`'s own module kept the landscape\n * CONTAINMENT — `readLandscape` / `unreadableLandscape` — which is genuinely\n * command-layer business: it is about which validate target a failed read is\n * filed against.\n */\nimport { join, relative, resolve } from \"node:path\";\nimport type { LoadedDoc } from \"../likec4.js\";\nimport { excludingPath, readRootExclude } from \"../root-project/exclude.js\";\nimport { architectureDocuments } from \"./documents.js\";\nimport { asLoadedDoc, loadProject } from \"./load.js\";\nimport type { DocsDir } from \"../../kernel/ids/dirs.js\";\nimport { GENERATED_ARCHITECTURE_FILES, landscapePath } from \"../../repo/paths.js\";\n\n/** `<docsDir>/architecture` — the LikeC4 project root, spelled once. */\nexport function architectureDir(docsDir: DocsDir): string {\n return join(docsDir, \"architecture\");\n}\n\n/**\n * Every document inside `architecture/` that LOAM GENERATES, absolute — the ONE\n * answer four project reads have to agree on, and the reason it is a SET rather\n * than the single path it used to be.\n *\n * There are two generated files now (`subsystems.likec4` and `services.likec4`,\n * both written by `loam subsystem sync`), and every one of these readers must\n * drop BOTH: this module's own project list, the merge-preview staging beside it\n * (`./staged.ts`), and the renderer's-eye load `c4.fleet-project-invalid` builds\n * (`commands/validate/fleet/views/root-documents.ts`). A views-only document\n * does not parse standalone — its `include` lines name elements the landscape\n * declares — so a reader that keeps one loads a document whose only fault may be\n * that `sync` has not run yet, and ONE parse error in a project blanks the model\n * for every document in it. A file listed by one reader and forgotten by another\n * fails silently in exactly that direction, which is why the NAMES live in one\n * table (`repo/paths.ts`'s `GENERATED_ARCHITECTURE_FILES`) and the join to a\n * docsDir lives here, once, rather than as one `…ViewsPath(docsDir)` call per\n * reader that somebody has to remember to extend.\n */\nexport function generatedArchitectureDocuments(docsDir: DocsDir): string[] {\n return Object.keys(GENERATED_ARCHITECTURE_FILES).map((name) => join(architectureDir(docsDir), name));\n}\n\n/**\n * Exactly the documents `loadArchitecture` puts in the fleet map's project, as\n * one named answer.\n *\n * It exists because a SECOND project now has to contain the same set: a service\n * model that extends the map is parsed beside it (`c4/service-model/`), and the\n * set it must be staged with is this one plus the model. Two spellings of \"the\n * architecture project's documents\" would be two chances to forget the\n * generated documents' exclusion — a correctness rule, not an optimisation\n * (`./documents.ts` says what it prevents) — and the reader that forgot it would\n * blank a service's whole model over a stale generated file it has nothing to do\n * with.\n *\n * THE ROOT `exclude` IS HONOURED HERE, and that is the second correctness rule\n * this one list carries. A `.likec4` under `architecture/` that the root project\n * excludes is a document the RENDERER never loads, so a global style group\n * declared in one is a name the fleet project cannot resolve — and until this\n * filter existed the generated subsystem views referenced it anyway: `likec4\n * validate` Invalid on every run, `loam validate --all` at 0 errors, `doctor`\n * healthy and `sync` \"current\" (verification 2026-09-04, W5). A root config loam\n * cannot read an `exclude` list out of filters nothing: asserting an exclusion on\n * evidence loam does not have is worse than loading one document too many.\n *\n * THE MAP ITSELF IS THE FLOOR, and it is not a special case for tidiness. The\n * filter had none, so an entry covering `architecture/landscape.likec4` — an\n * `architecture/*.likec4` a team wrote for a palette, say — left this list\n * EMPTY, and an empty project parses with zero errors. Every reader then took\n * that for \"the map declares nothing\": `validate --all` reported\n * `landscape.service-unmodelled` naming the very file that models the service\n * and `c4.invalid` on every extending model, `subsystem sync` rewrote a good\n * generated file down to a title, and nothing in either run mentioned the\n * `exclude` entry that caused it (verification 2026-09-04, review C). Keeping\n * the landscape is the tolerant direction this same banner already chooses for\n * an unreadable config: loam grades one document the renderer may skip, rather\n * than asserting a fleet's whole architecture says nothing. The entry is NAMED\n * to its author one target over — `landscape.excluded`, in\n * `commands/validate/fleet/views/projects.ts`, which reads this same list\n * through this same matcher so the two can never disagree about which file the\n * renderer drops.\n */\nexport async function architectureProjectDocuments(docsDir: DocsDir): Promise<string[]> {\n const paths = await architectureDocuments(architectureDir(docsDir), generatedArchitectureDocuments(docsDir));\n const exclude = await readRootExclude(docsDir);\n if (exclude === null || exclude.length === 0) return paths;\n const map = resolve(landscapePath(docsDir));\n return paths.filter((path) => resolve(path) === map || excludingPath(exclude, relative(docsDir, path)) === null);\n}\n\n/**\n * The fleet map, read as the PROJECT it actually is.\n *\n * `architecture/landscape.likec4` plus every `architecture/usecases/*.likec4`,\n * merged the way the renderer merges them — because a use case declares views\n * over the landscape's elements and does not parse standalone (measured: five\n * errors). `architectureDocuments` owns which files are in and why the\n * generated one is not.\n *\n * A fleet with no use cases loads exactly the landscape and behaves as it\n * always did, which is what keeps this a widening rather than a change.\n */\nexport async function loadArchitecture(docsDir: DocsDir): Promise<LoadedDoc> {\n return asLoadedDoc(await loadProject(architectureDir(docsDir), await architectureProjectDocuments(docsDir)));\n}\n"]}
@@ -9,8 +9,8 @@
9
9
  * that STANDS ALONE — one `services/<tree>/**` entry per standalone model,
10
10
  * never the whole subtree (`c4/service-model/renderer.ts` owns that list, and
11
11
  * is the one place loam reads that file). loam's own answer here is all of them
12
- * EXCEPT the generated `architecture/subsystems.likec4`, and that exception is
13
- * not an optimisation:
12
+ * EXCEPT the documents loam GENERATES there (`architecture/subsystems.likec4`
13
+ * and `architecture/services.likec4`), and that exception is not an optimisation:
14
14
  *
15
15
  * - docs/DESIGN.md rule 26 excludes the generated file from every read loam
16
16
  * performs. Its staleness is a byte compare (`subsystem.views-stale`) and
@@ -22,8 +22,9 @@
22
22
  * whose only problem is a stale generated file would lose its whole map
23
23
  * instead of getting the one finding that names the repair.
24
24
  *
25
- * The skip is passed in by the caller rather than spelled here, so the filename
26
- * stays written in `repo/paths.ts` alone.
25
+ * The skip is passed in by the caller rather than spelled here, so the filenames
26
+ * stay written in `repo/paths.ts` alone, and every reader takes the same SET out
27
+ * (`./architecture.ts` holds the one join).
27
28
  */
28
29
  import { readdir } from "node:fs/promises";
29
30
  import { join, resolve } from "node:path";
@@ -1 +1 @@
1
- {"version":3,"file":"documents.js","sourceRoot":"","sources":["../../../../src/core/c4/project/documents.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;GA0BG;AACH,OAAO,EAAE,OAAO,EAAE,MAAM,kBAAkB,CAAC;AAC3C,OAAO,EAAE,IAAI,EAAE,OAAO,EAAE,MAAM,WAAW,CAAC;AAE1C;;;;;;;;;;GAUG;AACH,MAAM,CAAC,KAAK,UAAU,qBAAqB,CAAC,GAAW,EAAE,IAAI,GAAa,EAAE;IAC1E,MAAM,QAAQ,GAAG,IAAI,GAAG,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC,IAAI,EAAE,EAAE,CAAC,OAAO,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC;IAC5D,MAAM,KAAK,GAAa,EAAE,CAAC;IAC3B,MAAM,IAAI,GAAG,KAAK,EAAE,EAAU,EAAiB,EAAE;QAC/C,IAAI,OAAO,CAAC;QACZ,IAAI,CAAC;YACH,OAAO,GAAG,MAAM,OAAO,CAAC,EAAE,EAAE,EAAE,aAAa,EAAE,IAAI,EAAE,CAAC,CAAC;QACvD,CAAC;QAAC,MAAM,CAAC;YACP,OAAO;QACT,CAAC;QACD,KAAK,MAAM,KAAK,IAAI,OAAO,EAAE,CAAC;YAC5B,MAAM,IAAI,GAAG,IAAI,CAAC,EAAE,EAAE,KAAK,CAAC,IAAI,CAAC,CAAC;YAClC,IAAI,KAAK,CAAC,WAAW,EAAE;gBAAE,MAAM,IAAI,CAAC,IAAI,CAAC,CAAC;iBACrC,IAAI,KAAK,CAAC,IAAI,CAAC,QAAQ,CAAC,SAAS,CAAC,IAAI,CAAC,QAAQ,CAAC,GAAG,CAAC,OAAO,CAAC,IAAI,CAAC,CAAC;gBAAE,KAAK,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;QAC5F,CAAC;IACH,CAAC,CAAC;IACF,MAAM,IAAI,CAAC,OAAO,CAAC,GAAG,CAAC,CAAC,CAAC;IACzB,OAAO,KAAK,CAAC,IAAI,EAAE,CAAC;AACtB,CAAC","sourcesContent":["/**\n * Which `.likec4` documents in a docs repo's `architecture/` loam loads as one\n * project.\n *\n * The renderer's answer is \"all of them\". The root `likec4.config.json` no\n * longer excludes `services/**`: the root project holds the map AND every\n * service model that EXTENDS it, because such a model is only readable beside\n * the map, and `loam subsystem sync` excludes only the directory of each model\n * that STANDS ALONE — one `services/<tree>/**` entry per standalone model,\n * never the whole subtree (`c4/service-model/renderer.ts` owns that list, and\n * is the one place loam reads that file). loam's own answer here is all of them\n * EXCEPT the generated `architecture/subsystems.likec4`, and that exception is\n * not an optimisation:\n *\n * - docs/DESIGN.md rule 26 excludes the generated file from every read loam\n * performs. Its staleness is a byte compare (`subsystem.views-stale`) and\n * nothing anywhere reads its contents.\n * - Including it would invent a failure mode. Its `include` lines name\n * landscape element ids, so a file that has drifted — a service removed\n * since the last `loam subsystem sync` — would be a parse error, and a parse\n * error in one project document blanks the model for all of them. A fleet\n * whose only problem is a stale generated file would lose its whole map\n * instead of getting the one finding that names the repair.\n *\n * The skip is passed in by the caller rather than spelled here, so the filename\n * stays written in `repo/paths.ts` alone.\n */\nimport { readdir } from \"node:fs/promises\";\nimport { join, resolve } from \"node:path\";\n\n/**\n * Every `.likec4` file under `dir`, recursively, minus `skip`, in a stable\n * order.\n *\n * Sorted because it decides the staging order and therefore nothing at all —\n * LikeC4 merges a project regardless — but a stable list makes a failure\n * reproducible and a diff of one readable. A directory that does not exist is\n * an empty list, never a throw: a fleet with no `architecture/` is a fleet with\n * nothing to load, which is already `landscape.missing`'s question, not this\n * function's.\n */\nexport async function architectureDocuments(dir: string, skip: string[] = []): Promise<string[]> {\n const excluded = new Set(skip.map((path) => resolve(path)));\n const found: string[] = [];\n const walk = async (at: string): Promise<void> => {\n let entries;\n try {\n entries = await readdir(at, { withFileTypes: true });\n } catch {\n return;\n }\n for (const entry of entries) {\n const path = join(at, entry.name);\n if (entry.isDirectory()) await walk(path);\n else if (entry.name.endsWith(\".likec4\") && !excluded.has(resolve(path))) found.push(path);\n }\n };\n await walk(resolve(dir));\n return found.sort();\n}\n"]}
1
+ {"version":3,"file":"documents.js","sourceRoot":"","sources":["../../../../src/core/c4/project/documents.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;GA2BG;AACH,OAAO,EAAE,OAAO,EAAE,MAAM,kBAAkB,CAAC;AAC3C,OAAO,EAAE,IAAI,EAAE,OAAO,EAAE,MAAM,WAAW,CAAC;AAE1C;;;;;;;;;;GAUG;AACH,MAAM,CAAC,KAAK,UAAU,qBAAqB,CAAC,GAAW,EAAE,IAAI,GAAa,EAAE;IAC1E,MAAM,QAAQ,GAAG,IAAI,GAAG,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC,IAAI,EAAE,EAAE,CAAC,OAAO,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC;IAC5D,MAAM,KAAK,GAAa,EAAE,CAAC;IAC3B,MAAM,IAAI,GAAG,KAAK,EAAE,EAAU,EAAiB,EAAE;QAC/C,IAAI,OAAO,CAAC;QACZ,IAAI,CAAC;YACH,OAAO,GAAG,MAAM,OAAO,CAAC,EAAE,EAAE,EAAE,aAAa,EAAE,IAAI,EAAE,CAAC,CAAC;QACvD,CAAC;QAAC,MAAM,CAAC;YACP,OAAO;QACT,CAAC;QACD,KAAK,MAAM,KAAK,IAAI,OAAO,EAAE,CAAC;YAC5B,MAAM,IAAI,GAAG,IAAI,CAAC,EAAE,EAAE,KAAK,CAAC,IAAI,CAAC,CAAC;YAClC,IAAI,KAAK,CAAC,WAAW,EAAE;gBAAE,MAAM,IAAI,CAAC,IAAI,CAAC,CAAC;iBACrC,IAAI,KAAK,CAAC,IAAI,CAAC,QAAQ,CAAC,SAAS,CAAC,IAAI,CAAC,QAAQ,CAAC,GAAG,CAAC,OAAO,CAAC,IAAI,CAAC,CAAC;gBAAE,KAAK,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;QAC5F,CAAC;IACH,CAAC,CAAC;IACF,MAAM,IAAI,CAAC,OAAO,CAAC,GAAG,CAAC,CAAC,CAAC;IACzB,OAAO,KAAK,CAAC,IAAI,EAAE,CAAC;AACtB,CAAC","sourcesContent":["/**\n * Which `.likec4` documents in a docs repo's `architecture/` loam loads as one\n * project.\n *\n * The renderer's answer is \"all of them\". The root `likec4.config.json` no\n * longer excludes `services/**`: the root project holds the map AND every\n * service model that EXTENDS it, because such a model is only readable beside\n * the map, and `loam subsystem sync` excludes only the directory of each model\n * that STANDS ALONE — one `services/<tree>/**` entry per standalone model,\n * never the whole subtree (`c4/service-model/renderer.ts` owns that list, and\n * is the one place loam reads that file). loam's own answer here is all of them\n * EXCEPT the documents loam GENERATES there (`architecture/subsystems.likec4`\n * and `architecture/services.likec4`), and that exception is not an optimisation:\n *\n * - docs/DESIGN.md rule 26 excludes the generated file from every read loam\n * performs. Its staleness is a byte compare (`subsystem.views-stale`) and\n * nothing anywhere reads its contents.\n * - Including it would invent a failure mode. Its `include` lines name\n * landscape element ids, so a file that has drifted — a service removed\n * since the last `loam subsystem sync` — would be a parse error, and a parse\n * error in one project document blanks the model for all of them. A fleet\n * whose only problem is a stale generated file would lose its whole map\n * instead of getting the one finding that names the repair.\n *\n * The skip is passed in by the caller rather than spelled here, so the filenames\n * stay written in `repo/paths.ts` alone, and every reader takes the same SET out\n * (`./architecture.ts` holds the one join).\n */\nimport { readdir } from \"node:fs/promises\";\nimport { join, resolve } from \"node:path\";\n\n/**\n * Every `.likec4` file under `dir`, recursively, minus `skip`, in a stable\n * order.\n *\n * Sorted because it decides the staging order and therefore nothing at all —\n * LikeC4 merges a project regardless — but a stable list makes a failure\n * reproducible and a diff of one readable. A directory that does not exist is\n * an empty list, never a throw: a fleet with no `architecture/` is a fleet with\n * nothing to load, which is already `landscape.missing`'s question, not this\n * function's.\n */\nexport async function architectureDocuments(dir: string, skip: string[] = []): Promise<string[]> {\n const excluded = new Set(skip.map((path) => resolve(path)));\n const found: string[] = [];\n const walk = async (at: string): Promise<void> => {\n let entries;\n try {\n entries = await readdir(at, { withFileTypes: true });\n } catch {\n return;\n }\n for (const entry of entries) {\n const path = join(at, entry.name);\n if (entry.isDirectory()) await walk(path);\n else if (entry.name.endsWith(\".likec4\") && !excluded.has(resolve(path))) found.push(path);\n }\n };\n await walk(resolve(dir));\n return found.sort();\n}\n"]}
@@ -88,10 +88,14 @@ export async function stageProject(root, project, base, targets) {
88
88
  * landscape — but loam's standing rule is that errors mean no model, and
89
89
  * here the model is the project's. `clean` says which case the caller is in.
90
90
  *
91
- * The generated `architecture/subsystems.likec4` must NOT be in `targets`:
92
- * docs/DESIGN.md rule 26 keeps it a byte compare, and a stale one whose
93
- * `include` names a removed element would otherwise be a parse error that
94
- * blanks the whole fleet map.
91
+ * The documents loam GENERATES under `architecture/` must NOT be in `targets`
92
+ * — there are two of them now (`subsystems.likec4` and `services.likec4`, both
93
+ * written by `loam subsystem sync`), and `repo/paths.ts`'s
94
+ * `GENERATED_ARCHITECTURE_FILES` is the one table that names them. docs/DESIGN.md
95
+ * rule 26 keeps each a byte compare, and a stale one whose `include` names a
96
+ * removed element would otherwise be a parse error that blanks the whole fleet
97
+ * map. `./architecture.ts` holds the join from that table to a docsDir, so no
98
+ * caller has to remember the list.
95
99
  */
96
100
  export async function loadProject(base, paths) {
97
101
  const targets = [...new Set(paths.map((path) => resolve(path)))];
@@ -1 +1 @@
1
- {"version":3,"file":"load.js","sourceRoot":"","sources":["../../../../src/core/c4/project/load.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * One directory of `.likec4` documents, loaded as ONE LikeC4 project — the way\n * the renderer already reads `architecture/`.\n *\n * A package of its own rather than more of `../workspace.ts`, and the seam is\n * the isolation rule, not the line count. That module stages every document as\n * its own single-file project ON PURPOSE, so an author-written `import` can\n * never resolve against a sibling; this one does the opposite deliberately, for\n * the set of documents that are meant to see each other. Two loaders with\n * opposite contracts should not share a file: the next reader has to be able to\n * tell at a glance which one they are looking at.\n */\nimport { randomBytes } from \"node:crypto\";\nimport { copyFile, mkdir, mkdtemp, rm, writeFile } from \"node:fs/promises\";\nimport { tmpdir } from \"node:os\";\nimport { dirname, join, relative, resolve } from \"node:path\";\nimport { LikeC4 } from \"likec4\";\nimport { inOrder } from \"../../kernel/concurrency.js\";\nimport { flattenModel, type Elem, type LikeC4Error, type LoadedDoc, type ReadableModel, type Rel } from \"../likec4.js\";\nimport { readDynamicViews, type ParsedView } from \"../parsed/dynamic-views.js\";\nimport { readSpecification, type DocSpecification } from \"../parsed/specification.js\";\nimport { readGlobalStyleIds, readViewIds, type ViewIdClaim } from \"../parsed/view-ids.js\";\nimport { readDeployment, NO_DEPLOYMENT, type DeploymentModel } from \"../parsed/deployment.js\";\n\n/**\n * A diagnostic exactly as a WORKSPACE hands it back, where `sourceFsPath` is\n * always present — unlike loam's own `LikeC4Error`, whose field is optional\n * because the several literals standing in for an unread file have no path to\n * put there. Named so the attribution below can take diagnostics rather than\n * the workspace object, without a non-null assertion per error.\n */\ntype WorkspaceError = LikeC4Error & { sourceFsPath: string };\n\n/** A directory of `.likec4` documents loaded as one LikeC4 project. */\nexport interface ProjectDoc {\n /** Errors by the REAL absolute path of the document that raised them. */\n errors: Map<string, LikeC4Error[]>;\n /** True when no document in the project raised one — loam's \"errors mean no model\" gate. */\n clean: boolean;\n elements: Elem[];\n relationships: Rel[];\n specification?: DocSpecification;\n /** Every dynamic view the project declares, each carrying the file it is written in. */\n views: ParsedView[];\n /** Every authored view id the project claims, each with its file. */\n viewIds: ViewIdClaim[];\n /**\n * Every global style id declared anywhere in the project, sorted — the\n * renderer's table, so a palette kept in `usecases/style.likec4` counts\n * exactly as one in the landscape does. Empty when none, and when it did\n * not parse.\n */\n globalStyles: string[];\n /** The project's deployment model — empty when it declares none, and when it did not parse. */\n deployment: DeploymentModel;\n}\n\n/**\n * Stage a set of documents as ONE project named `project`, mirroring their\n * paths relative to `base`.\n *\n * The relative structure is preserved rather than flattened to basenames, and\n * that is load-bearing twice over: two use cases named `checkout.likec4` in\n * different subdirectories would overwrite each other on copy, and LikeC4\n * reports a view's `sourcePath` relative to the project root — so flattening\n * would make every finding name a file the author cannot find.\n *\n * The NAME arrives from the caller rather than being minted in here, and that\n * is what lets `./batch.ts` reuse this staging verbatim: many projects in one\n * workspace need the folder name twice over — `parsedModel(<name>)` asks for\n * that project's model, and error attribution keys on the folder SEGMENT.\n * `loadProject` below has one project and passes a name it never looks at\n * again; both spellings carry a crypto-random per-invocation token for\n * `../workspace.ts`'s reason (an author-written `import` must not be able to\n * name a sibling project).\n */\nexport async function stageProject(\n root: string,\n project: string,\n base: string,\n targets: string[],\n): Promise<Map<string, string>> {\n const folder = join(root, project);\n await mkdir(folder, { recursive: true });\n await writeFile(join(folder, \"likec4.config.json\"), JSON.stringify({ name: project }), \"utf8\");\n // rel -> real path, so an error carrying the staged path can be handed back\n // spelled the way the author wrote it.\n const byRel = new Map<string, string>();\n await inOrder(targets, async (path) => {\n const rel = relative(base, path).split(/[\\\\/]/).join(\"/\");\n const dest = join(folder, ...rel.split(\"/\"));\n try {\n await mkdir(dirname(dest), { recursive: true });\n await copyFile(path, dest);\n byRel.set(rel, path);\n } catch {\n // Dropped, exactly as `stage` drops one: the caller's ordinary per-path\n // load owns the error, and a project missing one document still answers\n // about the rest.\n }\n return null;\n });\n return byRel;\n}\n\n/**\n * Load a directory of `.likec4` documents as ONE LikeC4 project — the way the\n * renderer already reads `architecture/`, and the only way a document that\n * declares views over ANOTHER file's elements can be read at all.\n *\n * Measured at the 1.59.2 pin: a views-only `usecases/checkout.likec4` parsed\n * alone is five errors (unresolved tag, unresolved elements); the same file\n * beside its landscape in one project is zero, and each view comes back with\n * its own `sourcePath`. That is the whole reason this function exists — a use\n * case lives in a file of its own, and `loadBatch`'s one-project-per-document\n * isolation makes such a file unreadable by construction.\n *\n * Two rules the caller must know, because both invert what holds elsewhere:\n *\n * - **Exactly one document in the set declares the `specification` block.**\n * Every `.likec4` file loam parses alone must declare its own; inside one\n * project a second declaration is a duplicate error blamed on BOTH files\n * (measured).\n * - **Errors are per document, but the MODEL is all-or-nothing.** A typo in\n * one file is attributed to that file — so the finding names it, which is\n * better than today, where a use case in the landscape would blame the\n * landscape — but loam's standing rule is that errors mean no model, and\n * here the model is the project's. `clean` says which case the caller is in.\n *\n * The generated `architecture/subsystems.likec4` must NOT be in `targets`:\n * docs/DESIGN.md rule 26 keeps it a byte compare, and a stale one whose\n * `include` names a removed element would otherwise be a parse error that\n * blanks the whole fleet map.\n */\nexport async function loadProject(base: string, paths: string[]): Promise<ProjectDoc> {\n const targets = [...new Set(paths.map((path) => resolve(path)))];\n if (targets.length === 0) return emptyProject(new Map(), true);\n const root = await mkdtemp(join(tmpdir(), \"loam-c4-\"));\n try {\n const byRel = await stageProject(root, `arch_${randomBytes(8).toString(\"hex\")}`, resolve(base), targets);\n if (byRel.size === 0) return emptyProject(new Map(), true);\n const likec4 = await LikeC4.fromWorkspace(root, { logger: false });\n try {\n const errors = groupErrorsByRel(likec4.getErrors(), byRel);\n if (errors.size > 0) return emptyProject(errors, false);\n return cleanProjectDoc((await likec4.parsedModel()) as ReadableModel);\n } finally {\n await likec4.dispose();\n }\n } finally {\n await rm(root, { recursive: true, force: true });\n }\n}\n\n/**\n * A `ProjectDoc` carrying NO model — the answer for a project that did not\n * parse (`clean: false`) and for one there was nothing to load (`clean: true`,\n * empty errors).\n *\n * Exported for `./batch.ts`, which answers about many projects at once and\n * must answer each of them in exactly this shape: a second literal there would\n * be a second chance to spell `deployment` as `undefined` and turn \"this\n * project declares no topology\" into a crash in every reader.\n */\nexport function emptyProject(errors: Map<string, LikeC4Error[]>, clean: boolean): ProjectDoc {\n return {\n errors,\n clean,\n elements: [],\n relationships: [],\n views: [],\n viewIds: [],\n globalStyles: [],\n deployment: NO_DEPLOYMENT,\n };\n}\n\n/**\n * Everything loam reads out of a project that parsed, from LikeC4's parsed\n * stage — parsed, never computed, for the reason `../likec4.ts` records.\n *\n * Exported beside `emptyProject` and for the same reason: `./batch.ts` builds\n * one of these per clean project, and an adapter wired into one loader and not\n * the other reads as an absence rather than an error — the failure shape\n * test/likec4-batch-parity.test.ts was written to catch.\n */\nexport function cleanProjectDoc(model: ReadableModel): ProjectDoc {\n return {\n errors: new Map(),\n clean: true,\n specification: readSpecification(model.specification),\n views: readDynamicViews(model),\n viewIds: readViewIds(model),\n globalStyles: readGlobalStyleIds(model),\n deployment: readDeployment(model),\n ...flattenModel(model),\n };\n}\n\n/**\n * Diagnostics split per DOCUMENT inside one project, each error's\n * `sourceFsPath` rewritten to the document's real absolute path.\n *\n * `../workspace.ts`'s `groupErrors` keys on the project folder segment, which\n * cannot work here — every document shares one project. This keys on the staged\n * relative path instead, longest first so `usecases/checkout.likec4` is matched\n * before a hypothetical `checkout.likec4` at the root. An error nobody can\n * attribute throws for the same reason it does there: a dropped error grades\n * its document clean, which is failing open in the fleet gate.\n *\n * It takes the diagnostics rather than the workspace so `./batch.ts` can hand\n * it ONE project's share of a multi-project `getErrors()` — the two questions\n * compose (which project, then which document) exactly because neither is\n * baked in here.\n */\nexport function groupErrorsByRel(\n errors: readonly WorkspaceError[],\n byRel: Map<string, string>,\n): Map<string, LikeC4Error[]> {\n const rels = [...byRel.keys()].sort((a, b) => b.length - a.length);\n const grouped = new Map<string, LikeC4Error[]>();\n for (const err of errors) {\n const spelled = err.sourceFsPath.split(/[\\\\/]/).join(\"/\");\n const rel = rels.find((candidate) => spelled.endsWith(`/${candidate}`) || spelled === candidate);\n if (rel === undefined) {\n throw new Error(`unattributable LikeC4 error in project workspace: ${err.message}`);\n }\n const real = byRel.get(rel)!;\n const list = grouped.get(real) ?? [];\n list.push({ ...err, sourceFsPath: real });\n grouped.set(real, list);\n }\n return grouped;\n}\n\n/**\n * A `ProjectDoc` in the shape every existing landscape reader already takes.\n *\n * The per-file error map flattens into one list, which loses nothing: each\n * error still carries the real path of the document that raised it, so a\n * finding can name the file even though the list is flat. Sorted by that path\n * so a project with two broken documents reports them in a stable order —\n * `getErrors()` returns whatever order the workspace scan produced.\n *\n * The model travels only when the project is clean, which is loam's standing\n * rule (`errors mean no model`) applied at project altitude: a use-case file\n * with an unresolved element leaves the landscape unusable, and pretending\n * otherwise would grade a fleet against half a map.\n */\nexport function asLoadedDoc(doc: ProjectDoc): LoadedDoc {\n const errors = [...doc.errors]\n .sort(([a], [b]) => (a < b ? -1 : a > b ? 1 : 0))\n .flatMap(([, list]) => list);\n if (!doc.clean) return { errors, elements: [], relationships: [] };\n return {\n errors,\n ...(doc.specification === undefined ? {} : { specification: doc.specification }),\n views: doc.views,\n viewIds: doc.viewIds,\n globalStyles: doc.globalStyles,\n deployment: doc.deployment,\n elements: doc.elements,\n relationships: doc.relationships,\n };\n}\n"]}
1
+ {"version":3,"file":"load.js","sourceRoot":"","sources":["../../../../src/core/c4/project/load.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * One directory of `.likec4` documents, loaded as ONE LikeC4 project — the way\n * the renderer already reads `architecture/`.\n *\n * A package of its own rather than more of `../workspace.ts`, and the seam is\n * the isolation rule, not the line count. That module stages every document as\n * its own single-file project ON PURPOSE, so an author-written `import` can\n * never resolve against a sibling; this one does the opposite deliberately, for\n * the set of documents that are meant to see each other. Two loaders with\n * opposite contracts should not share a file: the next reader has to be able to\n * tell at a glance which one they are looking at.\n */\nimport { randomBytes } from \"node:crypto\";\nimport { copyFile, mkdir, mkdtemp, rm, writeFile } from \"node:fs/promises\";\nimport { tmpdir } from \"node:os\";\nimport { dirname, join, relative, resolve } from \"node:path\";\nimport { LikeC4 } from \"likec4\";\nimport { inOrder } from \"../../kernel/concurrency.js\";\nimport { flattenModel, type Elem, type LikeC4Error, type LoadedDoc, type ReadableModel, type Rel } from \"../likec4.js\";\nimport { readDynamicViews, type ParsedView } from \"../parsed/dynamic-views.js\";\nimport { readSpecification, type DocSpecification } from \"../parsed/specification.js\";\nimport { readGlobalStyleIds, readViewIds, type ViewIdClaim } from \"../parsed/view-ids.js\";\nimport { readDeployment, NO_DEPLOYMENT, type DeploymentModel } from \"../parsed/deployment.js\";\n\n/**\n * A diagnostic exactly as a WORKSPACE hands it back, where `sourceFsPath` is\n * always present — unlike loam's own `LikeC4Error`, whose field is optional\n * because the several literals standing in for an unread file have no path to\n * put there. Named so the attribution below can take diagnostics rather than\n * the workspace object, without a non-null assertion per error.\n */\ntype WorkspaceError = LikeC4Error & { sourceFsPath: string };\n\n/** A directory of `.likec4` documents loaded as one LikeC4 project. */\nexport interface ProjectDoc {\n /** Errors by the REAL absolute path of the document that raised them. */\n errors: Map<string, LikeC4Error[]>;\n /** True when no document in the project raised one — loam's \"errors mean no model\" gate. */\n clean: boolean;\n elements: Elem[];\n relationships: Rel[];\n specification?: DocSpecification;\n /** Every dynamic view the project declares, each carrying the file it is written in. */\n views: ParsedView[];\n /** Every authored view id the project claims, each with its file. */\n viewIds: ViewIdClaim[];\n /**\n * Every global style id declared anywhere in the project, sorted — the\n * renderer's table, so a palette kept in `usecases/style.likec4` counts\n * exactly as one in the landscape does. Empty when none, and when it did\n * not parse.\n */\n globalStyles: string[];\n /** The project's deployment model — empty when it declares none, and when it did not parse. */\n deployment: DeploymentModel;\n}\n\n/**\n * Stage a set of documents as ONE project named `project`, mirroring their\n * paths relative to `base`.\n *\n * The relative structure is preserved rather than flattened to basenames, and\n * that is load-bearing twice over: two use cases named `checkout.likec4` in\n * different subdirectories would overwrite each other on copy, and LikeC4\n * reports a view's `sourcePath` relative to the project root — so flattening\n * would make every finding name a file the author cannot find.\n *\n * The NAME arrives from the caller rather than being minted in here, and that\n * is what lets `./batch.ts` reuse this staging verbatim: many projects in one\n * workspace need the folder name twice over — `parsedModel(<name>)` asks for\n * that project's model, and error attribution keys on the folder SEGMENT.\n * `loadProject` below has one project and passes a name it never looks at\n * again; both spellings carry a crypto-random per-invocation token for\n * `../workspace.ts`'s reason (an author-written `import` must not be able to\n * name a sibling project).\n */\nexport async function stageProject(\n root: string,\n project: string,\n base: string,\n targets: string[],\n): Promise<Map<string, string>> {\n const folder = join(root, project);\n await mkdir(folder, { recursive: true });\n await writeFile(join(folder, \"likec4.config.json\"), JSON.stringify({ name: project }), \"utf8\");\n // rel -> real path, so an error carrying the staged path can be handed back\n // spelled the way the author wrote it.\n const byRel = new Map<string, string>();\n await inOrder(targets, async (path) => {\n const rel = relative(base, path).split(/[\\\\/]/).join(\"/\");\n const dest = join(folder, ...rel.split(\"/\"));\n try {\n await mkdir(dirname(dest), { recursive: true });\n await copyFile(path, dest);\n byRel.set(rel, path);\n } catch {\n // Dropped, exactly as `stage` drops one: the caller's ordinary per-path\n // load owns the error, and a project missing one document still answers\n // about the rest.\n }\n return null;\n });\n return byRel;\n}\n\n/**\n * Load a directory of `.likec4` documents as ONE LikeC4 project — the way the\n * renderer already reads `architecture/`, and the only way a document that\n * declares views over ANOTHER file's elements can be read at all.\n *\n * Measured at the 1.59.2 pin: a views-only `usecases/checkout.likec4` parsed\n * alone is five errors (unresolved tag, unresolved elements); the same file\n * beside its landscape in one project is zero, and each view comes back with\n * its own `sourcePath`. That is the whole reason this function exists — a use\n * case lives in a file of its own, and `loadBatch`'s one-project-per-document\n * isolation makes such a file unreadable by construction.\n *\n * Two rules the caller must know, because both invert what holds elsewhere:\n *\n * - **Exactly one document in the set declares the `specification` block.**\n * Every `.likec4` file loam parses alone must declare its own; inside one\n * project a second declaration is a duplicate error blamed on BOTH files\n * (measured).\n * - **Errors are per document, but the MODEL is all-or-nothing.** A typo in\n * one file is attributed to that file — so the finding names it, which is\n * better than today, where a use case in the landscape would blame the\n * landscape — but loam's standing rule is that errors mean no model, and\n * here the model is the project's. `clean` says which case the caller is in.\n *\n * The documents loam GENERATES under `architecture/` must NOT be in `targets`\n * — there are two of them now (`subsystems.likec4` and `services.likec4`, both\n * written by `loam subsystem sync`), and `repo/paths.ts`'s\n * `GENERATED_ARCHITECTURE_FILES` is the one table that names them. docs/DESIGN.md\n * rule 26 keeps each a byte compare, and a stale one whose `include` names a\n * removed element would otherwise be a parse error that blanks the whole fleet\n * map. `./architecture.ts` holds the join from that table to a docsDir, so no\n * caller has to remember the list.\n */\nexport async function loadProject(base: string, paths: string[]): Promise<ProjectDoc> {\n const targets = [...new Set(paths.map((path) => resolve(path)))];\n if (targets.length === 0) return emptyProject(new Map(), true);\n const root = await mkdtemp(join(tmpdir(), \"loam-c4-\"));\n try {\n const byRel = await stageProject(root, `arch_${randomBytes(8).toString(\"hex\")}`, resolve(base), targets);\n if (byRel.size === 0) return emptyProject(new Map(), true);\n const likec4 = await LikeC4.fromWorkspace(root, { logger: false });\n try {\n const errors = groupErrorsByRel(likec4.getErrors(), byRel);\n if (errors.size > 0) return emptyProject(errors, false);\n return cleanProjectDoc((await likec4.parsedModel()) as ReadableModel);\n } finally {\n await likec4.dispose();\n }\n } finally {\n await rm(root, { recursive: true, force: true });\n }\n}\n\n/**\n * A `ProjectDoc` carrying NO model — the answer for a project that did not\n * parse (`clean: false`) and for one there was nothing to load (`clean: true`,\n * empty errors).\n *\n * Exported for `./batch.ts`, which answers about many projects at once and\n * must answer each of them in exactly this shape: a second literal there would\n * be a second chance to spell `deployment` as `undefined` and turn \"this\n * project declares no topology\" into a crash in every reader.\n */\nexport function emptyProject(errors: Map<string, LikeC4Error[]>, clean: boolean): ProjectDoc {\n return {\n errors,\n clean,\n elements: [],\n relationships: [],\n views: [],\n viewIds: [],\n globalStyles: [],\n deployment: NO_DEPLOYMENT,\n };\n}\n\n/**\n * Everything loam reads out of a project that parsed, from LikeC4's parsed\n * stage — parsed, never computed, for the reason `../likec4.ts` records.\n *\n * Exported beside `emptyProject` and for the same reason: `./batch.ts` builds\n * one of these per clean project, and an adapter wired into one loader and not\n * the other reads as an absence rather than an error — the failure shape\n * test/likec4-batch-parity.test.ts was written to catch.\n */\nexport function cleanProjectDoc(model: ReadableModel): ProjectDoc {\n return {\n errors: new Map(),\n clean: true,\n specification: readSpecification(model.specification),\n views: readDynamicViews(model),\n viewIds: readViewIds(model),\n globalStyles: readGlobalStyleIds(model),\n deployment: readDeployment(model),\n ...flattenModel(model),\n };\n}\n\n/**\n * Diagnostics split per DOCUMENT inside one project, each error's\n * `sourceFsPath` rewritten to the document's real absolute path.\n *\n * `../workspace.ts`'s `groupErrors` keys on the project folder segment, which\n * cannot work here — every document shares one project. This keys on the staged\n * relative path instead, longest first so `usecases/checkout.likec4` is matched\n * before a hypothetical `checkout.likec4` at the root. An error nobody can\n * attribute throws for the same reason it does there: a dropped error grades\n * its document clean, which is failing open in the fleet gate.\n *\n * It takes the diagnostics rather than the workspace so `./batch.ts` can hand\n * it ONE project's share of a multi-project `getErrors()` — the two questions\n * compose (which project, then which document) exactly because neither is\n * baked in here.\n */\nexport function groupErrorsByRel(\n errors: readonly WorkspaceError[],\n byRel: Map<string, string>,\n): Map<string, LikeC4Error[]> {\n const rels = [...byRel.keys()].sort((a, b) => b.length - a.length);\n const grouped = new Map<string, LikeC4Error[]>();\n for (const err of errors) {\n const spelled = err.sourceFsPath.split(/[\\\\/]/).join(\"/\");\n const rel = rels.find((candidate) => spelled.endsWith(`/${candidate}`) || spelled === candidate);\n if (rel === undefined) {\n throw new Error(`unattributable LikeC4 error in project workspace: ${err.message}`);\n }\n const real = byRel.get(rel)!;\n const list = grouped.get(real) ?? [];\n list.push({ ...err, sourceFsPath: real });\n grouped.set(real, list);\n }\n return grouped;\n}\n\n/**\n * A `ProjectDoc` in the shape every existing landscape reader already takes.\n *\n * The per-file error map flattens into one list, which loses nothing: each\n * error still carries the real path of the document that raised it, so a\n * finding can name the file even though the list is flat. Sorted by that path\n * so a project with two broken documents reports them in a stable order —\n * `getErrors()` returns whatever order the workspace scan produced.\n *\n * The model travels only when the project is clean, which is loam's standing\n * rule (`errors mean no model`) applied at project altitude: a use-case file\n * with an unresolved element leaves the landscape unusable, and pretending\n * otherwise would grade a fleet against half a map.\n */\nexport function asLoadedDoc(doc: ProjectDoc): LoadedDoc {\n const errors = [...doc.errors]\n .sort(([a], [b]) => (a < b ? -1 : a > b ? 1 : 0))\n .flatMap(([, list]) => list);\n if (!doc.clean) return { errors, elements: [], relationships: [] };\n return {\n errors,\n ...(doc.specification === undefined ? {} : { specification: doc.specification }),\n views: doc.views,\n viewIds: doc.viewIds,\n globalStyles: doc.globalStyles,\n deployment: doc.deployment,\n elements: doc.elements,\n relationships: doc.relationships,\n };\n}\n"]}
@@ -56,8 +56,8 @@ import { errorText, loadFile } from "../likec4.js";
56
56
  import { planLandscapeMerge } from "../splice/landscape-merge.js";
57
57
  import { inOrder } from "../../kernel/concurrency.js";
58
58
  import { decodeDocument } from "../../kernel/document-bytes.js";
59
- import { featurePaths, landscapePath, subsystemViewsPath } from "../../repo/paths.js";
60
- import { architectureDir } from "./architecture.js";
59
+ import { featurePaths, landscapePath } from "../../repo/paths.js";
60
+ import { architectureDir, generatedArchitectureDocuments } from "./architecture.js";
61
61
  import { architectureDocuments } from "./documents.js";
62
62
  import { asLoadedDoc, loadProject } from "./load.js";
63
63
  /**
@@ -74,7 +74,11 @@ export async function stageMergedProject(req) {
74
74
  if (merged.kind === "failed")
75
75
  return { kind: "unreadable", errors: [merged.reason] };
76
76
  const arch = architectureDir(req.docsDir);
77
- const living = await architectureDocuments(arch, [subsystemViewsPath(req.docsDir)]);
77
+ // BOTH generated documents come out, through the one list every project read
78
+ // shares: a views-only file does not parse standalone, so staging one that
79
+ // `sync` has not caught up with would blank the merge preview this whole
80
+ // module exists to parse (`./architecture.ts` states the rule).
81
+ const living = await architectureDocuments(arch, generatedArchitectureDocuments(req.docsDir));
78
82
  const root = await mkdtemp(join(tmpdir(), "loam-staged-"));
79
83
  try {
80
84
  // Staged rel -> the path the AUTHOR wrote, so every message that leaves
@@ -1 +1 @@
1
- {"version":3,"file":"staged.js","sourceRoot":"","sources":["../../../../src/core/c4/project/staged.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * The `architecture/` project AS THIS FEATURE'S MERGE WOULD LEAVE IT, staged\n * and parsed, with a feature's own documents standing beside it.\n *\n * WHY THE POST-MERGE MAP AND NOT THE LIVING ONE. A document a feature brings\n * names elements, and a feature that adds a service adds the element its own\n * document names. Graded against the LIVING landscape that name is unresolved —\n * LikeC4 refuses the document, the project blanks, and loam reports a hole\n * against a file whose only fault is arriving with the change that makes it\n * true. Graded against the merge it resolves exactly when the merge lands the\n * element it names, which is `ARCH-LOAM-FEATURE-CORPUS`: inside a feature\n * window, the corpus is feature ∪ living.\n *\n * IT IS ALSO THE CHECK. The merge preview is the only reading under which \"a\n * document naming an element the merge does not land\" is answerable at all, and\n * that refusal is why it runs before `loam archive` writes rather than after:\n * without it the document is copied into `architecture/` and the NEXT reader's\n * `loam validate --all` carries the failure, against a file they did not write.\n *\n * ## Why it lives here rather than in one axis\n *\n * It was `core/usecases/delta/overlay.ts`'s private machinery while the\n * use-case slot was the only feature-local `.likec4` document. The deployment\n * slot is the second, and a second copy of this staging is the copy that\n * drifts — the two would disagree first about the merge preview, which is the\n * one thing both refusals rest on. So the staging moved down to the package\n * that owns the project loader and the merge, and each axis keeps only its own\n * INTERPRETATION of the parse: which views are the feature's, or which\n * documents failed.\n *\n * ## Why it stages its own tree\n *\n * `loadProject` copies documents from disk, keyed on their path relative to a\n * base — so it can be handed neither a landscape that exists only in memory nor\n * a document living outside `architecture/`, and both are exactly what this\n * needs. Teaching it to take content would put a second, rarely-exercised\n * staging rule inside a loader whose docstring already carries two rules that\n * invert what holds elsewhere. Staging here costs one copy of a handful of\n * small files and keeps that loader's contract at one.\n *\n * THE PRICE OF THAT CHOICE IS PATHS, and it is paid here rather than left to\n * the caller: every path LikeC4 reports — a parse error's `sourceFsPath`, a\n * view's `sourcePath` — names a temp tree, and a finding naming a temp\n * directory is a finding nobody can act on. `real()` maps them back to the file\n * the author wrote, and the temp tree is gone before this returns.\n *\n * FAIL CLOSED, and not silently. Every failure is the `unreadable` arm carrying\n * LikeC4's own messages: a document this cannot read is never one that grades\n * clean.\n */\nimport { existsSync } from \"node:fs\";\nimport { copyFile, mkdir, mkdtemp, readFile, rm, writeFile } from \"node:fs/promises\";\nimport { tmpdir } from \"node:os\";\nimport { dirname, join, relative, resolve } from \"node:path\";\nimport { errorText, loadFile, type LoadedDoc } from \"../likec4.js\";\nimport type { ExtendingModel } from \"../splice/contract.js\";\nimport { planLandscapeMerge } from \"../splice/landscape-merge.js\";\nimport { inOrder } from \"../../kernel/concurrency.js\";\nimport { decodeDocument } from \"../../kernel/document-bytes.js\";\nimport type { DocsDir, FeatureDir } from \"../../kernel/ids/dirs.js\";\nimport { featurePaths, landscapePath, subsystemViewsPath } from \"../../repo/paths.js\";\nimport { architectureDir } from \"./architecture.js\";\nimport { architectureDocuments } from \"./documents.js\";\nimport { asLoadedDoc, loadProject } from \"./load.js\";\n\n/** One document a feature brings, and where it would live under `architecture/`. */\nexport interface StagedDocument {\n /** Its path relative to `architecture/`, `/`-separated — where the merge would write it. */\n rel: string;\n /** The absolute path of the authored file. */\n path: string;\n}\n\nexport interface StagedProjectRequest {\n docsDir: DocsDir;\n featureDir: FeatureDir;\n /** The feature's id — the tag that selects the delta's own additions. */\n featureId: string;\n /** The feature's own documents, staged LAST so they win over a living twin. */\n documents: readonly StagedDocument[];\n /**\n * The caller's memoised LikeC4 read, when it has one. Only `delta.likec4`\n * goes through it, and only to compute the merge preview — but a caller\n * inside `featureCoherence` has already loaded that exact document, and\n * loading one spins a fresh Langium workspace, which\n * `core/coherence/coherence.ts` calls the dominant per-feature cost.\n */\n load?: (path: string) => Promise<LoadedDoc>;\n /**\n * The fleet's extending models — the SAME list `loam archive` merges with.\n * The merge routes a nested addition into the model that owns that service's\n * interior, so a preview computed without this list holds interior the\n * archive will put somewhere else, and every gate resting on the preview\n * grades a map that will never exist (verification 2026-09-04, review C).\n *\n * Optional because it must be INJECTED rather than read here: the shape scan\n * that decides which models extend the map lives in\n * `../service-model/shape.ts`, and that package imports this one — reading it\n * from here would be a package cycle. The reader is\n * `../service-model/fleet/extending.ts`, one level below that scan, and both\n * shipped callers fill this in from it: `core/delta/delta.ts` for the\n * use-case overlay, `core/coherence/coherence.ts` for the deployment one.\n * Omitted, the preview is the pre-routing one — the behaviour every caller\n * had before this field existed, and what a fleet whose models all stand\n * alone gets either way.\n */\n models?: readonly ExtendingModel[];\n}\n\n/** The staged parse, or the honest refusal to say. */\nexport type StagedProject =\n | {\n kind: \"read\";\n /** The whole staged project, parsed. */\n doc: LoadedDoc;\n /** A path LikeC4 reported, spelled as the author wrote it. */\n real: (path: string) => string;\n /** The staged rels that are THIS feature's documents — everything else is living. */\n mine: ReadonlySet<string>;\n }\n | { kind: \"unreadable\"; errors: string[] };\n\n/**\n * Stage the living `architecture/` with the merge preview in place of the\n * landscape, add the feature's own documents, and parse the result.\n *\n * A feature bringing no documents is not this function's business: both callers\n * return their own empty answer before reaching it, at the cost of one walk\n * over a directory that is not there. That walk is the axis's per-feature\n * opt-in and the whole price a fleet that has not adopted it pays.\n */\nexport async function stageMergedProject(req: StagedProjectRequest): Promise<StagedProject> {\n const merged = await mergedLandscape(req);\n if (merged.kind === \"failed\") return { kind: \"unreadable\", errors: [merged.reason] };\n\n const arch = architectureDir(req.docsDir);\n const living = await architectureDocuments(arch, [subsystemViewsPath(req.docsDir)]);\n const root = await mkdtemp(join(tmpdir(), \"loam-staged-\"));\n try {\n // Staged rel -> the path the AUTHOR wrote, so every message that leaves\n // here names a file they can open. Built as the tree is staged, because\n // that is the one moment both spellings are in hand.\n const authored = new Map<string, string>();\n const landscape = resolve(landscapePath(req.docsDir));\n await inOrder(living, async (path) => {\n // The living landscape is staged from the merge preview when there is\n // one; copying it as well would declare every element twice.\n if (merged.kind === \"merged\" && resolve(path) === landscape) return null;\n await stage(root, spell(relative(arch, path)), path, authored);\n return null;\n });\n if (merged.kind === \"merged\") {\n const rel = spell(relative(arch, landscape));\n await write(root, rel, merged.content);\n authored.set(rel, landscape);\n }\n // The feature's own documents LAST, so one whose living twin exists wins\n // the staging rather than colliding with it. That case is already an error\n // the gate refuses before any merge — `usecase.flow-exists`,\n // `deployment.doc-exists` — and grading the feature's version is the more\n // useful of the two answers to give an author who is about to be told to\n // delete the file.\n const mine = new Set<string>();\n for (const document of req.documents) {\n await stage(root, document.rel, document.path, authored);\n mine.add(document.rel);\n }\n const doc = asLoadedDoc(await loadProject(root, await architectureDocuments(root)));\n // TWO SPELLINGS REACH HERE and both must map, which is the one thing this\n // helper cannot be allowed to get half right. A parse error's\n // `sourceFsPath` is an ABSOLUTE path into the temp tree; a view's\n // `sourcePath` is already the project-RELATIVE rel LikeC4 reports. The\n // rel-first lookup answers the second, and the relative() computation the\n // first. Handling only one leaves the other silently returning the temp\n // spelling, which is a finding naming a directory that no longer exists.\n const real = (path: string): string =>\n authored.get(spell(path)) ?? authored.get(spell(relative(root, path))) ?? path;\n if (doc.errors.length > 0) {\n // An error naming no document is LikeC4 declining to attribute one. It\n // still travels: a hole loam cannot place is still a hole, and dropping\n // it here would grade the feature clean on the strength of a missing\n // field.\n //\n // DOCS-RELATIVE, `/`-separated, and carrying the line — the spelling the\n // SERVICE arm of `usecase.flow-invalid` uses on purpose. These strings are\n // the feature arm's whole payload (text, `--json` and the `loam archive`\n // block message), and `real()` alone answered with an absolute Windows path\n // and no line at all: `C:\\Users\\…\\docs\\features\\FEAT-1\\usecases\\x.likec4:\n // <message>` in a finding a person is asked to act on (verification\n // 2026-09-04, W7). A path outside the docs root — which nothing here\n // produces, but `relative` would spell `..\\..` — keeps its absolute form.\n return {\n kind: \"unreadable\",\n errors: doc.errors.map((e) =>\n e.sourceFsPath === undefined ? errorText(e) : `${spellReal(req.docsDir, real(e.sourceFsPath))}: ${errorText(e)}`,\n ),\n };\n }\n return { kind: \"read\", doc, real, mine };\n } catch (err) {\n // The `unreadable` arm is a PROMISE, not a description of the happy path.\n // Two things under here throw rather than return: `loadProject` refuses an\n // error it cannot attribute to a document, and every `mkdir`/`copyFile` in\n // the staging above can fail on a full disk or a denied temp directory.\n // Callers run inside `loam validate --feature` and the archive gate — and\n // the one behaviour a check added to those may never have is failing the\n // run it was added to. So a throw becomes the same could-not-look answer a\n // parse error already produces, which each caller turns into its own\n // refusal that writes nothing.\n return { kind: \"unreadable\", errors: [err instanceof Error ? err.message : String(err)] };\n } finally {\n await rm(root, { recursive: true, force: true });\n }\n}\n\n/** What the landscape looks like after this feature's C4 delta is spliced in. */\ntype MergedLandscape =\n /** The delta adds nothing, or there is none — the living landscape stands. */\n | { kind: \"living\" }\n /** The merge preview, as authored bytes. */\n | { kind: \"merged\"; content: string }\n /** The merge could not be computed; `planLandscape` owns the refusal and says why. */\n | { kind: \"failed\"; reason: string };\n\n/**\n * The merge preview, computed with the SAME function `loam archive` commits\n * with.\n *\n * Reusing `planLandscapeMerge` rather than approximating it is what makes this\n * an answer about the archive rather than a second opinion beside it: an\n * element placed differently, a tag stripped differently, or a splice refused\n * there and allowed here would be a document graded against a map the merge\n * never writes.\n *\n * THE PARITY IS THE MERGE, NOT THE WHOLE GATE, and the two places the sides\n * differ are worth naming rather than leaving for a reader to discover.\n *\n * The first changes no verdict. `planLandscape` runs one check BEFORE it\n * computes the merge and this does not: a delta declaring the feature tag on a\n * specification KIND is refused there, because every element of that kind\n * inherits the tag and the merge would splice the whole document. Such a delta\n * reaches `newEls` here unfiltered, so the preview this grades against is one\n * the archive would never write — but that archive refuses either way, before\n * anything is written.\n *\n * The second changed every verdict about a fleet with extending models, and is\n * why `models` is on the request at all. `planLandscape` names the fleet's\n * extending models, so a nested addition is ROUTED into the model that owns\n * that service's interior and never reaches the map; a request that omits them\n * previews a map holding interior the archive will put somewhere else. A\n * feature-local `usecases/<x>.likec4` whose hop names `<service-fqn>.<container>`\n * then resolved in the preview, passed `usecase.flow-invalid` — the gate whose\n * own message is \"the archive would land an architecture/ nothing has checked\",\n * and which `--approve` cannot override — and was copied verbatim into\n * `architecture/`, where it resolves against nothing and takes the whole fleet\n * map down (verification 2026-09-04, review C; SCHEMA names that outcome as the\n * thing that must not happen). A caller that leaves `models` out is asking for\n * the pre-routing preview, and gets exactly the answers it got before.\n *\n * A throw is `failed` rather than propagated. Every `LandscapeSpliceError` this\n * can raise is one `planLandscape` raises again — with its own message, its own\n * refusal code, and before anything is written — so throwing here would only\n * decide which of two identical refusals an author reads first.\n */\nasync function mergedLandscape(req: StagedProjectRequest): Promise<MergedLandscape> {\n const deltaPath = featurePaths(req.featureDir).delta;\n const landscape = landscapePath(req.docsDir);\n if (!existsSync(deltaPath) || !existsSync(landscape)) return { kind: \"living\" };\n const delta = await (req.load ?? loadFile)(deltaPath);\n if (delta.errors.length > 0) {\n return {\n kind: \"failed\",\n reason: `delta.likec4 has ${delta.errors.length} parse error(s) — a feature's own documents cannot be graded against a map that does not parse`,\n };\n }\n try {\n const plan = await planLandscapeMerge({\n landscapeText: await text(landscape),\n deltaText: await text(deltaPath),\n deltaElements: delta.elements,\n newEls: delta.elements.filter((e) => e.tags.includes(req.featureId)),\n newRels: delta.relationships.filter((r) => r.tags.includes(req.featureId)),\n featureId: req.featureId,\n ...(req.models === undefined ? {} : { models: req.models }),\n });\n return plan.content === null ? { kind: \"living\" } : { kind: \"merged\", content: plan.content };\n } catch (err) {\n return { kind: \"failed\", reason: err instanceof Error ? err.message : String(err) };\n }\n}\n\n/** Read a document loam is about to hand LikeC4, refusing the encodings that decode as empty. */\nasync function text(path: string): Promise<string> {\n return decodeDocument(await readFile(path), path);\n}\n\n/** A path spelled the way a staged rel is spelled — `/`-separated, on every platform. */\nfunction spell(path: string): string {\n return path.split(/[\\\\/]/).join(\"/\");\n}\n\n/**\n * An authored path as a FINDING spells it: relative to the docs root,\n * `/`-separated. A path that does not sit under the docs root keeps its absolute\n * form — `relative` would answer with `..` segments, and a finding naming a\n * document outside the repository is more honest as the absolute path it is.\n */\nfunction spellReal(docsDir: DocsDir, path: string): string {\n const rel = relative(docsDir, path);\n return rel.length > 0 && !rel.startsWith(\"..\") ? spell(rel) : spell(path);\n}\n\n/** Copy one authored document into the staged tree, remembering where it came from. */\nasync function stage(root: string, rel: string, path: string, authored: Map<string, string>): Promise<void> {\n const dest = join(root, ...rel.split(\"/\"));\n await mkdir(dirname(dest), { recursive: true });\n await copyFile(path, dest);\n authored.set(rel, resolve(path));\n}\n\n/** Write one document into the staged tree from bytes loam holds rather than a file. */\nasync function write(root: string, rel: string, content: string): Promise<void> {\n const dest = join(root, ...rel.split(\"/\"));\n await mkdir(dirname(dest), { recursive: true });\n await writeFile(dest, content, \"utf8\");\n}\n"]}
1
+ {"version":3,"file":"staged.js","sourceRoot":"","sources":["../../../../src/core/c4/project/staged.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * The `architecture/` project AS THIS FEATURE'S MERGE WOULD LEAVE IT, staged\n * and parsed, with a feature's own documents standing beside it.\n *\n * WHY THE POST-MERGE MAP AND NOT THE LIVING ONE. A document a feature brings\n * names elements, and a feature that adds a service adds the element its own\n * document names. Graded against the LIVING landscape that name is unresolved —\n * LikeC4 refuses the document, the project blanks, and loam reports a hole\n * against a file whose only fault is arriving with the change that makes it\n * true. Graded against the merge it resolves exactly when the merge lands the\n * element it names, which is `ARCH-LOAM-FEATURE-CORPUS`: inside a feature\n * window, the corpus is feature ∪ living.\n *\n * IT IS ALSO THE CHECK. The merge preview is the only reading under which \"a\n * document naming an element the merge does not land\" is answerable at all, and\n * that refusal is why it runs before `loam archive` writes rather than after:\n * without it the document is copied into `architecture/` and the NEXT reader's\n * `loam validate --all` carries the failure, against a file they did not write.\n *\n * ## Why it lives here rather than in one axis\n *\n * It was `core/usecases/delta/overlay.ts`'s private machinery while the\n * use-case slot was the only feature-local `.likec4` document. The deployment\n * slot is the second, and a second copy of this staging is the copy that\n * drifts — the two would disagree first about the merge preview, which is the\n * one thing both refusals rest on. So the staging moved down to the package\n * that owns the project loader and the merge, and each axis keeps only its own\n * INTERPRETATION of the parse: which views are the feature's, or which\n * documents failed.\n *\n * ## Why it stages its own tree\n *\n * `loadProject` copies documents from disk, keyed on their path relative to a\n * base — so it can be handed neither a landscape that exists only in memory nor\n * a document living outside `architecture/`, and both are exactly what this\n * needs. Teaching it to take content would put a second, rarely-exercised\n * staging rule inside a loader whose docstring already carries two rules that\n * invert what holds elsewhere. Staging here costs one copy of a handful of\n * small files and keeps that loader's contract at one.\n *\n * THE PRICE OF THAT CHOICE IS PATHS, and it is paid here rather than left to\n * the caller: every path LikeC4 reports — a parse error's `sourceFsPath`, a\n * view's `sourcePath` — names a temp tree, and a finding naming a temp\n * directory is a finding nobody can act on. `real()` maps them back to the file\n * the author wrote, and the temp tree is gone before this returns.\n *\n * FAIL CLOSED, and not silently. Every failure is the `unreadable` arm carrying\n * LikeC4's own messages: a document this cannot read is never one that grades\n * clean.\n */\nimport { existsSync } from \"node:fs\";\nimport { copyFile, mkdir, mkdtemp, readFile, rm, writeFile } from \"node:fs/promises\";\nimport { tmpdir } from \"node:os\";\nimport { dirname, join, relative, resolve } from \"node:path\";\nimport { errorText, loadFile, type LoadedDoc } from \"../likec4.js\";\nimport type { ExtendingModel } from \"../splice/contract.js\";\nimport { planLandscapeMerge } from \"../splice/landscape-merge.js\";\nimport { inOrder } from \"../../kernel/concurrency.js\";\nimport { decodeDocument } from \"../../kernel/document-bytes.js\";\nimport type { DocsDir, FeatureDir } from \"../../kernel/ids/dirs.js\";\nimport { featurePaths, landscapePath } from \"../../repo/paths.js\";\nimport { architectureDir, generatedArchitectureDocuments } from \"./architecture.js\";\nimport { architectureDocuments } from \"./documents.js\";\nimport { asLoadedDoc, loadProject } from \"./load.js\";\n\n/** One document a feature brings, and where it would live under `architecture/`. */\nexport interface StagedDocument {\n /** Its path relative to `architecture/`, `/`-separated — where the merge would write it. */\n rel: string;\n /** The absolute path of the authored file. */\n path: string;\n}\n\nexport interface StagedProjectRequest {\n docsDir: DocsDir;\n featureDir: FeatureDir;\n /** The feature's id — the tag that selects the delta's own additions. */\n featureId: string;\n /** The feature's own documents, staged LAST so they win over a living twin. */\n documents: readonly StagedDocument[];\n /**\n * The caller's memoised LikeC4 read, when it has one. Only `delta.likec4`\n * goes through it, and only to compute the merge preview — but a caller\n * inside `featureCoherence` has already loaded that exact document, and\n * loading one spins a fresh Langium workspace, which\n * `core/coherence/coherence.ts` calls the dominant per-feature cost.\n */\n load?: (path: string) => Promise<LoadedDoc>;\n /**\n * The fleet's extending models — the SAME list `loam archive` merges with.\n * The merge routes a nested addition into the model that owns that service's\n * interior, so a preview computed without this list holds interior the\n * archive will put somewhere else, and every gate resting on the preview\n * grades a map that will never exist (verification 2026-09-04, review C).\n *\n * Optional because it must be INJECTED rather than read here: the shape scan\n * that decides which models extend the map lives in\n * `../service-model/shape.ts`, and that package imports this one — reading it\n * from here would be a package cycle. The reader is\n * `../service-model/fleet/extending.ts`, one level below that scan, and both\n * shipped callers fill this in from it: `core/delta/delta.ts` for the\n * use-case overlay, `core/coherence/coherence.ts` for the deployment one.\n * Omitted, the preview is the pre-routing one — the behaviour every caller\n * had before this field existed, and what a fleet whose models all stand\n * alone gets either way.\n */\n models?: readonly ExtendingModel[];\n}\n\n/** The staged parse, or the honest refusal to say. */\nexport type StagedProject =\n | {\n kind: \"read\";\n /** The whole staged project, parsed. */\n doc: LoadedDoc;\n /** A path LikeC4 reported, spelled as the author wrote it. */\n real: (path: string) => string;\n /** The staged rels that are THIS feature's documents — everything else is living. */\n mine: ReadonlySet<string>;\n }\n | { kind: \"unreadable\"; errors: string[] };\n\n/**\n * Stage the living `architecture/` with the merge preview in place of the\n * landscape, add the feature's own documents, and parse the result.\n *\n * A feature bringing no documents is not this function's business: both callers\n * return their own empty answer before reaching it, at the cost of one walk\n * over a directory that is not there. That walk is the axis's per-feature\n * opt-in and the whole price a fleet that has not adopted it pays.\n */\nexport async function stageMergedProject(req: StagedProjectRequest): Promise<StagedProject> {\n const merged = await mergedLandscape(req);\n if (merged.kind === \"failed\") return { kind: \"unreadable\", errors: [merged.reason] };\n\n const arch = architectureDir(req.docsDir);\n // BOTH generated documents come out, through the one list every project read\n // shares: a views-only file does not parse standalone, so staging one that\n // `sync` has not caught up with would blank the merge preview this whole\n // module exists to parse (`./architecture.ts` states the rule).\n const living = await architectureDocuments(arch, generatedArchitectureDocuments(req.docsDir));\n const root = await mkdtemp(join(tmpdir(), \"loam-staged-\"));\n try {\n // Staged rel -> the path the AUTHOR wrote, so every message that leaves\n // here names a file they can open. Built as the tree is staged, because\n // that is the one moment both spellings are in hand.\n const authored = new Map<string, string>();\n const landscape = resolve(landscapePath(req.docsDir));\n await inOrder(living, async (path) => {\n // The living landscape is staged from the merge preview when there is\n // one; copying it as well would declare every element twice.\n if (merged.kind === \"merged\" && resolve(path) === landscape) return null;\n await stage(root, spell(relative(arch, path)), path, authored);\n return null;\n });\n if (merged.kind === \"merged\") {\n const rel = spell(relative(arch, landscape));\n await write(root, rel, merged.content);\n authored.set(rel, landscape);\n }\n // The feature's own documents LAST, so one whose living twin exists wins\n // the staging rather than colliding with it. That case is already an error\n // the gate refuses before any merge — `usecase.flow-exists`,\n // `deployment.doc-exists` — and grading the feature's version is the more\n // useful of the two answers to give an author who is about to be told to\n // delete the file.\n const mine = new Set<string>();\n for (const document of req.documents) {\n await stage(root, document.rel, document.path, authored);\n mine.add(document.rel);\n }\n const doc = asLoadedDoc(await loadProject(root, await architectureDocuments(root)));\n // TWO SPELLINGS REACH HERE and both must map, which is the one thing this\n // helper cannot be allowed to get half right. A parse error's\n // `sourceFsPath` is an ABSOLUTE path into the temp tree; a view's\n // `sourcePath` is already the project-RELATIVE rel LikeC4 reports. The\n // rel-first lookup answers the second, and the relative() computation the\n // first. Handling only one leaves the other silently returning the temp\n // spelling, which is a finding naming a directory that no longer exists.\n const real = (path: string): string =>\n authored.get(spell(path)) ?? authored.get(spell(relative(root, path))) ?? path;\n if (doc.errors.length > 0) {\n // An error naming no document is LikeC4 declining to attribute one. It\n // still travels: a hole loam cannot place is still a hole, and dropping\n // it here would grade the feature clean on the strength of a missing\n // field.\n //\n // DOCS-RELATIVE, `/`-separated, and carrying the line — the spelling the\n // SERVICE arm of `usecase.flow-invalid` uses on purpose. These strings are\n // the feature arm's whole payload (text, `--json` and the `loam archive`\n // block message), and `real()` alone answered with an absolute Windows path\n // and no line at all: `C:\\Users\\…\\docs\\features\\FEAT-1\\usecases\\x.likec4:\n // <message>` in a finding a person is asked to act on (verification\n // 2026-09-04, W7). A path outside the docs root — which nothing here\n // produces, but `relative` would spell `..\\..` — keeps its absolute form.\n return {\n kind: \"unreadable\",\n errors: doc.errors.map((e) =>\n e.sourceFsPath === undefined ? errorText(e) : `${spellReal(req.docsDir, real(e.sourceFsPath))}: ${errorText(e)}`,\n ),\n };\n }\n return { kind: \"read\", doc, real, mine };\n } catch (err) {\n // The `unreadable` arm is a PROMISE, not a description of the happy path.\n // Two things under here throw rather than return: `loadProject` refuses an\n // error it cannot attribute to a document, and every `mkdir`/`copyFile` in\n // the staging above can fail on a full disk or a denied temp directory.\n // Callers run inside `loam validate --feature` and the archive gate — and\n // the one behaviour a check added to those may never have is failing the\n // run it was added to. So a throw becomes the same could-not-look answer a\n // parse error already produces, which each caller turns into its own\n // refusal that writes nothing.\n return { kind: \"unreadable\", errors: [err instanceof Error ? err.message : String(err)] };\n } finally {\n await rm(root, { recursive: true, force: true });\n }\n}\n\n/** What the landscape looks like after this feature's C4 delta is spliced in. */\ntype MergedLandscape =\n /** The delta adds nothing, or there is none — the living landscape stands. */\n | { kind: \"living\" }\n /** The merge preview, as authored bytes. */\n | { kind: \"merged\"; content: string }\n /** The merge could not be computed; `planLandscape` owns the refusal and says why. */\n | { kind: \"failed\"; reason: string };\n\n/**\n * The merge preview, computed with the SAME function `loam archive` commits\n * with.\n *\n * Reusing `planLandscapeMerge` rather than approximating it is what makes this\n * an answer about the archive rather than a second opinion beside it: an\n * element placed differently, a tag stripped differently, or a splice refused\n * there and allowed here would be a document graded against a map the merge\n * never writes.\n *\n * THE PARITY IS THE MERGE, NOT THE WHOLE GATE, and the two places the sides\n * differ are worth naming rather than leaving for a reader to discover.\n *\n * The first changes no verdict. `planLandscape` runs one check BEFORE it\n * computes the merge and this does not: a delta declaring the feature tag on a\n * specification KIND is refused there, because every element of that kind\n * inherits the tag and the merge would splice the whole document. Such a delta\n * reaches `newEls` here unfiltered, so the preview this grades against is one\n * the archive would never write — but that archive refuses either way, before\n * anything is written.\n *\n * The second changed every verdict about a fleet with extending models, and is\n * why `models` is on the request at all. `planLandscape` names the fleet's\n * extending models, so a nested addition is ROUTED into the model that owns\n * that service's interior and never reaches the map; a request that omits them\n * previews a map holding interior the archive will put somewhere else. A\n * feature-local `usecases/<x>.likec4` whose hop names `<service-fqn>.<container>`\n * then resolved in the preview, passed `usecase.flow-invalid` — the gate whose\n * own message is \"the archive would land an architecture/ nothing has checked\",\n * and which `--approve` cannot override — and was copied verbatim into\n * `architecture/`, where it resolves against nothing and takes the whole fleet\n * map down (verification 2026-09-04, review C; SCHEMA names that outcome as the\n * thing that must not happen). A caller that leaves `models` out is asking for\n * the pre-routing preview, and gets exactly the answers it got before.\n *\n * A throw is `failed` rather than propagated. Every `LandscapeSpliceError` this\n * can raise is one `planLandscape` raises again — with its own message, its own\n * refusal code, and before anything is written — so throwing here would only\n * decide which of two identical refusals an author reads first.\n */\nasync function mergedLandscape(req: StagedProjectRequest): Promise<MergedLandscape> {\n const deltaPath = featurePaths(req.featureDir).delta;\n const landscape = landscapePath(req.docsDir);\n if (!existsSync(deltaPath) || !existsSync(landscape)) return { kind: \"living\" };\n const delta = await (req.load ?? loadFile)(deltaPath);\n if (delta.errors.length > 0) {\n return {\n kind: \"failed\",\n reason: `delta.likec4 has ${delta.errors.length} parse error(s) — a feature's own documents cannot be graded against a map that does not parse`,\n };\n }\n try {\n const plan = await planLandscapeMerge({\n landscapeText: await text(landscape),\n deltaText: await text(deltaPath),\n deltaElements: delta.elements,\n newEls: delta.elements.filter((e) => e.tags.includes(req.featureId)),\n newRels: delta.relationships.filter((r) => r.tags.includes(req.featureId)),\n featureId: req.featureId,\n ...(req.models === undefined ? {} : { models: req.models }),\n });\n return plan.content === null ? { kind: \"living\" } : { kind: \"merged\", content: plan.content };\n } catch (err) {\n return { kind: \"failed\", reason: err instanceof Error ? err.message : String(err) };\n }\n}\n\n/** Read a document loam is about to hand LikeC4, refusing the encodings that decode as empty. */\nasync function text(path: string): Promise<string> {\n return decodeDocument(await readFile(path), path);\n}\n\n/** A path spelled the way a staged rel is spelled — `/`-separated, on every platform. */\nfunction spell(path: string): string {\n return path.split(/[\\\\/]/).join(\"/\");\n}\n\n/**\n * An authored path as a FINDING spells it: relative to the docs root,\n * `/`-separated. A path that does not sit under the docs root keeps its absolute\n * form — `relative` would answer with `..` segments, and a finding naming a\n * document outside the repository is more honest as the absolute path it is.\n */\nfunction spellReal(docsDir: DocsDir, path: string): string {\n const rel = relative(docsDir, path);\n return rel.length > 0 && !rel.startsWith(\"..\") ? spell(rel) : spell(path);\n}\n\n/** Copy one authored document into the staged tree, remembering where it came from. */\nasync function stage(root: string, rel: string, path: string, authored: Map<string, string>): Promise<void> {\n const dest = join(root, ...rel.split(\"/\"));\n await mkdir(dirname(dest), { recursive: true });\n await copyFile(path, dest);\n authored.set(rel, resolve(path));\n}\n\n/** Write one document into the staged tree from bytes loam holds rather than a file. */\nasync function write(root: string, rel: string, content: string): Promise<void> {\n const dest = join(root, ...rel.split(\"/\"));\n await mkdir(dirname(dest), { recursive: true });\n await writeFile(dest, content, \"utf8\");\n}\n"]}
@@ -94,10 +94,11 @@ export function livingDeploymentPath(docsDir, rel) {
94
94
  *
95
95
  * `deployment.doc-reserved` — the name is one loam owns inside `architecture/`.
96
96
  * The merge writes straight into that directory, unlike a flow, which lands
97
- * under `usecases/`, so `<name>` is free to collide with the living map, the
98
- * generated subsystem views, or the flow tree. `reservedArchitectureName`
99
- * carries which and why. It is asked FIRST and WITHOUT an existence test,
100
- * because two of the three fail silently when the target does not exist yet.
97
+ * under `usecases/`, so `<name>` is free to collide with the living map, either
98
+ * of the two GENERATED views files (`subsystems.likec4` and `services.likec4`),
99
+ * or the flow tree. `reservedArchitectureName` carries which and why. It is
100
+ * asked FIRST and WITHOUT an existence test, because three of the four fail
101
+ * silently when the target does not exist yet.
101
102
  *
102
103
  * `deployment.doc-exists` — a topology document this feature introduces that
103
104
  * the living `architecture/` already holds.
@@ -118,12 +119,13 @@ export function livingDeploymentPath(docsDir, rel) {
118
119
  export async function deploymentDeltaIssues(docsDir, featureDir) {
119
120
  const docs = await featureDeployments(featureDir);
120
121
  const issues = [];
121
- // The reservation runs FIRST and independently of existence, because two of
122
- // the three names it refuses need not exist for the merge to be wrong. A
123
- // topology copied over `subsystems.likec4` is destroyed by the next
124
- // `loam subsystem sync` and is invisible to every project load until then —
125
- // the merge would report `created architecture/subsystems.likec4` and nothing
126
- // downstream would ever say another word about it.
122
+ // The reservation runs FIRST and independently of existence, because three of
123
+ // the four names it refuses need not exist for the merge to be wrong. A
124
+ // topology copied over either GENERATED file — `subsystems.likec4` or
125
+ // `services.likec4` — is destroyed by the next `loam subsystem sync` and is
126
+ // invisible to every project load until then: the merge would report
127
+ // `created architecture/subsystems.likec4` and nothing downstream would ever
128
+ // say another word about it.
127
129
  for (const doc of docs) {
128
130
  const reason = reservedArchitectureName(doc.rel);
129
131
  if (reason === null)