@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":"delta.js","sourceRoot":"","sources":["../../../src/core/deployment/delta.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * A feature's own topology: `features/<FEAT>/deployment/<name>.likec4`.\n *\n * Until this slot existed a change that stood up a standby cluster had to be\n * written in two places at once — the requirement in the feature, the topology\n * edited straight into `architecture/` — because `delta.likec4` refuses a\n * `deployment { }` block and always will: that document re-declares the\n * landscape's own identifiers and carries its own `specification`, so it cannot\n * be staged beside the map in one LikeC4 project. A document that declares\n * NEITHER can, which is exactly why the use-case axis put its flows in files of\n * their own, and why this mirrors it rather than inventing a shape.\n *\n * WHAT MAKES IT WORK, and it was measured before the axis was written: LikeC4\n * `extend` resolves across documents of one project. A feature writes\n *\n * deployment {\n * extend eu {\n * dcC = datacenter 'DC-C' { k8sC = cluster 'cluster-c' { … } }\n * }\n * eu.dcA.k8sA -> eu.dcC.k8sC 'async replication'\n * }\n *\n * and the living region gains a datacenter with the living file untouched. So\n * the merge is a copy, not a splice, and `unarchive` needs nothing new — a\n * create is undone by deleting, which the snapshot manifest already records for\n * every write in the plan.\n *\n * CREATE-ONLY, for `core/usecases/delta/flows.ts`'s reasons rather than a\n * weaker version of them. A requirement delta merges INTO a living document and\n * two features editing one requirement would overwrite each other silently; a\n * topology document has no such algebra — the file is one block, the merge is a\n * whole-file copy, and there is nothing to merge partially. Rewriting a living\n * one through a feature would be a silent whole-file replacement with no pin to\n * collide on, where editing `architecture/<name>.likec4` directly in the same\n * pull request produces an ordinary git conflict a human resolves.\n *\n * ## Graded against the map its own merge would leave behind\n *\n * `ARCH-LOAM-FEATURE-CORPUS`, and this axis needs it more literally than the\n * use-case one does: a feature that stands a service up in a new cluster\n * declares that service in its `delta.likec4` and instances it here, so the\n * element the topology names exists ONLY post-merge. Read against the living\n * landscape it is unresolved, LikeC4 refuses the whole project, and loam would\n * report a hole against a file whose only fault is arriving with the change\n * that makes it true.\n *\n * The staging is `core/c4/project/staged.ts`, shared with the use-case slot\n * rather than copied — the two would have disagreed first about the merge\n * preview, which is the one thing both refusals rest on.\n */\nimport { existsSync } from \"node:fs\";\nimport { basename, join, relative, resolve } from \"node:path\";\nimport { architectureDir } from \"../c4/project/architecture.js\";\nimport { stageMergedProject } from \"../c4/project/staged.js\";\nimport { type LoadedDoc } from \"../c4/likec4.js\";\nimport type { ExtendingModel } from \"../c4/splice/contract.js\";\nimport { architectureDocuments } from \"../c4/project/documents.js\";\nimport { featureDeploymentDir } from \"../repo/authored/paths.js\";\nimport { reservedArchitectureName } from \"../repo/paths.js\";\nimport type { DocsDir, FeatureDir } from \"../kernel/ids/dirs.js\";\nimport { type Issue } from \"../vocabulary/issue.js\";\n\n/** One topology document a feature carries. */\nexport interface FeatureDeployment {\n /**\n * The path INSIDE the feature's `deployment/` directory, `/`-separated — the\n * id this axis has. Nesting is spelled by the tree, as it is for every other\n * authored tree in the repo.\n */\n rel: string;\n /** The absolute path of the authored file. */\n path: string;\n}\n\n/**\n * The topology documents one feature carries, ordered by path.\n *\n * Zero readdirs when the feature has no `deployment/` directory, which is every\n * feature in a fleet that has not adopted the axis: `architectureDocuments`\n * answers an absent directory with an empty list rather than a throw. That\n * directory's existence is the whole per-feature opt-in.\n */\nexport async function featureDeployments(featureDir: FeatureDir): Promise<FeatureDeployment[]> {\n const dir = featureDeploymentDir(featureDir);\n const paths = await architectureDocuments(dir);\n const base = resolve(dir);\n return paths.map((path) => ({ rel: relative(base, path).split(/[\\\\/]/).join(\"/\"), path }));\n}\n\n/**\n * The LIVING path a feature-local topology document addresses.\n *\n * Straight into `architecture/`, with no subdirectory of its own — unlike\n * `architecture/usecases/`, and the difference is the model rather than taste.\n * A `deployment { }` block is part of the fleet's ONE architecture project and\n * is read as such by every loader; filing it under a subdirectory would suggest\n * a second project, which is exactly the reading `subsystems.likec4` and\n * `landscape.likec4` already refuse by sitting at the top level together.\n *\n * `rel` is joined un-split, which `node:path` normalizes. What holds the join is\n * PROVENANCE: every `rel` here came out of an `architectureDocuments` walk, so\n * each component is a directory entry that exists. No caller takes one from argv.\n */\nexport function livingDeploymentPath(docsDir: DocsDir, rel: string): string {\n return join(architectureDir(docsDir), rel);\n}\n\n/**\n * The two things that can go wrong about a topology document's NAME, before\n * anything about its contents is asked.\n *\n * `deployment.doc-reserved` — the name is one loam owns inside `architecture/`.\n * The merge writes straight into that directory, unlike a flow, which lands\n * under `usecases/`, so `<name>` is free to collide with the living map, the\n * generated subsystem views, or the flow tree. `reservedArchitectureName`\n * carries which and why. It is asked FIRST and WITHOUT an existence test,\n * because two of the three fail silently when the target does not exist yet.\n *\n * `deployment.doc-exists` — a topology document this feature introduces that\n * the living `architecture/` already holds.\n *\n * An ERROR and never a warning-that-gates, for `usecase.flow-exists`'s reason:\n * there is no legal reading of it. The merge would replace an authored topology\n * wholesale, and no `--approve` makes that a different act than it is. Two\n * features introducing the same file is the case that makes it load-bearing\n * rather than pedantic — whichever archives first creates the living document,\n * and the second is told before it merges that its own file is the one to\n * delete.\n *\n * The collision is on the FILE, not on what is inside it. Two features may both\n * extend the same living region from two documents of their own and both\n * archive; that is the shape `extend` exists for, and refusing it would make the\n * slot useless for the fleet-wide change it was built for.\n */\nexport async function deploymentDeltaIssues(docsDir: DocsDir, featureDir: FeatureDir): Promise<Issue[]> {\n const docs = await featureDeployments(featureDir);\n const issues: Issue[] = [];\n // The reservation runs FIRST and independently of existence, because two of\n // the three names it refuses need not exist for the merge to be wrong. A\n // topology copied over `subsystems.likec4` is destroyed by the next\n // `loam subsystem sync` and is invisible to every project load until then —\n // the merge would report `created architecture/subsystems.likec4` and nothing\n // downstream would ever say another word about it.\n for (const doc of docs) {\n const reason = reservedArchitectureName(doc.rel);\n if (reason === null) continue;\n issues.push({\n severity: \"error\",\n code: \"deployment.doc-reserved\",\n subject: doc.rel,\n message:\n `features/<FEAT>/deployment/${doc.rel} would merge to architecture/${doc.rel}, and that name is loam's: ` +\n `${reason}. Rename the feature's document — its name is yours to choose, and \\`extend\\` is how it adds to ` +\n \"what a living document already declares.\",\n });\n }\n return issues.concat(\n docs\n .filter((doc) => reservedArchitectureName(doc.rel) === null)\n .filter((doc) => existsSync(livingDeploymentPath(docsDir, doc.rel)))\n .map((doc) => ({\n severity: \"error\" as const,\n code: \"deployment.doc-exists\" as const,\n subject: doc.rel,\n message:\n `architecture/${doc.rel} already exists, so this feature's topology would replace an authored one wholesale — ` +\n \"a feature-local deployment document INTRODUCES topology, it does not rewrite it. \" +\n `Delete features/<FEAT>/deployment/${doc.rel} and edit architecture/${doc.rel} directly in this same change, ` +\n \"where git produces an ordinary conflict if somebody else is editing it too. \" +\n `To ADD to what that file declares, write a document of another name that says \\`extend\\` — ` +\n `\\`${suggest(doc.rel)}\\` — which is what lets two features grow one region without touching each other.`,\n })),\n );\n}\n\n/** What the caller needs to grade this feature's topology against the merge preview. */\nexport interface DeploymentOverlayRequest {\n docsDir: DocsDir;\n featureDir: FeatureDir;\n /** The feature's id — the tag that selects the delta's own additions. */\n featureId: string;\n /**\n * The caller's memoised LikeC4 read, when it has one. A FUNCTION rather than\n * the `FleetContext` itself, for the reason the use-case overlay states: one\n * memoised read does not justify an edge from this package to\n * `core/fleet-context.ts`, and the edge would push it up a DAG level for\n * nothing.\n */\n load?: (path: string) => Promise<LoadedDoc>;\n /**\n * The fleet's extending models, for the merge preview — the SAME list\n * `loam archive` routes with. A preview computed without them holds interior\n * the archive puts in `services/<…>/model.likec4`, so a topology document\n * naming `<service-fqn>.<container>` reads clean here and against nothing\n * once it is living; `deployment.doc-invalid` is the gate that then fails\n * open (verification 2026-09-04, review F8).\n *\n * A THUNK for `load`'s reason and one more: the early return below is the\n * axis's per-feature opt-in, and a feature with no `deployment/` directory\n * must not pay a fleet enumeration to be told so.\n */\n models?: () => Promise<readonly ExtendingModel[]>;\n}\n\n/** Whether this feature's topology could be read at all, against the map its merge would leave. */\nexport type DeploymentOverlay = { kind: \"read\" } | { kind: \"unreadable\"; errors: string[] };\n\n/**\n * Parse this feature's topology documents over the post-merge map.\n *\n * A feature with no `deployment/` directory is `read` at the cost of one walk\n * over a directory that is not there — the axis's per-feature opt-in, and the\n * whole price a fleet that has not adopted it pays. Nothing is staged, nothing\n * is parsed, and no temp tree is made.\n *\n * The verdict is deliberately BINARY. The use-case overlay hands its caller the\n * views it read because the axis has questions about their content; this one\n * has none — a topology document is checked by being parseable against the map\n * the merge will write, and everything else about it is graded by\n * `loam validate --all` once it is living, exactly as the landscape is.\n */\nexport async function readFeatureDeployments(req: DeploymentOverlayRequest): Promise<DeploymentOverlay> {\n const docs = await featureDeployments(req.featureDir);\n if (docs.length === 0) return { kind: \"read\" };\n // AFTER the early return: the fleet read is paid only by a feature that\n // actually brings a topology document.\n const models = req.models === undefined ? undefined : await req.models();\n const staged = await stageMergedProject({\n docsDir: req.docsDir,\n featureDir: req.featureDir,\n featureId: req.featureId,\n documents: docs.map((doc) => ({ rel: doc.rel, path: doc.path })),\n ...(req.load === undefined ? {} : { load: req.load }),\n ...(models === undefined ? {} : { models }),\n });\n return staged.kind === \"unreadable\" ? { kind: \"unreadable\", errors: staged.errors } : { kind: \"read\" };\n}\n\n/** A free name near the colliding one, so the fix is a rename the author can copy. */\nfunction suggest(rel: string): string {\n const name = basename(rel).replace(/\\.likec4$/, \"\");\n const dir = rel.slice(0, rel.length - basename(rel).length);\n return `${dir}${name}-extend.likec4`;\n}\n"]}
1
+ {"version":3,"file":"delta.js","sourceRoot":"","sources":["../../../src/core/deployment/delta.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * A feature's own topology: `features/<FEAT>/deployment/<name>.likec4`.\n *\n * Until this slot existed a change that stood up a standby cluster had to be\n * written in two places at once — the requirement in the feature, the topology\n * edited straight into `architecture/` — because `delta.likec4` refuses a\n * `deployment { }` block and always will: that document re-declares the\n * landscape's own identifiers and carries its own `specification`, so it cannot\n * be staged beside the map in one LikeC4 project. A document that declares\n * NEITHER can, which is exactly why the use-case axis put its flows in files of\n * their own, and why this mirrors it rather than inventing a shape.\n *\n * WHAT MAKES IT WORK, and it was measured before the axis was written: LikeC4\n * `extend` resolves across documents of one project. A feature writes\n *\n * deployment {\n * extend eu {\n * dcC = datacenter 'DC-C' { k8sC = cluster 'cluster-c' { … } }\n * }\n * eu.dcA.k8sA -> eu.dcC.k8sC 'async replication'\n * }\n *\n * and the living region gains a datacenter with the living file untouched. So\n * the merge is a copy, not a splice, and `unarchive` needs nothing new — a\n * create is undone by deleting, which the snapshot manifest already records for\n * every write in the plan.\n *\n * CREATE-ONLY, for `core/usecases/delta/flows.ts`'s reasons rather than a\n * weaker version of them. A requirement delta merges INTO a living document and\n * two features editing one requirement would overwrite each other silently; a\n * topology document has no such algebra — the file is one block, the merge is a\n * whole-file copy, and there is nothing to merge partially. Rewriting a living\n * one through a feature would be a silent whole-file replacement with no pin to\n * collide on, where editing `architecture/<name>.likec4` directly in the same\n * pull request produces an ordinary git conflict a human resolves.\n *\n * ## Graded against the map its own merge would leave behind\n *\n * `ARCH-LOAM-FEATURE-CORPUS`, and this axis needs it more literally than the\n * use-case one does: a feature that stands a service up in a new cluster\n * declares that service in its `delta.likec4` and instances it here, so the\n * element the topology names exists ONLY post-merge. Read against the living\n * landscape it is unresolved, LikeC4 refuses the whole project, and loam would\n * report a hole against a file whose only fault is arriving with the change\n * that makes it true.\n *\n * The staging is `core/c4/project/staged.ts`, shared with the use-case slot\n * rather than copied — the two would have disagreed first about the merge\n * preview, which is the one thing both refusals rest on.\n */\nimport { existsSync } from \"node:fs\";\nimport { basename, join, relative, resolve } from \"node:path\";\nimport { architectureDir } from \"../c4/project/architecture.js\";\nimport { stageMergedProject } from \"../c4/project/staged.js\";\nimport { type LoadedDoc } from \"../c4/likec4.js\";\nimport type { ExtendingModel } from \"../c4/splice/contract.js\";\nimport { architectureDocuments } from \"../c4/project/documents.js\";\nimport { featureDeploymentDir } from \"../repo/authored/paths.js\";\nimport { reservedArchitectureName } from \"../repo/paths.js\";\nimport type { DocsDir, FeatureDir } from \"../kernel/ids/dirs.js\";\nimport { type Issue } from \"../vocabulary/issue.js\";\n\n/** One topology document a feature carries. */\nexport interface FeatureDeployment {\n /**\n * The path INSIDE the feature's `deployment/` directory, `/`-separated — the\n * id this axis has. Nesting is spelled by the tree, as it is for every other\n * authored tree in the repo.\n */\n rel: string;\n /** The absolute path of the authored file. */\n path: string;\n}\n\n/**\n * The topology documents one feature carries, ordered by path.\n *\n * Zero readdirs when the feature has no `deployment/` directory, which is every\n * feature in a fleet that has not adopted the axis: `architectureDocuments`\n * answers an absent directory with an empty list rather than a throw. That\n * directory's existence is the whole per-feature opt-in.\n */\nexport async function featureDeployments(featureDir: FeatureDir): Promise<FeatureDeployment[]> {\n const dir = featureDeploymentDir(featureDir);\n const paths = await architectureDocuments(dir);\n const base = resolve(dir);\n return paths.map((path) => ({ rel: relative(base, path).split(/[\\\\/]/).join(\"/\"), path }));\n}\n\n/**\n * The LIVING path a feature-local topology document addresses.\n *\n * Straight into `architecture/`, with no subdirectory of its own — unlike\n * `architecture/usecases/`, and the difference is the model rather than taste.\n * A `deployment { }` block is part of the fleet's ONE architecture project and\n * is read as such by every loader; filing it under a subdirectory would suggest\n * a second project, which is exactly the reading `subsystems.likec4` and\n * `landscape.likec4` already refuse by sitting at the top level together.\n *\n * `rel` is joined un-split, which `node:path` normalizes. What holds the join is\n * PROVENANCE: every `rel` here came out of an `architectureDocuments` walk, so\n * each component is a directory entry that exists. No caller takes one from argv.\n */\nexport function livingDeploymentPath(docsDir: DocsDir, rel: string): string {\n return join(architectureDir(docsDir), rel);\n}\n\n/**\n * The two things that can go wrong about a topology document's NAME, before\n * anything about its contents is asked.\n *\n * `deployment.doc-reserved` — the name is one loam owns inside `architecture/`.\n * The merge writes straight into that directory, unlike a flow, which lands\n * under `usecases/`, so `<name>` is free to collide with the living map, either\n * of the two GENERATED views files (`subsystems.likec4` and `services.likec4`),\n * or the flow tree. `reservedArchitectureName` carries which and why. It is\n * asked FIRST and WITHOUT an existence test, because three of the four fail\n * silently when the target does not exist yet.\n *\n * `deployment.doc-exists` — a topology document this feature introduces that\n * the living `architecture/` already holds.\n *\n * An ERROR and never a warning-that-gates, for `usecase.flow-exists`'s reason:\n * there is no legal reading of it. The merge would replace an authored topology\n * wholesale, and no `--approve` makes that a different act than it is. Two\n * features introducing the same file is the case that makes it load-bearing\n * rather than pedantic — whichever archives first creates the living document,\n * and the second is told before it merges that its own file is the one to\n * delete.\n *\n * The collision is on the FILE, not on what is inside it. Two features may both\n * extend the same living region from two documents of their own and both\n * archive; that is the shape `extend` exists for, and refusing it would make the\n * slot useless for the fleet-wide change it was built for.\n */\nexport async function deploymentDeltaIssues(docsDir: DocsDir, featureDir: FeatureDir): Promise<Issue[]> {\n const docs = await featureDeployments(featureDir);\n const issues: Issue[] = [];\n // The reservation runs FIRST and independently of existence, because three of\n // the four names it refuses need not exist for the merge to be wrong. A\n // topology copied over either GENERATED file — `subsystems.likec4` or\n // `services.likec4` — is destroyed by the next `loam subsystem sync` and is\n // invisible to every project load until then: the merge would report\n // `created architecture/subsystems.likec4` and nothing downstream would ever\n // say another word about it.\n for (const doc of docs) {\n const reason = reservedArchitectureName(doc.rel);\n if (reason === null) continue;\n issues.push({\n severity: \"error\",\n code: \"deployment.doc-reserved\",\n subject: doc.rel,\n message:\n `features/<FEAT>/deployment/${doc.rel} would merge to architecture/${doc.rel}, and that name is loam's: ` +\n `${reason}. Rename the feature's document — its name is yours to choose, and \\`extend\\` is how it adds to ` +\n \"what a living document already declares.\",\n });\n }\n return issues.concat(\n docs\n .filter((doc) => reservedArchitectureName(doc.rel) === null)\n .filter((doc) => existsSync(livingDeploymentPath(docsDir, doc.rel)))\n .map((doc) => ({\n severity: \"error\" as const,\n code: \"deployment.doc-exists\" as const,\n subject: doc.rel,\n message:\n `architecture/${doc.rel} already exists, so this feature's topology would replace an authored one wholesale — ` +\n \"a feature-local deployment document INTRODUCES topology, it does not rewrite it. \" +\n `Delete features/<FEAT>/deployment/${doc.rel} and edit architecture/${doc.rel} directly in this same change, ` +\n \"where git produces an ordinary conflict if somebody else is editing it too. \" +\n `To ADD to what that file declares, write a document of another name that says \\`extend\\` — ` +\n `\\`${suggest(doc.rel)}\\` — which is what lets two features grow one region without touching each other.`,\n })),\n );\n}\n\n/** What the caller needs to grade this feature's topology against the merge preview. */\nexport interface DeploymentOverlayRequest {\n docsDir: DocsDir;\n featureDir: FeatureDir;\n /** The feature's id — the tag that selects the delta's own additions. */\n featureId: string;\n /**\n * The caller's memoised LikeC4 read, when it has one. A FUNCTION rather than\n * the `FleetContext` itself, for the reason the use-case overlay states: one\n * memoised read does not justify an edge from this package to\n * `core/fleet-context.ts`, and the edge would push it up a DAG level for\n * nothing.\n */\n load?: (path: string) => Promise<LoadedDoc>;\n /**\n * The fleet's extending models, for the merge preview — the SAME list\n * `loam archive` routes with. A preview computed without them holds interior\n * the archive puts in `services/<…>/model.likec4`, so a topology document\n * naming `<service-fqn>.<container>` reads clean here and against nothing\n * once it is living; `deployment.doc-invalid` is the gate that then fails\n * open (verification 2026-09-04, review F8).\n *\n * A THUNK for `load`'s reason and one more: the early return below is the\n * axis's per-feature opt-in, and a feature with no `deployment/` directory\n * must not pay a fleet enumeration to be told so.\n */\n models?: () => Promise<readonly ExtendingModel[]>;\n}\n\n/** Whether this feature's topology could be read at all, against the map its merge would leave. */\nexport type DeploymentOverlay = { kind: \"read\" } | { kind: \"unreadable\"; errors: string[] };\n\n/**\n * Parse this feature's topology documents over the post-merge map.\n *\n * A feature with no `deployment/` directory is `read` at the cost of one walk\n * over a directory that is not there — the axis's per-feature opt-in, and the\n * whole price a fleet that has not adopted it pays. Nothing is staged, nothing\n * is parsed, and no temp tree is made.\n *\n * The verdict is deliberately BINARY. The use-case overlay hands its caller the\n * views it read because the axis has questions about their content; this one\n * has none — a topology document is checked by being parseable against the map\n * the merge will write, and everything else about it is graded by\n * `loam validate --all` once it is living, exactly as the landscape is.\n */\nexport async function readFeatureDeployments(req: DeploymentOverlayRequest): Promise<DeploymentOverlay> {\n const docs = await featureDeployments(req.featureDir);\n if (docs.length === 0) return { kind: \"read\" };\n // AFTER the early return: the fleet read is paid only by a feature that\n // actually brings a topology document.\n const models = req.models === undefined ? undefined : await req.models();\n const staged = await stageMergedProject({\n docsDir: req.docsDir,\n featureDir: req.featureDir,\n featureId: req.featureId,\n documents: docs.map((doc) => ({ rel: doc.rel, path: doc.path })),\n ...(req.load === undefined ? {} : { load: req.load }),\n ...(models === undefined ? {} : { models }),\n });\n return staged.kind === \"unreadable\" ? { kind: \"unreadable\", errors: staged.errors } : { kind: \"read\" };\n}\n\n/** A free name near the colliding one, so the fix is a rename the author can copy. */\nfunction suggest(rel: string): string {\n const name = basename(rel).replace(/\\.likec4$/, \"\");\n const dir = rel.slice(0, rel.length - basename(rel).length);\n return `${dir}${name}-extend.likec4`;\n}\n"]}
@@ -111,7 +111,11 @@ function partnersFindings(scan, service) {
111
111
  for (const p of scan.partners) {
112
112
  // An `#external` partner is somebody else's system: its side of the join
113
113
  // is unrecorded ON PURPOSE, the same exemption the landscape census grants
114
- // (`landscape.service-undocumented` skips #external too).
114
+ // (`landscape.service-undocumented` skips #external too). The fleet's own
115
+ // infrastructure — a `database`, `queue`, `topic`, `cache` or `bucket` — is
116
+ // not a partner at all and never reaches this loop: `./partners.ts` drops it
117
+ // from the SET, beside the actors, because the three remedies below are all
118
+ // false of a store the fleet runs.
115
119
  if (p.external)
116
120
  continue;
117
121
  if (p.maturity === null) {
@@ -1 +1 @@
1
- {"version":3,"file":"checks.js","sourceRoot":"","sources":["../../../src/core/gate/checks.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;GAsBG;AACH,OAAO,EAAE,UAAU,EAAE,MAAM,SAAS,CAAC;AACrC,OAAO,EAAE,OAAO,EAAE,MAAM,0BAA0B,CAAC;AACnD,OAAO,EAAE,iBAAiB,EAAE,MAAM,2BAA2B,CAAC;AAC9D,OAAO,EAAE,wBAAwB,EAAE,MAAM,gCAAgC,CAAC;AAC1E,OAAO,EAAE,cAAc,EAAE,SAAS,EAAqB,MAAM,kBAAkB,CAAC;AAChF,OAAO,EAAE,eAAe,EAAE,MAAM,uBAAuB,CAAC;AACxD,OAAO,EAAE,kBAAkB,EAAE,MAAM,2BAA2B,CAAC;AAK/D,OAAO,EAAE,iBAAiB,EAAE,WAAW,EAAoB,MAAM,eAAe,CAAC;AACjF,OAAO,EAAE,iBAAiB,EAAE,MAAM,mBAAmB,CAAC;AACtD,OAAO,EAAE,iBAAiB,EAAmC,MAAM,aAAa,CAAC;AAgBjF,MAAM,CAAC,KAAK,UAAU,UAAU,CAAC,GAAgB;IAC/C,yEAAyE;IACzE,0EAA0E;IAC1E,4EAA4E;IAC5E,MAAM,IAAI,GAAG,MAAM,WAAW,CAAC,EAAE,OAAO,EAAE,GAAG,CAAC,OAAO,EAAE,OAAO,EAAE,GAAG,CAAC,OAAO,EAAE,KAAK,EAAE,GAAG,CAAC,KAAK,EAAE,CAAC,CAAC;IACjG,MAAM,CAAC,SAAS,EAAE,YAAY,EAAE,WAAW,CAAC,GAAG,MAAM,OAAO,CAAC,GAAG,CAAC;QAC/D,iBAAiB,CAAC,GAAG,EAAE,IAAI,CAAC;QAC5B,iBAAiB,CAAC,EAAE,OAAO,EAAE,GAAG,CAAC,OAAO,EAAE,OAAO,EAAE,GAAG,CAAC,OAAO,EAAE,KAAK,EAAE,GAAG,CAAC,KAAK,EAAE,CAAC;QACnF,wBAAwB,CAAC,GAAG,CAAC,OAAO,CAAC;KACtC,CAAC,CAAC;IACH,MAAM,MAAM,GAAgB;QAC1B,EAAE,KAAK,EAAE,UAAU,EAAE,QAAQ,EAAE,gBAAgB,CAAC,IAAI,EAAE,GAAG,CAAC,OAAO,CAAC,EAAE;QACpE,EAAE,KAAK,EAAE,WAAW,EAAE,QAAQ,EAAE,SAAS,EAAE;QAC3C,EAAE,KAAK,EAAE,cAAc,EAAE,QAAQ,EAAE,YAAY,CAAC,QAAQ,EAAE;QAC1D,EAAE,KAAK,EAAE,aAAa,EAAE,QAAQ,EAAE,WAAW,KAAK,IAAI,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,CAAC,WAAW,CAAC,EAAE;KAC9E,CAAC;IACF,OAAO;QACL,OAAO,EAAE,GAAG,CAAC,OAAO;QACpB,SAAS,EAAE,IAAI,CAAC,SAAS;QACzB,QAAQ,EAAE,IAAI,CAAC,QAAQ;QACvB,QAAQ,EAAE,YAAY,CAAC,QAAQ;QAC/B,MAAM;KACP,CAAC;AACJ,CAAC;AAED,wEAAwE;AACxE,yEAAyE;AACzE,wEAAwE;AAExE,SAAS,gBAAgB,CAAC,IAAiB,EAAE,OAAe;IAC1D,MAAM,QAAQ,GAAc,EAAE,CAAC;IAC/B,wEAAwE;IACxE,sEAAsE;IACtE,2EAA2E;IAC3E,mEAAmE;IACnE,yEAAyE;IACzE,0EAA0E;IAC1E,uEAAuE;IACvE,2EAA2E;IAC3E,4DAA4D;IAC5D,IAAI,IAAI,CAAC,SAAS,KAAK,QAAQ,EAAE,CAAC;QAChC,QAAQ,CAAC,IAAI,CAAC;YACZ,QAAQ,EAAE,MAAM;YAChB,IAAI,EAAE,uBAAuB;YAC7B,OAAO,EAAE,yEAAyE,OAAO,8JAA8J;SACxP,CAAC,CAAC;IACL,CAAC;SAAM,IAAI,IAAI,CAAC,SAAS,KAAK,SAAS,EAAE,CAAC;QACxC,QAAQ,CAAC,IAAI,CAAC;YACZ,QAAQ,EAAE,OAAO;YACjB,IAAI,EAAE,uBAAuB;YAC7B,OAAO,EACL,6DAA6D,IAAI,CAAC,gBAAgB,IAAI,kBAAkB,8BAA8B,OAAO,0BAA0B;gBACvK,2KAA2K;SAC9K,CAAC,CAAC;IACL,CAAC;IACD,wEAAwE;IACxE,sEAAsE;IACtE,4EAA4E;IAC5E,yEAAyE;IACzE,oEAAoE;IACpE,4EAA4E;IAC5E,yEAAyE;IACzE,MAAM,MAAM,GAAG,IAAI,CAAC,SAAS,KAAK,MAAM,CAAC,CAAC,CAAC,IAAI,CAAC,MAAM,CAAC,CAAC,CAAC,IAAI,CAAC,gBAAgB,CAAC;IAC/E,IAAI,CAAC,iBAAiB,CAAC,MAAM,CAAC,QAAQ,CAAC,EAAE,CAAC;QACxC,QAAQ,CAAC,IAAI,CAAC;YACZ,QAAQ,EAAE,OAAO;YACjB,IAAI,EAAE,2BAA2B;YACjC,OAAO,EAAE,OAAO;YAChB,OAAO,EACL,GAAG,eAAe,CAAC,IAAI,CAAC,WAAW,CAAC,cAAc,MAAM,CAAC,QAAQ,sJAAsJ;gBACvN,+CAA+C,OAAO,yEAAyE;YACjI,OAAO,EAAE,MAAM,CAAC,IAAI;YACpB,IAAI,EAAE,EAAE,YAAY,EAAE,IAAI,EAAE;SAC7B,CAAC,CAAC;IACL,CAAC;IACD,4EAA4E;IAC5E,2EAA2E;IAC3E,4EAA4E;IAC5E,wEAAwE;IACxE,MAAM,WAAW,GAAG,IAAI,GAAG,CAAC,IAAI,CAAC,cAAc,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,CAAC,EAAY,EAAE,eAAe,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC;IAClG,KAAK,MAAM,CAAC,IAAI,IAAI,CAAC,QAAQ,EAAE,CAAC;QAC9B,yEAAyE;QACzE,2EAA2E;QAC3E,0DAA0D;QAC1D,IAAI,CAAC,CAAC,QAAQ;YAAE,SAAS;QACzB,IAAI,CAAC,CAAC,QAAQ,KAAK,IAAI,EAAE,CAAC;YACxB,QAAQ,CAAC,IAAI,CAAC;gBACZ,QAAQ,EAAE,MAAM;gBAChB,IAAI,EAAE,2BAA2B;gBACjC,OAAO,EAAE,CAAC,CAAC,OAAO;gBAClB,OAAO,EACL,IAAI,CAAC,CAAC,OAAO,YAAY,OAAO,MAAM,CAAC,CAAC,GAAG,CAAC,IAAI,CAAC,IAAI,CAAC,yBAAyB,CAAC,CAAC,OAAO,oDAAoD;oBAC5I,oIAAoI;gBACtI,OAAO,EAAE,CAAC,CAAC,GAAG;aACf,CAAC,CAAC;QACL,CAAC;aAAM,IAAI,CAAC,iBAAiB,CAAC,CAAC,CAAC,QAAQ,CAAC,EAAE,CAAC;YAC1C,QAAQ,CAAC,IAAI,CAAC;gBACZ,QAAQ,EAAE,MAAM;gBAChB,IAAI,EAAE,2BAA2B;gBACjC,OAAO,EAAE,CAAC,CAAC,OAAO;gBAClB,OAAO,EACL,GAAG,WAAW,CAAC,GAAG,CAAC,CAAC,CAAC,OAAO,CAAC,IAAI,YAAY,CAAC,CAAC,OAAO,EAAE,cAAc,CAAC,CAAC,QAAQ,4CAA4C,OAAO,MAAM,CAAC,CAAC,GAAG,CAAC,IAAI,CAAC,IAAI,CAAC,yEAAyE,CAAC,CAAC,OAAO,4BAA4B;gBACzQ,OAAO,EAAE,CAAC,CAAC,GAAG;aACf,CAAC,CAAC;QACL,CAAC;IACH,CAAC;IACD,OAAO,QAAQ,CAAC;AAClB,CAAC;AAED,wEAAwE;AACxE,yEAAyE;AACzE,wEAAwE;AAExE;;;;;;;;;GASG;AACH,KAAK,UAAU,iBAAiB,CAAC,GAAgB,EAAE,IAAiB;IAClE,MAAM,QAAQ,GAA6E;QACzF;YACE,EAAE,EAAE,GAAG,CAAC,OAAO;YACf,OAAO,EAAE,GAAG,CAAC,OAAO;YACpB,KAAK,EAAE,MAAM,kBAAkB,CAAC,GAAG,CAAC,OAAO,EAAE,GAAG,CAAC,OAAO,EAAE,GAAG,CAAC,KAAK,CAAC;SACrE;QACD,2EAA2E;QAC3E,qEAAqE;QACrE,GAAG,IAAI,CAAC,cAAc,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC;YACjC,EAAE,EAAE,CAAC,CAAC,EAAE;YACR,OAAO,EAAE,SAAS;YAClB,KAAK,EAAE,cAAc,CAAC,CAAC,CAAC,GAAG,CAAC;SAC7B,CAAC,CAAC;KACJ,CAAC;IACF,2EAA2E;IAC3E,yEAAyE;IACzE,gCAAgC;IAChC,MAAM,MAAM,GAAG,MAAM,OAAO,CAAC,QAAQ,EAAE,KAAK,EAAE,CAAC,EAAE,EAAE;QACjD,IAAI,CAAC;YACH,yEAAyE;YACzE,kEAAkE;YAClE,uEAAuE;YACvE,iEAAiE;YACjE,mEAAmE;YACnE,sEAAsE;YACtE,qEAAqE;YACrE,wDAAwD;YACxD,KAAK,MAAM,IAAI,IAAI,SAAS,EAAE,CAAC;gBAC7B,MAAM,IAAI,GAAG,CAAC,CAAC,KAAK,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC;gBAC/B,IAAI,UAAU,CAAC,IAAI,CAAC;oBAAE,MAAM,GAAG,CAAC,KAAK,CAAC,QAAQ,CAAC,IAAI,CAAC,CAAC;YACvD,CAAC;YACD,MAAM,GAAG,GAAG,MAAM,iBAAiB,CAAC,GAAG,CAAC,OAAO,EAAE,CAAC,CAAC,EAAE,EAAE,EAAE,OAAO,EAAE,CAAC,CAAC,OAAO,EAAE,KAAK,EAAE,GAAG,CAAC,KAAK,EAAE,CAAC,CAAC;YACjG,OAAO,GAAG;iBACP,MAAM,CACL,CAAC,CAAC,EAAE,EAAE,CACJ,CAAC,CAAC,IAAI,KAAK,eAAe;gBAC1B,CAAC,CAAC,IAAI,KAAK,eAAe;gBAC1B,CAAC,CAAC,IAAI,KAAK,uBAAuB,CACrC;gBACD,iEAAiE;gBACjE,mEAAmE;gBACnE,qCAAqC;iBACpC,GAAG,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,CAAC,OAAO,KAAK,SAAS,CAAC,CAAC,CAAC,EAAE,GAAG,CAAC,EAAE,OAAO,EAAE,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC;QACzE,CAAC;QAAC,OAAO,GAAG,EAAE,CAAC;YACb,OAAO,CAAC,iBAAiB,CAAC,SAAS,EAAE,CAAC,CAAC,EAAE,EAAE,GAAG,CAAC,CAAC,CAAC;QACnD,CAAC;IACH,CAAC,CAAC,CAAC;IACH,OAAO,MAAM,CAAC,IAAI,EAAE,CAAC;AACvB,CAAC","sourcesContent":["/**\n * The four checks behind `loam gate` — a can-i-deploy-shaped PURE QUERY over\n * evidence other commands already recorded. Nothing here executes anything,\n * writes anything, takes the docs lock, or re-derives a coherence plan: every\n * answer is either already on disk (verification.yaml, vouch stamps, the\n * commit journal) or a presence/landscape fact the enumeration carries.\n *\n * partners the landscape's direct joins, both ends graded on the ladder\n * freshness content.stale / sources.stale over the target and partners,\n * plus the one integrity error that makes freshness\n * unjudgeable (frontmatter.malformed) and a refusing decode\n * of the spec bytes themselves\n * verification verificationState per active feature touching the service\n * (its own module, `./verification.ts`)\n * interrupted the `.loam-commit` journal — the one repo state that poisons\n * every other answer above\n *\n * Every `gate.*` code is spelled as a literal `code:` property at its emit\n * site, never through a code-taking factory parameter — verifyStep\n * (core/status/verification.ts) records that test/codes-drift.test.ts's\n * collector is blind to the parameter form, and a refactor toward a factory\n * would silently undocument the family.\n */\nimport { existsSync } from \"node:fs\";\nimport { inOrder } from \"../kernel/concurrency.js\";\nimport { serviceProvenance } from \"../provenance/findings.js\";\nimport { interruptedCommitFinding } from \"../staging/recovery/finding.js\";\nimport { servicePathsAt, SPEC_AXES, type ServicePaths } from \"../repo/paths.js\";\nimport { serviceTreePath } from \"../kernel/ids/dirs.js\";\nimport { locateServicePaths } from \"../repo/service-target.js\";\nimport type { Finding } from \"../vocabulary/report.js\";\nimport type { FleetContext } from \"../fleet-context.js\";\nimport type { DocsDir } from \"../kernel/ids/dirs.js\";\nimport type { RawServiceId } from \"../kernel/ids/service.js\";\nimport { atLeastDocumented, partnerScan, type PartnerScan } from \"./partners.js\";\nimport { verificationCheck } from \"./verification.js\";\nimport { unreadableSubject, type GateCheck, type GateReport } from \"./report.js\";\n\nexport interface GateRequest {\n docsDir: DocsDir;\n /** The service being deployed — a name the enumeration approved. */\n service: RawServiceId;\n /**\n * The repository loam is standing in, passed ONLY when the gated service is\n * this repo's own binding — validate's `repoOf` rule: `sources` are paths\n * into a service's own repository, so `sources.stale` is honestly checkable\n * only there, while `content.stale` needs nothing but the document.\n */\n repoDir?: string;\n fleet: FleetContext;\n}\n\nexport async function gateReport(req: GateRequest): Promise<GateReport> {\n // The scan first, alone: freshness walks the partner set it derives. The\n // three remaining checks are independent of each other — different files,\n // memoized reads — so they run concurrently; only the scan must be ordered.\n const scan = await partnerScan({ docsDir: req.docsDir, service: req.service, fleet: req.fleet });\n const [freshness, verification, interrupted] = await Promise.all([\n freshnessFindings(req, scan),\n verificationCheck({ docsDir: req.docsDir, service: req.service, fleet: req.fleet }),\n interruptedCommitFinding(req.docsDir),\n ]);\n const checks: GateCheck[] = [\n { check: \"partners\", findings: partnersFindings(scan, req.service) },\n { check: \"freshness\", findings: freshness },\n { check: \"verification\", findings: verification.findings },\n { check: \"interrupted\", findings: interrupted === null ? [] : [interrupted] },\n ];\n return {\n service: req.service,\n landscape: scan.landscape,\n partners: scan.partners,\n features: verification.features,\n checks,\n };\n}\n\n/* ------------------------------------------------------------------ */\n/* Check 1: partners */\n/* ------------------------------------------------------------------ */\n\nfunction partnersFindings(scan: PartnerScan, service: string): Finding[] {\n const findings: Finding[] = [];\n // \"Could not look\" must never read as \"no partners\" — the docsRepoReady\n // doctrine one level down. `validate --all` owns the diagnosis of the\n // landscape itself; this code only refuses the false silence. The two ways\n // of not looking are graded apart, the way `landscape.missing` and\n // `landscape.invalid` are: ABSENT is a warning — a repo before its first\n // adopt legitimately has no map yet — but a map that EXISTS and cannot be\n // parsed or read is an ERROR, because `validate --all` fails that repo\n // (`landscape.invalid`) and a deploy gate that passed where the fleet gate\n // fails would be the quieter of two contradictory verdicts.\n if (scan.landscape === \"absent\") {\n findings.push({\n severity: \"warn\",\n code: \"gate.partners-unknown\",\n message: `architecture/landscape.likec4 does not exist, so the partner set for '${service}' could not be derived — an absent map means nobody could look, not that nothing joins this service. \\`loam validate --all\\` diagnoses the landscape itself.`,\n });\n } else if (scan.landscape === \"invalid\") {\n findings.push({\n severity: \"error\",\n code: \"gate.partners-unknown\",\n message:\n `architecture/landscape.likec4 exists but cannot be used — ${scan.landscapeProblem ?? \"it did not parse\"} — so the partner set for '${service}' could not be derived. ` +\n `A broken map means nobody could look, not that nothing joins this service, and \\`loam validate --all\\` fails this repo for the same file. Fix the landscape, then re-run.`,\n });\n }\n // The rung depends on the landscape only through the api question (does\n // anything call this service?). With no readable map that question is\n // unanswerable, and the fail-closed grading — `landscapeEvidence`: no proof\n // means an API is expected — would convict a worker or UI for a contract\n // nobody may be owed. One unanswerable fact yields ONE finding (the\n // landscape's own, above), so under an unreadable map this error fires only\n // when the service is below `documented` REGARDLESS of the api question.\n const target = scan.landscape === \"read\" ? scan.target : scan.targetWithoutApi;\n if (!atLeastDocumented(target.maturity)) {\n findings.push({\n severity: \"error\",\n code: \"gate.service-undocumented\",\n subject: service,\n message:\n `${serviceTreePath(scan.targetEntry)}/ sits at '${target.maturity}' on the adoption ladder — below 'documented', the docs cannot say what its joins even are, so no recorded evidence can answer the deploy question. ` +\n `Author the baseline (\\`loam adopt --service ${service}\\` briefs it); this check passes once the required artifact set exists.`,\n details: target.gaps,\n text: { detailPrefix: \"- \" },\n });\n }\n // The partners that HAVE a directory, by id: their findings name it, and it\n // is `services/<subsystem>/<id>/` for a filed one. The no-directory branch\n // below keeps the bare `services/<id>/` spelling deliberately — there is no\n // directory to name, and the unfiled form is what the fix would create.\n const partnerPath = new Map(scan.partnerEntries.map((e) => [e.id as string, serviceTreePath(e)]));\n for (const p of scan.partners) {\n // An `#external` partner is somebody else's system: its side of the join\n // is unrecorded ON PURPOSE, the same exemption the landscape census grants\n // (`landscape.service-undocumented` skips #external too).\n if (p.external) continue;\n if (p.maturity === null) {\n findings.push({\n severity: \"warn\",\n code: \"gate.partner-undocumented\",\n subject: p.service,\n message:\n `'${p.service}' joins '${service}' (${p.via.join(\", \")}) but has no services/${p.service}/ directory — its side of the join is unrecorded. ` +\n `Adopt it, bind its landscape element to the right service with metadata { service '<id>' }, or tag it #external if it is not ours.`,\n details: p.via,\n });\n } else if (!atLeastDocumented(p.maturity)) {\n findings.push({\n severity: \"warn\",\n code: \"gate.partner-undocumented\",\n subject: p.service,\n message:\n `${partnerPath.get(p.service) ?? `services/${p.service}`}/ sits at '${p.maturity}' — below 'documented' — while it joins '${service}' (${p.via.join(\", \")}): its side of the join is not fully recorded. \\`loam adopt --service ${p.service}\\` briefs what is missing.`,\n details: p.via,\n });\n }\n }\n return findings;\n}\n\n/* ------------------------------------------------------------------ */\n/* Check 2: freshness */\n/* ------------------------------------------------------------------ */\n\n/**\n * `serviceProvenance` filtered to the two staleness codes plus the ONE\n * integrity error that makes freshness unjudgeable: `frontmatter.malformed`.\n * A header YAML refuses to parse cannot certify a `content_digest`, and\n * dropping that error read \"cannot judge\" as \"nothing stale\" — the exact\n * fail-open a deploy gate must not have. Everything else provenance grades\n * (missing owners, unstamped sources, absent frontmatter) is `validate`'s\n * report to make — a deploy gate that repeated it would drown the findings a\n * pipeline can actually act on at deploy time.\n */\nasync function freshnessFindings(req: GateRequest, scan: PartnerScan): Promise<Finding[]> {\n const subjects: { id: RawServiceId; repoDir: string | undefined; paths: ServicePaths }[] = [\n {\n id: req.service,\n repoDir: req.repoDir,\n paths: await locateServicePaths(req.docsDir, req.service, req.fleet),\n },\n // Only the DOCUMENTED partners: a partner with no directory has nothing to\n // grade, and already carries gate.partner-undocumented from check 1.\n ...scan.partnerEntries.map((e) => ({\n id: e.id,\n repoDir: undefined,\n paths: servicePathsAt(e.dir),\n })),\n ];\n // Independent per-subject reads through the shared memo; inOrder keeps the\n // findings in subject order (target first, partners sorted) so identical\n // state yields identical bytes.\n const graded = await inOrder(subjects, async (s) => {\n try {\n // The integrity gate serviceProvenance cannot give: its own reads decode\n // with 'utf8', which silently turns UTF-16 bytes into replacement\n // characters — the file then grades as having no frontmatter at all (a\n // warning this check does not retain), and mangled bytes read as\n // \"nothing stale\". So both spec axes are decoded FIRST through the\n // invocation's read index, whose decoder refuses non-UTF-8 naming the\n // file (NotUtf8DocumentError spells `path` the way Node does, so the\n // containment below names it without knowing the type).\n for (const axis of SPEC_AXES) {\n const path = s.paths[axis.key];\n if (existsSync(path)) await req.fleet.readText(path);\n }\n const all = await serviceProvenance(req.docsDir, s.id, { repoDir: s.repoDir, fleet: req.fleet });\n return all\n .filter(\n (f) =>\n f.code === \"content.stale\" ||\n f.code === \"sources.stale\" ||\n f.code === \"frontmatter.malformed\",\n )\n // The staleness emitters name the file in the message but set no\n // subject; the payload's per-service attribution should not make a\n // caller parse it back out of prose.\n .map((f) => (f.subject === undefined ? { ...f, subject: s.id } : f));\n } catch (err) {\n return [unreadableSubject(\"service\", s.id, err)];\n }\n });\n return graded.flat();\n}\n"]}
1
+ {"version":3,"file":"checks.js","sourceRoot":"","sources":["../../../src/core/gate/checks.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * The four checks behind `loam gate` — a can-i-deploy-shaped PURE QUERY over\n * evidence other commands already recorded. Nothing here executes anything,\n * writes anything, takes the docs lock, or re-derives a coherence plan: every\n * answer is either already on disk (verification.yaml, vouch stamps, the\n * commit journal) or a presence/landscape fact the enumeration carries.\n *\n * partners the landscape's direct joins, both ends graded on the ladder\n * freshness content.stale / sources.stale over the target and partners,\n * plus the one integrity error that makes freshness\n * unjudgeable (frontmatter.malformed) and a refusing decode\n * of the spec bytes themselves\n * verification verificationState per active feature touching the service\n * (its own module, `./verification.ts`)\n * interrupted the `.loam-commit` journal — the one repo state that poisons\n * every other answer above\n *\n * Every `gate.*` code is spelled as a literal `code:` property at its emit\n * site, never through a code-taking factory parameter — verifyStep\n * (core/status/verification.ts) records that test/codes-drift.test.ts's\n * collector is blind to the parameter form, and a refactor toward a factory\n * would silently undocument the family.\n */\nimport { existsSync } from \"node:fs\";\nimport { inOrder } from \"../kernel/concurrency.js\";\nimport { serviceProvenance } from \"../provenance/findings.js\";\nimport { interruptedCommitFinding } from \"../staging/recovery/finding.js\";\nimport { servicePathsAt, SPEC_AXES, type ServicePaths } from \"../repo/paths.js\";\nimport { serviceTreePath } from \"../kernel/ids/dirs.js\";\nimport { locateServicePaths } from \"../repo/service-target.js\";\nimport type { Finding } from \"../vocabulary/report.js\";\nimport type { FleetContext } from \"../fleet-context.js\";\nimport type { DocsDir } from \"../kernel/ids/dirs.js\";\nimport type { RawServiceId } from \"../kernel/ids/service.js\";\nimport { atLeastDocumented, partnerScan, type PartnerScan } from \"./partners.js\";\nimport { verificationCheck } from \"./verification.js\";\nimport { unreadableSubject, type GateCheck, type GateReport } from \"./report.js\";\n\nexport interface GateRequest {\n docsDir: DocsDir;\n /** The service being deployed — a name the enumeration approved. */\n service: RawServiceId;\n /**\n * The repository loam is standing in, passed ONLY when the gated service is\n * this repo's own binding — validate's `repoOf` rule: `sources` are paths\n * into a service's own repository, so `sources.stale` is honestly checkable\n * only there, while `content.stale` needs nothing but the document.\n */\n repoDir?: string;\n fleet: FleetContext;\n}\n\nexport async function gateReport(req: GateRequest): Promise<GateReport> {\n // The scan first, alone: freshness walks the partner set it derives. The\n // three remaining checks are independent of each other — different files,\n // memoized reads — so they run concurrently; only the scan must be ordered.\n const scan = await partnerScan({ docsDir: req.docsDir, service: req.service, fleet: req.fleet });\n const [freshness, verification, interrupted] = await Promise.all([\n freshnessFindings(req, scan),\n verificationCheck({ docsDir: req.docsDir, service: req.service, fleet: req.fleet }),\n interruptedCommitFinding(req.docsDir),\n ]);\n const checks: GateCheck[] = [\n { check: \"partners\", findings: partnersFindings(scan, req.service) },\n { check: \"freshness\", findings: freshness },\n { check: \"verification\", findings: verification.findings },\n { check: \"interrupted\", findings: interrupted === null ? [] : [interrupted] },\n ];\n return {\n service: req.service,\n landscape: scan.landscape,\n partners: scan.partners,\n features: verification.features,\n checks,\n };\n}\n\n/* ------------------------------------------------------------------ */\n/* Check 1: partners */\n/* ------------------------------------------------------------------ */\n\nfunction partnersFindings(scan: PartnerScan, service: string): Finding[] {\n const findings: Finding[] = [];\n // \"Could not look\" must never read as \"no partners\" — the docsRepoReady\n // doctrine one level down. `validate --all` owns the diagnosis of the\n // landscape itself; this code only refuses the false silence. The two ways\n // of not looking are graded apart, the way `landscape.missing` and\n // `landscape.invalid` are: ABSENT is a warning — a repo before its first\n // adopt legitimately has no map yet — but a map that EXISTS and cannot be\n // parsed or read is an ERROR, because `validate --all` fails that repo\n // (`landscape.invalid`) and a deploy gate that passed where the fleet gate\n // fails would be the quieter of two contradictory verdicts.\n if (scan.landscape === \"absent\") {\n findings.push({\n severity: \"warn\",\n code: \"gate.partners-unknown\",\n message: `architecture/landscape.likec4 does not exist, so the partner set for '${service}' could not be derived — an absent map means nobody could look, not that nothing joins this service. \\`loam validate --all\\` diagnoses the landscape itself.`,\n });\n } else if (scan.landscape === \"invalid\") {\n findings.push({\n severity: \"error\",\n code: \"gate.partners-unknown\",\n message:\n `architecture/landscape.likec4 exists but cannot be used — ${scan.landscapeProblem ?? \"it did not parse\"} — so the partner set for '${service}' could not be derived. ` +\n `A broken map means nobody could look, not that nothing joins this service, and \\`loam validate --all\\` fails this repo for the same file. Fix the landscape, then re-run.`,\n });\n }\n // The rung depends on the landscape only through the api question (does\n // anything call this service?). With no readable map that question is\n // unanswerable, and the fail-closed grading — `landscapeEvidence`: no proof\n // means an API is expected — would convict a worker or UI for a contract\n // nobody may be owed. One unanswerable fact yields ONE finding (the\n // landscape's own, above), so under an unreadable map this error fires only\n // when the service is below `documented` REGARDLESS of the api question.\n const target = scan.landscape === \"read\" ? scan.target : scan.targetWithoutApi;\n if (!atLeastDocumented(target.maturity)) {\n findings.push({\n severity: \"error\",\n code: \"gate.service-undocumented\",\n subject: service,\n message:\n `${serviceTreePath(scan.targetEntry)}/ sits at '${target.maturity}' on the adoption ladder — below 'documented', the docs cannot say what its joins even are, so no recorded evidence can answer the deploy question. ` +\n `Author the baseline (\\`loam adopt --service ${service}\\` briefs it); this check passes once the required artifact set exists.`,\n details: target.gaps,\n text: { detailPrefix: \"- \" },\n });\n }\n // The partners that HAVE a directory, by id: their findings name it, and it\n // is `services/<subsystem>/<id>/` for a filed one. The no-directory branch\n // below keeps the bare `services/<id>/` spelling deliberately — there is no\n // directory to name, and the unfiled form is what the fix would create.\n const partnerPath = new Map(scan.partnerEntries.map((e) => [e.id as string, serviceTreePath(e)]));\n for (const p of scan.partners) {\n // An `#external` partner is somebody else's system: its side of the join\n // is unrecorded ON PURPOSE, the same exemption the landscape census grants\n // (`landscape.service-undocumented` skips #external too). The fleet's own\n // infrastructure — a `database`, `queue`, `topic`, `cache` or `bucket` — is\n // not a partner at all and never reaches this loop: `./partners.ts` drops it\n // from the SET, beside the actors, because the three remedies below are all\n // false of a store the fleet runs.\n if (p.external) continue;\n if (p.maturity === null) {\n findings.push({\n severity: \"warn\",\n code: \"gate.partner-undocumented\",\n subject: p.service,\n message:\n `'${p.service}' joins '${service}' (${p.via.join(\", \")}) but has no services/${p.service}/ directory — its side of the join is unrecorded. ` +\n `Adopt it, bind its landscape element to the right service with metadata { service '<id>' }, or tag it #external if it is not ours.`,\n details: p.via,\n });\n } else if (!atLeastDocumented(p.maturity)) {\n findings.push({\n severity: \"warn\",\n code: \"gate.partner-undocumented\",\n subject: p.service,\n message:\n `${partnerPath.get(p.service) ?? `services/${p.service}`}/ sits at '${p.maturity}' — below 'documented' — while it joins '${service}' (${p.via.join(\", \")}): its side of the join is not fully recorded. \\`loam adopt --service ${p.service}\\` briefs what is missing.`,\n details: p.via,\n });\n }\n }\n return findings;\n}\n\n/* ------------------------------------------------------------------ */\n/* Check 2: freshness */\n/* ------------------------------------------------------------------ */\n\n/**\n * `serviceProvenance` filtered to the two staleness codes plus the ONE\n * integrity error that makes freshness unjudgeable: `frontmatter.malformed`.\n * A header YAML refuses to parse cannot certify a `content_digest`, and\n * dropping that error read \"cannot judge\" as \"nothing stale\" — the exact\n * fail-open a deploy gate must not have. Everything else provenance grades\n * (missing owners, unstamped sources, absent frontmatter) is `validate`'s\n * report to make — a deploy gate that repeated it would drown the findings a\n * pipeline can actually act on at deploy time.\n */\nasync function freshnessFindings(req: GateRequest, scan: PartnerScan): Promise<Finding[]> {\n const subjects: { id: RawServiceId; repoDir: string | undefined; paths: ServicePaths }[] = [\n {\n id: req.service,\n repoDir: req.repoDir,\n paths: await locateServicePaths(req.docsDir, req.service, req.fleet),\n },\n // Only the DOCUMENTED partners: a partner with no directory has nothing to\n // grade, and already carries gate.partner-undocumented from check 1.\n ...scan.partnerEntries.map((e) => ({\n id: e.id,\n repoDir: undefined,\n paths: servicePathsAt(e.dir),\n })),\n ];\n // Independent per-subject reads through the shared memo; inOrder keeps the\n // findings in subject order (target first, partners sorted) so identical\n // state yields identical bytes.\n const graded = await inOrder(subjects, async (s) => {\n try {\n // The integrity gate serviceProvenance cannot give: its own reads decode\n // with 'utf8', which silently turns UTF-16 bytes into replacement\n // characters — the file then grades as having no frontmatter at all (a\n // warning this check does not retain), and mangled bytes read as\n // \"nothing stale\". So both spec axes are decoded FIRST through the\n // invocation's read index, whose decoder refuses non-UTF-8 naming the\n // file (NotUtf8DocumentError spells `path` the way Node does, so the\n // containment below names it without knowing the type).\n for (const axis of SPEC_AXES) {\n const path = s.paths[axis.key];\n if (existsSync(path)) await req.fleet.readText(path);\n }\n const all = await serviceProvenance(req.docsDir, s.id, { repoDir: s.repoDir, fleet: req.fleet });\n return all\n .filter(\n (f) =>\n f.code === \"content.stale\" ||\n f.code === \"sources.stale\" ||\n f.code === \"frontmatter.malformed\",\n )\n // The staleness emitters name the file in the message but set no\n // subject; the payload's per-service attribution should not make a\n // caller parse it back out of prose.\n .map((f) => (f.subject === undefined ? { ...f, subject: s.id } : f));\n } catch (err) {\n return [unreadableSubject(\"service\", s.id, err)];\n }\n });\n return graded.flat();\n}\n"]}
@@ -15,7 +15,7 @@
15
15
  import { existsSync } from "node:fs";
16
16
  import {} from "../c4/likec4.js";
17
17
  import { serviceResolver } from "../c4/resolve/service.js";
18
- import { ACTOR_KINDS, EXTERNAL_TAG, landscapeEvidence, maturityGaps, serviceMaturity, MATURITY_LADDER, } from "../vocabulary/maturity.js";
18
+ import { ACTOR_KINDS, EXTERNAL_TAG, INFRA_KINDS, landscapeEvidence, maturityGaps, serviceMaturity, MATURITY_LADDER, } from "../vocabulary/maturity.js";
19
19
  import { compareIds } from "../repo/entries.js";
20
20
  import { landscapePath, servicePathsAt } from "../repo/paths.js";
21
21
  import { enumeratedServices } from "../repo/service-target.js";
@@ -25,19 +25,36 @@ export function atLeastDocumented(m) {
25
25
  return MATURITY_LADDER.indexOf(m) >= MATURITY_LADDER.indexOf("documented");
26
26
  }
27
27
  /**
28
- * What the map says about one edge END: does it stand for a person, and is it
29
- * tagged `#external`? Walked up the ancestor chain because both facts are
30
- * usually written on the containing system while the edge is drawn from a
31
- * nested element — but the walk STOPS at the first element that stands for a
32
- * service (an explicit binding or a title naming a real directory,
33
- * `serviceResolver`'s own first two rungs, checked after that element's own
34
- * facts are read). What sits above the service boundary is a grouping, and a
35
- * grouping's `#external` is the grouping's claim, not the service's: without
36
- * the stop, an adopted service drawn inside an `#external` zone was exempted
37
- * from `gate.partner-undocumented` and its real rung hidden.
28
+ * What the map says about one edge END: does it stand for a person, is it
29
+ * INFRASTRUCTURE, and is it tagged `#external`? Walked up the ancestor chain
30
+ * because all three facts are usually written on the containing system while
31
+ * the edge is drawn from a nested element — but the walk STOPS at the first
32
+ * element that stands for a service (an explicit binding or a title naming a
33
+ * real directory, `serviceResolver`'s own first two rungs, checked after that
34
+ * element's own facts are read). What sits above the service boundary is a
35
+ * grouping, and a grouping's `#external` is the grouping's claim, not the
36
+ * service's: without the stop, an adopted service drawn inside an `#external`
37
+ * zone was exempted from `gate.partner-undocumented` and its real rung hidden.
38
+ *
39
+ * `infra` is read only off an element that stands for NO service AND sits
40
+ * inside none, which is the asymmetry the `standsFor` local exists for. That is
41
+ * `commands/validate/fleet/census.ts` in two steps and in its order:
42
+ * `serviceLevelElements` drops everything drawn inside a service BEFORE
43
+ * `drawnSystems` ever applies the kind test, so a service's own interior never
44
+ * reaches it. Both halves are load-bearing. A `database` bound with
45
+ * `metadata { service '<id>' }` is an adopted service that happens to be drawn
46
+ * as a store — its side of the join IS recorded, and dropping it would hide a
47
+ * real partner's rung. And a store drawn INSIDE its owner — `orderService.db`,
48
+ * the ordinary way a team draws a private datastore — is that service's
49
+ * interior: reading the kind off it convicted the WHOLE endpoint as
50
+ * infrastructure, and `partnerScan` then dropped order-service from the partner
51
+ * set, so a service this gate exists to grade vanished because of how somebody
52
+ * drew its storage. The kind is the fallback for an element nothing else
53
+ * answers for, never the override.
38
54
  */
39
55
  function endFacts(byId, known, id) {
40
56
  let actor = false;
57
+ let infra = false;
41
58
  let external = false;
42
59
  // The id itself, then each dotted ancestor — the same walk `serviceResolver`
43
60
  // resolves through, out of the one module that spells it
@@ -46,14 +63,26 @@ function endFacts(byId, known, id) {
46
63
  const e = byId.get(candidate);
47
64
  if (e === undefined)
48
65
  continue;
66
+ const standsFor = e.service !== undefined || known.has(e.title);
49
67
  if (ACTOR_KINDS.has(e.kind.toLowerCase()))
50
68
  actor = true;
69
+ if (!standsFor && INFRA_KINDS.has(e.kind.toLowerCase()))
70
+ infra = true;
51
71
  if (e.tags.includes(EXTERNAL_TAG))
52
72
  external = true;
53
- if (e.service !== undefined || known.has(e.title))
73
+ if (standsFor) {
74
+ // Everything the walk has visited so far was drawn INSIDE this service,
75
+ // so nothing visited so far was service-LEVEL and no kind read off it
76
+ // answers for a service — `serviceLevelElements` filtering ahead of the
77
+ // kind test, spelled as a walk. `actor` and `external` are deliberately
78
+ // left standing: a person or an `#external` tag written on a container is
79
+ // still a claim about this endpoint, while the store shape is a claim
80
+ // about the owner's interior.
81
+ infra = false;
54
82
  break;
83
+ }
55
84
  }
56
- return { actor, external };
85
+ return { actor, infra, external };
57
86
  }
58
87
  /** How one edge joins the two ends, in the words the finding and the payload both use. */
59
88
  function viaOf(r) {
@@ -158,14 +187,28 @@ export async function partnerScan(req) {
158
187
  continue;
159
188
  if (target === service) {
160
189
  const facts = endFacts(byId, known, r.source);
161
- // A person is never a deploy participant: the actor exemption every
162
- // landscape census applies (drawnSystems, commands/validate/fleet/census.ts).
163
- if (!facts.actor)
190
+ // A person is never a deploy participant, and neither is the fleet's own
191
+ // infrastructure: the two kind exemptions every landscape census applies
192
+ // (drawnSystems, commands/validate/fleet/census.ts), spelled here off the
193
+ // same two sets so a gate and a validate run cannot disagree about what a
194
+ // partner IS.
195
+ //
196
+ // The infra half is the newer one, and it had a measured cost. A
197
+ // `database` the fleet owns and draws at fleet level joined every service
198
+ // that reads it, with `maturity: null` because there is no
199
+ // `services/<title>/` — so `gate.partner-undocumented` told the deployer
200
+ // to "adopt it, bind it … or tag it #external", three remedies none of
201
+ // which is true of a Postgres (measured on a one-service fixture,
202
+ // 2026-09-07). It is dropped from the partner SET rather than exempted
203
+ // from the finding, which is the actor precedent exactly: `partners` is
204
+ // the list of counterparties whose DOCS the gate grades, and a store has
205
+ // none to grade.
206
+ if (!facts.actor && !facts.infra)
164
207
  join(source, "consumer", r, facts.external);
165
208
  }
166
209
  else if (source === service) {
167
210
  const facts = endFacts(byId, known, r.target);
168
- if (!facts.actor)
211
+ if (!facts.actor && !facts.infra)
169
212
  join(target, "provider", r, facts.external);
170
213
  }
171
214
  }
@@ -1 +1 @@
1
- {"version":3,"file":"partners.js","sourceRoot":"","sources":["../../../src/core/gate/partners.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * The direct join partners of one service, derived from the LIVING landscape,\n * and the maturity grading of everything on either end of a join.\n *\n * The landscape is the only place the fleet's joins are drawn, so the partner\n * set is exactly the edges whose resolved source or target is the gated\n * service — resolved through `serviceResolver` with the ENUMERATED fleet\n * riding in, the same ride that keeps container-modelling landscapes attached\n * everywhere else (an edge into `paymentService.api` is an edge into the\n * service that owns it). A landscape that is absent or does not parse yields\n * NO partner set, never an empty one: `landscape` says which, and the check\n * layer turns \"could not look\" into `gate.partners-unknown` rather than\n * letting it read as \"no partners\".\n */\nimport { existsSync } from \"node:fs\";\nimport { type Elem, type Rel } from \"../c4/likec4.js\";\nimport { serviceResolver } from \"../c4/resolve/service.js\";\nimport {\n ACTOR_KINDS,\n EXTERNAL_TAG,\n landscapeEvidence,\n maturityGaps,\n serviceMaturity,\n MATURITY_LADDER,\n type Maturity,\n} from \"../vocabulary/maturity.js\";\nimport { compareIds, type ServiceEntry } from \"../repo/entries.js\";\nimport { landscapePath, servicePathsAt } from \"../repo/paths.js\";\nimport { enumeratedServices } from \"../repo/service-target.js\";\nimport type { FleetContext } from \"../fleet-context.js\";\nimport { ancestorIds } from \"../kernel/ids/fqn/ancestors.js\";\nimport type { DocsDir } from \"../kernel/ids/dirs.js\";\nimport type { RawServiceId } from \"../kernel/ids/service.js\";\nimport type { GatePartner, LandscapeRead, PartnerRole } from \"./report.js\";\n\nexport interface PartnerScanRequest {\n docsDir: DocsDir;\n /** The gated service — a name the enumeration approved (`resolveServiceTarget`). */\n service: RawServiceId;\n fleet: FleetContext;\n}\n\n/** One service graded on the adoption ladder, with what the next rung wants. */\nexport interface GradedService {\n maturity: Maturity;\n gaps: string[];\n}\n\nexport interface PartnerScan {\n landscape: LandscapeRead;\n /**\n * Why the landscape graded `invalid` — the parse-error tally or the read\n * failure's own words. Null on `read` and `absent`; the finding that fails\n * the gate must say what broke, not merely that something did.\n */\n landscapeProblem: string | null;\n /**\n * The gated service's own rung, graded under the landscape's evidence —\n * `gate.service-undocumented`'s subject. Fail-closed on the api question:\n * with no readable map, `landscapeEvidence` assumes an API is expected.\n */\n target: GradedService;\n /**\n * The same service graded with NO api expected of it — the reading that\n * does not depend on the landscape at all. The check layer answers from\n * THIS one when the map is absent or broken: the api question is then\n * unanswerable, and one unanswerable fact must not convict twice (the\n * landscape already carries its own finding).\n */\n targetWithoutApi: GradedService;\n /**\n * The gated service's own enumerated entry — carried so a finding can name\n * the directory it actually occupies. `services/<id>/` is right only for an\n * unfiled fleet, and a check that sends a reader to a directory they cannot\n * open has answered worse than not at all.\n */\n targetEntry: ServiceEntry;\n partners: GatePartner[];\n /**\n * The enumerated entries behind the partners that HAVE a `services/`\n * directory, in `partners` order — what the freshness check walks. Kept as\n * entries rather than names so the ids that travel on carry the\n * enumeration's provenance (`RawServiceId`), never a document's.\n */\n partnerEntries: ServiceEntry[];\n}\n\n/** Is this rung at or above `documented` — the fixed threshold check 1 grades against? */\nexport function atLeastDocumented(m: Maturity): boolean {\n return MATURITY_LADDER.indexOf(m) >= MATURITY_LADDER.indexOf(\"documented\");\n}\n\n/**\n * What the map says about one edge END: does it stand for a person, and is it\n * tagged `#external`? Walked up the ancestor chain because both facts are\n * usually written on the containing system while the edge is drawn from a\n * nested element — but the walk STOPS at the first element that stands for a\n * service (an explicit binding or a title naming a real directory,\n * `serviceResolver`'s own first two rungs, checked after that element's own\n * facts are read). What sits above the service boundary is a grouping, and a\n * grouping's `#external` is the grouping's claim, not the service's: without\n * the stop, an adopted service drawn inside an `#external` zone was exempted\n * from `gate.partner-undocumented` and its real rung hidden.\n */\nfunction endFacts(\n byId: Map<string, Elem>,\n known: ReadonlySet<string>,\n id: string,\n): { actor: boolean; external: boolean } {\n let actor = false;\n let external = false;\n // The id itself, then each dotted ancestor — the same walk `serviceResolver`\n // resolves through, out of the one module that spells it\n // (`core/kernel/ids/fqn/ancestors.ts`).\n for (const candidate of ancestorIds(id)) {\n const e = byId.get(candidate);\n if (e === undefined) continue;\n if (ACTOR_KINDS.has(e.kind.toLowerCase())) actor = true;\n if (e.tags.includes(EXTERNAL_TAG)) external = true;\n if (e.service !== undefined || known.has(e.title)) break;\n }\n return { actor, external };\n}\n\n/** How one edge joins the two ends, in the words the finding and the payload both use. */\nfunction viaOf(r: Rel): string[] {\n const typed = [\n ...(r.op === undefined ? [] : [`operation ${r.op}`]),\n ...(r.publishes === undefined ? [] : [`message ${r.publishes}`]),\n ...(r.consumes === undefined ? [] : [`message ${r.consumes}`]),\n ];\n if (typed.length > 0) return typed;\n return [r.title === undefined ? \"untyped edge\" : `edge '${r.title}'`];\n}\n\ninterface PartnerAccumulator {\n consumer: boolean;\n provider: boolean;\n external: boolean;\n via: Set<string>;\n}\n\nexport async function partnerScan(req: PartnerScanRequest): Promise<PartnerScan> {\n const { docsDir, service, fleet } = req;\n // The same memoized enumeration the command's resolveServiceTarget already\n // performed on this FleetContext — one walk answers both questions.\n const entries = await enumeratedServices(docsDir, fleet);\n // Keyed as plain strings on purpose: the lookups below arrive from the\n // resolver, whose names are document-asserted, and only a hit proves a\n // directory. The VALUES keep the enumeration's provenance.\n const entryById = new Map<string, ServiceEntry>(entries.map((e) => [e.id, e]));\n const known: ReadonlySet<string> = new Set(entries.map((e) => e.id));\n\n const lp = landscapePath(docsDir);\n let landscape: LandscapeRead = \"absent\";\n let landscapeProblem: string | null = null;\n let elements: Elem[] = [];\n let relationships: Rel[] = [];\n if (existsSync(lp)) {\n try {\n const doc = await fleet.loadLikeC4(lp);\n if (doc.errors.length > 0) {\n landscape = \"invalid\";\n const first = doc.errors[0];\n landscapeProblem =\n `${doc.errors.length} parse error(s)` +\n (first === undefined ? \"\" : ` — first: ${first.message}`);\n } else {\n landscape = \"read\";\n elements = doc.elements;\n relationships = doc.relationships;\n }\n } catch (err) {\n // A landscape that could not be READ, graded as one that did not PARSE —\n // commands/validate/fleet/load.ts's containment doctrine, spelled here\n // because core cannot import it: \"could not be read\" and \"did not parse\"\n // have the same consequence (nothing may be concluded from this file).\n // The reason travels into the finding, message-preserving as\n // `unreadableLandscape` is; the read error must not kill the run — the\n // other three checks still have answers.\n landscape = \"invalid\";\n landscapeProblem = err instanceof Error ? err.message : String(err);\n }\n }\n const parses = landscape === \"read\";\n const svcOf: (id: string) => string = parses\n ? serviceResolver(elements, known)\n : (id: string) => id;\n const elementIds = elements.map((e) => e.id);\n\n // One service's ladder rung. `landscapeEvidence` is the ONE shared spelling\n // of the positive-evidence apiExpected rule (core/vocabulary/maturity.ts);\n // its own docblock warns against calling it per service inside a FLEET loop,\n // and this loop is not one — it runs over the gated service and its direct\n // partners, a handful, never the enumeration.\n const gradeWith = (entry: ServiceEntry, apiExpected: boolean): GradedService => {\n const input = {\n entry,\n archSpec: existsSync(servicePathsAt(entry.dir).archSpec),\n apiExpected,\n };\n return { maturity: serviceMaturity(input), gaps: maturityGaps(input) };\n };\n const grade = (entry: ServiceEntry): GradedService =>\n gradeWith(\n entry,\n landscapeEvidence({ id: entry.id, parses, relationships, elementIds, svcOf }).apiExpected,\n );\n\n // Unreachable by construction: the command layer resolved `service` against\n // the same memoized enumeration this scan just read, and refused\n // `unknown-service` when no directory answered. Failing closed here beats\n // grading a service nobody enumerated as if its absence were a rung.\n const targetEntry = entryById.get(service);\n if (targetEntry === undefined) {\n throw new Error(`gate: '${service}' vanished from the enumeration between resolve and scan`);\n }\n\n const byId = new Map(elements.map((e) => [e.id, e]));\n const acc = new Map<string, PartnerAccumulator>();\n const join = (name: string, role: PartnerRole, r: Rel, external: boolean): void => {\n const slot = acc.get(name) ?? { consumer: false, provider: false, external: false, via: new Set<string>() };\n if (role === \"consumer\") slot.consumer = true;\n else slot.provider = true;\n slot.external = slot.external || external;\n for (const v of viaOf(r)) slot.via.add(v);\n acc.set(name, slot);\n };\n for (const r of relationships) {\n const source = svcOf(r.source);\n const target = svcOf(r.target);\n // A self-edge (both ends resolve to the gated service — its own containers\n // talking to each other) names no partner.\n if (source === target) continue;\n if (target === service) {\n const facts = endFacts(byId, known, r.source);\n // A person is never a deploy participant: the actor exemption every\n // landscape census applies (drawnSystems, commands/validate/fleet/census.ts).\n if (!facts.actor) join(source, \"consumer\", r, facts.external);\n } else if (source === service) {\n const facts = endFacts(byId, known, r.target);\n if (!facts.actor) join(target, \"provider\", r, facts.external);\n }\n }\n\n // Sorted by id so identical state yields identical bytes — the accumulator's\n // order is the landscape's edge order, which a reformat may shuffle.\n const joined = [...acc.entries()].sort((a, b) => compareIds(a[0], b[0]));\n const partners: GatePartner[] = joined.map(([name, slot]) => {\n const entry = entryById.get(name);\n return {\n service: name,\n maturity: entry === undefined ? null : grade(entry).maturity,\n role: slot.consumer && slot.provider ? \"both\" : slot.consumer ? \"consumer\" : \"provider\",\n via: [...slot.via].sort(),\n // An adopted service is OURS whatever a zone's tag says: `external` and\n // a maturity rung together must be unrepresentable, or the rung the\n // payload carries is one the exemption hides and the human view drops.\n external: entry === undefined && slot.external,\n };\n });\n\n return {\n landscape,\n landscapeProblem,\n targetEntry,\n target: grade(targetEntry),\n targetWithoutApi: gradeWith(targetEntry, false),\n partners,\n partnerEntries: joined.flatMap(([name]) => {\n const entry = entryById.get(name);\n return entry === undefined ? [] : [entry];\n }),\n };\n}\n"]}
1
+ {"version":3,"file":"partners.js","sourceRoot":"","sources":["../../../src/core/gate/partners.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * The direct join partners of one service, derived from the LIVING landscape,\n * and the maturity grading of everything on either end of a join.\n *\n * The landscape is the only place the fleet's joins are drawn, so the partner\n * set is exactly the edges whose resolved source or target is the gated\n * service — resolved through `serviceResolver` with the ENUMERATED fleet\n * riding in, the same ride that keeps container-modelling landscapes attached\n * everywhere else (an edge into `paymentService.api` is an edge into the\n * service that owns it). A landscape that is absent or does not parse yields\n * NO partner set, never an empty one: `landscape` says which, and the check\n * layer turns \"could not look\" into `gate.partners-unknown` rather than\n * letting it read as \"no partners\".\n */\nimport { existsSync } from \"node:fs\";\nimport { type Elem, type Rel } from \"../c4/likec4.js\";\nimport { serviceResolver } from \"../c4/resolve/service.js\";\nimport {\n ACTOR_KINDS,\n EXTERNAL_TAG,\n INFRA_KINDS,\n landscapeEvidence,\n maturityGaps,\n serviceMaturity,\n MATURITY_LADDER,\n type Maturity,\n} from \"../vocabulary/maturity.js\";\nimport { compareIds, type ServiceEntry } from \"../repo/entries.js\";\nimport { landscapePath, servicePathsAt } from \"../repo/paths.js\";\nimport { enumeratedServices } from \"../repo/service-target.js\";\nimport type { FleetContext } from \"../fleet-context.js\";\nimport { ancestorIds } from \"../kernel/ids/fqn/ancestors.js\";\nimport type { DocsDir } from \"../kernel/ids/dirs.js\";\nimport type { RawServiceId } from \"../kernel/ids/service.js\";\nimport type { GatePartner, LandscapeRead, PartnerRole } from \"./report.js\";\n\nexport interface PartnerScanRequest {\n docsDir: DocsDir;\n /** The gated service — a name the enumeration approved (`resolveServiceTarget`). */\n service: RawServiceId;\n fleet: FleetContext;\n}\n\n/** One service graded on the adoption ladder, with what the next rung wants. */\nexport interface GradedService {\n maturity: Maturity;\n gaps: string[];\n}\n\nexport interface PartnerScan {\n landscape: LandscapeRead;\n /**\n * Why the landscape graded `invalid` — the parse-error tally or the read\n * failure's own words. Null on `read` and `absent`; the finding that fails\n * the gate must say what broke, not merely that something did.\n */\n landscapeProblem: string | null;\n /**\n * The gated service's own rung, graded under the landscape's evidence —\n * `gate.service-undocumented`'s subject. Fail-closed on the api question:\n * with no readable map, `landscapeEvidence` assumes an API is expected.\n */\n target: GradedService;\n /**\n * The same service graded with NO api expected of it — the reading that\n * does not depend on the landscape at all. The check layer answers from\n * THIS one when the map is absent or broken: the api question is then\n * unanswerable, and one unanswerable fact must not convict twice (the\n * landscape already carries its own finding).\n */\n targetWithoutApi: GradedService;\n /**\n * The gated service's own enumerated entry — carried so a finding can name\n * the directory it actually occupies. `services/<id>/` is right only for an\n * unfiled fleet, and a check that sends a reader to a directory they cannot\n * open has answered worse than not at all.\n */\n targetEntry: ServiceEntry;\n partners: GatePartner[];\n /**\n * The enumerated entries behind the partners that HAVE a `services/`\n * directory, in `partners` order — what the freshness check walks. Kept as\n * entries rather than names so the ids that travel on carry the\n * enumeration's provenance (`RawServiceId`), never a document's.\n */\n partnerEntries: ServiceEntry[];\n}\n\n/** Is this rung at or above `documented` — the fixed threshold check 1 grades against? */\nexport function atLeastDocumented(m: Maturity): boolean {\n return MATURITY_LADDER.indexOf(m) >= MATURITY_LADDER.indexOf(\"documented\");\n}\n\n/**\n * What the map says about one edge END: does it stand for a person, is it\n * INFRASTRUCTURE, and is it tagged `#external`? Walked up the ancestor chain\n * because all three facts are usually written on the containing system while\n * the edge is drawn from a nested element — but the walk STOPS at the first\n * element that stands for a service (an explicit binding or a title naming a\n * real directory, `serviceResolver`'s own first two rungs, checked after that\n * element's own facts are read). What sits above the service boundary is a\n * grouping, and a grouping's `#external` is the grouping's claim, not the\n * service's: without the stop, an adopted service drawn inside an `#external`\n * zone was exempted from `gate.partner-undocumented` and its real rung hidden.\n *\n * `infra` is read only off an element that stands for NO service AND sits\n * inside none, which is the asymmetry the `standsFor` local exists for. That is\n * `commands/validate/fleet/census.ts` in two steps and in its order:\n * `serviceLevelElements` drops everything drawn inside a service BEFORE\n * `drawnSystems` ever applies the kind test, so a service's own interior never\n * reaches it. Both halves are load-bearing. A `database` bound with\n * `metadata { service '<id>' }` is an adopted service that happens to be drawn\n * as a store — its side of the join IS recorded, and dropping it would hide a\n * real partner's rung. And a store drawn INSIDE its owner — `orderService.db`,\n * the ordinary way a team draws a private datastore — is that service's\n * interior: reading the kind off it convicted the WHOLE endpoint as\n * infrastructure, and `partnerScan` then dropped order-service from the partner\n * set, so a service this gate exists to grade vanished because of how somebody\n * drew its storage. The kind is the fallback for an element nothing else\n * answers for, never the override.\n */\nfunction endFacts(\n byId: Map<string, Elem>,\n known: ReadonlySet<string>,\n id: string,\n): { actor: boolean; infra: boolean; external: boolean } {\n let actor = false;\n let infra = false;\n let external = false;\n // The id itself, then each dotted ancestor — the same walk `serviceResolver`\n // resolves through, out of the one module that spells it\n // (`core/kernel/ids/fqn/ancestors.ts`).\n for (const candidate of ancestorIds(id)) {\n const e = byId.get(candidate);\n if (e === undefined) continue;\n const standsFor = e.service !== undefined || known.has(e.title);\n if (ACTOR_KINDS.has(e.kind.toLowerCase())) actor = true;\n if (!standsFor && INFRA_KINDS.has(e.kind.toLowerCase())) infra = true;\n if (e.tags.includes(EXTERNAL_TAG)) external = true;\n if (standsFor) {\n // Everything the walk has visited so far was drawn INSIDE this service,\n // so nothing visited so far was service-LEVEL and no kind read off it\n // answers for a service — `serviceLevelElements` filtering ahead of the\n // kind test, spelled as a walk. `actor` and `external` are deliberately\n // left standing: a person or an `#external` tag written on a container is\n // still a claim about this endpoint, while the store shape is a claim\n // about the owner's interior.\n infra = false;\n break;\n }\n }\n return { actor, infra, external };\n}\n\n/** How one edge joins the two ends, in the words the finding and the payload both use. */\nfunction viaOf(r: Rel): string[] {\n const typed = [\n ...(r.op === undefined ? [] : [`operation ${r.op}`]),\n ...(r.publishes === undefined ? [] : [`message ${r.publishes}`]),\n ...(r.consumes === undefined ? [] : [`message ${r.consumes}`]),\n ];\n if (typed.length > 0) return typed;\n return [r.title === undefined ? \"untyped edge\" : `edge '${r.title}'`];\n}\n\ninterface PartnerAccumulator {\n consumer: boolean;\n provider: boolean;\n external: boolean;\n via: Set<string>;\n}\n\nexport async function partnerScan(req: PartnerScanRequest): Promise<PartnerScan> {\n const { docsDir, service, fleet } = req;\n // The same memoized enumeration the command's resolveServiceTarget already\n // performed on this FleetContext — one walk answers both questions.\n const entries = await enumeratedServices(docsDir, fleet);\n // Keyed as plain strings on purpose: the lookups below arrive from the\n // resolver, whose names are document-asserted, and only a hit proves a\n // directory. The VALUES keep the enumeration's provenance.\n const entryById = new Map<string, ServiceEntry>(entries.map((e) => [e.id, e]));\n const known: ReadonlySet<string> = new Set(entries.map((e) => e.id));\n\n const lp = landscapePath(docsDir);\n let landscape: LandscapeRead = \"absent\";\n let landscapeProblem: string | null = null;\n let elements: Elem[] = [];\n let relationships: Rel[] = [];\n if (existsSync(lp)) {\n try {\n const doc = await fleet.loadLikeC4(lp);\n if (doc.errors.length > 0) {\n landscape = \"invalid\";\n const first = doc.errors[0];\n landscapeProblem =\n `${doc.errors.length} parse error(s)` +\n (first === undefined ? \"\" : ` — first: ${first.message}`);\n } else {\n landscape = \"read\";\n elements = doc.elements;\n relationships = doc.relationships;\n }\n } catch (err) {\n // A landscape that could not be READ, graded as one that did not PARSE —\n // commands/validate/fleet/load.ts's containment doctrine, spelled here\n // because core cannot import it: \"could not be read\" and \"did not parse\"\n // have the same consequence (nothing may be concluded from this file).\n // The reason travels into the finding, message-preserving as\n // `unreadableLandscape` is; the read error must not kill the run — the\n // other three checks still have answers.\n landscape = \"invalid\";\n landscapeProblem = err instanceof Error ? err.message : String(err);\n }\n }\n const parses = landscape === \"read\";\n const svcOf: (id: string) => string = parses\n ? serviceResolver(elements, known)\n : (id: string) => id;\n const elementIds = elements.map((e) => e.id);\n\n // One service's ladder rung. `landscapeEvidence` is the ONE shared spelling\n // of the positive-evidence apiExpected rule (core/vocabulary/maturity.ts);\n // its own docblock warns against calling it per service inside a FLEET loop,\n // and this loop is not one — it runs over the gated service and its direct\n // partners, a handful, never the enumeration.\n const gradeWith = (entry: ServiceEntry, apiExpected: boolean): GradedService => {\n const input = {\n entry,\n archSpec: existsSync(servicePathsAt(entry.dir).archSpec),\n apiExpected,\n };\n return { maturity: serviceMaturity(input), gaps: maturityGaps(input) };\n };\n const grade = (entry: ServiceEntry): GradedService =>\n gradeWith(\n entry,\n landscapeEvidence({ id: entry.id, parses, relationships, elementIds, svcOf }).apiExpected,\n );\n\n // Unreachable by construction: the command layer resolved `service` against\n // the same memoized enumeration this scan just read, and refused\n // `unknown-service` when no directory answered. Failing closed here beats\n // grading a service nobody enumerated as if its absence were a rung.\n const targetEntry = entryById.get(service);\n if (targetEntry === undefined) {\n throw new Error(`gate: '${service}' vanished from the enumeration between resolve and scan`);\n }\n\n const byId = new Map(elements.map((e) => [e.id, e]));\n const acc = new Map<string, PartnerAccumulator>();\n const join = (name: string, role: PartnerRole, r: Rel, external: boolean): void => {\n const slot = acc.get(name) ?? { consumer: false, provider: false, external: false, via: new Set<string>() };\n if (role === \"consumer\") slot.consumer = true;\n else slot.provider = true;\n slot.external = slot.external || external;\n for (const v of viaOf(r)) slot.via.add(v);\n acc.set(name, slot);\n };\n for (const r of relationships) {\n const source = svcOf(r.source);\n const target = svcOf(r.target);\n // A self-edge (both ends resolve to the gated service — its own containers\n // talking to each other) names no partner.\n if (source === target) continue;\n if (target === service) {\n const facts = endFacts(byId, known, r.source);\n // A person is never a deploy participant, and neither is the fleet's own\n // infrastructure: the two kind exemptions every landscape census applies\n // (drawnSystems, commands/validate/fleet/census.ts), spelled here off the\n // same two sets so a gate and a validate run cannot disagree about what a\n // partner IS.\n //\n // The infra half is the newer one, and it had a measured cost. A\n // `database` the fleet owns and draws at fleet level joined every service\n // that reads it, with `maturity: null` because there is no\n // `services/<title>/` — so `gate.partner-undocumented` told the deployer\n // to \"adopt it, bind it … or tag it #external\", three remedies none of\n // which is true of a Postgres (measured on a one-service fixture,\n // 2026-09-07). It is dropped from the partner SET rather than exempted\n // from the finding, which is the actor precedent exactly: `partners` is\n // the list of counterparties whose DOCS the gate grades, and a store has\n // none to grade.\n if (!facts.actor && !facts.infra) join(source, \"consumer\", r, facts.external);\n } else if (source === service) {\n const facts = endFacts(byId, known, r.target);\n if (!facts.actor && !facts.infra) join(target, \"provider\", r, facts.external);\n }\n }\n\n // Sorted by id so identical state yields identical bytes — the accumulator's\n // order is the landscape's edge order, which a reformat may shuffle.\n const joined = [...acc.entries()].sort((a, b) => compareIds(a[0], b[0]));\n const partners: GatePartner[] = joined.map(([name, slot]) => {\n const entry = entryById.get(name);\n return {\n service: name,\n maturity: entry === undefined ? null : grade(entry).maturity,\n role: slot.consumer && slot.provider ? \"both\" : slot.consumer ? \"consumer\" : \"provider\",\n via: [...slot.via].sort(),\n // An adopted service is OURS whatever a zone's tag says: `external` and\n // a maturity rung together must be unrepresentable, or the rung the\n // payload carries is one the exemption hides and the human view drops.\n external: entry === undefined && slot.external,\n };\n });\n\n return {\n landscape,\n landscapeProblem,\n targetEntry,\n target: grade(targetEntry),\n targetWithoutApi: gradeWith(targetEntry, false),\n partners,\n partnerEntries: joined.flatMap(([name]) => {\n const entry = entryById.get(name);\n return entry === undefined ? [] : [entry];\n }),\n };\n}\n"]}
@@ -182,49 +182,54 @@ export function obligationsPath(docsDir) {
182
182
  export function fleetAdrsDir(docsDir) {
183
183
  return join(docsDir, "architecture", ADRS_DIR);
184
184
  }
185
+ const SUBSYSTEM_VIEWS_FILE = "subsystems.likec4";
186
+ const SERVICE_VIEWS_FILE = "services.likec4";
185
187
  /**
186
- * The GENERATED subsystem views — beside the fleet map because the LikeC4
188
+ * The documents inside `architecture/` that LOAM GENERATES — ONE list, because
189
+ * four questions read it and a name in one reader and missing from another fails
190
+ * silently both ways: the path functions below, the reservation below, the project
191
+ * loads that must EXCLUDE these files (`core/c4/project/architecture.ts` and
192
+ * `commands/validate/fleet/views/projects.ts` — a views-only document does not
193
+ * parse standalone, so loading one blanks the project it lands in), and `loam
194
+ * archive`'s withhold-and-name — each of which hard-coded "the generated one"
195
+ * while there was only one. The VALUE is what the reservation below prints.
196
+ */
197
+ export const GENERATED_ARCHITECTURE_FILES = {
198
+ [SUBSYSTEM_VIEWS_FILE]: "`subsystems.likec4` is generated by `loam subsystem sync`, which overwrites it wholesale — and nothing in loam ever parses it, so anything landing there is invisible from the moment it archives",
199
+ [SERVICE_VIEWS_FILE]: "`services.likec4` is generated by `loam subsystem sync`, one overview view per service, and is overwritten wholesale — and nothing in loam ever parses it, so anything landing there is invisible from the moment it archives",
200
+ };
201
+ /**
202
+ * The two GENERATED views files — beside the fleet map because the LikeC4
187
203
  * renderer merges the whole `architecture/` project, and a view can only
188
- * `include` elements the landscape beside it defines. `loam subsystem sync`
189
- * owns the bytes (`core/repo/tree/render/views.ts` renders them); nothing in loam
190
- * ever parses the file, and staleness against the tree is a byte compare.
204
+ * `include` elements the landscape beside it defines. `loam subsystem sync` owns
205
+ * both files' bytes (`render/views.ts`, `render/service/views.ts`); loam parses
206
+ * neither, and staleness against the tree is a content compare.
191
207
  */
192
208
  export function subsystemViewsPath(docsDir) {
193
209
  return join(docsDir, "architecture", SUBSYSTEM_VIEWS_FILE);
194
210
  }
195
- /** The generated views' file name, spelled once because the reservation below reads it too. */
196
- const SUBSYSTEM_VIEWS_FILE = "subsystems.likec4";
211
+ export function serviceViewsPath(docsDir) {
212
+ return join(docsDir, "architecture", SERVICE_VIEWS_FILE);
213
+ }
197
214
  /**
198
215
  * Names inside `architecture/` that LOAM owns, and what goes wrong if a
199
- * feature's own document claims one.
200
- *
201
- * `features/<FEAT>/deployment/<name>.likec4` merges as a whole-file copy
202
- * straight into `architecture/` — unlike a flow, which lands under
203
- * `usecases/` — so its `<name>` is free to be any of these, and two of them
204
- * fail in ways nothing downstream would report:
205
- *
206
- * - `subsystems.likec4` is GENERATED and is EXCLUDED from every project load
207
- * (docs/DESIGN.md rule 26 keeps it a byte compare), so topology landing
208
- * there is invisible to loam from the moment it archives — and the next
209
- * `loam subsystem sync` overwrites the file wholesale. Silent both ways,
210
- * which is the worst shape a failure can have;
211
- * - `landscape.likec4` is the living map the archive SPLICES; a whole-file
212
- * copy over it would discard the fleet;
213
- * - `usecases/` is the flow tree, walked by the use-case scan. A deployment
214
- * document filed there parses (one project) and is then looked for by a
215
- * reader asking a different question.
216
- *
217
- * Returns the reason, or null when the name is the author's to choose. Takes
218
- * the `/`-separated rel a directory walk produced, never a string from argv.
216
+ * feature's own document claims one. `features/<FEAT>/deployment/<name>.likec4`
217
+ * merges as a whole-file COPY straight into `architecture/` — unlike a flow,
218
+ * which lands under `usecases/` — so its `<name>` is free to be any of them. A
219
+ * generated name fails silently both ways (the table above says how); a copy over
220
+ * `landscape.likec4` would discard the living map the archive SPLICES; and
221
+ * `usecases/` is the flow tree the scan walks, where a deployment document parses
222
+ * and is then looked for by a reader asking another question. Returns the reason,
223
+ * or null when the name is the author's — over the rel a walk produced, never argv.
219
224
  */
220
225
  export function reservedArchitectureName(rel) {
221
226
  const parts = rel.split("/");
222
227
  if (parts[0] === USECASE_DIR) {
223
228
  return `\`${USECASE_DIR}/\` is the use-case tree — a flow belongs in \`features/<FEAT>/usecases/\`, which merges there`;
224
229
  }
225
- if (rel === SUBSYSTEM_VIEWS_FILE) {
226
- return "`subsystems.likec4` is generated by `loam subsystem sync`, which overwrites it wholesale — and nothing in loam ever parses it, so anything landing there is invisible from the moment it archives";
227
- }
230
+ // Own key only: a bare lookup answers `constructor` with something inherited.
231
+ if (Object.hasOwn(GENERATED_ARCHITECTURE_FILES, rel))
232
+ return GENERATED_ARCHITECTURE_FILES[rel];
228
233
  if (rel === LANDSCAPE_FILE) {
229
234
  return "`landscape.likec4` is the living fleet map, which the archive SPLICES into rather than replaces";
230
235
  }