@ybotok/loam 0.2.0-alpha.4 → 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 +947 -0
- package/MIGRATING-from-OpenSpec.md +7 -1
- package/QUICKSTART.md +8 -3
- package/README.md +20 -13
- package/ROADMAP.md +329 -22
- package/SCHEMA.md +472 -24
- 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 +184 -39
- 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 +29 -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 +128 -114
- 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 +278 -0
- package/dist/commands/validate/fleet/views/projects.js.map +1 -0
- 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 -2
- 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 -2
- 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 +125 -6
- package/dist/core/agent/workflows/adopt.js.map +1 -1
- package/dist/core/agent/workflows/check.js +35 -19
- 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 +138 -3
- 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 -26
- 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 +31 -22
- 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 +48 -9
- 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/families.js +1 -1
- package/dist/core/explain/families.js.map +1 -1
- 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 +40 -0
- package/dist/core/repo/paths.js.map +1 -1
- package/dist/core/repo/tree/render/projects.js +182 -0
- package/dist/core/repo/tree/render/projects.js.map +1 -0
- 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 +23 -0
- 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 -33
- 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
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"steps.js","sourceRoot":"","sources":["../../../../src/core/c4/resolve/steps.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * Which declared relationship backs one hop of a use case, and what it says the\n * operation is.\n *\n * A `dynamic view`'s hop says \"web talks to orders\". Answering it is the same\n * shape of join `Covers:` performs for an edge entry one module over — same two\n * tiers, same cached `serviceResolver` (`./service.ts`'s `resolverFor`, which\n * both axes share), same \"an endpoint names an element or the service it stands\n * for\" rule.\n *\n * It lived in `../arch.ts` while that file was the only home the join had, and\n * moved here when the Covers grammar grew a fourth entry form and the file\n * reached its line limit. The move changed nothing: the seam was already named\n * in that file's own header, and this is the package the resolution it depends\n * on lives in.\n *\n * It does NOT live beside the view reader in `../parsed/dynamic-views.ts`, and\n * that is a rule rather than a preference: a second copy of this join is the\n * copy that drifts, and that module may read `$data` and nothing else\n * (docs/DESIGN.md rule 26) — it has no business holding the model join.\n */\nimport { type Elem, type Rel } from \"../likec4.js\";\nimport { resolverFor } from \"./service.js\";\nimport type { ParsedStep } from \"../parsed/dynamic-views.js\";\n\n/**\n * The three fields an attribution reads off a step, and no more.\n *\n * Narrower than `ParsedStep` on purpose. Attribution must not depend on the\n * ordinal, the title or the notes — a hop is graded by where it points, never by\n * what the author called it — and a signature that says so is also a signature a\n * test can satisfy with a literal instead of a parsed document. Derived from\n * `ParsedStep` rather than restated, so a rename upstream is a compile error\n * here instead of a second definition that quietly disagrees.\n */\nexport type StepEndpoints = Pick<ParsedStep, \"source\" | \"target\" | \"isBackward\">;\n\n/**\n * What a step is attributed against: the model in view, plus the enumerated\n * fleet where the caller has it.\n *\n * A deliberate subset of `CoverageScope` rather than a reuse of it — an\n * attribution never reads `health`, and demanding one would make every caller\n * fabricate an empty `HealthIds` to ask a question that has nothing to do with\n * alerts. A `CoverageScope` is still structurally assignable here, so a caller\n * already holding the fuller scope passes it through unchanged.\n */\nexport interface StepScope {\n elements: Elem[];\n relationships: Rel[];\n /** As `CoverageScope.known`, for the same reason: one set per run, shared. */\n known?: ReadonlySet<string>;\n}\n\n/** The oriented pair a verdict is about — see `callPair` for which way round. */\ninterface CallPair {\n from: string;\n to: string;\n}\n\n/**\n * What the model says about one hop of a use case.\n *\n * Three variants rather than one record with optional fields, because the fields\n * are not independent: `op` is meaningful only when exactly one operation was\n * implied, `ops` only when more than one was, and an unbacked step has neither\n * and no tier either. Tagged, those states stop being constructible.\n *\n * - `attributed` — the candidates on the matched tier agree, so the hop\n * exercises exactly one operation. `op` is absent when that one agreed\n * operation is *no* operation: the relationships back the hop but none of\n * them carries `metadata { op '…' }`.\n * - `contested` — the candidates disagree, so loam cannot say which operation\n * the hop exercises. `ops` lists the distinct values in candidate order,\n * `undefined` standing for the candidate that declared none.\n * - `unbacked` — nothing in the model backs the hop on either tier. The pair is\n * still carried, because a message about it has to be able to name the edge\n * the author would have to draw.\n *\n * `rels` carries every candidate on the matched tier, not just the first: an\n * `attributed` verdict over two relationships is the normal shape once a service\n * is drawn as containers, and a caller pinning evidence needs all of them.\n */\nexport type StepAttribution =\n | (CallPair & { verdict: \"attributed\"; tier: 1 | 2; rels: readonly Rel[]; op?: string })\n | (CallPair & { verdict: \"contested\"; tier: 1 | 2; rels: readonly Rel[]; ops: readonly (string | undefined)[] })\n | (CallPair & { verdict: \"unbacked\" });\n\n/**\n * The pair a step's backing relationship is looked up under. This is the half\n * that mis-grades the whole fleet if it is wrong, because a reply arrow is the\n * commonest step in any sequence diagram.\n *\n * MEASURED at the `likec4@1.59.2` pin, and `test/likec4-view-shape.test.ts` holds\n * the raw form: a reply written `a <- b 'reply'` parses as\n * `{source: \"b\", target: \"a\", isBackward: true}` — LikeC4 has ALREADY reversed\n * the endpoints and set the flag — while the forward `b -> a` yields that same\n * pair with no `isBackward` key at all (ABSENT, not false). So the endpoints as\n * parsed describe the direction the MESSAGE travels, and the flag records which\n * arrow the author typed.\n *\n * loam attributes a reply to the CALL it answers, so the reversal is undone\n * here. A landscape declares `web -> orders` because that edge is the\n * dependency; a mirror `orders -> web` for each response is not something a\n * fleet writes down. Reading a reply hop under the message's own direction would\n * report every return hop against such a landscape as `unbacked`, and where a\n * fleet does draw the mirror edge it would file the two halves of one call under\n * two operations — the call under `op 'createOrder'`, its reply under none.\n *\n * That is deliberately NOT what LikeC4's own computed stage does, and the\n * difference is measured rather than assumed: that stage resolves a backward\n * step's `relations` from the UNFLIPPED pair, so at this pin a `web <- orders`\n * step computes to `relations: []` when only `web -> orders` exists, and to the\n * mirror edge when only `orders -> web` does. It is answering which arrow to\n * draw; loam is answering which operation the hop exercises, and reads what a\n * view DECLARES rather than what it shows — which is why loam never reaches that\n * stage at all (docs/DESIGN.md rule 26, enforced by `scripts/arch-check.mjs`).\n * The consequence to know before changing this: where\n * a fleet declares BOTH directions, the mirror edge is not what backs the reply\n * here — the call is, and the mirror edge backs only a hop drawn forward along\n * it.\n */\nfunction callPair(step: StepEndpoints): CallPair {\n return step.isBackward ? { from: step.target, to: step.source } : { from: step.source, to: step.target };\n}\n\n/**\n * The distinct `op` values across candidates, first-seen order preserved.\n *\n * Absent is a VALUE here, not a gap to skip: a relationship carrying no `op` and\n * one carrying `createOrder` disagree about the operation just as surely as two\n * different ops do, and skipping the absent one would silently promote that\n * disagreement to a confident answer. `includes` rather than a `Set` because a\n * candidate list is the handful of relationships between one pair of endpoints,\n * and it keeps `undefined` comparing the way the rest of this file expects.\n */\nfunction distinctOps(rels: readonly Rel[]): (string | undefined)[] {\n const out: (string | undefined)[] = [];\n for (const r of rels) if (!out.includes(r.op)) out.push(r.op);\n return out;\n}\n\n/**\n * The verdict one tier's candidates imply, or `undefined` when that tier found\n * nothing and is therefore not the tier that answers.\n *\n * The count that decides is the DISTINCT-OP count, never the candidate count.\n * That distinction is what makes the service tier safe: once a service is drawn\n * as containers, `a.api -> b` and `a.worker -> b` both carrying\n * `op 'createOrder'` are two relationships and one operation, and a verdict read\n * off `rels.length` would convict a perfectly consistent model of contesting\n * itself.\n */\nfunction verdictOn(pair: CallPair, tier: 1 | 2, rels: readonly Rel[]): StepAttribution | undefined {\n if (rels.length === 0) return undefined;\n const ops = distinctOps(rels);\n if (ops.length > 1) return { ...pair, verdict: \"contested\", tier, rels, ops };\n // Length is exactly one here, so the single distinct value is the answer —\n // and `undefined` is a legitimate answer: every candidate agreed that this\n // hop names no operation.\n const [only] = ops;\n return { ...pair, verdict: \"attributed\", tier, rels, ...(only === undefined ? {} : { op: only }) };\n}\n\n/**\n * Relationships whose ENDPOINTS resolve to the same two services as the pair —\n * the fallback tier, and the reason a step drawn between two services matches an\n * edge drawn between two containers.\n *\n * Through the module's cached `resolverFor`, not a fresh `serviceResolver`: this\n * runs once per step of every use case in the fleet, and building the id map per\n * step is the cost that comment above `RESOLVERS` exists to prevent.\n */\nfunction resolvedTier(pair: CallPair, scope: StepScope): Rel[] {\n const resolve = resolverFor(scope.elements, scope.known);\n const from = resolve(pair.from);\n const to = resolve(pair.to);\n return scope.relationships.filter((r) => resolve(r.source) === from && resolve(r.target) === to);\n}\n\n/**\n * Which relationship in the model backs one hop of a use case, and what it says\n * the operation is.\n *\n * Two tiers, and the second runs ONLY when the first is empty. That ordering is\n * load-bearing rather than an optimisation: the service tier necessarily\n * re-finds every exact match — an endpoint that matches literally resolves to\n * the same service trivially — so running it unconditionally would let a\n * container-level edge with a different `op` contest a step the model already\n * answers exactly, turning an `attributed` verdict into a `contested` one on a\n * document nobody changed.\n *\n * Relationship `kind` is deliberately not part of the match, and that is\n * measured too: at this pin `a -[http]-> b` without a `relationship http` in the\n * `specification` block is \"Could not resolve reference to RelationshipKind\n * named 'http'\" — a parse error, which under loam's standing rule means no model\n * at all — and `test/helpers/harness.ts`'s own `LANDSCAPE` declares no\n * relationship kinds. So narrowing by kind would mean a fix message asking an\n * author to write a line their document does not parse.\n */\nexport function attributeStep(step: StepEndpoints, scope: StepScope): StepAttribution {\n const pair = callPair(step);\n const exact = scope.relationships.filter((r) => r.source === pair.from && r.target === pair.to);\n const declared = verdictOn(pair, 1, exact);\n if (declared !== undefined) return declared;\n return verdictOn(pair, 2, resolvedTier(pair, scope)) ?? { ...pair, verdict: \"unbacked\" };\n}\n"]}
|
|
1
|
+
{"version":3,"file":"steps.js","sourceRoot":"","sources":["../../../../src/core/c4/resolve/steps.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * Which declared relationship backs one hop of a use case, and what it says the\n * operation is.\n *\n * A `dynamic view`'s hop says \"web talks to orders\". Answering it is the same\n * shape of join `Covers:` performs for an edge entry one module over — same two\n * tiers, same cached `serviceResolver` (`./service.ts`'s `resolverFor`, which\n * both axes share), same \"an endpoint names an element or the service it stands\n * for\" rule.\n *\n * It lived in `../arch.ts` while that file was the only home the join had, and\n * moved here when the Covers grammar grew a fourth entry form and the file\n * reached its line limit. The move changed nothing: the seam was already named\n * in that file's own header, and this is the package the resolution it depends\n * on lives in.\n *\n * It does NOT live beside the view reader in `../parsed/dynamic-views.ts`, and\n * that is a rule rather than a preference: a second copy of this join is the\n * copy that drifts, and that module may read `$data` and nothing else\n * (docs/DESIGN.md rule 26) — it has no business holding the model join.\n */\nimport { type Elem, type Rel } from \"../likec4.js\";\nimport { resolverFor } from \"./service.js\";\nimport type { ParsedStep } from \"../parsed/dynamic-views.js\";\n\n/**\n * The three fields an attribution reads off a step, and no more.\n *\n * Narrower than `ParsedStep` on purpose. Attribution must not depend on the\n * ordinal, the title or the notes — a hop is graded by where it points, never by\n * what the author called it — and a signature that says so is also a signature a\n * test can satisfy with a literal instead of a parsed document. Derived from\n * `ParsedStep` rather than restated, so a rename upstream is a compile error\n * here instead of a second definition that quietly disagrees.\n */\nexport type StepEndpoints = Pick<ParsedStep, \"source\" | \"target\" | \"isBackward\">;\n\n/**\n * What a step is attributed against: the model in view, plus the enumerated\n * fleet where the caller has it.\n *\n * A deliberate subset of `CoverageScope` rather than a reuse of it — an\n * attribution never reads `health`, and demanding one would make every caller\n * fabricate an empty `HealthIds` to ask a question that has nothing to do with\n * alerts. A `CoverageScope` is still structurally assignable here, so a caller\n * already holding the fuller scope passes it through unchanged.\n */\nexport interface StepScope {\n elements: Elem[];\n relationships: Rel[];\n /** As `CoverageScope.known`, for the same reason: one set per run, shared. */\n known?: ReadonlySet<string>;\n}\n\n/** The oriented pair a verdict is about — see `callPair` for which way round. */\ninterface CallPair {\n from: string;\n to: string;\n}\n\n/**\n * What the model says about one hop of a use case.\n *\n * Three variants rather than one record with optional fields, because the fields\n * are not independent: `op` is meaningful only when exactly one operation was\n * implied, `ops` only when more than one was, and an unbacked step has neither\n * and no tier either. Tagged, those states stop being constructible.\n *\n * - `attributed` — the candidates on the matched tier agree, so the hop\n * exercises exactly one operation. `op` is absent when that one agreed\n * operation is *no* operation: the relationships back the hop but none of\n * them carries `metadata { op '…' }`.\n * - `contested` — the candidates disagree, so loam cannot say which operation\n * the hop exercises. `ops` lists the distinct values in candidate order,\n * `undefined` standing for the candidate that declared none.\n * - `unbacked` — nothing in the model backs the hop on either tier. The pair is\n * still carried, because a message about it has to be able to name the edge\n * the author would have to draw.\n *\n * `rels` carries every candidate on the matched tier, not just the first: an\n * `attributed` verdict over two relationships is the normal shape once a service\n * is drawn as containers, and a caller pinning evidence needs all of them.\n */\nexport type StepAttribution =\n | (CallPair & { verdict: \"attributed\"; tier: 1 | 2; rels: readonly Rel[]; op?: string })\n | (CallPair & { verdict: \"contested\"; tier: 1 | 2; rels: readonly Rel[]; ops: readonly (string | undefined)[] })\n | (CallPair & { verdict: \"unbacked\" });\n\n/**\n * The pair a step's backing relationship is looked up under. This is the half\n * that mis-grades the whole fleet if it is wrong, because a reply arrow is the\n * commonest step in any sequence diagram.\n *\n * MEASURED at the `likec4@1.59.2` pin, and `test/likec4-view-shape.test.ts` holds\n * the raw form: a reply written `a <- b 'reply'` parses as\n * `{source: \"b\", target: \"a\", isBackward: true}` — LikeC4 has ALREADY reversed\n * the endpoints and set the flag — while the forward `b -> a` yields that same\n * pair with no `isBackward` key at all (ABSENT, not false). So the endpoints as\n * parsed describe the direction the MESSAGE travels, and the flag records which\n * arrow the author typed.\n *\n * loam attributes a reply to the CALL it answers, so the reversal is undone\n * here. A landscape declares `web -> orders` because that edge is the\n * dependency; a mirror `orders -> web` for each response is not something a\n * fleet writes down. Reading a reply hop under the message's own direction would\n * report every return hop against such a landscape as `unbacked`, and where a\n * fleet does draw the mirror edge it would file the two halves of one call under\n * two operations — the call under `op 'createOrder'`, its reply under none.\n *\n * That is deliberately NOT what LikeC4's own computed stage does, and the\n * difference is measured rather than assumed: that stage resolves a backward\n * step's `relations` from the UNFLIPPED pair, so at this pin a `web <- orders`\n * step computes to `relations: []` when only `web -> orders` exists, and to the\n * mirror edge when only `orders -> web` does. It is answering which arrow to\n * draw; loam is answering which operation the hop exercises, and reads what a\n * view DECLARES rather than what it shows — which is why loam never reaches that\n * stage at all (docs/DESIGN.md rule 26, enforced by `scripts/arch-check.mjs`).\n * The consequence to know before changing this: where\n * a fleet declares BOTH directions, the mirror edge is not what backs the reply\n * here — the call is, and the mirror edge backs only a hop drawn forward along\n * it.\n */\nfunction callPair(step: StepEndpoints): CallPair {\n return step.isBackward ? { from: step.target, to: step.source } : { from: step.source, to: step.target };\n}\n\n/**\n * The distinct `op` values across candidates, first-seen order preserved.\n *\n * Absent is a VALUE here, not a gap to skip: a relationship carrying no `op` and\n * one carrying `createOrder` disagree about the operation just as surely as two\n * different ops do, and skipping the absent one would silently promote that\n * disagreement to a confident answer. `includes` rather than a `Set` because a\n * candidate list is the handful of relationships between one pair of endpoints,\n * and it keeps `undefined` comparing the way the rest of this file expects.\n */\nfunction distinctOps(rels: readonly Rel[]): (string | undefined)[] {\n const out: (string | undefined)[] = [];\n for (const r of rels) if (!out.includes(r.op)) out.push(r.op);\n return out;\n}\n\n/**\n * The verdict one tier's candidates imply, or `undefined` when that tier found\n * nothing and is therefore not the tier that answers.\n *\n * The count that decides is the DISTINCT-OP count, never the candidate count.\n * That distinction is what makes the service tier safe: once a service is drawn\n * as containers, `a.api -> b` and `a.worker -> b` both carrying\n * `op 'createOrder'` are two relationships and one operation, and a verdict read\n * off `rels.length` would convict a perfectly consistent model of contesting\n * itself.\n */\nfunction verdictOn(pair: CallPair, tier: 1 | 2, rels: readonly Rel[]): StepAttribution | undefined {\n if (rels.length === 0) return undefined;\n const ops = distinctOps(rels);\n if (ops.length > 1) return { ...pair, verdict: \"contested\", tier, rels, ops };\n // Length is exactly one here, so the single distinct value is the answer —\n // and `undefined` is a legitimate answer: every candidate agreed that this\n // hop names no operation.\n const [only] = ops;\n return { ...pair, verdict: \"attributed\", tier, rels, ...(only === undefined ? {} : { op: only }) };\n}\n\n/**\n * Relationships whose ENDPOINTS resolve to the same two services as the pair —\n * the fallback tier, and the reason a step drawn between two services matches an\n * edge drawn between two containers.\n *\n * EMPTY when both endpoints resolve to ONE service, and that is a correction\n * rather than an optimisation. The service tier exists for a step drawn between\n * two SERVICES, where \"which edge backs it\" is answered by collapsing each\n * side's containers to the service that owns them. Inside one service there is\n * no service-tier reading: collapsing both sides yields the pair\n * (svc, svc), which EVERY internal relationship of that service matches —\n * measured at the 1.59.2 pin on a bound service drawn as three containers:\n * a hop `api -> db` with no declared edge and two unrelated internal edges\n * (`api -> workflow`, `workflow -> db`) came back `attributed` on tier 2 with\n * both edges as candidates, all agreeing on no `op`. So an unbacked hop between\n * two containers of one service could never earn `usecase.step-unbacked` — the\n * one grade this join exists to produce — until the moment a fleet drew a\n * service's flow over its own containers (`core/usecases/service/flows.ts`),\n * and a landscape that draws a service as containers had the same wrong answer\n * for the same hop all along.\n *\n * Through the module's cached `resolverFor`, not a fresh `serviceResolver`: this\n * runs once per step of every use case in the fleet, and building the id map per\n * step is the cost that comment above `RESOLVERS` exists to prevent.\n */\nfunction resolvedTier(pair: CallPair, scope: StepScope): Rel[] {\n const resolve = resolverFor(scope.elements, scope.known);\n const from = resolve(pair.from);\n const to = resolve(pair.to);\n if (from === to) return [];\n return scope.relationships.filter((r) => resolve(r.source) === from && resolve(r.target) === to);\n}\n\n/**\n * Which relationship in the model backs one hop of a use case, and what it says\n * the operation is.\n *\n * Two tiers, and the second runs ONLY when the first is empty. That ordering is\n * load-bearing rather than an optimisation: the service tier necessarily\n * re-finds every exact match — an endpoint that matches literally resolves to\n * the same service trivially — so running it unconditionally would let a\n * container-level edge with a different `op` contest a step the model already\n * answers exactly, turning an `attributed` verdict into a `contested` one on a\n * document nobody changed.\n *\n * Relationship `kind` is deliberately not part of the match, and that is\n * measured too: at this pin `a -[http]-> b` without a `relationship http` in the\n * `specification` block is \"Could not resolve reference to RelationshipKind\n * named 'http'\" — a parse error, which under loam's standing rule means no model\n * at all — and `test/helpers/harness.ts`'s own `LANDSCAPE` declares no\n * relationship kinds. So narrowing by kind would mean a fix message asking an\n * author to write a line their document does not parse.\n */\nexport function attributeStep(step: StepEndpoints, scope: StepScope): StepAttribution {\n const pair = callPair(step);\n const exact = scope.relationships.filter((r) => r.source === pair.from && r.target === pair.to);\n const declared = verdictOn(pair, 1, exact);\n if (declared !== undefined) return declared;\n return verdictOn(pair, 2, resolvedTier(pair, scope)) ?? { ...pair, verdict: \"unbacked\" };\n}\n"]}
|
|
@@ -0,0 +1,352 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The ONE literal fact loam reads out of the root `likec4.config.json`: which
|
|
3
|
+
* paths that file excludes from the renderer's root project, and whether a
|
|
4
|
+
* given directory or document is one of them.
|
|
5
|
+
*
|
|
6
|
+
* Until this module existed loam read NOTHING out of that file. It wrote it
|
|
7
|
+
* once (`core/docs.ts`, `loam init --create`), never re-read it, and the team
|
|
8
|
+
* owned it from then on — the ownership rule `core/repo/tree/render/projects.ts`
|
|
9
|
+
* still states for the per-service files. That rule is kept everywhere it still
|
|
10
|
+
* applies; this is the one exception, and it is earned by what report #05
|
|
11
|
+
* measured: a model that EXTENDS the fleet map lives in the ROOT project, so
|
|
12
|
+
* the root's `exclude` decides whether the renderer can see it at all. A repo
|
|
13
|
+
* whose root config still says `services/**` — every repo scaffolded before
|
|
14
|
+
* this axis — renders such a model as a box with nothing inside it, and no
|
|
15
|
+
* check could say why, because nothing here had ever opened the file.
|
|
16
|
+
*
|
|
17
|
+
* READ here, and written back by one command only (`loam subsystem sync`,
|
|
18
|
+
* through `standaloneExclude` in `../service-model/renderer.ts`). Nothing else
|
|
19
|
+
* about the file is interpreted: `exclude` is a glob list in a tool's own
|
|
20
|
+
* config, and the entries loam does not recognise as naming a directory are
|
|
21
|
+
* left exactly where the team put them.
|
|
22
|
+
*
|
|
23
|
+
* IT LIVES BELOW `../project/` BECAUSE THE PROJECT LOADER IS A READER. The
|
|
24
|
+
* architecture document set is the one list every reader shares, and it has to
|
|
25
|
+
* drop a `.likec4` the root project excludes — a palette under an excluded path
|
|
26
|
+
* otherwise yields a `global style` reference the fleet project cannot resolve,
|
|
27
|
+
* `likec4 validate` Invalid on every run and `loam validate --all` at 0 errors
|
|
28
|
+
* (verification 2026-09-04, W5). `../service-model/` already imports
|
|
29
|
+
* `../project/`, so this could not stay there without reversing that edge.
|
|
30
|
+
*/
|
|
31
|
+
import { readFile } from "node:fs/promises";
|
|
32
|
+
import { isRecord } from "../../kernel/records.js";
|
|
33
|
+
import { rootProjectPath } from "../../repo/paths.js";
|
|
34
|
+
/** The `services/` prefix every tree path is spelled under, in the config and on disk alike. */
|
|
35
|
+
const SERVICES = "services";
|
|
36
|
+
/**
|
|
37
|
+
* The root project's `exclude` list, or null when the file cannot be read as a
|
|
38
|
+
* project config at all.
|
|
39
|
+
*
|
|
40
|
+
* The three answers are deliberately distinct, because two of them mean
|
|
41
|
+
* opposite things to a grader:
|
|
42
|
+
*
|
|
43
|
+
* - `[]` — the file is a JSON object that excludes nothing (or names no
|
|
44
|
+
* `exclude` key at all, which is the same thing to the renderer). Every
|
|
45
|
+
* extending model is visible; a standalone one is in the root project where
|
|
46
|
+
* it cannot parse.
|
|
47
|
+
* - a list — the entries as written, in file order.
|
|
48
|
+
* - `null` — absent, unreadable, not JSON, or JSON whose `exclude` is not a
|
|
49
|
+
* list of strings. loam does not know what the renderer will do, so it says
|
|
50
|
+
* nothing rather than guessing: the findings that read this list are silent
|
|
51
|
+
* on null, `subsystem sync` leaves such a file alone, and the project loader
|
|
52
|
+
* filters nothing. Failing closed here means the team keeps a file loam
|
|
53
|
+
* cannot parse; failing open would mean loam asserting the renderer excludes
|
|
54
|
+
* something on evidence it does not have.
|
|
55
|
+
*/
|
|
56
|
+
export async function readRootExclude(docsDir) {
|
|
57
|
+
const parsed = await readRootProject(docsDir);
|
|
58
|
+
if (parsed === null)
|
|
59
|
+
return null;
|
|
60
|
+
const exclude = parsed["exclude"];
|
|
61
|
+
if (exclude === undefined)
|
|
62
|
+
return [];
|
|
63
|
+
if (!Array.isArray(exclude))
|
|
64
|
+
return null;
|
|
65
|
+
// A predicate rather than a cast, so the narrowing below is the compiler's
|
|
66
|
+
// conclusion from a check that actually ran: an `exclude` holding a number is
|
|
67
|
+
// a file loam does not understand, and understanding it half way is how a
|
|
68
|
+
// grader ends up asserting a glob nobody wrote.
|
|
69
|
+
const entries = exclude;
|
|
70
|
+
return entries.every((entry) => typeof entry === "string") ? [...entries] : null;
|
|
71
|
+
}
|
|
72
|
+
/**
|
|
73
|
+
* The root project file as a JSON object, or null when it is absent or is not
|
|
74
|
+
* one — the ONE reader, shared with the one writer that re-parses it before a
|
|
75
|
+
* rewrite (`commands/subsystem/txn/exclude.ts`).
|
|
76
|
+
*
|
|
77
|
+
* A LEADING BYTE-ORDER MARK IS STRIPPED, and that is not tidiness: PowerShell's
|
|
78
|
+
* `Out-File` writes `ef bb bf` by default on this platform, so a Windows shell
|
|
79
|
+
* saving the very file loam wrote left `JSON.parse` throwing while the renderer
|
|
80
|
+
* — whose reader tolerates the mark — went on applying every entry in the list.
|
|
81
|
+
* loam then graded nothing at all: `validate --all` silent about an exclusion in
|
|
82
|
+
* full force, `doctor` healthy, `subsystem sync` reporting the list unreadable
|
|
83
|
+
* and naming a fault (`not a JSON object with a string exclude list`) that was
|
|
84
|
+
* not the one present (re-verification 2026-09-04, area C item 3). One reader
|
|
85
|
+
* means the grade and the rewrite can never disagree about whether the file
|
|
86
|
+
* parses.
|
|
87
|
+
*/
|
|
88
|
+
async function readRootProject(docsDir) {
|
|
89
|
+
let text;
|
|
90
|
+
try {
|
|
91
|
+
text = await readFile(rootProjectPath(docsDir), "utf8");
|
|
92
|
+
}
|
|
93
|
+
catch {
|
|
94
|
+
// Absent reads as "loam cannot say", not as "excludes nothing": a docs repo
|
|
95
|
+
// with no root project file has no root project either, and the renderer
|
|
96
|
+
// opened there shows nothing at all — a different problem, with a different
|
|
97
|
+
// finding (`doctor.likec4-config-missing`).
|
|
98
|
+
return null;
|
|
99
|
+
}
|
|
100
|
+
return parseRootProject(text);
|
|
101
|
+
}
|
|
102
|
+
/** The byte-order mark PowerShell's `Out-File` prepends by default on this platform. */
|
|
103
|
+
const BOM = "";
|
|
104
|
+
/** The same parse, over bytes a caller already read. Exported for the rewrite path only. */
|
|
105
|
+
export function parseRootProject(text) {
|
|
106
|
+
let parsed;
|
|
107
|
+
try {
|
|
108
|
+
parsed = JSON.parse(text.startsWith(BOM) ? text.slice(BOM.length) : text);
|
|
109
|
+
}
|
|
110
|
+
catch {
|
|
111
|
+
// Not JSON is the team's file to fix, and nothing here may rewrite it.
|
|
112
|
+
return null;
|
|
113
|
+
}
|
|
114
|
+
return isRecord(parsed) ? parsed : null;
|
|
115
|
+
}
|
|
116
|
+
/** A PATH as segments — `/` or `\`, either spelling, no empty parts, no `.`. Never an ENTRY: see `rendererEntry`. */
|
|
117
|
+
function segments(path) {
|
|
118
|
+
return path.split(/[\\/]/).filter((s) => s.length > 0 && s !== ".");
|
|
119
|
+
}
|
|
120
|
+
function rendererEntry(entry) {
|
|
121
|
+
if (entry.includes("\\"))
|
|
122
|
+
return null;
|
|
123
|
+
let rest = entry;
|
|
124
|
+
let anchored = false;
|
|
125
|
+
if (rest.startsWith("./")) {
|
|
126
|
+
rest = rest.slice(2);
|
|
127
|
+
anchored = true;
|
|
128
|
+
// `.//x` — the dot survives as a literal directory name, so the pattern
|
|
129
|
+
// names a path nothing on disk has.
|
|
130
|
+
if (rest.startsWith("/"))
|
|
131
|
+
return null;
|
|
132
|
+
}
|
|
133
|
+
else if (rest.startsWith("../")) {
|
|
134
|
+
return null;
|
|
135
|
+
}
|
|
136
|
+
const trailing = rest.endsWith("/");
|
|
137
|
+
const parts = [];
|
|
138
|
+
for (const part of rest.split("/")) {
|
|
139
|
+
if (part.length === 0 || part === ".")
|
|
140
|
+
continue;
|
|
141
|
+
if (part === "..") {
|
|
142
|
+
if (parts.pop() === undefined)
|
|
143
|
+
return null;
|
|
144
|
+
continue;
|
|
145
|
+
}
|
|
146
|
+
parts.push(part);
|
|
147
|
+
}
|
|
148
|
+
// An entry that normalises to nothing at all is the project folder itself,
|
|
149
|
+
// which `contains: true` makes a match for every document under it.
|
|
150
|
+
const glob = parts.length === 0 ? "**" : `${parts.join("/")}${trailing ? "/" : ""}`;
|
|
151
|
+
return { glob, anchored: anchored && parts.length > 0 };
|
|
152
|
+
}
|
|
153
|
+
/** The expression one entry matches with, or null when it matches nothing — `rendererEntry` says which. */
|
|
154
|
+
function matcherFor(parsed, glob) {
|
|
155
|
+
return new RegExp(`^${globSource(parsed.anchored ? glob : prefixed(glob))}`);
|
|
156
|
+
}
|
|
157
|
+
/**
|
|
158
|
+
* An entry's segments with the trailing bare wildcards taken off — the question
|
|
159
|
+
* "which directory is this line ABOUT", asked of the four spellings that name
|
|
160
|
+
* one: `services/pay` doubled-star, that entry with a trailing `/*` after it,
|
|
161
|
+
* `services/pay/*` and `services/pay/` all reduce to `services/pay`. A wildcard
|
|
162
|
+
* that is NOT trailing survives — one segment of its own — because a starred
|
|
163
|
+
* middle segment picks one directory level and no more.
|
|
164
|
+
*
|
|
165
|
+
* It is NOT the covering rule. What an entry HIDES is `entryRegex` below, and
|
|
166
|
+
* the two answer differently on purpose: `services/pay/**` is about the
|
|
167
|
+
* directory `services/pay`, while the bare `services/pay` hides
|
|
168
|
+
* `services/payment-service` as well (measured — see below).
|
|
169
|
+
*
|
|
170
|
+
* It takes the NORMALISED glob, never the raw entry: an entry is split on `/`
|
|
171
|
+
* alone, because a backslash is a literal to the renderer and splitting on it
|
|
172
|
+
* here was what made `services\order-service\**` read as naming a directory it
|
|
173
|
+
* hides nothing in.
|
|
174
|
+
*/
|
|
175
|
+
function patternSegments(glob) {
|
|
176
|
+
const parts = glob.split("/").filter((part) => part.length > 0);
|
|
177
|
+
while (parts.length > 0 && (parts.at(-1) === "*" || parts.at(-1) === "**"))
|
|
178
|
+
parts.pop();
|
|
179
|
+
return parts;
|
|
180
|
+
}
|
|
181
|
+
/**
|
|
182
|
+
* One `exclude` entry as the expression the RENDERER matches paths with:
|
|
183
|
+
* anchored at the start of a path segment, and NOT anchored at the end.
|
|
184
|
+
*
|
|
185
|
+
* MEASURED, at the 1.59.2 pin, on a scratch fleet of five documents —
|
|
186
|
+
* `architecture/landscape.likec4`, `architecture/usecases/uc.likec4`,
|
|
187
|
+
* `services/payment-service/model.likec4`,
|
|
188
|
+
* `services/payments/payment-service/model.likec4` and
|
|
189
|
+
* `services/order-service/model.likec4` — counting the "N files" `npx likec4
|
|
190
|
+
* validate .` reports with one entry in the list:
|
|
191
|
+
*
|
|
192
|
+
* services/zzz 5 services/pay 3
|
|
193
|
+
* services/payment-service 4 a star as the middle segment 4
|
|
194
|
+
* services/pay/** 5 architect 3
|
|
195
|
+
* architecture/*.likec4 4 *.likec4 0
|
|
196
|
+
* ervices 5 ces/payment-service 5
|
|
197
|
+
* payment-service 3 services/** 2
|
|
198
|
+
* services 2 services/payment-service/* hides a nested file too
|
|
199
|
+
*
|
|
200
|
+
* Four rules come out of that table, and every one of them contradicts what
|
|
201
|
+
* this module asserted before (verification 2026-09-04, review C): the match is
|
|
202
|
+
* a raw PREFIX with no end anchor (`services/pay` takes both payment trees,
|
|
203
|
+
* `architect` takes all of `architecture/`); it must START on a `/` boundary
|
|
204
|
+
* (`ervices` and `ces/payment-service` take nothing); `*` stays inside one
|
|
205
|
+
* segment (`architecture/*.likec4` leaves `architecture/usecases/uc.likec4`);
|
|
206
|
+
* and a glob form does NOT prefix-match (`services/pay/**` needs the literal
|
|
207
|
+
* separator, so it takes nothing from `services/payment-service`).
|
|
208
|
+
*
|
|
209
|
+
* The shape falls out of what LikeC4 does with the list: an entry that does not
|
|
210
|
+
* already begin with a doubled star is prefixed with one, joined onto the
|
|
211
|
+
* project folder and handed to picomatch with `contains: true` — and `contains`
|
|
212
|
+
* is exactly the missing end anchor. So the optional any-directories group
|
|
213
|
+
* `globSource` emits for a leading doubled star IS that prefix, and the absent
|
|
214
|
+
* `$` is that option.
|
|
215
|
+
*
|
|
216
|
+
* NULL when the entry hides nothing at all — `rendererEntry` names the two
|
|
217
|
+
* spellings that do, and every caller has to skip such an entry rather than
|
|
218
|
+
* treat it as covering: a warning derived from one sent a team after a line the
|
|
219
|
+
* renderer ignores, and `subsystem sync` then deleted it.
|
|
220
|
+
*/
|
|
221
|
+
function entryRegex(entry) {
|
|
222
|
+
const parsed = rendererEntry(entry);
|
|
223
|
+
return parsed === null ? null : matcherFor(parsed, parsed.glob);
|
|
224
|
+
}
|
|
225
|
+
/** An entry as LikeC4 spells it before matching: a doubled star in front, unless it has one. */
|
|
226
|
+
function prefixed(entry) {
|
|
227
|
+
return entry.startsWith("**") ? entry : `**/${entry}`;
|
|
228
|
+
}
|
|
229
|
+
/**
|
|
230
|
+
* A glob as a regular-expression SOURCE. `**` crosses `/` and, followed by a
|
|
231
|
+
* separator, may stand for no directory at all (a doubled star between two literals matches with no directory between them, which
|
|
232
|
+
* is what makes a doubled-star-then-star entry cover a model directly under `services/`);
|
|
233
|
+
* `*` and `?` never cross one.
|
|
234
|
+
*/
|
|
235
|
+
function globSource(pattern) {
|
|
236
|
+
let source = "";
|
|
237
|
+
let i = 0;
|
|
238
|
+
while (i < pattern.length) {
|
|
239
|
+
if (pattern.startsWith("**/", i)) {
|
|
240
|
+
source += "(?:.*/)?";
|
|
241
|
+
i += 3;
|
|
242
|
+
continue;
|
|
243
|
+
}
|
|
244
|
+
if (pattern.startsWith("**", i)) {
|
|
245
|
+
source += ".*";
|
|
246
|
+
i += 2;
|
|
247
|
+
continue;
|
|
248
|
+
}
|
|
249
|
+
const c = pattern[i];
|
|
250
|
+
if (c === "*")
|
|
251
|
+
source += "[^/]*";
|
|
252
|
+
else if (c === "?")
|
|
253
|
+
source += "[^/]";
|
|
254
|
+
else
|
|
255
|
+
source += c.replace(/[.+^${}()|[\]\\]/g, "\\$&");
|
|
256
|
+
i += 1;
|
|
257
|
+
}
|
|
258
|
+
return source;
|
|
259
|
+
}
|
|
260
|
+
/**
|
|
261
|
+
* The entry that hides `relPath` from the root project, or null when nothing in
|
|
262
|
+
* the list does. `relPath` is a docs-relative FILE path, spelled either way
|
|
263
|
+
* round the slash.
|
|
264
|
+
*
|
|
265
|
+
* Returned VERBATIM because every message that carries it asks its reader to
|
|
266
|
+
* delete or narrow THAT line, and an entry re-spelled by loam is a line they
|
|
267
|
+
* would go looking for and not find.
|
|
268
|
+
*/
|
|
269
|
+
export function excludingPath(exclude, relPath) {
|
|
270
|
+
const path = segments(relPath).join("/");
|
|
271
|
+
if (path.length === 0)
|
|
272
|
+
return null;
|
|
273
|
+
for (const entry of exclude) {
|
|
274
|
+
if (entryRegex(entry)?.test(path) === true)
|
|
275
|
+
return entry;
|
|
276
|
+
}
|
|
277
|
+
return null;
|
|
278
|
+
}
|
|
279
|
+
/**
|
|
280
|
+
* The entry that hides a SERVICE TREE — the directory and everything under it —
|
|
281
|
+
* or null. The tree's spelling is normalised here because the repository writes
|
|
282
|
+
* it both ways: `serviceTreePath` includes the leading `services/` and the
|
|
283
|
+
* entries `standaloneExclude` writes are built from the part under it, and
|
|
284
|
+
* getting that wrong would silently answer "not excluded" for every service.
|
|
285
|
+
*
|
|
286
|
+
* A DIRECTORY IS NOT A FILE, and that is why this is not `excludingPath` on the
|
|
287
|
+
* directory's own path. `services/payment-service/**` hides every document in
|
|
288
|
+
* that tree while matching no prefix of the directory string itself — the
|
|
289
|
+
* pattern wants the separator the directory path ends before. So a second arm
|
|
290
|
+
* asks the other half: does the directory the entry NAMES (`patternSegments`,
|
|
291
|
+
* the trailing wildcards off) contain this tree? Asking it of the named
|
|
292
|
+
* directory rather than of the raw prefix is what keeps `services/pay/**` about
|
|
293
|
+
* `services/pay` and away from `services/payment-service`, which is exactly
|
|
294
|
+
* where the renderer leaves it.
|
|
295
|
+
*
|
|
296
|
+
* What this deliberately does NOT report is an entry that hides only PART of a
|
|
297
|
+
* tree (`services/x/model`, say). The four findings that read this answer are
|
|
298
|
+
* about a directory the renderer either loads or does not, and half of one is a
|
|
299
|
+
* state `subsystem sync` has no entry to write for.
|
|
300
|
+
*/
|
|
301
|
+
export function excludingEntry(exclude, treePath) {
|
|
302
|
+
const tree = segments(treePath).join("/");
|
|
303
|
+
if (tree.length === 0)
|
|
304
|
+
return null;
|
|
305
|
+
const full = tree === SERVICES || tree.startsWith(`${SERVICES}/`) ? tree : `${SERVICES}/${tree}`;
|
|
306
|
+
for (const entry of exclude) {
|
|
307
|
+
if (coversTree(entry, full))
|
|
308
|
+
return entry;
|
|
309
|
+
}
|
|
310
|
+
return null;
|
|
311
|
+
}
|
|
312
|
+
/** Does one entry hide `dir` and everything under it? The two arms `excludingEntry` documents. */
|
|
313
|
+
function coversTree(entry, dir) {
|
|
314
|
+
const parsed = rendererEntry(entry);
|
|
315
|
+
// An entry that hides nothing covers nothing — the backslash spelling, which
|
|
316
|
+
// loam used to read as covering the tree while the renderer loaded every file
|
|
317
|
+
// in it (measured: 8 of 8), warning `service.model-excluded` about a healthy
|
|
318
|
+
// service and then deleting the team's line on the next sync.
|
|
319
|
+
if (parsed === null)
|
|
320
|
+
return false;
|
|
321
|
+
if (matcherFor(parsed, parsed.glob).test(dir))
|
|
322
|
+
return true;
|
|
323
|
+
const named = patternSegments(parsed.glob);
|
|
324
|
+
if (named.length === 0)
|
|
325
|
+
return false;
|
|
326
|
+
// The named directory, or an ancestor of it, matched on a segment boundary —
|
|
327
|
+
// never mid-segment, which is the whole difference between this arm and the
|
|
328
|
+
// prefix one above.
|
|
329
|
+
return new RegExp(`${matcherFor(parsed, named.join("/")).source}(?:/|$)`).test(dir);
|
|
330
|
+
}
|
|
331
|
+
/**
|
|
332
|
+
* The DIRECTORY an entry names, `/`-joined, or null when the entry does not
|
|
333
|
+
* plainly name one — a wildcard segment between two literal ones, or the
|
|
334
|
+
* leading doubled star of the `node_modules` glob.
|
|
335
|
+
*
|
|
336
|
+
* The trailing wildcards are what get stripped: `services/pay` doubled-star and
|
|
337
|
+
* the three other spellings `patternSegments` lists all name `services/pay`. It
|
|
338
|
+
* is the AUTHORSHIP question the writer asks — "did loam write this line, and is
|
|
339
|
+
* it about a directory the enumeration returns?" — never the covering one, which
|
|
340
|
+
* is `excludingEntry`'s.
|
|
341
|
+
*/
|
|
342
|
+
export function excludedDirectory(entry) {
|
|
343
|
+
const parsed = rendererEntry(entry);
|
|
344
|
+
// An entry that hides nothing names nothing either, so a backslash spelling is
|
|
345
|
+
// never loam's to rewrite: `subsystem sync` leaves it exactly where the team
|
|
346
|
+
// put it, which is the only honest answer about a line the renderer ignores.
|
|
347
|
+
if (parsed === null)
|
|
348
|
+
return null;
|
|
349
|
+
const parts = patternSegments(parsed.glob);
|
|
350
|
+
return parts.some((part) => part.includes("*")) ? null : parts.join("/");
|
|
351
|
+
}
|
|
352
|
+
//# sourceMappingURL=exclude.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"exclude.js","sourceRoot":"","sources":["../../../../src/core/c4/root-project/exclude.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * The ONE literal fact loam reads out of the root `likec4.config.json`: which\n * paths that file excludes from the renderer's root project, and whether a\n * given directory or document is one of them.\n *\n * Until this module existed loam read NOTHING out of that file. It wrote it\n * once (`core/docs.ts`, `loam init --create`), never re-read it, and the team\n * owned it from then on — the ownership rule `core/repo/tree/render/projects.ts`\n * still states for the per-service files. That rule is kept everywhere it still\n * applies; this is the one exception, and it is earned by what report #05\n * measured: a model that EXTENDS the fleet map lives in the ROOT project, so\n * the root's `exclude` decides whether the renderer can see it at all. A repo\n * whose root config still says `services/**` — every repo scaffolded before\n * this axis — renders such a model as a box with nothing inside it, and no\n * check could say why, because nothing here had ever opened the file.\n *\n * READ here, and written back by one command only (`loam subsystem sync`,\n * through `standaloneExclude` in `../service-model/renderer.ts`). Nothing else\n * about the file is interpreted: `exclude` is a glob list in a tool's own\n * config, and the entries loam does not recognise as naming a directory are\n * left exactly where the team put them.\n *\n * IT LIVES BELOW `../project/` BECAUSE THE PROJECT LOADER IS A READER. The\n * architecture document set is the one list every reader shares, and it has to\n * drop a `.likec4` the root project excludes — a palette under an excluded path\n * otherwise yields a `global style` reference the fleet project cannot resolve,\n * `likec4 validate` Invalid on every run and `loam validate --all` at 0 errors\n * (verification 2026-09-04, W5). `../service-model/` already imports\n * `../project/`, so this could not stay there without reversing that edge.\n */\nimport { readFile } from \"node:fs/promises\";\nimport { isRecord } from \"../../kernel/records.js\";\nimport type { DocsDir } from \"../../kernel/ids/dirs.js\";\nimport { rootProjectPath } from \"../../repo/paths.js\";\n\n/** The `services/` prefix every tree path is spelled under, in the config and on disk alike. */\nconst SERVICES = \"services\";\n\n/**\n * The root project's `exclude` list, or null when the file cannot be read as a\n * project config at all.\n *\n * The three answers are deliberately distinct, because two of them mean\n * opposite things to a grader:\n *\n * - `[]` — the file is a JSON object that excludes nothing (or names no\n * `exclude` key at all, which is the same thing to the renderer). Every\n * extending model is visible; a standalone one is in the root project where\n * it cannot parse.\n * - a list — the entries as written, in file order.\n * - `null` — absent, unreadable, not JSON, or JSON whose `exclude` is not a\n * list of strings. loam does not know what the renderer will do, so it says\n * nothing rather than guessing: the findings that read this list are silent\n * on null, `subsystem sync` leaves such a file alone, and the project loader\n * filters nothing. Failing closed here means the team keeps a file loam\n * cannot parse; failing open would mean loam asserting the renderer excludes\n * something on evidence it does not have.\n */\nexport async function readRootExclude(docsDir: DocsDir): Promise<string[] | null> {\n const parsed = await readRootProject(docsDir);\n if (parsed === null) return null;\n const exclude = parsed[\"exclude\"];\n if (exclude === undefined) return [];\n if (!Array.isArray(exclude)) return null;\n // A predicate rather than a cast, so the narrowing below is the compiler's\n // conclusion from a check that actually ran: an `exclude` holding a number is\n // a file loam does not understand, and understanding it half way is how a\n // grader ends up asserting a glob nobody wrote.\n const entries: unknown[] = exclude;\n return entries.every((entry): entry is string => typeof entry === \"string\") ? [...entries] : null;\n}\n\n/**\n * The root project file as a JSON object, or null when it is absent or is not\n * one — the ONE reader, shared with the one writer that re-parses it before a\n * rewrite (`commands/subsystem/txn/exclude.ts`).\n *\n * A LEADING BYTE-ORDER MARK IS STRIPPED, and that is not tidiness: PowerShell's\n * `Out-File` writes `ef bb bf` by default on this platform, so a Windows shell\n * saving the very file loam wrote left `JSON.parse` throwing while the renderer\n * — whose reader tolerates the mark — went on applying every entry in the list.\n * loam then graded nothing at all: `validate --all` silent about an exclusion in\n * full force, `doctor` healthy, `subsystem sync` reporting the list unreadable\n * and naming a fault (`not a JSON object with a string exclude list`) that was\n * not the one present (re-verification 2026-09-04, area C item 3). One reader\n * means the grade and the rewrite can never disagree about whether the file\n * parses.\n */\nasync function readRootProject(docsDir: DocsDir): Promise<Record<string, unknown> | null> {\n let text: string;\n try {\n text = await readFile(rootProjectPath(docsDir), \"utf8\");\n } catch {\n // Absent reads as \"loam cannot say\", not as \"excludes nothing\": a docs repo\n // with no root project file has no root project either, and the renderer\n // opened there shows nothing at all — a different problem, with a different\n // finding (`doctor.likec4-config-missing`).\n return null;\n }\n return parseRootProject(text);\n}\n\n/** The byte-order mark PowerShell's `Out-File` prepends by default on this platform. */\nconst BOM = \"\";\n\n/** The same parse, over bytes a caller already read. Exported for the rewrite path only. */\nexport function parseRootProject(text: string): Record<string, unknown> | null {\n let parsed: unknown;\n try {\n parsed = JSON.parse(text.startsWith(BOM) ? text.slice(BOM.length) : text);\n } catch {\n // Not JSON is the team's file to fix, and nothing here may rewrite it.\n return null;\n }\n return isRecord(parsed) ? parsed : null;\n}\n\n/** A PATH as segments — `/` or `\\`, either spelling, no empty parts, no `.`. Never an ENTRY: see `rendererEntry`. */\nfunction segments(path: string): string[] {\n return path.split(/[\\\\/]/).filter((s) => s.length > 0 && s !== \".\");\n}\n\n/**\n * One `exclude` entry as the RENDERER will match it — the glob with the\n * spellings that mean nothing to it taken out — or null when the entry can hide\n * nothing loam is able to spell.\n *\n * WHY IT EXISTS. Every matcher below used to build its expression from the entry\n * exactly as written while normalising only the PATH, so four spellings of one\n * line disagreed with the renderer in loam's favour: `./architecture/palette.likec4`,\n * `architecture/./palette.likec4` and `architecture//palette.likec4` each hid the\n * palette from `likec4 validate` (8 source files, down from 9) and from nothing\n * in loam — the census loaded it, `subsystem sync` wrote a `global style\n * subsystems` line naming a group the fleet project cannot resolve, and the run\n * reported 0 errors while every render was Invalid. `./architecture/**` did the\n * same to the whole map with `landscape.excluded` silent (re-verification\n * 2026-09-04, area C items 1-2).\n *\n * MEASURED, at the 1.59.2 pin, against the renderer's own matcher — likec4\n * builds `withoutDoubleSlashes(joinRelativeURL(<project folder>, entry))` (a\n * doubled star and a slash joined in front unless the entry already leads with\n * one or is spelled relative) and hands it to picomatch with `contains: true,\n * dot: true`. The rules that fall out, each pinned in\n * `test/root-exclude.test.ts` against a `npx likec4 validate .` file count:\n *\n * - a leading `./` is resolved, and it ANCHORS the entry at the docs root —\n * the any-directories prefix is not added, so `./architecture/**` is about that one\n * directory and `architecture/**` is about one at any depth;\n * - `/./` and `//` inside the entry collapse (`architecture//palette.likec4`\n * hides the palette exactly as the plain spelling does);\n * - a TRAILING `/` does not: `architecture` hides `architecture-old/old.likec4`\n * as a raw prefix and `architecture/` does not (measured 5 files vs 6), so\n * the slash is kept rather than stripped;\n * - `.//` keeps a literal `.` directory in the pattern and matches NOTHING\n * (measured: 10 files of 10);\n * - a BACKSLASH is a literal character to picomatch, never a separator, so\n * `services\\order-service\\**` hides nothing at all (measured: 8 files of 8)\n * — loam used to read it as covering the tree, warn `service.model-excluded`\n * about a service the renderer loads perfectly well, and then DELETE the\n * team's line on the next `subsystem sync`.\n *\n * The two arms that answer null are the entries loam declines to reason about:\n * a backslash-bearing one (it covers nothing, so nothing may be graded on it)\n * and one whose `..` walks above the docs root, where the renderer's pattern\n * stops describing this tree at all.\n */\ninterface RendererEntry {\n /** The glob, `/`-separated, `.` segments resolved and `//` collapsed, any trailing `/` kept. */\n glob: string;\n /** True when a leading `./` anchored the entry at the docs root instead of at any depth. */\n anchored: boolean;\n}\n\nfunction rendererEntry(entry: string): RendererEntry | null {\n if (entry.includes(\"\\\\\")) return null;\n let rest = entry;\n let anchored = false;\n if (rest.startsWith(\"./\")) {\n rest = rest.slice(2);\n anchored = true;\n // `.//x` — the dot survives as a literal directory name, so the pattern\n // names a path nothing on disk has.\n if (rest.startsWith(\"/\")) return null;\n } else if (rest.startsWith(\"../\")) {\n return null;\n }\n const trailing = rest.endsWith(\"/\");\n const parts: string[] = [];\n for (const part of rest.split(\"/\")) {\n if (part.length === 0 || part === \".\") continue;\n if (part === \"..\") {\n if (parts.pop() === undefined) return null;\n continue;\n }\n parts.push(part);\n }\n // An entry that normalises to nothing at all is the project folder itself,\n // which `contains: true` makes a match for every document under it.\n const glob = parts.length === 0 ? \"**\" : `${parts.join(\"/\")}${trailing ? \"/\" : \"\"}`;\n return { glob, anchored: anchored && parts.length > 0 };\n}\n\n/** The expression one entry matches with, or null when it matches nothing — `rendererEntry` says which. */\nfunction matcherFor(parsed: RendererEntry, glob: string): RegExp {\n return new RegExp(`^${globSource(parsed.anchored ? glob : prefixed(glob))}`);\n}\n\n/**\n * An entry's segments with the trailing bare wildcards taken off — the question\n * \"which directory is this line ABOUT\", asked of the four spellings that name\n * one: `services/pay` doubled-star, that entry with a trailing `/*` after it,\n * `services/pay/*` and `services/pay/` all reduce to `services/pay`. A wildcard\n * that is NOT trailing survives — one segment of its own — because a starred\n * middle segment picks one directory level and no more.\n *\n * It is NOT the covering rule. What an entry HIDES is `entryRegex` below, and\n * the two answer differently on purpose: `services/pay/**` is about the\n * directory `services/pay`, while the bare `services/pay` hides\n * `services/payment-service` as well (measured — see below).\n *\n * It takes the NORMALISED glob, never the raw entry: an entry is split on `/`\n * alone, because a backslash is a literal to the renderer and splitting on it\n * here was what made `services\\order-service\\**` read as naming a directory it\n * hides nothing in.\n */\nfunction patternSegments(glob: string): string[] {\n const parts = glob.split(\"/\").filter((part) => part.length > 0);\n while (parts.length > 0 && (parts.at(-1) === \"*\" || parts.at(-1) === \"**\")) parts.pop();\n return parts;\n}\n\n/**\n * One `exclude` entry as the expression the RENDERER matches paths with:\n * anchored at the start of a path segment, and NOT anchored at the end.\n *\n * MEASURED, at the 1.59.2 pin, on a scratch fleet of five documents —\n * `architecture/landscape.likec4`, `architecture/usecases/uc.likec4`,\n * `services/payment-service/model.likec4`,\n * `services/payments/payment-service/model.likec4` and\n * `services/order-service/model.likec4` — counting the \"N files\" `npx likec4\n * validate .` reports with one entry in the list:\n *\n * services/zzz 5 services/pay 3\n * services/payment-service 4 a star as the middle segment 4\n * services/pay/** 5 architect 3\n * architecture/*.likec4 4 *.likec4 0\n * ervices 5 ces/payment-service 5\n * payment-service 3 services/** 2\n * services 2 services/payment-service/* hides a nested file too\n *\n * Four rules come out of that table, and every one of them contradicts what\n * this module asserted before (verification 2026-09-04, review C): the match is\n * a raw PREFIX with no end anchor (`services/pay` takes both payment trees,\n * `architect` takes all of `architecture/`); it must START on a `/` boundary\n * (`ervices` and `ces/payment-service` take nothing); `*` stays inside one\n * segment (`architecture/*.likec4` leaves `architecture/usecases/uc.likec4`);\n * and a glob form does NOT prefix-match (`services/pay/**` needs the literal\n * separator, so it takes nothing from `services/payment-service`).\n *\n * The shape falls out of what LikeC4 does with the list: an entry that does not\n * already begin with a doubled star is prefixed with one, joined onto the\n * project folder and handed to picomatch with `contains: true` — and `contains`\n * is exactly the missing end anchor. So the optional any-directories group\n * `globSource` emits for a leading doubled star IS that prefix, and the absent\n * `$` is that option.\n *\n * NULL when the entry hides nothing at all — `rendererEntry` names the two\n * spellings that do, and every caller has to skip such an entry rather than\n * treat it as covering: a warning derived from one sent a team after a line the\n * renderer ignores, and `subsystem sync` then deleted it.\n */\nfunction entryRegex(entry: string): RegExp | null {\n const parsed = rendererEntry(entry);\n return parsed === null ? null : matcherFor(parsed, parsed.glob);\n}\n\n/** An entry as LikeC4 spells it before matching: a doubled star in front, unless it has one. */\nfunction prefixed(entry: string): string {\n return entry.startsWith(\"**\") ? entry : `**/${entry}`;\n}\n\n/**\n * A glob as a regular-expression SOURCE. `**` crosses `/` and, followed by a\n * separator, may stand for no directory at all (a doubled star between two literals matches with no directory between them, which\n * is what makes a doubled-star-then-star entry cover a model directly under `services/`);\n * `*` and `?` never cross one.\n */\nfunction globSource(pattern: string): string {\n let source = \"\";\n let i = 0;\n while (i < pattern.length) {\n if (pattern.startsWith(\"**/\", i)) {\n source += \"(?:.*/)?\";\n i += 3;\n continue;\n }\n if (pattern.startsWith(\"**\", i)) {\n source += \".*\";\n i += 2;\n continue;\n }\n const c = pattern[i]!;\n if (c === \"*\") source += \"[^/]*\";\n else if (c === \"?\") source += \"[^/]\";\n else source += c.replace(/[.+^${}()|[\\]\\\\]/g, \"\\\\$&\");\n i += 1;\n }\n return source;\n}\n\n/**\n * The entry that hides `relPath` from the root project, or null when nothing in\n * the list does. `relPath` is a docs-relative FILE path, spelled either way\n * round the slash.\n *\n * Returned VERBATIM because every message that carries it asks its reader to\n * delete or narrow THAT line, and an entry re-spelled by loam is a line they\n * would go looking for and not find.\n */\nexport function excludingPath(exclude: readonly string[], relPath: string): string | null {\n const path = segments(relPath).join(\"/\");\n if (path.length === 0) return null;\n for (const entry of exclude) {\n if (entryRegex(entry)?.test(path) === true) return entry;\n }\n return null;\n}\n\n/**\n * The entry that hides a SERVICE TREE — the directory and everything under it —\n * or null. The tree's spelling is normalised here because the repository writes\n * it both ways: `serviceTreePath` includes the leading `services/` and the\n * entries `standaloneExclude` writes are built from the part under it, and\n * getting that wrong would silently answer \"not excluded\" for every service.\n *\n * A DIRECTORY IS NOT A FILE, and that is why this is not `excludingPath` on the\n * directory's own path. `services/payment-service/**` hides every document in\n * that tree while matching no prefix of the directory string itself — the\n * pattern wants the separator the directory path ends before. So a second arm\n * asks the other half: does the directory the entry NAMES (`patternSegments`,\n * the trailing wildcards off) contain this tree? Asking it of the named\n * directory rather than of the raw prefix is what keeps `services/pay/**` about\n * `services/pay` and away from `services/payment-service`, which is exactly\n * where the renderer leaves it.\n *\n * What this deliberately does NOT report is an entry that hides only PART of a\n * tree (`services/x/model`, say). The four findings that read this answer are\n * about a directory the renderer either loads or does not, and half of one is a\n * state `subsystem sync` has no entry to write for.\n */\nexport function excludingEntry(exclude: readonly string[], treePath: string): string | null {\n const tree = segments(treePath).join(\"/\");\n if (tree.length === 0) return null;\n const full = tree === SERVICES || tree.startsWith(`${SERVICES}/`) ? tree : `${SERVICES}/${tree}`;\n for (const entry of exclude) {\n if (coversTree(entry, full)) return entry;\n }\n return null;\n}\n\n/** Does one entry hide `dir` and everything under it? The two arms `excludingEntry` documents. */\nfunction coversTree(entry: string, dir: string): boolean {\n const parsed = rendererEntry(entry);\n // An entry that hides nothing covers nothing — the backslash spelling, which\n // loam used to read as covering the tree while the renderer loaded every file\n // in it (measured: 8 of 8), warning `service.model-excluded` about a healthy\n // service and then deleting the team's line on the next sync.\n if (parsed === null) return false;\n if (matcherFor(parsed, parsed.glob).test(dir)) return true;\n const named = patternSegments(parsed.glob);\n if (named.length === 0) return false;\n // The named directory, or an ancestor of it, matched on a segment boundary —\n // never mid-segment, which is the whole difference between this arm and the\n // prefix one above.\n return new RegExp(`${matcherFor(parsed, named.join(\"/\")).source}(?:/|$)`).test(dir);\n}\n\n/**\n * The DIRECTORY an entry names, `/`-joined, or null when the entry does not\n * plainly name one — a wildcard segment between two literal ones, or the\n * leading doubled star of the `node_modules` glob.\n *\n * The trailing wildcards are what get stripped: `services/pay` doubled-star and\n * the three other spellings `patternSegments` lists all name `services/pay`. It\n * is the AUTHORSHIP question the writer asks — \"did loam write this line, and is\n * it about a directory the enumeration returns?\" — never the covering one, which\n * is `excludingEntry`'s.\n */\nexport function excludedDirectory(entry: string): string | null {\n const parsed = rendererEntry(entry);\n // An entry that hides nothing names nothing either, so a backslash spelling is\n // never loam's to rewrite: `subsystem sync` leaves it exactly where the team\n // put it, which is the only honest answer about a line the renderer ignores.\n if (parsed === null) return null;\n const parts = patternSegments(parsed.glob);\n return parts.some((part) => part.includes(\"*\")) ? null : parts.join(\"/\");\n}\n"]}
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
import { architectureProjectDocuments } from "../project/architecture.js";
|
|
2
|
+
import { architectureDocuments } from "../project/documents.js";
|
|
3
|
+
/**
|
|
4
|
+
* The fleet map's project, plus one service model: the set an extending model
|
|
5
|
+
* is graded in.
|
|
6
|
+
*
|
|
7
|
+
* The model is appended rather than merged in through a walk, because it is
|
|
8
|
+
* named — not everything under `services/<tree>/` belongs in this project, and
|
|
9
|
+
* in particular a sibling use-case file does not: it is read separately (see
|
|
10
|
+
* below) so that a broken flow cannot blank the model that the whole service
|
|
11
|
+
* target is graded against.
|
|
12
|
+
*/
|
|
13
|
+
export async function modelProjectDocuments(docsDir, modelPath) {
|
|
14
|
+
const sets = await modelProjectSets(docsDir, [modelPath]);
|
|
15
|
+
// One set in, one set out — spelled as a flatMap rather than an index so the
|
|
16
|
+
// empty case is impossible rather than asserted away.
|
|
17
|
+
return sets.flatMap((set) => set.paths);
|
|
18
|
+
}
|
|
19
|
+
/**
|
|
20
|
+
* The same set for MANY models, with exactly ONE walk of `architecture/`
|
|
21
|
+
* between them.
|
|
22
|
+
*
|
|
23
|
+
* The fleet's prefetch stages one project per extending model, and asking the
|
|
24
|
+
* single-model form above per service would readdir the map's directory once
|
|
25
|
+
* per service — 56 walks of the same tree on the 56-service shape this axis was
|
|
26
|
+
* measured against, for one answer that cannot differ between them.
|
|
27
|
+
*/
|
|
28
|
+
export async function modelProjectSets(docsDir, models) {
|
|
29
|
+
const architecture = await architectureProjectDocuments(docsDir);
|
|
30
|
+
return models.map((model) => ({ model, paths: [...architecture, model] }));
|
|
31
|
+
}
|
|
32
|
+
/**
|
|
33
|
+
* The same set plus every OTHER `.likec4` under the service's own directory —
|
|
34
|
+
* the project a service's use cases are declared in.
|
|
35
|
+
*
|
|
36
|
+
* `architectureDocuments` is the walk in both cases (it is "every `.likec4`
|
|
37
|
+
* under a directory, sorted, minus a skip list", and the name is about where it
|
|
38
|
+
* first ran rather than about `architecture/`), skipping the model so the
|
|
39
|
+
* concatenation cannot list it twice: a document staged twice at one relative
|
|
40
|
+
* path is one file, but the second copy would overwrite the first and the
|
|
41
|
+
* ordering would decide which — a coin flip loam must not build on.
|
|
42
|
+
*/
|
|
43
|
+
export async function serviceFlowDocuments(docsDir, paths) {
|
|
44
|
+
const siblings = await architectureDocuments(paths.dir, [paths.model]);
|
|
45
|
+
return [...(await modelProjectDocuments(docsDir, paths.model)), ...siblings];
|
|
46
|
+
}
|
|
47
|
+
//# sourceMappingURL=documents.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"documents.js","sourceRoot":"","sources":["../../../../src/core/c4/service-model/documents.ts"],"names":[],"mappings":"AA6BA,OAAO,EAAE,4BAA4B,EAAE,MAAM,4BAA4B,CAAC;AAC1E,OAAO,EAAE,qBAAqB,EAAE,MAAM,yBAAyB,CAAC;AAEhE;;;;;;;;;GASG;AACH,MAAM,CAAC,KAAK,UAAU,qBAAqB,CAAC,OAAgB,EAAE,SAAiB;IAC7E,MAAM,IAAI,GAAG,MAAM,gBAAgB,CAAC,OAAO,EAAE,CAAC,SAAS,CAAC,CAAC,CAAC;IAC1D,6EAA6E;IAC7E,sDAAsD;IACtD,OAAO,IAAI,CAAC,OAAO,CAAC,CAAC,GAAG,EAAE,EAAE,CAAC,GAAG,CAAC,KAAK,CAAC,CAAC;AAC1C,CAAC;AAED;;;;;;;;GAQG;AACH,MAAM,CAAC,KAAK,UAAU,gBAAgB,CACpC,OAAgB,EAChB,MAAyB;IAEzB,MAAM,YAAY,GAAG,MAAM,4BAA4B,CAAC,OAAO,CAAC,CAAC;IACjE,OAAO,MAAM,CAAC,GAAG,CAAC,CAAC,KAAK,EAAE,EAAE,CAAC,CAAC,EAAE,KAAK,EAAE,KAAK,EAAE,CAAC,GAAG,YAAY,EAAE,KAAK,CAAC,EAAE,CAAC,CAAC,CAAC;AAC7E,CAAC;AAED;;;;;;;;;;GAUG;AACH,MAAM,CAAC,KAAK,UAAU,oBAAoB,CAAC,OAAgB,EAAE,KAAmB;IAC9E,MAAM,QAAQ,GAAG,MAAM,qBAAqB,CAAC,KAAK,CAAC,GAAG,EAAE,CAAC,KAAK,CAAC,KAAK,CAAC,CAAC,CAAC;IACvE,OAAO,CAAC,GAAG,CAAC,MAAM,qBAAqB,CAAC,OAAO,EAAE,KAAK,CAAC,KAAK,CAAC,CAAC,EAAE,GAAG,QAAQ,CAAC,CAAC;AAC/E,CAAC","sourcesContent":["/**\n * Which documents a service model is parsed WITH.\n *\n * An extending model declares no element kinds and names the map's elements\n * directly (`extend marketplace.orderService { … }`, an edge to\n * `kafka.orderEvents`), so the set it parses in is not a directory — it is the\n * fleet map's project plus this one file. Measured at the 1.59.2 pin: staged\n * that way the model parses, its containers land under the landscape's element,\n * and every parse error is attributed to `services/<svc>/model.likec4` with a\n * line of its own — including a missing kind, which is ONE error on the model\n * rather than a cascade on the map.\n *\n * Two sets, because two questions are asked of a model. The first is what the\n * model itself says (`modelProjectDocuments`); the second adds every `.likec4`\n * sitting beside it, which is how a service's own use cases are read — a flow\n * file declares views over the model's containers and does not parse alone, the\n * same reason `../project/load.ts` exists at fleet altitude.\n *\n * A CONTRACT ON THE CALLER, since neither function stages anything itself:\n * both sets must be loaded with `base` = the DOCS ROOT, never `architecture/`\n * or the service directory. LikeC4 reports a view's `sourcePath` relative to\n * the project root, so mirroring from the docs root is what makes a view come\n * back spelled `services/<tree>/model.likec4` — the spelling `./load.ts`\n * filters the model's own views by, and the spelling a person reads in a\n * finding. Staged from anywhere else the filter matches nothing and every\n * service reports that it declares no views.\n */\nimport type { DocsDir } from \"../../kernel/ids/dirs.js\";\nimport type { ServicePaths } from \"../../repo/paths.js\";\nimport { architectureProjectDocuments } from \"../project/architecture.js\";\nimport { architectureDocuments } from \"../project/documents.js\";\n\n/**\n * The fleet map's project, plus one service model: the set an extending model\n * is graded in.\n *\n * The model is appended rather than merged in through a walk, because it is\n * named — not everything under `services/<tree>/` belongs in this project, and\n * in particular a sibling use-case file does not: it is read separately (see\n * below) so that a broken flow cannot blank the model that the whole service\n * target is graded against.\n */\nexport async function modelProjectDocuments(docsDir: DocsDir, modelPath: string): Promise<string[]> {\n const sets = await modelProjectSets(docsDir, [modelPath]);\n // One set in, one set out — spelled as a flatMap rather than an index so the\n // empty case is impossible rather than asserted away.\n return sets.flatMap((set) => set.paths);\n}\n\n/**\n * The same set for MANY models, with exactly ONE walk of `architecture/`\n * between them.\n *\n * The fleet's prefetch stages one project per extending model, and asking the\n * single-model form above per service would readdir the map's directory once\n * per service — 56 walks of the same tree on the 56-service shape this axis was\n * measured against, for one answer that cannot differ between them.\n */\nexport async function modelProjectSets(\n docsDir: DocsDir,\n models: readonly string[],\n): Promise<{ model: string; paths: string[] }[]> {\n const architecture = await architectureProjectDocuments(docsDir);\n return models.map((model) => ({ model, paths: [...architecture, model] }));\n}\n\n/**\n * The same set plus every OTHER `.likec4` under the service's own directory —\n * the project a service's use cases are declared in.\n *\n * `architectureDocuments` is the walk in both cases (it is \"every `.likec4`\n * under a directory, sorted, minus a skip list\", and the name is about where it\n * first ran rather than about `architecture/`), skipping the model so the\n * concatenation cannot list it twice: a document staged twice at one relative\n * path is one file, but the second copy would overwrite the first and the\n * ordering would decide which — a coin flip loam must not build on.\n */\nexport async function serviceFlowDocuments(docsDir: DocsDir, paths: ServicePaths): Promise<string[]> {\n const siblings = await architectureDocuments(paths.dir, [paths.model]);\n return [...(await modelProjectDocuments(docsDir, paths.model)), ...siblings];\n}\n"]}
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Every service whose `model.likec4` EXTENDS the fleet map, with its living
|
|
3
|
+
* source in hand — the list the landscape merge routes a nested addition
|
|
4
|
+
* through.
|
|
5
|
+
*
|
|
6
|
+
* ## Why it is here and not beside the archive
|
|
7
|
+
*
|
|
8
|
+
* It was `commands/archive/plan/model/extending.ts`'s, beside the proof that
|
|
9
|
+
* the merged model parses, and for one merge that was the right seam: reading
|
|
10
|
+
* the fleet and deciding to write belong to the command, `core/c4/splice/` is a
|
|
11
|
+
* pure text-to-text computation, and nothing else needed the list.
|
|
12
|
+
*
|
|
13
|
+
* Then the archive stopped being the only reader. `../../project/staged.ts`
|
|
14
|
+
* previews the map a feature's own merge would leave, and every feature-side
|
|
15
|
+
* gate rests on that preview — `usecase.flow-invalid` and
|
|
16
|
+
* `deployment.doc-invalid` both refuse on it, and `--approve` reaches neither.
|
|
17
|
+
* A preview computed without this list previews the WRONG map: it holds
|
|
18
|
+
* interior the archive routes into `services/<…>/model.likec4`, so a
|
|
19
|
+
* feature-local flow hopping to `<service-fqn>.<container>` resolved against it,
|
|
20
|
+
* passed the gate, and was copied verbatim into `architecture/usecases/`, where
|
|
21
|
+
* it resolves against nothing (verification 2026-09-04, review finding F8;
|
|
22
|
+
* measured: `loam archive --approve` at exit 0, then `loam validate --all` with
|
|
23
|
+
* one `landscape.invalid` and one `spine.landscape-invalid` per service).
|
|
24
|
+
*
|
|
25
|
+
* A reader in `commands/` cannot close that: `core/` may never import
|
|
26
|
+
* `commands/`. Nor could it live in `../../project/` beside the staging that
|
|
27
|
+
* needs it — the shape scan is `../shape.ts`, and `../` imports
|
|
28
|
+
* `../../project/`, so the edge would be a package cycle
|
|
29
|
+
* (`node scripts/package-graph.mjs` refuses it). So it sits one level BELOW the
|
|
30
|
+
* shape scan it calls, in a package that nothing in `core/c4/` imports back:
|
|
31
|
+
* `core/delta/`, `core/coherence/` and the archive all reach it, and the graph
|
|
32
|
+
* stays acyclic.
|
|
33
|
+
*
|
|
34
|
+
* ## The shape decision is not this file's
|
|
35
|
+
*
|
|
36
|
+
* `../shape.ts` owns it: a model declaring an element kind is STANDALONE, is
|
|
37
|
+
* parsed alone, and owns no share of the merge — the map draws its containers
|
|
38
|
+
* exactly as it did before the extending shape existed. A model that cannot be
|
|
39
|
+
* read is not in the list either, which is the same conservative arm the shape
|
|
40
|
+
* reader takes.
|
|
41
|
+
*/
|
|
42
|
+
import { readFile } from "node:fs/promises";
|
|
43
|
+
import { resolve } from "node:path";
|
|
44
|
+
import { inOrder } from "../../../kernel/concurrency.js";
|
|
45
|
+
import { repoPath } from "../../../envelope/json.js";
|
|
46
|
+
import { servicePathsAt } from "../../../repo/paths.js";
|
|
47
|
+
import { enumeratedServices } from "../../../repo/service-target.js";
|
|
48
|
+
import { readModelShapes } from "../shape.js";
|
|
49
|
+
/**
|
|
50
|
+
* The fleet's extending models, ordered by the enumeration.
|
|
51
|
+
*
|
|
52
|
+
* `fleet` is OPTIONAL for the reason `enumeratedServices` makes it optional:
|
|
53
|
+
* the callers are three, and only two of them run inside an invocation index.
|
|
54
|
+
* With one, the enumeration and the shape scan are memo hits and this costs one
|
|
55
|
+
* small read per extending model; without one, the same bulk scan runs directly
|
|
56
|
+
* (`readModelShapes`) and the answer is identical. What must never differ
|
|
57
|
+
* between the two is the LIST — a gate that routed differently depending on
|
|
58
|
+
* whether its caller happened to hold a read index would be a gate nobody could
|
|
59
|
+
* reproduce.
|
|
60
|
+
*/
|
|
61
|
+
export async function extendingModels(docsDir, fleet) {
|
|
62
|
+
const services = await enumeratedServices(docsDir, fleet);
|
|
63
|
+
if (services.length === 0)
|
|
64
|
+
return [];
|
|
65
|
+
const paths = services.map((entry) => servicePathsAt(entry.dir).model);
|
|
66
|
+
const shapes = fleet === undefined ? await readModelShapes(paths) : await fleet.modelShapes(paths);
|
|
67
|
+
const found = await inOrder(services, async (entry) => {
|
|
68
|
+
const path = servicePathsAt(entry.dir).model;
|
|
69
|
+
if (shapes.get(resolve(path)) !== "extending")
|
|
70
|
+
return null;
|
|
71
|
+
try {
|
|
72
|
+
return { service: entry.id, path: repoPath(docsDir, path), text: await readFile(path, "utf8") };
|
|
73
|
+
}
|
|
74
|
+
catch {
|
|
75
|
+
// Unreadable is not extending: the merge would be splicing into bytes it
|
|
76
|
+
// never saw, and the map's arm is the honest fallback.
|
|
77
|
+
return null;
|
|
78
|
+
}
|
|
79
|
+
});
|
|
80
|
+
return found.filter((model) => model !== null);
|
|
81
|
+
}
|
|
82
|
+
//# sourceMappingURL=extending.js.map
|