@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":"writes.js","sourceRoot":"","sources":["../../../src/core/staging/writes.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;GAUG;AACH,OAAO,EAAE,MAAM,EAAE,MAAM,aAAa,CAAC;AACrC,OAAO,EAAE,UAAU,EAAE,MAAM,aAAa,CAAC;AACzC,OAAO,EAAE,UAAU,EAAE,MAAM,SAAS,CAAC;AACrC,OAAO,EAAE,QAAQ,EAAE,MAAM,kBAAkB,CAAC;AA8B5C,mDAAmD;AACnD,MAAM,UAAU,OAAO,CAAC,OAA+B;IACrD,OAAO,OAAO,OAAO,KAAK,QAAQ,CAAC,CAAC,CAAC,MAAM,CAAC,IAAI,CAAC,OAAO,EAAE,MAAM,CAAC,CAAC,CAAC,CAAC,OAAO,CAAC;AAC9E,CAAC;AAED;;;;;;;;;;;;;;GAcG;AACH,MAAM,UAAU,SAAS,CAAC,IAAY,EAAE,OAA+B;IACrE,IAAI,OAAO,KAAK,IAAI,IAAI,CAAC,UAAU,CAAC,IAAI,CAAC;QAAE,OAAO,EAAE,IAAI,EAAE,OAAO,EAAE,SAAS,EAAE,IAAI,EAAE,CAAC;IACrF,OAAO,EAAE,IAAI,EAAE,OAAO,EAAE,CAAC;AAC3B,CAAC;AAsBD;;;;;;;;;;;;;;GAcG;AACH,MAAM,OAAO,YAAa,SAAQ,KAAK;IAI1B,IAAI;IAHG,IAAI,GAAG,cAAc,CAAC;IAExC,YACW,IAAY,EACrB,MAAc;QAEd,KAAK,CAAC,GAAG,IAAI,IAAI,MAAM,yDAAyD,CAAC,CAAC;oBAHzE,IAAI;IAIf,CAAC;CACF;AAED;;;;;;;;;;;;;;;GAeG;AACH,SAAS,UAAU,CAAC,KAAa,EAAE,IAAY;IAC7C,IAAI,CAAC,MAAM,CAAC,KAAK,CAAC,EAAE,CAAC;QACnB,MAAM,IAAI,YAAY,CACpB,IAAI,EACJ,mGAAmG;YACjG,gFAAgF,CACnF,CAAC;IACJ,CAAC;IACD,MAAM,IAAI,GAAG,KAAK,CAAC,QAAQ,CAAC,MAAM,CAAC,CAAC;IACpC,IAAI,IAAI,CAAC,QAAQ,CAAC,QAAQ,CAAC,EAAE,CAAC;QAC5B,MAAM,IAAI,YAAY,CACpB,IAAI,EACJ,2GAA2G;YACzG,iGAAiG;YACjG,yGAAyG;YACzG,0DAA0D,CAC7D,CAAC;IACJ,CAAC;IACD,OAAO,IAAI,CAAC;AACd,CAAC;AAED,oGAAoG;AACpG,MAAM,CAAC,KAAK,UAAU,QAAQ,CAAC,IAAY;IACzC,OAAO,UAAU,CAAC,MAAM,QAAQ,CAAC,IAAI,CAAC,EAAE,IAAI,CAAC,CAAC;AAChD,CAAC;AAED,6FAA6F;AAC7F,MAAM,UAAU,SAAS,CAAC,CAAgB,EAAE,CAAgB;IAC1D,OAAO,CAAC,KAAK,IAAI,IAAI,CAAC,KAAK,IAAI,CAAC,CAAC,CAAC,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,MAAM,CAAC,CAAC,CAAC,CAAC;AAC1D,CAAC;AAED,wEAAwE;AACxE,yEAAyE;AACzE,wEAAwE;AAExE,qFAAqF;AACrF,MAAM,UAAU,QAAQ,CAAC,KAAc;IACrC,OAAO,OAAO,KAAK,KAAK,QAAQ,IAAI,gBAAgB,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC;AACnE,CAAC;AAED,MAAM,UAAU,MAAM,CAAC,OAAwB;IAC7C,OAAO,OAAO,OAAO,KAAK,QAAQ;QAChC,CAAC,CAAC,UAAU,CAAC,QAAQ,CAAC,CAAC,MAAM,CAAC,OAAO,EAAE,MAAM,CAAC,CAAC,MAAM,CAAC,KAAK,CAAC;QAC5D,CAAC,CAAC,UAAU,CAAC,QAAQ,CAAC,CAAC,MAAM,CAAC,OAAO,CAAC,CAAC,MAAM,CAAC,KAAK,CAAC,CAAC;AACzD,CAAC","sourcesContent":["/**\n * What a write IS before anything touches disk: the planned record, the staged\n * record, and the bytes both are made of.\n *\n * This is the package's floor. Everything else here — the lock, the swap, the\n * snapshot, the recovery journal — is a rule about WHEN bytes may land; only\n * this module decides what they are. Decoding lives here for the same reason:\n * text is produced only where a parser needs it and `readUtf8` is the one door,\n * so no later addition can reach a docs file through `readFile(…, \"utf8\")` and\n * silently substitute U+FFFD for bytes loam did not understand.\n */\nimport { isUtf8 } from \"node:buffer\";\nimport { createHash } from \"node:crypto\";\nimport { existsSync } from \"node:fs\";\nimport { readFile } from \"node:fs/promises\";\n\n/**\n * A planned file write — the merge is computed fully before anything touches\n * disk. `content: null` means \"delete this file\"; only `unarchive` plans those,\n * to take back a file the archive created.\n *\n * BYTES, not text. The whole write path — pre-image, staged content, snapshot,\n * the compare-and-set before each swap — moves Buffers, because a docs repo is\n * not guaranteed to hold text loam can decode: `Buffer.toString(\"utf8\")` never\n * fails, it substitutes U+FFFD, so reading a file as a string and writing the\n * string back is a silent, permanent rewrite of every byte loam did not\n * understand. It reached the snapshot too, so `unarchive` restored the damage\n * and reported success. Text is now produced only where a PARSER needs it, by\n * `decodeUtf8`, which refuses instead of substituting.\n */\nexport interface PlannedWrite {\n path: string;\n /**\n * A `string` is accepted for the callers whose new version IS text they\n * composed from nothing (a re-pinned delta, a restamped spec) and is encoded\n * as UTF-8 once, at staging. A caller holding BYTES — anything derived from a\n * file loam read — must hand over the Buffer: the round trip through a string\n * is the corruption above.\n */\n content: string | Buffer | null;\n /** Create the target atomically without replacing a concurrently created file. */\n exclusive?: boolean;\n}\n\n/** The one place a planned write becomes bytes. */\nexport function toBytes(content: string | Buffer | null): Buffer | null {\n return typeof content === \"string\" ? Buffer.from(content, \"utf8\") : content;\n}\n\n/**\n * The one place that decides whether a planned write is a CREATE *from the\n * filesystem*. (`core/verify/store/commit.ts` is the deliberate second rule:\n * it decides from the caller's held pre-image instead, because a CAS must\n * follow the read it promised, not what `existsSync` says now.)\n *\n * `exclusive` is a no-clobber create (link(2)), so it can only be asked of a\n * path that does not exist yet: asking it of an overwrite would fail EEXIST on\n * every single run. Deciding here — rather than at each of the dozen\n * `writes.push` sites in archive and unarchive — is what keeps the two commands\n * from disagreeing about which of their writes may safely lose a race. An\n * OVERWRITE is not left unguarded: `swapStaged` compares the target against the\n * pre-image immediately before the rename, so a file another writer changed\n * under us stops the commit instead of silently burying their bytes.\n */\nexport function planWrite(path: string, content: string | Buffer | null): PlannedWrite {\n if (content !== null && !existsSync(path)) return { path, content, exclusive: true };\n return { path, content };\n}\n\n/** A planned write with its new bytes parked next to the target, ready to swap in. */\nexport interface StagedWrite {\n write: PlannedWrite;\n /** What this write puts on disk — `write.content` encoded once. Null for a delete. */\n content: Buffer | null;\n /** Temp file holding the new bytes, in the target's OWN directory. Null for a delete. */\n tmp: string | null;\n /** The target's bytes before the swap; null when it did not exist. */\n before: Buffer | null;\n /**\n * Topmost directory this write had to create, if any — a rollback owes the repo\n * its directories back too. An empty `services/<svc>/` left standing is the\n * fleet claiming a service that was never merged.\n */\n createdDir: string | null;\n swapped: boolean;\n}\n\n\n\n/**\n * A file loam must PARSE that does not decode as the UTF-8 text loam owns.\n *\n * Commands map this to `merge-failed`: nothing was written, the file is named,\n * and re-saving it as UTF-8 makes the command work. The read-only migration\n * ingest already refuses non-UTF-8 input outright (core/openspec/) —\n * the destructive path has strictly more to lose, so it refuses the same way\n * rather than substituting U+FFFD and rewriting the file over the author.\n *\n * `reason` says what the bytes actually ARE, because the two ways a document\n * fails this test destroy different things: one substitutes bytes, the other\n * erases the whole document's meaning. A reader told the wrong one goes looking\n * in the wrong place. The advice that follows is the same either way, so it is\n * spelled once, here.\n */\nexport class NotUtf8Error extends Error {\n override readonly name = \"NotUtf8Error\";\n\n constructor(\n readonly path: string,\n reason: string,\n ) {\n super(`${path} ${reason} Nothing was written. Re-save it as UTF-8, then re-run.`);\n }\n}\n\n/**\n * Decode bytes a parser is about to be handed, or refuse naming the file.\n *\n * Two tests, the same two the read path makes (document-bytes.ts's\n * `decodeDocument`), because this side has strictly more to lose by missing\n * either. `isUtf8` catches the byte sequences that cannot be UTF-8 at all —\n * which is what a byte-order mark makes UTF-16 into. Without a BOM, UTF-16LE of\n * ASCII is a sequence of perfectly VALID UTF-8 bytes: every other byte is NUL,\n * so `isUtf8` says yes, the file decodes with a U+0000 between every character,\n * and every parser loam owns reads its requirements, frontmatter and headings\n * as absent rather than as unreadable. On the read path that is a document\n * graded green while nothing in it was checked; here it is worse, because\n * `archive` merges against that empty baseline and writes the result back — the\n * author's requirements are not mis-read, they are gone, and the undo snapshot\n * records the same emptiness as the pre-image.\n */\nfunction decodeUtf8(bytes: Buffer, path: string): string {\n if (!isUtf8(bytes)) {\n throw new NotUtf8Error(\n path,\n `is not valid UTF-8 — loam parses this file as text and writes it back, which would replace every ` +\n `undecodable byte with U+FFFD, in the living document AND in the undo snapshot.`,\n );\n }\n const text = bytes.toString(\"utf8\");\n if (text.includes(\"\\u0000\")) {\n throw new NotUtf8Error(\n path,\n `holds NUL characters, which no document loam owns does — almost always UTF-16 saved without a byte-order ` +\n `mark. Its bytes are valid UTF-8, so it decodes without complaint into text whose requirements, ` +\n `frontmatter and headings all parse as ABSENT: the merge would compute over an empty baseline and write ` +\n `that back, taking every requirement in the file with it.`,\n );\n }\n return text;\n}\n\n/** Read a file loam is going to parse. The only supported way to turn a docs file into a string. */\nexport async function readUtf8(path: string): Promise<string> {\n return decodeUtf8(await readFile(path), path);\n}\n\n/** Same bytes? Absence is its own value: a file that is gone is not a file that is empty. */\nexport function sameBytes(a: Buffer | null, b: Buffer | null): boolean {\n return a === null || b === null ? a === b : a.equals(b);\n}\n\n/* ------------------------------------------------------------------ */\n/* Reading loam's own records back off the disk */\n/* ------------------------------------------------------------------ */\n\n/** The one spelling of a sha256 loam wrote, for reading one back out of a record. */\nexport function isDigest(value: unknown): value is string {\n return typeof value === \"string\" && /^[0-9a-f]{64}$/.test(value);\n}\n\nexport function sha256(content: Buffer | string): string {\n return typeof content === \"string\"\n ? createHash(\"sha256\").update(content, \"utf8\").digest(\"hex\")\n : createHash(\"sha256\").update(content).digest(\"hex\");\n}\n\n"]}
|
|
1
|
+
{"version":3,"file":"writes.js","sourceRoot":"","sources":["../../../src/core/staging/writes.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;GAUG;AACH,OAAO,EAAE,MAAM,EAAE,MAAM,aAAa,CAAC;AACrC,OAAO,EAAE,UAAU,EAAE,MAAM,aAAa,CAAC;AACzC,OAAO,EAAE,UAAU,EAAE,MAAM,SAAS,CAAC;AACrC,OAAO,EAAE,QAAQ,EAAE,MAAM,kBAAkB,CAAC;AA+B5C,mDAAmD;AACnD,MAAM,UAAU,OAAO,CAAC,OAA+B;IACrD,OAAO,OAAO,OAAO,KAAK,QAAQ,CAAC,CAAC,CAAC,MAAM,CAAC,IAAI,CAAC,OAAO,EAAE,MAAM,CAAC,CAAC,CAAC,CAAC,OAAO,CAAC;AAC9E,CAAC;AAED;;;;;;;;;;;;;;GAcG;AACH,MAAM,UAAU,SAAS,CAAC,IAAY,EAAE,OAA+B;IACrE,IAAI,OAAO,KAAK,IAAI,IAAI,CAAC,UAAU,CAAC,IAAI,CAAC;QAAE,OAAO,EAAE,IAAI,EAAE,OAAO,EAAE,SAAS,EAAE,IAAI,EAAE,CAAC;IACrF,OAAO,EAAE,IAAI,EAAE,OAAO,EAAE,CAAC;AAC3B,CAAC;AAsBD;;;;;;;;;;;;;;GAcG;AACH,MAAM,OAAO,YAAa,SAAQ,KAAK;IAI1B,IAAI;IAHG,IAAI,GAAG,cAAc,CAAC;IAExC,YACW,IAAY,EACrB,MAAc;QAEd,KAAK,CAAC,GAAG,IAAI,IAAI,MAAM,yDAAyD,CAAC,CAAC;oBAHzE,IAAI;IAIf,CAAC;CACF;AAED;;;;;;;;;;;;;;;GAeG;AACH,SAAS,UAAU,CAAC,KAAa,EAAE,IAAY;IAC7C,IAAI,CAAC,MAAM,CAAC,KAAK,CAAC,EAAE,CAAC;QACnB,MAAM,IAAI,YAAY,CACpB,IAAI,EACJ,mGAAmG;YACjG,gFAAgF,CACnF,CAAC;IACJ,CAAC;IACD,MAAM,IAAI,GAAG,KAAK,CAAC,QAAQ,CAAC,MAAM,CAAC,CAAC;IACpC,IAAI,IAAI,CAAC,QAAQ,CAAC,QAAQ,CAAC,EAAE,CAAC;QAC5B,MAAM,IAAI,YAAY,CACpB,IAAI,EACJ,2GAA2G;YACzG,iGAAiG;YACjG,yGAAyG;YACzG,0DAA0D,CAC7D,CAAC;IACJ,CAAC;IACD,OAAO,IAAI,CAAC;AACd,CAAC;AAED,oGAAoG;AACpG,MAAM,CAAC,KAAK,UAAU,QAAQ,CAAC,IAAY;IACzC,OAAO,UAAU,CAAC,MAAM,QAAQ,CAAC,IAAI,CAAC,EAAE,IAAI,CAAC,CAAC;AAChD,CAAC;AAED,6FAA6F;AAC7F,MAAM,UAAU,SAAS,CAAC,CAAgB,EAAE,CAAgB;IAC1D,OAAO,CAAC,KAAK,IAAI,IAAI,CAAC,KAAK,IAAI,CAAC,CAAC,CAAC,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,MAAM,CAAC,CAAC,CAAC,CAAC;AAC1D,CAAC;AAED,wEAAwE;AACxE,yEAAyE;AACzE,wEAAwE;AAExE,qFAAqF;AACrF,MAAM,UAAU,QAAQ,CAAC,KAAc;IACrC,OAAO,OAAO,KAAK,KAAK,QAAQ,IAAI,gBAAgB,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC;AACnE,CAAC;AAED,MAAM,UAAU,MAAM,CAAC,OAAwB;IAC7C,OAAO,OAAO,OAAO,KAAK,QAAQ;QAChC,CAAC,CAAC,UAAU,CAAC,QAAQ,CAAC,CAAC,MAAM,CAAC,OAAO,EAAE,MAAM,CAAC,CAAC,MAAM,CAAC,KAAK,CAAC;QAC5D,CAAC,CAAC,UAAU,CAAC,QAAQ,CAAC,CAAC,MAAM,CAAC,OAAO,CAAC,CAAC,MAAM,CAAC,KAAK,CAAC,CAAC;AACzD,CAAC","sourcesContent":["/**\n * What a write IS before anything touches disk: the planned record, the staged\n * record, and the bytes both are made of.\n *\n * This is the package's floor. Everything else here — the lock, the swap, the\n * snapshot, the recovery journal — is a rule about WHEN bytes may land; only\n * this module decides what they are. Decoding lives here for the same reason:\n * text is produced only where a parser needs it and `readUtf8` is the one door,\n * so no later addition can reach a docs file through `readFile(…, \"utf8\")` and\n * silently substitute U+FFFD for bytes loam did not understand.\n */\nimport { isUtf8 } from \"node:buffer\";\nimport { createHash } from \"node:crypto\";\nimport { existsSync } from \"node:fs\";\nimport { readFile } from \"node:fs/promises\";\n\n/**\n * A planned file write — the merge is computed fully before anything touches\n * disk. `content: null` means \"delete this file\": `unarchive` takes back a file the\n * archive created, and `subsystem sync` removes the generated views file and a\n * stray per-service project file beside an extending model.\n *\n * BYTES, not text. The whole write path — pre-image, staged content, snapshot,\n * the compare-and-set before each swap — moves Buffers, because a docs repo is\n * not guaranteed to hold text loam can decode: `Buffer.toString(\"utf8\")` never\n * fails, it substitutes U+FFFD, so reading a file as a string and writing the\n * string back is a silent, permanent rewrite of every byte loam did not\n * understand. It reached the snapshot too, so `unarchive` restored the damage\n * and reported success. Text is now produced only where a PARSER needs it, by\n * `decodeUtf8`, which refuses instead of substituting.\n */\nexport interface PlannedWrite {\n path: string;\n /**\n * A `string` is accepted for the callers whose new version IS text they\n * composed from nothing (a re-pinned delta, a restamped spec) and is encoded\n * as UTF-8 once, at staging. A caller holding BYTES — anything derived from a\n * file loam read — must hand over the Buffer: the round trip through a string\n * is the corruption above.\n */\n content: string | Buffer | null;\n /** Create the target atomically without replacing a concurrently created file. */\n exclusive?: boolean;\n}\n\n/** The one place a planned write becomes bytes. */\nexport function toBytes(content: string | Buffer | null): Buffer | null {\n return typeof content === \"string\" ? Buffer.from(content, \"utf8\") : content;\n}\n\n/**\n * The one place that decides whether a planned write is a CREATE *from the\n * filesystem*. (`core/verify/store/commit.ts` is the deliberate second rule:\n * it decides from the caller's held pre-image instead, because a CAS must\n * follow the read it promised, not what `existsSync` says now.)\n *\n * `exclusive` is a no-clobber create (link(2)), so it can only be asked of a\n * path that does not exist yet: asking it of an overwrite would fail EEXIST on\n * every single run. Deciding here — rather than at each of the dozen\n * `writes.push` sites in archive and unarchive — is what keeps the two commands\n * from disagreeing about which of their writes may safely lose a race. An\n * OVERWRITE is not left unguarded: `swapStaged` compares the target against the\n * pre-image immediately before the rename, so a file another writer changed\n * under us stops the commit instead of silently burying their bytes.\n */\nexport function planWrite(path: string, content: string | Buffer | null): PlannedWrite {\n if (content !== null && !existsSync(path)) return { path, content, exclusive: true };\n return { path, content };\n}\n\n/** A planned write with its new bytes parked next to the target, ready to swap in. */\nexport interface StagedWrite {\n write: PlannedWrite;\n /** What this write puts on disk — `write.content` encoded once. Null for a delete. */\n content: Buffer | null;\n /** Temp file holding the new bytes, in the target's OWN directory. Null for a delete. */\n tmp: string | null;\n /** The target's bytes before the swap; null when it did not exist. */\n before: Buffer | null;\n /**\n * Topmost directory this write had to create, if any — a rollback owes the repo\n * its directories back too. An empty `services/<svc>/` left standing is the\n * fleet claiming a service that was never merged.\n */\n createdDir: string | null;\n swapped: boolean;\n}\n\n\n\n/**\n * A file loam must PARSE that does not decode as the UTF-8 text loam owns.\n *\n * Commands map this to `merge-failed`: nothing was written, the file is named,\n * and re-saving it as UTF-8 makes the command work. The read-only migration\n * ingest already refuses non-UTF-8 input outright (core/openspec/) —\n * the destructive path has strictly more to lose, so it refuses the same way\n * rather than substituting U+FFFD and rewriting the file over the author.\n *\n * `reason` says what the bytes actually ARE, because the two ways a document\n * fails this test destroy different things: one substitutes bytes, the other\n * erases the whole document's meaning. A reader told the wrong one goes looking\n * in the wrong place. The advice that follows is the same either way, so it is\n * spelled once, here.\n */\nexport class NotUtf8Error extends Error {\n override readonly name = \"NotUtf8Error\";\n\n constructor(\n readonly path: string,\n reason: string,\n ) {\n super(`${path} ${reason} Nothing was written. Re-save it as UTF-8, then re-run.`);\n }\n}\n\n/**\n * Decode bytes a parser is about to be handed, or refuse naming the file.\n *\n * Two tests, the same two the read path makes (document-bytes.ts's\n * `decodeDocument`), because this side has strictly more to lose by missing\n * either. `isUtf8` catches the byte sequences that cannot be UTF-8 at all —\n * which is what a byte-order mark makes UTF-16 into. Without a BOM, UTF-16LE of\n * ASCII is a sequence of perfectly VALID UTF-8 bytes: every other byte is NUL,\n * so `isUtf8` says yes, the file decodes with a U+0000 between every character,\n * and every parser loam owns reads its requirements, frontmatter and headings\n * as absent rather than as unreadable. On the read path that is a document\n * graded green while nothing in it was checked; here it is worse, because\n * `archive` merges against that empty baseline and writes the result back — the\n * author's requirements are not mis-read, they are gone, and the undo snapshot\n * records the same emptiness as the pre-image.\n */\nfunction decodeUtf8(bytes: Buffer, path: string): string {\n if (!isUtf8(bytes)) {\n throw new NotUtf8Error(\n path,\n `is not valid UTF-8 — loam parses this file as text and writes it back, which would replace every ` +\n `undecodable byte with U+FFFD, in the living document AND in the undo snapshot.`,\n );\n }\n const text = bytes.toString(\"utf8\");\n if (text.includes(\"\\u0000\")) {\n throw new NotUtf8Error(\n path,\n `holds NUL characters, which no document loam owns does — almost always UTF-16 saved without a byte-order ` +\n `mark. Its bytes are valid UTF-8, so it decodes without complaint into text whose requirements, ` +\n `frontmatter and headings all parse as ABSENT: the merge would compute over an empty baseline and write ` +\n `that back, taking every requirement in the file with it.`,\n );\n }\n return text;\n}\n\n/** Read a file loam is going to parse. The only supported way to turn a docs file into a string. */\nexport async function readUtf8(path: string): Promise<string> {\n return decodeUtf8(await readFile(path), path);\n}\n\n/** Same bytes? Absence is its own value: a file that is gone is not a file that is empty. */\nexport function sameBytes(a: Buffer | null, b: Buffer | null): boolean {\n return a === null || b === null ? a === b : a.equals(b);\n}\n\n/* ------------------------------------------------------------------ */\n/* Reading loam's own records back off the disk */\n/* ------------------------------------------------------------------ */\n\n/** The one spelling of a sha256 loam wrote, for reading one back out of a record. */\nexport function isDigest(value: unknown): value is string {\n return typeof value === \"string\" && /^[0-9a-f]{64}$/.test(value);\n}\n\nexport function sha256(content: Buffer | string): string {\n return typeof content === \"string\"\n ? createHash(\"sha256\").update(content, \"utf8\").digest(\"hex\")\n : createHash(\"sha256\").update(content).digest(\"hex\");\n}\n\n"]}
|
|
@@ -111,6 +111,7 @@ export async function flowCoverage(req) {
|
|
|
111
111
|
declared: gradableCapabilityIds(vocab),
|
|
112
112
|
requirementsOf: (capability) => byCapability.get(capability),
|
|
113
113
|
...(req.load === undefined ? {} : { load: req.load }),
|
|
114
|
+
...(req.models === undefined ? {} : { models: req.models }),
|
|
114
115
|
});
|
|
115
116
|
}
|
|
116
117
|
//# sourceMappingURL=claims.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"claims.js","sourceRoot":"","sources":["../../../../src/core/usecases/delta/claims.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * Which of this feature's own promises its own flows keep.\n *\n * A cross-service criterion — \"I enter a login and a password and I am in\" —\n * belongs to no single service's spec, because each service promises only its\n * own part. A `dynamic view` can carry it, because it IS the hop sequence, and\n * that is the whole reason `#req-` exists. Until `./flows.ts` gave a feature a\n * slot for one, this answer could only ever be empty inside a feature window:\n * the flow was a fleet document, so a tag naming a requirement the same change\n * was adding named something that was not living yet.\n *\n * WHAT THIS FEEDS. `capability.uncovered` — the archive gate that refuses a\n * promise nothing keeps. Its message used to end by telling authors to archive\n * with `--approve` and tag the flow AFTERWARDS, which was honest advice about a\n * hole rather than a workflow anybody wanted: it lands the promise first, kept\n * by nothing, and relies on somebody coming back.\n *\n * THE RULE IS `../capability.ts`'s AND MUST STAY SO. Only a RESOLVED claim\n * counts, and both halves must resolve — the `#cap-` tag to exactly one declared\n * capability, the `#req-` tag to exactly one of its requirements. A broken tag\n * of either kind is already an error (`usecase.capability-unresolved`,\n * `usecase.requirement-unresolved`) and must never also mark a promise kept: a\n * typo that silenced the gate would turn a mistake into a green archive. The two\n * resolvers are called directly rather than through\n * `useCaseRequirementClaims` for one reason, and it is not a difference of\n * opinion about the rule — that function flattens its answer to a `<cap>#<id>`\n * string, and `capabilities/delta/uncovered.ts` keys this same join on NUL\n * precisely because a capability id has no grammar forbidding `#`. Handing it a\n * key it has written down a reason not to use would be the wrong kind of reuse.\n *\n * THE LADDER TRAVELS. `declared === null` — an absent or unreadable\n * `architecture/capabilities.yaml` — is an empty answer, not a suspended one, and\n * the caller is already suppressing the whole family behind it. An `unreadable`\n * overlay is different and must NOT be flattened to \"keeps nothing\": loam did not\n * look, which is never the same answer as \"there is nothing there\", and a gate\n * that refused on it would blame the author for a map it could not read.\n */\nimport type { LoadedDoc } from \"../../c4/likec4.js\";\nimport type { CapabilityVocabulary } from \"../../capabilities/capabilities.js\";\nimport { withFeatureCapabilities } from \"../../capabilities/delta/overlay.js\";\nimport type { CapabilityDeltaDoc } from \"../../capabilities/delta/uncovered.js\";\nimport { gradableCapabilityIds } from \"../../capabilities/findings.js\";\nimport { resolveCapabilityTags, resolveRequirementTags } from \"../../capabilities/usecase-join.js\";\nimport type { DocsDir, FeatureDir } from \"../../kernel/ids/dirs.js\";\nimport { compareIds } from \"../../repo/entries.js\";\nimport { readFeatureFlows, type FlowOverlayRequest } from \"./overlay.js\";\n\n/** One promise this feature's flows keep, with the flows that keep it. */\nexport interface FlowPromise {\n /** The capability the keeping view's `#cap-` tag resolved to. */\n capability: string;\n /** The `Requirement-ID` its `#req-` tag resolved to, inside that capability. */\n requirement: string;\n /** The view ids keeping it, deduplicated and sorted — message text, never identity. */\n flows: readonly string[];\n}\n\n/**\n * What the feature's flows keep, or one of the two honest refusals to say.\n *\n * THREE ARMS, because the two failures are not the same failure and a caller\n * that merged them would say the wrong thing. `unreadable` is loam unable to\n * READ the flows — a parse error, a hop naming an element the merge does not\n * land — and it earns `usecase.flow-invalid` and a refusal. `ungraded` is loam\n * able to read them and unable to RESOLVE their tags, because the capability\n * vocabulary this feature's merge would leave behind does not parse; the flows\n * are fine, the file they point into is not, and refusing over the flows would\n * name the wrong document.\n *\n * `read` with an empty `kept` is the ordinary answer and a real one: loam\n * looked, and no flow of this feature keeps a promise it adds.\n */\nexport type FlowPromises =\n | { kind: \"read\"; kept: readonly FlowPromise[] }\n | { kind: \"ungraded\"; why: string }\n | { kind: \"unreadable\"; errors: string[] };\n\n/** Everything the join needs, on top of what the overlay already asks for. */\nexport interface FlowPromisesRequest extends FlowOverlayRequest {\n /**\n * The capability ids to resolve `#cap-` against — the LIVING vocabulary\n * widened with the ids this feature's own capability deltas introduce, which\n * is the same both-corpora rule `Realizes:` follows\n * (`core/capabilities/delta/overlay.ts`). `null` suspends the axis.\n */\n declared: readonly string[] | null;\n /**\n * The requirement ids per capability, widened the same way\n * (`withFeatureRequirements`). `undefined` for a capability with no document\n * at all is a different answer from an empty set, and both are real here.\n */\n requirementsOf: (capability: string) => ReadonlySet<string> | undefined;\n}\n\n/**\n * The promises this feature's own flows keep, read over the post-merge map.\n *\n * A feature with no flow is `read` with nothing kept, which is the ordinary\n * answer and costs one walk over a directory that is not there.\n */\nexport async function featureFlowPromises(req: FlowPromisesRequest): Promise<FlowPromises> {\n const scan = await readFeatureFlows(req);\n if (scan.kind === \"unreadable\") return { kind: \"unreadable\", errors: scan.errors };\n // The ladder, and it is `ungraded` rather than an empty answer. Once the\n // feature's own capability deltas are unioned in, `declared` is null for\n // exactly one reason — `architecture/capabilities.yaml` exists and does not\n // read as a vocabulary — so no `#cap-` tag can resolve and no claim can be\n // made either way. Returning \"keeps nothing\" here is what let\n // `capability.uncovered` convict a flow that does resolve, over a file the\n // feature gate reports nothing else about.\n if (req.declared === null) {\n return {\n kind: \"ungraded\",\n why: \"architecture/capabilities.yaml does not read as a capability vocabulary (`loam validate --all` reports it as capability.invalid)\",\n };\n }\n // A real, empty answer: loam read the flows and none of them claims anything.\n if (scan.views.length === 0) return { kind: \"read\", kept: [] };\n\n const byPromise = new Map<string, { promise: FlowPromise; flows: Set<string> }>();\n for (const view of scan.views) {\n const scope = resolveCapabilityTags(view.tags, req.declared).flatMap((claim) =>\n claim.kind === \"resolved\" ? [claim.id] : [],\n );\n for (const claim of resolveRequirementTags(view.tags, scope, req.requirementsOf)) {\n if (claim.kind !== \"resolved\") continue;\n // Keyed on the pair, never on a joined string: see the header. The Map is\n // only here to collapse two views keeping one promise into one row.\n const key = JSON.stringify([claim.capability, claim.id]);\n const found = byPromise.get(key);\n if (found === undefined) {\n byPromise.set(key, {\n promise: { capability: claim.capability, requirement: claim.id, flows: [] },\n flows: new Set([view.id]),\n });\n } else {\n found.flows.add(view.id);\n }\n }\n }\n // Sorted, and the view ids inside each row sorted too: nothing in loam has\n // measured that LikeC4 preserves view declaration order, so a message built\n // from it would reorder under a dependency bump.\n const kept = [...byPromise.values()]\n .map(({ promise, flows }) => ({ ...promise, flows: [...flows].sort(compareIds) }))\n .sort((a, b) => compareIds(a.capability, b.capability) || compareIds(a.requirement, b.requirement));\n return { kind: \"read\", kept };\n}\n\n/** Everything the coverage join needs, and nothing it would have to read twice. */\nexport interface FlowCoverageRequest {\n docsDir: DocsDir;\n featureDir: FeatureDir;\n featureId: string;\n /**\n * The feature's capability deltas, ALREADY PARSED by the caller's walk. They\n * arrive rather than being read because the one caller has just produced them,\n * and re-reading a document to answer a question about it is how two answers\n * about one file appear.\n */\n capabilities: readonly CapabilityDeltaDoc[];\n /** The LIVING capability vocabulary, read by the caller through its own index. */\n vocabulary: CapabilityVocabulary;\n /** The enumerated fleet, for `readFeatureFlows`' resolver and for its reason. */\n known: ReadonlySet<string>;\n /** The caller's memoised LikeC4 read; see `./overlay.ts`. */\n load?: (path: string) => Promise<LoadedDoc>;\n}\n\n/**\n * What this feature's own flows keep, resolved against the vocabulary and the\n * requirement ids its own merge would leave behind.\n *\n * THE WIDENING IS THE POINT, and it is `capabilities/delta/overlay.ts`'s\n * judgement applied to the other carrier: an analyst adds `NOTIFY-ONCE` to a\n * capability delta and an architect answers it with a flow in the same change,\n * so a `#req-` tag resolved against the LIVING tree alone names a promise that\n * does not exist yet — and the same archive that would land the flow lands the\n * promise it points at.\n *\n * `requirementsOf` KNOWS ONLY THE CAPABILITIES THIS FEATURE HAS A DELTA FOR, and\n * within those it carries living ∪ ADDED ∪ MODIFIED ids. The narrowing changes\n * no answer, for a reason stronger than \"it is probably fine\":\n * `uncoveredIssues` grades `r.kind === \"ADDED\"` and nothing else, and every\n * ADDED requirement it grades lives in one of these documents by construction —\n * so no id outside this map can reach a verdict. The LIVING half is in there\n * because a widening of the graded set must not silently need a second edit\n * here; today it decides nothing. A flow keeping a promise of a capability this\n * feature does NOT touch resolves to `undocumented` and counts for nothing,\n * which is correct: the fleet-scope `capability.requirement-unrealized` grades\n * that join over the whole corpus, and answering it here from a partial index is\n * how two surfaces start disagreeing.\n *\n * `declared` IS NOT NARROWED, and that asymmetry is load-bearing. It is what\n * `resolveCapabilityTags` compares a `#cap-` slug against, and a slug is not\n * injective — `payments/refunds` and `payments-refunds` flatten to one. Handing\n * it a short list would resolve a tag that is genuinely ambiguous fleet-wide and\n * attribute a whole flow to the wrong capability, silently. So the vocabulary is\n * the fleet's whole one, read once by the caller.\n */\nexport async function flowCoverage(req: FlowCoverageRequest): Promise<FlowPromises> {\n const vocab = withFeatureCapabilities(req.vocabulary, req.capabilities.map((doc) => doc.id));\n const byCapability = new Map<string, ReadonlySet<string>>();\n for (const doc of req.capabilities) {\n const ids = new Set<string>();\n // REMOVED is excluded on both sides — a promise on its way out keeps nothing.\n for (const r of doc.living) if (r.kind !== \"REMOVED\" && r.id !== undefined) ids.add(r.id);\n for (const r of doc.reqs) if ((r.kind === \"ADDED\" || r.kind === \"MODIFIED\") && r.id !== undefined) ids.add(r.id);\n byCapability.set(doc.id, ids);\n }\n return featureFlowPromises({\n docsDir: req.docsDir,\n featureDir: req.featureDir,\n featureId: req.featureId,\n known: req.known,\n declared: gradableCapabilityIds(vocab),\n requirementsOf: (capability) => byCapability.get(capability),\n ...(req.load === undefined ? {} : { load: req.load }),\n });\n}\n"]}
|
|
1
|
+
{"version":3,"file":"claims.js","sourceRoot":"","sources":["../../../../src/core/usecases/delta/claims.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * Which of this feature's own promises its own flows keep.\n *\n * A cross-service criterion — \"I enter a login and a password and I am in\" —\n * belongs to no single service's spec, because each service promises only its\n * own part. A `dynamic view` can carry it, because it IS the hop sequence, and\n * that is the whole reason `#req-` exists. Until `./flows.ts` gave a feature a\n * slot for one, this answer could only ever be empty inside a feature window:\n * the flow was a fleet document, so a tag naming a requirement the same change\n * was adding named something that was not living yet.\n *\n * WHAT THIS FEEDS. `capability.uncovered` — the archive gate that refuses a\n * promise nothing keeps. Its message used to end by telling authors to archive\n * with `--approve` and tag the flow AFTERWARDS, which was honest advice about a\n * hole rather than a workflow anybody wanted: it lands the promise first, kept\n * by nothing, and relies on somebody coming back.\n *\n * THE RULE IS `../capability.ts`'s AND MUST STAY SO. Only a RESOLVED claim\n * counts, and both halves must resolve — the `#cap-` tag to exactly one declared\n * capability, the `#req-` tag to exactly one of its requirements. A broken tag\n * of either kind is already an error (`usecase.capability-unresolved`,\n * `usecase.requirement-unresolved`) and must never also mark a promise kept: a\n * typo that silenced the gate would turn a mistake into a green archive. The two\n * resolvers are called directly rather than through\n * `useCaseRequirementClaims` for one reason, and it is not a difference of\n * opinion about the rule — that function flattens its answer to a `<cap>#<id>`\n * string, and `capabilities/delta/uncovered.ts` keys this same join on NUL\n * precisely because a capability id has no grammar forbidding `#`. Handing it a\n * key it has written down a reason not to use would be the wrong kind of reuse.\n *\n * THE LADDER TRAVELS. `declared === null` — an absent or unreadable\n * `architecture/capabilities.yaml` — is an empty answer, not a suspended one, and\n * the caller is already suppressing the whole family behind it. An `unreadable`\n * overlay is different and must NOT be flattened to \"keeps nothing\": loam did not\n * look, which is never the same answer as \"there is nothing there\", and a gate\n * that refused on it would blame the author for a map it could not read.\n */\nimport type { LoadedDoc } from \"../../c4/likec4.js\";\nimport type { ExtendingModel } from \"../../c4/splice/contract.js\";\nimport type { CapabilityVocabulary } from \"../../capabilities/capabilities.js\";\nimport { withFeatureCapabilities } from \"../../capabilities/delta/overlay.js\";\nimport type { CapabilityDeltaDoc } from \"../../capabilities/delta/uncovered.js\";\nimport { gradableCapabilityIds } from \"../../capabilities/findings.js\";\nimport { resolveCapabilityTags, resolveRequirementTags } from \"../../capabilities/usecase-join.js\";\nimport type { DocsDir, FeatureDir } from \"../../kernel/ids/dirs.js\";\nimport { compareIds } from \"../../repo/entries.js\";\nimport { readFeatureFlows, type FlowOverlayRequest } from \"./overlay.js\";\n\n/** One promise this feature's flows keep, with the flows that keep it. */\nexport interface FlowPromise {\n /** The capability the keeping view's `#cap-` tag resolved to. */\n capability: string;\n /** The `Requirement-ID` its `#req-` tag resolved to, inside that capability. */\n requirement: string;\n /** The view ids keeping it, deduplicated and sorted — message text, never identity. */\n flows: readonly string[];\n}\n\n/**\n * What the feature's flows keep, or one of the two honest refusals to say.\n *\n * THREE ARMS, because the two failures are not the same failure and a caller\n * that merged them would say the wrong thing. `unreadable` is loam unable to\n * READ the flows — a parse error, a hop naming an element the merge does not\n * land — and it earns `usecase.flow-invalid` and a refusal. `ungraded` is loam\n * able to read them and unable to RESOLVE their tags, because the capability\n * vocabulary this feature's merge would leave behind does not parse; the flows\n * are fine, the file they point into is not, and refusing over the flows would\n * name the wrong document.\n *\n * `read` with an empty `kept` is the ordinary answer and a real one: loam\n * looked, and no flow of this feature keeps a promise it adds.\n */\nexport type FlowPromises =\n | { kind: \"read\"; kept: readonly FlowPromise[] }\n | { kind: \"ungraded\"; why: string }\n | { kind: \"unreadable\"; errors: string[] };\n\n/** Everything the join needs, on top of what the overlay already asks for. */\nexport interface FlowPromisesRequest extends FlowOverlayRequest {\n /**\n * The capability ids to resolve `#cap-` against — the LIVING vocabulary\n * widened with the ids this feature's own capability deltas introduce, which\n * is the same both-corpora rule `Realizes:` follows\n * (`core/capabilities/delta/overlay.ts`). `null` suspends the axis.\n */\n declared: readonly string[] | null;\n /**\n * The requirement ids per capability, widened the same way\n * (`withFeatureRequirements`). `undefined` for a capability with no document\n * at all is a different answer from an empty set, and both are real here.\n */\n requirementsOf: (capability: string) => ReadonlySet<string> | undefined;\n}\n\n/**\n * The promises this feature's own flows keep, read over the post-merge map.\n *\n * A feature with no flow is `read` with nothing kept, which is the ordinary\n * answer and costs one walk over a directory that is not there.\n */\nexport async function featureFlowPromises(req: FlowPromisesRequest): Promise<FlowPromises> {\n const scan = await readFeatureFlows(req);\n if (scan.kind === \"unreadable\") return { kind: \"unreadable\", errors: scan.errors };\n // The ladder, and it is `ungraded` rather than an empty answer. Once the\n // feature's own capability deltas are unioned in, `declared` is null for\n // exactly one reason — `architecture/capabilities.yaml` exists and does not\n // read as a vocabulary — so no `#cap-` tag can resolve and no claim can be\n // made either way. Returning \"keeps nothing\" here is what let\n // `capability.uncovered` convict a flow that does resolve, over a file the\n // feature gate reports nothing else about.\n if (req.declared === null) {\n return {\n kind: \"ungraded\",\n why: \"architecture/capabilities.yaml does not read as a capability vocabulary (`loam validate --all` reports it as capability.invalid)\",\n };\n }\n // A real, empty answer: loam read the flows and none of them claims anything.\n if (scan.views.length === 0) return { kind: \"read\", kept: [] };\n\n const byPromise = new Map<string, { promise: FlowPromise; flows: Set<string> }>();\n for (const view of scan.views) {\n const scope = resolveCapabilityTags(view.tags, req.declared).flatMap((claim) =>\n claim.kind === \"resolved\" ? [claim.id] : [],\n );\n for (const claim of resolveRequirementTags(view.tags, scope, req.requirementsOf)) {\n if (claim.kind !== \"resolved\") continue;\n // Keyed on the pair, never on a joined string: see the header. The Map is\n // only here to collapse two views keeping one promise into one row.\n const key = JSON.stringify([claim.capability, claim.id]);\n const found = byPromise.get(key);\n if (found === undefined) {\n byPromise.set(key, {\n promise: { capability: claim.capability, requirement: claim.id, flows: [] },\n flows: new Set([view.id]),\n });\n } else {\n found.flows.add(view.id);\n }\n }\n }\n // Sorted, and the view ids inside each row sorted too: nothing in loam has\n // measured that LikeC4 preserves view declaration order, so a message built\n // from it would reorder under a dependency bump.\n const kept = [...byPromise.values()]\n .map(({ promise, flows }) => ({ ...promise, flows: [...flows].sort(compareIds) }))\n .sort((a, b) => compareIds(a.capability, b.capability) || compareIds(a.requirement, b.requirement));\n return { kind: \"read\", kept };\n}\n\n/** Everything the coverage join needs, and nothing it would have to read twice. */\nexport interface FlowCoverageRequest {\n docsDir: DocsDir;\n featureDir: FeatureDir;\n featureId: string;\n /**\n * The feature's capability deltas, ALREADY PARSED by the caller's walk. They\n * arrive rather than being read because the one caller has just produced them,\n * and re-reading a document to answer a question about it is how two answers\n * about one file appear.\n */\n capabilities: readonly CapabilityDeltaDoc[];\n /** The LIVING capability vocabulary, read by the caller through its own index. */\n vocabulary: CapabilityVocabulary;\n /** The enumerated fleet, for `readFeatureFlows`' resolver and for its reason. */\n known: ReadonlySet<string>;\n /** The caller's memoised LikeC4 read; see `./overlay.ts`. */\n load?: (path: string) => Promise<LoadedDoc>;\n /**\n * The fleet's extending models for the merge preview, as a thunk the overlay\n * calls only when this feature really brings a flow; see `./overlay.ts` for\n * why the preview is wrong without them and why it arrives as a function.\n */\n models?: () => Promise<readonly ExtendingModel[]>;\n}\n\n/**\n * What this feature's own flows keep, resolved against the vocabulary and the\n * requirement ids its own merge would leave behind.\n *\n * THE WIDENING IS THE POINT, and it is `capabilities/delta/overlay.ts`'s\n * judgement applied to the other carrier: an analyst adds `NOTIFY-ONCE` to a\n * capability delta and an architect answers it with a flow in the same change,\n * so a `#req-` tag resolved against the LIVING tree alone names a promise that\n * does not exist yet — and the same archive that would land the flow lands the\n * promise it points at.\n *\n * `requirementsOf` KNOWS ONLY THE CAPABILITIES THIS FEATURE HAS A DELTA FOR, and\n * within those it carries living ∪ ADDED ∪ MODIFIED ids. The narrowing changes\n * no answer, for a reason stronger than \"it is probably fine\":\n * `uncoveredIssues` grades `r.kind === \"ADDED\"` and nothing else, and every\n * ADDED requirement it grades lives in one of these documents by construction —\n * so no id outside this map can reach a verdict. The LIVING half is in there\n * because a widening of the graded set must not silently need a second edit\n * here; today it decides nothing. A flow keeping a promise of a capability this\n * feature does NOT touch resolves to `undocumented` and counts for nothing,\n * which is correct: the fleet-scope `capability.requirement-unrealized` grades\n * that join over the whole corpus, and answering it here from a partial index is\n * how two surfaces start disagreeing.\n *\n * `declared` IS NOT NARROWED, and that asymmetry is load-bearing. It is what\n * `resolveCapabilityTags` compares a `#cap-` slug against, and a slug is not\n * injective — `payments/refunds` and `payments-refunds` flatten to one. Handing\n * it a short list would resolve a tag that is genuinely ambiguous fleet-wide and\n * attribute a whole flow to the wrong capability, silently. So the vocabulary is\n * the fleet's whole one, read once by the caller.\n */\nexport async function flowCoverage(req: FlowCoverageRequest): Promise<FlowPromises> {\n const vocab = withFeatureCapabilities(req.vocabulary, req.capabilities.map((doc) => doc.id));\n const byCapability = new Map<string, ReadonlySet<string>>();\n for (const doc of req.capabilities) {\n const ids = new Set<string>();\n // REMOVED is excluded on both sides — a promise on its way out keeps nothing.\n for (const r of doc.living) if (r.kind !== \"REMOVED\" && r.id !== undefined) ids.add(r.id);\n for (const r of doc.reqs) if ((r.kind === \"ADDED\" || r.kind === \"MODIFIED\") && r.id !== undefined) ids.add(r.id);\n byCapability.set(doc.id, ids);\n }\n return featureFlowPromises({\n docsDir: req.docsDir,\n featureDir: req.featureDir,\n featureId: req.featureId,\n known: req.known,\n declared: gradableCapabilityIds(vocab),\n requirementsOf: (capability) => byCapability.get(capability),\n ...(req.load === undefined ? {} : { load: req.load }),\n ...(req.models === undefined ? {} : { models: req.models }),\n });\n}\n"]}
|
|
@@ -33,12 +33,16 @@ export async function readFeatureFlows(req) {
|
|
|
33
33
|
if (flows.length === 0) {
|
|
34
34
|
return { kind: "read", views: [], model: { elements: [], relationships: [], known: req.known }, resolve: (id) => id };
|
|
35
35
|
}
|
|
36
|
+
// AFTER the early return, which is what makes the thunk worth its shape: the
|
|
37
|
+
// fleet read is paid only by a feature that actually brings a flow.
|
|
38
|
+
const models = req.models === undefined ? undefined : await req.models();
|
|
36
39
|
const staged = await stageMergedProject({
|
|
37
40
|
docsDir: req.docsDir,
|
|
38
41
|
featureDir: req.featureDir,
|
|
39
42
|
featureId: req.featureId,
|
|
40
43
|
documents: flows.map((flow) => ({ rel: `${USECASE_SUBDIR}/${flow.rel}`, path: flow.path })),
|
|
41
44
|
...(req.load === undefined ? {} : { load: req.load }),
|
|
45
|
+
...(models === undefined ? {} : { models }),
|
|
42
46
|
});
|
|
43
47
|
return interpret(staged, req.known);
|
|
44
48
|
}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"overlay.js","sourceRoot":"","sources":["../../../../src/core/usecases/delta/overlay.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;GAgBG;AACH,OAAO,EAAkB,MAAM,oBAAoB,CAAC;AACpD,OAAO,EAAE,kBAAkB,EAAsB,MAAM,4BAA4B,CAAC;
|
|
1
|
+
{"version":3,"file":"overlay.js","sourceRoot":"","sources":["../../../../src/core/usecases/delta/overlay.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * A feature's flows, read against the fleet map AS THIS FEATURE'S MERGE WOULD\n * LEAVE IT.\n *\n * The staging and the merge preview that make that possible are no longer here:\n * they moved to `core/c4/project/staged.ts` when the deployment slot became the\n * second feature-local `.likec4` document, because a second copy of that\n * machinery is the copy that drifts, and the two would disagree first about the\n * merge preview — the one thing both axes' refusals rest on. That module's\n * header carries the WHY of the post-merge corpus; this one keeps what is\n * particular to use cases, which is the interpretation: which of the staged\n * views are this feature's, and what they resolve against.\n *\n * FAIL CLOSED, and not silently. Every failure is the `unreadable` arm carrying\n * LikeC4's own messages: a flow this cannot read is never a flow that grades\n * clean, which is the asymmetry `../fleet.ts` states for the fleet-scope scan.\n */\nimport { type LoadedDoc } from \"../../c4/likec4.js\";\nimport { stageMergedProject, type StagedProject } from \"../../c4/project/staged.js\";\nimport type { ExtendingModel } from \"../../c4/splice/contract.js\";\nimport { serviceResolver } from \"../../c4/resolve/service.js\";\nimport type { DocsDir, FeatureDir } from \"../../kernel/ids/dirs.js\";\nimport { isUseCase, type UseCaseScan } from \"../fleet.js\";\nimport { featureFlows, USECASE_SUBDIR } from \"./flows.js\";\n\n/** Everything the overlay needs, and nothing it would have to read twice. */\nexport interface FlowOverlayRequest {\n docsDir: DocsDir;\n featureDir: FeatureDir;\n /** The feature's id — the tag that selects the delta's own additions. */\n featureId: string;\n /**\n * The enumerated fleet, for the element→service resolver. It rides in for the\n * reason every other edge join carries it: without it a hop drawn into a\n * modelled container `payment.api` resolves to a service called \"api\" that\n * has never existed.\n */\n known: ReadonlySet<string>;\n /**\n * The caller's memoised LikeC4 read, when it has one. Only `delta.likec4` goes\n * through it, and only to compute the merge preview — but the caller\n * (`featureCoherence`) has already loaded that exact document, and loading one\n * spins a fresh Langium workspace, which `core/coherence/coherence.ts` calls\n * the dominant per-feature cost.\n *\n * A FUNCTION rather than the `FleetContext` itself, deliberately: this package\n * has no edge to `core/fleet-context.ts`, one memoised read does not justify\n * adding one, and the edge would push `core/usecases/` up a DAG level for\n * nothing. Absent, the load happens directly, which is right for a caller\n * running outside an invocation index.\n */\n load?: (path: string) => Promise<LoadedDoc>;\n /**\n * The fleet's extending models, for the merge preview — the SAME list\n * `loam archive` routes with. Without it the preview holds interior the\n * archive will put in `services/<…>/model.likec4`, so a hop naming\n * `<service-fqn>.<container>` resolves here and against nothing once the flow\n * is living: `usecase.flow-invalid` fails open, and it is the gate\n * `--approve` cannot override (verification 2026-09-04, review F8).\n *\n * A THUNK, and both halves of that matter. A function for the reason `load`\n * is one: the reader takes a `FleetContext` and this package has no edge to\n * it, so the caller that owns the fleet read supplies the answer. LAZY\n * because the early return above is the axis's per-feature opt-in — a feature\n * with no `usecases/` directory must not pay for a fleet enumeration and one\n * read per extending model to be told there is nothing to grade.\n */\n models?: () => Promise<readonly ExtendingModel[]>;\n}\n\n/**\n * The flows this feature carries, parsed over the post-merge map — or the\n * honest refusal to say.\n *\n * A feature with no `usecases/` directory is `read` with no views, at the cost\n * of one walk over a directory that is not there. That is the axis's\n * per-feature opt-in and the whole price a fleet that has not adopted it pays.\n */\nexport async function readFeatureFlows(req: FlowOverlayRequest): Promise<UseCaseScan> {\n const flows = await featureFlows(req.featureDir);\n if (flows.length === 0) {\n return { kind: \"read\", views: [], model: { elements: [], relationships: [], known: req.known }, resolve: (id) => id };\n }\n // AFTER the early return, which is what makes the thunk worth its shape: the\n // fleet read is paid only by a feature that actually brings a flow.\n const models = req.models === undefined ? undefined : await req.models();\n const staged = await stageMergedProject({\n docsDir: req.docsDir,\n featureDir: req.featureDir,\n featureId: req.featureId,\n documents: flows.map((flow) => ({ rel: `${USECASE_SUBDIR}/${flow.rel}`, path: flow.path })),\n ...(req.load === undefined ? {} : { load: req.load }),\n ...(models === undefined ? {} : { models }),\n });\n return interpret(staged, req.known);\n}\n\n/**\n * The staged parse as the use-case axis reads it.\n *\n * THE FEATURE'S OWN FLOWS ONLY. A living use case is the fleet scan's to report,\n * and repeating it here would file somebody else's finding against this\n * feature's gate — the same scoping every other feature-side check keeps.\n */\nfunction interpret(staged: StagedProject, known: ReadonlySet<string>): UseCaseScan {\n if (staged.kind === \"unreadable\") return { kind: \"unreadable\", errors: staged.errors };\n const { doc, real, mine } = staged;\n // Through `isUseCase`, because `UseCaseScan.views` is documented as the\n // reserved-tag views ONLY and this is the type's second producer. The\n // resolvers below filter on the prefix anyway, so no answer changes today —\n // but `flowsClaiming` and `servicesInFlowsClaiming` take a `UseCaseScan` and\n // read `scan.views` straight, and the whole point of exporting one predicate\n // was that a second spelling of the opt-in drifts.\n const views = (doc.views ?? [])\n .filter((view) => view.sourcePath !== undefined && mine.has(view.sourcePath))\n .filter(isUseCase);\n return {\n kind: \"read\",\n views: views.map((view) => ({ ...view, sourcePath: view.sourcePath === undefined ? view.sourcePath : real(view.sourcePath) })),\n model: { elements: doc.elements, relationships: doc.relationships, known },\n resolve: serviceResolver(doc.elements, known),\n };\n}\n"]}
|
|
@@ -38,10 +38,8 @@
|
|
|
38
38
|
*/
|
|
39
39
|
import { readFile } from "node:fs/promises";
|
|
40
40
|
import { CAP_TAG_PREFIX, REQ_TAG_PREFIX } from "../capabilities/usecase-join.js";
|
|
41
|
-
import {
|
|
42
|
-
import { architectureDocuments } from "../c4/project/documents.js";
|
|
41
|
+
import { architectureProjectDocuments, loadArchitecture } from "../c4/project/architecture.js";
|
|
43
42
|
import { serviceResolver } from "../c4/resolve/service.js";
|
|
44
|
-
import { subsystemViewsPath } from "../repo/paths.js";
|
|
45
43
|
/**
|
|
46
44
|
* Does any document in the set even mention a reserved tag prefix?
|
|
47
45
|
*
|
|
@@ -53,8 +51,15 @@ import { subsystemViewsPath } from "../repo/paths.js";
|
|
|
53
51
|
* `req-` is the looser of the two substrings and that is accepted knowingly: a
|
|
54
52
|
* document mentioning it in prose costs one project load loam would otherwise
|
|
55
53
|
* have skipped, which is a slower right answer. Dropping it costs a wrong one.
|
|
54
|
+
*
|
|
55
|
+
* Exported for the service-scope reader (`./service/flows.ts`), which asks the
|
|
56
|
+
* same question of the `.likec4` files beside one `model.likec4`: a sibling set
|
|
57
|
+
* that mentions neither prefix declares no use case, so a hand-drawn
|
|
58
|
+
* `views.likec4` costs that service no LikeC4 workspace. One gate, two callers,
|
|
59
|
+
* because a second byte scan spelled in the service package would be free to
|
|
60
|
+
* disagree with this one about which prefixes opt a view in.
|
|
56
61
|
*/
|
|
57
|
-
async function mentionsTagPrefix(paths) {
|
|
62
|
+
export async function mentionsTagPrefix(paths) {
|
|
58
63
|
for (const path of paths) {
|
|
59
64
|
try {
|
|
60
65
|
const bytes = await readFile(path);
|
|
@@ -116,8 +121,15 @@ export async function readUseCases(req) {
|
|
|
116
121
|
model: { elements: [], relationships: [], known: req.known },
|
|
117
122
|
resolve: (id) => id,
|
|
118
123
|
};
|
|
119
|
-
|
|
120
|
-
|
|
124
|
+
// EXACTLY the documents `loadArchitecture` will load two lines down, root
|
|
125
|
+
// `exclude` and all — `architectureProjectDocuments` is that one answer
|
|
126
|
+
// (`c4/project/architecture.ts`). The gate used to walk `architecture/` for
|
|
127
|
+
// itself and skip the exclude, which could only over-scan: the byte gate
|
|
128
|
+
// reading a document the loader then drops decides "there might be a use
|
|
129
|
+
// case here" and pays for a project load that finds none. Never a wrong
|
|
130
|
+
// answer, but two lists that disagree about what the project is, and the
|
|
131
|
+
// next reader to take the wider one for the project's would be wrong.
|
|
132
|
+
const documents = await architectureProjectDocuments(req.docsDir);
|
|
121
133
|
if (!(await mentionsTagPrefix(documents)))
|
|
122
134
|
return none;
|
|
123
135
|
const doc = await loadArchitecture(req.docsDir);
|
|
@@ -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":"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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 // 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"]}
|