@ybotok/loam 0.2.0-alpha.5 → 0.2.0-alpha.6
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 +901 -0
- package/MIGRATING-from-OpenSpec.md +7 -4
- package/QUICKSTART.md +8 -3
- package/README.md +13 -7
- package/ROADMAP.md +264 -11
- package/SCHEMA.md +439 -38
- package/WORKFLOW.md +23 -10
- package/dist/commands/adopt/render.js +47 -8
- package/dist/commands/adopt/render.js.map +1 -1
- package/dist/commands/archive/plan/landscape.js +45 -1
- package/dist/commands/archive/plan/landscape.js.map +1 -1
- package/dist/commands/archive/plan/model/extending.js +85 -0
- package/dist/commands/archive/plan/model/extending.js.map +1 -0
- package/dist/commands/archive/run.js +5 -2
- package/dist/commands/archive/run.js.map +1 -1
- package/dist/commands/context/print.js +11 -1
- package/dist/commands/context/print.js.map +1 -1
- package/dist/commands/doctor.js +46 -0
- package/dist/commands/doctor.js.map +1 -1
- package/dist/commands/explore.js +12 -2
- package/dist/commands/explore.js.map +1 -1
- package/dist/commands/list/views.js +5 -1
- package/dist/commands/list/views.js.map +1 -1
- package/dist/commands/seed/plan.js +97 -3
- package/dist/commands/seed/plan.js.map +1 -1
- package/dist/commands/seed/seed.js +7 -2
- package/dist/commands/seed/seed.js.map +1 -1
- package/dist/commands/show/feature.js +2 -1
- package/dist/commands/show/feature.js.map +1 -1
- package/dist/commands/show/marks.js +3 -5
- package/dist/commands/show/marks.js.map +1 -1
- package/dist/commands/show/service.js +39 -18
- package/dist/commands/show/service.js.map +1 -1
- package/dist/commands/subsystem/sync.js +128 -74
- package/dist/commands/subsystem/sync.js.map +1 -1
- package/dist/commands/subsystem/txn/exclude.js +132 -0
- package/dist/commands/subsystem/txn/exclude.js.map +1 -0
- package/dist/commands/subsystem/txn/report.js +124 -0
- package/dist/commands/subsystem/txn/report.js.map +1 -0
- package/dist/commands/subsystem/txn/txn.js +10 -3
- package/dist/commands/subsystem/txn/txn.js.map +1 -1
- package/dist/commands/subsystem/txn/views.js +113 -16
- package/dist/commands/subsystem/txn/views.js.map +1 -1
- package/dist/commands/validate/arch-coverage.js +16 -9
- package/dist/commands/validate/arch-coverage.js.map +1 -1
- package/dist/commands/validate/checks/fleet-shape.js +30 -77
- package/dist/commands/validate/checks/fleet-shape.js.map +1 -1
- package/dist/commands/validate/checks/vocabulary.js +4 -19
- package/dist/commands/validate/checks/vocabulary.js.map +1 -1
- package/dist/commands/validate/feature.js +1 -1
- package/dist/commands/validate/feature.js.map +1 -1
- package/dist/commands/validate/fleet/census.js +11 -10
- package/dist/commands/validate/fleet/census.js.map +1 -1
- package/dist/commands/validate/fleet/landscape.js +127 -119
- package/dist/commands/validate/fleet/landscape.js.map +1 -1
- package/dist/commands/validate/fleet/load.js +66 -0
- package/dist/commands/validate/fleet/load.js.map +1 -1
- package/dist/commands/validate/fleet/map/attest.js +103 -0
- package/dist/commands/validate/fleet/map/attest.js.map +1 -0
- package/dist/commands/validate/fleet/map/bindings.js +96 -0
- package/dist/commands/validate/fleet/map/bindings.js.map +1 -0
- package/dist/commands/validate/fleet/map/consumers.js +298 -0
- package/dist/commands/validate/fleet/map/consumers.js.map +1 -0
- package/dist/commands/validate/fleet/map/isolation.js +116 -0
- package/dist/commands/validate/fleet/map/isolation.js.map +1 -0
- package/dist/commands/validate/fleet/map/usecases.js +56 -0
- package/dist/commands/validate/fleet/map/usecases.js.map +1 -0
- package/dist/commands/validate/fleet/usecases/steps.js +49 -17
- package/dist/commands/validate/fleet/usecases/steps.js.map +1 -1
- package/dist/commands/validate/fleet/views/fleet-project.js +217 -0
- package/dist/commands/validate/fleet/views/fleet-project.js.map +1 -0
- package/dist/commands/validate/fleet/views/ids.js +13 -9
- package/dist/commands/validate/fleet/views/ids.js.map +1 -1
- package/dist/commands/validate/fleet/views/projects.js +258 -70
- package/dist/commands/validate/fleet/views/projects.js.map +1 -1
- package/dist/commands/validate/fleet/views/root-documents.js +97 -0
- package/dist/commands/validate/fleet/views/root-documents.js.map +1 -0
- package/dist/commands/validate/fleet/views/stale.js +2 -2
- package/dist/commands/validate/fleet/views/stale.js.map +1 -1
- package/dist/commands/validate/service/model/diverged.js +226 -0
- package/dist/commands/validate/service/model/diverged.js.map +1 -0
- package/dist/commands/validate/service/model/grade.js +163 -0
- package/dist/commands/validate/service/model/grade.js.map +1 -0
- package/dist/commands/validate/service/service.js +110 -34
- package/dist/commands/validate/service/service.js.map +1 -1
- package/dist/commands/validate/service/specs.js +39 -5
- package/dist/commands/validate/service/specs.js.map +1 -1
- package/dist/commands/validate/service/spine.js +41 -3
- package/dist/commands/validate/service/spine.js.map +1 -1
- package/dist/commands/validate/service/usecases/tags.js +113 -0
- package/dist/commands/validate/service/usecases/tags.js.map +1 -0
- package/dist/commands/validate/service/usecases/usecases.js +209 -0
- package/dist/commands/validate/service/usecases/usecases.js.map +1 -0
- package/dist/commands/validate/validate.js +10 -24
- package/dist/commands/validate/validate.js.map +1 -1
- package/dist/core/agent/agents-md/artifacts.js +1 -0
- package/dist/core/agent/agents-md/artifacts.js.map +1 -1
- package/dist/core/agent/agents-md/command-map.js +18 -4
- package/dist/core/agent/agents-md/command-map.js.map +1 -1
- package/dist/core/agent/agents-md/cycle.js +24 -9
- package/dist/core/agent/agents-md/cycle.js.map +1 -1
- package/dist/core/agent/agents-md/map/lenses/context.js +4 -3
- package/dist/core/agent/agents-md/map/lenses/context.js.map +1 -1
- package/dist/core/agent/agents-md/map/subsystem.js +51 -11
- package/dist/core/agent/agents-md/map/subsystem.js.map +1 -1
- package/dist/core/agent/agents-md/map/usecases.js +45 -3
- package/dist/core/agent/agents-md/map/usecases.js.map +1 -1
- package/dist/core/agent/agents-md.js +14 -4
- package/dist/core/agent/agents-md.js.map +1 -1
- package/dist/core/agent/support/report.js +39 -5
- package/dist/core/agent/support/report.js.map +1 -1
- package/dist/core/agent/workflows/adopt.js +123 -14
- package/dist/core/agent/workflows/adopt.js.map +1 -1
- package/dist/core/agent/workflows/check.js +35 -20
- package/dist/core/agent/workflows/check.js.map +1 -1
- package/dist/core/agent/workflows/closing.js +3 -1
- package/dist/core/agent/workflows/closing.js.map +1 -1
- package/dist/core/brief/brief.js +63 -11
- package/dist/core/brief/brief.js.map +1 -1
- package/dist/core/brief/checks.js +133 -7
- package/dist/core/brief/checks.js.map +1 -1
- package/dist/core/brief/landscape.js +212 -129
- package/dist/core/brief/landscape.js.map +1 -1
- package/dist/core/brief/map/extend.js +81 -0
- package/dist/core/brief/map/extend.js.map +1 -0
- package/dist/core/brief/map/owed.js +284 -0
- package/dist/core/brief/map/owed.js.map +1 -0
- package/dist/core/brief/targets.js +30 -29
- package/dist/core/brief/targets.js.map +1 -1
- package/dist/core/brief/unchecked.js +12 -2
- package/dist/core/brief/unchecked.js.map +1 -1
- package/dist/core/c4/likec4.js +23 -1
- package/dist/core/c4/likec4.js.map +1 -1
- package/dist/core/c4/parsed/deployment.js +5 -3
- package/dist/core/c4/parsed/deployment.js.map +1 -1
- package/dist/core/c4/parsed/view-ids.js +39 -1
- package/dist/core/c4/parsed/view-ids.js.map +1 -1
- package/dist/core/c4/prefetch/fleet.js +142 -0
- package/dist/core/c4/prefetch/fleet.js.map +1 -0
- package/dist/core/c4/project/architecture.js +52 -4
- package/dist/core/c4/project/architecture.js.map +1 -1
- package/dist/core/c4/project/batch.js +140 -0
- package/dist/core/c4/project/batch.js.map +1 -0
- package/dist/core/c4/project/documents.js +9 -4
- package/dist/core/c4/project/documents.js.map +1 -1
- package/dist/core/c4/project/load.js +77 -38
- package/dist/core/c4/project/load.js.map +1 -1
- package/dist/core/c4/project/staged.js +46 -12
- package/dist/core/c4/project/staged.js.map +1 -1
- package/dist/core/c4/resolve/attested.js +0 -0
- package/dist/core/c4/resolve/attested.js.map +1 -0
- package/dist/core/c4/resolve/service.js +8 -17
- package/dist/core/c4/resolve/service.js.map +1 -1
- package/dist/core/c4/resolve/steps.js +18 -0
- package/dist/core/c4/resolve/steps.js.map +1 -1
- package/dist/core/c4/root-project/exclude.js +352 -0
- package/dist/core/c4/root-project/exclude.js.map +1 -0
- package/dist/core/c4/service-model/documents.js +47 -0
- package/dist/core/c4/service-model/documents.js.map +1 -0
- package/dist/core/c4/service-model/fleet/extending.js +82 -0
- package/dist/core/c4/service-model/fleet/extending.js.map +1 -0
- package/dist/core/c4/service-model/load.js +165 -0
- package/dist/core/c4/service-model/load.js.map +1 -0
- package/dist/core/c4/service-model/renderer.js +128 -0
- package/dist/core/c4/service-model/renderer.js.map +1 -0
- package/dist/core/c4/service-model/shape.js +126 -0
- package/dist/core/c4/service-model/shape.js.map +1 -0
- package/dist/core/c4/service-model/slice.js +188 -0
- package/dist/core/c4/service-model/slice.js.map +1 -0
- package/dist/core/c4/source-scan.js +26 -0
- package/dist/core/c4/source-scan.js.map +1 -1
- package/dist/core/c4/splice/authored-source.js +60 -13
- package/dist/core/c4/splice/authored-source.js.map +1 -1
- package/dist/core/c4/splice/contract.js.map +1 -1
- package/dist/core/c4/splice/identity/declared.js +87 -0
- package/dist/core/c4/splice/identity/declared.js.map +1 -0
- package/dist/core/c4/splice/identity/edges.js +17 -0
- package/dist/core/c4/splice/identity/edges.js.map +1 -1
- package/dist/core/c4/splice/identity/existing.js +80 -0
- package/dist/core/c4/splice/identity/existing.js.map +1 -0
- package/dist/core/c4/splice/identity/interior.js +80 -0
- package/dist/core/c4/splice/identity/interior.js.map +1 -0
- package/dist/core/c4/splice/identity/owner.js +50 -0
- package/dist/core/c4/splice/identity/owner.js.map +1 -0
- package/dist/core/c4/splice/landscape-merge.js +154 -100
- package/dist/core/c4/splice/landscape-merge.js.map +1 -1
- package/dist/core/c4/splice/model/merge.js +140 -0
- package/dist/core/c4/splice/model/merge.js.map +1 -0
- package/dist/core/c4/splice/placement.js +14 -4
- package/dist/core/c4/splice/placement.js.map +1 -1
- package/dist/core/c4/workspace.js +2 -1
- package/dist/core/c4/workspace.js.map +1 -1
- package/dist/core/coherence/coherence.js +7 -0
- package/dist/core/coherence/coherence.js.map +1 -1
- package/dist/core/delta/delta.js +9 -0
- package/dist/core/delta/delta.js.map +1 -1
- package/dist/core/deployment/delta.js +4 -0
- package/dist/core/deployment/delta.js.map +1 -1
- package/dist/core/diff/base-state.js +21 -5
- package/dist/core/diff/base-state.js.map +1 -1
- package/dist/core/diff/victims.js +10 -1
- package/dist/core/diff/victims.js.map +1 -1
- package/dist/core/docs.js +29 -24
- package/dist/core/docs.js.map +1 -1
- package/dist/core/doctor/config.js +7 -3
- package/dist/core/doctor/config.js.map +1 -1
- package/dist/core/doctor/doctor.js +36 -8
- package/dist/core/doctor/doctor.js.map +1 -1
- package/dist/core/doctor/report.js.map +1 -1
- package/dist/core/doctor/reports/scan.js +242 -0
- package/dist/core/doctor/reports/scan.js.map +1 -0
- package/dist/core/explain/refusals.js +1 -1
- package/dist/core/explain/refusals.js.map +1 -1
- package/dist/core/explore/explore.js +44 -9
- package/dist/core/explore/explore.js.map +1 -1
- package/dist/core/fleet-context.js +47 -0
- package/dist/core/fleet-context.js.map +1 -1
- package/dist/core/gate/partners.js +5 -9
- package/dist/core/gate/partners.js.map +1 -1
- package/dist/core/kernel/ids/fqn/ancestors.js +65 -0
- package/dist/core/kernel/ids/fqn/ancestors.js.map +1 -0
- package/dist/core/pack/living.js +47 -13
- package/dist/core/pack/living.js.map +1 -1
- package/dist/core/pack/pack.js +9 -6
- package/dist/core/pack/pack.js.map +1 -1
- package/dist/core/projection/arch-slice.js +2 -2
- package/dist/core/projection/arch-slice.js.map +1 -1
- package/dist/core/repo/paths.js +11 -9
- package/dist/core/repo/paths.js.map +1 -1
- package/dist/core/repo/tree/render/projects.js +93 -35
- package/dist/core/repo/tree/render/projects.js.map +1 -1
- package/dist/core/repo/tree/render/stale.js +2 -3
- package/dist/core/repo/tree/render/stale.js.map +1 -1
- package/dist/core/repo/tree/render/views.js +51 -3
- package/dist/core/repo/tree/render/views.js.map +1 -1
- package/dist/core/repo/tree/walk.js +45 -2
- package/dist/core/repo/tree/walk.js.map +1 -1
- package/dist/core/scaffold/landscape.js +219 -9
- package/dist/core/scaffold/landscape.js.map +1 -1
- package/dist/core/staging/writes.js.map +1 -1
- package/dist/core/usecases/delta/claims.js +1 -0
- package/dist/core/usecases/delta/claims.js.map +1 -1
- package/dist/core/usecases/delta/overlay.js +4 -0
- package/dist/core/usecases/delta/overlay.js.map +1 -1
- package/dist/core/usecases/fleet.js +18 -6
- package/dist/core/usecases/fleet.js.map +1 -1
- package/dist/core/usecases/operations.js +7 -0
- package/dist/core/usecases/operations.js.map +1 -1
- package/dist/core/usecases/service/exclude.js +119 -0
- package/dist/core/usecases/service/exclude.js.map +1 -0
- package/dist/core/usecases/service/flows.js +161 -0
- package/dist/core/usecases/service/flows.js.map +1 -0
- package/dist/core/vocabulary/maturity.js +10 -0
- package/dist/core/vocabulary/maturity.js.map +1 -1
- package/examples/README.md +24 -6
- package/examples/docs/architecture/landscape.likec4 +41 -0
- package/examples/docs/architecture/usecases/order-notification.likec4 +5 -2
- package/examples/docs/likec4.config.json +0 -1
- package/examples/docs/services/checkout-web/model.likec4 +39 -56
- package/examples/docs/services/order-service/arch.spec.md +13 -6
- package/examples/docs/services/order-service/model.likec4 +53 -67
- package/examples/docs/services/payment-service/arch.spec.md +7 -4
- package/examples/docs/services/payment-service/model.likec4 +32 -35
- package/examples/docs/services/platform/identity-service/arch.spec.md +7 -4
- package/examples/docs/services/platform/identity-service/model.likec4 +29 -33
- package/examples/docs/services/platform/notification-service/health.yaml +8 -3
- package/examples/docs/services/platform/notification-service/model.likec4 +39 -59
- package/package.json +1 -1
- package/examples/docs/services/checkout-web/likec4.config.json +0 -4
- package/examples/docs/services/order-service/likec4.config.json +0 -4
- package/examples/docs/services/payment-service/likec4.config.json +0 -4
- package/examples/docs/services/platform/identity-service/likec4.config.json +0 -4
- package/examples/docs/services/platform/notification-service/likec4.config.json +0 -4
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"fleet.js","sourceRoot":"","sources":["../../../src/core/usecases/fleet.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * The fleet's use cases, read once for whoever asks — and NOT read at all when\n * the fleet demonstrably has none.\n *\n * `validate --all` grades use cases as part of a run that was already loading\n * `architecture/` as a LikeC4 project, so it pays nothing extra. Every reader\n * added in phase 4 is in a different position: `loam diff`, `loam delta`,\n * `loam context` and `loam explore` did not load the fleet map as a project\n * before, and `loam delta` in particular sits in `/loam-implement`'s inner loop\n * — a Langium workspace spin-up there is felt on every iteration of somebody's\n * day. So this module's whole job is to answer \"which use cases does the fleet\n * declare?\" and, in the common case where the answer is none, to answer it\n * without starting LikeC4 at all.\n *\n * THE CHEAP GATE IS A BYTE SCAN, and it is sound rather than heuristic. A view\n * is a use case only if it carries a reserved tag — `#cap-<slug>` or\n * `#req-<slug>`, `isUseCase` below is the whole test — and LikeC4 refuses an\n * undeclared tag, so a fleet with a use case has the literal text `cap-` or\n * `req-` in the file that declares the view AND in the file that declares the\n * tag. A document set in which no file contains either substring therefore\n * declares no use case, and `{ views: [] }` is the correct answer, not an\n * evasion. The scan reads bytes rather than decoded text on purpose: it is\n * looking for an ASCII substring, and `readFile(path, \"utf8\")` on a document\n * that is not UTF-8 would substitute U+FFFD and could hide it.\n *\n * BOTH PREFIXES, because the gate and the predicate answer one question and a\n * gate narrower than the predicate is silence rather than a smaller answer. A\n * fleet whose only flow is tagged `#req-` alone declares `tag req-…` and no\n * `cap-` anywhere, so a `cap-`-only scan returned `false` and none of the five\n * readers ever loaded the project — the widening below would have fixed nothing\n * without this one.\n *\n * The gate FAILS CLOSED. A document this module cannot open is treated as one\n * that might mention the prefix, so the project load happens and reports the\n * failure as a hole. The alternative — reading an EACCES as \"no tag here\" — is\n * the fail-open shape docs/CODE-STYLE.md forbids at a validator, arriving in an\n * optimisation.\n */\nimport { readFile } from \"node:fs/promises\";\nimport type { StepScope } from \"../c4/resolve/steps.js\";\nimport { CAP_TAG_PREFIX, REQ_TAG_PREFIX } from \"../capabilities/usecase-join.js\";\nimport type { ParsedView } from \"../c4/parsed/dynamic-views.js\";\nimport { architectureDir, loadArchitecture } from \"../c4/project/architecture.js\";\nimport { architectureDocuments } from \"../c4/project/documents.js\";\nimport { serviceResolver } from \"../c4/resolve/service.js\";\nimport type { DocsDir } from \"../kernel/ids/dirs.js\";\nimport { subsystemViewsPath } from \"../repo/paths.js\";\n\n/**\n * What the fleet's use cases are, or the honest refusal to say.\n *\n * Two variants rather than a record with an `unreadable` flag, because the\n * fields are not independent: a project that did not parse has no elements to\n * resolve against and no views to grade, and a caller handed\n * `{ views: [], unreadable: true }` is one `.length` check away from printing\n * \"no use cases touch this service\" about a fleet nobody could read. Tagged,\n * that state stops being constructible — every caller has to name the\n * `unreadable` arm to get at the views.\n *\n * `read` with an empty `views` is the ordinary answer for a fleet that draws no\n * use cases, and it is exactly what the cheap gate returns without loading\n * anything: the gate is an optimisation over a question with a known answer,\n * never a third verdict.\n */\nexport type UseCaseScan =\n | {\n kind: \"read\";\n /** The reserved-tag views ONLY — `isUseCase` below is the opt-in, and the only one. */\n views: readonly ParsedView[];\n /**\n * What a hop is attributed against, built ONCE per scan. It carries the\n * fleet set that `resolve` below was built from, which is what stops\n * \"which service is this element\" being answered one way by\n * `attributeStep`'s fallback tier and another by a caller's own join —\n * the same reason `validate`'s `StepGrading` holds both together.\n *\n * `known` is REQUIRED here while `StepScope` leaves it optional, and the\n * narrowing is load-bearing rather than tidy: every scan is built from a\n * `UseCaseRequest` that has one, so a consumer reading `model.known` was\n * writing an `undefined` branch that could not be reached and could not be\n * tested. A `StepScope` is still what `attributeStep` receives.\n */\n model: StepScope & { known: ReadonlySet<string> };\n /** One element→service resolver for the whole scan, built with the enumerated fleet. */\n resolve: (id: string) => string;\n }\n | {\n kind: \"unreadable\";\n /** LikeC4's own messages, so a caller can say WHICH document broke. */\n errors: string[];\n };\n\nexport interface UseCaseRequest {\n docsDir: DocsDir;\n /**\n * The enumerated fleet. It rides into the resolver for the reason every other\n * edge join carries it: without it, a step drawn into a modelled container\n * `payment.api` resolves to a service called \"api\" that has never existed\n * (docs/DESIGN.md's serviceResolver-known row).\n */\n known: ReadonlySet<string>;\n}\n\n/**\n * Does any document in the set even mention a reserved tag prefix?\n *\n * Sequential and short-circuiting ON PURPOSE — the usual \"no await in a loop\"\n * rule is about independent work that all has to finish, and this loop exists\n * precisely so that a fleet whose first file declares a use case stops reading\n * the other ninety-nine.\n *\n * `req-` is the looser of the two substrings and that is accepted knowingly: a\n * document mentioning it in prose costs one project load loam would otherwise\n * have skipped, which is a slower right answer. Dropping it costs a wrong one.\n */\nasync function mentionsTagPrefix(paths: readonly string[]): Promise<boolean> {\n for (const path of paths) {\n try {\n const bytes = await readFile(path);\n if (bytes.includes(CAP_TAG_PREFIX) || bytes.includes(REQ_TAG_PREFIX)) return true;\n } catch {\n // Unreadable is not \"no tag\": the project load below owns that failure and\n // will report it as a hole naming the document. Answering `false` here\n // would let an unopenable file grade the fleet as having no use cases.\n return true;\n }\n }\n return false;\n}\n\n/**\n * Is this a use case at all? — the ONE answer, for `validate` and every reader\n * alike.\n *\n * EITHER RESERVED PREFIX OPTS A VIEW IN. A view carrying `#req-CHK-ONCE` and no\n * `#cap-` tag is an author who asked to be graded and forgot half the claim; it\n * earns `usecase.requirement-unresolved`'s `unscoped` arm rather than silence,\n * and SCHEMA.md has said so since the tag shipped.\n *\n * It is exported because it was the asymmetry, not the rule, that was the\n * defect. `validate --all` widened here and the readers did not, so a\n * `#req-`-only flow was graded hop by hop by the gate and invisible to `loam\n * diff`, `delta`, `status`, the packs and `list capabilities` — a fleet where\n * the gate and the report disagree about what exists, which is worse than\n * either answer alone. One predicate is what stops that recurring: a caller\n * that spells `startsWith` itself is a second answer waiting to drift.\n *\n * The parameter is the smallest shape the test needs rather than `ParsedView`,\n * so a caller holding a view-like row does not have to build one to ask. Both\n * prefix tests match their resolver's exactly, case included, so a view can\n * never be opted in here and read as untagged in `resolveCapabilityTags`.\n *\n * NOT the same question as `flowsClaiming`'s. That one asks which flows claim\n * capability X and matches the whole `cap-<slug>` tag; a `#req-`-only view\n * claims none, and it is correct for it to stay out of that answer while\n * counting as a use case here.\n */\nexport function isUseCase(view: { tags: readonly string[] }): boolean {\n return view.tags.some((tag) => tag.startsWith(CAP_TAG_PREFIX) || tag.startsWith(REQ_TAG_PREFIX));\n}\n\n/**\n * The fleet's declared use cases, with the model they are drawn over.\n *\n * Reads only, throws nothing, and refuses nothing: every failure is the\n * `unreadable` arm. Callers memoise this per invocation — nothing is cached\n * here, because a module-level cache keyed on a docsDir would leak across the\n * `chdir`-per-test processes AGENTS.md warns about and, in a long-running host,\n * across invocations.\n */\nexport async function readUseCases(req: UseCaseRequest): Promise<UseCaseScan> {\n const none: UseCaseScan = {\n kind: \"read\",\n views: [],\n model: { elements: [], relationships: [], known: req.known },\n resolve: (id) => id,\n };\n const dir = architectureDir(req.docsDir);\n const documents = await architectureDocuments(dir, [subsystemViewsPath(req.docsDir)]);\n if (!(await mentionsTagPrefix(documents))) return none;\n\n const doc = await loadArchitecture(req.docsDir);\n // loam's standing rule, at project altitude: errors mean no model. A use-case\n // file with one unresolved element leaves the whole project unusable, and\n // grading a flow against half a map is the wrong answer this refuses to give.\n if (doc.errors.length > 0) {\n return { kind: \"unreadable\", errors: doc.errors.map((e) => e.message) };\n }\n return {\n kind: \"read\",\n views: (doc.views ?? []).filter(isUseCase),\n model: { elements: doc.elements, relationships: doc.relationships, known: req.known },\n resolve: serviceResolver(doc.elements, req.known),\n };\n}\n"]}
|
|
1
|
+
{"version":3,"file":"fleet.js","sourceRoot":"","sources":["../../../src/core/usecases/fleet.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAqCG;AACH,OAAO,EAAE,QAAQ,EAAE,MAAM,kBAAkB,CAAC;AAE5C,OAAO,EAAE,cAAc,EAAE,cAAc,EAAE,MAAM,iCAAiC,CAAC;AAEjF,OAAO,EAAE,4BAA4B,EAAE,gBAAgB,EAAE,MAAM,+BAA+B,CAAC;AAC/F,OAAO,EAAE,eAAe,EAAE,MAAM,0BAA0B,CAAC;AA0D3D;;;;;;;;;;;;;;;;;;GAkBG;AACH,MAAM,CAAC,KAAK,UAAU,iBAAiB,CAAC,KAAwB;IAC9D,KAAK,MAAM,IAAI,IAAI,KAAK,EAAE,CAAC;QACzB,IAAI,CAAC;YACH,MAAM,KAAK,GAAG,MAAM,QAAQ,CAAC,IAAI,CAAC,CAAC;YACnC,IAAI,KAAK,CAAC,QAAQ,CAAC,cAAc,CAAC,IAAI,KAAK,CAAC,QAAQ,CAAC,cAAc,CAAC;gBAAE,OAAO,IAAI,CAAC;QACpF,CAAC;QAAC,MAAM,CAAC;YACP,2EAA2E;YAC3E,uEAAuE;YACvE,uEAAuE;YACvE,OAAO,IAAI,CAAC;QACd,CAAC;IACH,CAAC;IACD,OAAO,KAAK,CAAC;AACf,CAAC;AAED;;;;;;;;;;;;;;;;;;;;;;;;;;GA0BG;AACH,MAAM,UAAU,SAAS,CAAC,IAAiC;IACzD,OAAO,IAAI,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC,GAAG,EAAE,EAAE,CAAC,GAAG,CAAC,UAAU,CAAC,cAAc,CAAC,IAAI,GAAG,CAAC,UAAU,CAAC,cAAc,CAAC,CAAC,CAAC;AACnG,CAAC;AAED;;;;;;;;GAQG;AACH,MAAM,CAAC,KAAK,UAAU,YAAY,CAAC,GAAmB;IACpD,MAAM,IAAI,GAAgB;QACxB,IAAI,EAAE,MAAM;QACZ,KAAK,EAAE,EAAE;QACT,KAAK,EAAE,EAAE,QAAQ,EAAE,EAAE,EAAE,aAAa,EAAE,EAAE,EAAE,KAAK,EAAE,GAAG,CAAC,KAAK,EAAE;QAC5D,OAAO,EAAE,CAAC,EAAE,EAAE,EAAE,CAAC,EAAE;KACpB,CAAC;IACF,0EAA0E;IAC1E,wEAAwE;IACxE,4EAA4E;IAC5E,yEAAyE;IACzE,yEAAyE;IACzE,wEAAwE;IACxE,yEAAyE;IACzE,sEAAsE;IACtE,MAAM,SAAS,GAAG,MAAM,4BAA4B,CAAC,GAAG,CAAC,OAAO,CAAC,CAAC;IAClE,IAAI,CAAC,CAAC,MAAM,iBAAiB,CAAC,SAAS,CAAC,CAAC;QAAE,OAAO,IAAI,CAAC;IAEvD,MAAM,GAAG,GAAG,MAAM,gBAAgB,CAAC,GAAG,CAAC,OAAO,CAAC,CAAC;IAChD,8EAA8E;IAC9E,0EAA0E;IAC1E,8EAA8E;IAC9E,IAAI,GAAG,CAAC,MAAM,CAAC,MAAM,GAAG,CAAC,EAAE,CAAC;QAC1B,OAAO,EAAE,IAAI,EAAE,YAAY,EAAE,MAAM,EAAE,GAAG,CAAC,MAAM,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,OAAO,CAAC,EAAE,CAAC;IAC1E,CAAC;IACD,OAAO;QACL,IAAI,EAAE,MAAM;QACZ,KAAK,EAAE,CAAC,GAAG,CAAC,KAAK,IAAI,EAAE,CAAC,CAAC,MAAM,CAAC,SAAS,CAAC;QAC1C,KAAK,EAAE,EAAE,QAAQ,EAAE,GAAG,CAAC,QAAQ,EAAE,aAAa,EAAE,GAAG,CAAC,aAAa,EAAE,KAAK,EAAE,GAAG,CAAC,KAAK,EAAE;QACrF,OAAO,EAAE,eAAe,CAAC,GAAG,CAAC,QAAQ,EAAE,GAAG,CAAC,KAAK,CAAC;KAClD,CAAC;AACJ,CAAC","sourcesContent":["/**\n * The fleet's use cases, read once for whoever asks — and NOT read at all when\n * the fleet demonstrably has none.\n *\n * `validate --all` grades use cases as part of a run that was already loading\n * `architecture/` as a LikeC4 project, so it pays nothing extra. Every reader\n * added in phase 4 is in a different position: `loam diff`, `loam delta`,\n * `loam context` and `loam explore` did not load the fleet map as a project\n * before, and `loam delta` in particular sits in `/loam-implement`'s inner loop\n * — a Langium workspace spin-up there is felt on every iteration of somebody's\n * day. So this module's whole job is to answer \"which use cases does the fleet\n * declare?\" and, in the common case where the answer is none, to answer it\n * without starting LikeC4 at all.\n *\n * THE CHEAP GATE IS A BYTE SCAN, and it is sound rather than heuristic. A view\n * is a use case only if it carries a reserved tag — `#cap-<slug>` or\n * `#req-<slug>`, `isUseCase` below is the whole test — and LikeC4 refuses an\n * undeclared tag, so a fleet with a use case has the literal text `cap-` or\n * `req-` in the file that declares the view AND in the file that declares the\n * tag. A document set in which no file contains either substring therefore\n * declares no use case, and `{ views: [] }` is the correct answer, not an\n * evasion. The scan reads bytes rather than decoded text on purpose: it is\n * looking for an ASCII substring, and `readFile(path, \"utf8\")` on a document\n * that is not UTF-8 would substitute U+FFFD and could hide it.\n *\n * BOTH PREFIXES, because the gate and the predicate answer one question and a\n * gate narrower than the predicate is silence rather than a smaller answer. A\n * fleet whose only flow is tagged `#req-` alone declares `tag req-…` and no\n * `cap-` anywhere, so a `cap-`-only scan returned `false` and none of the five\n * readers ever loaded the project — the widening below would have fixed nothing\n * without this one.\n *\n * The gate FAILS CLOSED. A document this module cannot open is treated as one\n * that might mention the prefix, so the project load happens and reports the\n * failure as a hole. The alternative — reading an EACCES as \"no tag here\" — is\n * the fail-open shape docs/CODE-STYLE.md forbids at a validator, arriving in an\n * optimisation.\n */\nimport { readFile } from \"node:fs/promises\";\nimport type { StepScope } from \"../c4/resolve/steps.js\";\nimport { CAP_TAG_PREFIX, REQ_TAG_PREFIX } from \"../capabilities/usecase-join.js\";\nimport type { ParsedView } from \"../c4/parsed/dynamic-views.js\";\nimport { architectureProjectDocuments, loadArchitecture } from \"../c4/project/architecture.js\";\nimport { serviceResolver } from \"../c4/resolve/service.js\";\nimport type { DocsDir } from \"../kernel/ids/dirs.js\";\n\n/**\n * What the fleet's use cases are, or the honest refusal to say.\n *\n * Two variants rather than a record with an `unreadable` flag, because the\n * fields are not independent: a project that did not parse has no elements to\n * resolve against and no views to grade, and a caller handed\n * `{ views: [], unreadable: true }` is one `.length` check away from printing\n * \"no use cases touch this service\" about a fleet nobody could read. Tagged,\n * that state stops being constructible — every caller has to name the\n * `unreadable` arm to get at the views.\n *\n * `read` with an empty `views` is the ordinary answer for a fleet that draws no\n * use cases, and it is exactly what the cheap gate returns without loading\n * anything: the gate is an optimisation over a question with a known answer,\n * never a third verdict.\n */\nexport type UseCaseScan =\n | {\n kind: \"read\";\n /** The reserved-tag views ONLY — `isUseCase` below is the opt-in, and the only one. */\n views: readonly ParsedView[];\n /**\n * What a hop is attributed against, built ONCE per scan. It carries the\n * fleet set that `resolve` below was built from, which is what stops\n * \"which service is this element\" being answered one way by\n * `attributeStep`'s fallback tier and another by a caller's own join —\n * the same reason `validate`'s `StepGrading` holds both together.\n *\n * `known` is REQUIRED here while `StepScope` leaves it optional, and the\n * narrowing is load-bearing rather than tidy: every scan is built from a\n * `UseCaseRequest` that has one, so a consumer reading `model.known` was\n * writing an `undefined` branch that could not be reached and could not be\n * tested. A `StepScope` is still what `attributeStep` receives.\n */\n model: StepScope & { known: ReadonlySet<string> };\n /** One element→service resolver for the whole scan, built with the enumerated fleet. */\n resolve: (id: string) => string;\n }\n | {\n kind: \"unreadable\";\n /** LikeC4's own messages, so a caller can say WHICH document broke. */\n errors: string[];\n };\n\nexport interface UseCaseRequest {\n docsDir: DocsDir;\n /**\n * The enumerated fleet. It rides into the resolver for the reason every other\n * edge join carries it: without it, a step drawn into a modelled container\n * `payment.api` resolves to a service called \"api\" that has never existed\n * (docs/DESIGN.md's serviceResolver-known row).\n */\n known: ReadonlySet<string>;\n}\n\n/**\n * Does any document in the set even mention a reserved tag prefix?\n *\n * Sequential and short-circuiting ON PURPOSE — the usual \"no await in a loop\"\n * rule is about independent work that all has to finish, and this loop exists\n * precisely so that a fleet whose first file declares a use case stops reading\n * the other ninety-nine.\n *\n * `req-` is the looser of the two substrings and that is accepted knowingly: a\n * document mentioning it in prose costs one project load loam would otherwise\n * have skipped, which is a slower right answer. Dropping it costs a wrong one.\n *\n * Exported for the service-scope reader (`./service/flows.ts`), which asks the\n * same question of the `.likec4` files beside one `model.likec4`: a sibling set\n * that mentions neither prefix declares no use case, so a hand-drawn\n * `views.likec4` costs that service no LikeC4 workspace. One gate, two callers,\n * because a second byte scan spelled in the service package would be free to\n * disagree with this one about which prefixes opt a view in.\n */\nexport async function mentionsTagPrefix(paths: readonly string[]): Promise<boolean> {\n for (const path of paths) {\n try {\n const bytes = await readFile(path);\n if (bytes.includes(CAP_TAG_PREFIX) || bytes.includes(REQ_TAG_PREFIX)) return true;\n } catch {\n // Unreadable is not \"no tag\": the project load below owns that failure and\n // will report it as a hole naming the document. Answering `false` here\n // would let an unopenable file grade the fleet as having no use cases.\n return true;\n }\n }\n return false;\n}\n\n/**\n * Is this a use case at all? — the ONE answer, for `validate` and every reader\n * alike.\n *\n * EITHER RESERVED PREFIX OPTS A VIEW IN. A view carrying `#req-CHK-ONCE` and no\n * `#cap-` tag is an author who asked to be graded and forgot half the claim; it\n * earns `usecase.requirement-unresolved`'s `unscoped` arm rather than silence,\n * and SCHEMA.md has said so since the tag shipped.\n *\n * It is exported because it was the asymmetry, not the rule, that was the\n * defect. `validate --all` widened here and the readers did not, so a\n * `#req-`-only flow was graded hop by hop by the gate and invisible to `loam\n * diff`, `delta`, `status`, the packs and `list capabilities` — a fleet where\n * the gate and the report disagree about what exists, which is worse than\n * either answer alone. One predicate is what stops that recurring: a caller\n * that spells `startsWith` itself is a second answer waiting to drift.\n *\n * The parameter is the smallest shape the test needs rather than `ParsedView`,\n * so a caller holding a view-like row does not have to build one to ask. Both\n * prefix tests match their resolver's exactly, case included, so a view can\n * never be opted in here and read as untagged in `resolveCapabilityTags`.\n *\n * NOT the same question as `flowsClaiming`'s. That one asks which flows claim\n * capability X and matches the whole `cap-<slug>` tag; a `#req-`-only view\n * claims none, and it is correct for it to stay out of that answer while\n * counting as a use case here.\n */\nexport function isUseCase(view: { tags: readonly string[] }): boolean {\n return view.tags.some((tag) => tag.startsWith(CAP_TAG_PREFIX) || tag.startsWith(REQ_TAG_PREFIX));\n}\n\n/**\n * The fleet's declared use cases, with the model they are drawn over.\n *\n * Reads only, throws nothing, and refuses nothing: every failure is the\n * `unreadable` arm. Callers memoise this per invocation — nothing is cached\n * here, because a module-level cache keyed on a docsDir would leak across the\n * `chdir`-per-test processes AGENTS.md warns about and, in a long-running host,\n * across invocations.\n */\nexport async function readUseCases(req: UseCaseRequest): Promise<UseCaseScan> {\n const none: UseCaseScan = {\n kind: \"read\",\n views: [],\n model: { elements: [], relationships: [], known: req.known },\n resolve: (id) => id,\n };\n // EXACTLY the documents `loadArchitecture` will load two lines down, root\n // `exclude` and all — `architectureProjectDocuments` is that one answer\n // (`c4/project/architecture.ts`). The gate used to walk `architecture/` for\n // itself and skip the exclude, which could only over-scan: the byte gate\n // reading a document the loader then drops decides \"there might be a use\n // case here\" and pays for a project load that finds none. Never a wrong\n // answer, but two lists that disagree about what the project is, and the\n // next reader to take the wider one for the project's would be wrong.\n const documents = await architectureProjectDocuments(req.docsDir);\n if (!(await mentionsTagPrefix(documents))) return none;\n\n const doc = await loadArchitecture(req.docsDir);\n // loam's standing rule, at project altitude: errors mean no model. A use-case\n // file with one unresolved element leaves the whole project unusable, and\n // grading a flow against half a map is the wrong answer this refuses to give.\n if (doc.errors.length > 0) {\n return { kind: \"unreadable\", errors: doc.errors.map((e) => e.message) };\n }\n return {\n kind: \"read\",\n views: (doc.views ?? []).filter(isUseCase),\n model: { elements: doc.elements, relationships: doc.relationships, known: req.known },\n resolve: serviceResolver(doc.elements, req.known),\n };\n}\n"]}
|
|
@@ -68,6 +68,13 @@ export function hopsExercising(scan, removed) {
|
|
|
68
68
|
// payment-service rather than against a service called "api".
|
|
69
69
|
if (scan.resolve(attribution.to) !== removed.provider)
|
|
70
70
|
continue;
|
|
71
|
+
// And the CALLER, through the same resolver: a hop whose two endpoints
|
|
72
|
+
// resolve to one service is that service calling itself, which owes no
|
|
73
|
+
// operationId of its own contract — `usecase.step-unlinked`'s fourth guard,
|
|
74
|
+
// applied to the victim join. Without it an internal hop was a break of the
|
|
75
|
+
// provider's own removal (verification 2026-09-04, R3).
|
|
76
|
+
if (scan.resolve(attribution.from) === removed.provider)
|
|
77
|
+
continue;
|
|
71
78
|
if (attribution.verdict === "attributed") {
|
|
72
79
|
if (attribution.op === removed.op)
|
|
73
80
|
out.breaks.push(hopPlace(view, step));
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"operations.js","sourceRoot":"","sources":["../../../src/core/usecases/operations.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * Which hops of which business flows a contract removal breaks.\n *\n * `loam diff --base` already answers \"who still depends on this operation\" with\n * landscape edges and other services' living requirements. Both are true and\n * neither is the sentence a reviewer wants: an edge is a line in a diagram and a\n * requirement is a heading in somebody else's spec, while \"step 4 of Checkout\n * stops working\" is the fact a human weighs a pull request against. This module\n * is the join that produces that sentence, and it produces NOTHING ELSE — no\n * finding, no code, no severity. `core/diff/victims.ts` folds its answers into\n * the `details[]` of the two codes that already exist.\n *\n * ## Attributed is a victim; contested is a suspension\n *\n * The distinction is the whole reason this module is careful rather than a\n * filter. `attributeStep` (core/c4/resolve/steps.ts) grades a hop by the DISTINCT-op\n * count across the relationships backing it:\n *\n * - `attributed` with `op === X` means every relationship behind the hop agrees\n * the hop exercises X. Remove X and the flow breaks at that hop. That is\n * positive evidence, and it is a victim.\n * - `contested` means the relationships DISAGREE, so loam does not know which\n * operation the hop exercises. It must not claim the flow breaks — a guessed\n * victim reads exactly like a real one and turns a warn into an error on\n * somebody's PR — and it must not go silent either, because silence here is\n * the fail-open shape `ConsumerScan.suspended` exists to forbid. It is a\n * suspension: loam says it could not answer.\n *\n * A contested hop is suspended only when X is among its candidate operations.\n * That is not a noise trade, it is the honest boundary: the candidates ARE the\n * relationships backing the hop, so an operation none of them names is an\n * operation the hop cannot be exercising under any reading of the disagreement.\n * Suspending every contested hop in the fleet on every removal would report\n * doubt loam does not actually have.\n *\n * An `unbacked` hop is neither. Nothing in the model backs it, so it names no\n * operation of anybody's contract — `usecase.step-unbacked` is already the error\n * that convicts it, and repeating that here would file a modelling defect\n * against whoever happened to remove an endpoint.\n */\nimport { attributeStep } from \"../c4/resolve/steps.js\";\nimport type { ParsedStep, ParsedView } from \"../c4/parsed/dynamic-views.js\";\nimport { hopPlace } from \"./place.js\";\nimport type { UseCaseScan } from \"./fleet.js\";\n\n/**\n * What one contract removal does to the fleet's use cases.\n *\n * Two lists rather than `victims`/`suspended` — the vocabulary\n * `core/diff/victims.ts` uses — because this module answers about FLOWS and that\n * one answers about consumers. The caller does the folding, and naming the two\n * differently is what stops a reader taking this for a second implementation of\n * `ConsumerScan` (it is not: it never scans a document, and it cannot be\n * unreadable — the scan it runs over was already graded).\n */\nexport interface UseCaseHits {\n /** Hops the removal breaks, each spelled by `hopPlace`. */\n breaks: string[];\n /** Hops loam refuses to grade, each saying why in its own sentence. */\n unsure: string[];\n}\n\n/** The one removal a scan is being asked about. */\nexport interface RemovedOperation {\n /** The service dropping it — the same id the diff files its findings under. */\n provider: string;\n /** The operationId leaving the contract. */\n op: string;\n}\n\nconst EMPTY: UseCaseHits = { breaks: [], unsure: [] };\n\n/** Every hop of every graded use case, view order preserved, so output is stable. */\nfunction hops(scan: Extract<UseCaseScan, { kind: \"read\" }>): Array<{ view: ParsedView; step: ParsedStep }> {\n return scan.views.flatMap((view) => view.steps.map((step) => ({ view, step })));\n}\n\n/**\n * The flows one removed operation breaks, and the ones loam cannot answer for.\n *\n * An `unreadable` scan contributes NOTHING — neither break nor suspension — and\n * that is `core/diff/victims.ts`'s own landscape doctrine applied one axis over:\n * an unreadable fleet map proves nothing either way, `landscape.invalid` is\n * validate's finding to make, and inventing doubt out of a parse error would\n * point the reviewer at a file their change never touched. The caller reports\n * the hole; this join stays quiet about it.\n */\nexport function hopsExercising(scan: UseCaseScan, removed: RemovedOperation): UseCaseHits {\n if (scan.kind !== \"read\") return EMPTY;\n const out: UseCaseHits = { breaks: [], unsure: [] };\n for (const { view, step } of hops(scan)) {\n const attribution = attributeStep(step, scan.model);\n if (attribution.verdict === \"unbacked\") continue;\n // The callee, through the SAME resolver every other join in this run uses,\n // so a hop drawn into a modelled container `payment.api` is filed against\n // payment-service rather than against a service called \"api\".\n if (scan.resolve(attribution.to) !== removed.provider) continue;\n if (attribution.verdict === \"attributed\") {\n if (attribution.op === removed.op) out.breaks.push(hopPlace(view, step));\n continue;\n }\n if (!attribution.ops.includes(removed.op)) continue;\n out.unsure.push(\n `${hopPlace(view, step)} — ${attribution.rels.length} relationships back ` +\n `${attribution.from} -> ${attribution.to} and they name different operations, so whether this hop ` +\n `exercises '${removed.op}' could not be answered`,\n );\n }\n return out;\n}\n\n/** The one message removal a scan is being asked about. */\nexport interface RemovedMessage {\n /** The service dropping the declaration — the diff's own subject. */\n provider: string;\n /** The AsyncAPI message name leaving the contract. */\n name: string;\n}\n\n/**\n * The flows one removed message breaks.\n *\n * No suspension arm, and the asymmetry is a fact about the model rather than an\n * omission: `attributeStep` contests OPERATIONS, because that is where two\n * relationships between one pair of endpoints can disagree. A `consumes` edge\n * names a message outright, so a hop either is backed by one or is not — there\n * is no disagreement for loam to refuse to resolve.\n *\n * The provider exclusion mirrors `messageConsumers` exactly: an edge INTO the\n * provider is the provider's own consumption, which leaves with the same change,\n * so a hop backed only by that edge is not a victim of it.\n */\nexport function hopsConsuming(scan: UseCaseScan, removed: RemovedMessage): string[] {\n if (scan.kind !== \"read\") return [];\n const out: string[] = [];\n for (const { view, step } of hops(scan)) {\n const attribution = attributeStep(step, scan.model);\n if (attribution.verdict === \"unbacked\") continue;\n const consumes = attribution.rels.some(\n (rel) => rel.consumes === removed.name && scan.resolve(rel.target) !== removed.provider,\n );\n if (consumes) out.push(hopPlace(view, step));\n }\n return out;\n}\n"]}
|
|
1
|
+
{"version":3,"file":"operations.js","sourceRoot":"","sources":["../../../src/core/usecases/operations.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAuCG;AACH,OAAO,EAAE,aAAa,EAAE,MAAM,wBAAwB,CAAC;AAEvD,OAAO,EAAE,QAAQ,EAAE,MAAM,YAAY,CAAC;AA4BtC,MAAM,KAAK,GAAgB,EAAE,MAAM,EAAE,EAAE,EAAE,MAAM,EAAE,EAAE,EAAE,CAAC;AAEtD,qFAAqF;AACrF,SAAS,IAAI,CAAC,IAA4C;IACxD,OAAO,IAAI,CAAC,KAAK,CAAC,OAAO,CAAC,CAAC,IAAI,EAAE,EAAE,CAAC,IAAI,CAAC,KAAK,CAAC,GAAG,CAAC,CAAC,IAAI,EAAE,EAAE,CAAC,CAAC,EAAE,IAAI,EAAE,IAAI,EAAE,CAAC,CAAC,CAAC,CAAC;AAClF,CAAC;AAED;;;;;;;;;GASG;AACH,MAAM,UAAU,cAAc,CAAC,IAAiB,EAAE,OAAyB;IACzE,IAAI,IAAI,CAAC,IAAI,KAAK,MAAM;QAAE,OAAO,KAAK,CAAC;IACvC,MAAM,GAAG,GAAgB,EAAE,MAAM,EAAE,EAAE,EAAE,MAAM,EAAE,EAAE,EAAE,CAAC;IACpD,KAAK,MAAM,EAAE,IAAI,EAAE,IAAI,EAAE,IAAI,IAAI,CAAC,IAAI,CAAC,EAAE,CAAC;QACxC,MAAM,WAAW,GAAG,aAAa,CAAC,IAAI,EAAE,IAAI,CAAC,KAAK,CAAC,CAAC;QACpD,IAAI,WAAW,CAAC,OAAO,KAAK,UAAU;YAAE,SAAS;QACjD,2EAA2E;QAC3E,0EAA0E;QAC1E,8DAA8D;QAC9D,IAAI,IAAI,CAAC,OAAO,CAAC,WAAW,CAAC,EAAE,CAAC,KAAK,OAAO,CAAC,QAAQ;YAAE,SAAS;QAChE,uEAAuE;QACvE,uEAAuE;QACvE,4EAA4E;QAC5E,4EAA4E;QAC5E,wDAAwD;QACxD,IAAI,IAAI,CAAC,OAAO,CAAC,WAAW,CAAC,IAAI,CAAC,KAAK,OAAO,CAAC,QAAQ;YAAE,SAAS;QAClE,IAAI,WAAW,CAAC,OAAO,KAAK,YAAY,EAAE,CAAC;YACzC,IAAI,WAAW,CAAC,EAAE,KAAK,OAAO,CAAC,EAAE;gBAAE,GAAG,CAAC,MAAM,CAAC,IAAI,CAAC,QAAQ,CAAC,IAAI,EAAE,IAAI,CAAC,CAAC,CAAC;YACzE,SAAS;QACX,CAAC;QACD,IAAI,CAAC,WAAW,CAAC,GAAG,CAAC,QAAQ,CAAC,OAAO,CAAC,EAAE,CAAC;YAAE,SAAS;QACpD,GAAG,CAAC,MAAM,CAAC,IAAI,CACb,GAAG,QAAQ,CAAC,IAAI,EAAE,IAAI,CAAC,MAAM,WAAW,CAAC,IAAI,CAAC,MAAM,sBAAsB;YACxE,GAAG,WAAW,CAAC,IAAI,OAAO,WAAW,CAAC,EAAE,2DAA2D;YACnG,cAAc,OAAO,CAAC,EAAE,yBAAyB,CACpD,CAAC;IACJ,CAAC;IACD,OAAO,GAAG,CAAC;AACb,CAAC;AAUD;;;;;;;;;;;;GAYG;AACH,MAAM,UAAU,aAAa,CAAC,IAAiB,EAAE,OAAuB;IACtE,IAAI,IAAI,CAAC,IAAI,KAAK,MAAM;QAAE,OAAO,EAAE,CAAC;IACpC,MAAM,GAAG,GAAa,EAAE,CAAC;IACzB,KAAK,MAAM,EAAE,IAAI,EAAE,IAAI,EAAE,IAAI,IAAI,CAAC,IAAI,CAAC,EAAE,CAAC;QACxC,MAAM,WAAW,GAAG,aAAa,CAAC,IAAI,EAAE,IAAI,CAAC,KAAK,CAAC,CAAC;QACpD,IAAI,WAAW,CAAC,OAAO,KAAK,UAAU;YAAE,SAAS;QACjD,MAAM,QAAQ,GAAG,WAAW,CAAC,IAAI,CAAC,IAAI,CACpC,CAAC,GAAG,EAAE,EAAE,CAAC,GAAG,CAAC,QAAQ,KAAK,OAAO,CAAC,IAAI,IAAI,IAAI,CAAC,OAAO,CAAC,GAAG,CAAC,MAAM,CAAC,KAAK,OAAO,CAAC,QAAQ,CACxF,CAAC;QACF,IAAI,QAAQ;YAAE,GAAG,CAAC,IAAI,CAAC,QAAQ,CAAC,IAAI,EAAE,IAAI,CAAC,CAAC,CAAC;IAC/C,CAAC;IACD,OAAO,GAAG,CAAC;AACb,CAAC","sourcesContent":["/**\n * Which hops of which business flows a contract removal breaks.\n *\n * `loam diff --base` already answers \"who still depends on this operation\" with\n * landscape edges and other services' living requirements. Both are true and\n * neither is the sentence a reviewer wants: an edge is a line in a diagram and a\n * requirement is a heading in somebody else's spec, while \"step 4 of Checkout\n * stops working\" is the fact a human weighs a pull request against. This module\n * is the join that produces that sentence, and it produces NOTHING ELSE — no\n * finding, no code, no severity. `core/diff/victims.ts` folds its answers into\n * the `details[]` of the two codes that already exist.\n *\n * ## Attributed is a victim; contested is a suspension\n *\n * The distinction is the whole reason this module is careful rather than a\n * filter. `attributeStep` (core/c4/resolve/steps.ts) grades a hop by the DISTINCT-op\n * count across the relationships backing it:\n *\n * - `attributed` with `op === X` means every relationship behind the hop agrees\n * the hop exercises X. Remove X and the flow breaks at that hop. That is\n * positive evidence, and it is a victim.\n * - `contested` means the relationships DISAGREE, so loam does not know which\n * operation the hop exercises. It must not claim the flow breaks — a guessed\n * victim reads exactly like a real one and turns a warn into an error on\n * somebody's PR — and it must not go silent either, because silence here is\n * the fail-open shape `ConsumerScan.suspended` exists to forbid. It is a\n * suspension: loam says it could not answer.\n *\n * A contested hop is suspended only when X is among its candidate operations.\n * That is not a noise trade, it is the honest boundary: the candidates ARE the\n * relationships backing the hop, so an operation none of them names is an\n * operation the hop cannot be exercising under any reading of the disagreement.\n * Suspending every contested hop in the fleet on every removal would report\n * doubt loam does not actually have.\n *\n * An `unbacked` hop is neither. Nothing in the model backs it, so it names no\n * operation of anybody's contract — `usecase.step-unbacked` is already the error\n * that convicts it, and repeating that here would file a modelling defect\n * against whoever happened to remove an endpoint.\n */\nimport { attributeStep } from \"../c4/resolve/steps.js\";\nimport type { ParsedStep, ParsedView } from \"../c4/parsed/dynamic-views.js\";\nimport { hopPlace } from \"./place.js\";\nimport type { UseCaseScan } from \"./fleet.js\";\n\n/**\n * What one contract removal does to the fleet's use cases.\n *\n * Two lists rather than `victims`/`suspended` — the vocabulary\n * `core/diff/victims.ts` uses — because this module answers about FLOWS and that\n * one answers about consumers. The caller does the folding, and naming the two\n * differently is what stops a reader taking this for a second implementation of\n * `ConsumerScan` (it is not: it never scans a document, and it cannot be\n * unreadable — the scan it runs over was already graded).\n */\nexport interface UseCaseHits {\n /** Hops the removal breaks, each spelled by `hopPlace`. */\n breaks: string[];\n /** Hops loam refuses to grade, each saying why in its own sentence. */\n unsure: string[];\n}\n\n/** The one removal a scan is being asked about. */\nexport interface RemovedOperation {\n /** The service dropping it — the same id the diff files its findings under. */\n provider: string;\n /** The operationId leaving the contract. */\n op: string;\n}\n\nconst EMPTY: UseCaseHits = { breaks: [], unsure: [] };\n\n/** Every hop of every graded use case, view order preserved, so output is stable. */\nfunction hops(scan: Extract<UseCaseScan, { kind: \"read\" }>): Array<{ view: ParsedView; step: ParsedStep }> {\n return scan.views.flatMap((view) => view.steps.map((step) => ({ view, step })));\n}\n\n/**\n * The flows one removed operation breaks, and the ones loam cannot answer for.\n *\n * An `unreadable` scan contributes NOTHING — neither break nor suspension — and\n * that is `core/diff/victims.ts`'s own landscape doctrine applied one axis over:\n * an unreadable fleet map proves nothing either way, `landscape.invalid` is\n * validate's finding to make, and inventing doubt out of a parse error would\n * point the reviewer at a file their change never touched. The caller reports\n * the hole; this join stays quiet about it.\n */\nexport function hopsExercising(scan: UseCaseScan, removed: RemovedOperation): UseCaseHits {\n if (scan.kind !== \"read\") return EMPTY;\n const out: UseCaseHits = { breaks: [], unsure: [] };\n for (const { view, step } of hops(scan)) {\n const attribution = attributeStep(step, scan.model);\n if (attribution.verdict === \"unbacked\") continue;\n // The callee, through the SAME resolver every other join in this run uses,\n // so a hop drawn into a modelled container `payment.api` is filed against\n // payment-service rather than against a service called \"api\".\n if (scan.resolve(attribution.to) !== removed.provider) continue;\n // And the CALLER, through the same resolver: a hop whose two endpoints\n // resolve to one service is that service calling itself, which owes no\n // operationId of its own contract — `usecase.step-unlinked`'s fourth guard,\n // applied to the victim join. Without it an internal hop was a break of the\n // provider's own removal (verification 2026-09-04, R3).\n if (scan.resolve(attribution.from) === removed.provider) continue;\n if (attribution.verdict === \"attributed\") {\n if (attribution.op === removed.op) out.breaks.push(hopPlace(view, step));\n continue;\n }\n if (!attribution.ops.includes(removed.op)) continue;\n out.unsure.push(\n `${hopPlace(view, step)} — ${attribution.rels.length} relationships back ` +\n `${attribution.from} -> ${attribution.to} and they name different operations, so whether this hop ` +\n `exercises '${removed.op}' could not be answered`,\n );\n }\n return out;\n}\n\n/** The one message removal a scan is being asked about. */\nexport interface RemovedMessage {\n /** The service dropping the declaration — the diff's own subject. */\n provider: string;\n /** The AsyncAPI message name leaving the contract. */\n name: string;\n}\n\n/**\n * The flows one removed message breaks.\n *\n * No suspension arm, and the asymmetry is a fact about the model rather than an\n * omission: `attributeStep` contests OPERATIONS, because that is where two\n * relationships between one pair of endpoints can disagree. A `consumes` edge\n * names a message outright, so a hop either is backed by one or is not — there\n * is no disagreement for loam to refuse to resolve.\n *\n * The provider exclusion mirrors `messageConsumers` exactly: an edge INTO the\n * provider is the provider's own consumption, which leaves with the same change,\n * so a hop backed only by that edge is not a victim of it.\n */\nexport function hopsConsuming(scan: UseCaseScan, removed: RemovedMessage): string[] {\n if (scan.kind !== \"read\") return [];\n const out: string[] = [];\n for (const { view, step } of hops(scan)) {\n const attribution = attributeStep(step, scan.model);\n if (attribution.verdict === \"unbacked\") continue;\n const consumes = attribution.rels.some(\n (rel) => rel.consumes === removed.name && scan.resolve(rel.target) !== removed.provider,\n );\n if (consumes) out.push(hopPlace(view, step));\n }\n return out;\n}\n"]}
|
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Which `.likec4` files beside a `model.likec4` the RENDERER actually loads —
|
|
3
|
+
* the `exclude` list of the service's own `likec4.config.json`.
|
|
4
|
+
*
|
|
5
|
+
* loam writes that file with two keys and never reads it back
|
|
6
|
+
* (`core/repo/tree/render/projects.ts` states the ownership rule), but a team
|
|
7
|
+
* may add keys to it, and `exclude` is the one that changes what the project
|
|
8
|
+
* holds. Until this module existed the service-flow reader staged every sibling
|
|
9
|
+
* regardless: a view the renderer never sees was graded, and the finding it
|
|
10
|
+
* earned said the project is "read the way the renderer reads it" and that the
|
|
11
|
+
* view still renders — three claims about a file nobody would ever open
|
|
12
|
+
* (verification 2026-09-04, D10).
|
|
13
|
+
*
|
|
14
|
+
* STANDALONE MODELS ONLY, which is where this file IS the project config. A
|
|
15
|
+
* model that extends the map is read inside the ROOT project, whose own
|
|
16
|
+
* `exclude` decides what it holds; a per-service config beside such a model is
|
|
17
|
+
* a stray with a finding of its own (`service.likec4-config-stray`), and
|
|
18
|
+
* honouring its list here would silence a document the renderer really does
|
|
19
|
+
* load.
|
|
20
|
+
*
|
|
21
|
+
* THE MATCHER IS THE ONE MEASURED, at the likec4 1.59.2 pin, on a scratch
|
|
22
|
+
* service directory holding `model.likec4`, `views.likec4` and
|
|
23
|
+
* `usecases/flow.likec4` (`npx likec4 validate <dir>`, counting "found N source
|
|
24
|
+
* files"): an entry drops a file when the file's directory-relative path STARTS
|
|
25
|
+
* WITH the entry (`views`, `views.`, `views.likec4`, `usecases`, `usecases/`,
|
|
26
|
+
* `usecases/flow` each dropped one; `ews.likec4` dropped none), or when it
|
|
27
|
+
* matches as a glob whose `*` crosses `/` (`*.likec4` dropped all THREE), or
|
|
28
|
+
* when a leading RECURSIVE segment — a doubled star and a slash — stands for
|
|
29
|
+
* the zero or more directories above the file: that segment before `*.likec4`
|
|
30
|
+
* dropped all three, and before `usecases` (with a recursive tail) or
|
|
31
|
+
* `views.likec4` it dropped one each. All three halves are implemented because
|
|
32
|
+
* all three were observed; nothing here interprets more than that, and an entry
|
|
33
|
+
* loam cannot match leaves the file loaded — grading a file the renderer skips
|
|
34
|
+
* is a wrong sentence, and skipping one it reads is a missing finding, which is
|
|
35
|
+
* the worse of the two.
|
|
36
|
+
*/
|
|
37
|
+
import { readFile } from "node:fs/promises";
|
|
38
|
+
import { relative } from "node:path";
|
|
39
|
+
import { isRecord } from "../../kernel/records.js";
|
|
40
|
+
import { excludingPath } from "../../c4/root-project/exclude.js";
|
|
41
|
+
/** The per-service project file, by the name `core/repo/tree/render/projects.ts` writes it under. */
|
|
42
|
+
const CONFIG = "likec4.config.json";
|
|
43
|
+
/**
|
|
44
|
+
* The service project's `exclude` entries, or `[]` when there is no list to
|
|
45
|
+
* read — absent, unreadable, not JSON, or an `exclude` that is not a list of
|
|
46
|
+
* strings. Failing open is right here and nowhere else: the answer decides
|
|
47
|
+
* whether loam GRADES a document, and an empty list grades everything, which is
|
|
48
|
+
* exactly what loam did before this module and never a new silence.
|
|
49
|
+
*/
|
|
50
|
+
export async function serviceProjectExclude(dir) {
|
|
51
|
+
const text = await readFile(`${dir}/${CONFIG}`, "utf8").catch(() => null);
|
|
52
|
+
if (text === null)
|
|
53
|
+
return [];
|
|
54
|
+
let parsed;
|
|
55
|
+
try {
|
|
56
|
+
parsed = JSON.parse(text);
|
|
57
|
+
}
|
|
58
|
+
catch {
|
|
59
|
+
return [];
|
|
60
|
+
}
|
|
61
|
+
if (!isRecord(parsed))
|
|
62
|
+
return [];
|
|
63
|
+
const exclude = parsed["exclude"];
|
|
64
|
+
if (!Array.isArray(exclude))
|
|
65
|
+
return [];
|
|
66
|
+
const entries = exclude;
|
|
67
|
+
return entries.filter((entry) => typeof entry === "string" && entry.trim() !== "");
|
|
68
|
+
}
|
|
69
|
+
/** The siblings that survive the list — absolute paths in, absolute paths out, order kept. */
|
|
70
|
+
export function keepIncluded(dir, exclude, paths) {
|
|
71
|
+
if (exclude.length === 0)
|
|
72
|
+
return [...paths];
|
|
73
|
+
return paths.filter((path) => !excluded(exclude, relative(dir, path).split(/[\\/]/).join("/")));
|
|
74
|
+
}
|
|
75
|
+
/**
|
|
76
|
+
* Three rungs, because three separate spellings were measured hiding a file and
|
|
77
|
+
* no one of them subsumes the others.
|
|
78
|
+
*
|
|
79
|
+
* - the bare PREFIX (`views`, `usecases/flow`), which is the renderer's own
|
|
80
|
+
* behaviour and matches nothing a glob would;
|
|
81
|
+
* - `excludingPath`, the segment walk `core/c4/root-project/exclude.ts` already
|
|
82
|
+
* owns, which is what reads a doubled star anywhere in the entry — the three
|
|
83
|
+
* recursive spellings the docstring above measured each hide what the
|
|
84
|
+
* renderer hides, and the `globRe` half alone matched NONE of them, because a
|
|
85
|
+
* leading recursive segment compiled to a REQUIRED slash. loam therefore
|
|
86
|
+
* graded a file in no project it may claim to be reading, and gated the run
|
|
87
|
+
* on it (verification 2026-09-04);
|
|
88
|
+
* - `globRe`, kept for the one case the segment walk cannot express: a `*` that
|
|
89
|
+
* CROSSES `/`, which is what `*.likec4` hiding `usecases/flow.likec4` was
|
|
90
|
+
* measured doing.
|
|
91
|
+
*
|
|
92
|
+
* A union only ever hides more, and the module's own rule says the worse of the
|
|
93
|
+
* two mistakes is skipping a file the renderer reads; each rung above is a
|
|
94
|
+
* measurement rather than an interpretation, so the union stays honest.
|
|
95
|
+
*/
|
|
96
|
+
function excluded(exclude, rel) {
|
|
97
|
+
return exclude.some((raw) => {
|
|
98
|
+
const entry = raw.replace(/^\.\//, "");
|
|
99
|
+
if (entry === "")
|
|
100
|
+
return false;
|
|
101
|
+
if (rel.startsWith(entry))
|
|
102
|
+
return true;
|
|
103
|
+
if (excludingPath([entry], rel) !== null)
|
|
104
|
+
return true;
|
|
105
|
+
return entry.includes("*") && globRe(entry).test(rel);
|
|
106
|
+
});
|
|
107
|
+
}
|
|
108
|
+
/**
|
|
109
|
+
* One entry as a whole-path regular expression, with `*` crossing `/` — which
|
|
110
|
+
* is what `*.likec4` excluding `usecases/flow.likec4` measures. Built per call
|
|
111
|
+
* rather than cached: a `RegExp` is cheap beside the readdir that produced the
|
|
112
|
+
* paths, and a module-level cache keyed on a string is the shared state
|
|
113
|
+
* AGENTS.md warns about.
|
|
114
|
+
*/
|
|
115
|
+
function globRe(entry) {
|
|
116
|
+
const source = entry.replace(/[.+^${}()|[\]\\]/g, "\\$&").replace(/\*+/g, ".*").replace(/\?/g, ".");
|
|
117
|
+
return new RegExp(`^${source}$`);
|
|
118
|
+
}
|
|
119
|
+
//# sourceMappingURL=exclude.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"exclude.js","sourceRoot":"","sources":["../../../../src/core/usecases/service/exclude.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * Which `.likec4` files beside a `model.likec4` the RENDERER actually loads —\n * the `exclude` list of the service's own `likec4.config.json`.\n *\n * loam writes that file with two keys and never reads it back\n * (`core/repo/tree/render/projects.ts` states the ownership rule), but a team\n * may add keys to it, and `exclude` is the one that changes what the project\n * holds. Until this module existed the service-flow reader staged every sibling\n * regardless: a view the renderer never sees was graded, and the finding it\n * earned said the project is \"read the way the renderer reads it\" and that the\n * view still renders — three claims about a file nobody would ever open\n * (verification 2026-09-04, D10).\n *\n * STANDALONE MODELS ONLY, which is where this file IS the project config. A\n * model that extends the map is read inside the ROOT project, whose own\n * `exclude` decides what it holds; a per-service config beside such a model is\n * a stray with a finding of its own (`service.likec4-config-stray`), and\n * honouring its list here would silence a document the renderer really does\n * load.\n *\n * THE MATCHER IS THE ONE MEASURED, at the likec4 1.59.2 pin, on a scratch\n * service directory holding `model.likec4`, `views.likec4` and\n * `usecases/flow.likec4` (`npx likec4 validate <dir>`, counting \"found N source\n * files\"): an entry drops a file when the file's directory-relative path STARTS\n * WITH the entry (`views`, `views.`, `views.likec4`, `usecases`, `usecases/`,\n * `usecases/flow` each dropped one; `ews.likec4` dropped none), or when it\n * matches as a glob whose `*` crosses `/` (`*.likec4` dropped all THREE), or\n * when a leading RECURSIVE segment — a doubled star and a slash — stands for\n * the zero or more directories above the file: that segment before `*.likec4`\n * dropped all three, and before `usecases` (with a recursive tail) or\n * `views.likec4` it dropped one each. All three halves are implemented because\n * all three were observed; nothing here interprets more than that, and an entry\n * loam cannot match leaves the file loaded — grading a file the renderer skips\n * is a wrong sentence, and skipping one it reads is a missing finding, which is\n * the worse of the two.\n */\nimport { readFile } from \"node:fs/promises\";\nimport { relative } from \"node:path\";\nimport { isRecord } from \"../../kernel/records.js\";\nimport { excludingPath } from \"../../c4/root-project/exclude.js\";\nimport type { ServiceDir } from \"../../kernel/ids/dirs.js\";\n\n/** The per-service project file, by the name `core/repo/tree/render/projects.ts` writes it under. */\nconst CONFIG = \"likec4.config.json\";\n\n/**\n * The service project's `exclude` entries, or `[]` when there is no list to\n * read — absent, unreadable, not JSON, or an `exclude` that is not a list of\n * strings. Failing open is right here and nowhere else: the answer decides\n * whether loam GRADES a document, and an empty list grades everything, which is\n * exactly what loam did before this module and never a new silence.\n */\nexport async function serviceProjectExclude(dir: ServiceDir): Promise<string[]> {\n const text = await readFile(`${dir}/${CONFIG}`, \"utf8\").catch(() => null);\n if (text === null) return [];\n let parsed: unknown;\n try {\n parsed = JSON.parse(text);\n } catch {\n return [];\n }\n if (!isRecord(parsed)) return [];\n const exclude: unknown = parsed[\"exclude\"];\n if (!Array.isArray(exclude)) return [];\n const entries: unknown[] = exclude;\n return entries.filter((entry): entry is string => typeof entry === \"string\" && entry.trim() !== \"\");\n}\n\n/** The siblings that survive the list — absolute paths in, absolute paths out, order kept. */\nexport function keepIncluded(dir: ServiceDir, exclude: readonly string[], paths: readonly string[]): string[] {\n if (exclude.length === 0) return [...paths];\n return paths.filter((path) => !excluded(exclude, relative(dir, path).split(/[\\\\/]/).join(\"/\")));\n}\n\n/**\n * Three rungs, because three separate spellings were measured hiding a file and\n * no one of them subsumes the others.\n *\n * - the bare PREFIX (`views`, `usecases/flow`), which is the renderer's own\n * behaviour and matches nothing a glob would;\n * - `excludingPath`, the segment walk `core/c4/root-project/exclude.ts` already\n * owns, which is what reads a doubled star anywhere in the entry — the three\n * recursive spellings the docstring above measured each hide what the\n * renderer hides, and the `globRe` half alone matched NONE of them, because a\n * leading recursive segment compiled to a REQUIRED slash. loam therefore\n * graded a file in no project it may claim to be reading, and gated the run\n * on it (verification 2026-09-04);\n * - `globRe`, kept for the one case the segment walk cannot express: a `*` that\n * CROSSES `/`, which is what `*.likec4` hiding `usecases/flow.likec4` was\n * measured doing.\n *\n * A union only ever hides more, and the module's own rule says the worse of the\n * two mistakes is skipping a file the renderer reads; each rung above is a\n * measurement rather than an interpretation, so the union stays honest.\n */\nfunction excluded(exclude: readonly string[], rel: string): boolean {\n return exclude.some((raw) => {\n const entry = raw.replace(/^\\.\\//, \"\");\n if (entry === \"\") return false;\n if (rel.startsWith(entry)) return true;\n if (excludingPath([entry], rel) !== null) return true;\n return entry.includes(\"*\") && globRe(entry).test(rel);\n });\n}\n\n/**\n * One entry as a whole-path regular expression, with `*` crossing `/` — which\n * is what `*.likec4` excluding `usecases/flow.likec4` measures. Built per call\n * rather than cached: a `RegExp` is cheap beside the readdir that produced the\n * paths, and a module-level cache keyed on a string is the shared state\n * AGENTS.md warns about.\n */\nfunction globRe(entry: string): RegExp {\n const source = entry.replace(/[.+^${}()|[\\]\\\\]/g, \"\\\\$&\").replace(/\\*+/g, \".*\").replace(/\\?/g, \".\");\n return new RegExp(`^${source}$`);\n}\n"]}
|
|
@@ -0,0 +1,161 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* A service's OWN use cases: the `dynamic view`s its LikeC4 project declares,
|
|
3
|
+
* read the way the renderer reads that project.
|
|
4
|
+
*
|
|
5
|
+
* THE SLOT IS THE PROJECT. `loam subsystem sync` writes a
|
|
6
|
+
* `services/<…>/<svc>/likec4.config.json` that registers every `.likec4` under
|
|
7
|
+
* the service directory as one LikeC4 project, so the renderer already shows a
|
|
8
|
+
* `views.likec4` beside the model, or a `usecases/<name>.likec4` under it. loam
|
|
9
|
+
* reads exactly that set — `model.likec4` plus every sibling, recursively, minus
|
|
10
|
+
* whatever that project file's own `exclude` drops (`./exclude.ts`) — and
|
|
11
|
+
* nothing else, because a slot narrower than the renderer's would leave a flow
|
|
12
|
+
* that renders and is graded by nothing, which is the report this module
|
|
13
|
+
* answers: an intra-service flow behind an `arch.spec.md` requirement whose
|
|
14
|
+
* `Covers:` names containers had no home. `architecture/usecases/` is a
|
|
15
|
+
* separate project that cannot resolve a container (one such file turned a
|
|
16
|
+
* whole fleet `landscape.invalid`), and the author's workaround beside the
|
|
17
|
+
* model was invisible to every check. The read is NOT gated on the project
|
|
18
|
+
* file's presence: that file is create-only and a team may delete it, its
|
|
19
|
+
* absence has a warning of its own (`service.likec4-config-missing`), and a
|
|
20
|
+
* flow the renderer cannot show yet is still a flow loam can grade.
|
|
21
|
+
*
|
|
22
|
+
* MODEL FACTS STAY SINGLE-FILE. `c4.valid`'s counts, `Covers:` resolution,
|
|
23
|
+
* `health.dependency-unmodelled` and every other model reader keep the
|
|
24
|
+
* single-file load the service target already made; the project is loaded here
|
|
25
|
+
* for the VIEWS and their `sourcePath` only, and only when there is something
|
|
26
|
+
* to grade. A sibling that declares elements extends what a hop may name, and
|
|
27
|
+
* that is the renderer's reading too.
|
|
28
|
+
*
|
|
29
|
+
* TWO SHAPES, ONE SLOT. A model that stands alone is a project of its own
|
|
30
|
+
* directory, exactly as above. A model that EXTENDS the fleet map is not: its
|
|
31
|
+
* containers are declared inside an element the map owns, so a flow beside it
|
|
32
|
+
* naming one of those containers only resolves in a project that also holds the
|
|
33
|
+
* map (`c4/service-model/documents.ts`, `serviceFlowDocuments`). That project is
|
|
34
|
+
* staged from the DOCS ROOT, so every view comes back spelled
|
|
35
|
+
* `services/<tree>/usecases/x.likec4` — and the service prefix is stripped here,
|
|
36
|
+
* because every finding this scan feeds names a file relative to the service
|
|
37
|
+
* directory and a reader must not have to work out which spelling they were
|
|
38
|
+
* given. The same prefix is the FILTER: the map's own `architecture/usecases/`
|
|
39
|
+
* flows are in that project too, and without it every service in the fleet would
|
|
40
|
+
* be graded for every fleet-level use case.
|
|
41
|
+
*
|
|
42
|
+
* THE CHEAP GATE, in order, so a fleet without the axis pays one recursive
|
|
43
|
+
* readdir per service and nothing else:
|
|
44
|
+
*
|
|
45
|
+
* 1. no sibling → nothing new is loaded, and the model's own tagged views are
|
|
46
|
+
* graded at zero cost against the project the model was READ in. For a
|
|
47
|
+
* standalone model that is the single-file load already in hand, which IS
|
|
48
|
+
* its whole project; for an extending one it is `ServiceModel.project`, the
|
|
49
|
+
* map and this model together — NOT the service's own slice of it, which is
|
|
50
|
+
* what the caller passes as `model` and what every count and `Covers:` line
|
|
51
|
+
* is answered from. A hop backed by a map edge is backed wherever the view
|
|
52
|
+
* is written, and grading the two arms against different documents made one
|
|
53
|
+
* view `usecase.step-unbacked` in model.likec4 and clean in a sibling. A
|
|
54
|
+
* standalone model's views came back named `source.c4`
|
|
55
|
+
* (`core/usecases/place.ts` records the rule) and an extending model's carry
|
|
56
|
+
* their docs-relative path, so the file is spelled here, where it is known,
|
|
57
|
+
* for both: `model.likec4`;
|
|
58
|
+
* 2. siblings, no tagged view in the model, and no reserved prefix in any
|
|
59
|
+
* document (`mentionsTagPrefix`, the fleet's own byte gate) → nothing opted
|
|
60
|
+
* in, no workspace. An untagged `views.likec4` is somebody's hand-drawn
|
|
61
|
+
* diagram, never graded, exactly as an untagged fleet view is — and its
|
|
62
|
+
* parse errors are the renderer's business until a view opts the project
|
|
63
|
+
* in;
|
|
64
|
+
* 3. otherwise `loadProject` over model + siblings, the way the renderer reads
|
|
65
|
+
* them. `own` and the project's views are never merged: when siblings
|
|
66
|
+
* exist the project is the whole answer, which is what keeps one view from
|
|
67
|
+
* being graded twice.
|
|
68
|
+
*
|
|
69
|
+
* A REQUEST RECORD rather than three parameters, for the reason
|
|
70
|
+
* docs/CODE-STYLE.md gives: the paths, the single-file doc and the fleet set
|
|
71
|
+
* are only ever passed together, and a caller handing in a doc loaded from
|
|
72
|
+
* some other path is exactly the inconsistent pair a record makes
|
|
73
|
+
* unrepresentable.
|
|
74
|
+
*/
|
|
75
|
+
import { relative } from "node:path";
|
|
76
|
+
import { architectureDocuments } from "../../c4/project/documents.js";
|
|
77
|
+
import { asLoadedDoc, loadProject } from "../../c4/project/load.js";
|
|
78
|
+
import { serviceFlowDocuments } from "../../c4/service-model/documents.js";
|
|
79
|
+
import { ARTIFACT_FILES } from "../../repo/paths.js";
|
|
80
|
+
import { isUseCase, mentionsTagPrefix } from "../fleet.js";
|
|
81
|
+
import { keepIncluded, serviceProjectExclude } from "./exclude.js";
|
|
82
|
+
/**
|
|
83
|
+
* The service's declared use cases, with the model they are drawn over.
|
|
84
|
+
*
|
|
85
|
+
* Reads only, throws nothing: every failure is the `unreadable` arm. The caller
|
|
86
|
+
* has already established that `model.likec4` parses on its own — a project
|
|
87
|
+
* whose model is broken is `c4.invalid`'s business, and loading it again here
|
|
88
|
+
* would only repeat that cascade under a second code.
|
|
89
|
+
*/
|
|
90
|
+
export async function readServiceFlows(req) {
|
|
91
|
+
const found = await architectureDocuments(req.paths.dir, [req.paths.model]);
|
|
92
|
+
// What the RENDERER holds, for the standalone shape where this directory's
|
|
93
|
+
// own `likec4.config.json` IS the project config: a sibling its `exclude`
|
|
94
|
+
// covers is in no project loam may claim to be reading (`./exclude.ts`). The
|
|
95
|
+
// extending shape reads the root project instead, so its list does not apply
|
|
96
|
+
// — a per-service config beside an extending model is `service.likec4-config-stray`.
|
|
97
|
+
const siblings = req.extending === undefined
|
|
98
|
+
? keepIncluded(req.paths.dir, await serviceProjectExclude(req.paths.dir), found)
|
|
99
|
+
: found;
|
|
100
|
+
// The scope of the no-sibling arm, and it is the PROJECT for both shapes: the
|
|
101
|
+
// single-file load for a standalone model (which is its whole project), and
|
|
102
|
+
// the map-plus-model load for an extending one. Reading `req.model` there for
|
|
103
|
+
// an extending model read its own slice instead, so one view was graded two
|
|
104
|
+
// different ways depending on which file it was written in.
|
|
105
|
+
const alone = req.extending?.project ?? req.model;
|
|
106
|
+
const scope = { elements: alone.elements, relationships: alone.relationships, known: req.known };
|
|
107
|
+
const own = (req.model.views ?? []).filter(isUseCase);
|
|
108
|
+
if (siblings.length === 0) {
|
|
109
|
+
if (own.length === 0)
|
|
110
|
+
return { kind: "none" };
|
|
111
|
+
return { kind: "read", views: own.map((view) => ({ ...view, sourcePath: ARTIFACT_FILES.model })), model: scope };
|
|
112
|
+
}
|
|
113
|
+
if (own.length === 0 && !(await mentionsTagPrefix(siblings)))
|
|
114
|
+
return { kind: "none" };
|
|
115
|
+
const base = req.extending?.docsDir ?? req.paths.dir;
|
|
116
|
+
const documents = req.extending === undefined
|
|
117
|
+
? [req.paths.model, ...siblings]
|
|
118
|
+
: await serviceFlowDocuments(req.extending.docsDir, req.paths);
|
|
119
|
+
const project = await loadProject(base, documents);
|
|
120
|
+
if (!project.clean) {
|
|
121
|
+
// Each error already carries the real path of its document (`loadProject`
|
|
122
|
+
// rewrites the staged path back), and the flattening is `asLoadedDoc`'s —
|
|
123
|
+
// the fleet's own path-sorted order, taken from the one place it is
|
|
124
|
+
// spelled rather than re-spelled here: the caller names the FIRST broken
|
|
125
|
+
// file, and a second sort rule would let the service target name a
|
|
126
|
+
// different first file than the fleet target names for the same project.
|
|
127
|
+
return { kind: "unreadable", errors: asLoadedDoc(project).errors };
|
|
128
|
+
}
|
|
129
|
+
// The prefix the service's own documents are spelled under inside this
|
|
130
|
+
// project: empty for a standalone model (the project IS the directory, so
|
|
131
|
+
// every view in it is this service's) and `services/<tree>/` for an extending
|
|
132
|
+
// one, whose project also holds the whole fleet map.
|
|
133
|
+
const prefix = req.extending === undefined ? "" : `${relative(base, req.paths.dir).split(/[\\/]/).join("/")}/`;
|
|
134
|
+
return {
|
|
135
|
+
kind: "read",
|
|
136
|
+
views: rebase(project.views.filter(isUseCase), prefix),
|
|
137
|
+
model: { elements: project.elements, relationships: project.relationships, known: req.known },
|
|
138
|
+
};
|
|
139
|
+
}
|
|
140
|
+
/**
|
|
141
|
+
* The views this SERVICE declares, spelled relative to its own directory.
|
|
142
|
+
*
|
|
143
|
+
* With an empty prefix — the standalone shape — this is the identity, including
|
|
144
|
+
* for a view whose `sourcePath` is absent: that project holds nothing but the
|
|
145
|
+
* service's own files, so there is nothing to attribute and nothing to strip.
|
|
146
|
+
*
|
|
147
|
+
* With a prefix, it is both the filter and the rebase, and both halves are one
|
|
148
|
+
* rule about which document belongs to whom. The project also holds the fleet
|
|
149
|
+
* map, so `architecture/usecases/checkout.likec4` is in it — and without the
|
|
150
|
+
* filter every service in the fleet would be graded for every fleet-level use
|
|
151
|
+
* case. A view carrying no `sourcePath` cannot be attributed at all and is
|
|
152
|
+
* dropped for the same reason: the path is the only evidence there is.
|
|
153
|
+
*/
|
|
154
|
+
function rebase(views, prefix) {
|
|
155
|
+
if (prefix === "")
|
|
156
|
+
return [...views];
|
|
157
|
+
return views
|
|
158
|
+
.filter((view) => view.sourcePath?.startsWith(prefix) === true)
|
|
159
|
+
.map((view) => ({ ...view, sourcePath: (view.sourcePath ?? "").slice(prefix.length) }));
|
|
160
|
+
}
|
|
161
|
+
//# sourceMappingURL=flows.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"flows.js","sourceRoot":"","sources":["../../../../src/core/usecases/service/flows.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAyEG;AACH,OAAO,EAAE,QAAQ,EAAE,MAAM,WAAW,CAAC;AAGrC,OAAO,EAAE,qBAAqB,EAAE,MAAM,+BAA+B,CAAC;AACtE,OAAO,EAAE,WAAW,EAAE,WAAW,EAAE,MAAM,0BAA0B,CAAC;AACpE,OAAO,EAAE,oBAAoB,EAAE,MAAM,qCAAqC,CAAC;AAG3E,OAAO,EAAE,cAAc,EAAqB,MAAM,qBAAqB,CAAC;AACxE,OAAO,EAAE,SAAS,EAAE,iBAAiB,EAAE,MAAM,aAAa,CAAC;AAC3D,OAAO,EAAE,YAAY,EAAE,qBAAqB,EAAE,MAAM,cAAc,CAAC;AAuEnE;;;;;;;GAOG;AACH,MAAM,CAAC,KAAK,UAAU,gBAAgB,CAAC,GAAuB;IAC5D,MAAM,KAAK,GAAG,MAAM,qBAAqB,CAAC,GAAG,CAAC,KAAK,CAAC,GAAG,EAAE,CAAC,GAAG,CAAC,KAAK,CAAC,KAAK,CAAC,CAAC,CAAC;IAC5E,2EAA2E;IAC3E,0EAA0E;IAC1E,6EAA6E;IAC7E,6EAA6E;IAC7E,qFAAqF;IACrF,MAAM,QAAQ,GACZ,GAAG,CAAC,SAAS,KAAK,SAAS;QACzB,CAAC,CAAC,YAAY,CAAC,GAAG,CAAC,KAAK,CAAC,GAAG,EAAE,MAAM,qBAAqB,CAAC,GAAG,CAAC,KAAK,CAAC,GAAG,CAAC,EAAE,KAAK,CAAC;QAChF,CAAC,CAAC,KAAK,CAAC;IACZ,8EAA8E;IAC9E,4EAA4E;IAC5E,8EAA8E;IAC9E,4EAA4E;IAC5E,4DAA4D;IAC5D,MAAM,KAAK,GAAG,GAAG,CAAC,SAAS,EAAE,OAAO,IAAI,GAAG,CAAC,KAAK,CAAC;IAClD,MAAM,KAAK,GAAG,EAAE,QAAQ,EAAE,KAAK,CAAC,QAAQ,EAAE,aAAa,EAAE,KAAK,CAAC,aAAa,EAAE,KAAK,EAAE,GAAG,CAAC,KAAK,EAAE,CAAC;IACjG,MAAM,GAAG,GAAG,CAAC,GAAG,CAAC,KAAK,CAAC,KAAK,IAAI,EAAE,CAAC,CAAC,MAAM,CAAC,SAAS,CAAC,CAAC;IACtD,IAAI,QAAQ,CAAC,MAAM,KAAK,CAAC,EAAE,CAAC;QAC1B,IAAI,GAAG,CAAC,MAAM,KAAK,CAAC;YAAE,OAAO,EAAE,IAAI,EAAE,MAAM,EAAE,CAAC;QAC9C,OAAO,EAAE,IAAI,EAAE,MAAM,EAAE,KAAK,EAAE,GAAG,CAAC,GAAG,CAAC,CAAC,IAAI,EAAE,EAAE,CAAC,CAAC,EAAE,GAAG,IAAI,EAAE,UAAU,EAAE,cAAc,CAAC,KAAK,EAAE,CAAC,CAAC,EAAE,KAAK,EAAE,KAAK,EAAE,CAAC;IACnH,CAAC;IACD,IAAI,GAAG,CAAC,MAAM,KAAK,CAAC,IAAI,CAAC,CAAC,MAAM,iBAAiB,CAAC,QAAQ,CAAC,CAAC;QAAE,OAAO,EAAE,IAAI,EAAE,MAAM,EAAE,CAAC;IAEtF,MAAM,IAAI,GAAG,GAAG,CAAC,SAAS,EAAE,OAAO,IAAI,GAAG,CAAC,KAAK,CAAC,GAAG,CAAC;IACrD,MAAM,SAAS,GACb,GAAG,CAAC,SAAS,KAAK,SAAS;QACzB,CAAC,CAAC,CAAC,GAAG,CAAC,KAAK,CAAC,KAAK,EAAE,GAAG,QAAQ,CAAC;QAChC,CAAC,CAAC,MAAM,oBAAoB,CAAC,GAAG,CAAC,SAAS,CAAC,OAAO,EAAE,GAAG,CAAC,KAAK,CAAC,CAAC;IACnE,MAAM,OAAO,GAAG,MAAM,WAAW,CAAC,IAAI,EAAE,SAAS,CAAC,CAAC;IACnD,IAAI,CAAC,OAAO,CAAC,KAAK,EAAE,CAAC;QACnB,0EAA0E;QAC1E,0EAA0E;QAC1E,oEAAoE;QACpE,yEAAyE;QACzE,mEAAmE;QACnE,yEAAyE;QACzE,OAAO,EAAE,IAAI,EAAE,YAAY,EAAE,MAAM,EAAE,WAAW,CAAC,OAAO,CAAC,CAAC,MAAM,EAAE,CAAC;IACrE,CAAC;IACD,uEAAuE;IACvE,0EAA0E;IAC1E,8EAA8E;IAC9E,qDAAqD;IACrD,MAAM,MAAM,GAAG,GAAG,CAAC,SAAS,KAAK,SAAS,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,GAAG,QAAQ,CAAC,IAAI,EAAE,GAAG,CAAC,KAAK,CAAC,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,CAAC,CAAC,IAAI,CAAC,GAAG,CAAC,GAAG,CAAC;IAC/G,OAAO;QACL,IAAI,EAAE,MAAM;QACZ,KAAK,EAAE,MAAM,CAAC,OAAO,CAAC,KAAK,CAAC,MAAM,CAAC,SAAS,CAAC,EAAE,MAAM,CAAC;QACtD,KAAK,EAAE,EAAE,QAAQ,EAAE,OAAO,CAAC,QAAQ,EAAE,aAAa,EAAE,OAAO,CAAC,aAAa,EAAE,KAAK,EAAE,GAAG,CAAC,KAAK,EAAE;KAC9F,CAAC;AACJ,CAAC;AAED;;;;;;;;;;;;;GAaG;AACH,SAAS,MAAM,CAAC,KAA4B,EAAE,MAAc;IAC1D,IAAI,MAAM,KAAK,EAAE;QAAE,OAAO,CAAC,GAAG,KAAK,CAAC,CAAC;IACrC,OAAO,KAAK;SACT,MAAM,CAAC,CAAC,IAAI,EAAE,EAAE,CAAC,IAAI,CAAC,UAAU,EAAE,UAAU,CAAC,MAAM,CAAC,KAAK,IAAI,CAAC;SAC9D,GAAG,CAAC,CAAC,IAAI,EAAE,EAAE,CAAC,CAAC,EAAE,GAAG,IAAI,EAAE,UAAU,EAAE,CAAC,IAAI,CAAC,UAAU,IAAI,EAAE,CAAC,CAAC,KAAK,CAAC,MAAM,CAAC,MAAM,CAAC,EAAE,CAAC,CAAC,CAAC;AAC5F,CAAC","sourcesContent":["/**\n * A service's OWN use cases: the `dynamic view`s its LikeC4 project declares,\n * read the way the renderer reads that project.\n *\n * THE SLOT IS THE PROJECT. `loam subsystem sync` writes a\n * `services/<…>/<svc>/likec4.config.json` that registers every `.likec4` under\n * the service directory as one LikeC4 project, so the renderer already shows a\n * `views.likec4` beside the model, or a `usecases/<name>.likec4` under it. loam\n * reads exactly that set — `model.likec4` plus every sibling, recursively, minus\n * whatever that project file's own `exclude` drops (`./exclude.ts`) — and\n * nothing else, because a slot narrower than the renderer's would leave a flow\n * that renders and is graded by nothing, which is the report this module\n * answers: an intra-service flow behind an `arch.spec.md` requirement whose\n * `Covers:` names containers had no home. `architecture/usecases/` is a\n * separate project that cannot resolve a container (one such file turned a\n * whole fleet `landscape.invalid`), and the author's workaround beside the\n * model was invisible to every check. The read is NOT gated on the project\n * file's presence: that file is create-only and a team may delete it, its\n * absence has a warning of its own (`service.likec4-config-missing`), and a\n * flow the renderer cannot show yet is still a flow loam can grade.\n *\n * MODEL FACTS STAY SINGLE-FILE. `c4.valid`'s counts, `Covers:` resolution,\n * `health.dependency-unmodelled` and every other model reader keep the\n * single-file load the service target already made; the project is loaded here\n * for the VIEWS and their `sourcePath` only, and only when there is something\n * to grade. A sibling that declares elements extends what a hop may name, and\n * that is the renderer's reading too.\n *\n * TWO SHAPES, ONE SLOT. A model that stands alone is a project of its own\n * directory, exactly as above. A model that EXTENDS the fleet map is not: its\n * containers are declared inside an element the map owns, so a flow beside it\n * naming one of those containers only resolves in a project that also holds the\n * map (`c4/service-model/documents.ts`, `serviceFlowDocuments`). That project is\n * staged from the DOCS ROOT, so every view comes back spelled\n * `services/<tree>/usecases/x.likec4` — and the service prefix is stripped here,\n * because every finding this scan feeds names a file relative to the service\n * directory and a reader must not have to work out which spelling they were\n * given. The same prefix is the FILTER: the map's own `architecture/usecases/`\n * flows are in that project too, and without it every service in the fleet would\n * be graded for every fleet-level use case.\n *\n * THE CHEAP GATE, in order, so a fleet without the axis pays one recursive\n * readdir per service and nothing else:\n *\n * 1. no sibling → nothing new is loaded, and the model's own tagged views are\n * graded at zero cost against the project the model was READ in. For a\n * standalone model that is the single-file load already in hand, which IS\n * its whole project; for an extending one it is `ServiceModel.project`, the\n * map and this model together — NOT the service's own slice of it, which is\n * what the caller passes as `model` and what every count and `Covers:` line\n * is answered from. A hop backed by a map edge is backed wherever the view\n * is written, and grading the two arms against different documents made one\n * view `usecase.step-unbacked` in model.likec4 and clean in a sibling. A\n * standalone model's views came back named `source.c4`\n * (`core/usecases/place.ts` records the rule) and an extending model's carry\n * their docs-relative path, so the file is spelled here, where it is known,\n * for both: `model.likec4`;\n * 2. siblings, no tagged view in the model, and no reserved prefix in any\n * document (`mentionsTagPrefix`, the fleet's own byte gate) → nothing opted\n * in, no workspace. An untagged `views.likec4` is somebody's hand-drawn\n * diagram, never graded, exactly as an untagged fleet view is — and its\n * parse errors are the renderer's business until a view opts the project\n * in;\n * 3. otherwise `loadProject` over model + siblings, the way the renderer reads\n * them. `own` and the project's views are never merged: when siblings\n * exist the project is the whole answer, which is what keeps one view from\n * being graded twice.\n *\n * A REQUEST RECORD rather than three parameters, for the reason\n * docs/CODE-STYLE.md gives: the paths, the single-file doc and the fleet set\n * are only ever passed together, and a caller handing in a doc loaded from\n * some other path is exactly the inconsistent pair a record makes\n * unrepresentable.\n */\nimport { relative } from \"node:path\";\nimport type { LikeC4Error, LoadedDoc } from \"../../c4/likec4.js\";\nimport type { ParsedView } from \"../../c4/parsed/dynamic-views.js\";\nimport { architectureDocuments } from \"../../c4/project/documents.js\";\nimport { asLoadedDoc, loadProject } from \"../../c4/project/load.js\";\nimport { serviceFlowDocuments } from \"../../c4/service-model/documents.js\";\nimport type { StepScope } from \"../../c4/resolve/steps.js\";\nimport type { DocsDir } from \"../../kernel/ids/dirs.js\";\nimport { ARTIFACT_FILES, type ServicePaths } from \"../../repo/paths.js\";\nimport { isUseCase, mentionsTagPrefix } from \"../fleet.js\";\nimport { keepIncluded, serviceProjectExclude } from \"./exclude.js\";\n\nexport interface ServiceFlowRequest {\n paths: ServicePaths;\n /**\n * The single-file load the service target already made and has verified\n * parses. Its views are the project when no sibling exists; its elements are\n * the model a no-sibling flow is graded over.\n */\n model: LoadedDoc;\n /** The enumerated fleet, for `serviceResolver`'s reason: without it a container resolves to a service that has never existed. */\n known: ReadonlySet<string>;\n /**\n * Set only when this model EXTENDS the fleet map, and then both halves of it\n * — a record rather than two optional fields, for docs/CODE-STYLE.md's reason:\n * they are only ever passed together, and a caller that supplied one without\n * the other is exactly the inconsistent pair a record makes unrepresentable.\n *\n * `docsDir` decides which documents the sibling project holds (the map as\n * well, or the directory alone) and which root they are staged from, and\n * therefore how LikeC4 spells every `sourcePath` that comes back.\n *\n * `project` is the per-service project the model was READ in — the map plus\n * this model (`ServiceModel.project`) — and it is the scope a hop resolves\n * against when there is no sibling file. `model` above is then the service's\n * own SLICE of that project, which is the right answer for every count and\n * every `Covers:` line and the wrong one here: a hop backed by a MAP edge is\n * backed, and grading it against the slice reported `usecase.step-unbacked`\n * (exit 1) for a view in model.likec4 that was clean the moment the same view\n * moved to a sibling file, where the loaded project holds the map. One\n * question, two answers, decided by which file the author happened to write\n * the view in.\n *\n * Absent means the standalone shape — the service directory is the project and\n * the single-file load IS that project, exactly as it has always been.\n */\n extending?: { docsDir: DocsDir; project: LoadedDoc };\n}\n\n/**\n * What a service's flows are, or the honest refusal to say.\n *\n * Three variants rather than a record with flags, for `UseCaseScan`'s reason\n * one package up: a project that did not parse has no elements to resolve\n * against and no views to grade, and a caller handed `{ views: [], unreadable:\n * true }` is one `.length` check away from reporting a service as flow-free\n * when nobody could read it. `none` is the gate's answer — nothing opted in,\n * nothing was loaded — and it is distinct from `read` with no views (a project\n * that mentions a prefix in prose and declares no tagged view), because only\n * the second one cost a workspace.\n */\nexport type ServiceFlowScan =\n | { kind: \"none\" }\n | {\n kind: \"read\";\n /** The reserved-tag views ONLY, each carrying a `sourcePath` relative to the SERVICE directory. */\n views: ParsedView[];\n /** What a hop is attributed against — `known` required, as `UseCaseScan.model` requires it. */\n model: StepScope & { known: ReadonlySet<string> };\n }\n | {\n kind: \"unreadable\";\n /**\n * Every error the project raised, each carrying the real absolute path of\n * the document that raised it, sorted by that path — so a caller can name\n * the file, and a project with two broken documents reports them in a\n * stable order.\n */\n errors: LikeC4Error[];\n };\n\n/**\n * The service's declared use cases, with the model they are drawn over.\n *\n * Reads only, throws nothing: every failure is the `unreadable` arm. The caller\n * has already established that `model.likec4` parses on its own — a project\n * whose model is broken is `c4.invalid`'s business, and loading it again here\n * would only repeat that cascade under a second code.\n */\nexport async function readServiceFlows(req: ServiceFlowRequest): Promise<ServiceFlowScan> {\n const found = await architectureDocuments(req.paths.dir, [req.paths.model]);\n // What the RENDERER holds, for the standalone shape where this directory's\n // own `likec4.config.json` IS the project config: a sibling its `exclude`\n // covers is in no project loam may claim to be reading (`./exclude.ts`). The\n // extending shape reads the root project instead, so its list does not apply\n // — a per-service config beside an extending model is `service.likec4-config-stray`.\n const siblings =\n req.extending === undefined\n ? keepIncluded(req.paths.dir, await serviceProjectExclude(req.paths.dir), found)\n : found;\n // The scope of the no-sibling arm, and it is the PROJECT for both shapes: the\n // single-file load for a standalone model (which is its whole project), and\n // the map-plus-model load for an extending one. Reading `req.model` there for\n // an extending model read its own slice instead, so one view was graded two\n // different ways depending on which file it was written in.\n const alone = req.extending?.project ?? req.model;\n const scope = { elements: alone.elements, relationships: alone.relationships, known: req.known };\n const own = (req.model.views ?? []).filter(isUseCase);\n if (siblings.length === 0) {\n if (own.length === 0) return { kind: \"none\" };\n return { kind: \"read\", views: own.map((view) => ({ ...view, sourcePath: ARTIFACT_FILES.model })), model: scope };\n }\n if (own.length === 0 && !(await mentionsTagPrefix(siblings))) return { kind: \"none\" };\n\n const base = req.extending?.docsDir ?? req.paths.dir;\n const documents =\n req.extending === undefined\n ? [req.paths.model, ...siblings]\n : await serviceFlowDocuments(req.extending.docsDir, req.paths);\n const project = await loadProject(base, documents);\n if (!project.clean) {\n // Each error already carries the real path of its document (`loadProject`\n // rewrites the staged path back), and the flattening is `asLoadedDoc`'s —\n // the fleet's own path-sorted order, taken from the one place it is\n // spelled rather than re-spelled here: the caller names the FIRST broken\n // file, and a second sort rule would let the service target name a\n // different first file than the fleet target names for the same project.\n return { kind: \"unreadable\", errors: asLoadedDoc(project).errors };\n }\n // The prefix the service's own documents are spelled under inside this\n // project: empty for a standalone model (the project IS the directory, so\n // every view in it is this service's) and `services/<tree>/` for an extending\n // one, whose project also holds the whole fleet map.\n const prefix = req.extending === undefined ? \"\" : `${relative(base, req.paths.dir).split(/[\\\\/]/).join(\"/\")}/`;\n return {\n kind: \"read\",\n views: rebase(project.views.filter(isUseCase), prefix),\n model: { elements: project.elements, relationships: project.relationships, known: req.known },\n };\n}\n\n/**\n * The views this SERVICE declares, spelled relative to its own directory.\n *\n * With an empty prefix — the standalone shape — this is the identity, including\n * for a view whose `sourcePath` is absent: that project holds nothing but the\n * service's own files, so there is nothing to attribute and nothing to strip.\n *\n * With a prefix, it is both the filter and the rebase, and both halves are one\n * rule about which document belongs to whom. The project also holds the fleet\n * map, so `architecture/usecases/checkout.likec4` is in it — and without the\n * filter every service in the fleet would be graded for every fleet-level use\n * case. A view carrying no `sourcePath` cannot be attributed at all and is\n * dropped for the same reason: the path is the only evidence there is.\n */\nfunction rebase(views: readonly ParsedView[], prefix: string): ParsedView[] {\n if (prefix === \"\") return [...views];\n return views\n .filter((view) => view.sourcePath?.startsWith(prefix) === true)\n .map((view) => ({ ...view, sourcePath: (view.sourcePath ?? \"\").slice(prefix.length) }));\n}\n"]}
|
|
@@ -129,6 +129,16 @@ export function landscapeEvidence(req) {
|
|
|
129
129
|
const inbound = [];
|
|
130
130
|
const outbound = [];
|
|
131
131
|
for (const r of relationships) {
|
|
132
|
+
// An edge whose BOTH endpoints resolve to this service is its own internal
|
|
133
|
+
// wiring (`svc.web -> svc.db`): filed as inbound it made the service its own
|
|
134
|
+
// caller — `← <id> "reads"` in `loam context` (verification 2026-09-04) —
|
|
135
|
+
// and, through `apiExpected` below, told a service nobody calls that it owed
|
|
136
|
+
// an OpenAPI contract. `core/brief/landscape.ts` drops it for the same
|
|
137
|
+
// reason. It still TOUCHES the service, which is why the brief's `touched`
|
|
138
|
+
// and `landscape.service-isolated` read the relationships rather than these
|
|
139
|
+
// two lists.
|
|
140
|
+
if (svcOf(r.source) === id && svcOf(r.target) === id)
|
|
141
|
+
continue;
|
|
132
142
|
const edge = { op: r.op ?? null, title: r.title ?? null };
|
|
133
143
|
if (svcOf(r.target) === id)
|
|
134
144
|
inbound.push({ service: svcOf(r.source), ...edge });
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"maturity.js","sourceRoot":"","sources":["../../../src/core/vocabulary/maturity.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * The adoption-maturity ladder: one monotone word for how far a service's\n * documentation has got, and what stands between it and the next rung.\n *\n * This lived inside `commands/list.ts` while `list` was the only caller. It\n * moved here when `explore` needed the same answer, because the alternative is\n * the failure `core/kernel/ids/service.ts` already records for the service-id grammar: a\n * second, subtly different copy, so two commands grade the same directory\n * differently and nobody can tell which one is lying. The rung is a fleet dial\n * — `list` prints it per service and in a rollup, `explore` prints it for a\n * service somebody is about to build against — and a dial with two readings is\n * not a dial.\n */\nimport type { ServiceEntry } from \"../repo/entries.js\";\n\n/** The ladder, in order. Each rung stands on every rung below it. */\nexport const MATURITY_LADDER = [\"empty\", \"partial\", \"documented\", \"sourced\", \"vouched\"] as const;\nexport type Maturity = (typeof MATURITY_LADDER)[number];\n\n/**\n * A service entry plus the two facts the directory enumeration cannot know on\n * its own. Both change what a rung MEANS, and both are fleet-level: they need\n * the landscape, not `services/<id>/`.\n */\nexport interface MaturityInput {\n entry: ServiceEntry;\n /** `arch.spec.md` — the architecture-obligations axis, absent from ServiceEntry.has. */\n archSpec: boolean;\n /**\n * Does anything in the fleet call an operation on this service? True unless\n * the landscape PROVES otherwise, exactly as `validate`'s no-openapi grace\n * reads it: a missing or unparseable landscape proves nothing.\n */\n apiExpected: boolean;\n}\n\n/**\n * How far a service's documentation has got. Derived from artifact PRESENCE and\n * provenance state only — the data an enumeration already holds — never from\n * what the artifacts say. COMPLETENESS of adopted docs is explicitly on the\n * unchecked list (brief/unchecked.ts): a service with one endpoint documented out of\n * thirty climbs this ladder exactly as fast as a thorough one, which is why no\n * rung is called \"adopted\".\n *\n * empty services/<id>/ exists, no artifact is in it\n * partial some artifacts, but not the required set\n * documented the artifacts the adopt brief marks required are present\n * sourced the living spec declares `sources` — something ties it to code\n * vouched status: verified with a sources_digest behind it — a person\n * stamped it. `verified` with no digest is a claim with nothing\n * behind it and stays below this rung.\n *\n * `openapi.yaml` is required ONLY where an API is expected. The rung used to\n * demand it of everything, which permanently pinned every worker, cron and\n * consumer in the fleet at `partial` — services that are fully documented and\n * vouched for, reported as unfinished forever, leaving the rollup permanently\n * below its true documentation state. The evidence is the same one\n * `validate` uses to keep `service.no-openapi` quiet, so a service cannot be\n * green in one command and unfinished in the other.\n */\nexport function serviceMaturity(v: MaturityInput): Maturity {\n const s = v.entry;\n if (!Object.values(s.has).some(Boolean) && !v.archSpec && s.adrs === 0) return \"empty\";\n if (!(s.has.model && s.has.spec && (!v.apiExpected || s.has.openapi))) return \"partial\";\n if (!s.sources.declared) return \"documented\";\n if (!(s.status === \"verified\" && s.sources.stamped)) return \"sourced\";\n return \"vouched\";\n}\n\n/**\n * What stands between this service and `vouched`, named as the thing to go and\n * do. One rung's worth at a time: telling somebody at `empty` that they also\n * need a vouch is noise, and the vouch is not even possible yet.\n */\nexport function maturityGaps(v: MaturityInput): string[] {\n const s = v.entry;\n const files = [\n ...(s.has.model ? [] : [\"model.likec4\"]),\n ...(s.has.spec ? [] : [\"spec.md\"]),\n ...(v.apiExpected && !s.has.openapi ? [\"openapi.yaml\"] : []),\n ];\n if (files.length > 0) return files;\n if (!s.sources.declared) return [\"sources: in the spec.md frontmatter\"];\n if (!(s.status === \"verified\" && s.sources.stamped)) return [`\\`loam vouch --service ${s.id}\\``];\n return [];\n}\n\n/** Counts per rung, every rung present — a stable shape a fleet dashboard can diff. */\nexport function maturityRollup(graded: readonly { maturity: Maturity }[]): Record<Maturity, number> {\n const out = Object.fromEntries(MATURITY_LADDER.map((m) => [m, 0])) as Record<Maturity, number>;\n for (const v of graded) out[v.maturity] += 1;\n return out;\n}\n\n/* ------------------------------------------------------------------ */\n/* What the fleet map proves about one service */\n/* ------------------------------------------------------------------ */\n\n/**\n * C4 kinds that model people, and the tag marking an element as somebody\n * else's system. A person is never a service directory, and `#external` is\n * undocumented on purpose — every census and exemption over the fleet map\n * asks both questions.\n *\n * Extracted on the third strike: `commands/validate/checks/vocabulary.ts`,\n * `core/gate/partners.ts` and `core/verify/checklist.ts` each carried a copy.\n * Their real home is `core/c4/likec4.ts` beside the model they describe, but\n * that file sits against the 300-line limit and its package holds five files\n * — so the trio lives here, beside `landscapeEvidence`, which reads the same\n * map for the same per-service questions.\n *\n * `PLATFORM_TAG` joined them from `commands/validate/checks/fleet-shape.ts`,\n * where it was declared beside its one reader. It moved for the rule above the\n * others came for: `fleet/kind-tags.ts` needs to know which tag names loam\n * grades on, and reaching into fleet-shape.ts for a five-character constant\n * would import a module that reads permissions, capabilities and every\n * service's requirements to get it. The two tags loam gives meaning to belong\n * in one place anyway — that list IS what the kind-tag check is about.\n */\nexport const ACTOR_KINDS = new Set([\"person\", \"actor\", \"user\"]);\nexport const EXTERNAL_TAG = \"external\";\n/** Tag marking ubiquitous infrastructure; the scaffolded fleet view excludes it. */\nexport const PLATFORM_TAG = \"platform\";\n\n/**\n * The tags loam GRADES on, as opposed to the ones it merely carries.\n *\n * A tag in this list changes what loam concludes about a fleet: `#external`\n * exempts an element from the landscape↔services reconciliation entirely, and\n * `#platform` silences the hub warning. Every other tag on an element is the\n * author's own vocabulary and loam does not read it.\n *\n * The list exists because since LikeC4 1.59.0 a tag can be declared on a KIND\n * — `specification { element softwareSystem { #external } }` — and LikeC4\n * applies it to every element of that kind. A kind-wide declaration of a tag on\n * this list switches loam's grading off for the whole fleet at once, which is\n * what `kindTagFindings` refuses. A tag NOT on this list is safe to declare\n * kind-wide, so the check must ask exactly this question and no broader one.\n */\nexport const GRADED_TAGS: readonly string[] = [EXTERNAL_TAG, PLATFORM_TAG];\n\n/** One end of a call the fleet map draws, as every per-service view spells it. */\nexport interface LandscapeEdge {\n service: string;\n op: string | null;\n title: string | null;\n}\n\n/**\n * The slice of a parsed relationship the partition reads. Structural on\n * purpose — the smallest parameter type the derivation needs — so this module\n * stays a leaf: `core/c4/likec4.ts`'s `Rel` satisfies it without an import\n * edge from the vocabulary package into the parser.\n */\nexport interface EdgeSource {\n source: string;\n target: string;\n op?: string;\n title?: string;\n}\n\nexport interface LandscapeEvidenceRequest {\n /** The service being asked about. */\n id: string;\n /** Whether the landscape parsed — what makes its silence evidence rather than absence. */\n parses: boolean;\n /** The parsed relationships; empty when the landscape is absent or unparseable. */\n relationships: readonly EdgeSource[];\n /** Every element id in the map, for the `modelled` probe. */\n elementIds: readonly string[];\n /** The shared resolver (`core/c4/resolve/service.ts` `serviceResolver`), widened to plain strings. */\n svcOf: (id: string) => string;\n}\n\nexport interface LandscapeEvidence {\n inbound: LandscapeEdge[];\n outbound: LandscapeEdge[];\n /** Whether anything in the fleet map resolves to this service. */\n modelled: boolean;\n /** The `apiExpected` input the ladder above consumes. */\n apiExpected: boolean;\n}\n\n/**\n * The fleet map's evidence about one service: the edges into and out of it,\n * whether the map draws it at all, and whether an API is expected of it.\n *\n * One spelling, because it had grown three. `explore`'s describe and the\n * context pack's living slice partitioned the same relationships with the\n * same positive-evidence rule line for line — an inbound edge carrying an\n * operation proves somebody calls the service; a landscape that is absent or\n * does not parse proves NOTHING about who calls it, so the contract is still\n * owed — and a rule spelled twice is how the same service grades `partial` in\n * one command and `documented` in another. `list`'s serviceViews\n * (`commands/list/views.ts`) derives the same rule fleet-wide in ONE pass —\n * a single `called` set for every service, not a partition per service —\n * so that spelling deliberately stays there, cross-referenced from both\n * sides: calling this per service inside a fleet loop would re-walk the\n * whole relationship list once per service.\n */\nexport function landscapeEvidence(req: LandscapeEvidenceRequest): LandscapeEvidence {\n const { id, parses, relationships, svcOf } = req;\n const inbound: LandscapeEdge[] = [];\n const outbound: LandscapeEdge[] = [];\n for (const r of relationships) {\n const edge = { op: r.op ?? null, title: r.title ?? null };\n if (svcOf(r.target) === id) inbound.push({ service: svcOf(r.source), ...edge });\n else if (svcOf(r.source) === id) outbound.push({ service: svcOf(r.target), ...edge });\n }\n return {\n inbound,\n outbound,\n modelled: req.elementIds.some((e) => svcOf(e) === id),\n apiExpected: !parses || inbound.some((e) => e.op !== null),\n };\n}\n"]}
|
|
1
|
+
{"version":3,"file":"maturity.js","sourceRoot":"","sources":["../../../src/core/vocabulary/maturity.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * The adoption-maturity ladder: one monotone word for how far a service's\n * documentation has got, and what stands between it and the next rung.\n *\n * This lived inside `commands/list.ts` while `list` was the only caller. It\n * moved here when `explore` needed the same answer, because the alternative is\n * the failure `core/kernel/ids/service.ts` already records for the service-id grammar: a\n * second, subtly different copy, so two commands grade the same directory\n * differently and nobody can tell which one is lying. The rung is a fleet dial\n * — `list` prints it per service and in a rollup, `explore` prints it for a\n * service somebody is about to build against — and a dial with two readings is\n * not a dial.\n */\nimport type { ServiceEntry } from \"../repo/entries.js\";\n\n/** The ladder, in order. Each rung stands on every rung below it. */\nexport const MATURITY_LADDER = [\"empty\", \"partial\", \"documented\", \"sourced\", \"vouched\"] as const;\nexport type Maturity = (typeof MATURITY_LADDER)[number];\n\n/**\n * A service entry plus the two facts the directory enumeration cannot know on\n * its own. Both change what a rung MEANS, and both are fleet-level: they need\n * the landscape, not `services/<id>/`.\n */\nexport interface MaturityInput {\n entry: ServiceEntry;\n /** `arch.spec.md` — the architecture-obligations axis, absent from ServiceEntry.has. */\n archSpec: boolean;\n /**\n * Does anything in the fleet call an operation on this service? True unless\n * the landscape PROVES otherwise, exactly as `validate`'s no-openapi grace\n * reads it: a missing or unparseable landscape proves nothing.\n */\n apiExpected: boolean;\n}\n\n/**\n * How far a service's documentation has got. Derived from artifact PRESENCE and\n * provenance state only — the data an enumeration already holds — never from\n * what the artifacts say. COMPLETENESS of adopted docs is explicitly on the\n * unchecked list (brief/unchecked.ts): a service with one endpoint documented out of\n * thirty climbs this ladder exactly as fast as a thorough one, which is why no\n * rung is called \"adopted\".\n *\n * empty services/<id>/ exists, no artifact is in it\n * partial some artifacts, but not the required set\n * documented the artifacts the adopt brief marks required are present\n * sourced the living spec declares `sources` — something ties it to code\n * vouched status: verified with a sources_digest behind it — a person\n * stamped it. `verified` with no digest is a claim with nothing\n * behind it and stays below this rung.\n *\n * `openapi.yaml` is required ONLY where an API is expected. The rung used to\n * demand it of everything, which permanently pinned every worker, cron and\n * consumer in the fleet at `partial` — services that are fully documented and\n * vouched for, reported as unfinished forever, leaving the rollup permanently\n * below its true documentation state. The evidence is the same one\n * `validate` uses to keep `service.no-openapi` quiet, so a service cannot be\n * green in one command and unfinished in the other.\n */\nexport function serviceMaturity(v: MaturityInput): Maturity {\n const s = v.entry;\n if (!Object.values(s.has).some(Boolean) && !v.archSpec && s.adrs === 0) return \"empty\";\n if (!(s.has.model && s.has.spec && (!v.apiExpected || s.has.openapi))) return \"partial\";\n if (!s.sources.declared) return \"documented\";\n if (!(s.status === \"verified\" && s.sources.stamped)) return \"sourced\";\n return \"vouched\";\n}\n\n/**\n * What stands between this service and `vouched`, named as the thing to go and\n * do. One rung's worth at a time: telling somebody at `empty` that they also\n * need a vouch is noise, and the vouch is not even possible yet.\n */\nexport function maturityGaps(v: MaturityInput): string[] {\n const s = v.entry;\n const files = [\n ...(s.has.model ? [] : [\"model.likec4\"]),\n ...(s.has.spec ? [] : [\"spec.md\"]),\n ...(v.apiExpected && !s.has.openapi ? [\"openapi.yaml\"] : []),\n ];\n if (files.length > 0) return files;\n if (!s.sources.declared) return [\"sources: in the spec.md frontmatter\"];\n if (!(s.status === \"verified\" && s.sources.stamped)) return [`\\`loam vouch --service ${s.id}\\``];\n return [];\n}\n\n/** Counts per rung, every rung present — a stable shape a fleet dashboard can diff. */\nexport function maturityRollup(graded: readonly { maturity: Maturity }[]): Record<Maturity, number> {\n const out = Object.fromEntries(MATURITY_LADDER.map((m) => [m, 0])) as Record<Maturity, number>;\n for (const v of graded) out[v.maturity] += 1;\n return out;\n}\n\n/* ------------------------------------------------------------------ */\n/* What the fleet map proves about one service */\n/* ------------------------------------------------------------------ */\n\n/**\n * C4 kinds that model people, and the tag marking an element as somebody\n * else's system. A person is never a service directory, and `#external` is\n * undocumented on purpose — every census and exemption over the fleet map\n * asks both questions.\n *\n * Extracted on the third strike: `commands/validate/checks/vocabulary.ts`,\n * `core/gate/partners.ts` and `core/verify/checklist.ts` each carried a copy.\n * Their real home is `core/c4/likec4.ts` beside the model they describe, but\n * that file sits against the 300-line limit and its package holds five files\n * — so the trio lives here, beside `landscapeEvidence`, which reads the same\n * map for the same per-service questions.\n *\n * `PLATFORM_TAG` joined them from `commands/validate/checks/fleet-shape.ts`,\n * where it was declared beside its one reader. It moved for the rule above the\n * others came for: `fleet/kind-tags.ts` needs to know which tag names loam\n * grades on, and reaching into fleet-shape.ts for a five-character constant\n * would import a module that reads permissions, capabilities and every\n * service's requirements to get it. The two tags loam gives meaning to belong\n * in one place anyway — that list IS what the kind-tag check is about.\n */\nexport const ACTOR_KINDS = new Set([\"person\", \"actor\", \"user\"]);\nexport const EXTERNAL_TAG = \"external\";\n/** Tag marking ubiquitous infrastructure; the scaffolded fleet view excludes it. */\nexport const PLATFORM_TAG = \"platform\";\n\n/**\n * The tags loam GRADES on, as opposed to the ones it merely carries.\n *\n * A tag in this list changes what loam concludes about a fleet: `#external`\n * exempts an element from the landscape↔services reconciliation entirely, and\n * `#platform` silences the hub warning. Every other tag on an element is the\n * author's own vocabulary and loam does not read it.\n *\n * The list exists because since LikeC4 1.59.0 a tag can be declared on a KIND\n * — `specification { element softwareSystem { #external } }` — and LikeC4\n * applies it to every element of that kind. A kind-wide declaration of a tag on\n * this list switches loam's grading off for the whole fleet at once, which is\n * what `kindTagFindings` refuses. A tag NOT on this list is safe to declare\n * kind-wide, so the check must ask exactly this question and no broader one.\n */\nexport const GRADED_TAGS: readonly string[] = [EXTERNAL_TAG, PLATFORM_TAG];\n\n/** One end of a call the fleet map draws, as every per-service view spells it. */\nexport interface LandscapeEdge {\n service: string;\n op: string | null;\n title: string | null;\n}\n\n/**\n * The slice of a parsed relationship the partition reads. Structural on\n * purpose — the smallest parameter type the derivation needs — so this module\n * stays a leaf: `core/c4/likec4.ts`'s `Rel` satisfies it without an import\n * edge from the vocabulary package into the parser.\n */\nexport interface EdgeSource {\n source: string;\n target: string;\n op?: string;\n title?: string;\n}\n\nexport interface LandscapeEvidenceRequest {\n /** The service being asked about. */\n id: string;\n /** Whether the landscape parsed — what makes its silence evidence rather than absence. */\n parses: boolean;\n /** The parsed relationships; empty when the landscape is absent or unparseable. */\n relationships: readonly EdgeSource[];\n /** Every element id in the map, for the `modelled` probe. */\n elementIds: readonly string[];\n /** The shared resolver (`core/c4/resolve/service.ts` `serviceResolver`), widened to plain strings. */\n svcOf: (id: string) => string;\n}\n\nexport interface LandscapeEvidence {\n inbound: LandscapeEdge[];\n outbound: LandscapeEdge[];\n /** Whether anything in the fleet map resolves to this service. */\n modelled: boolean;\n /** The `apiExpected` input the ladder above consumes. */\n apiExpected: boolean;\n}\n\n/**\n * The fleet map's evidence about one service: the edges into and out of it,\n * whether the map draws it at all, and whether an API is expected of it.\n *\n * One spelling, because it had grown three. `explore`'s describe and the\n * context pack's living slice partitioned the same relationships with the\n * same positive-evidence rule line for line — an inbound edge carrying an\n * operation proves somebody calls the service; a landscape that is absent or\n * does not parse proves NOTHING about who calls it, so the contract is still\n * owed — and a rule spelled twice is how the same service grades `partial` in\n * one command and `documented` in another. `list`'s serviceViews\n * (`commands/list/views.ts`) derives the same rule fleet-wide in ONE pass —\n * a single `called` set for every service, not a partition per service —\n * so that spelling deliberately stays there, cross-referenced from both\n * sides: calling this per service inside a fleet loop would re-walk the\n * whole relationship list once per service.\n */\nexport function landscapeEvidence(req: LandscapeEvidenceRequest): LandscapeEvidence {\n const { id, parses, relationships, svcOf } = req;\n const inbound: LandscapeEdge[] = [];\n const outbound: LandscapeEdge[] = [];\n for (const r of relationships) {\n // An edge whose BOTH endpoints resolve to this service is its own internal\n // wiring (`svc.web -> svc.db`): filed as inbound it made the service its own\n // caller — `← <id> \"reads\"` in `loam context` (verification 2026-09-04) —\n // and, through `apiExpected` below, told a service nobody calls that it owed\n // an OpenAPI contract. `core/brief/landscape.ts` drops it for the same\n // reason. It still TOUCHES the service, which is why the brief's `touched`\n // and `landscape.service-isolated` read the relationships rather than these\n // two lists.\n if (svcOf(r.source) === id && svcOf(r.target) === id) continue;\n const edge = { op: r.op ?? null, title: r.title ?? null };\n if (svcOf(r.target) === id) inbound.push({ service: svcOf(r.source), ...edge });\n else if (svcOf(r.source) === id) outbound.push({ service: svcOf(r.target), ...edge });\n }\n return {\n inbound,\n outbound,\n modelled: req.elementIds.some((e) => svcOf(e) === id),\n apiExpected: !parses || inbound.some((e) => e.op !== null),\n };\n}\n"]}
|
package/examples/README.md
CHANGED
|
@@ -145,15 +145,33 @@ contracts and digests are byte-identical to their unfiled days, every command ad
|
|
|
145
145
|
id, and `architecture/subsystems.likec4` — GENERATED by `loam subsystem sync`, one view per
|
|
146
146
|
subsystem, one `include` per line — is the only file the grouping added. Edit the tree with
|
|
147
147
|
`loam subsystem move|rename|rm` and the views file travels in the same transaction; edit it by hand
|
|
148
|
-
and `validate --all` answers `subsystem.views-stale`.
|
|
149
|
-
`likec4.config.json` — written by `loam subsystem sync`, create-only, never graded for content —
|
|
150
|
-
because the root project excludes `services/**` and that file is what registers a service's
|
|
151
|
-
`model.likec4` as a LikeC4 project of its own: `npx likec4 validate examples/docs` names six
|
|
152
|
-
projects, `fleet` and the five services, and with more than one project it needs `--project fleet`
|
|
153
|
-
(or a service's name) to grade one. (`order-service` and `payment-service` stay
|
|
148
|
+
and `validate --all` answers `subsystem.views-stale`. (`order-service` and `payment-service` stay
|
|
154
149
|
unfiled deliberately: FEAT-088's committed version-2 snapshot restores them by literal path, and the
|
|
155
150
|
`loam unarchive FEAT-088` walkthrough must keep working byte-for-byte.)
|
|
156
151
|
|
|
152
|
+
**All five models are EXTENDING models, and that is the shape this example teaches.** None of them
|
|
153
|
+
declares a `specification` block, none re-declares a partner, and none has a `likec4.config.json`
|
|
154
|
+
beside it. Each one says what is inside its service with `extend marketplace.<svc> { … }` on the
|
|
155
|
+
element `architecture/landscape.likec4` already binds to its directory, so `kafka`, `stripe`,
|
|
156
|
+
`notification-provider` and every peer service are declared exactly once — in the map — and no two
|
|
157
|
+
documents can disagree about their tags. The root `likec4.config.json` therefore excludes only
|
|
158
|
+
`**/node_modules/**` and `features/**`: `npx likec4 validate examples/docs` names ONE project,
|
|
159
|
+
`fleet`, holding the map and all five models, and needs no `--project` flag. The other shape — a
|
|
160
|
+
model that declares its own `specification` and is parsed alone, which is what every model here was
|
|
161
|
+
before — is still legal and still supported; SCHEMA.md's "Two shapes of a service model" has both,
|
|
162
|
+
the eight grades in its table — seven that came with the pair, plus the one it narrowed
|
|
163
|
+
(`service.likec4-config-missing`, now standalone-only) — and the
|
|
164
|
+
ordered recipe for migrating between them.
|
|
165
|
+
|
|
166
|
+
Two consequences are visible in the tree and worth reading for. Element ids carry their full path,
|
|
167
|
+
so `services/order-service/arch.spec.md` writes `Covers: marketplace.orderService.db` — the
|
|
168
|
+
container has no shorter name, because the model extends an element nested in the `marketplace`
|
|
169
|
+
group. And a cross-boundary call is written **twice** on purpose: container-level in the model
|
|
170
|
+
(`marketplace.notificationService.sender -> notificationProvider`) and service-level in the map
|
|
171
|
+
(`marketplace.notificationService -> notificationProvider`), because every fleet check — the spine,
|
|
172
|
+
`landscape.service-isolated`, the platform census — reads the map alone. ROADMAP.md keeps that
|
|
173
|
+
remainder written down rather than hidden.
|
|
174
|
+
|
|
157
175
|
Plus one count rather than a finding: `sourcesUnverifiableFromHere: 4`. Four specs name paths in
|
|
158
176
|
their own service repositories, and this is none of those repositories — the fleet gate reports the
|
|
159
177
|
blind spot instead of resolving it. Running `loam validate --service <id>` inside each service repo
|