@ybotok/loam 0.2.0-alpha.6 → 0.2.0-alpha.7
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +162 -0
- package/README.md +4 -4
- package/ROADMAP.md +27 -6
- package/SCHEMA.md +152 -32
- package/dist/commands/archive/run.js +49 -22
- package/dist/commands/archive/run.js.map +1 -1
- package/dist/commands/seed/commit.js +34 -1
- package/dist/commands/seed/commit.js.map +1 -1
- package/dist/commands/seed/plan.js +64 -13
- package/dist/commands/seed/plan.js.map +1 -1
- package/dist/commands/subsystem/edit.js +4 -2
- package/dist/commands/subsystem/edit.js.map +1 -1
- package/dist/commands/subsystem/move.js +55 -17
- package/dist/commands/subsystem/move.js.map +1 -1
- package/dist/commands/subsystem/sync.js +142 -45
- package/dist/commands/subsystem/sync.js.map +1 -1
- package/dist/commands/subsystem/txn/report.js +55 -5
- package/dist/commands/subsystem/txn/report.js.map +1 -1
- package/dist/commands/subsystem/txn/txn.js +118 -21
- package/dist/commands/subsystem/txn/txn.js.map +1 -1
- package/dist/commands/subsystem/txn/views.js +51 -8
- package/dist/commands/subsystem/txn/views.js.map +1 -1
- package/dist/commands/validate/checks/health-deps.js +9 -6
- package/dist/commands/validate/checks/health-deps.js.map +1 -1
- package/dist/commands/validate/fleet/census.js +26 -3
- package/dist/commands/validate/fleet/census.js.map +1 -1
- package/dist/commands/validate/fleet/landscape.js +12 -9
- package/dist/commands/validate/fleet/landscape.js.map +1 -1
- package/dist/commands/validate/fleet/map/attest.js +4 -5
- package/dist/commands/validate/fleet/map/attest.js.map +1 -1
- package/dist/commands/validate/fleet/map/bindings.js +15 -2
- package/dist/commands/validate/fleet/map/bindings.js.map +1 -1
- package/dist/commands/validate/fleet/map/consumers.js +43 -122
- package/dist/commands/validate/fleet/map/consumers.js.map +1 -1
- package/dist/commands/validate/fleet/scorecard/scorecard.js.map +1 -1
- package/dist/commands/validate/fleet/views/fleet-project.js +1 -1
- package/dist/commands/validate/fleet/views/fleet-project.js.map +1 -1
- package/dist/commands/validate/fleet/views/ids.js +33 -7
- package/dist/commands/validate/fleet/views/ids.js.map +1 -1
- package/dist/commands/validate/fleet/views/root-documents.js +9 -7
- package/dist/commands/validate/fleet/views/root-documents.js.map +1 -1
- package/dist/commands/validate/fleet/views/stale.js +31 -14
- package/dist/commands/validate/fleet/views/stale.js.map +1 -1
- package/dist/core/agent/agents-md/command-map.js +1 -1
- package/dist/core/agent/agents-md/command-map.js.map +1 -1
- package/dist/core/agent/agents-md/map/subsystem.js +27 -14
- package/dist/core/agent/agents-md/map/subsystem.js.map +1 -1
- package/dist/core/agent/agents-md/refusals.js +5 -3
- package/dist/core/agent/agents-md/refusals.js.map +1 -1
- package/dist/core/agent/agents-md.js +16 -7
- package/dist/core/agent/agents-md.js.map +1 -1
- package/dist/core/agent/workflows/check.js +5 -6
- package/dist/core/agent/workflows/check.js.map +1 -1
- package/dist/core/agent/workflows/reference/spine.js +12 -6
- package/dist/core/agent/workflows/reference/spine.js.map +1 -1
- package/dist/core/brief/brief.js +25 -8
- package/dist/core/brief/brief.js.map +1 -1
- package/dist/core/brief/targets.js +64 -0
- package/dist/core/brief/targets.js.map +1 -1
- package/dist/core/brief/unchecked.js +1 -1
- package/dist/core/brief/unchecked.js.map +1 -1
- package/dist/core/c4/project/architecture.js +31 -9
- package/dist/core/c4/project/architecture.js.map +1 -1
- package/dist/core/c4/project/documents.js +5 -4
- package/dist/core/c4/project/documents.js.map +1 -1
- package/dist/core/c4/project/load.js +8 -4
- package/dist/core/c4/project/load.js.map +1 -1
- package/dist/core/c4/project/staged.js +7 -3
- package/dist/core/c4/project/staged.js.map +1 -1
- package/dist/core/deployment/delta.js +12 -10
- package/dist/core/deployment/delta.js.map +1 -1
- package/dist/core/gate/checks.js +5 -1
- package/dist/core/gate/checks.js.map +1 -1
- package/dist/core/gate/partners.js +60 -17
- package/dist/core/gate/partners.js.map +1 -1
- package/dist/core/repo/paths.js +34 -29
- package/dist/core/repo/paths.js.map +1 -1
- package/dist/core/repo/tree/render/body.js +45 -6
- package/dist/core/repo/tree/render/body.js.map +1 -1
- package/dist/core/repo/tree/render/service/header.js +125 -0
- package/dist/core/repo/tree/render/service/header.js.map +1 -0
- package/dist/core/repo/tree/render/service/peers.js +28 -0
- package/dist/core/repo/tree/render/service/peers.js.map +1 -0
- package/dist/core/repo/tree/render/service/view-id.js +67 -0
- package/dist/core/repo/tree/render/service/view-id.js.map +1 -0
- package/dist/core/repo/tree/render/service/views.js +78 -0
- package/dist/core/repo/tree/render/service/views.js.map +1 -0
- package/dist/core/repo/tree/render/stale.js +19 -2
- package/dist/core/repo/tree/render/stale.js.map +1 -1
- package/dist/core/repo/tree/render/views.js +102 -33
- package/dist/core/repo/tree/render/views.js.map +1 -1
- package/dist/core/verify/checklist.js +42 -10
- package/dist/core/verify/checklist.js.map +1 -1
- package/dist/core/vocabulary/issue.js.map +1 -1
- package/dist/core/vocabulary/maturity.js +41 -1
- package/dist/core/vocabulary/maturity.js.map +1 -1
- package/examples/README.md +7 -3
- package/examples/docs/architecture/landscape.likec4 +13 -7
- package/examples/docs/architecture/services.likec4 +70 -0
- package/examples/docs/architecture/subsystems.likec4 +3 -0
- package/examples/docs/services/order-service/model.likec4 +10 -8
- package/examples/docs/services/payment-service/model.likec4 +6 -4
- package/examples/docs/services/platform/identity-service/model.likec4 +9 -7
- package/examples/docs/services/platform/notification-service/model.likec4 +4 -5
- package/package.json +1 -1
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"consumers.js","sourceRoot":"","sources":["../../../../../src/commands/validate/fleet/map/consumers.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * Who consumes a drawn element — and the datastore grades that read the answer.\n *\n * The census used to walk the MAP's relationships alone, and under the extending\n * shape that is a partial view of the fleet: a service draws the container-level\n * call in its own `model.likec4` and the map carries the service-level twin, so a\n * store whose consuming edges were all drawn in models had ZERO consumers and\n * earned nothing at all, while a store reached by three models and one map edge\n * was reported `landscape.datastore-private` naming the one service the map\n * happened to draw (R1). A model is evidence about the fleet exactly as the map\n * is, so both are counted here, once, by one derivation two checks share.\n *\n * WHERE A PRIVATE STORE LIVES, stated once because three surfaces used to answer\n * it differently: a store one service owns is NESTED UNDER THAT SERVICE'S\n * ELEMENT — in the service's extending model (`extend <fqn> { store = database\n * … }`), or inside the service's element on the map when the map draws that\n * service's containers. A second service reaching it later is drawn where that\n * service draws every other call: a container-level edge in ITS model naming\n * `<owner-fqn>.<store>`, carried up to the map as a service-level edge to the\n * owning service. Nothing moves back, and nothing goes ungraded — a nested store\n * two services reach is `landscape.datastore-shared` here (E3).\n *\n * WHICH OF THE TWO PLACEMENTS, and this is measured rather than assumed: a\n * per-service project is `architecture/` plus that ONE model, so an edge naming\n * `<owner-fqn>.<store>` resolves for the second service only when the MAP\n * declares the store. Written in the owner's `extend` block instead, the same\n * edge is two errors on the consumer's own model (\"Could not resolve reference\n * to Referenceable named 'store'\"), so that store is private for good and its\n * second consumer can only ever be drawn at service level. The message below\n * says so; SCHEMA carries the same sentence.\n *\n * The choice is cheap, and both messages now say exactly HOW cheap, because\n * \"reversible\" was false in the one state that used to assert it. The two\n * placements do give the store the same id, `<owner-fqn>.<store>` — but the\n * move is only free in ONE direction once a second service names it. Measured\n * on `examples/docs`: moving `db` out of order-service's `extend` block into\n * `orderService` on the map left the model at 12 elements · 8 relationships,\n * left `Covers: marketplace.orderService.db` in its arch.spec.md resolving, and\n * left checkout-web's container-level edge to it resolving too. Moving it BACK\n * with that second edge drawn is two errors on checkout-web's own model\n * (\"Could not resolve reference to Referenceable named 'db'\"), or\n * `landscape.invalid` plus one `spine.landscape-invalid` per service when the\n * second edge is drawn on the map — because an `extend` block is visible to its\n * own service's project alone. `datastore-shared` printed the unconditional\n * clause in 100% of the states it fires in, every one of which already has the\n * second consumer that makes map→extend break (verification 2026-09-04). So the\n * rule both messages now state is directional: extend→map is always safe;\n * map→extend only while no other service's model or map edge names the store.\n *\n * And the remedy is named for the consumer's own model SHAPE rather than\n * assumed: `extend <fqn>` in a STANDALONE model (one that declares its own\n * `specification`) resolves nothing, because such a model is parsed alone — so\n * prescribing it there turns a clean run into `c4.invalid`. The shape comes off\n * the same `./attest.ts` record the consumer join reads.\n *\n * A sub-package of `fleet/` for `./isolation.ts`'s reason and with its rule: it\n * imports NOTHING from `fleet/`, so the map facts it reads arrive on the input\n * records the caller fills.\n */\nimport { type Elem, type Rel } from \"../../../../core/c4/likec4.js\";\nimport { type Finding } from \"../../../../core/vocabulary/report.js\";\nimport { type AttestedModel, type NestedStore } from \"./attest.js\";\n\n/** The element kind read as a datastore, compared case-insensitively. */\nconst DATASTORE_KIND = \"database\";\n\nexport interface CensusInput {\n /** The map's own edges — a service-level twin is what the map carries of a container-level call. */\n relationships: Rel[];\n /** The services/<id>/ directories that exist: a consumer is a source filed under a real one. */\n services: ReadonlySet<string>;\n /** The shared element→service resolver every edge join uses. */\n resolve: (id: string) => string;\n /** What each service's own model attests (`./attest.ts`). */\n attested: readonly AttestedModel[];\n}\n\n/**\n * Distinct consumer SERVICES of an element, its nested children included: an\n * edge into `kafka.paymentEvents` is consumption of `kafka`.\n *\n * Two kinds of evidence, unioned. From the MAP: a relationship whose target is\n * the element or a descendant and whose source the shared resolver files under a\n * real `services/<id>/` — persons, other external systems and unresolved sources\n * never count, and two edges from one service are one consumer. From the\n * MODELS: a service whose own model attests an OUTBOUND call at the element or a\n * descendant.\n *\n * The model half reads EXTENDING models only, and the reason is the id space. An\n * extending model's ids are the map's fully-qualified ids — one project — so the\n * join is exact. A standalone model's ids are its own file's, and\n * `core/c4/resolve/attested.ts` says outright that a counterpart is never\n * matched to a landscape element: joining one would file a coincidence of\n * spelling as a coupling between two services.\n */\nexport function consumerCensus(input: CensusInput): (elementId: string) => string[] {\n const reaches = (elementId: string, id: string): boolean => id === elementId || id.startsWith(`${elementId}.`);\n return (elementId: string): string[] => {\n const out = new Set<string>();\n for (const r of input.relationships) {\n if (!reaches(elementId, r.target)) continue;\n const svc = input.resolve(r.source);\n if (input.services.has(svc)) out.add(svc);\n }\n for (const model of input.attested) {\n if (!model.extending) continue;\n if (model.calls.some((c) => c.direction === \"out\" && reaches(elementId, c.counterpartId))) {\n out.add(model.service);\n }\n }\n return [...out].sort();\n };\n}\n\n/** What loam actually read at a consumer's own `model.likec4`, which decides where a store may be nested. */\nexport type ConsumerShape = \"extending\" | \"standalone\" | null;\n\nexport interface DatastoreInput {\n /** Service-LEVEL elements — the fleet map's own boxes, where a store drawn as a peer is found. */\n drawn: Elem[];\n /** Every element the MAP declares, at any depth — the other place a nested store is written. */\n elements: Elem[];\n /** Stores an extending model declares inside its own element (`./attest.ts`). */\n nestedStores: readonly NestedStore[];\n /** The services/<id>/ directories that exist. */\n services: ReadonlySet<string>;\n /** The shared element→service resolver, for naming the consumer's own element on the map. */\n resolve: (id: string) => string;\n /**\n * `census.ts`'s one predicate for \"this element stands for a `services/<id>/`\",\n * injected because a `fleet/map/` module may not import `fleet/`. Both halves\n * of the grade below skip such an element: it is the fleet itself, not its\n * shape, and a store bound to a directory is the binding checks' subject.\n * `./attest.ts` applies the same predicate to the stores a MODEL declares, so\n * all three places answer it once.\n */\n standsForService: (e: Elem) => boolean;\n /** What each service's own model attests (`./attest.ts`) — the consumer join AND the shape the remedy names. */\n attested: readonly AttestedModel[];\n /** The census above, built once and shared with the platform-candidate advisory. */\n consumers: (elementId: string) => string[];\n /** Where a service's directory sits, repo-relative — the enumeration's answer, never `services/<id>`. */\n pathOf: (id: string) => string;\n}\n\n/**\n * A datastore drawn as a fleet-level peer makes a claim its consumer count\n * either supports or refutes: one consumer means the drawing is false — the\n * store is that service's internals, not a system in its own right — while two\n * or more mean the strongest coupling two services can have, which deserves to\n * be stated rather than inferred. A NESTED store is private by construction, so\n * it is silent at one consumer and earns the same `datastore-shared` at two.\n */\nexport function datastoreFindings(input: DatastoreInput): Finding[] {\n const findings: Finding[] = [];\n for (const e of input.drawn) {\n // An element that stands for a real service is the fleet itself, not its\n // shape; a datastore bound to a directory is graded by the binding checks.\n if (input.standsForService(e)) continue;\n if (e.kind.toLowerCase() !== DATASTORE_KIND) continue;\n const consumers = input.consumers(e.id);\n if (consumers.length === 0) continue;\n findings.push(\n consumers.length === 1 ? privateFinding(e, consumers[0]!, input) : sharedFinding(e.title, consumers, \"\", \"\"),\n );\n }\n for (const store of nestedStoresOf(input)) {\n // The owner is a consumer by construction — the store sits inside the element\n // that resolves to it — so silence at one consumer IS the fixed state, and\n // the only question left is whether a second service reaches the same data.\n const consumers = [...new Set([store.owner, ...input.consumers(store.fqn)])].sort();\n if (consumers.length < 2) continue;\n findings.push(\n sharedFinding(\n store.title,\n consumers,\n // The store's own id, and said as such. The clause read \"nested under\n // '<fqn>'\", which sends a reader looking for an element that HOLDS the\n // store — when that id IS the store, and its container is its prefix.\n ` — ${store.owner}'s own store, written as '${store.fqn}'`,\n reversibility(store, consumers.filter((c) => c !== store.owner)),\n ),\n );\n }\n\n return findings;\n}\n\n/**\n * Every store nested under the element that resolves to its owner, from both\n * places the stance allows one to be written: inside the service's element ON\n * THE MAP, and in the service's own extending model. Keyed by fqn, so a\n * map-declared store — which the owner's model slice carries too, since a slice\n * is everything resolving to the service — is one store and not two.\n *\n * Both sources are needed and neither is redundant. The map's covers a service\n * whose model is standalone or absent (no slice to read); the model's covers a\n * store the map never draws, which is the placement the adopt brief prescribes.\n */\nfunction nestedStoresOf(input: DatastoreInput): NestedStore[] {\n const drawnIds = new Set(input.drawn.map((e) => e.id));\n const byFqn = new Map<string, NestedStore>();\n for (const e of input.elements) {\n if (e.kind.toLowerCase() !== DATASTORE_KIND || drawnIds.has(e.id)) continue;\n // The peer branch's guard, which this half was missing: a `database` bound\n // with `metadata { service '<id>' }` and drawn one level down became a store\n // \"owned\" by its own binding, so a second reader made it a shared datastore\n // and the fix offered was to give each service its own copy of a service.\n if (input.standsForService(e)) continue;\n const owner = input.resolve(e.id);\n if (!input.services.has(owner)) continue;\n byFqn.set(e.id, { owner, fqn: e.id, title: e.title });\n }\n for (const store of input.nestedStores) if (!byFqn.has(store.fqn)) byFqn.set(store.fqn, store);\n return [...byFqn.values()];\n}\n\n/**\n * Which way the store may still be moved, now that a second service reaches it.\n *\n * The clause this replaces said the two placements were interchangeable — \"the\n * two placements give it the same id … so moving the declaration between them\n * rewrites no edge\" — and that is false in every state `datastore-shared` fires\n * in, because the finding needs a second consumer and a second consumer's edge\n * resolves only against a store the MAP declares (a per-service project is\n * `architecture/` plus one model). Measured: performing the move the sentence\n * called free turned a clean `examples/docs` into `c4.invalid` on the consumer's\n * model, or into `landscape.invalid` + one `spine.landscape-invalid` per service\n * when the second edge is on the map (verification 2026-09-04).\n *\n * The direction is stated rather than the current placement, and it holds\n * either way round: out of the owner's `extend` block onto the map is always\n * safe, back in is not while another service names the store. (In practice a\n * store this finding reaches is one the MAP declares — a second consumer's edge\n * cannot resolve against an `extend` block, so it could never have been drawn\n * — which is the same fact said from the other side.)\n */\nfunction reversibility(store: NestedStore, others: string[]): string {\n const named = others.length === 1 ? `${others[0]!}'s edge` : `${others.join(\", \")}'s edges`;\n return (\n `. Both placements give it the same id, ${store.fqn}, but with a second consumer the move is ONE-WAY: ` +\n `${named} resolves only against a declaration the MAP holds — a per-service project is \\`architecture/\\` ` +\n `plus one model, so an \\`extend\\` block is visible to its own service's project alone. Out of ` +\n `${store.owner}'s \\`extend\\` block onto ${store.owner}'s element on the map is always safe; moving it ` +\n `back INTO the \\`extend\\` block breaks that edge — \\`c4.invalid\\` on the other service's model, or ` +\n `\\`landscape.invalid\\` when the edge is drawn on the map`\n );\n}\n\n/**\n * Where the store goes, named for the consumer's own model SHAPE — which this\n * sentence used to assert instead of reading. It always prescribed the\n * extending-shape remedy, and following it on a standalone consumer turns a\n * clean run into `c4.invalid`: a model that declares its own `specification` is\n * parsed alone, so `extend <fqn>` there resolves nothing at all (measured — one\n * error, \"Could not resolve reference to Element named '<fqn>'\"). The two\n * placements otherwise give the store the SAME id, which is the fact that makes\n * the choice cheap: measured on examples/docs, moving `db` out of\n * order-service's extend block into `orderService` on the map left the model at\n * 12 elements · 8 relationships, left `Covers: marketplace.orderService.db` in\n * its arch.spec.md resolving, and left checkout-web's edge to it resolving too.\n */\nfunction placement(at: { fqn: string; local: string; path: string; shape: ConsumerShape }): string {\n // Direction, not symmetry. The clause used to read \"moving the declaration\n // between them later rewrites no edge\", which is true only while this store\n // has ONE consumer — the state this finding is in — and false the moment the\n // second consumer it goes on to promise arrives: that consumer's edge resolves\n // against the map's declaration alone. Saying it unconditionally here is what\n // made `datastore-shared` inherit a sentence that was false in every state it\n // printed in (verification 2026-09-04).\n const same =\n `the two placements give it the same id, ${at.fqn}.${at.local}, and with one consumer either move ` +\n `is free — out of the \\`extend\\` block onto the map always is, and back into it while no other ` +\n `service's model or map edge names the store, an \\`extend\\` block being visible to its own service's ` +\n `project alone`;\n if (at.shape === \"extending\") {\n return (\n `A store one service owns is nested UNDER that service's element: write it in the ` +\n `\\`extend ${at.fqn} { … }\\` block of ${at.path}, or inside '${at.fqn}' here when the map draws ` +\n `that service's containers, and delete this peer — ${same}.`\n );\n }\n // The two absent-extending-model arms differ in what there is to do: a\n // STANDALONE model exists and can be migrated; with no model at all there is\n // nothing to migrate, and \"migrate the model\" sent a reader looking for a file\n // loam had just said it could not find (verification 2026-09-04).\n const otherwise =\n at.shape === \"standalone\"\n ? `${at.path} declares its own \\`specification\\`, so it is parsed alone and \\`extend ${at.fqn}\\` ` +\n `resolves nothing there`\n : `loam read no extending model at ${at.path}`;\n const migrate =\n at.shape === \"standalone\"\n ? `migrate the model to the extending shape (SCHEMA.md, \"Two shapes of a service model\")`\n : `write an extending model at ${at.path} (SCHEMA.md, \"Two shapes of a service model\")`;\n return (\n `A store one service owns is nested UNDER that service's element, and this consumer has no ` +\n `extending model to nest it in (${otherwise}): write it inside '${at.fqn}' here, where the map draws ` +\n `that service's containers, and delete this peer — or ${migrate} and write it in an ` +\n `\\`extend ${at.fqn} { … }\\` block there, where ${same}.`\n );\n}\n\n/**\n * The one-consumer message, and it names the PLACEMENT rather than a directory:\n * the remedy used to read \"move it into services/<id>/model.likec4 as a nested\n * container, or add the second consumer's edge\", which offered two remedies as\n * equals and left the second one impossible — an edge naming the moved store is\n * `landscape.invalid` for loam, so an author who took the first remedy believed\n * they had shut the second door (#01). They had not, and the sentence now says\n * how: a later consumer is drawn in ITS OWN model, and loam counts that — as\n * long as that model EXTENDS the map, which the caveat below is careful to say.\n */\nfunction privateFinding(store: Elem, consumer: string, input: DatastoreInput): Finding {\n const fqn = input.drawn.find((e) => input.resolve(e.id) === consumer)?.id ?? consumer;\n const local = store.id.split(\".\").at(-1) ?? store.id;\n const model = input.attested.find((m) => m.service === consumer);\n const shape: ConsumerShape = model === undefined ? null : model.extending ? \"extending\" : \"standalone\";\n return {\n severity: \"warn\",\n code: \"landscape.datastore-private\",\n subject: store.title,\n message:\n `landscape: '${store.title}' is a datastore with a single consumer at fleet level ` +\n `('${consumer}') — drawn as a peer it reads as a system in its own right, available to be ` +\n `depended on. ${placement({ fqn, local, path: `${input.pathOf(consumer)}/model.likec4`, shape })} ` +\n `Nesting closes no door: a second service reaching the same data later draws a container-level ` +\n `edge to ${fqn}.${local} in ITS OWN model plus a service-level edge to '${fqn}' here, and loam ` +\n `counts a consumer an EXTENDING model attests exactly as it counts one drawn here — so that ` +\n `state is \\`landscape.datastore-shared\\`, never silence. A consumer whose model stands alone is ` +\n `not counted (its ids are its own file's, never the map's), so its edge belongs on the map. ` +\n `Which placement: that edge RESOLVES only against a store the map declares (a per-service ` +\n `project is architecture/ plus one model), so ` +\n (shape === \"extending\"\n ? `nest it here when a second consumer is foreseeable and in the model when it is not. `\n : `the map is the placement that keeps that door open. `) +\n `If another service reaches the same data today, draw its edge and this becomes that warning now`,\n };\n}\n\n/** The two-or-more message, shared by the peer and the nested store — one word about coupling, once. */\nfunction sharedFinding(title: string, consumers: string[], where: string, tail: string): Finding {\n return {\n severity: \"warn\",\n code: \"landscape.datastore-shared\",\n subject: title,\n message:\n `landscape: '${title}' is a datastore shared by ${consumers.length} services ` +\n `(${consumers.join(\", \")})${where} — the strongest coupling two services can have, and the ` +\n `hardest to undo. Shared means the same DATA: if they read the same tables or keys, keep it ` +\n `where it is and let this warning state the coupling; if they only share a host or cluster ` +\n `(two schemas, two lock paths), that is operational blast radius — a runbook fact — and the ` +\n `honest model is one private store per service${tail}`,\n };\n}\n"]}
|
|
1
|
+
{"version":3,"file":"consumers.js","sourceRoot":"","sources":["../../../../../src/commands/validate/fleet/map/consumers.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAkDG;AACH,OAAO,EAAuB,MAAM,+BAA+B,CAAC;AACpE,OAAO,EAAE,cAAc,EAAE,MAAM,yCAAyC,CAAC;AACzE,OAAO,EAAgB,MAAM,uCAAuC,CAAC;AACrE,OAAO,EAAwC,MAAM,aAAa,CAAC;AAanE;;;;;;;;;;;;;;;;;GAiBG;AACH,MAAM,UAAU,cAAc,CAAC,KAAkB;IAC/C,MAAM,OAAO,GAAG,CAAC,SAAiB,EAAE,EAAU,EAAW,EAAE,CAAC,EAAE,KAAK,SAAS,IAAI,EAAE,CAAC,UAAU,CAAC,GAAG,SAAS,GAAG,CAAC,CAAC;IAC/G,OAAO,CAAC,SAAiB,EAAY,EAAE;QACrC,MAAM,GAAG,GAAG,IAAI,GAAG,EAAU,CAAC;QAC9B,KAAK,MAAM,CAAC,IAAI,KAAK,CAAC,aAAa,EAAE,CAAC;YACpC,IAAI,CAAC,OAAO,CAAC,SAAS,EAAE,CAAC,CAAC,MAAM,CAAC;gBAAE,SAAS;YAC5C,MAAM,GAAG,GAAG,KAAK,CAAC,OAAO,CAAC,CAAC,CAAC,MAAM,CAAC,CAAC;YACpC,IAAI,KAAK,CAAC,QAAQ,CAAC,GAAG,CAAC,GAAG,CAAC;gBAAE,GAAG,CAAC,GAAG,CAAC,GAAG,CAAC,CAAC;QAC5C,CAAC;QACD,KAAK,MAAM,KAAK,IAAI,KAAK,CAAC,QAAQ,EAAE,CAAC;YACnC,IAAI,CAAC,KAAK,CAAC,SAAS;gBAAE,SAAS;YAC/B,IAAI,KAAK,CAAC,KAAK,CAAC,IAAI,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,SAAS,KAAK,KAAK,IAAI,OAAO,CAAC,SAAS,EAAE,CAAC,CAAC,aAAa,CAAC,CAAC,EAAE,CAAC;gBAC1F,GAAG,CAAC,GAAG,CAAC,KAAK,CAAC,OAAO,CAAC,CAAC;YACzB,CAAC;QACH,CAAC;QACD,OAAO,CAAC,GAAG,GAAG,CAAC,CAAC,IAAI,EAAE,CAAC;IACzB,CAAC,CAAC;AACJ,CAAC;AA0BD;;;;;;;;;;;;;;;GAeG;AACH,MAAM,UAAU,iBAAiB,CAAC,KAAqB;IACrD,MAAM,QAAQ,GAAc,EAAE,CAAC;IAC/B,KAAK,MAAM,CAAC,IAAI,KAAK,CAAC,KAAK,EAAE,CAAC;QAC5B,yEAAyE;QACzE,2EAA2E;QAC3E,IAAI,KAAK,CAAC,gBAAgB,CAAC,CAAC,CAAC;YAAE,SAAS;QACxC,IAAI,CAAC,CAAC,IAAI,CAAC,WAAW,EAAE,KAAK,cAAc;YAAE,SAAS;QACtD,MAAM,SAAS,GAAG,KAAK,CAAC,SAAS,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC;QACxC,2EAA2E;QAC3E,0EAA0E;QAC1E,6DAA6D;QAC7D,IAAI,SAAS,CAAC,MAAM,GAAG,CAAC;YAAE,SAAS;QACnC,QAAQ,CAAC,IAAI,CAAC,aAAa,CAAC,CAAC,CAAC,KAAK,EAAE,SAAS,EAAE,EAAE,EAAE,EAAE,CAAC,CAAC,CAAC;IAC3D,CAAC;IACD,KAAK,MAAM,KAAK,IAAI,cAAc,CAAC,KAAK,CAAC,EAAE,CAAC;QAC1C,8EAA8E;QAC9E,2EAA2E;QAC3E,4EAA4E;QAC5E,MAAM,SAAS,GAAG,CAAC,GAAG,IAAI,GAAG,CAAC,CAAC,KAAK,CAAC,KAAK,EAAE,GAAG,KAAK,CAAC,SAAS,CAAC,KAAK,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC,CAAC,IAAI,EAAE,CAAC;QACpF,IAAI,SAAS,CAAC,MAAM,GAAG,CAAC;YAAE,SAAS;QACnC,QAAQ,CAAC,IAAI,CACX,aAAa,CACX,KAAK,CAAC,KAAK,EACX,SAAS;QACT,sEAAsE;QACtE,uEAAuE;QACvE,sEAAsE;QACtE,MAAM,KAAK,CAAC,KAAK,6BAA6B,KAAK,CAAC,GAAG,GAAG,EAC1D,aAAa,CAAC,KAAK,EAAE,SAAS,CAAC,MAAM,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,KAAK,KAAK,CAAC,KAAK,CAAC,CAAC,CACjE,CACF,CAAC;IACJ,CAAC;IAED,OAAO,QAAQ,CAAC;AAClB,CAAC;AAED;;;;;;;;;;GAUG;AACH,SAAS,cAAc,CAAC,KAAqB;IAC3C,MAAM,QAAQ,GAAG,IAAI,GAAG,CAAC,KAAK,CAAC,KAAK,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC;IACvD,MAAM,KAAK,GAAG,IAAI,GAAG,EAAuB,CAAC;IAC7C,KAAK,MAAM,CAAC,IAAI,KAAK,CAAC,QAAQ,EAAE,CAAC;QAC/B,IAAI,CAAC,CAAC,IAAI,CAAC,WAAW,EAAE,KAAK,cAAc,IAAI,QAAQ,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,CAAC;YAAE,SAAS;QAC5E,2EAA2E;QAC3E,6EAA6E;QAC7E,4EAA4E;QAC5E,0EAA0E;QAC1E,IAAI,KAAK,CAAC,gBAAgB,CAAC,CAAC,CAAC;YAAE,SAAS;QACxC,MAAM,KAAK,GAAG,KAAK,CAAC,OAAO,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC;QAClC,IAAI,CAAC,KAAK,CAAC,QAAQ,CAAC,GAAG,CAAC,KAAK,CAAC;YAAE,SAAS;QACzC,KAAK,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,EAAE,EAAE,KAAK,EAAE,GAAG,EAAE,CAAC,CAAC,EAAE,EAAE,KAAK,EAAE,CAAC,CAAC,KAAK,EAAE,CAAC,CAAC;IACxD,CAAC;IACD,KAAK,MAAM,KAAK,IAAI,KAAK,CAAC,YAAY;QAAE,IAAI,CAAC,KAAK,CAAC,GAAG,CAAC,KAAK,CAAC,GAAG,CAAC;YAAE,KAAK,CAAC,GAAG,CAAC,KAAK,CAAC,GAAG,EAAE,KAAK,CAAC,CAAC;IAC/F,OAAO,CAAC,GAAG,KAAK,CAAC,MAAM,EAAE,CAAC,CAAC;AAC7B,CAAC;AAED;;;;;;;;;;;;;;;;;;;GAmBG;AACH,SAAS,aAAa,CAAC,KAAkB,EAAE,MAAgB;IACzD,MAAM,KAAK,GAAG,MAAM,CAAC,MAAM,KAAK,CAAC,CAAC,CAAC,CAAC,GAAG,MAAM,CAAC,CAAC,CAAE,SAAS,CAAC,CAAC,CAAC,GAAG,MAAM,CAAC,IAAI,CAAC,IAAI,CAAC,UAAU,CAAC;IAC5F,OAAO,CACL,0CAA0C,KAAK,CAAC,GAAG,oDAAoD;QACvG,GAAG,KAAK,kGAAkG;QAC1G,+FAA+F;QAC/F,GAAG,KAAK,CAAC,KAAK,4BAA4B,KAAK,CAAC,KAAK,kDAAkD;QACvG,oGAAoG;QACpG,yDAAyD,CAC1D,CAAC;AACJ,CAAC;AAED,wGAAwG;AACxG,SAAS,aAAa,CAAC,KAAa,EAAE,SAAmB,EAAE,KAAa,EAAE,IAAY;IACpF,OAAO;QACL,QAAQ,EAAE,MAAM;QAChB,IAAI,EAAE,4BAA4B;QAClC,OAAO,EAAE,KAAK;QACd,OAAO,EACL,eAAe,KAAK,8BAA8B,SAAS,CAAC,MAAM,YAAY;YAC9E,IAAI,SAAS,CAAC,IAAI,CAAC,IAAI,CAAC,IAAI,KAAK,2DAA2D;YAC5F,6FAA6F;YAC7F,4FAA4F;YAC5F,6FAA6F;YAC7F,gDAAgD,IAAI,EAAE;KACzD,CAAC;AACJ,CAAC","sourcesContent":["/**\n * Who consumes a drawn element — and the datastore grade that reads the answer.\n *\n * The census used to walk the MAP's relationships alone, and under the extending\n * shape that is a partial view of the fleet: a service draws the container-level\n * call in its own `model.likec4` and the map carries the service-level twin, so a\n * store whose consuming edges were all drawn in models had ZERO consumers and\n * was graded on the one service the map happened to draw (R1). A model is\n * evidence about the fleet exactly as the map is, so both are counted here,\n * once, by one derivation two checks share.\n *\n * WHERE A STORE LIVES IS THE TEAM'S CHOICE, and this module used to say the\n * opposite at length. Nested under the element that resolves to its owner — in\n * that service's `extend <fqn> { store = database … }` block, or inside its\n * element on the map — and drawn as a fleet-level PEER are both first-class\n * placements, and loam grades neither as wrong. `landscape.datastore-private`\n * existed to push every one-consumer store into the first, its whole message a\n * prescription to delete the peer; it is retired (2026-09-07), the arm below is\n * gone, and no message here prescribes a move.\n *\n * What survives the retirement is the one CONSEQUENCE, which is a fact about\n * LikeC4 rather than a preference: a per-service project is `architecture/` plus\n * that ONE model, so an edge naming `<owner-fqn>.<store>` resolves for a SECOND\n * service only when the MAP declares the store. Written in the owner's `extend`\n * block instead, the same edge is two errors on the consumer's own model\n * (\"Could not resolve reference to Referenceable named 'store'\"). That is the\n * load-bearing half, it is stated by `reversibility` below where it actually\n * bites, and SCHEMA carries the same sentence.\n *\n * The choice is cheap, and the surviving message says exactly HOW cheap, because\n * \"reversible\" was false in the one state that used to assert it. The two\n * placements do give the store the same id, `<owner-fqn>.<store>` — but the\n * move is only free in ONE direction once a second service names it. Measured\n * on `examples/docs`: moving `db` out of order-service's `extend` block into\n * `orderService` on the map left the model at 12 elements · 8 relationships,\n * left `Covers: marketplace.orderService.db` in its arch.spec.md resolving, and\n * left checkout-web's container-level edge to it resolving too. Moving it BACK\n * with that second edge drawn is two errors on checkout-web's own model\n * (\"Could not resolve reference to Referenceable named 'db'\"), or\n * `landscape.invalid` plus one `spine.landscape-invalid` per service when the\n * second edge is drawn on the map — because an `extend` block is visible to its\n * own service's project alone. `datastore-shared` printed the unconditional\n * clause in 100% of the states it fires in, every one of which already has the\n * second consumer that makes map→extend break (verification 2026-09-04). So the\n * rule the message states is directional: extend→map is always safe; map→extend\n * only while no other service's model or map edge names the store.\n *\n * A sub-package of `fleet/` for `./isolation.ts`'s reason and with its rule: it\n * imports NOTHING from `fleet/`, so the map facts it reads arrive on the input\n * records the caller fills.\n */\nimport { type Elem, type Rel } from \"../../../../core/c4/likec4.js\";\nimport { DATASTORE_KIND } from \"../../../../core/vocabulary/maturity.js\";\nimport { type Finding } from \"../../../../core/vocabulary/report.js\";\nimport { type AttestedModel, type NestedStore } from \"./attest.js\";\n\nexport interface CensusInput {\n /** The map's own edges — a service-level twin is what the map carries of a container-level call. */\n relationships: Rel[];\n /** The services/<id>/ directories that exist: a consumer is a source filed under a real one. */\n services: ReadonlySet<string>;\n /** The shared element→service resolver every edge join uses. */\n resolve: (id: string) => string;\n /** What each service's own model attests (`./attest.ts`). */\n attested: readonly AttestedModel[];\n}\n\n/**\n * Distinct consumer SERVICES of an element, its nested children included: an\n * edge into `kafka.paymentEvents` is consumption of `kafka`.\n *\n * Two kinds of evidence, unioned. From the MAP: a relationship whose target is\n * the element or a descendant and whose source the shared resolver files under a\n * real `services/<id>/` — persons, other external systems and unresolved sources\n * never count, and two edges from one service are one consumer. From the\n * MODELS: a service whose own model attests an OUTBOUND call at the element or a\n * descendant.\n *\n * The model half reads EXTENDING models only, and the reason is the id space. An\n * extending model's ids are the map's fully-qualified ids — one project — so the\n * join is exact. A standalone model's ids are its own file's, and\n * `core/c4/resolve/attested.ts` says outright that a counterpart is never\n * matched to a landscape element: joining one would file a coincidence of\n * spelling as a coupling between two services.\n */\nexport function consumerCensus(input: CensusInput): (elementId: string) => string[] {\n const reaches = (elementId: string, id: string): boolean => id === elementId || id.startsWith(`${elementId}.`);\n return (elementId: string): string[] => {\n const out = new Set<string>();\n for (const r of input.relationships) {\n if (!reaches(elementId, r.target)) continue;\n const svc = input.resolve(r.source);\n if (input.services.has(svc)) out.add(svc);\n }\n for (const model of input.attested) {\n if (!model.extending) continue;\n if (model.calls.some((c) => c.direction === \"out\" && reaches(elementId, c.counterpartId))) {\n out.add(model.service);\n }\n }\n return [...out].sort();\n };\n}\n\nexport interface DatastoreInput {\n /** Service-LEVEL elements — the fleet map's own boxes, where a store drawn as a peer is found. */\n drawn: Elem[];\n /** Every element the MAP declares, at any depth — the other place a nested store is written. */\n elements: Elem[];\n /** Stores an extending model declares inside its own element (`./attest.ts`). */\n nestedStores: readonly NestedStore[];\n /** The services/<id>/ directories that exist. */\n services: ReadonlySet<string>;\n /** The shared element→service resolver, for naming the consumer's own element on the map. */\n resolve: (id: string) => string;\n /**\n * `census.ts`'s one predicate for \"this element stands for a `services/<id>/`\",\n * injected because a `fleet/map/` module may not import `fleet/`. Both halves\n * of the grade below skip such an element: it is the fleet itself, not its\n * shape, and a store bound to a directory is the binding checks' subject.\n * `./attest.ts` applies the same predicate to the stores a MODEL declares, so\n * all three places answer it once.\n */\n standsForService: (e: Elem) => boolean;\n /** The census above, built once and shared with the platform-candidate advisory. */\n consumers: (elementId: string) => string[];\n}\n\n/**\n * A datastore two or more services reach is the strongest coupling two services\n * can have and the hardest to undo, which deserves to be STATED rather than\n * inferred. That is the whole of this grade now, and it applies to both\n * placements alike: a fleet-level peer and a store nested under its owner earn\n * the same `landscape.datastore-shared` at two consumers, and neither is graded\n * at one.\n *\n * The one-consumer arm is gone (2026-09-07). A fleet-level peer with a single\n * consumer used to be `landscape.datastore-private` — \"drawn as a peer it reads\n * as a system in its own right … delete this peer\" — which made WHERE a store is\n * drawn a restriction rather than the team's choice. It was never a fact about\n * the fleet: the same store, nested, was silent, so the finding graded the\n * drawing and not the architecture. The nested half was ALREADY silent at one\n * consumer, and it is that half's rule that now holds for both.\n */\nexport function datastoreFindings(input: DatastoreInput): Finding[] {\n const findings: Finding[] = [];\n for (const e of input.drawn) {\n // An element that stands for a real service is the fleet itself, not its\n // shape; a datastore bound to a directory is graded by the binding checks.\n if (input.standsForService(e)) continue;\n if (e.kind.toLowerCase() !== DATASTORE_KIND) continue;\n const consumers = input.consumers(e.id);\n // One consumer is a store that service owns, drawn where the team chose to\n // draw it — a fact about the map's layout, not about the fleet. Zero is a\n // store nothing reaches, which was never graded here either.\n if (consumers.length < 2) continue;\n findings.push(sharedFinding(e.title, consumers, \"\", \"\"));\n }\n for (const store of nestedStoresOf(input)) {\n // The owner is a consumer by construction — the store sits inside the element\n // that resolves to it — so silence at one consumer IS the fixed state, and\n // the only question left is whether a second service reaches the same data.\n const consumers = [...new Set([store.owner, ...input.consumers(store.fqn)])].sort();\n if (consumers.length < 2) continue;\n findings.push(\n sharedFinding(\n store.title,\n consumers,\n // The store's own id, and said as such. The clause read \"nested under\n // '<fqn>'\", which sends a reader looking for an element that HOLDS the\n // store — when that id IS the store, and its container is its prefix.\n ` — ${store.owner}'s own store, written as '${store.fqn}'`,\n reversibility(store, consumers.filter((c) => c !== store.owner)),\n ),\n );\n }\n\n return findings;\n}\n\n/**\n * Every store nested under the element that resolves to its owner, from both\n * places the stance allows one to be written: inside the service's element ON\n * THE MAP, and in the service's own extending model. Keyed by fqn, so a\n * map-declared store — which the owner's model slice carries too, since a slice\n * is everything resolving to the service — is one store and not two.\n *\n * Both sources are needed and neither is redundant. The map's covers a service\n * whose model is standalone or absent (no slice to read); the model's covers a\n * store the map never draws, which is the placement the adopt brief prescribes.\n */\nfunction nestedStoresOf(input: DatastoreInput): NestedStore[] {\n const drawnIds = new Set(input.drawn.map((e) => e.id));\n const byFqn = new Map<string, NestedStore>();\n for (const e of input.elements) {\n if (e.kind.toLowerCase() !== DATASTORE_KIND || drawnIds.has(e.id)) continue;\n // The peer branch's guard, which this half was missing: a `database` bound\n // with `metadata { service '<id>' }` and drawn one level down became a store\n // \"owned\" by its own binding, so a second reader made it a shared datastore\n // and the fix offered was to give each service its own copy of a service.\n if (input.standsForService(e)) continue;\n const owner = input.resolve(e.id);\n if (!input.services.has(owner)) continue;\n byFqn.set(e.id, { owner, fqn: e.id, title: e.title });\n }\n for (const store of input.nestedStores) if (!byFqn.has(store.fqn)) byFqn.set(store.fqn, store);\n return [...byFqn.values()];\n}\n\n/**\n * Which way the store may still be moved, now that a second service reaches it.\n *\n * The clause this replaces said the two placements were interchangeable — \"the\n * two placements give it the same id … so moving the declaration between them\n * rewrites no edge\" — and that is false in every state `datastore-shared` fires\n * in, because the finding needs a second consumer and a second consumer's edge\n * resolves only against a store the MAP declares (a per-service project is\n * `architecture/` plus one model). Measured: performing the move the sentence\n * called free turned a clean `examples/docs` into `c4.invalid` on the consumer's\n * model, or into `landscape.invalid` + one `spine.landscape-invalid` per service\n * when the second edge is on the map (verification 2026-09-04).\n *\n * The direction is stated rather than the current placement, and it holds\n * either way round: out of the owner's `extend` block onto the map is always\n * safe, back in is not while another service names the store. (In practice a\n * store this finding reaches is one the MAP declares — a second consumer's edge\n * cannot resolve against an `extend` block, so it could never have been drawn\n * — which is the same fact said from the other side.)\n */\nfunction reversibility(store: NestedStore, others: string[]): string {\n const named = others.length === 1 ? `${others[0]!}'s edge` : `${others.join(\", \")}'s edges`;\n return (\n `. Both placements give it the same id, ${store.fqn}, but with a second consumer the move is ONE-WAY: ` +\n `${named} resolves only against a declaration the MAP holds — a per-service project is \\`architecture/\\` ` +\n `plus one model, so an \\`extend\\` block is visible to its own service's project alone. Out of ` +\n `${store.owner}'s \\`extend\\` block onto ${store.owner}'s element on the map is always safe; moving it ` +\n `back INTO the \\`extend\\` block breaks that edge — \\`c4.invalid\\` on the other service's model, or ` +\n `\\`landscape.invalid\\` when the edge is drawn on the map`\n );\n}\n\n/** The two-or-more message, shared by the peer and the nested store — one word about coupling, once. */\nfunction sharedFinding(title: string, consumers: string[], where: string, tail: string): Finding {\n return {\n severity: \"warn\",\n code: \"landscape.datastore-shared\",\n subject: title,\n message:\n `landscape: '${title}' is a datastore shared by ${consumers.length} services ` +\n `(${consumers.join(\", \")})${where} — the strongest coupling two services can have, and the ` +\n `hardest to undo. Shared means the same DATA: if they read the same tables or keys, keep it ` +\n `where it is and let this warning state the coupling; if they only share a host or cluster ` +\n `(two schemas, two lock paths), that is operational blast radius — a runbook fact — and the ` +\n `honest model is one private store per service${tail}`,\n };\n}\n"]}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"scorecard.js","sourceRoot":"","sources":["../../../../../src/commands/validate/fleet/scorecard/scorecard.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * The fleet scorecard: ceiling-vs-actual aggregates for `validate --all`,\n * recomputed per invocation from the same memoized FleetContext reads the run\n * already paid for. loam stores no history — week-over-week tracking is the\n * caller's pipeline's job: capture the `scorecard` payload key per run into a\n * metrics store. Each axis is a pair of domain-named counts (`defined` vs\n * `governed`) rather than abstract ceiling/actual keys, because payload keys\n * freeze the moment they ship and the domain nouns survive axis nuance.\n *\n * This module derives and never prints; `./print.js` renders the one table\n * the `--all` text report appends, and `./contracts.js` owns the per-service\n * contract reads it sums.\n */\nimport { existsSync } from \"node:fs\";\nimport { inOrder } from \"../../../../core/kernel/concurrency.js\";\nimport { coversElement } from \"../../../../core/c4/arch.js\";\nimport { serviceResolver } from \"../../../../core/c4/resolve/service.js\";\nimport { type FleetContext } from \"../../../../core/fleet-context.js\";\nimport { type DocsDir } from \"../../../../core/kernel/ids/dirs.js\";\nimport { landscapePath } from \"../../../../core/repo/paths.js\";\nimport { fleetStatus } from \"../../../../core/status/fleet/fleet.js\";\nimport { ARTIFACT_STATUSES, type ArtifactStatus } from \"../../../../core/status/report.js\";\nimport { ANSWERED_BY, type AnsweredBy } from \"../../../../core/verify/answers.js\";\nimport { VERIFICATION_VERDICTS, type VerificationVerdict } from \"../../../../core/verify/record.js\";\nimport { maturityRollup, type Maturity } from \"../../../../core/vocabulary/maturity.js\";\nimport { subjectsWith, type TargetReport } from \"../../../../core/vocabulary/report.js\";\nimport { featureVerification, serviceViews, type VerificationCell } from \"../../../list/views.js\";\nimport { drawnSystems } from \"../census.js\";\nimport { readLandscape } from \"../load.js\";\nimport { adoptionOf, type Adoption } from \"./adoption.js\";\nimport { contractCounts, type ContractCounts } from \"./contracts.js\";\n\n/** What `buildScorecard` needs of the run that is already in hand. */\nexport interface ScorecardInput {\n docsDir: DocsDir;\n /** The run's graded targets — the source for the one count only a repo-bound check can make (`sources.stale` walks the service's own repository, and must not run twice). */\n targets: TargetReport[];\n /** The run's memoized snapshot: every read below is a hit on it. */\n fleet: FleetContext;\n /** loam.json's `service` — the one repo whose `sources` are checkable from here (serviceViews' rule). */\n boundService: string | undefined;\n}\n\n/**\n * The additive `scorecard` payload key of `validate --all --json`, and the\n * `--all` text table. Shape frozen on ship, like every payload key: fields\n * may be added, never changed or removed.\n */\nexport interface Scorecard {\n /** Every `services/` directory the enumeration lists — the maturity rollup's denominator. */\n services: number;\n /** Counts per adoption-maturity rung, every rung present (core/vocabulary/maturity.ts). */\n maturity: Record<Maturity, number>;\n provenance: {\n /** Services at the `vouched` rung — a person stamped them. */\n vouched: number;\n /**\n * Of those, the ones where the person read a recorded SAMPLE of the\n * document rather than all of it, by subject off this run's\n * `sources.sampled-vouch` findings.\n *\n * Additive beside `vouched` rather than subtracted from it, because the\n * rung is real — a person did stamp it — but a dial reading\n * `vouched: 120` where 100 of them are sampled is exactly the\n * partial-trust-as-full-trust misreading the scope exists to prevent. A\n * dashboard that wants the stricter number subtracts this one knowingly.\n */\n sampledVouched: number;\n /**\n * Services whose stamped sources digest no longer matches the code, by\n * subject off this run's `sources.stale` findings — only what THIS run\n * could verify, so read it beside the count below, never alone.\n */\n staleDigests: number;\n /** Services whose declared `sources` only their own repos can check. */\n unverifiableFromHere: number;\n };\n verification: {\n /**\n * Active features with a verification record beside them. Its ceiling is\n * `features.active` — deliberately not repeated here: one denominator,\n * derived once, so the two axes cannot disagree about how many features\n * are in flight.\n */\n recorded: number;\n /** The recorded features' three-valued verdicts (core/verify/record.ts), every key present. */\n verdicts: Record<VerificationVerdict, number>;\n claims: {\n /** Questions asked across every recorded feature, `tallyRecord`'s counting. */\n total: number;\n confirmed: number;\n /**\n * Of the confirmed, one count per `answered_by` value (`ANSWERED_BY`),\n * every key present — the stable-shape rule `verdicts` and `stages`\n * follow, so a fourth provenance would appear rather than be absorbed.\n *\n * Read off the answers' own provenance, never by subtracting the\n * attested share: `attestedClaims` counts `scenario.tested` alone, so\n * `confirmed - attested` filed every OTHER kind an agent confirmed — a\n * permission, a capability, an exposed operation — under a test runner\n * that had not run. A record with four agent answers printed as three\n * runner and one agent, which is the reading this axis exists to give.\n */\n answered: Record<AnsweredBy, number>;\n };\n };\n /**\n * The HTTP axis, with a deliberate UNIT split: `defined`/`governed` count\n * contract SLOTS, exactly as the API axis's own findings count operations,\n * while `deprecated`/`deprecatedStillConsumed` count DISTINCT operationIds —\n * a duplicate-id slot pair (already its own warning) retires as ONE\n * operation, one migration owed.\n */\n operations: {\n /** Slots the living contracts define (removal markers are never operations). */\n defined: number;\n /** Slots (like `defined`) whose id a living requirement's `Operations:` line governs. */\n governed: number;\n /** Distinct deprecated operationIds. */\n deprecated: number;\n /** Distinct deprecated ids something still joins to — a living `Operations:` line, or an op-linked inbound landscape edge. */\n deprecatedStillConsumed: number;\n };\n messages: {\n /** Distinct message names the living async contracts declare. */\n defined: number;\n /** Of them, the ones a requirement's `Publishes:`/`Consumes:` line governs. */\n linked: number;\n };\n c4: {\n /** The drawn SYSTEMS — `drawnSystems`, the fleet map's own census, so the map's exemptions (actors, `#external`, groupings, containers) and this count cannot drift. */\n elements: number;\n /** Of them, the ones some living arch requirement's `Covers:` entry names. */\n covered: number;\n };\n /**\n * Services PARTICIPATING in each contract axis — how far the staged\n * adoption has spread, axis by axis. The denominator is `services`, carried\n * once above like every other denominator here. One mechanical rule per\n * axis, per service: `requirements` — spec.md holds at least one\n * `### Requirement:` block (content, not file presence, so a fleet of empty\n * adopt scaffolds honestly reads \"not started\"); `arch` — the same for\n * arch.spec.md; `openapi` / `asyncapi` — the contract file exists;\n * `permissions` / `capabilities` — at least one non-REMOVED requirement in\n * either spec document carries a `Requires:` / `Capability:` entry (the\n * used-set rule the fleet vocabulary checks apply). An axis at 0 is \"not\n * started\" — distinct from partially adopted — and is what licenses the\n * text report's per-axis warning banner (./adoption.js). An unreadable\n * service counts as participating in nothing, the same zeros doctrine as\n * every count here — read these beside the run's `service.unreadable`\n * findings, and note the text renderer refuses to group warnings on a run\n * that has any (report.ts).\n */\n adoption: Adoption;\n features: {\n active: number;\n /**\n * The fleet form's stage per active feature — all five ARTIFACT_STATUSES\n * keys even though that form never grades `draft` (the maturityRollup\n * rule: a stable shape a dashboard can diff).\n */\n stages: Record<ArtifactStatus, number>;\n };\n}\n\n/**\n * Derive the scorecard, or decline. The failure story, per axis: a memoized\n * read that FAILED rejects again on every await, so the per-service fan-out\n * contains those per subject — an unreadable service contributes zeros while\n * its own `service.unreadable` finding names the cause — and every landscape\n * read goes through `../load.ts`'s containment, so an unreadable fleet map\n * zeroes the map-derived axes on every platform instead of deleting the card.\n * The catch is the last line of defence for the fleet-wide reads this module\n * does not own (the verification cells, the fleet status): a rollup that can\n * die must not take the graded report with it. No number rather than a wrong\n * number.\n */\nexport async function buildScorecard(input: ScorecardInput): Promise<Scorecard | null> {\n try {\n return await derive(input);\n } catch {\n // Unreadable is not zero: the axes cannot be counted, so no claim is made.\n return null;\n }\n}\n\nasync function derive(input: ScorecardInput): Promise<Scorecard> {\n const { docsDir, targets, fleet, boundService } = input;\n const services = await fleet.listServices(docsDir);\n\n // The landscape, through the run's memo and the fleet target's containment:\n // an unreadable file grades as one that did not parse, and a landscape that\n // proves nothing contributes no inbound edges and no element census —\n // fail-closed, exactly as the no-openapi grace reads the same absence.\n // Loaded FIRST and handed to serviceViews below, so the maturity rollup\n // costs no second parse of a document this run already holds.\n const lp = landscapePath(docsDir);\n const land = existsSync(lp) ? await readLandscape(() => fleet.loadLikeC4(lp)) : null;\n const proven = land !== null && land.errors.length === 0;\n\n const views = await serviceViews(docsDir, services, boundService, land);\n const maturity = maturityRollup(views);\n\n // Inbound op-linked edges per service, resolved exactly as the API axis\n // resolves them (service/api.ts): the container-aware resolver, so an edge\n // drawn into `paymentService.api` counts for the service that owns it.\n const known: ReadonlySet<string> = new Set(services.map((s) => s.id));\n const inbound = new Map<string, Set<string>>();\n if (proven) {\n const svcOf = serviceResolver(land.elements, known);\n for (const r of land.relationships) {\n if (r.op === undefined) continue;\n const owner: string = svcOf(r.target);\n const ops = inbound.get(owner) ?? new Set<string>();\n ops.add(r.op);\n inbound.set(owner, ops);\n }\n }\n\n // Every read in THIS fan-out is a memo hit — the service targets already\n // parsed both requirement documents and both contracts — so it spawns no new\n // filesystem work and needs no pool (unlike the verification fan-out below).\n const perService = await Promise.all(\n services.map((svc) => contractCounts(svc, fleet, inbound.get(svc.id) ?? new Set<string>())),\n );\n const sum = (pick: (c: ContractCounts) => number): number =>\n perService.reduce((n, c) => n + pick(c), 0);\n const operations = {\n defined: sum((c) => c.operations.defined),\n governed: sum((c) => c.operations.governed),\n deprecated: sum((c) => c.operations.deprecated),\n deprecatedStillConsumed: sum((c) => c.operations.deprecatedStillConsumed),\n };\n const messages = { defined: sum((c) => c.messages.defined), linked: sum((c) => c.messages.linked) };\n const adoption = adoptionOf(perService);\n\n // C4 coverage: the fleet map's own system census (`drawnSystems` — the set\n // the undocumented pass walks) against every living arch.spec.md's `Covers:`\n // entries. Element entries only — an edge entry names no element.\n const covers = perService.flatMap((c) => c.covers);\n const eligible = proven ? drawnSystems(land.elements, known) : [];\n const covered = eligible.filter((e) => covers.some((c) => coversElement(c, e))).length;\n\n // Verification: `list`'s own cells, so the two commands cannot disagree\n // about a record's verdict. Active features only — an archived record is\n // frozen history, not fleet debt. This fan-out is NOT memo hits:\n // readVerification is one small file read per feature, and a recorded cell\n // additionally derives its checklist (the same cost `list` pays for the same\n // column) — real filesystem work, so it runs through the bounded pool.\n const features = await fleet.listFeatures(docsDir);\n const cells = await inOrder(features, (f) => featureVerification(docsDir, f));\n const recorded = cells.filter((c): c is VerificationCell => c !== null);\n const verdicts = Object.fromEntries(VERIFICATION_VERDICTS.map((v) => [v, 0])) as Record<VerificationVerdict, number>;\n for (const cell of recorded) verdicts[cell.verdict] += 1;\n const confirmed = recorded.reduce((n, c) => n + c.confirmed, 0);\n const answered = Object.fromEntries(\n ANSWERED_BY.map((who) => [who, recorded.reduce((n, c) => n + c.answered[who], 0)]),\n ) as Record<AnsweredBy, number>;\n\n // The fleet form of `status`, on this run's own snapshot: presence and\n // dependency order, nothing graded — the stages an agent already branches on.\n const status = await fleetStatus(docsDir, { context: fleet });\n const stages = Object.fromEntries(ARTIFACT_STATUSES.map((s) => [s, 0])) as Record<ArtifactStatus, number>;\n for (const f of status.features) stages[f.stage] += 1;\n\n return {\n services: services.length,\n maturity,\n provenance: {\n vouched: maturity.vouched,\n // Both counts join through `subjectsWith` on the run's OWN findings, so\n // each is structurally the same number as the report beside it — the\n // unverifiable count is the footer's (unverifiableSubjects is the same\n // call on the same code; this package cannot import the parent back).\n // Both codes are LITERALS — provenance/sources.ts's own spellings —\n // because the stable-code collector reads counting sites and refuses a\n // slot it cannot read; the collector is also what convicts a typo here.\n staleDigests: subjectsWith(targets, \"sources.stale\"),\n unverifiableFromHere: subjectsWith(targets, \"sources.unverifiable-from-here\"),\n // Doc-side, so unlike `staleDigests` this one is complete even from the\n // docs repo: `sources.sampled-vouch` needs no service repo to fire.\n sampledVouched: subjectsWith(targets, \"sources.sampled-vouch\"),\n },\n verification: {\n recorded: recorded.length,\n verdicts,\n claims: {\n total: recorded.reduce((n, c) => n + c.claims, 0),\n confirmed,\n answered,\n },\n },\n operations,\n messages,\n c4: { elements: eligible.length, covered },\n adoption,\n features: { active: status.features.length, stages },\n };\n}\n"]}
|
|
1
|
+
{"version":3,"file":"scorecard.js","sourceRoot":"","sources":["../../../../../src/commands/validate/fleet/scorecard/scorecard.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;GAYG;AACH,OAAO,EAAE,UAAU,EAAE,MAAM,SAAS,CAAC;AACrC,OAAO,EAAE,OAAO,EAAE,MAAM,wCAAwC,CAAC;AACjE,OAAO,EAAE,aAAa,EAAE,MAAM,6BAA6B,CAAC;AAC5D,OAAO,EAAE,eAAe,EAAE,MAAM,wCAAwC,CAAC;AACzE,OAAO,EAAqB,MAAM,mCAAmC,CAAC;AACtE,OAAO,EAAgB,MAAM,qCAAqC,CAAC;AACnE,OAAO,EAAE,aAAa,EAAE,MAAM,gCAAgC,CAAC;AAC/D,OAAO,EAAE,WAAW,EAAE,MAAM,wCAAwC,CAAC;AACrE,OAAO,EAAE,iBAAiB,EAAuB,MAAM,mCAAmC,CAAC;AAC3F,OAAO,EAAE,WAAW,EAAmB,MAAM,oCAAoC,CAAC;AAClF,OAAO,EAAE,qBAAqB,EAA4B,MAAM,mCAAmC,CAAC;AACpG,OAAO,EAAE,cAAc,EAAiB,MAAM,yCAAyC,CAAC;AACxF,OAAO,EAAE,YAAY,EAAqB,MAAM,uCAAuC,CAAC;AACxF,OAAO,EAAE,mBAAmB,EAAE,YAAY,EAAyB,MAAM,wBAAwB,CAAC;AAClG,OAAO,EAAE,YAAY,EAAE,MAAM,cAAc,CAAC;AAC5C,OAAO,EAAE,aAAa,EAAE,MAAM,YAAY,CAAC;AAC3C,OAAO,EAAE,UAAU,EAAiB,MAAM,eAAe,CAAC;AAC1D,OAAO,EAAE,cAAc,EAAuB,MAAM,gBAAgB,CAAC;AAuIrE;;;;;;;;;;;GAWG;AACH,MAAM,CAAC,KAAK,UAAU,cAAc,CAAC,KAAqB;IACxD,IAAI,CAAC;QACH,OAAO,MAAM,MAAM,CAAC,KAAK,CAAC,CAAC;IAC7B,CAAC;IAAC,MAAM,CAAC;QACP,2EAA2E;QAC3E,OAAO,IAAI,CAAC;IACd,CAAC;AACH,CAAC;AAED,KAAK,UAAU,MAAM,CAAC,KAAqB;IACzC,MAAM,EAAE,OAAO,EAAE,OAAO,EAAE,KAAK,EAAE,YAAY,EAAE,GAAG,KAAK,CAAC;IACxD,MAAM,QAAQ,GAAG,MAAM,KAAK,CAAC,YAAY,CAAC,OAAO,CAAC,CAAC;IAEnD,4EAA4E;IAC5E,4EAA4E;IAC5E,sEAAsE;IACtE,uEAAuE;IACvE,wEAAwE;IACxE,8DAA8D;IAC9D,MAAM,EAAE,GAAG,aAAa,CAAC,OAAO,CAAC,CAAC;IAClC,MAAM,IAAI,GAAG,UAAU,CAAC,EAAE,CAAC,CAAC,CAAC,CAAC,MAAM,aAAa,CAAC,GAAG,EAAE,CAAC,KAAK,CAAC,UAAU,CAAC,EAAE,CAAC,CAAC,CAAC,CAAC,CAAC,IAAI,CAAC;IACrF,MAAM,MAAM,GAAG,IAAI,KAAK,IAAI,IAAI,IAAI,CAAC,MAAM,CAAC,MAAM,KAAK,CAAC,CAAC;IAEzD,MAAM,KAAK,GAAG,MAAM,YAAY,CAAC,OAAO,EAAE,QAAQ,EAAE,YAAY,EAAE,IAAI,CAAC,CAAC;IACxE,MAAM,QAAQ,GAAG,cAAc,CAAC,KAAK,CAAC,CAAC;IAEvC,wEAAwE;IACxE,2EAA2E;IAC3E,uEAAuE;IACvE,MAAM,KAAK,GAAwB,IAAI,GAAG,CAAC,QAAQ,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC;IACtE,MAAM,OAAO,GAAG,IAAI,GAAG,EAAuB,CAAC;IAC/C,IAAI,MAAM,EAAE,CAAC;QACX,MAAM,KAAK,GAAG,eAAe,CAAC,IAAI,CAAC,QAAQ,EAAE,KAAK,CAAC,CAAC;QACpD,KAAK,MAAM,CAAC,IAAI,IAAI,CAAC,aAAa,EAAE,CAAC;YACnC,IAAI,CAAC,CAAC,EAAE,KAAK,SAAS;gBAAE,SAAS;YACjC,MAAM,KAAK,GAAW,KAAK,CAAC,CAAC,CAAC,MAAM,CAAC,CAAC;YACtC,MAAM,GAAG,GAAG,OAAO,CAAC,GAAG,CAAC,KAAK,CAAC,IAAI,IAAI,GAAG,EAAU,CAAC;YACpD,GAAG,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC;YACd,OAAO,CAAC,GAAG,CAAC,KAAK,EAAE,GAAG,CAAC,CAAC;QAC1B,CAAC;IACH,CAAC;IAED,yEAAyE;IACzE,6EAA6E;IAC7E,6EAA6E;IAC7E,MAAM,UAAU,GAAG,MAAM,OAAO,CAAC,GAAG,CAClC,QAAQ,CAAC,GAAG,CAAC,CAAC,GAAG,EAAE,EAAE,CAAC,cAAc,CAAC,GAAG,EAAE,KAAK,EAAE,OAAO,CAAC,GAAG,CAAC,GAAG,CAAC,EAAE,CAAC,IAAI,IAAI,GAAG,EAAU,CAAC,CAAC,CAC5F,CAAC;IACF,MAAM,GAAG,GAAG,CAAC,IAAmC,EAAU,EAAE,CAC1D,UAAU,CAAC,MAAM,CAAC,CAAC,CAAC,EAAE,CAAC,EAAE,EAAE,CAAC,CAAC,GAAG,IAAI,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC;IAC9C,MAAM,UAAU,GAAG;QACjB,OAAO,EAAE,GAAG,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,UAAU,CAAC,OAAO,CAAC;QACzC,QAAQ,EAAE,GAAG,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,UAAU,CAAC,QAAQ,CAAC;QAC3C,UAAU,EAAE,GAAG,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,UAAU,CAAC,UAAU,CAAC;QAC/C,uBAAuB,EAAE,GAAG,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,UAAU,CAAC,uBAAuB,CAAC;KAC1E,CAAC;IACF,MAAM,QAAQ,GAAG,EAAE,OAAO,EAAE,GAAG,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,QAAQ,CAAC,OAAO,CAAC,EAAE,MAAM,EAAE,GAAG,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,QAAQ,CAAC,MAAM,CAAC,EAAE,CAAC;IACpG,MAAM,QAAQ,GAAG,UAAU,CAAC,UAAU,CAAC,CAAC;IAExC,2EAA2E;IAC3E,6EAA6E;IAC7E,kEAAkE;IAClE,MAAM,MAAM,GAAG,UAAU,CAAC,OAAO,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,MAAM,CAAC,CAAC;IACnD,MAAM,QAAQ,GAAG,MAAM,CAAC,CAAC,CAAC,YAAY,CAAC,IAAI,CAAC,QAAQ,EAAE,KAAK,CAAC,CAAC,CAAC,CAAC,EAAE,CAAC;IAClE,MAAM,OAAO,GAAG,QAAQ,CAAC,MAAM,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,MAAM,CAAC,IAAI,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,aAAa,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,CAAC,CAAC,MAAM,CAAC;IAEvF,wEAAwE;IACxE,yEAAyE;IACzE,iEAAiE;IACjE,2EAA2E;IAC3E,6EAA6E;IAC7E,uEAAuE;IACvE,MAAM,QAAQ,GAAG,MAAM,KAAK,CAAC,YAAY,CAAC,OAAO,CAAC,CAAC;IACnD,MAAM,KAAK,GAAG,MAAM,OAAO,CAAC,QAAQ,EAAE,CAAC,CAAC,EAAE,EAAE,CAAC,mBAAmB,CAAC,OAAO,EAAE,CAAC,CAAC,CAAC,CAAC;IAC9E,MAAM,QAAQ,GAAG,KAAK,CAAC,MAAM,CAAC,CAAC,CAAC,EAAyB,EAAE,CAAC,CAAC,KAAK,IAAI,CAAC,CAAC;IACxE,MAAM,QAAQ,GAAG,MAAM,CAAC,WAAW,CAAC,qBAAqB,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,CAAwC,CAAC;IACrH,KAAK,MAAM,IAAI,IAAI,QAAQ;QAAE,QAAQ,CAAC,IAAI,CAAC,OAAO,CAAC,IAAI,CAAC,CAAC;IACzD,MAAM,SAAS,GAAG,QAAQ,CAAC,MAAM,CAAC,CAAC,CAAC,EAAE,CAAC,EAAE,EAAE,CAAC,CAAC,GAAG,CAAC,CAAC,SAAS,EAAE,CAAC,CAAC,CAAC;IAChE,MAAM,QAAQ,GAAG,MAAM,CAAC,WAAW,CACjC,WAAW,CAAC,GAAG,CAAC,CAAC,GAAG,EAAE,EAAE,CAAC,CAAC,GAAG,EAAE,QAAQ,CAAC,MAAM,CAAC,CAAC,CAAC,EAAE,CAAC,EAAE,EAAE,CAAC,CAAC,GAAG,CAAC,CAAC,QAAQ,CAAC,GAAG,CAAC,EAAE,CAAC,CAAC,CAAC,CAAC,CACrD,CAAC;IAEhC,uEAAuE;IACvE,8EAA8E;IAC9E,MAAM,MAAM,GAAG,MAAM,WAAW,CAAC,OAAO,EAAE,EAAE,OAAO,EAAE,KAAK,EAAE,CAAC,CAAC;IAC9D,MAAM,MAAM,GAAG,MAAM,CAAC,WAAW,CAAC,iBAAiB,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,CAAmC,CAAC;IAC1G,KAAK,MAAM,CAAC,IAAI,MAAM,CAAC,QAAQ;QAAE,MAAM,CAAC,CAAC,CAAC,KAAK,CAAC,IAAI,CAAC,CAAC;IAEtD,OAAO;QACL,QAAQ,EAAE,QAAQ,CAAC,MAAM;QACzB,QAAQ;QACR,UAAU,EAAE;YACV,OAAO,EAAE,QAAQ,CAAC,OAAO;YACzB,wEAAwE;YACxE,qEAAqE;YACrE,uEAAuE;YACvE,sEAAsE;YACtE,oEAAoE;YACpE,uEAAuE;YACvE,wEAAwE;YACxE,YAAY,EAAE,YAAY,CAAC,OAAO,EAAE,eAAe,CAAC;YACpD,oBAAoB,EAAE,YAAY,CAAC,OAAO,EAAE,gCAAgC,CAAC;YAC7E,wEAAwE;YACxE,oEAAoE;YACpE,cAAc,EAAE,YAAY,CAAC,OAAO,EAAE,uBAAuB,CAAC;SAC/D;QACD,YAAY,EAAE;YACZ,QAAQ,EAAE,QAAQ,CAAC,MAAM;YACzB,QAAQ;YACR,MAAM,EAAE;gBACN,KAAK,EAAE,QAAQ,CAAC,MAAM,CAAC,CAAC,CAAC,EAAE,CAAC,EAAE,EAAE,CAAC,CAAC,GAAG,CAAC,CAAC,MAAM,EAAE,CAAC,CAAC;gBACjD,SAAS;gBACT,QAAQ;aACT;SACF;QACD,UAAU;QACV,QAAQ;QACR,EAAE,EAAE,EAAE,QAAQ,EAAE,QAAQ,CAAC,MAAM,EAAE,OAAO,EAAE;QAC1C,QAAQ;QACR,QAAQ,EAAE,EAAE,MAAM,EAAE,MAAM,CAAC,QAAQ,CAAC,MAAM,EAAE,MAAM,EAAE;KACrD,CAAC;AACJ,CAAC","sourcesContent":["/**\n * The fleet scorecard: ceiling-vs-actual aggregates for `validate --all`,\n * recomputed per invocation from the same memoized FleetContext reads the run\n * already paid for. loam stores no history — week-over-week tracking is the\n * caller's pipeline's job: capture the `scorecard` payload key per run into a\n * metrics store. Each axis is a pair of domain-named counts (`defined` vs\n * `governed`) rather than abstract ceiling/actual keys, because payload keys\n * freeze the moment they ship and the domain nouns survive axis nuance.\n *\n * This module derives and never prints; `./print.js` renders the one table\n * the `--all` text report appends, and `./contracts.js` owns the per-service\n * contract reads it sums.\n */\nimport { existsSync } from \"node:fs\";\nimport { inOrder } from \"../../../../core/kernel/concurrency.js\";\nimport { coversElement } from \"../../../../core/c4/arch.js\";\nimport { serviceResolver } from \"../../../../core/c4/resolve/service.js\";\nimport { type FleetContext } from \"../../../../core/fleet-context.js\";\nimport { type DocsDir } from \"../../../../core/kernel/ids/dirs.js\";\nimport { landscapePath } from \"../../../../core/repo/paths.js\";\nimport { fleetStatus } from \"../../../../core/status/fleet/fleet.js\";\nimport { ARTIFACT_STATUSES, type ArtifactStatus } from \"../../../../core/status/report.js\";\nimport { ANSWERED_BY, type AnsweredBy } from \"../../../../core/verify/answers.js\";\nimport { VERIFICATION_VERDICTS, type VerificationVerdict } from \"../../../../core/verify/record.js\";\nimport { maturityRollup, type Maturity } from \"../../../../core/vocabulary/maturity.js\";\nimport { subjectsWith, type TargetReport } from \"../../../../core/vocabulary/report.js\";\nimport { featureVerification, serviceViews, type VerificationCell } from \"../../../list/views.js\";\nimport { drawnSystems } from \"../census.js\";\nimport { readLandscape } from \"../load.js\";\nimport { adoptionOf, type Adoption } from \"./adoption.js\";\nimport { contractCounts, type ContractCounts } from \"./contracts.js\";\n\n/** What `buildScorecard` needs of the run that is already in hand. */\nexport interface ScorecardInput {\n docsDir: DocsDir;\n /** The run's graded targets — the source for the one count only a repo-bound check can make (`sources.stale` walks the service's own repository, and must not run twice). */\n targets: TargetReport[];\n /** The run's memoized snapshot: every read below is a hit on it. */\n fleet: FleetContext;\n /** loam.json's `service` — the one repo whose `sources` are checkable from here (serviceViews' rule). */\n boundService: string | undefined;\n}\n\n/**\n * The additive `scorecard` payload key of `validate --all --json`, and the\n * `--all` text table. Shape frozen on ship, like every payload key: fields\n * may be added, never changed or removed.\n */\nexport interface Scorecard {\n /** Every `services/` directory the enumeration lists — the maturity rollup's denominator. */\n services: number;\n /** Counts per adoption-maturity rung, every rung present (core/vocabulary/maturity.ts). */\n maturity: Record<Maturity, number>;\n provenance: {\n /** Services at the `vouched` rung — a person stamped them. */\n vouched: number;\n /**\n * Of those, the ones where the person read a recorded SAMPLE of the\n * document rather than all of it, by subject off this run's\n * `sources.sampled-vouch` findings.\n *\n * Additive beside `vouched` rather than subtracted from it, because the\n * rung is real — a person did stamp it — but a dial reading\n * `vouched: 120` where 100 of them are sampled is exactly the\n * partial-trust-as-full-trust misreading the scope exists to prevent. A\n * dashboard that wants the stricter number subtracts this one knowingly.\n */\n sampledVouched: number;\n /**\n * Services whose stamped sources digest no longer matches the code, by\n * subject off this run's `sources.stale` findings — only what THIS run\n * could verify, so read it beside the count below, never alone.\n */\n staleDigests: number;\n /** Services whose declared `sources` only their own repos can check. */\n unverifiableFromHere: number;\n };\n verification: {\n /**\n * Active features with a verification record beside them. Its ceiling is\n * `features.active` — deliberately not repeated here: one denominator,\n * derived once, so the two axes cannot disagree about how many features\n * are in flight.\n */\n recorded: number;\n /** The recorded features' three-valued verdicts (core/verify/record.ts), every key present. */\n verdicts: Record<VerificationVerdict, number>;\n claims: {\n /** Questions asked across every recorded feature, `tallyRecord`'s counting. */\n total: number;\n confirmed: number;\n /**\n * Of the confirmed, one count per `answered_by` value (`ANSWERED_BY`),\n * every key present — the stable-shape rule `verdicts` and `stages`\n * follow, so a fourth provenance would appear rather than be absorbed.\n *\n * Read off the answers' own provenance, never by subtracting the\n * attested share: `attestedClaims` counts `scenario.tested` alone, so\n * `confirmed - attested` filed every OTHER kind an agent confirmed — a\n * permission, a capability, an exposed operation — under a test runner\n * that had not run. A record with four agent answers printed as three\n * runner and one agent, which is the reading this axis exists to give.\n */\n answered: Record<AnsweredBy, number>;\n };\n };\n /**\n * The HTTP axis, with a deliberate UNIT split: `defined`/`governed` count\n * contract SLOTS, exactly as the API axis's own findings count operations,\n * while `deprecated`/`deprecatedStillConsumed` count DISTINCT operationIds —\n * a duplicate-id slot pair (already its own warning) retires as ONE\n * operation, one migration owed.\n */\n operations: {\n /** Slots the living contracts define (removal markers are never operations). */\n defined: number;\n /** Slots (like `defined`) whose id a living requirement's `Operations:` line governs. */\n governed: number;\n /** Distinct deprecated operationIds. */\n deprecated: number;\n /** Distinct deprecated ids something still joins to — a living `Operations:` line, or an op-linked inbound landscape edge. */\n deprecatedStillConsumed: number;\n };\n messages: {\n /** Distinct message names the living async contracts declare. */\n defined: number;\n /** Of them, the ones a requirement's `Publishes:`/`Consumes:` line governs. */\n linked: number;\n };\n c4: {\n /** The drawn SYSTEMS — `drawnSystems`, the fleet map's own census, so the map's exemptions (actors, INFRA kinds the fleet runs rather than adopts, `#external`, groupings, containers) and this count cannot drift. An INFRA-kind element that DOES stand for a `services/<id>/` stays in, so adopting a store never shrinks the denominator. */\n elements: number;\n /** Of them, the ones some living arch requirement's `Covers:` entry names. */\n covered: number;\n };\n /**\n * Services PARTICIPATING in each contract axis — how far the staged\n * adoption has spread, axis by axis. The denominator is `services`, carried\n * once above like every other denominator here. One mechanical rule per\n * axis, per service: `requirements` — spec.md holds at least one\n * `### Requirement:` block (content, not file presence, so a fleet of empty\n * adopt scaffolds honestly reads \"not started\"); `arch` — the same for\n * arch.spec.md; `openapi` / `asyncapi` — the contract file exists;\n * `permissions` / `capabilities` — at least one non-REMOVED requirement in\n * either spec document carries a `Requires:` / `Capability:` entry (the\n * used-set rule the fleet vocabulary checks apply). An axis at 0 is \"not\n * started\" — distinct from partially adopted — and is what licenses the\n * text report's per-axis warning banner (./adoption.js). An unreadable\n * service counts as participating in nothing, the same zeros doctrine as\n * every count here — read these beside the run's `service.unreadable`\n * findings, and note the text renderer refuses to group warnings on a run\n * that has any (report.ts).\n */\n adoption: Adoption;\n features: {\n active: number;\n /**\n * The fleet form's stage per active feature — all five ARTIFACT_STATUSES\n * keys even though that form never grades `draft` (the maturityRollup\n * rule: a stable shape a dashboard can diff).\n */\n stages: Record<ArtifactStatus, number>;\n };\n}\n\n/**\n * Derive the scorecard, or decline. The failure story, per axis: a memoized\n * read that FAILED rejects again on every await, so the per-service fan-out\n * contains those per subject — an unreadable service contributes zeros while\n * its own `service.unreadable` finding names the cause — and every landscape\n * read goes through `../load.ts`'s containment, so an unreadable fleet map\n * zeroes the map-derived axes on every platform instead of deleting the card.\n * The catch is the last line of defence for the fleet-wide reads this module\n * does not own (the verification cells, the fleet status): a rollup that can\n * die must not take the graded report with it. No number rather than a wrong\n * number.\n */\nexport async function buildScorecard(input: ScorecardInput): Promise<Scorecard | null> {\n try {\n return await derive(input);\n } catch {\n // Unreadable is not zero: the axes cannot be counted, so no claim is made.\n return null;\n }\n}\n\nasync function derive(input: ScorecardInput): Promise<Scorecard> {\n const { docsDir, targets, fleet, boundService } = input;\n const services = await fleet.listServices(docsDir);\n\n // The landscape, through the run's memo and the fleet target's containment:\n // an unreadable file grades as one that did not parse, and a landscape that\n // proves nothing contributes no inbound edges and no element census —\n // fail-closed, exactly as the no-openapi grace reads the same absence.\n // Loaded FIRST and handed to serviceViews below, so the maturity rollup\n // costs no second parse of a document this run already holds.\n const lp = landscapePath(docsDir);\n const land = existsSync(lp) ? await readLandscape(() => fleet.loadLikeC4(lp)) : null;\n const proven = land !== null && land.errors.length === 0;\n\n const views = await serviceViews(docsDir, services, boundService, land);\n const maturity = maturityRollup(views);\n\n // Inbound op-linked edges per service, resolved exactly as the API axis\n // resolves them (service/api.ts): the container-aware resolver, so an edge\n // drawn into `paymentService.api` counts for the service that owns it.\n const known: ReadonlySet<string> = new Set(services.map((s) => s.id));\n const inbound = new Map<string, Set<string>>();\n if (proven) {\n const svcOf = serviceResolver(land.elements, known);\n for (const r of land.relationships) {\n if (r.op === undefined) continue;\n const owner: string = svcOf(r.target);\n const ops = inbound.get(owner) ?? new Set<string>();\n ops.add(r.op);\n inbound.set(owner, ops);\n }\n }\n\n // Every read in THIS fan-out is a memo hit — the service targets already\n // parsed both requirement documents and both contracts — so it spawns no new\n // filesystem work and needs no pool (unlike the verification fan-out below).\n const perService = await Promise.all(\n services.map((svc) => contractCounts(svc, fleet, inbound.get(svc.id) ?? new Set<string>())),\n );\n const sum = (pick: (c: ContractCounts) => number): number =>\n perService.reduce((n, c) => n + pick(c), 0);\n const operations = {\n defined: sum((c) => c.operations.defined),\n governed: sum((c) => c.operations.governed),\n deprecated: sum((c) => c.operations.deprecated),\n deprecatedStillConsumed: sum((c) => c.operations.deprecatedStillConsumed),\n };\n const messages = { defined: sum((c) => c.messages.defined), linked: sum((c) => c.messages.linked) };\n const adoption = adoptionOf(perService);\n\n // C4 coverage: the fleet map's own system census (`drawnSystems` — the set\n // the undocumented pass walks) against every living arch.spec.md's `Covers:`\n // entries. Element entries only — an edge entry names no element.\n const covers = perService.flatMap((c) => c.covers);\n const eligible = proven ? drawnSystems(land.elements, known) : [];\n const covered = eligible.filter((e) => covers.some((c) => coversElement(c, e))).length;\n\n // Verification: `list`'s own cells, so the two commands cannot disagree\n // about a record's verdict. Active features only — an archived record is\n // frozen history, not fleet debt. This fan-out is NOT memo hits:\n // readVerification is one small file read per feature, and a recorded cell\n // additionally derives its checklist (the same cost `list` pays for the same\n // column) — real filesystem work, so it runs through the bounded pool.\n const features = await fleet.listFeatures(docsDir);\n const cells = await inOrder(features, (f) => featureVerification(docsDir, f));\n const recorded = cells.filter((c): c is VerificationCell => c !== null);\n const verdicts = Object.fromEntries(VERIFICATION_VERDICTS.map((v) => [v, 0])) as Record<VerificationVerdict, number>;\n for (const cell of recorded) verdicts[cell.verdict] += 1;\n const confirmed = recorded.reduce((n, c) => n + c.confirmed, 0);\n const answered = Object.fromEntries(\n ANSWERED_BY.map((who) => [who, recorded.reduce((n, c) => n + c.answered[who], 0)]),\n ) as Record<AnsweredBy, number>;\n\n // The fleet form of `status`, on this run's own snapshot: presence and\n // dependency order, nothing graded — the stages an agent already branches on.\n const status = await fleetStatus(docsDir, { context: fleet });\n const stages = Object.fromEntries(ARTIFACT_STATUSES.map((s) => [s, 0])) as Record<ArtifactStatus, number>;\n for (const f of status.features) stages[f.stage] += 1;\n\n return {\n services: services.length,\n maturity,\n provenance: {\n vouched: maturity.vouched,\n // Both counts join through `subjectsWith` on the run's OWN findings, so\n // each is structurally the same number as the report beside it — the\n // unverifiable count is the footer's (unverifiableSubjects is the same\n // call on the same code; this package cannot import the parent back).\n // Both codes are LITERALS — provenance/sources.ts's own spellings —\n // because the stable-code collector reads counting sites and refuses a\n // slot it cannot read; the collector is also what convicts a typo here.\n staleDigests: subjectsWith(targets, \"sources.stale\"),\n unverifiableFromHere: subjectsWith(targets, \"sources.unverifiable-from-here\"),\n // Doc-side, so unlike `staleDigests` this one is complete even from the\n // docs repo: `sources.sampled-vouch` needs no service repo to fire.\n sampledVouched: subjectsWith(targets, \"sources.sampled-vouch\"),\n },\n verification: {\n recorded: recorded.length,\n verdicts,\n claims: {\n total: recorded.reduce((n, c) => n + c.claims, 0),\n confirmed,\n answered,\n },\n },\n operations,\n messages,\n c4: { elements: eligible.length, covered },\n adoption,\n features: { active: status.features.length, stages },\n };\n}\n"]}
|
|
@@ -118,7 +118,7 @@ export async function fleetProjectFindings(check) {
|
|
|
118
118
|
details: [file === null ? errorText(err) : `${file}: ${errorText(err)}`],
|
|
119
119
|
message: `fleet project: ${file ?? "the merged project"}: ${errorText(err)} — each document reads clean where ` +
|
|
120
120
|
"loam grades it, but the renderer merges every `.likec4` the root project reads except the generated " +
|
|
121
|
-
"subsystems.likec4 — the map, every extending model and every `.likec4` beside one — into ONE project, " +
|
|
121
|
+
"subsystems.likec4 and services.likec4 — the map, every extending model and every `.likec4` beside one — into ONE project, " +
|
|
122
122
|
`and that project does not parse. ${remedy(err.message)}`,
|
|
123
123
|
});
|
|
124
124
|
}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"fleet-project.js","sourceRoot":"","sources":["../../../../../src/commands/validate/fleet/views/fleet-project.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * `c4.fleet-project-invalid` — the one thing no per-document grade can see:\n * every document loam reads is clean, and the project the RENDERER builds out of\n * them is not.\n *\n * loam grades a fleet document by document, and that is deliberate: a service's\n * model is graded on its own merits, and one broken model must not blank the\n * fleet. The renderer has no such rule. Point `npx likec4 start` at the docs\n * root and it merges every `.likec4` the root project reads into ONE model — the\n * map, every extending model, and every `.likec4` sitting beside one — so a\n * declaration made in two of those documents is a duplicate there and nowhere\n * else. Measured at the 1.59.2 pin: a tags-only `specification { tag req-X }` in\n * an extending model is legal, and the SAME tag declared in a second extending\n * model is a duplicate blamed on both files. Two services, two clean grades, and\n * a renderer that draws nothing.\n *\n * So this is one extra load, and it is the renderer's reading rather than\n * loam's. What it reports is the SET DIFFERENCE: an error already reported where\n * loam grades that document — the map's own parse, a per-service project's\n * `c4.invalid`, a service flow project's use-case failure — is not repeated\n * here, because the fix is one file either way and N copies of one cascade is\n * the report. What is left over is exactly the class above: errors that exist\n * only because these documents were merged.\n *\n * WHICH DOCUMENTS ARE IN IT is `./root-documents.ts`'s question, and it is a\n * walk of the docs root rather than a list of the shapes loam knows about: a\n * `.likec4` in a SUBSYSTEM directory — the parent of service directories — was\n * in none of the three roots this used to merge, and went unreported while the\n * renderer refused the whole project over it (verification 2026-09-04, R2). That\n * module also carries why the generated `architecture/subsystems.likec4`,\n * every standalone service directory and `features/` stay out.\n *\n * `--all` ONLY, and only when at least one model extends the map, the map itself\n * parses, and the root `exclude` does not hide it. A single-target run has not\n * enumerated the fleet, so it cannot build the project at all; a fleet whose\n * models all stand alone has no such project (every one of them is excluded from\n * the root); and a map that does not parse would report its own cascade here\n * under a second code while `landscape.invalid` already names the file.\n *\n * THE LAST OF THOSE GATES IS THE CASCADE RULE, the one `permissions.invalid` and\n * `capability.invalid` already state: a grade that resolves against a\n * document nobody loaded is not a diagnosis. `architectureProjectDocuments`\n * keeps the map whatever the root `exclude` says — that floor is what stopped\n * loam asserting a fleet's map declares nothing — but this load has no such\n * floor ON PURPOSE, because its whole claim is to read what the renderer reads.\n * So an entry covering `architecture/landscape.likec4` leaves every extending\n * model in the project with its `extend <fqn>` resolving against nothing:\n * measured on `examples/docs` with `architecture/*.likec4` in the list, one\n * authored line came back as 161 warnings under this code. `landscape.excluded`\n * (`./projects.ts`) is the one finding that names the cause, and it says the\n * fleet project is not graded until the entry goes. The fact arrives as\n * `mapExcluded` rather than being re-read here, so the two can never disagree\n * about which state they are in.\n *\n * WHY A WARNING. Nothing loam concludes changes: every model still parses where\n * loam reads it, every binding still resolves, the spine is unaffected. What is\n * broken is the picture, which is what `service.likec4-config-missing` and its\n * three siblings in `./projects.ts` also grade, at the same severity.\n */\nimport { existsSync } from \"node:fs\";\nimport { relative, resolve, sep } from \"node:path\";\nimport { errorText, type LikeC4Error, type LoadedDoc } from \"../../../../core/c4/likec4.js\";\nimport { asLoadedDoc, loadProject } from \"../../../../core/c4/project/load.js\";\nimport type { ViewIdClaim } from \"../../../../core/c4/parsed/view-ids.js\";\nimport type { ModelShape } from \"../../../../core/c4/service-model/shape.js\";\nimport { FleetContext } from \"../../../../core/fleet-context.js\";\nimport { inOrder } from \"../../../../core/kernel/concurrency.js\";\nimport type { DocsDir } from \"../../../../core/kernel/ids/dirs.js\";\nimport type { ServiceEntry } from \"../../../../core/repo/entries.js\";\nimport { servicePathsAt, type ServicePaths } from \"../../../../core/repo/paths.js\";\nimport type { FleetTree } from \"../../../../core/repo/tree/walk.js\";\nimport { readServiceFlows } from \"../../../../core/usecases/service/flows.js\";\nimport type { Finding } from \"../../../../core/vocabulary/report.js\";\nimport { rootProjectDocuments } from \"./root-documents.js\";\n\nexport interface FleetProjectCheck {\n docsDir: DocsDir;\n /** The FULL enumeration — the project the renderer builds holds every model, not a narrowed run's subset. */\n entries: readonly ServiceEntry[];\n /** Each model's shape, keyed by resolved path (`FleetContext.modelShapes`). */\n shapes: ReadonlyMap<string, ModelShape>;\n /** The run's read index: the per-service projects it already parsed are half of the set difference. */\n fleet: FleetContext;\n /** The map parsed ALONE — the gate, and the other half of the difference. */\n architecture: LoadedDoc;\n /**\n * Whether the root `exclude` hides the map itself — `landscape.excluded`'s own\n * condition, computed once in `./projects.ts` and carried here rather than\n * re-derived. It is a GATE, for the reason the banner's cascade-rule paragraph\n * gives: this load has no floor under the map, so the entry that hides it\n * turns one authored line into an unresolved reference in every extending\n * model.\n */\n mapExcluded: boolean;\n /** The walked tree, for the view-id census this load makes answerable. */\n tree: FleetTree;\n /** Every service directory that exists — the flow scan's resolver evidence. */\n known: ReadonlySet<string>;\n}\n\n/**\n * What the merged load found, as three answers rather than one list — because\n * the caller has a second question to ask of it and the answers are mutually\n * exclusive.\n *\n * `skipped` is \"there is no such project, or loam declines to read one\": no model\n * extends the map, the map does not parse, the root `exclude` hides the map\n * (`landscape.excluded` names that entry and this grade stands down behind it),\n * or the run has no fleet index. `clean` carries the view-id\n * CENSUS, which is the only thing a caller may take from a project that parsed —\n * and the reason this is not just an empty finding list: an empty list and a\n * clean census are the same thing to `.length`, and one of them licenses\n * `subsystem.view-id-collision` to be graded over every extending model's\n * authored ids while the other does not.\n */\nexport type FleetProjectGrade =\n | { kind: \"skipped\" }\n | { kind: \"invalid\"; findings: Finding[] }\n | { kind: \"clean\"; viewIds: ViewIdClaim[] };\n\nexport async function fleetProjectFindings(check: FleetProjectCheck): Promise<FleetProjectGrade> {\n // A map that did not parse is `landscape.invalid`'s, and this load would only\n // repeat its cascade under a second code.\n if (check.architecture.errors.length > 0) return { kind: \"skipped\" };\n // A map the root `exclude` hides is `landscape.excluded`'s, for the same\n // reason one line up — see the banner's cascade-rule paragraph. The view-id\n // census goes with it: `clean` is a claim about a project that parsed, and\n // there is none.\n if (check.mapExcluded) return { kind: \"skipped\" };\n const shapeOf = (entry: ServiceEntry): ModelShape | null => {\n const model = servicePathsAt(entry.dir).model;\n return existsSync(model) ? (check.shapes.get(resolve(model)) ?? null) : null;\n };\n const extending = check.entries.filter((entry) => shapeOf(entry) === \"extending\");\n if (extending.length === 0) return { kind: \"skipped\" };\n\n const paths = extending.map((entry) => servicePathsAt(entry.dir));\n // The TREE, not three roots. `./root-documents.ts` carries what comes out of\n // it and why; the standalone directories are handed in because their shape is\n // this run's memo and re-reading every model here would be a second scanner.\n const documents = await rootProjectDocuments(\n check.docsDir,\n check.entries.filter((entry) => shapeOf(entry) === \"standalone\").map((entry) => entry.dir),\n );\n const project = await loadProject(check.docsDir, documents);\n\n if (project.clean) {\n // The census `subsystem.view-id-collision` needs and could not have until\n // now: an extending model's authored view ids live in the SAME flat\n // namespace as the map's, because both documents are in this project. The\n // claims arrive docs-relative, which is the spelling `viewIdFindings` takes.\n return { kind: \"clean\", viewIds: project.viewIds };\n }\n\n const reported = await alreadyReported(check, paths);\n const findings: Finding[] = [];\n for (const err of asLoadedDoc(project).errors) {\n if (reported.has(errorKey(err))) continue;\n const owner = extending.find(\n (entry) => err.sourceFsPath !== undefined && err.sourceFsPath.startsWith(`${entry.dir}${sep}`),\n );\n const file =\n err.sourceFsPath === undefined ? null : relative(check.docsDir, err.sourceFsPath).split(/[\\\\/]/).join(\"/\");\n findings.push({\n severity: \"warn\",\n code: \"c4.fleet-project-invalid\",\n ...(owner === undefined ? {} : { subject: owner.id }),\n // The file the error is IN, never the map by default: `locations[0].path`\n // was `architecture/landscape.likec4` on every one of these, so a reader\n // acting on the payload opened a document that was not the one at fault\n // (verification 2026-09-04, D10). Absent when LikeC4 declined to attribute\n // the error to a document at all — a location loam cannot spell is left\n // unspelled rather than guessed.\n ...(file === null ? {} : { locations: [{ path: file, role: \"primary\" as const }] }),\n details: [file === null ? errorText(err) : `${file}: ${errorText(err)}`],\n message:\n `fleet project: ${file ?? \"the merged project\"}: ${errorText(err)} — each document reads clean where ` +\n \"loam grades it, but the renderer merges every `.likec4` the root project reads except the generated \" +\n \"subsystems.likec4 — the map, every extending model and every `.likec4` beside one — into ONE project, \" +\n `and that project does not parse. ${remedy(err.message)}`,\n });\n }\n return { kind: \"invalid\", findings };\n}\n\n/**\n * The tail that names WHICH failure this is.\n *\n * It used to say \"a tag or an element declared in two of those documents is\n * declared twice there\" for every error, including the ones that are not a\n * double declaration at all (verification 2026-09-04, D10) — an unresolved\n * reference sent its reader hunting for a second declaration that does not\n * exist. LikeC4's own wording is the evidence, matched loosely on purpose: an\n * unrecognised message falls to the third arm, which asserts nothing beyond\n * where to look.\n */\nfunction remedy(message: string): string {\n if (/duplicat|already (declared|defined)|declared (more than once|twice)/i.test(message)) {\n return (\n \"A tag or an element declared in two of those documents is declared twice there; declare it once \" +\n \"(the map, or the one service that owns it)\"\n );\n }\n if (/could not resolve|unresolved|unknown/i.test(message)) {\n return (\n \"A name resolves against ALL of those documents here, so a reference the renderer cannot place is one \" +\n \"no document in the project declares; declare it where the reader can see it, or drop the reference\"\n );\n }\n return \"Open the file and line named above — every reader who points a renderer at the docs root sees this\";\n}\n\n/**\n * Every parse error this run has ALREADY reported against one of these\n * documents, keyed so the merged project's copy can be recognised.\n *\n * Two sources, and both are loads the run made anyway or would make: each\n * extending model's own per-service project (`c4.invalid` on the service\n * target) and, where nothing was wrong there, each service's FLOW project\n * (`readServiceFlows` — the use-case grade names the file). The flow read goes\n * through that function rather than a second `loadProject` here so the OPT-IN\n * gate is the same one: a broken sibling that nothing tagged is loaded by\n * neither, which is exactly why the merged project has to report it.\n *\n * Computed only when the merged project actually failed, which is why it is a\n * function rather than an input: on a healthy fleet this whole file costs one\n * load and nothing else.\n *\n * THE FLOW PROJECTS ARE PARSED AGAIN by `validateService` later in the same run,\n * and that is accepted rather than unnoticed: it happens only on the path where\n * the merged project already failed, so a healthy fleet pays nothing, and the\n * `ServiceModel` half above IS the run's memo. If that path ever becomes hot,\n * the follow-up is to memoise the flow scan on `FleetContext` beside the\n * per-service projects — not to hand the scan in here, which would make this\n * grade depend on an argument the caller has no reason to have loaded.\n */\nasync function alreadyReported(\n check: FleetProjectCheck,\n paths: readonly ServicePaths[],\n): Promise<ReadonlySet<string>> {\n // Both sets reach the same documents through `architectureProjectDocuments`,\n // which now drops what the root `exclude` covers exactly as the merged load's\n // own walk does — so a document out of one is out of both, and the generated\n // `subsystems.likec4` is out of all of them without being remembered twice.\n //\n // Pooled rather than awaited one at a time: each entry is an independent\n // LikeC4 workspace over one service's own documents, which is exactly the\n // work `inOrder` rations — and a serial loop here made the ONE path that\n // already costs a fleet its whole set of flow projects pay for them in\n // sequence.\n const perService = await inOrder(paths, async (service) => {\n const model = await check.fleet.serviceModel(check.docsDir, service);\n const errors = [...model.doc.errors];\n if (errors.length > 0 || model.mapUnreadable) return errors;\n // `model.project` is non-null here by construction — these are extending\n // models whose own load came back clean, which is the arm that carries it —\n // and the guard is the compiler's rather than an assertion: a null would\n // mean grading a flow against a document that did not load.\n if (model.project === null) return [];\n const flows = await readServiceFlows({\n paths: service,\n model: model.doc,\n known: check.known,\n extending: { docsDir: check.docsDir, project: model.project },\n });\n return flows.kind === \"unreadable\" ? flows.errors : [];\n });\n return new Set(perService.flat().map(errorKey));\n}\n\n/**\n * One error's identity across two loads of overlapping document sets: the real\n * file, the line, and the message.\n *\n * There is no id to join on — LikeC4 mints diagnostics per parse — and the\n * message has to be part of the key: two DIFFERENT problems on one line of one\n * file are two errors, and collapsing them would let a merged-project-only\n * failure hide behind a per-service one that happens to share a line.\n */\nfunction errorKey(err: LikeC4Error): string {\n return JSON.stringify([err.sourceFsPath ?? \"\", err.line ?? -1, err.message]);\n}\n"]}
|
|
1
|
+
{"version":3,"file":"fleet-project.js","sourceRoot":"","sources":["../../../../../src/commands/validate/fleet/views/fleet-project.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * `c4.fleet-project-invalid` — the one thing no per-document grade can see:\n * every document loam reads is clean, and the project the RENDERER builds out of\n * them is not.\n *\n * loam grades a fleet document by document, and that is deliberate: a service's\n * model is graded on its own merits, and one broken model must not blank the\n * fleet. The renderer has no such rule. Point `npx likec4 start` at the docs\n * root and it merges every `.likec4` the root project reads into ONE model — the\n * map, every extending model, and every `.likec4` sitting beside one — so a\n * declaration made in two of those documents is a duplicate there and nowhere\n * else. Measured at the 1.59.2 pin: a tags-only `specification { tag req-X }` in\n * an extending model is legal, and the SAME tag declared in a second extending\n * model is a duplicate blamed on both files. Two services, two clean grades, and\n * a renderer that draws nothing.\n *\n * So this is one extra load, and it is the renderer's reading rather than\n * loam's. What it reports is the SET DIFFERENCE: an error already reported where\n * loam grades that document — the map's own parse, a per-service project's\n * `c4.invalid`, a service flow project's use-case failure — is not repeated\n * here, because the fix is one file either way and N copies of one cascade is\n * the report. What is left over is exactly the class above: errors that exist\n * only because these documents were merged.\n *\n * WHICH DOCUMENTS ARE IN IT is `./root-documents.ts`'s question, and it is a\n * walk of the docs root rather than a list of the shapes loam knows about: a\n * `.likec4` in a SUBSYSTEM directory — the parent of service directories — was\n * in none of the three roots this used to merge, and went unreported while the\n * renderer refused the whole project over it (verification 2026-09-04, R2). That\n * module also carries why the generated `architecture/subsystems.likec4`,\n * every standalone service directory and `features/` stay out.\n *\n * `--all` ONLY, and only when at least one model extends the map, the map itself\n * parses, and the root `exclude` does not hide it. A single-target run has not\n * enumerated the fleet, so it cannot build the project at all; a fleet whose\n * models all stand alone has no such project (every one of them is excluded from\n * the root); and a map that does not parse would report its own cascade here\n * under a second code while `landscape.invalid` already names the file.\n *\n * THE LAST OF THOSE GATES IS THE CASCADE RULE, the one `permissions.invalid` and\n * `capability.invalid` already state: a grade that resolves against a\n * document nobody loaded is not a diagnosis. `architectureProjectDocuments`\n * keeps the map whatever the root `exclude` says — that floor is what stopped\n * loam asserting a fleet's map declares nothing — but this load has no such\n * floor ON PURPOSE, because its whole claim is to read what the renderer reads.\n * So an entry covering `architecture/landscape.likec4` leaves every extending\n * model in the project with its `extend <fqn>` resolving against nothing:\n * measured on `examples/docs` with `architecture/*.likec4` in the list, one\n * authored line came back as 161 warnings under this code. `landscape.excluded`\n * (`./projects.ts`) is the one finding that names the cause, and it says the\n * fleet project is not graded until the entry goes. The fact arrives as\n * `mapExcluded` rather than being re-read here, so the two can never disagree\n * about which state they are in.\n *\n * WHY A WARNING. Nothing loam concludes changes: every model still parses where\n * loam reads it, every binding still resolves, the spine is unaffected. What is\n * broken is the picture, which is what `service.likec4-config-missing` and its\n * three siblings in `./projects.ts` also grade, at the same severity.\n */\nimport { existsSync } from \"node:fs\";\nimport { relative, resolve, sep } from \"node:path\";\nimport { errorText, type LikeC4Error, type LoadedDoc } from \"../../../../core/c4/likec4.js\";\nimport { asLoadedDoc, loadProject } from \"../../../../core/c4/project/load.js\";\nimport type { ViewIdClaim } from \"../../../../core/c4/parsed/view-ids.js\";\nimport type { ModelShape } from \"../../../../core/c4/service-model/shape.js\";\nimport { FleetContext } from \"../../../../core/fleet-context.js\";\nimport { inOrder } from \"../../../../core/kernel/concurrency.js\";\nimport type { DocsDir } from \"../../../../core/kernel/ids/dirs.js\";\nimport type { ServiceEntry } from \"../../../../core/repo/entries.js\";\nimport { servicePathsAt, type ServicePaths } from \"../../../../core/repo/paths.js\";\nimport type { FleetTree } from \"../../../../core/repo/tree/walk.js\";\nimport { readServiceFlows } from \"../../../../core/usecases/service/flows.js\";\nimport type { Finding } from \"../../../../core/vocabulary/report.js\";\nimport { rootProjectDocuments } from \"./root-documents.js\";\n\nexport interface FleetProjectCheck {\n docsDir: DocsDir;\n /** The FULL enumeration — the project the renderer builds holds every model, not a narrowed run's subset. */\n entries: readonly ServiceEntry[];\n /** Each model's shape, keyed by resolved path (`FleetContext.modelShapes`). */\n shapes: ReadonlyMap<string, ModelShape>;\n /** The run's read index: the per-service projects it already parsed are half of the set difference. */\n fleet: FleetContext;\n /** The map parsed ALONE — the gate, and the other half of the difference. */\n architecture: LoadedDoc;\n /**\n * Whether the root `exclude` hides the map itself — `landscape.excluded`'s own\n * condition, computed once in `./projects.ts` and carried here rather than\n * re-derived. It is a GATE, for the reason the banner's cascade-rule paragraph\n * gives: this load has no floor under the map, so the entry that hides it\n * turns one authored line into an unresolved reference in every extending\n * model.\n */\n mapExcluded: boolean;\n /** The walked tree, for the view-id census this load makes answerable. */\n tree: FleetTree;\n /** Every service directory that exists — the flow scan's resolver evidence. */\n known: ReadonlySet<string>;\n}\n\n/**\n * What the merged load found, as three answers rather than one list — because\n * the caller has a second question to ask of it and the answers are mutually\n * exclusive.\n *\n * `skipped` is \"there is no such project, or loam declines to read one\": no model\n * extends the map, the map does not parse, the root `exclude` hides the map\n * (`landscape.excluded` names that entry and this grade stands down behind it),\n * or the run has no fleet index. `clean` carries the view-id\n * CENSUS, which is the only thing a caller may take from a project that parsed —\n * and the reason this is not just an empty finding list: an empty list and a\n * clean census are the same thing to `.length`, and one of them licenses\n * `subsystem.view-id-collision` to be graded over every extending model's\n * authored ids while the other does not.\n */\nexport type FleetProjectGrade =\n | { kind: \"skipped\" }\n | { kind: \"invalid\"; findings: Finding[] }\n | { kind: \"clean\"; viewIds: ViewIdClaim[] };\n\nexport async function fleetProjectFindings(check: FleetProjectCheck): Promise<FleetProjectGrade> {\n // A map that did not parse is `landscape.invalid`'s, and this load would only\n // repeat its cascade under a second code.\n if (check.architecture.errors.length > 0) return { kind: \"skipped\" };\n // A map the root `exclude` hides is `landscape.excluded`'s, for the same\n // reason one line up — see the banner's cascade-rule paragraph. The view-id\n // census goes with it: `clean` is a claim about a project that parsed, and\n // there is none.\n if (check.mapExcluded) return { kind: \"skipped\" };\n const shapeOf = (entry: ServiceEntry): ModelShape | null => {\n const model = servicePathsAt(entry.dir).model;\n return existsSync(model) ? (check.shapes.get(resolve(model)) ?? null) : null;\n };\n const extending = check.entries.filter((entry) => shapeOf(entry) === \"extending\");\n if (extending.length === 0) return { kind: \"skipped\" };\n\n const paths = extending.map((entry) => servicePathsAt(entry.dir));\n // The TREE, not three roots. `./root-documents.ts` carries what comes out of\n // it and why; the standalone directories are handed in because their shape is\n // this run's memo and re-reading every model here would be a second scanner.\n const documents = await rootProjectDocuments(\n check.docsDir,\n check.entries.filter((entry) => shapeOf(entry) === \"standalone\").map((entry) => entry.dir),\n );\n const project = await loadProject(check.docsDir, documents);\n\n if (project.clean) {\n // The census `subsystem.view-id-collision` needs and could not have until\n // now: an extending model's authored view ids live in the SAME flat\n // namespace as the map's, because both documents are in this project. The\n // claims arrive docs-relative, which is the spelling `viewIdFindings` takes.\n return { kind: \"clean\", viewIds: project.viewIds };\n }\n\n const reported = await alreadyReported(check, paths);\n const findings: Finding[] = [];\n for (const err of asLoadedDoc(project).errors) {\n if (reported.has(errorKey(err))) continue;\n const owner = extending.find(\n (entry) => err.sourceFsPath !== undefined && err.sourceFsPath.startsWith(`${entry.dir}${sep}`),\n );\n const file =\n err.sourceFsPath === undefined ? null : relative(check.docsDir, err.sourceFsPath).split(/[\\\\/]/).join(\"/\");\n findings.push({\n severity: \"warn\",\n code: \"c4.fleet-project-invalid\",\n ...(owner === undefined ? {} : { subject: owner.id }),\n // The file the error is IN, never the map by default: `locations[0].path`\n // was `architecture/landscape.likec4` on every one of these, so a reader\n // acting on the payload opened a document that was not the one at fault\n // (verification 2026-09-04, D10). Absent when LikeC4 declined to attribute\n // the error to a document at all — a location loam cannot spell is left\n // unspelled rather than guessed.\n ...(file === null ? {} : { locations: [{ path: file, role: \"primary\" as const }] }),\n details: [file === null ? errorText(err) : `${file}: ${errorText(err)}`],\n message:\n `fleet project: ${file ?? \"the merged project\"}: ${errorText(err)} — each document reads clean where ` +\n \"loam grades it, but the renderer merges every `.likec4` the root project reads except the generated \" +\n \"subsystems.likec4 and services.likec4 — the map, every extending model and every `.likec4` beside one — into ONE project, \" +\n `and that project does not parse. ${remedy(err.message)}`,\n });\n }\n return { kind: \"invalid\", findings };\n}\n\n/**\n * The tail that names WHICH failure this is.\n *\n * It used to say \"a tag or an element declared in two of those documents is\n * declared twice there\" for every error, including the ones that are not a\n * double declaration at all (verification 2026-09-04, D10) — an unresolved\n * reference sent its reader hunting for a second declaration that does not\n * exist. LikeC4's own wording is the evidence, matched loosely on purpose: an\n * unrecognised message falls to the third arm, which asserts nothing beyond\n * where to look.\n */\nfunction remedy(message: string): string {\n if (/duplicat|already (declared|defined)|declared (more than once|twice)/i.test(message)) {\n return (\n \"A tag or an element declared in two of those documents is declared twice there; declare it once \" +\n \"(the map, or the one service that owns it)\"\n );\n }\n if (/could not resolve|unresolved|unknown/i.test(message)) {\n return (\n \"A name resolves against ALL of those documents here, so a reference the renderer cannot place is one \" +\n \"no document in the project declares; declare it where the reader can see it, or drop the reference\"\n );\n }\n return \"Open the file and line named above — every reader who points a renderer at the docs root sees this\";\n}\n\n/**\n * Every parse error this run has ALREADY reported against one of these\n * documents, keyed so the merged project's copy can be recognised.\n *\n * Two sources, and both are loads the run made anyway or would make: each\n * extending model's own per-service project (`c4.invalid` on the service\n * target) and, where nothing was wrong there, each service's FLOW project\n * (`readServiceFlows` — the use-case grade names the file). The flow read goes\n * through that function rather than a second `loadProject` here so the OPT-IN\n * gate is the same one: a broken sibling that nothing tagged is loaded by\n * neither, which is exactly why the merged project has to report it.\n *\n * Computed only when the merged project actually failed, which is why it is a\n * function rather than an input: on a healthy fleet this whole file costs one\n * load and nothing else.\n *\n * THE FLOW PROJECTS ARE PARSED AGAIN by `validateService` later in the same run,\n * and that is accepted rather than unnoticed: it happens only on the path where\n * the merged project already failed, so a healthy fleet pays nothing, and the\n * `ServiceModel` half above IS the run's memo. If that path ever becomes hot,\n * the follow-up is to memoise the flow scan on `FleetContext` beside the\n * per-service projects — not to hand the scan in here, which would make this\n * grade depend on an argument the caller has no reason to have loaded.\n */\nasync function alreadyReported(\n check: FleetProjectCheck,\n paths: readonly ServicePaths[],\n): Promise<ReadonlySet<string>> {\n // Both sets reach the same documents through `architectureProjectDocuments`,\n // which now drops what the root `exclude` covers exactly as the merged load's\n // own walk does — so a document out of one is out of both, and the generated\n // `subsystems.likec4` is out of all of them without being remembered twice.\n //\n // Pooled rather than awaited one at a time: each entry is an independent\n // LikeC4 workspace over one service's own documents, which is exactly the\n // work `inOrder` rations — and a serial loop here made the ONE path that\n // already costs a fleet its whole set of flow projects pay for them in\n // sequence.\n const perService = await inOrder(paths, async (service) => {\n const model = await check.fleet.serviceModel(check.docsDir, service);\n const errors = [...model.doc.errors];\n if (errors.length > 0 || model.mapUnreadable) return errors;\n // `model.project` is non-null here by construction — these are extending\n // models whose own load came back clean, which is the arm that carries it —\n // and the guard is the compiler's rather than an assertion: a null would\n // mean grading a flow against a document that did not load.\n if (model.project === null) return [];\n const flows = await readServiceFlows({\n paths: service,\n model: model.doc,\n known: check.known,\n extending: { docsDir: check.docsDir, project: model.project },\n });\n return flows.kind === \"unreadable\" ? flows.errors : [];\n });\n return new Set(perService.flat().map(errorKey));\n}\n\n/**\n * One error's identity across two loads of overlapping document sets: the real\n * file, the line, and the message.\n *\n * There is no id to join on — LikeC4 mints diagnostics per parse — and the\n * message has to be part of the key: two DIFFERENT problems on one line of one\n * file are two errors, and collapsing them would let a merged-project-only\n * failure hide behind a per-service one that happens to share a line.\n */\nfunction errorKey(err: LikeC4Error): string {\n return JSON.stringify([err.sourceFsPath ?? \"\", err.line ?? -1, err.message]);\n}\n"]}
|
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
import { renderContext } from "../../../../core/repo/tree/render/body.js";
|
|
2
|
+
import { SERVICE_VIEW_PREFIX, serviceViewId } from "../../../../core/repo/tree/render/service/view-id.js";
|
|
1
3
|
import { SUBSYSTEM_VIEW_PREFIX, subsystemViewId } from "../../../../core/repo/tree/render/views.js";
|
|
2
4
|
/**
|
|
3
5
|
* `authored` is every view id the project claims, each with the DOCS-RELATIVE
|
|
@@ -6,10 +8,34 @@ import { SUBSYSTEM_VIEW_PREFIX, subsystemViewId } from "../../../../core/repo/tr
|
|
|
6
8
|
* parse, or an older loader did not read them) is treated as "nothing to
|
|
7
9
|
* compare", never as "nothing collides".
|
|
8
10
|
*/
|
|
9
|
-
export function viewIdFindings(authored, tree) {
|
|
11
|
+
export function viewIdFindings(authored, tree, map) {
|
|
10
12
|
if (authored === undefined || authored.length === 0)
|
|
11
13
|
return [];
|
|
12
|
-
const generated = new Map(tree.subsystems.map((sub) => [
|
|
14
|
+
const generated = new Map(tree.subsystems.map((sub) => [
|
|
15
|
+
subsystemViewId(sub),
|
|
16
|
+
{ owner: sub.path.join("/"), file: "architecture/subsystems.likec4", prefix: SUBSYSTEM_VIEW_PREFIX },
|
|
17
|
+
]));
|
|
18
|
+
// The per-service ids, and ONLY for a service the map actually resolves —
|
|
19
|
+
// which is why the map comes in at all. `architecture/services.likec4` draws a
|
|
20
|
+
// view for a service exactly when the element→service join answers, so a
|
|
21
|
+
// service the map does not draw mints NOTHING and an authored `service_<id>`
|
|
22
|
+
// beside it breaks nothing today. Reserving it anyway would be this check
|
|
23
|
+
// inventing a rule about a name that harms no one — the same line the module
|
|
24
|
+
// banner already holds for `subsystem_overview` with no `overview` subsystem —
|
|
25
|
+
// and the latent case is not lost: the run after somebody puts that service on
|
|
26
|
+
// the map, the generator mints the id and this check fires. The join arrives
|
|
27
|
+
// through `renderContext`, the generator's OWN, so the census and the render
|
|
28
|
+
// can never disagree about which services are drawn.
|
|
29
|
+
const drawn = renderContext(tree, map.elements);
|
|
30
|
+
for (const service of tree.services) {
|
|
31
|
+
if (!drawn.elementOf.has(service.id))
|
|
32
|
+
continue;
|
|
33
|
+
generated.set(serviceViewId(service.id), {
|
|
34
|
+
owner: [...service.subsystem, service.id].join("/"),
|
|
35
|
+
file: "architecture/services.likec4",
|
|
36
|
+
prefix: SERVICE_VIEW_PREFIX,
|
|
37
|
+
});
|
|
38
|
+
}
|
|
13
39
|
const findings = [];
|
|
14
40
|
// Sorted and de-duplicated by id so a project claiming the same id twice —
|
|
15
41
|
// itself already a parse error, but reachable through a partial read — yields
|
|
@@ -21,8 +47,8 @@ export function viewIdFindings(authored, tree) {
|
|
|
21
47
|
if (!claimants.has(claim.id))
|
|
22
48
|
claimants.set(claim.id, claim.sourcePath);
|
|
23
49
|
for (const [id, file] of [...claimants].sort(([a], [b]) => (a < b ? -1 : 1))) {
|
|
24
|
-
const
|
|
25
|
-
if (
|
|
50
|
+
const mint = generated.get(id);
|
|
51
|
+
if (mint === undefined)
|
|
26
52
|
continue;
|
|
27
53
|
findings.push({
|
|
28
54
|
severity: "error",
|
|
@@ -34,14 +60,14 @@ export function viewIdFindings(authored, tree) {
|
|
|
34
60
|
// view in when the claim came from a document of their own.
|
|
35
61
|
locations: [{ path: file, role: "primary" }],
|
|
36
62
|
message: `subsystems: ${file} declares \`view ${id}\`, which is the id loam generates ` +
|
|
37
|
-
`into
|
|
63
|
+
`into ${mint.file} for services/${mint.owner}/ — LikeC4 merges both files into one project, ` +
|
|
38
64
|
// The ROOT project, not `architecture/`: the claimant may be a model's
|
|
39
65
|
// own view now (an extending model is a document of the root project),
|
|
40
66
|
// and telling that author the `architecture/` project is refused sends
|
|
41
67
|
// them to look for the breakage in a directory they never touched.
|
|
42
68
|
`so it refuses the whole root project and renders nothing. ` +
|
|
43
|
-
`Rename the view in ${file}; ids beginning \`${
|
|
44
|
-
|
|
69
|
+
`Rename the view in ${file}; ids beginning \`${mint.prefix}\` are loam's ` +
|
|
70
|
+
`to mint, and ${mint.file} is generated (\`loam subsystem sync\`) and must never be hand-edited.`,
|
|
45
71
|
});
|
|
46
72
|
}
|
|
47
73
|
return findings;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"ids.js","sourceRoot":"","sources":["../../../../../src/commands/validate/fleet/views/ids.ts"],"names":[],"mappings":"AAqDA,OAAO,EAAE,qBAAqB,EAAE,eAAe,EAAE,MAAM,4CAA4C,CAAC;AAIpG;;;;;;GAMG;AACH,MAAM,UAAU,cAAc,CAAC,QAAmC,EAAE,IAAe;IACjF,IAAI,QAAQ,KAAK,SAAS,IAAI,QAAQ,CAAC,MAAM,KAAK,CAAC;QAAE,OAAO,EAAE,CAAC;IAC/D,MAAM,SAAS,GAAG,IAAI,GAAG,CAAC,IAAI,CAAC,UAAU,CAAC,GAAG,CAAC,CAAC,GAAG,EAAE,EAAE,CAAC,CAAC,eAAe,CAAC,GAAG,CAAC,EAAE,GAAG,CAAC,IAAI,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC,CAAC;IACpG,MAAM,QAAQ,GAAc,EAAE,CAAC;IAC/B,2EAA2E;IAC3E,8EAA8E;IAC9E,+EAA+E;IAC/E,2EAA2E;IAC3E,iDAAiD;IACjD,MAAM,SAAS,GAAG,IAAI,GAAG,EAAkB,CAAC;IAC5C,KAAK,MAAM,KAAK,IAAI,QAAQ;QAAE,IAAI,CAAC,SAAS,CAAC,GAAG,CAAC,KAAK,CAAC,EAAE,CAAC;YAAE,SAAS,CAAC,GAAG,CAAC,KAAK,CAAC,EAAE,EAAE,KAAK,CAAC,UAAU,CAAC,CAAC;IACtG,KAAK,MAAM,CAAC,EAAE,EAAE,IAAI,CAAC,IAAI,CAAC,GAAG,SAAS,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,EAAE,CAAC;QAC7E,MAAM,KAAK,GAAG,SAAS,CAAC,GAAG,CAAC,EAAE,CAAC,CAAC;QAChC,IAAI,KAAK,KAAK,SAAS;YAAE,SAAS;QAClC,QAAQ,CAAC,IAAI,CAAC;YACZ,QAAQ,EAAE,OAAO;YACjB,IAAI,EAAE,6BAA6B;YACnC,OAAO,EAAE,EAAE;YACX,wEAAwE;YACxE,uEAAuE;YACvE,wEAAwE;YACxE,4DAA4D;YAC5D,SAAS,EAAE,CAAC,EAAE,IAAI,EAAE,IAAI,EAAE,IAAI,EAAE,SAAkB,EAAE,CAAC;YACrD,OAAO,EACL,eAAe,IAAI,oBAAoB,EAAE,qCAAqC;gBAC9E,oDAAoD,KAAK,iDAAiD;gBAC1G,uEAAuE;gBACvE,uEAAuE;gBACvE,uEAAuE;gBACvE,mEAAmE;gBACnE,4DAA4D;gBAC5D,sBAAsB,IAAI,qBAAqB,qBAAqB,gBAAgB;gBACpF,iHAAiH;SACpH,CAAC,CAAC;IACL,CAAC;IACD,OAAO,QAAQ,CAAC;AAClB,CAAC","sourcesContent":["/**\n * An authored view id that collides with one loam GENERATES into\n * `architecture/subsystems.likec4`.\n *\n * This closes a green-while-broken hole, and the shape of it is measured rather\n * than argued. The LikeC4 renderer merges every `.likec4` file in a project into\n * one model, and `architecture/landscape.likec4` and the generated\n * `architecture/subsystems.likec4` are two documents of the ROOT project — so\n * they share ONE flat view-id namespace. (This file used to say the reason was\n * that `likec4.config.json` scopes the root project to `architecture/`; it does\n * not, and never had to for the collision to exist. The scaffold's `exclude`\n * names `features/**` and node_modules, and a fleet whose models extend the map\n * has `services/` in the root project too — see `./fleet-project.ts`.) Author\n * `view subsystem_commerce { }` in the landscape while the tree has a `commerce`\n * subsystem and the renderer refuses the ENTIRE root project with two\n * `Duplicate view` diagnostics, one per file — while `loam validate --all`\n * printed `landscape: N service(s) modelled — architecture/landscape.likec4 and\n * services/ agree`. Reproduced at the 1.59.2 pin.\n *\n * WHICH DOCUMENTS CLAIM IDS IN THAT NAMESPACE, corrected. A duplicate INSIDE one\n * document is already 2 LikeC4 errors, so `landscape.invalid` has it (measured).\n * A feature's `delta.likec4` is its own LikeC4 project — the root excludes\n * `features/**` — and a delta's views never travel through `archive` anyway. A\n * service's `model.likec4` used to be in the same position and no longer is:\n * this file's original premise, that `likec4.config.json` excludes `services/**`\n * so a model's view ids cannot collide with the landscape's, holds only for a\n * model that STANDS ALONE. A model that EXTENDS the map lives in the root\n * project beside it, so its authored view ids — and those of every `.likec4`\n * beside it — share exactly this namespace. That census is made by the merged\n * load in `./fleet-project.ts` and handed here; the landscape's own claims are\n * still graded from the `architecture/` project, which is the only census a run\n * with no extending model can make.\n *\n * The claims therefore arrive spelled DOCS-RELATIVE, and the caller does the\n * spelling: the `architecture/` project reports a view's `sourcePath` relative\n * to `architecture/`, the merged project reports it relative to the docs root,\n * and a prefix pasted on here would name `architecture/services/…` for every\n * model. The message and the location both print the path verbatim.\n *\n * ONLY the real collision is refused, never the merely reserved-looking name.\n * A landscape view called `subsystem_overview` with no `overview` subsystem\n * breaks nothing today, and refusing it would be loam inventing a rule about a\n * name that harms no one. The latent case is not lost: the moment somebody\n * creates that subsystem, the generator mints the id, and this same check fires\n * on the same run. The message names the reservation anyway, so an author fixing\n * one collision learns not to make the next.\n *\n * Graded on `validate --all` only, and the caller holds that gate together with\n * the landscape-parsed one: under a single-target run the fleet tree is not\n * walked and the generated ids are unknown, so the question is unanswerable and\n * the check stays silent rather than half-answering.\n */\nimport type { ViewIdClaim } from \"../../../../core/c4/parsed/view-ids.js\";\nimport { SUBSYSTEM_VIEW_PREFIX, subsystemViewId } from \"../../../../core/repo/tree/render/views.js\";\nimport type { FleetTree } from \"../../../../core/repo/tree/walk.js\";\nimport type { Finding } from \"../../../../core/vocabulary/report.js\";\n\n/**\n * `authored` is every view id the project claims, each with the DOCS-RELATIVE\n * path of the file that claims it — static views included, because a static view\n * claims an id exactly as firmly as a dynamic one. Absent (the document did not\n * parse, or an older loader did not read them) is treated as \"nothing to\n * compare\", never as \"nothing collides\".\n */\nexport function viewIdFindings(authored: ViewIdClaim[] | undefined, tree: FleetTree): Finding[] {\n if (authored === undefined || authored.length === 0) return [];\n const generated = new Map(tree.subsystems.map((sub) => [subsystemViewId(sub), sub.path.join(\"/\")]));\n const findings: Finding[] = [];\n // Sorted and de-duplicated by id so a project claiming the same id twice —\n // itself already a parse error, but reachable through a partial read — yields\n // one finding per id rather than one per occurrence. The first claimant's file\n // is the one named: with a duplicate the project does not parse at all, so\n // there is never a second one to choose between.\n const claimants = new Map<string, string>();\n for (const claim of authored) if (!claimants.has(claim.id)) claimants.set(claim.id, claim.sourcePath);\n for (const [id, file] of [...claimants].sort(([a], [b]) => (a < b ? -1 : 1))) {\n const owner = generated.get(id);\n if (owner === undefined) continue;\n findings.push({\n severity: \"error\",\n code: \"subsystem.view-id-collision\",\n subject: id,\n // The CLAIMANT file — the one the message tells the reader to edit, and\n // the one the report's target fallback gets wrong: it files this under\n // the landscape, which is exactly the file a reader must NOT rename the\n // view in when the claim came from a document of their own.\n locations: [{ path: file, role: \"primary\" as const }],\n message:\n `subsystems: ${file} declares \\`view ${id}\\`, which is the id loam generates ` +\n `into architecture/subsystems.likec4 for services/${owner}/ — LikeC4 merges both files into one project, ` +\n // The ROOT project, not `architecture/`: the claimant may be a model's\n // own view now (an extending model is a document of the root project),\n // and telling that author the `architecture/` project is refused sends\n // them to look for the breakage in a directory they never touched.\n `so it refuses the whole root project and renders nothing. ` +\n `Rename the view in ${file}; ids beginning \\`${SUBSYSTEM_VIEW_PREFIX}\\` are loam's ` +\n \"to mint, and architecture/subsystems.likec4 is generated (`loam subsystem sync`) and must never be hand-edited.\",\n });\n }\n return findings;\n}\n"]}
|
|
1
|
+
{"version":3,"file":"ids.js","sourceRoot":"","sources":["../../../../../src/commands/validate/fleet/views/ids.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * An authored view id that collides with one loam GENERATES into\n * `architecture/subsystems.likec4` or `architecture/services.likec4`.\n *\n * TWO RESERVED PREFIXES, one check: `subsystem_<name>` for a subsystem's view\n * and `service_<id>` for a service's generated overview, both minted through the\n * one encoder (`render/service/view-id.ts`) so no two distinct names can fold\n * onto one id across BOTH files. The failure they guard is identical — the\n * renderer merges every document of the root project into one flat view-id\n * namespace and refuses the whole project over a duplicate — so they are one\n * finding code with the right file named in each message, not two codes.\n *\n * This closes a green-while-broken hole, and the shape of it is measured rather\n * than argued. The LikeC4 renderer merges every `.likec4` file in a project into\n * one model, and `architecture/landscape.likec4` and the generated\n * `architecture/subsystems.likec4` are two documents of the ROOT project — so\n * they share ONE flat view-id namespace. (This file used to say the reason was\n * that `likec4.config.json` scopes the root project to `architecture/`; it does\n * not, and never had to for the collision to exist. The scaffold's `exclude`\n * names `features/**` and node_modules, and a fleet whose models extend the map\n * has `services/` in the root project too — see `./fleet-project.ts`.) Author\n * `view subsystem_commerce { }` in the landscape while the tree has a `commerce`\n * subsystem and the renderer refuses the ENTIRE root project with two\n * `Duplicate view` diagnostics, one per file — while `loam validate --all`\n * printed `landscape: N service(s) modelled — architecture/landscape.likec4 and\n * services/ agree`. Reproduced at the 1.59.2 pin.\n *\n * WHICH DOCUMENTS CLAIM IDS IN THAT NAMESPACE, corrected. A duplicate INSIDE one\n * document is already 2 LikeC4 errors, so `landscape.invalid` has it (measured).\n * A feature's `delta.likec4` is its own LikeC4 project — the root excludes\n * `features/**` — and a delta's views never travel through `archive` anyway. A\n * service's `model.likec4` used to be in the same position and no longer is:\n * this file's original premise, that `likec4.config.json` excludes `services/**`\n * so a model's view ids cannot collide with the landscape's, holds only for a\n * model that STANDS ALONE. A model that EXTENDS the map lives in the root\n * project beside it, so its authored view ids — and those of every `.likec4`\n * beside it — share exactly this namespace. That census is made by the merged\n * load in `./fleet-project.ts` and handed here; the landscape's own claims are\n * still graded from the `architecture/` project, which is the only census a run\n * with no extending model can make.\n *\n * The claims therefore arrive spelled DOCS-RELATIVE, and the caller does the\n * spelling: the `architecture/` project reports a view's `sourcePath` relative\n * to `architecture/`, the merged project reports it relative to the docs root,\n * and a prefix pasted on here would name `architecture/services/…` for every\n * model. The message and the location both print the path verbatim.\n *\n * ONLY the real collision is refused, never the merely reserved-looking name.\n * A landscape view called `subsystem_overview` with no `overview` subsystem\n * breaks nothing today, and refusing it would be loam inventing a rule about a\n * name that harms no one. The latent case is not lost: the moment somebody\n * creates that subsystem, the generator mints the id, and this same check fires\n * on the same run. The message names the reservation anyway, so an author fixing\n * one collision learns not to make the next. The service half holds the same\n * line one step further in: an id is minted only for a service the MAP DRAWS,\n * because that is the render's own gate, so a `services/<id>/` the map does not\n * resolve reserves nothing until it is modelled.\n *\n * Graded on `validate --all` only, and the caller holds that gate together with\n * the landscape-parsed one: under a single-target run the fleet tree is not\n * walked and the generated ids are unknown, so the question is unanswerable and\n * the check stays silent rather than half-answering.\n */\nimport type { ViewIdClaim } from \"../../../../core/c4/parsed/view-ids.js\";\nimport { renderContext } from \"../../../../core/repo/tree/render/body.js\";\nimport { SERVICE_VIEW_PREFIX, serviceViewId } from \"../../../../core/repo/tree/render/service/view-id.js\";\nimport { type MapFacts, SUBSYSTEM_VIEW_PREFIX, subsystemViewId } from \"../../../../core/repo/tree/render/views.js\";\nimport type { FleetTree } from \"../../../../core/repo/tree/walk.js\";\nimport type { Finding } from \"../../../../core/vocabulary/report.js\";\n\n/**\n * `authored` is every view id the project claims, each with the DOCS-RELATIVE\n * path of the file that claims it — static views included, because a static view\n * claims an id exactly as firmly as a dynamic one. Absent (the document did not\n * parse, or an older loader did not read them) is treated as \"nothing to\n * compare\", never as \"nothing collides\".\n */\nexport function viewIdFindings(authored: ViewIdClaim[] | undefined, tree: FleetTree, map: MapFacts): Finding[] {\n if (authored === undefined || authored.length === 0) return [];\n const generated = new Map<string, Minted>(\n tree.subsystems.map((sub) => [\n subsystemViewId(sub),\n { owner: sub.path.join(\"/\"), file: \"architecture/subsystems.likec4\", prefix: SUBSYSTEM_VIEW_PREFIX },\n ]),\n );\n // The per-service ids, and ONLY for a service the map actually resolves —\n // which is why the map comes in at all. `architecture/services.likec4` draws a\n // view for a service exactly when the element→service join answers, so a\n // service the map does not draw mints NOTHING and an authored `service_<id>`\n // beside it breaks nothing today. Reserving it anyway would be this check\n // inventing a rule about a name that harms no one — the same line the module\n // banner already holds for `subsystem_overview` with no `overview` subsystem —\n // and the latent case is not lost: the run after somebody puts that service on\n // the map, the generator mints the id and this check fires. The join arrives\n // through `renderContext`, the generator's OWN, so the census and the render\n // can never disagree about which services are drawn.\n const drawn = renderContext(tree, map.elements);\n for (const service of tree.services) {\n if (!drawn.elementOf.has(service.id)) continue;\n generated.set(serviceViewId(service.id), {\n owner: [...service.subsystem, service.id].join(\"/\"),\n file: \"architecture/services.likec4\",\n prefix: SERVICE_VIEW_PREFIX,\n });\n }\n const findings: Finding[] = [];\n // Sorted and de-duplicated by id so a project claiming the same id twice —\n // itself already a parse error, but reachable through a partial read — yields\n // one finding per id rather than one per occurrence. The first claimant's file\n // is the one named: with a duplicate the project does not parse at all, so\n // there is never a second one to choose between.\n const claimants = new Map<string, string>();\n for (const claim of authored) if (!claimants.has(claim.id)) claimants.set(claim.id, claim.sourcePath);\n for (const [id, file] of [...claimants].sort(([a], [b]) => (a < b ? -1 : 1))) {\n const mint = generated.get(id);\n if (mint === undefined) continue;\n findings.push({\n severity: \"error\",\n code: \"subsystem.view-id-collision\",\n subject: id,\n // The CLAIMANT file — the one the message tells the reader to edit, and\n // the one the report's target fallback gets wrong: it files this under\n // the landscape, which is exactly the file a reader must NOT rename the\n // view in when the claim came from a document of their own.\n locations: [{ path: file, role: \"primary\" as const }],\n message:\n `subsystems: ${file} declares \\`view ${id}\\`, which is the id loam generates ` +\n `into ${mint.file} for services/${mint.owner}/ — LikeC4 merges both files into one project, ` +\n // The ROOT project, not `architecture/`: the claimant may be a model's\n // own view now (an extending model is a document of the root project),\n // and telling that author the `architecture/` project is refused sends\n // them to look for the breakage in a directory they never touched.\n `so it refuses the whole root project and renders nothing. ` +\n `Rename the view in ${file}; ids beginning \\`${mint.prefix}\\` are loam's ` +\n `to mint, and ${mint.file} is generated (\\`loam subsystem sync\\`) and must never be hand-edited.`,\n });\n }\n return findings;\n}\n\n/**\n * What loam mints one reserved id FOR — which subject, into which generated\n * file, under which reserved prefix.\n *\n * A record rather than the bare owner path the map used to hold, because the\n * message has to name the right FILE and the right PREFIX: telling the author of\n * a colliding `service_checkout` that `subsystem_` is loam's and that\n * `subsystems.likec4` is the generated file sends them to look at a file their\n * name has nothing to do with.\n */\ninterface Minted {\n /** The tree path of the subsystem or service the id belongs to, under `services/`. */\n owner: string;\n /** The generated document loam writes that id into, docs-relative. */\n file: string;\n /** The reserved prefix the author is told not to mint under. */\n prefix: string;\n}\n"]}
|
|
@@ -16,10 +16,12 @@
|
|
|
16
16
|
*
|
|
17
17
|
* FOUR THINGS COME OUT, and each is a rule rather than a filter of convenience:
|
|
18
18
|
*
|
|
19
|
-
* -
|
|
20
|
-
*
|
|
21
|
-
*
|
|
22
|
-
*
|
|
19
|
+
* - Every GENERATED `architecture/` document — `subsystems.likec4` and
|
|
20
|
+
* `services.likec4`, taken from the ONE list every project read shares
|
|
21
|
+
* (`c4/project/architecture.ts`) — for `c4/project/documents.ts`'s reason:
|
|
22
|
+
* docs/DESIGN.md rule 26 makes their staleness a byte compare, and a stale
|
|
23
|
+
* one naming a removed element would blank this load and report a cascade
|
|
24
|
+
* whose repair is `loam subsystem sync`.
|
|
23
25
|
* - Anything the root `exclude` covers, read through `c4/root-project/exclude.ts`
|
|
24
26
|
* — the renderer does not load it, so grading it would be loam reporting a
|
|
25
27
|
* failure nobody can see. A root config loam cannot read an `exclude` list out
|
|
@@ -43,7 +45,7 @@
|
|
|
43
45
|
import { readdir } from "node:fs/promises";
|
|
44
46
|
import { join, relative, resolve, sep } from "node:path";
|
|
45
47
|
import { excludingPath, readRootExclude } from "../../../../core/c4/root-project/exclude.js";
|
|
46
|
-
import {
|
|
48
|
+
import { generatedArchitectureDocuments } from "../../../../core/c4/project/architecture.js";
|
|
47
49
|
/** The directory name loam never walks, beside every dot-directory. */
|
|
48
50
|
const PACKAGES = "node_modules";
|
|
49
51
|
/** The one subtree loam drops whatever the root config says — see the banner. */
|
|
@@ -54,12 +56,12 @@ const FEATURES = "features";
|
|
|
54
56
|
*/
|
|
55
57
|
export async function rootProjectDocuments(docsDir, standalone) {
|
|
56
58
|
const exclude = await readRootExclude(docsDir);
|
|
57
|
-
const generated =
|
|
59
|
+
const generated = new Set(generatedArchitectureDocuments(docsDir).map((path) => resolve(path)));
|
|
58
60
|
const excluded = standalone.map((dir) => `${resolve(dir)}${sep}`);
|
|
59
61
|
const root = resolve(docsDir);
|
|
60
62
|
const found = [];
|
|
61
63
|
const keeps = (path) => {
|
|
62
|
-
if (resolve(path)
|
|
64
|
+
if (generated.has(resolve(path)))
|
|
63
65
|
return false;
|
|
64
66
|
if (excluded.some((dir) => resolve(path).startsWith(dir)))
|
|
65
67
|
return false;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"root-documents.js","sourceRoot":"","sources":["../../../../../src/commands/validate/fleet/views/root-documents.ts"],"names":[],"mappings":"AAAA
|
|
1
|
+
{"version":3,"file":"root-documents.js","sourceRoot":"","sources":["../../../../../src/commands/validate/fleet/views/root-documents.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * The documents the RENDERER's root project loads, enumerated the way the\n * renderer enumerates them: everything under the docs root that the root\n * `likec4.config.json` does not exclude.\n *\n * It exists because `./fleet-project.ts` used to build the set BY SHAPE — the\n * `architecture/` project, each extending `model.likec4`, and every `.likec4`\n * inside a service directory — and that is three roots, not a tree. A `.likec4`\n * one directory ABOVE a service (`services/platform/notes.likec4`, the parent of\n * service directories) was in none of them: `loam validate --all` came back\n * byte-identical to baseline and never named the file, while `npx likec4\n * validate --project fleet .` reported Invalid over nine files and thirteen\n * errors (verification 2026-09-04, R2). The same file one directory lower was a\n * loud error. SCHEMA, `loam-codes` and `explain` all said loam loads \"every\n * `.likec4` the root project reads\"; this is that sentence, executed.\n *\n * FOUR THINGS COME OUT, and each is a rule rather than a filter of convenience:\n *\n * - Every GENERATED `architecture/` document — `subsystems.likec4` and\n * `services.likec4`, taken from the ONE list every project read shares\n * (`c4/project/architecture.ts`) — for `c4/project/documents.ts`'s reason:\n * docs/DESIGN.md rule 26 makes their staleness a byte compare, and a stale\n * one naming a removed element would blank this load and report a cascade\n * whose repair is `loam subsystem sync`.\n * - Anything the root `exclude` covers, read through `c4/root-project/exclude.ts`\n * — the renderer does not load it, so grading it would be loam reporting a\n * failure nobody can see. A root config loam cannot read an `exclude` list out\n * of covers nothing: guessing an exclusion is worse than loading one document\n * too many.\n * - Every STANDALONE service directory. Such a model declares its own kinds, so\n * the root project must exclude it and `service.model-unexcluded` is the grade\n * for a root that does not — one finding naming the entry to add, rather than\n * a duplicate-kind cascade under a second code.\n * - `features/`. A feature's `delta.likec4` carries a `specification` block of\n * its own and the directory is transient — the scaffolded root config excludes\n * it for exactly that reason, and a fleet mid-feature would otherwise read as\n * a fleet whose map is declared twice. loam drops it whether or not the entry\n * is still there; `loam validate --feature` is where a delta is graded.\n *\n * Dot-directories and `node_modules` are not walked at all. Those are loam's own\n * machinery (`.loam-before` holds a verbatim copy of the landscape, which would\n * read as the map declared twice) and a package tree, never authored documents —\n * the same thing the scaffold's own `node_modules` glob says.\n */\nimport { readdir } from \"node:fs/promises\";\nimport { join, relative, resolve, sep } from \"node:path\";\nimport { excludingPath, readRootExclude } from \"../../../../core/c4/root-project/exclude.js\";\nimport type { DocsDir } from \"../../../../core/kernel/ids/dirs.js\";\nimport { generatedArchitectureDocuments } from \"../../../../core/c4/project/architecture.js\";\n\n/** The directory name loam never walks, beside every dot-directory. */\nconst PACKAGES = \"node_modules\";\n\n/** The one subtree loam drops whatever the root config says — see the banner. */\nconst FEATURES = \"features\";\n\n/**\n * Every `.likec4` the renderer's root project reads and loam can grade, absolute\n * and sorted. `standalone` is each standalone service's directory, absolute.\n */\nexport async function rootProjectDocuments(docsDir: DocsDir, standalone: readonly string[]): Promise<string[]> {\n const exclude = await readRootExclude(docsDir);\n const generated = new Set(generatedArchitectureDocuments(docsDir).map((path) => resolve(path)));\n const excluded = standalone.map((dir) => `${resolve(dir)}${sep}`);\n const root = resolve(docsDir);\n const found: string[] = [];\n const keeps = (path: string): boolean => {\n if (generated.has(resolve(path))) return false;\n if (excluded.some((dir) => resolve(path).startsWith(dir))) return false;\n const rel = relative(root, path).split(/[\\\\/]/);\n if (rel[0] === FEATURES) return false;\n return exclude === null || excludingPath(exclude, rel.join(\"/\")) === null;\n };\n const walk = async (at: string): Promise<void> => {\n let entries;\n try {\n entries = await readdir(at, { withFileTypes: true });\n } catch {\n // A directory that cannot be read contributes nothing: this grade runs\n // inside `validate --all` over a whole fleet, and the one behaviour it may\n // never have is failing the run it was added to.\n return;\n }\n for (const entry of entries) {\n const path = join(at, entry.name);\n if (entry.isDirectory()) {\n if (entry.name.startsWith(\".\") || entry.name === PACKAGES) continue;\n await walk(path);\n } else if (entry.name.endsWith(\".likec4\") && keeps(path)) {\n found.push(path);\n }\n }\n };\n await walk(root);\n return found.sort();\n}\n"]}
|