@ybotok/loam 0.2.0-alpha.5 → 0.2.0-alpha.7
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +1063 -0
- package/MIGRATING-from-OpenSpec.md +7 -4
- package/QUICKSTART.md +8 -3
- package/README.md +13 -7
- package/ROADMAP.md +285 -11
- package/SCHEMA.md +566 -45
- 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 +53 -23
- 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/commit.js +34 -1
- package/dist/commands/seed/commit.js.map +1 -1
- package/dist/commands/seed/plan.js +158 -13
- 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/edit.js +4 -2
- package/dist/commands/subsystem/edit.js.map +1 -1
- package/dist/commands/subsystem/move.js +55 -17
- package/dist/commands/subsystem/move.js.map +1 -1
- package/dist/commands/subsystem/sync.js +238 -87
- 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 +174 -0
- package/dist/commands/subsystem/txn/report.js.map +1 -0
- package/dist/commands/subsystem/txn/txn.js +126 -22
- package/dist/commands/subsystem/txn/txn.js.map +1 -1
- package/dist/commands/subsystem/txn/views.js +159 -19
- 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/health-deps.js +9 -6
- package/dist/commands/validate/checks/health-deps.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 +37 -13
- package/dist/commands/validate/fleet/census.js.map +1 -1
- package/dist/commands/validate/fleet/landscape.js +131 -120
- 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 +102 -0
- package/dist/commands/validate/fleet/map/attest.js.map +1 -0
- package/dist/commands/validate/fleet/map/bindings.js +109 -0
- package/dist/commands/validate/fleet/map/bindings.js.map +1 -0
- package/dist/commands/validate/fleet/map/consumers.js +219 -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/scorecard/scorecard.js.map +1 -1
- 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 +45 -15
- package/dist/commands/validate/fleet/views/ids.js.map +1 -1
- package/dist/commands/validate/fleet/views/projects.js +258 -70
- package/dist/commands/validate/fleet/views/projects.js.map +1 -1
- package/dist/commands/validate/fleet/views/root-documents.js +99 -0
- package/dist/commands/validate/fleet/views/root-documents.js.map +1 -0
- package/dist/commands/validate/fleet/views/stale.js +32 -15
- 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 +19 -5
- 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 +70 -17
- 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/refusals.js +5 -3
- package/dist/core/agent/agents-md/refusals.js.map +1 -1
- package/dist/core/agent/agents-md.js +26 -7
- package/dist/core/agent/agents-md.js.map +1 -1
- package/dist/core/agent/support/report.js +39 -5
- package/dist/core/agent/support/report.js.map +1 -1
- package/dist/core/agent/workflows/adopt.js +123 -14
- package/dist/core/agent/workflows/adopt.js.map +1 -1
- package/dist/core/agent/workflows/check.js +34 -20
- package/dist/core/agent/workflows/check.js.map +1 -1
- package/dist/core/agent/workflows/closing.js +3 -1
- package/dist/core/agent/workflows/closing.js.map +1 -1
- package/dist/core/agent/workflows/reference/spine.js +12 -6
- package/dist/core/agent/workflows/reference/spine.js.map +1 -1
- package/dist/core/brief/brief.js +88 -19
- package/dist/core/brief/brief.js.map +1 -1
- package/dist/core/brief/checks.js +133 -7
- package/dist/core/brief/checks.js.map +1 -1
- package/dist/core/brief/landscape.js +212 -129
- package/dist/core/brief/landscape.js.map +1 -1
- package/dist/core/brief/map/extend.js +81 -0
- package/dist/core/brief/map/extend.js.map +1 -0
- package/dist/core/brief/map/owed.js +284 -0
- package/dist/core/brief/map/owed.js.map +1 -0
- package/dist/core/brief/targets.js +94 -29
- package/dist/core/brief/targets.js.map +1 -1
- package/dist/core/brief/unchecked.js +13 -3
- 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 +77 -7
- 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 +12 -6
- package/dist/core/c4/project/documents.js.map +1 -1
- package/dist/core/c4/project/load.js +85 -42
- package/dist/core/c4/project/load.js.map +1 -1
- package/dist/core/c4/project/staged.js +53 -15
- 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 +16 -10
- package/dist/core/deployment/delta.js.map +1 -1
- package/dist/core/diff/base-state.js +21 -5
- package/dist/core/diff/base-state.js.map +1 -1
- package/dist/core/diff/victims.js +10 -1
- package/dist/core/diff/victims.js.map +1 -1
- package/dist/core/docs.js +29 -24
- package/dist/core/docs.js.map +1 -1
- package/dist/core/doctor/config.js +7 -3
- package/dist/core/doctor/config.js.map +1 -1
- package/dist/core/doctor/doctor.js +36 -8
- package/dist/core/doctor/doctor.js.map +1 -1
- package/dist/core/doctor/report.js.map +1 -1
- package/dist/core/doctor/reports/scan.js +242 -0
- package/dist/core/doctor/reports/scan.js.map +1 -0
- package/dist/core/explain/refusals.js +1 -1
- package/dist/core/explain/refusals.js.map +1 -1
- package/dist/core/explore/explore.js +44 -9
- package/dist/core/explore/explore.js.map +1 -1
- package/dist/core/fleet-context.js +47 -0
- package/dist/core/fleet-context.js.map +1 -1
- package/dist/core/gate/checks.js +5 -1
- package/dist/core/gate/checks.js.map +1 -1
- package/dist/core/gate/partners.js +65 -26
- 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 +45 -38
- package/dist/core/repo/paths.js.map +1 -1
- package/dist/core/repo/tree/render/body.js +45 -6
- package/dist/core/repo/tree/render/body.js.map +1 -1
- package/dist/core/repo/tree/render/projects.js +93 -35
- package/dist/core/repo/tree/render/projects.js.map +1 -1
- package/dist/core/repo/tree/render/service/header.js +125 -0
- package/dist/core/repo/tree/render/service/header.js.map +1 -0
- package/dist/core/repo/tree/render/service/peers.js +28 -0
- package/dist/core/repo/tree/render/service/peers.js.map +1 -0
- package/dist/core/repo/tree/render/service/view-id.js +67 -0
- package/dist/core/repo/tree/render/service/view-id.js.map +1 -0
- package/dist/core/repo/tree/render/service/views.js +78 -0
- package/dist/core/repo/tree/render/service/views.js.map +1 -0
- package/dist/core/repo/tree/render/stale.js +20 -4
- package/dist/core/repo/tree/render/stale.js.map +1 -1
- package/dist/core/repo/tree/render/views.js +149 -32
- 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/verify/checklist.js +42 -10
- package/dist/core/verify/checklist.js.map +1 -1
- package/dist/core/vocabulary/issue.js.map +1 -1
- package/dist/core/vocabulary/maturity.js +51 -1
- package/dist/core/vocabulary/maturity.js.map +1 -1
- package/examples/README.md +30 -8
- package/examples/docs/architecture/landscape.likec4 +47 -0
- package/examples/docs/architecture/services.likec4 +70 -0
- package/examples/docs/architecture/subsystems.likec4 +3 -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 +58 -70
- package/examples/docs/services/payment-service/arch.spec.md +7 -4
- package/examples/docs/services/payment-service/model.likec4 +38 -39
- package/examples/docs/services/platform/identity-service/arch.spec.md +7 -4
- package/examples/docs/services/platform/identity-service/model.likec4 +35 -37
- package/examples/docs/services/platform/notification-service/health.yaml +8 -3
- package/examples/docs/services/platform/notification-service/model.likec4 +43 -64
- package/package.json +1 -1
- package/examples/docs/services/checkout-web/likec4.config.json +0 -4
- package/examples/docs/services/order-service/likec4.config.json +0 -4
- package/examples/docs/services/payment-service/likec4.config.json +0 -4
- package/examples/docs/services/platform/identity-service/likec4.config.json +0 -4
- package/examples/docs/services/platform/notification-service/likec4.config.json +0 -4
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"projects.js","sourceRoot":"","sources":["../../../../../src/core/repo/tree/render/projects.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAoEG;AACH,OAAO,EAAE,UAAU,EAAE,MAAM,SAAS,CAAC;AAErC,OAAO,EAAE,aAAa,EAAE,MAAM,gCAAgC,CAAC;AAE/D,gFAAgF;AAChF,MAAM,uBAAuB,GAAG,SAAS,CAAC;AAE1C;;;GAGG;AACH,MAAM,UAAU,kBAAkB,CAAC,EAAU,EAAE,WAAmB;IAChE,MAAM,MAAM,GAAG,EAAE,CAAC,OAAO,CAAC,KAAK,EAAE,GAAG,CAAC,CAAC;IACtC,OAAO,MAAM,KAAK,WAAW,IAAI,MAAM,KAAK,uBAAuB,CAAC,CAAC,CAAC,WAAW,MAAM,EAAE,CAAC,CAAC,CAAC,MAAM,CAAC;AACrG,CAAC;AAED;;;;;GAKG;AACH,MAAM,UAAU,oBAAoB,CAAC,EAAU,EAAE,WAAmB;IAClE,OAAO,GAAG,IAAI,CAAC,SAAS,CAAC,EAAE,IAAI,EAAE,kBAAkB,CAAC,EAAE,EAAE,WAAW,CAAC,EAAE,KAAK,EAAE,EAAE,EAAE,EAAE,IAAI,EAAE,CAAC,CAAC,IAAI,CAAC;AAClG,CAAC;AA6BD;;;;;;;;;;;;;;GAcG;AACH,MAAM,UAAU,cAAc,CAC5B,QAAsB,EACtB,OAAgE;IAEhE,MAAM,IAAI,GAAoB,EAAE,CAAC;IACjC,IAAI,QAAQ,GAAG,CAAC,CAAC;IACjB,KAAK,MAAM,OAAO,IAAI,QAAQ,EAAE,CAAC;QAC/B,yEAAyE;QACzE,qEAAqE;QACrE,IAAI,aAAa,CAAC,OAAO,CAAC,EAAE,CAAC,KAAK,IAAI;YAAE,SAAS;QACjD,MAAM,KAAK,GAAG,OAAO,CAAC,OAAO,CAAC,GAAG,CAAC,CAAC;QACnC,IAAI,CAAC,UAAU,CAAC,KAAK,CAAC,KAAK,CAAC;YAAE,SAAS;QACvC,QAAQ,IAAI,CAAC,CAAC;QACd,IAAI,CAAC,UAAU,CAAC,KAAK,CAAC,OAAO,CAAC;YAAE,IAAI,CAAC,IAAI,CAAC,EAAE,OAAO,EAAE,IAAI,EAAE,KAAK,CAAC,OAAO,EAAE,CAAC,CAAC;IAC9E,CAAC;IACD,IAAI,CAAC,IAAI,CAAC,CAAC,CAAC,EAAE,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,CAAC,OAAO,CAAC,EAAE,GAAG,CAAC,CAAC,OAAO,CAAC,EAAE,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,OAAO,CAAC,EAAE,GAAG,CAAC,CAAC,OAAO,CAAC,EAAE,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC;IAC9F,OAAO,EAAE,IAAI,EAAE,QAAQ,EAAE,CAAC;AAC5B,CAAC","sourcesContent":["/**\n * The renderer's project file for ONE service — `services/<…>/<id>/likec4.config.json`\n * — the second thing under `services/` that exists only to be rendered.\n *\n * Why it exists. The root `likec4.config.json` scopes the fleet project to\n * `architecture/` and excludes `services/**`, and must: loam parses every\n * `.likec4` file ALONE, so each `model.likec4` declares its own `specification`\n * block, and a renderer merging the tree reports every declaration as a\n * duplicate. The cost of that exclusion went unstated for a release: an\n * adopted service's model — the artifact `loam adopt` spends the most effort\n * on — was a box on the fleet map with nothing renderable inside it from the\n * docs root, and the documented way to see it was one renderer invocation per\n * service directory. Measured at the 1.59.2 pin: a `likec4.config.json` INSIDE\n * a service directory is registered as a project of its own even though the\n * root excludes the directory (the root's `exclude` does not hide a nested\n * project file), the workspace then loads `fleet` plus one project per service\n * with zero errors, and each service project carries LikeC4's own `index` view\n * of the containers beside whatever view the model authors. One two-key file\n * per service directory is therefore what makes the model renderable from the\n * same root, in the same renderer, beside the map.\n *\n * CREATE-ONLY, never compared, never rewritten, never removed — the root\n * config's ownership rule (`core/docs.ts`: written once, never re-read, the\n * team owns it), NOT the generated views file's. The bytes are a function of\n * the service id alone, which SCHEMA.md defines as the identity no verb ever\n * changes, so nothing can make the file stale; a compare would only fight the\n * per-project keys LikeC4 lets a team add (`title`, styles, a contact), and\n * would restale every service on any change to this rule. Presence is the one\n * question loam owes the file, and presence needs no parse: rule 26 stands,\n * and loam still reads none of it. A leftover file after a model is deleted is\n * the team's, and harmless — measured, a nested project holding no `.likec4`\n * file is silently absent from the renderer's project list.\n *\n * THE NAME. LikeC4's project-name grammar (measured at the pin) refuses an\n * empty name, the word `default`, and the bytes `.`, `@` and `#`; loam's\n * service-id grammar admits `.`. So every `.` becomes `-`, and a name that\n * would then equal the ROOT project's (`fleet` — a nested project of that name\n * is silently registered as `fleet-1`) or LikeC4's reserved `default` is\n * prefixed `service-`. Readable and typeable after `--project`, and NOT\n * injective: an id spelled a.b beside a service literally named a-b, or\n * `service-fleet` beside `fleet`, share a name and the renderer suffixes the\n * later one `-1`. That is disclosed rather than escaped away, because the\n * picker shows the `title` (measured: `title ?? id`), which is ALWAYS the id\n * verbatim, and a collision costs one suffixed label rather than the blanked\n * workspace that made `subsystemViewId` injective. The root's name arrives as\n * a parameter, like every spelling this package needs from `repo/`: `render/`\n * cannot import `repo/paths.ts` (`stale.ts` records the cycle), and a second\n * copy of the word `fleet` here would be the drift the injection prevents.\n *\n * A DIRECTORY THAT IS NOT A LEGAL SERVICE ID IS OWED NO FILE. The tree walk\n * enumerates every directory on purpose — the fleet is never reported smaller\n * than it is — and `service.id-invalid` names the illegal ones. Folding only\n * `.` is right for every LEGAL id, whose remaining bytes LikeC4 accepts; a\n * name like `pay@1` would produce a project file the renderer silently drops\n * (measured: zero errors, the project simply absent from the list), and\n * because the file is create-only the grade below would then be silent\n * forever while the model stayed unrendered — the exact state this file\n * exists to remove, with a label saying it was fixed. So the survey skips\n * such a service: the writer creates nothing, the grader says nothing, and\n * `service.id-invalid`'s own repair — the rename — is what earns the file on\n * the first sync after it.\n *\n * NO MODEL, NO FILE: a service directory without `model.likec4` — one an\n * archive materialised, a spec-only baseline — is owed nothing, and gets its\n * file on the first `loam subsystem sync` after the model lands. A feature's\n * `delta.likec4` is deliberately out of scope: the directory is transient and\n * `loam archive` moves it where a project file would still be registered, so\n * every shipped feature would keep a dead project in the list forever.\n */\nimport { existsSync } from \"node:fs\";\nimport type { ServiceDir } from \"../../../kernel/ids/dirs.js\";\nimport { dirNameHazard } from \"../../../kernel/ids/service.js\";\n\n/** LikeC4's own reserved project name; a file naming it is refused outright. */\nconst LIKEC4_RESERVED_PROJECT = \"default\";\n\n/**\n * The project name for a service id — the id itself for every id made of\n * letters, digits, `_` and `-`, which is nearly all of them.\n */\nexport function serviceProjectName(id: string, rootProject: string): string {\n const folded = id.replace(/\\./g, \"-\");\n return folded === rootProject || folded === LIKEC4_RESERVED_PROJECT ? `service-${folded}` : folded;\n}\n\n/**\n * The exact bytes of a service's project file: two keys, two-space JSON, LF,\n * one trailing newline — the root file's own serialisation. `title` is the id\n * verbatim, always, so whatever the renderer labels a project with is the\n * service's real identity even where `name` had to bend.\n */\nexport function renderServiceProject(id: string, rootProject: string): string {\n return `${JSON.stringify({ name: serviceProjectName(id, rootProject), title: id }, null, 2)}\\n`;\n}\n\n/**\n * A service whose model has no project file beside it, and where the file\n * goes. The SERVICE travels whole — a walked service or a full entry,\n * whichever the caller surveyed — rather than its id: the grader spells the\n * finding's path from the entry's own placement, and a join back by id would\n * name the wrong directory the moment two services share a leaf name under\n * different subsystems (`subsystem.name-collision` — a broken tree the walk\n * still enumerates in full, and one this grade is emitted on by design).\n */\nexport interface ProjectGap<T> {\n service: T;\n /** Absolute path of the file `loam subsystem sync` will create. */\n path: string;\n}\n\n/** What one survey of the tree found: the gaps, and the domain they were counted against. */\nexport interface ProjectSurvey<T> {\n /** Every service owed a file and lacking one, sorted by id. */\n gaps: ProjectGap<T>[];\n /**\n * Every service the survey considered at all — a model on disk, a legal id\n * — file or no file. `modelled - gaps.length` is therefore \"already had\n * one\", derived from the same walk rather than counted a second way.\n */\n modelled: number;\n}\n\n/**\n * ONE predicate for the writer (`subsystem sync`) and the grader\n * (`validate --all`), on `viewsState`'s precedent — two private spellings of\n * \"is this model a project\" would be two chances to answer differently about\n * one file, and a grader naming a gap the writer would not fill is a loop no\n * command can clear. Both the gaps AND the count live here for the same\n * reason: a caller re-deriving \"has a model\" beside this function is the\n * second spelling in another coat.\n *\n * Structural over `{ id, dir }` so a walked service and a full entry both fit,\n * and the two file spellings arrive injected (`repo/paths.ts`'s\n * `serviceRenderPaths`) for the package reason the banner gives. Sorted by id,\n * plain lexicographic like every list this package emits, so the writer's\n * report and the grader's findings come out in one stable order.\n */\nexport function surveyProjects<T extends { id: string; dir: ServiceDir }>(\n services: readonly T[],\n pathsOf: (dir: ServiceDir) => { model: string; project: string },\n): ProjectSurvey<T> {\n const gaps: ProjectGap<T>[] = [];\n let modelled = 0;\n for (const service of services) {\n // The banner's rule: a name the renderer would refuse earns no file, and\n // `service.id-invalid` — not this survey — is what names the repair.\n if (dirNameHazard(service.id) !== null) continue;\n const paths = pathsOf(service.dir);\n if (!existsSync(paths.model)) continue;\n modelled += 1;\n if (!existsSync(paths.project)) gaps.push({ service, path: paths.project });\n }\n gaps.sort((a, b) => (a.service.id < b.service.id ? -1 : a.service.id > b.service.id ? 1 : 0));\n return { gaps, modelled };\n}\n"]}
|
|
1
|
+
{"version":3,"file":"projects.js","sourceRoot":"","sources":["../../../../../src/core/repo/tree/render/projects.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAyGG;AACH,OAAO,EAAE,UAAU,EAAE,MAAM,SAAS,CAAC;AAErC,OAAO,EAAE,aAAa,EAAE,MAAM,gCAAgC,CAAC;AAE/D,gFAAgF;AAChF,MAAM,uBAAuB,GAAG,SAAS,CAAC;AAE1C;;;GAGG;AACH,MAAM,UAAU,kBAAkB,CAAC,EAAU,EAAE,WAAmB;IAChE,MAAM,MAAM,GAAG,EAAE,CAAC,OAAO,CAAC,KAAK,EAAE,GAAG,CAAC,CAAC;IACtC,OAAO,MAAM,KAAK,WAAW,IAAI,MAAM,KAAK,uBAAuB,CAAC,CAAC,CAAC,WAAW,MAAM,EAAE,CAAC,CAAC,CAAC,MAAM,CAAC;AACrG,CAAC;AAED;;;;;GAKG;AACH,MAAM,UAAU,oBAAoB,CAAC,EAAU,EAAE,WAAmB;IAClE,OAAO,GAAG,IAAI,CAAC,SAAS,CAAC,EAAE,IAAI,EAAE,kBAAkB,CAAC,EAAE,EAAE,WAAW,CAAC,EAAE,KAAK,EAAE,EAAE,EAAE,EAAE,IAAI,EAAE,CAAC,CAAC,IAAI,CAAC;AAClG,CAAC;AA2ED;;;;;;;;;;;;;;GAcG;AACH,MAAM,UAAU,cAAc,CAC5B,QAAsB,EACtB,OAAgE,EAChE,YAA4C;IAE5C,MAAM,MAAM,GAAuB,EAAE,CAAC;IACtC,KAAK,MAAM,OAAO,IAAI,QAAQ,EAAE,CAAC;QAC/B,yEAAyE;QACzE,qEAAqE;QACrE,IAAI,aAAa,CAAC,OAAO,CAAC,EAAE,CAAC,KAAK,IAAI;YAAE,SAAS;QACjD,MAAM,KAAK,GAAG,OAAO,CAAC,OAAO,CAAC,GAAG,CAAC,CAAC;QACnC,IAAI,CAAC,UAAU,CAAC,KAAK,CAAC,KAAK,CAAC;YAAE,SAAS;QACvC,MAAM,CAAC,IAAI,CAAC;YACV,OAAO;YACP,IAAI,EAAE,KAAK,CAAC,OAAO;YACnB,mEAAmE;YACnE,wEAAwE;YACxE,iEAAiE;YACjE,sEAAsE;YACtE,yDAAyD;YACzD,UAAU,EAAE,YAAY,CAAC,KAAK,CAAC,KAAK,CAAC;YACrC,UAAU,EAAE,UAAU,CAAC,KAAK,CAAC,OAAO,CAAC;SACtC,CAAC,CAAC;IACL,CAAC;IACD,MAAM,CAAC,IAAI,CAAC,CAAC,CAAC,EAAE,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,CAAC,OAAO,CAAC,EAAE,GAAG,CAAC,CAAC,OAAO,CAAC,EAAE,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,OAAO,CAAC,EAAE,GAAG,CAAC,CAAC,OAAO,CAAC,EAAE,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC;IAChG,OAAO;QACL,0EAA0E;QAC1E,0EAA0E;QAC1E,0EAA0E;QAC1E,0EAA0E;QAC1E,6EAA6E;QAC7E,IAAI,EAAE,MAAM,CAAC,MAAM,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,UAAU,IAAI,CAAC,CAAC,CAAC,UAAU,CAAC,CAAC,GAAG,CAAC,CAAC,EAAE,OAAO,EAAE,IAAI,EAAE,EAAE,EAAE,CAAC,CAAC,EAAE,OAAO,EAAE,IAAI,EAAE,CAAC,CAAC;QACzG,yEAAyE;QACzE,4EAA4E;QAC5E,0DAA0D;QAC1D,MAAM,EAAE,MAAM,CAAC,MAAM,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,CAAC,UAAU,IAAI,CAAC,CAAC,UAAU,CAAC,CAAC,GAAG,CAAC,CAAC,EAAE,OAAO,EAAE,IAAI,EAAE,EAAE,EAAE,CAAC,CAAC,EAAE,OAAO,EAAE,IAAI,EAAE,CAAC,CAAC;QAC3G,MAAM;QACN,QAAQ,EAAE,MAAM,CAAC,MAAM;QACvB,UAAU,EAAE,MAAM,CAAC,MAAM,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,UAAU,CAAC,CAAC,MAAM;KACtD,CAAC;AACJ,CAAC","sourcesContent":["/**\n * The renderer's project file for ONE service — `services/<…>/<id>/likec4.config.json`\n * — the second thing under `services/` that exists only to be rendered.\n *\n * Why it exists, and WHICH MODELS IT IS FOR. A model that STANDS ALONE declares\n * its own `specification` block, so it is parsed alone and the root project must\n * exclude its directory — a renderer merging it into the map reports every kind\n * and element it declares as a duplicate, blamed on both files. The cost of that\n * exclusion went unstated for a release: an adopted service's model — the\n * artifact `loam adopt` spends the most effort on — was a box on the fleet map\n * with nothing renderable inside it from the docs root, and the documented way\n * to see it was one renderer invocation per service directory. Measured at the\n * 1.59.2 pin: a `likec4.config.json` INSIDE a service directory is registered as\n * a project of its own even though the root excludes the directory (the root's\n * `exclude` does not hide a nested project file), the workspace then loads\n * `fleet` plus one project per service with zero errors, and each service\n * project carries LikeC4's own `index` view of the containers beside whatever\n * view the model authors. One two-key file per such directory is therefore what\n * makes that model renderable from the same root, in the same renderer, beside\n * the map.\n *\n * A model that EXTENDS the map is owed NO file, and that is the same\n * measurement read the other way: it declares no kinds, it lives in the ROOT\n * project beside the map, and a nested project file beside it TAKES it out of\n * there — measured at the 1.59.2 pin, the workspace loads two projects and, where\n * the nested one claims the model, it holds it alone (the map's kinds are not in\n * there with it, so it does not resolve) and `export json --project fleet` loses\n * the service's containers. So the survey below takes the shape as a predicate, the writer\n * creates a file only for the standalone half and removes one beside the\n * extending half, and `service.likec4-config-stray` grades what is left.\n *\n * NEVER COMPARED, NEVER REWRITTEN — the root config's ownership rule\n * (`core/docs.ts`: written once, never re-read, the team owns it), NOT the\n * generated views file's. The bytes are a function of the service id alone,\n * which SCHEMA.md defines as the identity no verb ever changes, so nothing can\n * make the file stale; a compare would only fight the per-project keys LikeC4\n * lets a team add (`title`, styles, a contact), and would restale every service\n * on any change to this rule. Presence is the one question loam owes the file,\n * and presence needs no parse: rule 26 stands, and loam still reads none of it.\n *\n * REMOVED IN EXACTLY ONE STATE: beside a model that EXTENDS the map. That is a\n * correction, and it is measured rather than reasoned. The design note said a\n * nested project beside an extending model was registered EMPTY and therefore\n * harmless; on likec4 1.59.2 two agents measured the opposite (verification\n * 2026-09-04, W1) — the nested project CLAIMS the model, the workspace loads\n * \"2 projects\", the model is Invalid inside its own one (`Could not resolve\n * reference to Element named 'svc_svc_a'`, because the map is not in there with\n * it), and `export json --project fleet` drops the service's containers. So the\n * file is not a leftover, it is a working renderer taken away from the fleet,\n * and `loam subsystem sync` deletes it — which is what SCHEMA, the message and\n * `/loam-adopt` step 5 had been promising all along.\n *\n * THE EXPORT LOSS IS THE WORSE HALF, NOT THE CERTAIN ONE, and the finding's\n * message says so since it was re-measured (re-verification 2026-09-04, area C\n * item 7). Same pin, two fleets: on the seeded fleet above the containers went\n * and came back when the file was deleted; on `examples/docs` a stray beside the\n * extending order-service left `export json --project fleet` byte-identical —\n * 33 elements, every `marketplace.orderService.*` child present — because there\n * the ROOT project kept the model. What is true in every state measured is the\n * second project itself: the docs root stops being a one-project workspace, and\n * `likec4 validate .` goes from ✓ Valid to ✗ Invalid (\"Specify exact project,\n * known: [order-service, fleet]\"). A message that asserted the export loss\n * unconditionally was refutable on the repository's own example fleet.\n *\n * A leftover beside a model that is GONE stays. That one really is the team's:\n * a nested project holding no `.likec4` file is silently absent from the\n * renderer's project list (measured), so it costs nothing, and a survey that\n * deleted files in directories it found no model in would be a writer acting on\n * an absence.\n *\n * THE NAME. LikeC4's project-name grammar (measured at the pin) refuses an\n * empty name, the word `default`, and the bytes `.`, `@` and `#`; loam's\n * service-id grammar admits `.`. So every `.` becomes `-`, and a name that\n * would then equal the ROOT project's (`fleet` — a nested project of that name\n * is silently registered as `fleet-1`) or LikeC4's reserved `default` is\n * prefixed `service-`. Readable and typeable after `--project`, and NOT\n * injective: an id spelled a.b beside a service literally named a-b, or\n * `service-fleet` beside `fleet`, share a name and the renderer suffixes the\n * later one `-1`. That is disclosed rather than escaped away, because the\n * picker shows the `title` (measured: `title ?? id`), which is ALWAYS the id\n * verbatim, and a collision costs one suffixed label rather than the blanked\n * workspace that made `subsystemViewId` injective. The root's name arrives as\n * a parameter, like every spelling this package needs from `repo/`: `render/`\n * cannot import `repo/paths.ts` (`stale.ts` records the cycle), and a second\n * copy of the word `fleet` here would be the drift the injection prevents.\n *\n * A DIRECTORY THAT IS NOT A LEGAL SERVICE ID IS OWED NO FILE. The tree walk\n * enumerates every directory on purpose — the fleet is never reported smaller\n * than it is — and `service.id-invalid` names the illegal ones. Folding only\n * `.` is right for every LEGAL id, whose remaining bytes LikeC4 accepts; a\n * name like `pay@1` would produce a project file the renderer silently drops\n * (measured: zero errors, the project simply absent from the list), and\n * because the file is create-only the grade below would then be silent\n * forever while the model stayed unrendered — the exact state this file\n * exists to remove, with a label saying it was fixed. So the survey skips\n * such a service: the writer creates nothing, the grader says nothing, and\n * `service.id-invalid`'s own repair — the rename — is what earns the file on\n * the first sync after it.\n *\n * NO MODEL, NO FILE: a service directory without `model.likec4` — one an\n * archive materialised, a spec-only baseline — is owed nothing, and gets its\n * file on the first `loam subsystem sync` after the model lands. A feature's\n * `delta.likec4` is deliberately out of scope: the directory is transient and\n * `loam archive` moves it where a project file would still be registered, so\n * every shipped feature would keep a dead project in the list forever.\n */\nimport { existsSync } from \"node:fs\";\nimport type { ServiceDir } from \"../../../kernel/ids/dirs.js\";\nimport { dirNameHazard } from \"../../../kernel/ids/service.js\";\n\n/** LikeC4's own reserved project name; a file naming it is refused outright. */\nconst LIKEC4_RESERVED_PROJECT = \"default\";\n\n/**\n * The project name for a service id — the id itself for every id made of\n * letters, digits, `_` and `-`, which is nearly all of them.\n */\nexport function serviceProjectName(id: string, rootProject: string): string {\n const folded = id.replace(/\\./g, \"-\");\n return folded === rootProject || folded === LIKEC4_RESERVED_PROJECT ? `service-${folded}` : folded;\n}\n\n/**\n * The exact bytes of a service's project file: two keys, two-space JSON, LF,\n * one trailing newline — the root file's own serialisation. `title` is the id\n * verbatim, always, so whatever the renderer labels a project with is the\n * service's real identity even where `name` had to bend.\n */\nexport function renderServiceProject(id: string, rootProject: string): string {\n return `${JSON.stringify({ name: serviceProjectName(id, rootProject), title: id }, null, 2)}\\n`;\n}\n\n/**\n * A service whose model has no project file beside it, and where the file\n * goes. The SERVICE travels whole — a walked service or a full entry,\n * whichever the caller surveyed — rather than its id: the grader spells the\n * finding's path from the entry's own placement, and a join back by id would\n * name the wrong directory the moment two services share a leaf name under\n * different subsystems (`subsystem.name-collision` — a broken tree the walk\n * still enumerates in full, and one this grade is emitted on by design).\n */\nexport interface ProjectGap<T> {\n service: T;\n /** Absolute path of the file `loam subsystem sync` will create. */\n path: string;\n}\n\n/**\n * One surveyed model: everything the writer and the four renderer grades ask\n * about a single `model.likec4`, answered in one walk.\n *\n * The SHAPE is on here rather than re-derived per grade because it decides\n * opposite things for the two of them: a model that stands alone is owed a\n * project file and must be excluded from the root project, and a model that\n * extends the map is owed neither and must NOT be excluded. Two readers of the\n * same bytes would be two chances to answer that backwards, and backwards is\n * not a warning — it is a renderer that shows nothing.\n */\nexport interface SurveyedModel<T> {\n service: T;\n /** Absolute path of the per-service project file, whether or not it exists. */\n path: string;\n /** True when the model declares its own element kinds — the caller's byte scan, injected. */\n standalone: boolean;\n /** True when a `likec4.config.json` already sits beside the model. */\n configured: boolean;\n}\n\n/** What one survey of the tree found: the gaps, the models behind them, and the domain they were counted against. */\nexport interface ProjectSurvey<T> {\n /** Every service owed a file and lacking one, sorted by id. */\n gaps: ProjectGap<T>[];\n /**\n * Every service owed NO file that has one anyway — an extending model with a\n * `likec4.config.json` beside it, sorted by id. The mirror of `gaps`, and the\n * same shape, because the writer does the same thing with it: `subsystem sync`\n * plans a delete where it plans a create, and `service.likec4-config-stray`\n * grades what is left. One predicate for both, so the grader can never name a\n * stray the writer would decline to remove.\n */\n strays: ProjectGap<T>[];\n /** Every model the survey considered, sorted by id — the four renderer grades walk this. */\n models: SurveyedModel<T>[];\n /**\n * Every service the survey considered at all — a model on disk, a legal id\n * — file or no file, whichever shape the model has. It is what the\n * root-missing note is owed to: without the root project file NOTHING under\n * `services/` is written, and the count a person needs there is how many\n * models the run walked past.\n */\n modelled: number;\n /**\n * The subset of those whose model STANDS ALONE — the only services owed a\n * project file at all, and therefore the domain `gaps` was drawn from.\n *\n * Apart from `modelled` because the two stopped meaning the same thing when a\n * model could extend the map, and one subtraction rested on their being\n * equal: `modelled - gaps.length` was \"already had a file\", and it counted\n * every extending model as one — a fleet that had just migrated reported N\n * services holding a file that must not exist. `standalone - gaps.length` is\n * the same subtraction over the domain that is actually owed the file.\n */\n standalone: number;\n}\n\n/**\n * ONE predicate for the writer (`subsystem sync`) and the grader\n * (`validate --all`), on `viewsState`'s precedent — two private spellings of\n * \"is this model a project\" would be two chances to answer differently about\n * one file, and a grader naming a gap the writer would not fill is a loop no\n * command can clear. Both the gaps AND the count live here for the same\n * reason: a caller re-deriving \"has a model\" beside this function is the\n * second spelling in another coat.\n *\n * Structural over `{ id, dir }` so a walked service and a full entry both fit,\n * and the two file spellings arrive injected (`repo/paths.ts`'s\n * `serviceRenderPaths`) for the package reason the banner gives. Sorted by id,\n * plain lexicographic like every list this package emits, so the writer's\n * report and the grader's findings come out in one stable order.\n */\nexport function surveyProjects<T extends { id: string; dir: ServiceDir }>(\n services: readonly T[],\n pathsOf: (dir: ServiceDir) => { model: string; project: string },\n isStandalone: (modelPath: string) => boolean,\n): ProjectSurvey<T> {\n const models: SurveyedModel<T>[] = [];\n for (const service of services) {\n // The banner's rule: a name the renderer would refuse earns no file, and\n // `service.id-invalid` — not this survey — is what names the repair.\n if (dirNameHazard(service.id) !== null) continue;\n const paths = pathsOf(service.dir);\n if (!existsSync(paths.model)) continue;\n models.push({\n service,\n path: paths.project,\n // INJECTED rather than read here, and the reason is this package's\n // standing one: `render/` may not import `c4/` (see `./stale.ts` on the\n // cycle), and the shape is a fact about the model's GRAMMAR that\n // `c4/service-model/shape.ts` owns. A predicate crossing the boundary\n // keeps one scanner for the writer and the grader alike.\n standalone: isStandalone(paths.model),\n configured: existsSync(paths.project),\n });\n }\n models.sort((a, b) => (a.service.id < b.service.id ? -1 : a.service.id > b.service.id ? 1 : 0));\n return {\n // A model that EXTENDS the map is owed no project file: it belongs to the\n // root project, and a nested project file beside it CLAIMS it — the model\n // then parses alone, where the map's kinds are not, and the fleet project\n // loses the service's interior. So the gap set narrowed to the standalone\n // shape, and `strays` below is the grade's and the writer's other direction.\n gaps: models.filter((m) => m.standalone && !m.configured).map(({ service, path }) => ({ service, path })),\n // The other direction, and the one the banner's correction is about: the\n // nested project claims the model and the fleet project loses the service's\n // interior, so the file is removed rather than tolerated.\n strays: models.filter((m) => !m.standalone && m.configured).map(({ service, path }) => ({ service, path })),\n models,\n modelled: models.length,\n standalone: models.filter((m) => m.standalone).length,\n };\n}\n"]}
|
|
@@ -0,0 +1,125 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The one line that says a file under `architecture/` is LOAM'S — the opening
|
|
3
|
+
* comment both generated views files carry, and the predicate that reads it
|
|
4
|
+
* back off a file on disk.
|
|
5
|
+
*
|
|
6
|
+
* WHY A MARK EXISTS AT ALL. `architecture/subsystems.likec4` and
|
|
7
|
+
* `architecture/services.likec4` are filenames loam RESERVES inside a directory
|
|
8
|
+
* the team owns, and `loam subsystem sync` rewrites them wholesale. A docs repo
|
|
9
|
+
* that already kept a hand-authored diagram at one of those paths — plausible
|
|
10
|
+
* for `services.likec4` in particular, which loam reserved only now — would
|
|
11
|
+
* have lost it on the first sync after the upgrade, with no warning and nothing
|
|
12
|
+
* to restore it from. That is the one promise the rest of the tree keeps
|
|
13
|
+
* everywhere: authored bytes stay authored, which is what archive's verbatim
|
|
14
|
+
* splice is for. So the writer asks the file itself who wrote it, and refuses
|
|
15
|
+
* every path where the answer is not loam.
|
|
16
|
+
*
|
|
17
|
+
* IT LIVES HERE, in the leaf package, for the reason `./view-id.ts` does: the
|
|
18
|
+
* parent (`../views.ts`) may import a child and never the reverse, so the one
|
|
19
|
+
* spelling BOTH generated files must open with has to sit below both. Two
|
|
20
|
+
* literals for one mark is how the day comes that a file loam wrote is not
|
|
21
|
+
* recognised as loam's and the writer refuses to regenerate its own output.
|
|
22
|
+
*
|
|
23
|
+
* SO DOES THE READ, AND SO DOES THE SENTENCE, and that is the correction this
|
|
24
|
+
* module carries. The guard first shipped inside `loam subsystem sync` alone,
|
|
25
|
+
* and there are FOUR other writers of exactly these two paths: the shared commit
|
|
26
|
+
* window behind `subsystem new`, `rm`, `move` and `rename`
|
|
27
|
+
* (`commands/subsystem/txn/txn.ts`) and `loam seed`
|
|
28
|
+
* (`commands/seed/plan.ts`). Reproduced before this module grew: with a
|
|
29
|
+
* hand-authored `architecture/services.likec4` in the tree, `loam subsystem new
|
|
30
|
+
* platform` printed "architecture/services.likec4 updated" and destroyed it —
|
|
31
|
+
* the file's digest changed and its first line became loam's header. A guard
|
|
32
|
+
* that lives in one of five writers is not a guard. So the reader
|
|
33
|
+
* (`readGeneratedFile`, `authoredGeneratedPaths`) and the refusal sentence
|
|
34
|
+
* (`authoredRefusal`) sit beside the mark itself, below every writer, and a
|
|
35
|
+
* sixth writer added tomorrow has one place to reach for rather than a fourth
|
|
36
|
+
* spelling to invent.
|
|
37
|
+
*/
|
|
38
|
+
import { existsSync } from "node:fs";
|
|
39
|
+
import { readFile } from "node:fs/promises";
|
|
40
|
+
/**
|
|
41
|
+
* The first bytes of every generated views file. Part of the content-compare
|
|
42
|
+
* contract, so it is not prose that may be reworded: the rest of each header is,
|
|
43
|
+
* and deliberately, because only this much identifies the owner.
|
|
44
|
+
*/
|
|
45
|
+
export const GENERATED_MARK = "// generated by loam";
|
|
46
|
+
/**
|
|
47
|
+
* Was the text now at a generated path written by loam?
|
|
48
|
+
*
|
|
49
|
+
* A LEADING BYTE-ORDER MARK is stripped first, and that is not tidiness: a
|
|
50
|
+
* Windows editor that opens a generated file and saves it back adds one without
|
|
51
|
+
* changing a single fact in the file, and without this the writer would call its
|
|
52
|
+
* own output authored and refuse to regenerate it for good — a refusal no
|
|
53
|
+
* re-running could ever clear. CRLF needs no such treatment, for the reason
|
|
54
|
+
* `viewsAgree` records one file over: the mark is the head of the first line and
|
|
55
|
+
* the line ending is behind it.
|
|
56
|
+
*
|
|
57
|
+
* The test is the MARK and not the whole header, because the header's remaining
|
|
58
|
+
* lines differ between the two files and are prose. It is also deliberately not
|
|
59
|
+
* a test of the whole file: a hand EDIT of a generated file keeps the header, and
|
|
60
|
+
* that file is loam's to repair — `subsystem.views-stale` names it and one sync
|
|
61
|
+
* puts it back. Only a file that never carried the mark is somebody else's.
|
|
62
|
+
*/
|
|
63
|
+
export function isGenerated(text) {
|
|
64
|
+
return text.replace(/^\uFEFF/, "").startsWith(GENERATED_MARK);
|
|
65
|
+
}
|
|
66
|
+
/**
|
|
67
|
+
* Read one generated path and answer both questions about it at once \u2014 what it
|
|
68
|
+
* says, and whether loam may write it.
|
|
69
|
+
*
|
|
70
|
+
* ONE READ, because the only caller that needs the text is the one that also
|
|
71
|
+
* compares it (`subsystem sync`'s `planFile`), and asking the disk twice for the
|
|
72
|
+
* same small file so that two helpers could each own half the answer is how the
|
|
73
|
+
* two halves come to disagree about which file they were talking about.
|
|
74
|
+
*
|
|
75
|
+
* ABSENT IS NOT AUTHORED. There is nothing to lose at a path that holds
|
|
76
|
+
* nothing, and a fleet being scaffolded must still get its first generated file
|
|
77
|
+
* \u2014 a guard that refused an absent file would refuse every repository forever.
|
|
78
|
+
*/
|
|
79
|
+
export async function readGeneratedFile(path) {
|
|
80
|
+
if (!existsSync(path))
|
|
81
|
+
return { text: null, authored: false };
|
|
82
|
+
const text = await readFile(path, "utf8");
|
|
83
|
+
return { text, authored: !isGenerated(text) };
|
|
84
|
+
}
|
|
85
|
+
/**
|
|
86
|
+
* Which of these generated paths hold a file loam did not write \u2014 the question
|
|
87
|
+
* every writer that rewrites the PAIR wholesale asks before it stages anything.
|
|
88
|
+
*
|
|
89
|
+
* The paths come back in the order they were given, so a caller's message names
|
|
90
|
+
* the files in a stable order rather than in whichever order the reads finished.
|
|
91
|
+
*/
|
|
92
|
+
export async function authoredGeneratedPaths(paths) {
|
|
93
|
+
// Independent reads of at most two small files: `Promise.all`, per the house
|
|
94
|
+
// rule, and the order of the input is preserved by the map rather than by the
|
|
95
|
+
// scheduler.
|
|
96
|
+
const reads = await Promise.all(paths.map(async (path) => ({ path, read: await readGeneratedFile(path) })));
|
|
97
|
+
return reads.filter((r) => r.read.authored).map((r) => r.path);
|
|
98
|
+
}
|
|
99
|
+
/**
|
|
100
|
+
* The one sentence a refusal owes about ONE authored file, composed here and
|
|
101
|
+
* parameterised by the file, because FIVE writers refuse for this reason and a
|
|
102
|
+
* reader who met the sentence from `loam subsystem sync` must recognise it
|
|
103
|
+
* verbatim from `loam subsystem new`, `rm`, `move`, `rename` and `loam seed`.
|
|
104
|
+
*
|
|
105
|
+
* It names three things a person cannot get anywhere else: which file was left
|
|
106
|
+
* alone, WHY loam thinks the file is not its own (the mark it looked for), and
|
|
107
|
+
* the fact that nothing was written to it. The last clause closes the loop the
|
|
108
|
+
* refusal otherwise leaves open \u2014 `loam validate --all` will keep reporting
|
|
109
|
+
* `subsystem.views-stale` for that path and keep naming `loam subsystem sync` as
|
|
110
|
+
* the repair, so a person who is not told why the writers decline would be sent
|
|
111
|
+
* back and forth between the two forever.
|
|
112
|
+
*
|
|
113
|
+
* It deliberately does NOT name a command to re-run. It used to say "re-run
|
|
114
|
+
* `loam subsystem sync`", which was true of the one caller it then had and wrong
|
|
115
|
+
* for the four that were added: the command to re-run is the one the person
|
|
116
|
+
* typed, and the sentence a caller appends says what that command did instead.
|
|
117
|
+
*/
|
|
118
|
+
export function authoredRefusal(file) {
|
|
119
|
+
return (`${file} left as it is \u2014 its first line is not \`${GENERATED_MARK}\`, so it is an AUTHORED file standing at a ` +
|
|
120
|
+
"path loam generates and rewrites wholesale. Nothing was written to it and not one of its bytes changed. " +
|
|
121
|
+
"Rename it to something loam does not reserve (every other `.likec4` under architecture/ renders in the same " +
|
|
122
|
+
"project, so the diagram is not lost) or delete it, then re-run. Until then `loam validate --all` reports " +
|
|
123
|
+
"`subsystem.views-stale` for that path and every loam command that writes the file keeps refusing.");
|
|
124
|
+
}
|
|
125
|
+
//# sourceMappingURL=header.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"header.js","sourceRoot":"","sources":["../../../../../../src/core/repo/tree/render/service/header.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAoCG;AACH,OAAO,EAAE,UAAU,EAAE,MAAM,SAAS,CAAC;AACrC,OAAO,EAAE,QAAQ,EAAE,MAAM,kBAAkB,CAAC;AAE5C;;;;GAIG;AACH,MAAM,CAAC,MAAM,cAAc,GAAG,sBAAsB,CAAC;AAErD;;;;;;;;;;;;;;;;GAgBG;AACH,MAAM,UAAU,WAAW,CAAC,IAAY;IACtC,OAAO,IAAI,CAAC,OAAO,CAAC,SAAS,EAAE,EAAE,CAAC,CAAC,UAAU,CAAC,cAAc,CAAC,CAAC;AAChE,CAAC;AAUD;;;;;;;;;;;;GAYG;AACH,MAAM,CAAC,KAAK,UAAU,iBAAiB,CAAC,IAAY;IAClD,IAAI,CAAC,UAAU,CAAC,IAAI,CAAC;QAAE,OAAO,EAAE,IAAI,EAAE,IAAI,EAAE,QAAQ,EAAE,KAAK,EAAE,CAAC;IAC9D,MAAM,IAAI,GAAG,MAAM,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC,CAAC;IAC1C,OAAO,EAAE,IAAI,EAAE,QAAQ,EAAE,CAAC,WAAW,CAAC,IAAI,CAAC,EAAE,CAAC;AAChD,CAAC;AAED;;;;;;GAMG;AACH,MAAM,CAAC,KAAK,UAAU,sBAAsB,CAAC,KAAwB;IACnE,6EAA6E;IAC7E,8EAA8E;IAC9E,aAAa;IACb,MAAM,KAAK,GAAG,MAAM,OAAO,CAAC,GAAG,CAC7B,KAAK,CAAC,GAAG,CAAC,KAAK,EAAE,IAAI,EAAE,EAAE,CAAC,CAAC,EAAE,IAAI,EAAE,IAAI,EAAE,MAAM,iBAAiB,CAAC,IAAI,CAAC,EAAE,CAAC,CAAC,CAC3E,CAAC;IACF,OAAO,KAAK,CAAC,MAAM,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,IAAI,CAAC,QAAQ,CAAC,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,IAAI,CAAC,CAAC;AACjE,CAAC;AAED;;;;;;;;;;;;;;;;;;GAkBG;AACH,MAAM,UAAU,eAAe,CAAC,IAAY;IAC1C,OAAO,CACL,GAAG,IAAI,iDAAiD,cAAc,8CAA8C;QACpH,0GAA0G;QAC1G,8GAA8G;QAC9G,2GAA2G;QAC3G,mGAAmG,CACpG,CAAC;AACJ,CAAC","sourcesContent":["/**\n * The one line that says a file under `architecture/` is LOAM'S — the opening\n * comment both generated views files carry, and the predicate that reads it\n * back off a file on disk.\n *\n * WHY A MARK EXISTS AT ALL. `architecture/subsystems.likec4` and\n * `architecture/services.likec4` are filenames loam RESERVES inside a directory\n * the team owns, and `loam subsystem sync` rewrites them wholesale. A docs repo\n * that already kept a hand-authored diagram at one of those paths — plausible\n * for `services.likec4` in particular, which loam reserved only now — would\n * have lost it on the first sync after the upgrade, with no warning and nothing\n * to restore it from. That is the one promise the rest of the tree keeps\n * everywhere: authored bytes stay authored, which is what archive's verbatim\n * splice is for. So the writer asks the file itself who wrote it, and refuses\n * every path where the answer is not loam.\n *\n * IT LIVES HERE, in the leaf package, for the reason `./view-id.ts` does: the\n * parent (`../views.ts`) may import a child and never the reverse, so the one\n * spelling BOTH generated files must open with has to sit below both. Two\n * literals for one mark is how the day comes that a file loam wrote is not\n * recognised as loam's and the writer refuses to regenerate its own output.\n *\n * SO DOES THE READ, AND SO DOES THE SENTENCE, and that is the correction this\n * module carries. The guard first shipped inside `loam subsystem sync` alone,\n * and there are FOUR other writers of exactly these two paths: the shared commit\n * window behind `subsystem new`, `rm`, `move` and `rename`\n * (`commands/subsystem/txn/txn.ts`) and `loam seed`\n * (`commands/seed/plan.ts`). Reproduced before this module grew: with a\n * hand-authored `architecture/services.likec4` in the tree, `loam subsystem new\n * platform` printed \"architecture/services.likec4 updated\" and destroyed it —\n * the file's digest changed and its first line became loam's header. A guard\n * that lives in one of five writers is not a guard. So the reader\n * (`readGeneratedFile`, `authoredGeneratedPaths`) and the refusal sentence\n * (`authoredRefusal`) sit beside the mark itself, below every writer, and a\n * sixth writer added tomorrow has one place to reach for rather than a fourth\n * spelling to invent.\n */\nimport { existsSync } from \"node:fs\";\nimport { readFile } from \"node:fs/promises\";\n\n/**\n * The first bytes of every generated views file. Part of the content-compare\n * contract, so it is not prose that may be reworded: the rest of each header is,\n * and deliberately, because only this much identifies the owner.\n */\nexport const GENERATED_MARK = \"// generated by loam\";\n\n/**\n * Was the text now at a generated path written by loam?\n *\n * A LEADING BYTE-ORDER MARK is stripped first, and that is not tidiness: a\n * Windows editor that opens a generated file and saves it back adds one without\n * changing a single fact in the file, and without this the writer would call its\n * own output authored and refuse to regenerate it for good — a refusal no\n * re-running could ever clear. CRLF needs no such treatment, for the reason\n * `viewsAgree` records one file over: the mark is the head of the first line and\n * the line ending is behind it.\n *\n * The test is the MARK and not the whole header, because the header's remaining\n * lines differ between the two files and are prose. It is also deliberately not\n * a test of the whole file: a hand EDIT of a generated file keeps the header, and\n * that file is loam's to repair — `subsystem.views-stale` names it and one sync\n * puts it back. Only a file that never carried the mark is somebody else's.\n */\nexport function isGenerated(text: string): boolean {\n return text.replace(/^\\uFEFF/, \"\").startsWith(GENERATED_MARK);\n}\n\n/** What a writer of a generated path learns by reading it, in one read. */\nexport interface GeneratedFileRead {\n /** The bytes as text, or null when nothing stands at the path. */\n text: string | null;\n /** SOMEBODY ELSE wrote this: a file is there and its first line is not the mark. */\n authored: boolean;\n}\n\n/**\n * Read one generated path and answer both questions about it at once \\u2014 what it\n * says, and whether loam may write it.\n *\n * ONE READ, because the only caller that needs the text is the one that also\n * compares it (`subsystem sync`'s `planFile`), and asking the disk twice for the\n * same small file so that two helpers could each own half the answer is how the\n * two halves come to disagree about which file they were talking about.\n *\n * ABSENT IS NOT AUTHORED. There is nothing to lose at a path that holds\n * nothing, and a fleet being scaffolded must still get its first generated file\n * \\u2014 a guard that refused an absent file would refuse every repository forever.\n */\nexport async function readGeneratedFile(path: string): Promise<GeneratedFileRead> {\n if (!existsSync(path)) return { text: null, authored: false };\n const text = await readFile(path, \"utf8\");\n return { text, authored: !isGenerated(text) };\n}\n\n/**\n * Which of these generated paths hold a file loam did not write \\u2014 the question\n * every writer that rewrites the PAIR wholesale asks before it stages anything.\n *\n * The paths come back in the order they were given, so a caller's message names\n * the files in a stable order rather than in whichever order the reads finished.\n */\nexport async function authoredGeneratedPaths(paths: readonly string[]): Promise<string[]> {\n // Independent reads of at most two small files: `Promise.all`, per the house\n // rule, and the order of the input is preserved by the map rather than by the\n // scheduler.\n const reads = await Promise.all(\n paths.map(async (path) => ({ path, read: await readGeneratedFile(path) })),\n );\n return reads.filter((r) => r.read.authored).map((r) => r.path);\n}\n\n/**\n * The one sentence a refusal owes about ONE authored file, composed here and\n * parameterised by the file, because FIVE writers refuse for this reason and a\n * reader who met the sentence from `loam subsystem sync` must recognise it\n * verbatim from `loam subsystem new`, `rm`, `move`, `rename` and `loam seed`.\n *\n * It names three things a person cannot get anywhere else: which file was left\n * alone, WHY loam thinks the file is not its own (the mark it looked for), and\n * the fact that nothing was written to it. The last clause closes the loop the\n * refusal otherwise leaves open \\u2014 `loam validate --all` will keep reporting\n * `subsystem.views-stale` for that path and keep naming `loam subsystem sync` as\n * the repair, so a person who is not told why the writers decline would be sent\n * back and forth between the two forever.\n *\n * It deliberately does NOT name a command to re-run. It used to say \"re-run\n * `loam subsystem sync`\", which was true of the one caller it then had and wrong\n * for the four that were added: the command to re-run is the one the person\n * typed, and the sentence a caller appends says what that command did instead.\n */\nexport function authoredRefusal(file: string): string {\n return (\n `${file} left as it is \\u2014 its first line is not \\`${GENERATED_MARK}\\`, so it is an AUTHORED file standing at a ` +\n \"path loam generates and rewrites wholesale. Nothing was written to it and not one of its bytes changed. \" +\n \"Rename it to something loam does not reserve (every other `.likec4` under architecture/ renders in the same \" +\n \"project, so the diagram is not lost) or delete it, then re-run. Until then `loam validate --all` reports \" +\n \"`subsystem.views-stale` for that path and every loam command that writes the file keeps refusing.\"\n );\n}\n"]}
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Every element on the far side of an edge touching this service, sorted and
|
|
3
|
+
* de-duplicated.
|
|
4
|
+
*
|
|
5
|
+
* "Touching" is the subject OR any descendant of it: an edge into
|
|
6
|
+
* `svc.api` is an edge into the service, and the view that hid it would be
|
|
7
|
+
* lying about the service's surface. The far endpoint is dropped when it is
|
|
8
|
+
* itself the subject or one of its descendants — that is INTERIOR, already
|
|
9
|
+
* drawn by the view's own `include *`, and naming it again would put a second
|
|
10
|
+
* include line in the file for a node that is already there.
|
|
11
|
+
*
|
|
12
|
+
* Sorted and de-duplicated because the generated file is a pure function of
|
|
13
|
+
* committed bytes: document order is the parse's, not the author's, and two
|
|
14
|
+
* edges between the same pair must not emit the same include twice.
|
|
15
|
+
*/
|
|
16
|
+
export function peersOf(fqn, relationships) {
|
|
17
|
+
const prefix = `${fqn}.`;
|
|
18
|
+
const own = (id) => id === fqn || id.startsWith(prefix);
|
|
19
|
+
const found = new Set();
|
|
20
|
+
for (const rel of relationships) {
|
|
21
|
+
if (own(rel.source) && !own(rel.target))
|
|
22
|
+
found.add(rel.target);
|
|
23
|
+
if (own(rel.target) && !own(rel.source))
|
|
24
|
+
found.add(rel.source);
|
|
25
|
+
}
|
|
26
|
+
return [...found].sort();
|
|
27
|
+
}
|
|
28
|
+
//# sourceMappingURL=peers.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"peers.js","sourceRoot":"","sources":["../../../../../../src/core/repo/tree/render/service/peers.ts"],"names":[],"mappings":"AAoCA;;;;;;;;;;;;;;GAcG;AACH,MAAM,UAAU,OAAO,CAAC,GAAW,EAAE,aAA6B;IAChE,MAAM,MAAM,GAAG,GAAG,GAAG,GAAG,CAAC;IACzB,MAAM,GAAG,GAAG,CAAC,EAAU,EAAW,EAAE,CAAC,EAAE,KAAK,GAAG,IAAI,EAAE,CAAC,UAAU,CAAC,MAAM,CAAC,CAAC;IACzE,MAAM,KAAK,GAAG,IAAI,GAAG,EAAU,CAAC;IAChC,KAAK,MAAM,GAAG,IAAI,aAAa,EAAE,CAAC;QAChC,IAAI,GAAG,CAAC,GAAG,CAAC,MAAM,CAAC,IAAI,CAAC,GAAG,CAAC,GAAG,CAAC,MAAM,CAAC;YAAE,KAAK,CAAC,GAAG,CAAC,GAAG,CAAC,MAAM,CAAC,CAAC;QAC/D,IAAI,GAAG,CAAC,GAAG,CAAC,MAAM,CAAC,IAAI,CAAC,GAAG,CAAC,GAAG,CAAC,MAAM,CAAC;YAAE,KAAK,CAAC,GAAG,CAAC,GAAG,CAAC,MAAM,CAAC,CAAC;IACjE,CAAC;IACD,OAAO,CAAC,GAAG,KAAK,CAAC,CAAC,IAAI,EAAE,CAAC;AAC3B,CAAC","sourcesContent":["/**\n * The NEIGHBOUR SET of one service: every element the map draws an edge to or\n * from, computed by loam rather than asked of the renderer.\n *\n * This module exists because of a measurement, and the measurement is the whole\n * reason the per-service view is not three wildcard lines. At likec4 1.59.2, on\n * a 120-service map whose services are nested in `group` boxes, the obvious\n * recipe\n *\n * include *\n * include <fqn> -> *\n * include * -> <fqn>\n *\n * drew the GROUP BOXES as the counterparts — `growth`, `catalog`, `support`,\n * `notifications` — instead of the peer services, and NONE of them carried\n * drill-down. LikeC4 resolves a counterpart at the highest ancestor that is not\n * also an ancestor of the subject, so on any map that groups its services —\n * which is exactly what a fleet past about thirty services needs at fleet\n * level — the wildcard form says \"payments/service-0 talks to the growth group\"\n * and navigates nowhere. It looks perfect on a five-service FLAT fixture, which\n * is how it would have shipped.\n *\n * Re-measured here at 1.59.2 on the 58-service / 11-group fleet, subject\n * `payments.svc0` with 8 real peers: the wildcard recipe draws 10 nodes of which\n * four are group boxes; naming the peers draws 11 — the subject, its `api`, its\n * `db` and all 8 peers, each at its own level, each carrying `navigateTo` into\n * that peer's own generated view (6 of the 8 are services and have one; the\n * customer and the broker are not).\n *\n * The counterpart is taken AS THE MAP DECLARES IT — an edge landing on\n * `kafka.orderEvents` names the topic, not the broker — because that is the\n * truer picture and it is the one the map already draws. Anything the render\n * substituted here would be loam re-drawing somebody's architecture.\n */\nimport type { Rel } from \"../../../../c4/likec4.js\";\n\n/**\n * Every element on the far side of an edge touching this service, sorted and\n * de-duplicated.\n *\n * \"Touching\" is the subject OR any descendant of it: an edge into\n * `svc.api` is an edge into the service, and the view that hid it would be\n * lying about the service's surface. The far endpoint is dropped when it is\n * itself the subject or one of its descendants — that is INTERIOR, already\n * drawn by the view's own `include *`, and naming it again would put a second\n * include line in the file for a node that is already there.\n *\n * Sorted and de-duplicated because the generated file is a pure function of\n * committed bytes: document order is the parse's, not the author's, and two\n * edges between the same pair must not emit the same include twice.\n */\nexport function peersOf(fqn: string, relationships: readonly Rel[]): string[] {\n const prefix = `${fqn}.`;\n const own = (id: string): boolean => id === fqn || id.startsWith(prefix);\n const found = new Set<string>();\n for (const rel of relationships) {\n if (own(rel.source) && !own(rel.target)) found.add(rel.target);\n if (own(rel.target) && !own(rel.source)) found.add(rel.source);\n }\n return [...found].sort();\n}\n"]}
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The identifier loam mints for a per-service overview view, and the ONE
|
|
3
|
+
* encoder every generated view id is spelled through.
|
|
4
|
+
*
|
|
5
|
+
* The encoder lives HERE, in the child package, rather than beside the
|
|
6
|
+
* subsystem render that has used it since it was written — and the direction is
|
|
7
|
+
* forced rather than chosen. `../views.ts` imports this package to compose the
|
|
8
|
+
* per-service file, so an import the other way would be the package cycle
|
|
9
|
+
* `scripts/package-graph.mjs` refuses; that script's own advice for exactly this
|
|
10
|
+
* shape is "move the helper; do not import the weight", and an id encoder is the
|
|
11
|
+
* leaf while the two renders are the weight. One copy is also the point: the
|
|
12
|
+
* injectivity argument below holds across BOTH prefixes only while they share an
|
|
13
|
+
* encoder, and a second spelling would be a second chance to collide.
|
|
14
|
+
*
|
|
15
|
+
* INJECTIVITY is a correctness rule and not tidiness. The renderer refuses the
|
|
16
|
+
* whole `architecture/` project over a duplicate view id, and the naive folding
|
|
17
|
+
* (every non-identifier byte and the join separator alike becoming `_`) collided
|
|
18
|
+
* three DISTINCT healthy names onto one identifier — `a_b`, and `b` under `a`,
|
|
19
|
+
* both rendering `subsystem_a_b` — so `validate --all` was green while the
|
|
20
|
+
* renderer refused the fleet map, and `subsystem.name-collision` never fired
|
|
21
|
+
* because the flat namespace really was intact.
|
|
22
|
+
*
|
|
23
|
+
* Hence the encoding: an identifier byte (`[A-Za-z0-9]`) passes through, any
|
|
24
|
+
* other byte becomes `_` + two lowercase hex digits (`.` → `_2e`, `-` → `_2d`,
|
|
25
|
+
* `_` itself → `_5f` — the underscore must be escaped precisely because it is
|
|
26
|
+
* BOTH legal in a name and the separator between path segments). Underscore runs
|
|
27
|
+
* then decode uniquely — one is an escape, two a separator, three a separator
|
|
28
|
+
* followed by an escape — so no two distinct legal names share an identifier. A
|
|
29
|
+
* name `subsystem.name-invalid` already flagged may hold a code point above 0xff
|
|
30
|
+
* whose longer hex could in principle re-collide; it is deterministic still, and
|
|
31
|
+
* that directory is already an error finding by name.
|
|
32
|
+
*
|
|
33
|
+
* The prefix is EXPORTED, with the minting function, because the collision check
|
|
34
|
+
* (`subsystem.view-id-collision`) has to ask the generator what it mints rather
|
|
35
|
+
* than re-derive it. A check carrying its own copy of the escaping goes quiet the
|
|
36
|
+
* day the encoding changes, and going quiet is the failure that whole family
|
|
37
|
+
* exists to prevent.
|
|
38
|
+
*/
|
|
39
|
+
/**
|
|
40
|
+
* The reserved prefix for a generated per-service overview. Distinct from
|
|
41
|
+
* `subsystem_` so the two generated files can never mint the same id for
|
|
42
|
+
* different subjects, and it cannot match the shape LikeC4 mints for an unnamed
|
|
43
|
+
* view (`^view_[0-9a-z]+$`, `core/c4/parsed/view-ids.ts`), so a generated id is
|
|
44
|
+
* never mistaken for a synthesized one.
|
|
45
|
+
*/
|
|
46
|
+
export const SERVICE_VIEW_PREFIX = "service_";
|
|
47
|
+
/** One name, folded to identifier bytes injectively — the banner argues why this shape. */
|
|
48
|
+
export function escapeViewIdSegment(name) {
|
|
49
|
+
return [...name]
|
|
50
|
+
.map((ch) => (/[A-Za-z0-9]/.test(ch) ? ch : `_${ch.codePointAt(0).toString(16).padStart(2, "0")}`))
|
|
51
|
+
.join("");
|
|
52
|
+
}
|
|
53
|
+
/**
|
|
54
|
+
* The view id for one service, from its `services/<id>/` DIRECTORY name.
|
|
55
|
+
*
|
|
56
|
+
* The directory id rather than the map element's title or its fqn, for the same
|
|
57
|
+
* reason the title uses it: it is the name every loam command already addresses
|
|
58
|
+
* the service by, a directory name cannot hold a `/` or a `.`, and it cannot
|
|
59
|
+
* drift when somebody renames a box on the map. Two services cannot share a
|
|
60
|
+
* directory id — the walk's flat namespace already refuses that as
|
|
61
|
+
* `subsystem.name-collision` — so injectivity of the encoder is injectivity of
|
|
62
|
+
* the whole id.
|
|
63
|
+
*/
|
|
64
|
+
export function serviceViewId(id) {
|
|
65
|
+
return `${SERVICE_VIEW_PREFIX}${escapeViewIdSegment(id)}`;
|
|
66
|
+
}
|
|
67
|
+
//# sourceMappingURL=view-id.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"view-id.js","sourceRoot":"","sources":["../../../../../../src/core/repo/tree/render/service/view-id.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAqCG;AAEH;;;;;;GAMG;AACH,MAAM,CAAC,MAAM,mBAAmB,GAAG,UAAU,CAAC;AAE9C,2FAA2F;AAC3F,MAAM,UAAU,mBAAmB,CAAC,IAAY;IAC9C,OAAO,CAAC,GAAG,IAAI,CAAC;SACb,GAAG,CAAC,CAAC,EAAE,EAAE,EAAE,CAAC,CAAC,aAAa,CAAC,IAAI,CAAC,EAAE,CAAC,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,IAAI,EAAE,CAAC,WAAW,CAAC,CAAC,CAAE,CAAC,QAAQ,CAAC,EAAE,CAAC,CAAC,QAAQ,CAAC,CAAC,EAAE,GAAG,CAAC,EAAE,CAAC,CAAC;SACnG,IAAI,CAAC,EAAE,CAAC,CAAC;AACd,CAAC;AAED;;;;;;;;;;GAUG;AACH,MAAM,UAAU,aAAa,CAAC,EAAU;IACtC,OAAO,GAAG,mBAAmB,GAAG,mBAAmB,CAAC,EAAE,CAAC,EAAE,CAAC;AAC5D,CAAC","sourcesContent":["/**\n * The identifier loam mints for a per-service overview view, and the ONE\n * encoder every generated view id is spelled through.\n *\n * The encoder lives HERE, in the child package, rather than beside the\n * subsystem render that has used it since it was written — and the direction is\n * forced rather than chosen. `../views.ts` imports this package to compose the\n * per-service file, so an import the other way would be the package cycle\n * `scripts/package-graph.mjs` refuses; that script's own advice for exactly this\n * shape is \"move the helper; do not import the weight\", and an id encoder is the\n * leaf while the two renders are the weight. One copy is also the point: the\n * injectivity argument below holds across BOTH prefixes only while they share an\n * encoder, and a second spelling would be a second chance to collide.\n *\n * INJECTIVITY is a correctness rule and not tidiness. The renderer refuses the\n * whole `architecture/` project over a duplicate view id, and the naive folding\n * (every non-identifier byte and the join separator alike becoming `_`) collided\n * three DISTINCT healthy names onto one identifier — `a_b`, and `b` under `a`,\n * both rendering `subsystem_a_b` — so `validate --all` was green while the\n * renderer refused the fleet map, and `subsystem.name-collision` never fired\n * because the flat namespace really was intact.\n *\n * Hence the encoding: an identifier byte (`[A-Za-z0-9]`) passes through, any\n * other byte becomes `_` + two lowercase hex digits (`.` → `_2e`, `-` → `_2d`,\n * `_` itself → `_5f` — the underscore must be escaped precisely because it is\n * BOTH legal in a name and the separator between path segments). Underscore runs\n * then decode uniquely — one is an escape, two a separator, three a separator\n * followed by an escape — so no two distinct legal names share an identifier. A\n * name `subsystem.name-invalid` already flagged may hold a code point above 0xff\n * whose longer hex could in principle re-collide; it is deterministic still, and\n * that directory is already an error finding by name.\n *\n * The prefix is EXPORTED, with the minting function, because the collision check\n * (`subsystem.view-id-collision`) has to ask the generator what it mints rather\n * than re-derive it. A check carrying its own copy of the escaping goes quiet the\n * day the encoding changes, and going quiet is the failure that whole family\n * exists to prevent.\n */\n\n/**\n * The reserved prefix for a generated per-service overview. Distinct from\n * `subsystem_` so the two generated files can never mint the same id for\n * different subjects, and it cannot match the shape LikeC4 mints for an unnamed\n * view (`^view_[0-9a-z]+$`, `core/c4/parsed/view-ids.ts`), so a generated id is\n * never mistaken for a synthesized one.\n */\nexport const SERVICE_VIEW_PREFIX = \"service_\";\n\n/** One name, folded to identifier bytes injectively — the banner argues why this shape. */\nexport function escapeViewIdSegment(name: string): string {\n return [...name]\n .map((ch) => (/[A-Za-z0-9]/.test(ch) ? ch : `_${ch.codePointAt(0)!.toString(16).padStart(2, \"0\")}`))\n .join(\"\");\n}\n\n/**\n * The view id for one service, from its `services/<id>/` DIRECTORY name.\n *\n * The directory id rather than the map element's title or its fqn, for the same\n * reason the title uses it: it is the name every loam command already addresses\n * the service by, a directory name cannot hold a `/` or a `.`, and it cannot\n * drift when somebody renames a box on the map. Two services cannot share a\n * directory id — the walk's flat namespace already refuses that as\n * `subsystem.name-collision` — so injectivity of the encoder is injectivity of\n * the whole id.\n */\nexport function serviceViewId(id: string): string {\n return `${SERVICE_VIEW_PREFIX}${escapeViewIdSegment(id)}`;\n}\n"]}
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
import { GENERATED_MARK } from "./header.js";
|
|
2
|
+
import { peersOf } from "./peers.js";
|
|
3
|
+
import { serviceViewId } from "./view-id.js";
|
|
4
|
+
/**
|
|
5
|
+
* The global style group these views reference when the map declares one, the
|
|
6
|
+
* exact counterpart of `SUBSYSTEM_STYLE_GROUP` and reserved on the same terms:
|
|
7
|
+
* presence of the DECLARATION is the whole opt-in, because referencing an
|
|
8
|
+
* undeclared style id is a parse error that blanks the entire `architecture/`
|
|
9
|
+
* project in the renderer. The name mirrors the file it styles.
|
|
10
|
+
*/
|
|
11
|
+
export const SERVICE_STYLE_GROUP = "services";
|
|
12
|
+
/**
|
|
13
|
+
* The header every generated file opens with — stable bytes, part of the
|
|
14
|
+
* content-compare contract, naming the regenerator so a reader who found the
|
|
15
|
+
* file before the docs knows what owns it. `loam subsystem sync` regenerates
|
|
16
|
+
* BOTH generated files, which is why one command is named here and why one
|
|
17
|
+
* finding code (`subsystem.views-stale`) honestly covers both.
|
|
18
|
+
*
|
|
19
|
+
* It OPENS with the shared `GENERATED_MARK` rather than spelling it, because
|
|
20
|
+
* the same bytes are what `loam subsystem sync` reads back off disk to decide
|
|
21
|
+
* whether the file at this reserved path is loam's to rewrite at all
|
|
22
|
+
* (`./header.js`). A second literal here is how a file loam wrote stops being
|
|
23
|
+
* recognised as loam's.
|
|
24
|
+
*/
|
|
25
|
+
const HEADER = `${GENERATED_MARK} — one overview view per service under services/, drawn from the fleet map.\n` +
|
|
26
|
+
"// Do not edit: `loam subsystem sync` regenerates this file from the directories and the map,\n" +
|
|
27
|
+
"// and `loam validate --all` reports `subsystem.views-stale` when it drifts.\n" +
|
|
28
|
+
"// A navigation aid, not the fleet's architecture: author your own diagrams elsewhere.\n";
|
|
29
|
+
/**
|
|
30
|
+
* Render the whole file, or null when no service resolves to a map element —
|
|
31
|
+
* the file must then be ABSENT, so a fleet whose map draws nothing never
|
|
32
|
+
* carries a generated artifact that says nothing.
|
|
33
|
+
*
|
|
34
|
+
* Services sorted by id and peers sorted by id, one `include` per line: the
|
|
35
|
+
* output is byte-reproducible on any machine from the same committed bytes (no
|
|
36
|
+
* timestamps, no absolute paths, no readdir-order dependence), and two
|
|
37
|
+
* concurrent changes to different services touch different lines, so git merges
|
|
38
|
+
* the file without intervention.
|
|
39
|
+
*/
|
|
40
|
+
export function renderServiceViews(input) {
|
|
41
|
+
if (input.subjects.length === 0)
|
|
42
|
+
return null;
|
|
43
|
+
const lines = [HEADER, ...(input.disclosure === null ? [] : [input.disclosure]), "views {"];
|
|
44
|
+
const subjects = [...input.subjects].sort((a, b) => (a.id < b.id ? -1 : 1));
|
|
45
|
+
for (const [i, subject] of subjects.entries()) {
|
|
46
|
+
if (i > 0)
|
|
47
|
+
lines.push("");
|
|
48
|
+
lines.push(...viewLines(subject, peersOf(subject.fqn, input.relationships), input.styled));
|
|
49
|
+
}
|
|
50
|
+
lines.push("}");
|
|
51
|
+
return lines.join("\n") + "\n";
|
|
52
|
+
}
|
|
53
|
+
/**
|
|
54
|
+
* One view, in the order the parser demands: metadata FIRST, and that is a hard
|
|
55
|
+
* constraint rather than a taste — measured, an `include` line followed by
|
|
56
|
+
* `title` is a parse error ("Expecting token of type '}' but found `title`"),
|
|
57
|
+
* and one parse error in this file blanks the whole `architecture/` project in
|
|
58
|
+
* the renderer. `global style` sits after `title` and before anything that
|
|
59
|
+
* includes, where the same pin measured it parsing with zero errors.
|
|
60
|
+
*/
|
|
61
|
+
function viewLines(subject, peers, styled) {
|
|
62
|
+
const { fqn } = subject;
|
|
63
|
+
const out = [` view ${serviceViewId(subject.id)} of ${fqn} {`, ` title '${subject.title}'`];
|
|
64
|
+
if (styled)
|
|
65
|
+
out.push(` global style ${SERVICE_STYLE_GROUP}`);
|
|
66
|
+
out.push(" include *");
|
|
67
|
+
for (const peer of peers)
|
|
68
|
+
out.push(` include ${peer}`);
|
|
69
|
+
out.push(" exclude * -> *", ` include -> ${fqn}`, ` include ${fqn} ->`);
|
|
70
|
+
// The interior wiring, last. Measured: `include <fqn>.* -> <fqn>.*` on a
|
|
71
|
+
// CHILDLESS element is legal — `likec4 validate` reports valid and the export
|
|
72
|
+
// carries zero errors — so one generated line serves both model shapes: an
|
|
73
|
+
// EXTENDING model's containers live in the root project and are drawn, a
|
|
74
|
+
// STANDALONE model's are not in this project and the line matches nothing.
|
|
75
|
+
out.push(` include ${fqn}.* -> ${fqn}.*`, " }");
|
|
76
|
+
return out;
|
|
77
|
+
}
|
|
78
|
+
//# sourceMappingURL=views.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"views.js","sourceRoot":"","sources":["../../../../../../src/core/repo/tree/render/service/views.ts"],"names":[],"mappings":"AAkEA,OAAO,EAAE,cAAc,EAAE,MAAM,aAAa,CAAC;AAC7C,OAAO,EAAE,OAAO,EAAE,MAAM,YAAY,CAAC;AACrC,OAAO,EAAE,aAAa,EAAE,MAAM,cAAc,CAAC;AAE7C;;;;;;GAMG;AACH,MAAM,CAAC,MAAM,mBAAmB,GAAG,UAAU,CAAC;AAuB9C;;;;;;;;;;;;GAYG;AACH,MAAM,MAAM,GACV,GAAG,cAAc,+EAA+E;IAChG,iGAAiG;IACjG,gFAAgF;IAChF,0FAA0F,CAAC;AAE7F;;;;;;;;;;GAUG;AACH,MAAM,UAAU,kBAAkB,CAAC,KAAwB;IACzD,IAAI,KAAK,CAAC,QAAQ,CAAC,MAAM,KAAK,CAAC;QAAE,OAAO,IAAI,CAAC;IAC7C,MAAM,KAAK,GAAa,CAAC,MAAM,EAAE,GAAG,CAAC,KAAK,CAAC,UAAU,KAAK,IAAI,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,CAAC,KAAK,CAAC,UAAU,CAAC,CAAC,EAAE,SAAS,CAAC,CAAC;IACtG,MAAM,QAAQ,GAAG,CAAC,GAAG,KAAK,CAAC,QAAQ,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC,EAAE,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,CAAC,EAAE,GAAG,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC;IAC5E,KAAK,MAAM,CAAC,CAAC,EAAE,OAAO,CAAC,IAAI,QAAQ,CAAC,OAAO,EAAE,EAAE,CAAC;QAC9C,IAAI,CAAC,GAAG,CAAC;YAAE,KAAK,CAAC,IAAI,CAAC,EAAE,CAAC,CAAC;QAC1B,KAAK,CAAC,IAAI,CAAC,GAAG,SAAS,CAAC,OAAO,EAAE,OAAO,CAAC,OAAO,CAAC,GAAG,EAAE,KAAK,CAAC,aAAa,CAAC,EAAE,KAAK,CAAC,MAAM,CAAC,CAAC,CAAC;IAC7F,CAAC;IACD,KAAK,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC;IAChB,OAAO,KAAK,CAAC,IAAI,CAAC,IAAI,CAAC,GAAG,IAAI,CAAC;AACjC,CAAC;AAED;;;;;;;GAOG;AACH,SAAS,SAAS,CAAC,OAA2B,EAAE,KAAwB,EAAE,MAAe;IACvF,MAAM,EAAE,GAAG,EAAE,GAAG,OAAO,CAAC;IACxB,MAAM,GAAG,GAAG,CAAC,UAAU,aAAa,CAAC,OAAO,CAAC,EAAE,CAAC,OAAO,GAAG,IAAI,EAAE,cAAc,OAAO,CAAC,KAAK,GAAG,CAAC,CAAC;IAChG,IAAI,MAAM;QAAE,GAAG,CAAC,IAAI,CAAC,oBAAoB,mBAAmB,EAAE,CAAC,CAAC;IAChE,GAAG,CAAC,IAAI,CAAC,eAAe,CAAC,CAAC;IAC1B,KAAK,MAAM,IAAI,IAAI,KAAK;QAAE,GAAG,CAAC,IAAI,CAAC,eAAe,IAAI,EAAE,CAAC,CAAC;IAC1D,GAAG,CAAC,IAAI,CAAC,oBAAoB,EAAE,kBAAkB,GAAG,EAAE,EAAE,eAAe,GAAG,KAAK,CAAC,CAAC;IACjF,yEAAyE;IACzE,8EAA8E;IAC9E,2EAA2E;IAC3E,yEAAyE;IACzE,2EAA2E;IAC3E,GAAG,CAAC,IAAI,CAAC,eAAe,GAAG,SAAS,GAAG,IAAI,EAAE,KAAK,CAAC,CAAC;IACpD,OAAO,GAAG,CAAC;AACb,CAAC","sourcesContent":["/**\n * `architecture/services.likec4` — one generated LikeC4 view per service,\n * showing that service, its own interior, and its links to everything else the\n * fleet map draws.\n *\n * GENERATED, never authored, exactly like the subsystem views beside it:\n * authored bytes stay authored (archive's verbatim-splice promise), `loam\n * subsystem sync` owns this file outright, and staleness against the tree is a\n * content compare and never a parse — a views-only document does not parse\n * standalone, so nothing in loam ever loads it. docs/DESIGN.md rule 26 does not\n * change with a second file: what a view SHOWS stays unread in every document,\n * this one included.\n *\n * ## The recipe, and why the obvious one is wrong\n *\n * Measured at likec4 1.59.2 on a 120-service and a 58-service fleet. Each view\n * is written as\n *\n * view service_<id> of <fqn> {\n * title '<subsystem path> / <service id>'\n * global style services // only when the map declares the group\n * include * // the service and its own interior\n * include <peer> // one line per peer, id-sorted\n * exclude * -> * // drop the peer-to-peer cross traffic\n * include -> <fqn> // its inbound edges\n * include <fqn> -> // its outbound edges\n * include <fqn>.* -> <fqn>.* // its internal wiring\n * }\n *\n * Three things in that shape are measurements rather than taste, and each one\n * fails SILENTLY — zero errors, a wrong picture — if it is tidied away:\n *\n * 1. **The peers are named, not wildcarded.** `./peers.ts` carries the\n * measurement: `include <fqn> -> *` collapses every counterpart to its\n * GROUP box and yields zero drill-down on any map that nests services.\n * 2. **The edge predicates are UNARY.** `include -> <fqn>` and `include <fqn>\n * ->`, never the binary `include * -> <fqn>` / `include <fqn> -> *`.\n * Measured on `payments.svc0` of the 58-service fleet, swapping the two\n * unary lines for their binary forms takes the view from 11 nodes to 15:\n * the four parent GROUP boxes of the named peers come back as nodes of\n * their own, so the view regrows exactly what rule 1 removed. Zero errors\n * either way, which is what makes it worth a comment.\n * 3. **`exclude * -> *` sits between the includes and the edge lines.** Without\n * it the named peers bring their own cross traffic with them: on the same\n * subject, 24 edges instead of 9, on a view about one service.\n *\n * `of <fqn>` is the navigation half and costs one keyword: LikeC4 computes\n * `navigateTo` into a view declared `of` an element from every OTHER view that\n * draws that element, with no authored links. That is what makes the fleet map,\n * every subsystem view and every other service view click-through into a\n * service. Two views of one element are legal — the first document\n * alphabetically wins the default target and both stay reachable, because the\n * subject's own node cycles to the other — so a team's authored element view in\n * `landscape.likec4` keeps precedence over this file, which sorts later.\n *\n * ## What arrives from outside\n *\n * Everything about the TREE — which services exist, what each one's element is,\n * what its title reads — arrives composed, in `ServiceViewSubject`. This package\n * may not import `../views.ts` or `../text.ts`: the parent imports it, and the\n * reverse edge would be the package cycle `scripts/package-graph.mjs` refuses.\n * So the title arrives already through `likec4String` rather than escaped a\n * second time here; a second escaper is how one of the two forgets an\n * apostrophe and ships a fleet-wide parse error.\n */\nimport type { Rel } from \"../../../../c4/likec4.js\";\nimport { GENERATED_MARK } from \"./header.js\";\nimport { peersOf } from \"./peers.js\";\nimport { serviceViewId } from \"./view-id.js\";\n\n/**\n * The global style group these views reference when the map declares one, the\n * exact counterpart of `SUBSYSTEM_STYLE_GROUP` and reserved on the same terms:\n * presence of the DECLARATION is the whole opt-in, because referencing an\n * undeclared style id is a parse error that blanks the entire `architecture/`\n * project in the renderer. The name mirrors the file it styles.\n */\nexport const SERVICE_STYLE_GROUP = \"services\";\n\n/** One service this file draws — resolved against the map, and already escaped. */\nexport interface ServiceViewSubject {\n /** The `services/<id>/` directory id: the sort key and the view id's source. */\n id: string;\n /** The landscape element it resolves to — the view's `of`, and its subject. */\n fqn: string;\n /** `<subsystem path> / <service id>`, already through `likec4String`. */\n title: string;\n}\n\n/** Everything the file render reads, as one record — see the banner on why it arrives composed. */\nexport interface ServiceViewsInput {\n subjects: readonly ServiceViewSubject[];\n /** The map's edges, from which each subject's peer set is computed. */\n relationships: readonly Rel[];\n /** Emit `global style services` — true only when the map DECLARES that group. */\n styled: boolean;\n /** The one file-level comment the render may write, or null. */\n disclosure: string | null;\n}\n\n/**\n * The header every generated file opens with — stable bytes, part of the\n * content-compare contract, naming the regenerator so a reader who found the\n * file before the docs knows what owns it. `loam subsystem sync` regenerates\n * BOTH generated files, which is why one command is named here and why one\n * finding code (`subsystem.views-stale`) honestly covers both.\n *\n * It OPENS with the shared `GENERATED_MARK` rather than spelling it, because\n * the same bytes are what `loam subsystem sync` reads back off disk to decide\n * whether the file at this reserved path is loam's to rewrite at all\n * (`./header.js`). A second literal here is how a file loam wrote stops being\n * recognised as loam's.\n */\nconst HEADER =\n `${GENERATED_MARK} — one overview view per service under services/, drawn from the fleet map.\\n` +\n \"// Do not edit: `loam subsystem sync` regenerates this file from the directories and the map,\\n\" +\n \"// and `loam validate --all` reports `subsystem.views-stale` when it drifts.\\n\" +\n \"// A navigation aid, not the fleet's architecture: author your own diagrams elsewhere.\\n\";\n\n/**\n * Render the whole file, or null when no service resolves to a map element —\n * the file must then be ABSENT, so a fleet whose map draws nothing never\n * carries a generated artifact that says nothing.\n *\n * Services sorted by id and peers sorted by id, one `include` per line: the\n * output is byte-reproducible on any machine from the same committed bytes (no\n * timestamps, no absolute paths, no readdir-order dependence), and two\n * concurrent changes to different services touch different lines, so git merges\n * the file without intervention.\n */\nexport function renderServiceViews(input: ServiceViewsInput): string | null {\n if (input.subjects.length === 0) return null;\n const lines: string[] = [HEADER, ...(input.disclosure === null ? [] : [input.disclosure]), \"views {\"];\n const subjects = [...input.subjects].sort((a, b) => (a.id < b.id ? -1 : 1));\n for (const [i, subject] of subjects.entries()) {\n if (i > 0) lines.push(\"\");\n lines.push(...viewLines(subject, peersOf(subject.fqn, input.relationships), input.styled));\n }\n lines.push(\"}\");\n return lines.join(\"\\n\") + \"\\n\";\n}\n\n/**\n * One view, in the order the parser demands: metadata FIRST, and that is a hard\n * constraint rather than a taste — measured, an `include` line followed by\n * `title` is a parse error (\"Expecting token of type '}' but found `title`\"),\n * and one parse error in this file blanks the whole `architecture/` project in\n * the renderer. `global style` sits after `title` and before anything that\n * includes, where the same pin measured it parsing with zero errors.\n */\nfunction viewLines(subject: ServiceViewSubject, peers: readonly string[], styled: boolean): string[] {\n const { fqn } = subject;\n const out = [` view ${serviceViewId(subject.id)} of ${fqn} {`, ` title '${subject.title}'`];\n if (styled) out.push(` global style ${SERVICE_STYLE_GROUP}`);\n out.push(\" include *\");\n for (const peer of peers) out.push(` include ${peer}`);\n out.push(\" exclude * -> *\", ` include -> ${fqn}`, ` include ${fqn} ->`);\n // The interior wiring, last. Measured: `include <fqn>.* -> <fqn>.*` on a\n // CHILDLESS element is legal — `likec4 validate` reports valid and the export\n // carries zero errors — so one generated line serves both model shapes: an\n // EXTENDING model's containers live in the root project and are drawn, a\n // STANDALONE model's are not in this project and the line matches nothing.\n out.push(` include ${fqn}.* -> ${fqn}.*`, \" }\");\n return out;\n}\n"]}
|
|
@@ -16,10 +16,26 @@
|
|
|
16
16
|
*/
|
|
17
17
|
import { existsSync } from "node:fs";
|
|
18
18
|
import { readFile } from "node:fs/promises";
|
|
19
|
-
import {} from "
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
19
|
+
import { renderServiceOverviews, renderSubsystemViews, viewsAgree } from "./views.js";
|
|
20
|
+
export async function viewsState(path, tree, map) {
|
|
21
|
+
return stateOf(path, renderSubsystemViews(tree, map));
|
|
22
|
+
}
|
|
23
|
+
/**
|
|
24
|
+
* The same question about the OTHER generated file, `architecture/services.likec4`.
|
|
25
|
+
*
|
|
26
|
+
* A second exported function rather than a `which` parameter, and a second call
|
|
27
|
+
* at every asker rather than one that answers about both: the two files go stale
|
|
28
|
+
* for different reasons and are repaired in one command, so a caller that wants
|
|
29
|
+
* both asks twice and names each file in its own finding —
|
|
30
|
+
* `subsystem.views-stale` carries the path precisely so one code can honestly
|
|
31
|
+
* cover two files. Folding them into one state would lose which one drifted,
|
|
32
|
+
* which is the only thing the message has to say.
|
|
33
|
+
*/
|
|
34
|
+
export async function serviceViewsState(path, tree, map) {
|
|
35
|
+
return stateOf(path, renderServiceOverviews(tree, map));
|
|
36
|
+
}
|
|
37
|
+
/** Read the file, or record its absence, and compare — the half both askers share. */
|
|
38
|
+
async function stateOf(path, expected) {
|
|
23
39
|
const actual = existsSync(path) ? await readFile(path, "utf8") : null;
|
|
24
40
|
return { actual, expected, agrees: viewsAgree(actual, expected) };
|
|
25
41
|
}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"stale.js","sourceRoot":"","sources":["../../../../../src/core/repo/tree/render/stale.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;GAeG;AACH,OAAO,EAAE,UAAU,EAAE,MAAM,SAAS,CAAC;AACrC,OAAO,EAAE,QAAQ,EAAE,MAAM,kBAAkB,CAAC;
|
|
1
|
+
{"version":3,"file":"stale.js","sourceRoot":"","sources":["../../../../../src/core/repo/tree/render/stale.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;GAeG;AACH,OAAO,EAAE,UAAU,EAAE,MAAM,SAAS,CAAC;AACrC,OAAO,EAAE,QAAQ,EAAE,MAAM,kBAAkB,CAAC;AAE5C,OAAO,EAAiB,sBAAsB,EAAE,oBAAoB,EAAE,UAAU,EAAE,MAAM,YAAY,CAAC;AAgBrG,MAAM,CAAC,KAAK,UAAU,UAAU,CAAC,IAAY,EAAE,IAAe,EAAE,GAAa;IAC3E,OAAO,OAAO,CAAC,IAAI,EAAE,oBAAoB,CAAC,IAAI,EAAE,GAAG,CAAC,CAAC,CAAC;AACxD,CAAC;AAED;;;;;;;;;;GAUG;AACH,MAAM,CAAC,KAAK,UAAU,iBAAiB,CAAC,IAAY,EAAE,IAAe,EAAE,GAAa;IAClF,OAAO,OAAO,CAAC,IAAI,EAAE,sBAAsB,CAAC,IAAI,EAAE,GAAG,CAAC,CAAC,CAAC;AAC1D,CAAC;AAED,sFAAsF;AACtF,KAAK,UAAU,OAAO,CAAC,IAAY,EAAE,QAAuB;IAC1D,MAAM,MAAM,GAAG,UAAU,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC,MAAM,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC,CAAC,CAAC,CAAC,IAAI,CAAC;IACtE,OAAO,EAAE,MAAM,EAAE,QAAQ,EAAE,MAAM,EAAE,UAAU,CAAC,MAAM,EAAE,QAAQ,CAAC,EAAE,CAAC;AACpE,CAAC","sourcesContent":["/**\n * Is the generated file on disk what this tree and this map render to?\n *\n * One function, because the question now has three askers — `validate --all`\n * (which turns a disagreement into `subsystem.views-stale`), `loam subsystem\n * sync` and the write verbs (which repair it), and `loam archive` (which must\n * not report a repo current when it has just made the file stale). Three\n * private compares would be three chances to answer differently about one\n * file, and the module that mints the bytes is the one that owes the answer.\n *\n * The PATH arrives as a parameter rather than a `DocsDir`: `repo/paths.ts`\n * owns the spelling of every file in a docs repo, and importing it here would\n * close the package cycle `repo → repo/tree → repo/tree/render → repo` that\n * `scripts/package-graph.mjs` exists to refuse. `walk.ts` pays the same price\n * for the same reason and says so.\n */\nimport { existsSync } from \"node:fs\";\nimport { readFile } from \"node:fs/promises\";\nimport type { FleetTree } from \"../walk.js\";\nimport { type MapFacts, renderServiceOverviews, renderSubsystemViews, viewsAgree } from \"./views.js\";\n\n/**\n * The three states, kept apart: absent, must-be-absent, and\n * present-but-different are three different sentences, and a fleet whose views\n * file does not exist is not a fleet whose views file is blank.\n */\nexport interface ViewsState {\n /** The bytes on disk, or null when the file is not there. */\n actual: string | null;\n /** The bytes the tree renders to, or null when the file must be absent. */\n expected: string | null;\n /** Do the two say the same thing? Content, not bytes — `viewsAgree` says why. */\n agrees: boolean;\n}\n\nexport async function viewsState(path: string, tree: FleetTree, map: MapFacts): Promise<ViewsState> {\n return stateOf(path, renderSubsystemViews(tree, map));\n}\n\n/**\n * The same question about the OTHER generated file, `architecture/services.likec4`.\n *\n * A second exported function rather than a `which` parameter, and a second call\n * at every asker rather than one that answers about both: the two files go stale\n * for different reasons and are repaired in one command, so a caller that wants\n * both asks twice and names each file in its own finding —\n * `subsystem.views-stale` carries the path precisely so one code can honestly\n * cover two files. Folding them into one state would lose which one drifted,\n * which is the only thing the message has to say.\n */\nexport async function serviceViewsState(path: string, tree: FleetTree, map: MapFacts): Promise<ViewsState> {\n return stateOf(path, renderServiceOverviews(tree, map));\n}\n\n/** Read the file, or record its absence, and compare — the half both askers share. */\nasync function stateOf(path: string, expected: string | null): Promise<ViewsState> {\n const actual = existsSync(path) ? await readFile(path, \"utf8\") : null;\n return { actual, expected, agrees: viewsAgree(actual, expected) };\n}\n"]}
|