@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
|
@@ -4,14 +4,69 @@
|
|
|
4
4
|
*
|
|
5
5
|
* - `architecture/subsystems.likec4`, the GENERATED views file. Recomputed from
|
|
6
6
|
* the tree on every run, compared, written when it differs, removed when the
|
|
7
|
-
* tree has no subsystems.
|
|
7
|
+
* tree has no subsystems. One of the two repairs for `subsystem.views-stale`.
|
|
8
|
+
* Left exactly as it is when the `architecture/` project does not parse and
|
|
9
|
+
* the file already exists — `action: "blocked"`, below.
|
|
10
|
+
* - `architecture/services.likec4`, the OTHER generated views file: one
|
|
11
|
+
* overview view per service the map draws, so a reader can open a service and
|
|
12
|
+
* see what it touches. Same rules, same commit, same code when it drifts —
|
|
13
|
+
* which is what makes ONE `subsystem.views-stale` honest about two files, and
|
|
14
|
+
* why this command is named as the repair for both. Removed when NO service
|
|
15
|
+
* resolves to a map element, exactly as the subsystem file is removed when
|
|
16
|
+
* the tree has no subsystems. `serviceViews` is its `action`; `action` still
|
|
17
|
+
* means the subsystem file alone.
|
|
8
18
|
* - `services/<…>/<id>/likec4.config.json`, one LikeC4 project file beside
|
|
9
|
-
* every
|
|
10
|
-
*
|
|
19
|
+
* every model that STANDS ALONE. Never compared, never rewritten. Created
|
|
20
|
+
* where a standalone model has none, and DELETED where an extending model has
|
|
21
|
+
* one: measured at the 1.59.2 pin, that nested project claims the model out of
|
|
22
|
+
* the root project and the fleet loses the service's interior
|
|
23
|
+
* (`core/repo/tree/render/projects.ts` carries the measurement). This is the
|
|
24
|
+
* repair `service.likec4-config-stray` has always named and, until now, did
|
|
25
|
+
* not perform.
|
|
26
|
+
* - the ROOT `likec4.config.json`'s `exclude` list, and ONLY that key. Derived
|
|
27
|
+
* from the shapes on disk on every run and rewritten when it differs, because
|
|
28
|
+
* the two shapes need opposite answers from it: a standalone model must be
|
|
29
|
+
* excluded (inside the root project every kind it declares is a duplicate)
|
|
30
|
+
* and an extending model must not be (the root project is the only place it
|
|
31
|
+
* parses). `planExclude` below states the gate that keeps an old repository
|
|
32
|
+
* untouched, and `core/c4/service-model/renderer.ts` states which half of the
|
|
33
|
+
* list is loam's and which is the team's. One entry loam did NOT write is
|
|
34
|
+
* taken anyway, and only this one: an entry naming a directory that HIDES an
|
|
35
|
+
* extending model — a subsystem-wide `services/platform/**` survived every
|
|
36
|
+
* sync while `service.model-excluded` named this command as the repair.
|
|
11
37
|
*
|
|
12
|
-
*
|
|
13
|
-
*
|
|
14
|
-
*
|
|
38
|
+
* NEITHER views file is written over a document loam did not write. Both are
|
|
39
|
+
* filenames loam RESERVES inside `architecture/`, a directory the team owns, and
|
|
40
|
+
* both are rewritten whole — so a repository that already kept a hand-authored
|
|
41
|
+
* diagram at one of those paths would have lost it, silently, on the first sync
|
|
42
|
+
* after the release that reserved the name. One predicate answers it for both
|
|
43
|
+
* (`readGeneratedFile`, off the file's own first line), the run reports
|
|
44
|
+
* `refused` for that file, writes nothing for it, and exits non-zero. A hand
|
|
45
|
+
* EDIT of a generated file is a different thing and is still repaired: it keeps
|
|
46
|
+
* the mark, so it is loam's file with wrong bytes in it, which is exactly what
|
|
47
|
+
* `subsystem.views-stale` says.
|
|
48
|
+
*
|
|
49
|
+
* THIS COMMAND IS NOT THE ONLY WRITER, and for a while it was the only one
|
|
50
|
+
* holding that line — reproduced, with a hand-authored
|
|
51
|
+
* `architecture/services.likec4` in the tree: `loam subsystem new platform`
|
|
52
|
+
* printed "architecture/services.likec4 updated" and destroyed it. The predicate
|
|
53
|
+
* and the refusal sentence therefore live below every writer
|
|
54
|
+
* (`core/repo/tree/render/service/header.ts`), and `./txn/txn.ts` and
|
|
55
|
+
* `../seed/plan.ts` ask the same question of the same two paths.
|
|
56
|
+
*
|
|
57
|
+
* WHAT DIFFERS IS THE POLICY, not the question, and the difference is the shape
|
|
58
|
+
* of the operation rather than a second opinion about ownership. Sync REPORTS
|
|
59
|
+
* `refused` for the one file and does the rest: nothing it writes depends on
|
|
60
|
+
* that file, every part of it is idempotent, and a run that also created a
|
|
61
|
+
* project file really did create it. A tree-changing verb and `seed` REFUSE
|
|
62
|
+
* WHOLE, because their generated file has to land in the same commit as the
|
|
63
|
+
* change to the tree — completing half of that is the very state the commit
|
|
64
|
+
* window exists never to leave between two commits, and `subsystem move` would
|
|
65
|
+
* additionally leave git-staged renames behind it.
|
|
66
|
+
*
|
|
67
|
+
* The rules differ because the files answer to different owners. The
|
|
68
|
+
* views files are a pure function of the tree and the committed landscape, so
|
|
69
|
+
* every byte of them is loam's and a hand edit is by definition stale. The
|
|
15
70
|
* project file's bytes are a function of the service id ALONE — the identity
|
|
16
71
|
* no verb ever changes — so nothing can make it stale, and the keys LikeC4
|
|
17
72
|
* lets a team add to it (`title`, styles, a contact) are the team's; a compare
|
|
@@ -21,12 +76,12 @@
|
|
|
21
76
|
* grades with (`missingProjects`), so the grader can never name a gap this
|
|
22
77
|
* writer would not fill.
|
|
23
78
|
*
|
|
24
|
-
*
|
|
25
|
-
* Without it the tree is not a loadable
|
|
26
|
-
* merge into one project and report every
|
|
27
|
-
* nested project beside a missing root is a
|
|
28
|
-
* cannot be opened. `loam doctor` prints the root
|
|
29
|
-
* writes nothing for the services until it is there.
|
|
79
|
+
* Nothing under `services/` is written, and no `exclude` is recomputed, unless
|
|
80
|
+
* the ROOT `likec4.config.json` exists. Without it the tree is not a loadable
|
|
81
|
+
* workspace at all — every model would merge into one project and report every
|
|
82
|
+
* declaration as a duplicate — and a nested project beside a missing root is a
|
|
83
|
+
* promise about a renderer that cannot be opened. `loam doctor` prints the root
|
|
84
|
+
* file; sync says so and writes nothing for the services until it is there.
|
|
30
85
|
*
|
|
31
86
|
* Every write goes through the same lock + journaled transaction every docs
|
|
32
87
|
* writer uses (`commitStaged`), and both kinds land in ONE commit: a
|
|
@@ -39,21 +94,27 @@
|
|
|
39
94
|
* The expected views bytes come from `./txn/views.ts` — the same module every
|
|
40
95
|
* writer verb renders from, so sync and the transactions cannot disagree
|
|
41
96
|
* about what the file should say. The subsystem verbs' own commit window
|
|
42
|
-
* (`./txn/txn.ts`) does NOT write project files, and records why.
|
|
97
|
+
* (`./txn/txn.ts`) does NOT write project files, and records why. What this run
|
|
98
|
+
* SAYS it did is `./txn/report.ts`: the third answer about the same three files,
|
|
99
|
+
* and the phase that is not part of the commit.
|
|
43
100
|
*/
|
|
44
101
|
import { existsSync } from "node:fs";
|
|
45
|
-
import {
|
|
46
|
-
import {
|
|
47
|
-
import {
|
|
102
|
+
import { resolve } from "node:path";
|
|
103
|
+
import { readModelShapes } from "../../core/c4/service-model/shape.js";
|
|
104
|
+
import { fail, repoPath } from "../../core/envelope/json.js";
|
|
105
|
+
import { LIKEC4_ROOT_PROJECT, rootProjectPath, serviceRenderPaths } from "../../core/repo/paths.js";
|
|
48
106
|
import { listFleetTree } from "../../core/repo/repo.js";
|
|
49
|
-
import { renderServiceProject, surveyProjects } from "../../core/repo/tree/render/projects.js";
|
|
107
|
+
import { renderServiceProject, surveyProjects, } from "../../core/repo/tree/render/projects.js";
|
|
50
108
|
import { stageWrites } from "../../core/staging/commit.js";
|
|
51
109
|
import { InterruptedCommitError } from "../../core/staging/interrupted.js";
|
|
52
110
|
import { acquireDocsLockWaiting, DocsBusyError, LOCK_WAIT_MS } from "../../core/staging/lock.js";
|
|
53
111
|
import { recoverInterruptedCommit } from "../../core/staging/recovery/recover.js";
|
|
54
112
|
import { commitStaged } from "../../core/staging/txn/transaction.js";
|
|
55
113
|
import { planWrite } from "../../core/staging/writes.js";
|
|
114
|
+
import { readGeneratedFile } from "../../core/repo/tree/render/service/header.js";
|
|
56
115
|
import { viewsAgree } from "../../core/repo/tree/render/views.js";
|
|
116
|
+
import { NO_EXCLUDE, planExclude } from "./txn/exclude.js";
|
|
117
|
+
import { reportSync } from "./txn/report.js";
|
|
57
118
|
import { expectedViews } from "./txn/views.js";
|
|
58
119
|
export async function runSync(docsDir, json) {
|
|
59
120
|
let releaseLock;
|
|
@@ -80,39 +141,70 @@ export async function runSync(docsDir, json) {
|
|
|
80
141
|
return;
|
|
81
142
|
}
|
|
82
143
|
const tree = await listFleetTree(docsDir);
|
|
144
|
+
// BOTH generated files, from ONE read of the map — `./txn/views.ts` says why
|
|
145
|
+
// the pair is one answer and why the parsed document, never a literal, is
|
|
146
|
+
// what the per-service peer sets are computed from.
|
|
83
147
|
const expected = await expectedViews(docsDir, tree);
|
|
84
|
-
const
|
|
85
|
-
const
|
|
86
|
-
//
|
|
87
|
-
//
|
|
88
|
-
//
|
|
89
|
-
//
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
const
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
148
|
+
const views = await planFile(expected.subsystems, expected.known);
|
|
149
|
+
const services = await planFile(expected.services, expected.known);
|
|
150
|
+
// ONE read of every model's bytes for the whole run, and the same scan the
|
|
151
|
+
// grader uses: it decides which directories are owed a project file AND
|
|
152
|
+
// which the root project must exclude, and those are opposite answers for
|
|
153
|
+
// the two shapes (`core/repo/tree/render/projects.ts`).
|
|
154
|
+
const shapes = await readModelShapes(tree.services.map((svc) => serviceRenderPaths(svc.dir).model));
|
|
155
|
+
const plan = planProjects(docsDir, tree.services, shapes);
|
|
156
|
+
const exclude = plan.root ? await planExclude(docsDir, tree.services, plan.models) : NO_EXCLUDE;
|
|
157
|
+
// The plan IS the report: every gap is created and every stray removed in
|
|
158
|
+
// the one commit below, and the early return is the branch where both lists
|
|
159
|
+
// are empty.
|
|
160
|
+
const projects = () => ({
|
|
96
161
|
root: plan.root,
|
|
97
|
-
|
|
162
|
+
// SORTED BY PATH, which is what the contract says these keys hold and what
|
|
163
|
+
// the survey's own order is not: `surveyProjects` sorts by service id, so
|
|
164
|
+
// `services/svc-a/…` came out before `services/platform/svc-d/…` and a
|
|
165
|
+
// consumer told the list was "sorted" got an order no path sort produces
|
|
166
|
+
// (re-verification 2026-09-04, area C item 6). The text view prints the
|
|
167
|
+
// same array, so the two orders cannot drift apart.
|
|
168
|
+
created: plan.gaps.map((gap) => repoPath(docsDir, gap.path)).sort(),
|
|
169
|
+
removed: plan.strays.map((stray) => repoPath(docsDir, stray.path)).sort(),
|
|
98
170
|
current: plan.current,
|
|
99
171
|
modelled: plan.modelled,
|
|
172
|
+
exclude,
|
|
100
173
|
});
|
|
101
|
-
|
|
102
|
-
|
|
174
|
+
// `action` still describes `architecture/subsystems.likec4` ALONE — a run
|
|
175
|
+
// that only created project files, or only rewrote the per-service
|
|
176
|
+
// overviews, answers `current` for it, exactly as before either existed, so
|
|
177
|
+
// nothing reading the key sees a new meaning. The second generated file
|
|
178
|
+
// arrives as a key of its own (`serviceViews`), because a payload key's
|
|
179
|
+
// meaning is frozen and folding two files into one answer would lose which
|
|
180
|
+
// one moved — the only thing either sentence has to say.
|
|
181
|
+
const say = () => {
|
|
182
|
+
reportSync(json, {
|
|
183
|
+
action: views.action,
|
|
184
|
+
serviceViews: services.action,
|
|
185
|
+
subsystems: tree.subsystems.length,
|
|
186
|
+
recovered,
|
|
187
|
+
projects: projects(),
|
|
188
|
+
mapUnreadable: !expected.known,
|
|
189
|
+
});
|
|
190
|
+
// A REFUSAL MUST NOT READ AS A CLEAN RUN. The rest of the sync really did
|
|
191
|
+
// happen — the other generated file, the project files, the root
|
|
192
|
+
// `exclude` — so this is `loam doctor`'s and `loam validate`'s shape and
|
|
193
|
+
// not `fail()`'s: the whole report is emitted, and the exit code says the
|
|
194
|
+
// tree was not left in the state the command was asked to leave it in. A
|
|
195
|
+
// caller that runs `loam subsystem sync` in a hook or a CI step would
|
|
196
|
+
// otherwise be told nothing, forever, about a file it can never repair —
|
|
197
|
+
// and `loam validate --all` would go on reporting `subsystem.views-stale`
|
|
198
|
+
// for that path with this very command named as the fix.
|
|
199
|
+
if (views.action === "refused" || services.action === "refused")
|
|
200
|
+
process.exitCode = 1;
|
|
201
|
+
};
|
|
202
|
+
const generated = [views.write, services.write].filter((write) => write !== null);
|
|
203
|
+
if (generated.length === 0 && plan.gaps.length === 0 && plan.strays.length === 0 && exclude.write === null) {
|
|
204
|
+
say();
|
|
103
205
|
return;
|
|
104
206
|
}
|
|
105
|
-
|
|
106
|
-
// project files answers `current` for it, exactly as before the projects
|
|
107
|
-
// existed, so nothing reading the key sees a new meaning.
|
|
108
|
-
const action = agree
|
|
109
|
-
? "current"
|
|
110
|
-
: actual === null
|
|
111
|
-
? "created"
|
|
112
|
-
: expectedBytes === null
|
|
113
|
-
? "removed"
|
|
114
|
-
: "updated";
|
|
115
|
-
const writes = agree ? [] : [{ path, content: expectedBytes }];
|
|
207
|
+
const writes = [...generated];
|
|
116
208
|
// `planWrite` marks a non-existent target exclusive — a no-clobber create
|
|
117
209
|
// that fails EEXIST rather than replacing a file another writer landed in
|
|
118
210
|
// the same instant. That IS the create-only rule, enforced by the write
|
|
@@ -121,6 +213,18 @@ export async function runSync(docsDir, json) {
|
|
|
121
213
|
for (const gap of plan.gaps) {
|
|
122
214
|
writes.push(planWrite(gap.path, renderServiceProject(gap.service.id, LIKEC4_ROOT_PROJECT)));
|
|
123
215
|
}
|
|
216
|
+
// The one file this verb DELETES, and it rides the same journal as the rest,
|
|
217
|
+
// so a run killed mid-swap is rolled forward rather than half-done.
|
|
218
|
+
// `content: null` is the delete the views file already uses.
|
|
219
|
+
for (const stray of plan.strays)
|
|
220
|
+
writes.push({ path: stray.path, content: null });
|
|
221
|
+
// The root project's `exclude`, in the SAME transaction as the files it
|
|
222
|
+
// decides the fate of: a run that wrote the per-service projects and died
|
|
223
|
+
// before the exclude would leave a tree where the renderer loads a
|
|
224
|
+
// standalone model twice — once in the root project, where every kind it
|
|
225
|
+
// declares is a duplicate — which is worse than either end state.
|
|
226
|
+
if (exclude.write !== null)
|
|
227
|
+
writes.push(exclude.write);
|
|
124
228
|
// One lock, one journal, one commit for both kinds of file — a sync killed
|
|
125
229
|
// between the views swap and the project creates would otherwise leave a
|
|
126
230
|
// half-state no later run could tell from a team's deliberate deletion.
|
|
@@ -130,65 +234,112 @@ export async function runSync(docsDir, json) {
|
|
|
130
234
|
fail(json, committed.code, committed.message);
|
|
131
235
|
return;
|
|
132
236
|
}
|
|
133
|
-
|
|
134
|
-
report(json, { action, subsystems: tree.subsystems.length, recovered, projects: projects(created) });
|
|
237
|
+
say();
|
|
135
238
|
}
|
|
136
239
|
finally {
|
|
137
240
|
await releaseLock();
|
|
138
241
|
}
|
|
139
242
|
}
|
|
243
|
+
/**
|
|
244
|
+
* What this run will do to ONE generated file: the action to report, and the
|
|
245
|
+
* write to stage (or null for "leave it alone").
|
|
246
|
+
*
|
|
247
|
+
* ONE function for both generated files, because the rule is one rule and a
|
|
248
|
+
* second spelling of it would be a second chance for `subsystems.likec4` and
|
|
249
|
+
* `services.likec4` to answer differently about the same map.
|
|
250
|
+
*
|
|
251
|
+
* The compare is over CONTENT, not bytes: `viewsAgree` records why, beside the
|
|
252
|
+
* generator. A Windows clone hands a generated file back CRLF with not one fact
|
|
253
|
+
* changed, and a byte compare made `sync` answer `updated` and rewrite it LF —
|
|
254
|
+
* after which git shows the file permanently modified and the next checkout puts
|
|
255
|
+
* the CRLF back. `validate` stopped demanding that rewrite; `sync` must stop
|
|
256
|
+
* offering it, or the two disagree about the same file.
|
|
257
|
+
*
|
|
258
|
+
* `blocked` is the map's veto and it is the whole reason `known` is threaded
|
|
259
|
+
* this far. The render cannot be trusted against a file that already exists when
|
|
260
|
+
* `architecture/` does not parse as one project: without the element join and
|
|
261
|
+
* the style census, `subsystems.likec4` is a title and a description — writing
|
|
262
|
+
* it would take the `include` lines, the boundary comment and the palette
|
|
263
|
+
* reference off a file whose only fault is a sibling document that does not
|
|
264
|
+
* parse — and `services.likec4` is NOTHING AT ALL, because every one of its
|
|
265
|
+
* views is gated on a service resolving to a map element, so the tolerant render
|
|
266
|
+
* would delete the whole file. An absent file is not protected: there is nothing
|
|
267
|
+
* to lose and a fleet being scaffolded must still get its first one.
|
|
268
|
+
*
|
|
269
|
+
* `refused` is the OTHER veto and it comes from the file rather than from the
|
|
270
|
+
* map: a file at one of these reserved paths that does not open with loam's mark
|
|
271
|
+
* was written by somebody, and this command writes both files whole. It is
|
|
272
|
+
* checked first, applies to both files through one predicate, and is the one
|
|
273
|
+
* answer here that makes the run exit non-zero — see `runSync`.
|
|
274
|
+
*/
|
|
275
|
+
async function planFile(file, known) {
|
|
276
|
+
// ONE read for both questions — what the file says, and whose it is. The
|
|
277
|
+
// predicate and the reader live beside the mark itself
|
|
278
|
+
// (`core/repo/tree/render/service/header.ts`), below every writer of these two
|
|
279
|
+
// paths, because sync is one of FIVE and was for a while the only one holding
|
|
280
|
+
// the line.
|
|
281
|
+
const { text: actual, authored } = await readGeneratedFile(file.path);
|
|
282
|
+
// OWNERSHIP FIRST, before the map's veto and before the compare, because it is
|
|
283
|
+
// the one question whose wrong answer destroys work. Both of these are
|
|
284
|
+
// filenames loam RESERVES inside a directory the team owns, and every arm
|
|
285
|
+
// below writes the whole file or deletes it: a docs repo that already kept a
|
|
286
|
+
// hand-authored diagram at one of them would have lost it on the first sync
|
|
287
|
+
// after the upgrade that reserved the name, silently, breaking the promise the
|
|
288
|
+
// rest of the tree keeps (archive's verbatim splice: authored bytes stay
|
|
289
|
+
// authored). So the file is asked who wrote it. An EDIT of a generated file
|
|
290
|
+
// still carries the mark and is still loam's to repair — that is the whole
|
|
291
|
+
// point of `subsystem.views-stale` — and only a file that never carried it is
|
|
292
|
+
// somebody else's. Absent is not refused: there is nothing to lose.
|
|
293
|
+
if (authored)
|
|
294
|
+
return { action: "refused", write: null };
|
|
295
|
+
if (!known && actual !== null)
|
|
296
|
+
return { action: "blocked", write: null };
|
|
297
|
+
if (viewsAgree(actual, file.content))
|
|
298
|
+
return { action: "current", write: null };
|
|
299
|
+
const bytes = file.content === null ? null : Buffer.from(file.content, "utf8");
|
|
300
|
+
return {
|
|
301
|
+
action: actual === null ? "created" : bytes === null ? "removed" : "updated",
|
|
302
|
+
write: { path: file.path, content: bytes },
|
|
303
|
+
};
|
|
304
|
+
}
|
|
140
305
|
/**
|
|
141
306
|
* What the project half of this run owes, from ONE predicate. `surveyProjects`
|
|
142
307
|
* is the shared spelling of "has a model, a legal id, and no file", and it
|
|
143
|
-
* hands back the
|
|
144
|
-
*
|
|
145
|
-
*
|
|
308
|
+
* hands back the sizes of the two domains it counted against — every model it
|
|
309
|
+
* walked, and the standalone models a file is owed for — so `current` is a
|
|
310
|
+
* subtraction rather than a second reading of the same two paths. A loop here
|
|
311
|
+
* re-spelling "has a model" was the drift the shared predicate exists to
|
|
146
312
|
* prevent, and it would have kept counting a directory the survey had
|
|
147
313
|
* skipped. Without the root file the gaps are real but stay unfilled —
|
|
148
314
|
* writing them would promise a renderer nobody can open — and `current` is 0
|
|
149
315
|
* because "already had a file" is only a fact about a workspace that exists.
|
|
150
316
|
*/
|
|
151
|
-
function planProjects(docsDir, services) {
|
|
317
|
+
function planProjects(docsDir, services, shapes) {
|
|
152
318
|
const root = existsSync(rootProjectPath(docsDir));
|
|
153
|
-
|
|
319
|
+
// A model that EXTENDS the map is owed no file, and the survey is where that
|
|
320
|
+
// is decided — one predicate for the writer and the grader, so `validate --all`
|
|
321
|
+
// can never name a gap this run would not fill. An unreadable model classifies
|
|
322
|
+
// as standalone (`readModelShapes`), which is the conservative arm: it gets a
|
|
323
|
+
// project file, which is what every model got before shapes existed.
|
|
324
|
+
const survey = surveyProjects(services, serviceRenderPaths, (model) => shapes.get(resolve(model)) !== "extending");
|
|
154
325
|
if (!root)
|
|
155
|
-
return { root, gaps: [], current: 0, modelled: survey.modelled };
|
|
156
|
-
return {
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
}
|
|
173
|
-
const wrote = out.projects.created.length;
|
|
174
|
-
// "nothing to write" is a claim about the whole run, and it is false the
|
|
175
|
-
// moment a project file landed beside a current views file.
|
|
176
|
-
const sentence = {
|
|
177
|
-
current: `architecture/subsystems.likec4 is current (${out.subsystems} subsystem(s))${wrote === 0 ? " — nothing to write." : "."}`,
|
|
178
|
-
created: `wrote architecture/subsystems.likec4 — ${out.subsystems} subsystem view(s).`,
|
|
179
|
-
updated: `updated architecture/subsystems.likec4 — ${out.subsystems} subsystem view(s).`,
|
|
180
|
-
removed: `removed architecture/subsystems.likec4 — the tree has no subsystems, so the generated file must be absent.`,
|
|
326
|
+
return { root, gaps: [], strays: [], current: 0, modelled: survey.modelled, models: survey.models };
|
|
327
|
+
return {
|
|
328
|
+
root,
|
|
329
|
+
gaps: survey.gaps,
|
|
330
|
+
// The removal is gated on the root file for the same reason the create is:
|
|
331
|
+
// without it the docs root is not a LikeC4 workspace, so nothing there is
|
|
332
|
+
// claiming anything out of a root project that does not exist, and deleting
|
|
333
|
+
// a team's file over a renderer nobody can open is a writer acting on a
|
|
334
|
+
// hazard it cannot see.
|
|
335
|
+
strays: survey.strays,
|
|
336
|
+
// Over the STANDALONE domain, which is the one `gaps` was drawn from: a
|
|
337
|
+
// model that extends the map is owed no file, so it can neither be a gap nor
|
|
338
|
+
// "already have one", and subtracting from `modelled` counted every
|
|
339
|
+
// extending model as the latter.
|
|
340
|
+
current: survey.standalone - survey.gaps.length,
|
|
341
|
+
modelled: survey.modelled,
|
|
342
|
+
models: survey.models,
|
|
181
343
|
};
|
|
182
|
-
console.log(sentence[out.action]);
|
|
183
|
-
if (wrote > 0) {
|
|
184
|
-
console.log(`wrote ${wrote} services/<…>/likec4.config.json — one LikeC4 project per service model, create-only ` +
|
|
185
|
-
"(a file that already exists is yours and is never touched; commit the new ones before a " +
|
|
186
|
-
"`loam subsystem move`, which refuses over untracked paths). With more than one project, " +
|
|
187
|
-
"likec4 validate at the docs root needs --project <name>; build and export take every project.");
|
|
188
|
-
}
|
|
189
|
-
if (!out.projects.root && out.projects.modelled > 0) {
|
|
190
|
-
console.log("note: no likec4.config.json at the docs root, so no service project files were written — " +
|
|
191
|
-
"`loam doctor` prints the root file.");
|
|
192
|
-
}
|
|
193
344
|
}
|
|
194
345
|
//# sourceMappingURL=sync.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"sync.js","sourceRoot":"","sources":["../../../src/commands/subsystem/sync.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * `loam subsystem sync` — the writer of everything under the tree that exists\n * only to be RENDERED, which is the renderer's reading of the tree twice over:\n *\n * - `architecture/subsystems.likec4`, the GENERATED views file. Recomputed from\n * the tree on every run, compared, written when it differs, removed when the\n * tree has no subsystems. The ONE repair for `subsystem.views-stale`.\n * - `services/<…>/<id>/likec4.config.json`, one LikeC4 project file beside\n * every `model.likec4`. CREATE-ONLY: never compared, never rewritten, never\n * removed.\n *\n * The two rules differ because the two files answer to different owners. The\n * views file is a pure function of the tree and the committed landscape, so\n * every byte of it is loam's and a hand edit is by definition stale. The\n * project file's bytes are a function of the service id ALONE — the identity\n * no verb ever changes — so nothing can make it stale, and the keys LikeC4\n * lets a team add to it (`title`, styles, a contact) are the team's; a compare\n * would fight them, and a rewrite would take them. `core/repo/tree/render/projects.ts`\n * records the measurement behind the file and the naming rule; here, presence\n * is the one question asked of it, through the SAME predicate `validate --all`\n * grades with (`missingProjects`), so the grader can never name a gap this\n * writer would not fill.\n *\n * The project files are written only when the ROOT `likec4.config.json` exists.\n * Without it the tree is not a loadable workspace at all — every model would\n * merge into one project and report every declaration as a duplicate — and a\n * nested project beside a missing root is a promise about a renderer that\n * cannot be opened. `loam doctor` prints the root file; sync says so and\n * writes nothing for the services until it is there.\n *\n * Every write goes through the same lock + journaled transaction every docs\n * writer uses (`commitStaged`), and both kinds land in ONE commit: a\n * predecessor's interrupted commit is recovered first and a sync killed\n * mid-swap is rolled forward by the next writer. Idempotent by construction:\n * the expected views bytes are a pure function of the tree, and a project\n * file, once present, is never a gap again — so a second run reports\n * `current` and writes nothing.\n *\n * The expected views bytes come from `./txn/views.ts` — the same module every\n * writer verb renders from, so sync and the transactions cannot disagree\n * about what the file should say. The subsystem verbs' own commit window\n * (`./txn/txn.ts`) does NOT write project files, and records why.\n */\nimport { existsSync } from \"node:fs\";\nimport { readFile } from \"node:fs/promises\";\nimport type { DocsDir } from \"../../core/kernel/ids/dirs.js\";\nimport { emitJson, fail, repoPath } from \"../../core/envelope/json.js\";\nimport {\n LIKEC4_ROOT_PROJECT,\n rootProjectPath,\n serviceRenderPaths,\n subsystemViewsPath,\n} from \"../../core/repo/paths.js\";\nimport { listFleetTree } from \"../../core/repo/repo.js\";\nimport { type ProjectGap, renderServiceProject, surveyProjects } from \"../../core/repo/tree/render/projects.js\";\nimport type { WalkedService } from \"../../core/repo/tree/walk.js\";\nimport { stageWrites } from \"../../core/staging/commit.js\";\nimport { type CommitRecovery, InterruptedCommitError } from \"../../core/staging/interrupted.js\";\nimport { acquireDocsLockWaiting, DocsBusyError, LOCK_WAIT_MS } from \"../../core/staging/lock.js\";\nimport { recoverInterruptedCommit } from \"../../core/staging/recovery/recover.js\";\nimport { commitStaged } from \"../../core/staging/txn/transaction.js\";\nimport { planWrite, type PlannedWrite } from \"../../core/staging/writes.js\";\nimport { viewsAgree } from \"../../core/repo/tree/render/views.js\";\nimport { expectedViews } from \"./txn/views.js\";\n\n/** What one sync did to the views file — and ONLY that file; the projects report separately. */\ntype SyncAction = \"current\" | \"created\" | \"updated\" | \"removed\";\n\n/**\n * The per-service project half of one sync, before the write: what the\n * predicate found, and the two counts the report needs beside it.\n */\ninterface ProjectPlan {\n /** Whether the root project file exists — the gate on every service write. */\n root: boolean;\n /** The files this run will create. Empty whenever `root` is false. */\n gaps: ProjectGap<WalkedService>[];\n /** Model-bearing services that ALREADY had a file; 0 whenever `root` is false. */\n current: number;\n /** Model-bearing services, root or no root — what the root-missing note is owed to. */\n modelled: number;\n}\n\n/** The additive `projects` payload key, and what the text lines are composed from. */\ninterface ProjectsReport {\n root: boolean;\n /** Repo-relative POSIX paths written this run, sorted by id. */\n created: string[];\n current: number;\n modelled: number;\n}\n\nexport async function runSync(docsDir: DocsDir, json: boolean): Promise<void> {\n let releaseLock: () => Promise<void>;\n try {\n releaseLock = await acquireDocsLockWaiting(docsDir, LOCK_WAIT_MS);\n } catch (err) {\n if (!(err instanceof DocsBusyError)) throw err;\n fail(json, \"docs-busy\", err.message);\n return;\n }\n try {\n // A predecessor's journal is recovered (or refused) before the tree is\n // read: the views file itself may be the half-written one.\n let recovered: CommitRecovery | null;\n try {\n recovered = await recoverInterruptedCommit(docsDir);\n } catch (err) {\n if (!(err instanceof InterruptedCommitError)) throw err;\n fail(json, \"commit-interrupted\", err.message);\n return;\n }\n\n const tree = await listFleetTree(docsDir);\n const expected = await expectedViews(docsDir, tree);\n const path = subsystemViewsPath(docsDir);\n const expectedBytes = expected === null ? null : Buffer.from(expected, \"utf8\");\n // Content, not bytes: `viewsAgree` records why, beside the generator. A\n // Windows clone hands this file back CRLF with not one fact changed, and a\n // byte compare made `sync` answer `updated` and rewrite it LF — after which\n // git shows the file permanently modified and the next checkout puts the\n // CRLF back. `validate` stopped demanding that rewrite; `sync` must stop\n // offering it, or the two disagree about the same file.\n const actual = existsSync(path) ? await readFile(path, \"utf8\") : null;\n const agree = viewsAgree(actual, expected);\n const plan = planProjects(docsDir, tree.services);\n const projects = (created: string[]): ProjectsReport => ({\n root: plan.root,\n created,\n current: plan.current,\n modelled: plan.modelled,\n });\n if (agree && plan.gaps.length === 0) {\n report(json, { action: \"current\", subsystems: tree.subsystems.length, recovered, projects: projects([]) });\n return;\n }\n // `action` still describes the views file alone: a run that only created\n // project files answers `current` for it, exactly as before the projects\n // existed, so nothing reading the key sees a new meaning.\n const action: SyncAction = agree\n ? \"current\"\n : actual === null\n ? \"created\"\n : expectedBytes === null\n ? \"removed\"\n : \"updated\";\n const writes: PlannedWrite[] = agree ? [] : [{ path, content: expectedBytes }];\n // `planWrite` marks a non-existent target exclusive — a no-clobber create\n // that fails EEXIST rather than replacing a file another writer landed in\n // the same instant. That IS the create-only rule, enforced by the write\n // path rather than by the `existsSync` the predicate already ran: a file\n // that appears between the check and the swap is somebody's, and stays.\n for (const gap of plan.gaps) {\n writes.push(planWrite(gap.path, renderServiceProject(gap.service.id, LIKEC4_ROOT_PROJECT)));\n }\n // One lock, one journal, one commit for both kinds of file — a sync killed\n // between the views swap and the project creates would otherwise leave a\n // half-state no later run could tell from a team's deliberate deletion.\n const staged = await stageWrites(writes);\n const committed = await commitStaged(\n { root: docsDir, command: \"subsystem\", rerun: \"loam subsystem sync\", target: \"subsystems\" },\n staged,\n \"synced\",\n );\n if (!committed.ok) {\n fail(json, committed.code, committed.message);\n return;\n }\n const created = plan.gaps.map((gap) => repoPath(docsDir, gap.path));\n report(json, { action, subsystems: tree.subsystems.length, recovered, projects: projects(created) });\n } finally {\n await releaseLock();\n }\n}\n\n/**\n * What the project half of this run owes, from ONE predicate. `surveyProjects`\n * is the shared spelling of \"has a model, a legal id, and no file\", and it\n * hands back the size of the domain it counted against, so `current` is a\n * subtraction rather than a second reading of the same two paths — a loop\n * here re-spelling \"has a model\" was the drift the shared predicate exists to\n * prevent, and it would have kept counting a directory the survey had\n * skipped. Without the root file the gaps are real but stay unfilled —\n * writing them would promise a renderer nobody can open — and `current` is 0\n * because \"already had a file\" is only a fact about a workspace that exists.\n */\nfunction planProjects(docsDir: DocsDir, services: readonly WalkedService[]): ProjectPlan {\n const root = existsSync(rootProjectPath(docsDir));\n const survey = surveyProjects(services, serviceRenderPaths);\n if (!root) return { root, gaps: [], current: 0, modelled: survey.modelled };\n return { root, gaps: survey.gaps, current: survey.modelled - survey.gaps.length, modelled: survey.modelled };\n}\n\nfunction report(\n json: boolean,\n out: { action: SyncAction; subsystems: number; recovered: CommitRecovery | null; projects: ProjectsReport },\n): void {\n if (json) {\n emitJson({\n command: \"subsystem\",\n path: \"architecture/subsystems.likec4\",\n action: out.action,\n subsystems: out.subsystems,\n ...(out.recovered === null ? {} : { recovered: out.recovered }),\n projects: { root: out.projects.root, created: out.projects.created, current: out.projects.current },\n });\n return;\n }\n if (out.recovered !== null && out.recovered.outcome !== \"consistent\") {\n console.log(\n `note: recovered an interrupted \\`loam ${out.recovered.command}\\` commit first (${out.recovered.outcome}).`,\n );\n }\n const wrote = out.projects.created.length;\n // \"nothing to write\" is a claim about the whole run, and it is false the\n // moment a project file landed beside a current views file.\n const sentence: Record<SyncAction, string> = {\n current: `architecture/subsystems.likec4 is current (${out.subsystems} subsystem(s))${wrote === 0 ? \" — nothing to write.\" : \".\"}`,\n created: `wrote architecture/subsystems.likec4 — ${out.subsystems} subsystem view(s).`,\n updated: `updated architecture/subsystems.likec4 — ${out.subsystems} subsystem view(s).`,\n removed: `removed architecture/subsystems.likec4 — the tree has no subsystems, so the generated file must be absent.`,\n };\n console.log(sentence[out.action]);\n if (wrote > 0) {\n console.log(\n `wrote ${wrote} services/<…>/likec4.config.json — one LikeC4 project per service model, create-only ` +\n \"(a file that already exists is yours and is never touched; commit the new ones before a \" +\n \"`loam subsystem move`, which refuses over untracked paths). With more than one project, \" +\n \"likec4 validate at the docs root needs --project <name>; build and export take every project.\",\n );\n }\n if (!out.projects.root && out.projects.modelled > 0) {\n console.log(\n \"note: no likec4.config.json at the docs root, so no service project files were written — \" +\n \"`loam doctor` prints the root file.\",\n );\n }\n}\n"]}
|
|
1
|
+
{"version":3,"file":"sync.js","sourceRoot":"","sources":["../../../src/commands/subsystem/sync.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * `loam subsystem sync` — the writer of everything under the tree that exists\n * only to be RENDERED, which is the renderer's reading of the tree twice over:\n *\n * - `architecture/subsystems.likec4`, the GENERATED views file. Recomputed from\n * the tree on every run, compared, written when it differs, removed when the\n * tree has no subsystems. One of the two repairs for `subsystem.views-stale`.\n * Left exactly as it is when the `architecture/` project does not parse and\n * the file already exists — `action: \"blocked\"`, below.\n * - `architecture/services.likec4`, the OTHER generated views file: one\n * overview view per service the map draws, so a reader can open a service and\n * see what it touches. Same rules, same commit, same code when it drifts —\n * which is what makes ONE `subsystem.views-stale` honest about two files, and\n * why this command is named as the repair for both. Removed when NO service\n * resolves to a map element, exactly as the subsystem file is removed when\n * the tree has no subsystems. `serviceViews` is its `action`; `action` still\n * means the subsystem file alone.\n * - `services/<…>/<id>/likec4.config.json`, one LikeC4 project file beside\n * every model that STANDS ALONE. Never compared, never rewritten. Created\n * where a standalone model has none, and DELETED where an extending model has\n * one: measured at the 1.59.2 pin, that nested project claims the model out of\n * the root project and the fleet loses the service's interior\n * (`core/repo/tree/render/projects.ts` carries the measurement). This is the\n * repair `service.likec4-config-stray` has always named and, until now, did\n * not perform.\n * - the ROOT `likec4.config.json`'s `exclude` list, and ONLY that key. Derived\n * from the shapes on disk on every run and rewritten when it differs, because\n * the two shapes need opposite answers from it: a standalone model must be\n * excluded (inside the root project every kind it declares is a duplicate)\n * and an extending model must not be (the root project is the only place it\n * parses). `planExclude` below states the gate that keeps an old repository\n * untouched, and `core/c4/service-model/renderer.ts` states which half of the\n * list is loam's and which is the team's. One entry loam did NOT write is\n * taken anyway, and only this one: an entry naming a directory that HIDES an\n * extending model — a subsystem-wide `services/platform/**` survived every\n * sync while `service.model-excluded` named this command as the repair.\n *\n * NEITHER views file is written over a document loam did not write. Both are\n * filenames loam RESERVES inside `architecture/`, a directory the team owns, and\n * both are rewritten whole — so a repository that already kept a hand-authored\n * diagram at one of those paths would have lost it, silently, on the first sync\n * after the release that reserved the name. One predicate answers it for both\n * (`readGeneratedFile`, off the file's own first line), the run reports\n * `refused` for that file, writes nothing for it, and exits non-zero. A hand\n * EDIT of a generated file is a different thing and is still repaired: it keeps\n * the mark, so it is loam's file with wrong bytes in it, which is exactly what\n * `subsystem.views-stale` says.\n *\n * THIS COMMAND IS NOT THE ONLY WRITER, and for a while it was the only one\n * holding that line — reproduced, with a hand-authored\n * `architecture/services.likec4` in the tree: `loam subsystem new platform`\n * printed \"architecture/services.likec4 updated\" and destroyed it. The predicate\n * and the refusal sentence therefore live below every writer\n * (`core/repo/tree/render/service/header.ts`), and `./txn/txn.ts` and\n * `../seed/plan.ts` ask the same question of the same two paths.\n *\n * WHAT DIFFERS IS THE POLICY, not the question, and the difference is the shape\n * of the operation rather than a second opinion about ownership. Sync REPORTS\n * `refused` for the one file and does the rest: nothing it writes depends on\n * that file, every part of it is idempotent, and a run that also created a\n * project file really did create it. A tree-changing verb and `seed` REFUSE\n * WHOLE, because their generated file has to land in the same commit as the\n * change to the tree — completing half of that is the very state the commit\n * window exists never to leave between two commits, and `subsystem move` would\n * additionally leave git-staged renames behind it.\n *\n * The rules differ because the files answer to different owners. The\n * views files are a pure function of the tree and the committed landscape, so\n * every byte of them is loam's and a hand edit is by definition stale. The\n * project file's bytes are a function of the service id ALONE — the identity\n * no verb ever changes — so nothing can make it stale, and the keys LikeC4\n * lets a team add to it (`title`, styles, a contact) are the team's; a compare\n * would fight them, and a rewrite would take them. `core/repo/tree/render/projects.ts`\n * records the measurement behind the file and the naming rule; here, presence\n * is the one question asked of it, through the SAME predicate `validate --all`\n * grades with (`missingProjects`), so the grader can never name a gap this\n * writer would not fill.\n *\n * Nothing under `services/` is written, and no `exclude` is recomputed, unless\n * the ROOT `likec4.config.json` exists. Without it the tree is not a loadable\n * workspace at all — every model would merge into one project and report every\n * declaration as a duplicate — and a nested project beside a missing root is a\n * promise about a renderer that cannot be opened. `loam doctor` prints the root\n * file; sync says so and writes nothing for the services until it is there.\n *\n * Every write goes through the same lock + journaled transaction every docs\n * writer uses (`commitStaged`), and both kinds land in ONE commit: a\n * predecessor's interrupted commit is recovered first and a sync killed\n * mid-swap is rolled forward by the next writer. Idempotent by construction:\n * the expected views bytes are a pure function of the tree, and a project\n * file, once present, is never a gap again — so a second run reports\n * `current` and writes nothing.\n *\n * The expected views bytes come from `./txn/views.ts` — the same module every\n * writer verb renders from, so sync and the transactions cannot disagree\n * about what the file should say. The subsystem verbs' own commit window\n * (`./txn/txn.ts`) does NOT write project files, and records why. What this run\n * SAYS it did is `./txn/report.ts`: the third answer about the same three files,\n * and the phase that is not part of the commit.\n */\nimport { existsSync } from \"node:fs\";\nimport { resolve } from \"node:path\";\nimport { readModelShapes, type ModelShape } from \"../../core/c4/service-model/shape.js\";\nimport type { DocsDir } from \"../../core/kernel/ids/dirs.js\";\nimport { fail, repoPath } from \"../../core/envelope/json.js\";\nimport { LIKEC4_ROOT_PROJECT, rootProjectPath, serviceRenderPaths } from \"../../core/repo/paths.js\";\nimport { listFleetTree } from \"../../core/repo/repo.js\";\nimport {\n type ProjectGap,\n renderServiceProject,\n surveyProjects,\n type SurveyedModel,\n} from \"../../core/repo/tree/render/projects.js\";\nimport type { WalkedService } from \"../../core/repo/tree/walk.js\";\nimport { stageWrites } from \"../../core/staging/commit.js\";\nimport { type CommitRecovery, InterruptedCommitError } from \"../../core/staging/interrupted.js\";\nimport { acquireDocsLockWaiting, DocsBusyError, LOCK_WAIT_MS } from \"../../core/staging/lock.js\";\nimport { recoverInterruptedCommit } from \"../../core/staging/recovery/recover.js\";\nimport { commitStaged } from \"../../core/staging/txn/transaction.js\";\nimport { planWrite, type PlannedWrite } from \"../../core/staging/writes.js\";\nimport { readGeneratedFile } from \"../../core/repo/tree/render/service/header.js\";\nimport { viewsAgree } from \"../../core/repo/tree/render/views.js\";\nimport { NO_EXCLUDE, planExclude } from \"./txn/exclude.js\";\nimport { type ProjectsReport, reportSync, type SyncAction } from \"./txn/report.js\";\nimport { type ExpectedFile, expectedViews } from \"./txn/views.js\";\n\n/**\n * The per-service project half of one sync, before the write: what the\n * predicate found, and the two counts the report needs beside it.\n */\ninterface ProjectPlan {\n /** Whether the root project file exists — the gate on every service write. */\n root: boolean;\n /** The files this run will create. Empty whenever `root` is false. */\n gaps: ProjectGap<WalkedService>[];\n /** The files this run will DELETE — a project file beside an extending model. Empty whenever `root` is false. */\n strays: ProjectGap<WalkedService>[];\n /**\n * Services owed a project file that ALREADY had one — so STANDALONE models\n * only; 0 whenever `root` is false. An extending model is owed none, and\n * counting it here said a file was in place beside a model that must not have\n * one (`service.likec4-config-stray` grades exactly that state).\n */\n current: number;\n /** Model-bearing services, root or no root — what the root-missing note is owed to. */\n modelled: number;\n /** Every model this run considered, with its shape — the exclude list is derived from the same walk. */\n models: SurveyedModel<WalkedService>[];\n}\n\nexport async function runSync(docsDir: DocsDir, json: boolean): Promise<void> {\n let releaseLock: () => Promise<void>;\n try {\n releaseLock = await acquireDocsLockWaiting(docsDir, LOCK_WAIT_MS);\n } catch (err) {\n if (!(err instanceof DocsBusyError)) throw err;\n fail(json, \"docs-busy\", err.message);\n return;\n }\n try {\n // A predecessor's journal is recovered (or refused) before the tree is\n // read: the views file itself may be the half-written one.\n let recovered: CommitRecovery | null;\n try {\n recovered = await recoverInterruptedCommit(docsDir);\n } catch (err) {\n if (!(err instanceof InterruptedCommitError)) throw err;\n fail(json, \"commit-interrupted\", err.message);\n return;\n }\n\n const tree = await listFleetTree(docsDir);\n // BOTH generated files, from ONE read of the map — `./txn/views.ts` says why\n // the pair is one answer and why the parsed document, never a literal, is\n // what the per-service peer sets are computed from.\n const expected = await expectedViews(docsDir, tree);\n const views = await planFile(expected.subsystems, expected.known);\n const services = await planFile(expected.services, expected.known);\n // ONE read of every model's bytes for the whole run, and the same scan the\n // grader uses: it decides which directories are owed a project file AND\n // which the root project must exclude, and those are opposite answers for\n // the two shapes (`core/repo/tree/render/projects.ts`).\n const shapes = await readModelShapes(tree.services.map((svc) => serviceRenderPaths(svc.dir).model));\n const plan = planProjects(docsDir, tree.services, shapes);\n const exclude = plan.root ? await planExclude(docsDir, tree.services, plan.models) : NO_EXCLUDE;\n // The plan IS the report: every gap is created and every stray removed in\n // the one commit below, and the early return is the branch where both lists\n // are empty.\n const projects = (): ProjectsReport => ({\n root: plan.root,\n // SORTED BY PATH, which is what the contract says these keys hold and what\n // the survey's own order is not: `surveyProjects` sorts by service id, so\n // `services/svc-a/…` came out before `services/platform/svc-d/…` and a\n // consumer told the list was \"sorted\" got an order no path sort produces\n // (re-verification 2026-09-04, area C item 6). The text view prints the\n // same array, so the two orders cannot drift apart.\n created: plan.gaps.map((gap) => repoPath(docsDir, gap.path)).sort(),\n removed: plan.strays.map((stray) => repoPath(docsDir, stray.path)).sort(),\n current: plan.current,\n modelled: plan.modelled,\n exclude,\n });\n // `action` still describes `architecture/subsystems.likec4` ALONE — a run\n // that only created project files, or only rewrote the per-service\n // overviews, answers `current` for it, exactly as before either existed, so\n // nothing reading the key sees a new meaning. The second generated file\n // arrives as a key of its own (`serviceViews`), because a payload key's\n // meaning is frozen and folding two files into one answer would lose which\n // one moved — the only thing either sentence has to say.\n const say = (): void => {\n reportSync(json, {\n action: views.action,\n serviceViews: services.action,\n subsystems: tree.subsystems.length,\n recovered,\n projects: projects(),\n mapUnreadable: !expected.known,\n });\n // A REFUSAL MUST NOT READ AS A CLEAN RUN. The rest of the sync really did\n // happen — the other generated file, the project files, the root\n // `exclude` — so this is `loam doctor`'s and `loam validate`'s shape and\n // not `fail()`'s: the whole report is emitted, and the exit code says the\n // tree was not left in the state the command was asked to leave it in. A\n // caller that runs `loam subsystem sync` in a hook or a CI step would\n // otherwise be told nothing, forever, about a file it can never repair —\n // and `loam validate --all` would go on reporting `subsystem.views-stale`\n // for that path with this very command named as the fix.\n if (views.action === \"refused\" || services.action === \"refused\") process.exitCode = 1;\n };\n const generated = [views.write, services.write].filter((write) => write !== null);\n if (generated.length === 0 && plan.gaps.length === 0 && plan.strays.length === 0 && exclude.write === null) {\n say();\n return;\n }\n const writes: PlannedWrite[] = [...generated];\n // `planWrite` marks a non-existent target exclusive — a no-clobber create\n // that fails EEXIST rather than replacing a file another writer landed in\n // the same instant. That IS the create-only rule, enforced by the write\n // path rather than by the `existsSync` the predicate already ran: a file\n // that appears between the check and the swap is somebody's, and stays.\n for (const gap of plan.gaps) {\n writes.push(planWrite(gap.path, renderServiceProject(gap.service.id, LIKEC4_ROOT_PROJECT)));\n }\n // The one file this verb DELETES, and it rides the same journal as the rest,\n // so a run killed mid-swap is rolled forward rather than half-done.\n // `content: null` is the delete the views file already uses.\n for (const stray of plan.strays) writes.push({ path: stray.path, content: null });\n // The root project's `exclude`, in the SAME transaction as the files it\n // decides the fate of: a run that wrote the per-service projects and died\n // before the exclude would leave a tree where the renderer loads a\n // standalone model twice — once in the root project, where every kind it\n // declares is a duplicate — which is worse than either end state.\n if (exclude.write !== null) writes.push(exclude.write);\n // One lock, one journal, one commit for both kinds of file — a sync killed\n // between the views swap and the project creates would otherwise leave a\n // half-state no later run could tell from a team's deliberate deletion.\n const staged = await stageWrites(writes);\n const committed = await commitStaged(\n { root: docsDir, command: \"subsystem\", rerun: \"loam subsystem sync\", target: \"subsystems\" },\n staged,\n \"synced\",\n );\n if (!committed.ok) {\n fail(json, committed.code, committed.message);\n return;\n }\n say();\n } finally {\n await releaseLock();\n }\n}\n\n/**\n * What this run will do to ONE generated file: the action to report, and the\n * write to stage (or null for \"leave it alone\").\n *\n * ONE function for both generated files, because the rule is one rule and a\n * second spelling of it would be a second chance for `subsystems.likec4` and\n * `services.likec4` to answer differently about the same map.\n *\n * The compare is over CONTENT, not bytes: `viewsAgree` records why, beside the\n * generator. A Windows clone hands a generated file back CRLF with not one fact\n * changed, and a byte compare made `sync` answer `updated` and rewrite it LF —\n * after which git shows the file permanently modified and the next checkout puts\n * the CRLF back. `validate` stopped demanding that rewrite; `sync` must stop\n * offering it, or the two disagree about the same file.\n *\n * `blocked` is the map's veto and it is the whole reason `known` is threaded\n * this far. The render cannot be trusted against a file that already exists when\n * `architecture/` does not parse as one project: without the element join and\n * the style census, `subsystems.likec4` is a title and a description — writing\n * it would take the `include` lines, the boundary comment and the palette\n * reference off a file whose only fault is a sibling document that does not\n * parse — and `services.likec4` is NOTHING AT ALL, because every one of its\n * views is gated on a service resolving to a map element, so the tolerant render\n * would delete the whole file. An absent file is not protected: there is nothing\n * to lose and a fleet being scaffolded must still get its first one.\n *\n * `refused` is the OTHER veto and it comes from the file rather than from the\n * map: a file at one of these reserved paths that does not open with loam's mark\n * was written by somebody, and this command writes both files whole. It is\n * checked first, applies to both files through one predicate, and is the one\n * answer here that makes the run exit non-zero — see `runSync`.\n */\nasync function planFile(file: ExpectedFile, known: boolean): Promise<{ action: SyncAction; write: PlannedWrite | null }> {\n // ONE read for both questions — what the file says, and whose it is. The\n // predicate and the reader live beside the mark itself\n // (`core/repo/tree/render/service/header.ts`), below every writer of these two\n // paths, because sync is one of FIVE and was for a while the only one holding\n // the line.\n const { text: actual, authored } = await readGeneratedFile(file.path);\n // OWNERSHIP FIRST, before the map's veto and before the compare, because it is\n // the one question whose wrong answer destroys work. Both of these are\n // filenames loam RESERVES inside a directory the team owns, and every arm\n // below writes the whole file or deletes it: a docs repo that already kept a\n // hand-authored diagram at one of them would have lost it on the first sync\n // after the upgrade that reserved the name, silently, breaking the promise the\n // rest of the tree keeps (archive's verbatim splice: authored bytes stay\n // authored). So the file is asked who wrote it. An EDIT of a generated file\n // still carries the mark and is still loam's to repair — that is the whole\n // point of `subsystem.views-stale` — and only a file that never carried it is\n // somebody else's. Absent is not refused: there is nothing to lose.\n if (authored) return { action: \"refused\", write: null };\n if (!known && actual !== null) return { action: \"blocked\", write: null };\n if (viewsAgree(actual, file.content)) return { action: \"current\", write: null };\n const bytes = file.content === null ? null : Buffer.from(file.content, \"utf8\");\n return {\n action: actual === null ? \"created\" : bytes === null ? \"removed\" : \"updated\",\n write: { path: file.path, content: bytes },\n };\n}\n\n/**\n * What the project half of this run owes, from ONE predicate. `surveyProjects`\n * is the shared spelling of \"has a model, a legal id, and no file\", and it\n * hands back the sizes of the two domains it counted against — every model it\n * walked, and the standalone models a file is owed for — so `current` is a\n * subtraction rather than a second reading of the same two paths. A loop here\n * re-spelling \"has a model\" was the drift the shared predicate exists to\n * prevent, and it would have kept counting a directory the survey had\n * skipped. Without the root file the gaps are real but stay unfilled —\n * writing them would promise a renderer nobody can open — and `current` is 0\n * because \"already had a file\" is only a fact about a workspace that exists.\n */\nfunction planProjects(\n docsDir: DocsDir,\n services: readonly WalkedService[],\n shapes: ReadonlyMap<string, ModelShape>,\n): ProjectPlan {\n const root = existsSync(rootProjectPath(docsDir));\n // A model that EXTENDS the map is owed no file, and the survey is where that\n // is decided — one predicate for the writer and the grader, so `validate --all`\n // can never name a gap this run would not fill. An unreadable model classifies\n // as standalone (`readModelShapes`), which is the conservative arm: it gets a\n // project file, which is what every model got before shapes existed.\n const survey = surveyProjects(services, serviceRenderPaths, (model) => shapes.get(resolve(model)) !== \"extending\");\n if (!root) return { root, gaps: [], strays: [], current: 0, modelled: survey.modelled, models: survey.models };\n return {\n root,\n gaps: survey.gaps,\n // The removal is gated on the root file for the same reason the create is:\n // without it the docs root is not a LikeC4 workspace, so nothing there is\n // claiming anything out of a root project that does not exist, and deleting\n // a team's file over a renderer nobody can open is a writer acting on a\n // hazard it cannot see.\n strays: survey.strays,\n // Over the STANDALONE domain, which is the one `gaps` was drawn from: a\n // model that extends the map is owed no file, so it can neither be a gap nor\n // \"already have one\", and subtracting from `modelled` counted every\n // extending model as the latter.\n current: survey.standalone - survey.gaps.length,\n modelled: survey.modelled,\n models: survey.models,\n };\n}\n"]}
|