@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
|
@@ -0,0 +1,132 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The ROOT `likec4.config.json`'s `exclude` list, as `loam subsystem sync`
|
|
3
|
+
* derives it from the shapes on disk.
|
|
4
|
+
*
|
|
5
|
+
* It sits beside `./views.ts` for that module's reason and by the same seam:
|
|
6
|
+
* both answer "what should this renderer-only file say, given the tree?", both
|
|
7
|
+
* are pure derivations one command writes, and keeping them together is what
|
|
8
|
+
* stops the writer and the grader spelling one rule twice. The difference is
|
|
9
|
+
* ownership, and it is stated in `core/c4/service-model/renderer.ts`: every byte
|
|
10
|
+
* of the generated views file is loam's, while this file is the team's and loam
|
|
11
|
+
* maintains exactly the `services/` entries in it. What an entry COVERS is
|
|
12
|
+
* `core/c4/root-project/exclude.ts`'s question, asked of the directory the entry
|
|
13
|
+
* names rather than of its spelling.
|
|
14
|
+
*
|
|
15
|
+
* It is a module rather than three functions in `../sync.ts` because that file
|
|
16
|
+
* reached its line limit carrying them, and this is the phase boundary inside
|
|
17
|
+
* it: `sync.ts` is the lock, the transaction and the report; this is one of the
|
|
18
|
+
* three things it decides to write.
|
|
19
|
+
*/
|
|
20
|
+
import { readFile } from "node:fs/promises";
|
|
21
|
+
import { excludingEntry, parseRootProject, readRootExclude } from "../../../core/c4/root-project/exclude.js";
|
|
22
|
+
import { standaloneExclude } from "../../../core/c4/service-model/renderer.js";
|
|
23
|
+
import { serviceTreePath } from "../../../core/kernel/ids/dirs.js";
|
|
24
|
+
import { rootProjectPath } from "../../../core/repo/paths.js";
|
|
25
|
+
import { planWrite } from "../../../core/staging/writes.js";
|
|
26
|
+
/** The answer for a run that has no root file to read, and the shape every other arm fills. */
|
|
27
|
+
export const NO_EXCLUDE = {
|
|
28
|
+
updated: false,
|
|
29
|
+
entries: [],
|
|
30
|
+
added: [],
|
|
31
|
+
removed: [],
|
|
32
|
+
write: null,
|
|
33
|
+
unreadable: false,
|
|
34
|
+
};
|
|
35
|
+
/**
|
|
36
|
+
* The root project's `exclude` list, recomputed from the shapes on disk.
|
|
37
|
+
*
|
|
38
|
+
* WHAT MAKES THIS SAFE TO RUN ON EVERY SYNC is the split `standaloneExclude`
|
|
39
|
+
* enforces: the `services/` half of the list is loam's and every other entry is
|
|
40
|
+
* the team's, kept in order. The scaffold's node_modules glob, `features/**`, a
|
|
41
|
+
* team's own `drafts/**` all survive; only the entries that decide whether a
|
|
42
|
+
* model is renderable are derived. A standalone model MUST be excluded (inside
|
|
43
|
+
* the root project every kind it declares is a duplicate blamed on the map as
|
|
44
|
+
* well) and an extending model must NOT be (the root project is the only place
|
|
45
|
+
* it parses), so a service that migrates from one shape to the other has its
|
|
46
|
+
* entry added or dropped on the next sync instead of quietly blanking a project.
|
|
47
|
+
*
|
|
48
|
+
* THE GATE keeps an old repository untouched. A fleet whose models all stand
|
|
49
|
+
* alone and whose root already says `services/**` is already correct, and
|
|
50
|
+
* rewriting it into one entry per service would be a diff with no fact behind
|
|
51
|
+
* it — so the rewrite happens only when at least one model extends the map (the
|
|
52
|
+
* blanket exclusion is then wrong) or some standalone model is not covered at
|
|
53
|
+
* all (the renderer is merging it into the map right now). Comparing the parsed
|
|
54
|
+
* ARRAYS rather than the bytes is the second half of the same rule: a file whose
|
|
55
|
+
* `exclude` already says what this run would write is left exactly as the team
|
|
56
|
+
* formatted it.
|
|
57
|
+
*
|
|
58
|
+
* A root config loam cannot read an `exclude` list out of is REPORTED and never
|
|
59
|
+
* rewritten. Guessing at the JSON a team hand-wrote is how a writer destroys a
|
|
60
|
+
* file it did not understand.
|
|
61
|
+
*/
|
|
62
|
+
export async function planExclude(docsDir, services, models) {
|
|
63
|
+
const current = await readRootExclude(docsDir);
|
|
64
|
+
if (current === null)
|
|
65
|
+
return { ...NO_EXCLUDE, unreadable: true };
|
|
66
|
+
const standalone = models.filter((m) => m.standalone).map((m) => serviceTreePath(m.service));
|
|
67
|
+
// Both halves of the shape scan travel, not just the standalone one: an entry
|
|
68
|
+
// is loam's to take back either because loam wrote it (a tree the enumeration
|
|
69
|
+
// returns) or because it HIDES a model that only parses in the root project,
|
|
70
|
+
// and only the extending list can answer the second question. Without it a
|
|
71
|
+
// subsystem-wide `services/platform/**` survived every sync while
|
|
72
|
+
// `service.model-excluded` named that same sync as the repair.
|
|
73
|
+
const extending = models.filter((m) => !m.standalone).map((m) => serviceTreePath(m.service));
|
|
74
|
+
const wanted = standaloneExclude(current, {
|
|
75
|
+
standalone,
|
|
76
|
+
extending,
|
|
77
|
+
enumerated: services.map((svc) => serviceTreePath(svc)),
|
|
78
|
+
});
|
|
79
|
+
const uncovered = standalone.some((tree) => excludingEntry(current, tree) === null);
|
|
80
|
+
if (extending.length === 0 && !uncovered)
|
|
81
|
+
return { ...NO_EXCLUDE, entries: [...current] };
|
|
82
|
+
if (current.length === wanted.length && current.every((entry, i) => entry === wanted[i])) {
|
|
83
|
+
return { ...NO_EXCLUDE, entries: wanted };
|
|
84
|
+
}
|
|
85
|
+
const bytes = await rewrittenRootProject(docsDir, wanted);
|
|
86
|
+
if (bytes === null)
|
|
87
|
+
return { ...NO_EXCLUDE, entries: [...current], unreadable: true };
|
|
88
|
+
return {
|
|
89
|
+
updated: true,
|
|
90
|
+
entries: wanted,
|
|
91
|
+
added: wanted.filter((entry) => !current.includes(entry)),
|
|
92
|
+
removed: current.filter((entry) => !wanted.includes(entry)),
|
|
93
|
+
// `planWrite` decides create-vs-overwrite from the filesystem, so the root
|
|
94
|
+
// file — which by definition already exists here — is staged as an
|
|
95
|
+
// OVERWRITE, and `swapStaged` compares it against its pre-image immediately
|
|
96
|
+
// before the rename: a team member editing the same file during the sync
|
|
97
|
+
// stops the commit instead of having their bytes buried.
|
|
98
|
+
write: planWrite(rootProjectPath(docsDir), bytes),
|
|
99
|
+
unreadable: false,
|
|
100
|
+
};
|
|
101
|
+
}
|
|
102
|
+
/**
|
|
103
|
+
* The root project file with its `exclude` replaced and NOTHING else touched —
|
|
104
|
+
* every other key kept, in the order the team wrote them, re-serialised as
|
|
105
|
+
* 2-space JSON with a trailing newline (`core/docs.ts`'s own form, so a
|
|
106
|
+
* scaffolded file round-trips byte for byte).
|
|
107
|
+
*
|
|
108
|
+
* Null when the file stopped being a JSON object between `readRootExclude` and
|
|
109
|
+
* here, which is a concurrent edit rather than a state loam may write over.
|
|
110
|
+
*/
|
|
111
|
+
async function rewrittenRootProject(docsDir, exclude) {
|
|
112
|
+
let text;
|
|
113
|
+
try {
|
|
114
|
+
text = await readFile(rootProjectPath(docsDir), "utf8");
|
|
115
|
+
}
|
|
116
|
+
catch {
|
|
117
|
+
// Unreadable is not "excludes nothing": the caller reports it and writes
|
|
118
|
+
// nothing at all.
|
|
119
|
+
return null;
|
|
120
|
+
}
|
|
121
|
+
// The SAME parse the grader read the list with — `parseRootProject`, which
|
|
122
|
+
// tolerates the byte-order mark PowerShell's `Out-File` writes. Two parses
|
|
123
|
+
// would mean a run that read an `exclude` here and then declared the file
|
|
124
|
+
// unreadable at the write, which is exactly what a BOM produced before
|
|
125
|
+
// (re-verification 2026-09-04, area C item 3). Re-read rather than carried:
|
|
126
|
+
// the point of this read is that it happens immediately before the swap.
|
|
127
|
+
const parsed = parseRootProject(text);
|
|
128
|
+
if (parsed === null)
|
|
129
|
+
return null;
|
|
130
|
+
return `${JSON.stringify({ ...parsed, exclude: [...exclude] }, null, 2)}\n`;
|
|
131
|
+
}
|
|
132
|
+
//# sourceMappingURL=exclude.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"exclude.js","sourceRoot":"","sources":["../../../../src/commands/subsystem/txn/exclude.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * The ROOT `likec4.config.json`'s `exclude` list, as `loam subsystem sync`\n * derives it from the shapes on disk.\n *\n * It sits beside `./views.ts` for that module's reason and by the same seam:\n * both answer \"what should this renderer-only file say, given the tree?\", both\n * are pure derivations one command writes, and keeping them together is what\n * stops the writer and the grader spelling one rule twice. The difference is\n * ownership, and it is stated in `core/c4/service-model/renderer.ts`: every byte\n * of the generated views file is loam's, while this file is the team's and loam\n * maintains exactly the `services/` entries in it. What an entry COVERS is\n * `core/c4/root-project/exclude.ts`'s question, asked of the directory the entry\n * names rather than of its spelling.\n *\n * It is a module rather than three functions in `../sync.ts` because that file\n * reached its line limit carrying them, and this is the phase boundary inside\n * it: `sync.ts` is the lock, the transaction and the report; this is one of the\n * three things it decides to write.\n */\nimport { readFile } from \"node:fs/promises\";\nimport { excludingEntry, parseRootProject, readRootExclude } from \"../../../core/c4/root-project/exclude.js\";\nimport { standaloneExclude } from \"../../../core/c4/service-model/renderer.js\";\nimport { serviceTreePath, type DocsDir } from \"../../../core/kernel/ids/dirs.js\";\nimport { rootProjectPath } from \"../../../core/repo/paths.js\";\nimport type { SurveyedModel } from \"../../../core/repo/tree/render/projects.js\";\nimport type { WalkedService } from \"../../../core/repo/tree/walk.js\";\nimport { planWrite, type PlannedWrite } from \"../../../core/staging/writes.js\";\n\n/** What one sync did to the ROOT project's `exclude` list. */\nexport interface ExcludePlan {\n /** Whether this run rewrites the list. */\n updated: boolean;\n /** The list as it stands after this run — the current one when nothing changed. */\n entries: string[];\n added: string[];\n removed: string[];\n /** The rewrite, staged with everything else. Null when nothing is owed. */\n write: PlannedWrite | null;\n /** The root file exists and loam could not read an `exclude` list out of it — reported, never rewritten. */\n unreadable: boolean;\n}\n\n/** The answer for a run that has no root file to read, and the shape every other arm fills. */\nexport const NO_EXCLUDE: ExcludePlan = {\n updated: false,\n entries: [],\n added: [],\n removed: [],\n write: null,\n unreadable: false,\n};\n\n/**\n * The root project's `exclude` list, recomputed from the shapes on disk.\n *\n * WHAT MAKES THIS SAFE TO RUN ON EVERY SYNC is the split `standaloneExclude`\n * enforces: the `services/` half of the list is loam's and every other entry is\n * the team's, kept in order. The scaffold's node_modules glob, `features/**`, a\n * team's own `drafts/**` all survive; only the entries that decide whether a\n * model is renderable are derived. A standalone model MUST be excluded (inside\n * the root project every kind it declares is a duplicate blamed on the map as\n * well) and an extending model must NOT be (the root project is the only place\n * it parses), so a service that migrates from one shape to the other has its\n * entry added or dropped on the next sync instead of quietly blanking a project.\n *\n * THE GATE keeps an old repository untouched. A fleet whose models all stand\n * alone and whose root already says `services/**` is already correct, and\n * rewriting it into one entry per service would be a diff with no fact behind\n * it — so the rewrite happens only when at least one model extends the map (the\n * blanket exclusion is then wrong) or some standalone model is not covered at\n * all (the renderer is merging it into the map right now). Comparing the parsed\n * ARRAYS rather than the bytes is the second half of the same rule: a file whose\n * `exclude` already says what this run would write is left exactly as the team\n * formatted it.\n *\n * A root config loam cannot read an `exclude` list out of is REPORTED and never\n * rewritten. Guessing at the JSON a team hand-wrote is how a writer destroys a\n * file it did not understand.\n */\nexport async function planExclude(\n docsDir: DocsDir,\n services: readonly WalkedService[],\n models: readonly SurveyedModel<WalkedService>[],\n): Promise<ExcludePlan> {\n const current = await readRootExclude(docsDir);\n if (current === null) return { ...NO_EXCLUDE, unreadable: true };\n const standalone = models.filter((m) => m.standalone).map((m) => serviceTreePath(m.service));\n // Both halves of the shape scan travel, not just the standalone one: an entry\n // is loam's to take back either because loam wrote it (a tree the enumeration\n // returns) or because it HIDES a model that only parses in the root project,\n // and only the extending list can answer the second question. Without it a\n // subsystem-wide `services/platform/**` survived every sync while\n // `service.model-excluded` named that same sync as the repair.\n const extending = models.filter((m) => !m.standalone).map((m) => serviceTreePath(m.service));\n const wanted = standaloneExclude(current, {\n standalone,\n extending,\n enumerated: services.map((svc) => serviceTreePath(svc)),\n });\n const uncovered = standalone.some((tree) => excludingEntry(current, tree) === null);\n if (extending.length === 0 && !uncovered) return { ...NO_EXCLUDE, entries: [...current] };\n if (current.length === wanted.length && current.every((entry, i) => entry === wanted[i])) {\n return { ...NO_EXCLUDE, entries: wanted };\n }\n const bytes = await rewrittenRootProject(docsDir, wanted);\n if (bytes === null) return { ...NO_EXCLUDE, entries: [...current], unreadable: true };\n return {\n updated: true,\n entries: wanted,\n added: wanted.filter((entry) => !current.includes(entry)),\n removed: current.filter((entry) => !wanted.includes(entry)),\n // `planWrite` decides create-vs-overwrite from the filesystem, so the root\n // file — which by definition already exists here — is staged as an\n // OVERWRITE, and `swapStaged` compares it against its pre-image immediately\n // before the rename: a team member editing the same file during the sync\n // stops the commit instead of having their bytes buried.\n write: planWrite(rootProjectPath(docsDir), bytes),\n unreadable: false,\n };\n}\n\n/**\n * The root project file with its `exclude` replaced and NOTHING else touched —\n * every other key kept, in the order the team wrote them, re-serialised as\n * 2-space JSON with a trailing newline (`core/docs.ts`'s own form, so a\n * scaffolded file round-trips byte for byte).\n *\n * Null when the file stopped being a JSON object between `readRootExclude` and\n * here, which is a concurrent edit rather than a state loam may write over.\n */\nasync function rewrittenRootProject(docsDir: DocsDir, exclude: readonly string[]): Promise<string | null> {\n let text: string;\n try {\n text = await readFile(rootProjectPath(docsDir), \"utf8\");\n } catch {\n // Unreadable is not \"excludes nothing\": the caller reports it and writes\n // nothing at all.\n return null;\n }\n // The SAME parse the grader read the list with — `parseRootProject`, which\n // tolerates the byte-order mark PowerShell's `Out-File` writes. Two parses\n // would mean a run that read an `exclude` here and then declared the file\n // unreadable at the write, which is exactly what a BOM produced before\n // (re-verification 2026-09-04, area C item 3). Re-read rather than carried:\n // the point of this read is that it happens immediately before the swap.\n const parsed = parseRootProject(text);\n if (parsed === null) return null;\n return `${JSON.stringify({ ...parsed, exclude: [...exclude] }, null, 2)}\\n`;\n}\n"]}
|
|
@@ -0,0 +1,174 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* What one `loam subsystem sync` SAYS it did — the `--json` envelope and the
|
|
3
|
+
* human lines, in one place.
|
|
4
|
+
*
|
|
5
|
+
* It sits beside `./views.ts` and `./exclude.ts` because it is the third answer
|
|
6
|
+
* about the same three files, and it moved out of `../sync.ts` for that file's
|
|
7
|
+
* standing reason: `sync.ts` is the lock, the transaction and the plan, and it
|
|
8
|
+
* reached the 400-line limit carrying the output too. Composing a report is not
|
|
9
|
+
* a phase of the commit, so nothing here reads the disk or decides anything — a
|
|
10
|
+
* caller hands over what happened and this spells it.
|
|
11
|
+
*/
|
|
12
|
+
import { emitJson } from "../../../core/envelope/json.js";
|
|
13
|
+
import { authoredRefusal } from "../../../core/repo/tree/render/service/header.js";
|
|
14
|
+
import { LANDSCAPE_UNREADABLE_NOTE } from "./views.js";
|
|
15
|
+
/** The second generated file's name, spelled once here so the key and the sentence cannot drift. */
|
|
16
|
+
const SERVICE_VIEWS = "architecture/services.likec4";
|
|
17
|
+
export function reportSync(json, out) {
|
|
18
|
+
// One reason today, and it is exactly the `action` it explains — spelled as a
|
|
19
|
+
// record so a second cause is an added value rather than a re-read of the
|
|
20
|
+
// sentence a person sees.
|
|
21
|
+
const blocked = out.action === "blocked" ? { reason: "landscape-invalid" } : null;
|
|
22
|
+
if (json) {
|
|
23
|
+
emitJson({
|
|
24
|
+
command: "subsystem",
|
|
25
|
+
path: "architecture/subsystems.likec4",
|
|
26
|
+
action: out.action,
|
|
27
|
+
subsystems: out.subsystems,
|
|
28
|
+
// ADDITIVE, and emitted on every run including the untouched one: a
|
|
29
|
+
// consumer must be able to read what happened to the second generated file
|
|
30
|
+
// without branching on whether this run happened to change it. `path` and
|
|
31
|
+
// `action` keep their frozen meaning — the subsystem file — so nothing
|
|
32
|
+
// reading them sees a new one.
|
|
33
|
+
serviceViews: { path: SERVICE_VIEWS, action: out.serviceViews },
|
|
34
|
+
...(blocked === null ? {} : { blocked }),
|
|
35
|
+
...(out.recovered === null ? {} : { recovered: out.recovered }),
|
|
36
|
+
projects: {
|
|
37
|
+
root: out.projects.root,
|
|
38
|
+
created: out.projects.created,
|
|
39
|
+
removed: out.projects.removed,
|
|
40
|
+
current: out.projects.current,
|
|
41
|
+
// Additive, and emitted in every mode including the untouched one: a
|
|
42
|
+
// consumer must be able to read what the root project now excludes
|
|
43
|
+
// without branching on whether this run happened to change it.
|
|
44
|
+
//
|
|
45
|
+
// `unreadable` travels with it because `entries: []` alone collapses the
|
|
46
|
+
// two answers `readRootExclude` insists must stay apart — "the file
|
|
47
|
+
// excludes nothing" and "loam could not read the file" are opposite
|
|
48
|
+
// facts about what the renderer will load, and the text view had the
|
|
49
|
+
// distinction while `--json` did not (verification 2026-09-04, review C).
|
|
50
|
+
exclude: {
|
|
51
|
+
updated: out.projects.exclude.updated,
|
|
52
|
+
entries: out.projects.exclude.entries,
|
|
53
|
+
added: out.projects.exclude.added,
|
|
54
|
+
removed: out.projects.exclude.removed,
|
|
55
|
+
unreadable: out.projects.exclude.unreadable,
|
|
56
|
+
},
|
|
57
|
+
},
|
|
58
|
+
});
|
|
59
|
+
return;
|
|
60
|
+
}
|
|
61
|
+
if (out.recovered !== null && out.recovered.outcome !== "consistent") {
|
|
62
|
+
console.log(`note: recovered an interrupted \`loam ${out.recovered.command}\` commit first (${out.recovered.outcome}).`);
|
|
63
|
+
}
|
|
64
|
+
console.log(views(out));
|
|
65
|
+
serviceViews(out.serviceViews);
|
|
66
|
+
// Owed on BOTH arms: the run that declined to rewrite an existing file, and
|
|
67
|
+
// the run that wrote the tolerant shape because there was none to protect.
|
|
68
|
+
if (out.mapUnreadable)
|
|
69
|
+
console.log(LANDSCAPE_UNREADABLE_NOTE);
|
|
70
|
+
projects(out.projects);
|
|
71
|
+
exclude(out.projects);
|
|
72
|
+
}
|
|
73
|
+
/** The one line about the generated views file. */
|
|
74
|
+
function views(out) {
|
|
75
|
+
// "nothing to write" is a claim about the WHOLE run, and it is false the moment
|
|
76
|
+
// any of this verb's FOUR writers landed something beside a current subsystem
|
|
77
|
+
// views file: the per-service views file, a project file created, one removed,
|
|
78
|
+
// or the root `exclude` rewritten. The exclude arm was missed when it was
|
|
79
|
+
// added, so the tail contradicted the very next line of output (verification
|
|
80
|
+
// 2026-09-04, review C) — and the per-service file is the same trap one file
|
|
81
|
+
// over, which is why it is in the same expression rather than a second one.
|
|
82
|
+
const quiet = out.serviceViews === "current" &&
|
|
83
|
+
out.projects.created.length === 0 &&
|
|
84
|
+
out.projects.removed.length === 0 &&
|
|
85
|
+
!out.projects.exclude.updated;
|
|
86
|
+
const sentence = {
|
|
87
|
+
current: `architecture/subsystems.likec4 is current (${out.subsystems} subsystem(s))${quiet ? " — nothing to write." : "."}`,
|
|
88
|
+
created: `wrote architecture/subsystems.likec4 — ${out.subsystems} subsystem view(s).`,
|
|
89
|
+
updated: `updated architecture/subsystems.likec4 — ${out.subsystems} subsystem view(s).`,
|
|
90
|
+
removed: `removed architecture/subsystems.likec4 — the tree has no subsystems, so the generated file must be absent.`,
|
|
91
|
+
blocked: "architecture/subsystems.likec4 left as it is — architecture/ does not parse as one project, so the " +
|
|
92
|
+
"render's element join is the landscape file's alone (a use-case document that binds an element of its " +
|
|
93
|
+
"own is not in it), and rewriting the file would take its `include` lines with it.",
|
|
94
|
+
refused: authoredRefusal("architecture/subsystems.likec4"),
|
|
95
|
+
};
|
|
96
|
+
return sentence[out.action];
|
|
97
|
+
}
|
|
98
|
+
/**
|
|
99
|
+
* The one line about `architecture/services.likec4`, printed on every answer but
|
|
100
|
+
* `current` — so on the two that left the file alone (`blocked`, `refused`) as
|
|
101
|
+
* well as the three that changed it. "Nothing happened to it, and here is why"
|
|
102
|
+
* is exactly the sentence a person needs; only "nothing happened to it because
|
|
103
|
+
* nothing needed to" is silence worth keeping.
|
|
104
|
+
*
|
|
105
|
+
* SILENT WHEN CURRENT, and that is the difference from the subsystem file's line
|
|
106
|
+
* above: that one is the sentence this command has always ended on and a person
|
|
107
|
+
* reads it as "the run happened", while a second unconditional line about a file
|
|
108
|
+
* nothing touched is noise on every run in a fleet that is already in order. The
|
|
109
|
+
* `--json` key is unconditional for the opposite reason — a consumer branches on
|
|
110
|
+
* a value, not on a line's presence.
|
|
111
|
+
*
|
|
112
|
+
* No view COUNT here, deliberately. The subsystem line counts subsystems because
|
|
113
|
+
* the tree answers that without reading anything; the per-service file draws
|
|
114
|
+
* only the services the MAP resolves, and the honest count is the render's, not
|
|
115
|
+
* the tree's. loam never reads a generated file's content (docs/DESIGN.md rule
|
|
116
|
+
* 26), so counting the views in it is the one place the count could come from
|
|
117
|
+
* and the one place it may not.
|
|
118
|
+
*/
|
|
119
|
+
function serviceViews(action) {
|
|
120
|
+
if (action === "current")
|
|
121
|
+
return;
|
|
122
|
+
const sentence = {
|
|
123
|
+
created: `wrote ${SERVICE_VIEWS} — one overview view per service the fleet map draws.`,
|
|
124
|
+
updated: `updated ${SERVICE_VIEWS} — one overview view per service the fleet map draws.`,
|
|
125
|
+
removed: `removed ${SERVICE_VIEWS} — the fleet map draws none of the services under services/, so the generated file must be absent.`,
|
|
126
|
+
blocked: `${SERVICE_VIEWS} left as it is — architecture/ does not parse as one project, so no service resolves to a ` +
|
|
127
|
+
"map element and the render would have emptied the file rather than improved it.",
|
|
128
|
+
refused: authoredRefusal(SERVICE_VIEWS),
|
|
129
|
+
};
|
|
130
|
+
console.log(sentence[action]);
|
|
131
|
+
}
|
|
132
|
+
/** The per-service project files this run created and removed. */
|
|
133
|
+
function projects(out) {
|
|
134
|
+
if (out.created.length > 0) {
|
|
135
|
+
console.log(`wrote ${out.created.length} services/<…>/likec4.config.json — one LikeC4 project per service model whose ` +
|
|
136
|
+
"model stands alone (an existing file is yours and is never rewritten; commit the new ones before a " +
|
|
137
|
+
"`loam subsystem move`, which refuses over untracked paths). With more than one project, " +
|
|
138
|
+
"likec4 validate at the docs root needs --project <name>; build and export take every project.");
|
|
139
|
+
}
|
|
140
|
+
// Named one per line: this is a DELETE of a file a team may have written, and
|
|
141
|
+
// a count would leave a reader nothing to check against their own diff.
|
|
142
|
+
for (const path of out.removed)
|
|
143
|
+
console.log(`removed ${path}`);
|
|
144
|
+
if (out.removed.length > 0) {
|
|
145
|
+
console.log("— that model extends the fleet map, and a project file beside it registers a second renderer project " +
|
|
146
|
+
"rooted at the service directory: `likec4 validate .` then refuses without `--project`, and wherever " +
|
|
147
|
+
"that project claims the model it loads it alone, where the map's kinds are not, and the fleet project " +
|
|
148
|
+
"loses the service's interior. This is what `service.likec4-config-stray` names.");
|
|
149
|
+
}
|
|
150
|
+
if (!out.root && out.modelled > 0) {
|
|
151
|
+
console.log("note: no likec4.config.json at the docs root, so no service project files were written — " +
|
|
152
|
+
"`loam doctor` prints the root file.");
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
/** The root project's `exclude`, and the one state loam refuses to touch. */
|
|
156
|
+
function exclude(out) {
|
|
157
|
+
const plan = out.exclude;
|
|
158
|
+
if (plan.updated) {
|
|
159
|
+
// Named entries rather than a count: this line is the whole record of a
|
|
160
|
+
// rewrite to a file the team owns, and "updated the exclude list" gives a
|
|
161
|
+
// reader nothing to check against their own diff.
|
|
162
|
+
const parts = [
|
|
163
|
+
...(plan.added.length > 0 ? [`+${plan.added.join(" +")}`] : []),
|
|
164
|
+
...(plan.removed.length > 0 ? [`-${plan.removed.join(" -")}`] : []),
|
|
165
|
+
];
|
|
166
|
+
console.log(`rewrote likec4.config.json's \`exclude\` (${parts.join(" ")}) — the root project excludes exactly the ` +
|
|
167
|
+
"models that stand alone; a model that extends the fleet map renders from the docs root and must not be excluded.");
|
|
168
|
+
}
|
|
169
|
+
if (plan.unreadable) {
|
|
170
|
+
console.log("note: likec4.config.json at the docs root is not a JSON object with a string `exclude` list, so it was " +
|
|
171
|
+
"left exactly as it is — loam does not rewrite a project file it cannot read.");
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
//# sourceMappingURL=report.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"report.js","sourceRoot":"","sources":["../../../../src/commands/subsystem/txn/report.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;GAUG;AACH,OAAO,EAAE,QAAQ,EAAE,MAAM,gCAAgC,CAAC;AAC1D,OAAO,EAAE,eAAe,EAAE,MAAM,kDAAkD,CAAC;AAGnF,OAAO,EAAE,yBAAyB,EAAE,MAAM,YAAY,CAAC;AAoBvD,oGAAoG;AACpG,MAAM,aAAa,GAAG,8BAA8B,CAAC;AAiDrD,MAAM,UAAU,UAAU,CAAC,IAAa,EAAE,GAAe;IACvD,8EAA8E;IAC9E,0EAA0E;IAC1E,0BAA0B;IAC1B,MAAM,OAAO,GAAuB,GAAG,CAAC,MAAM,KAAK,SAAS,CAAC,CAAC,CAAC,EAAE,MAAM,EAAE,mBAAmB,EAAE,CAAC,CAAC,CAAC,IAAI,CAAC;IACtG,IAAI,IAAI,EAAE,CAAC;QACT,QAAQ,CAAC;YACP,OAAO,EAAE,WAAW;YACpB,IAAI,EAAE,gCAAgC;YACtC,MAAM,EAAE,GAAG,CAAC,MAAM;YAClB,UAAU,EAAE,GAAG,CAAC,UAAU;YAC1B,oEAAoE;YACpE,2EAA2E;YAC3E,0EAA0E;YAC1E,uEAAuE;YACvE,+BAA+B;YAC/B,YAAY,EAAE,EAAE,IAAI,EAAE,aAAa,EAAE,MAAM,EAAE,GAAG,CAAC,YAAY,EAAE;YAC/D,GAAG,CAAC,OAAO,KAAK,IAAI,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,EAAE,OAAO,EAAE,CAAC;YACxC,GAAG,CAAC,GAAG,CAAC,SAAS,KAAK,IAAI,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,EAAE,SAAS,EAAE,GAAG,CAAC,SAAS,EAAE,CAAC;YAC/D,QAAQ,EAAE;gBACR,IAAI,EAAE,GAAG,CAAC,QAAQ,CAAC,IAAI;gBACvB,OAAO,EAAE,GAAG,CAAC,QAAQ,CAAC,OAAO;gBAC7B,OAAO,EAAE,GAAG,CAAC,QAAQ,CAAC,OAAO;gBAC7B,OAAO,EAAE,GAAG,CAAC,QAAQ,CAAC,OAAO;gBAC7B,qEAAqE;gBACrE,mEAAmE;gBACnE,+DAA+D;gBAC/D,EAAE;gBACF,yEAAyE;gBACzE,oEAAoE;gBACpE,oEAAoE;gBACpE,qEAAqE;gBACrE,0EAA0E;gBAC1E,OAAO,EAAE;oBACP,OAAO,EAAE,GAAG,CAAC,QAAQ,CAAC,OAAO,CAAC,OAAO;oBACrC,OAAO,EAAE,GAAG,CAAC,QAAQ,CAAC,OAAO,CAAC,OAAO;oBACrC,KAAK,EAAE,GAAG,CAAC,QAAQ,CAAC,OAAO,CAAC,KAAK;oBACjC,OAAO,EAAE,GAAG,CAAC,QAAQ,CAAC,OAAO,CAAC,OAAO;oBACrC,UAAU,EAAE,GAAG,CAAC,QAAQ,CAAC,OAAO,CAAC,UAAU;iBAC5C;aACF;SACF,CAAC,CAAC;QACH,OAAO;IACT,CAAC;IACD,IAAI,GAAG,CAAC,SAAS,KAAK,IAAI,IAAI,GAAG,CAAC,SAAS,CAAC,OAAO,KAAK,YAAY,EAAE,CAAC;QACrE,OAAO,CAAC,GAAG,CACT,yCAAyC,GAAG,CAAC,SAAS,CAAC,OAAO,oBAAoB,GAAG,CAAC,SAAS,CAAC,OAAO,IAAI,CAC5G,CAAC;IACJ,CAAC;IACD,OAAO,CAAC,GAAG,CAAC,KAAK,CAAC,GAAG,CAAC,CAAC,CAAC;IACxB,YAAY,CAAC,GAAG,CAAC,YAAY,CAAC,CAAC;IAC/B,4EAA4E;IAC5E,2EAA2E;IAC3E,IAAI,GAAG,CAAC,aAAa;QAAE,OAAO,CAAC,GAAG,CAAC,yBAAyB,CAAC,CAAC;IAC9D,QAAQ,CAAC,GAAG,CAAC,QAAQ,CAAC,CAAC;IACvB,OAAO,CAAC,GAAG,CAAC,QAAQ,CAAC,CAAC;AACxB,CAAC;AAED,mDAAmD;AACnD,SAAS,KAAK,CAAC,GAAe;IAC5B,gFAAgF;IAChF,8EAA8E;IAC9E,+EAA+E;IAC/E,0EAA0E;IAC1E,6EAA6E;IAC7E,6EAA6E;IAC7E,4EAA4E;IAC5E,MAAM,KAAK,GACT,GAAG,CAAC,YAAY,KAAK,SAAS;QAC9B,GAAG,CAAC,QAAQ,CAAC,OAAO,CAAC,MAAM,KAAK,CAAC;QACjC,GAAG,CAAC,QAAQ,CAAC,OAAO,CAAC,MAAM,KAAK,CAAC;QACjC,CAAC,GAAG,CAAC,QAAQ,CAAC,OAAO,CAAC,OAAO,CAAC;IAChC,MAAM,QAAQ,GAA+B;QAC3C,OAAO,EAAE,8CAA8C,GAAG,CAAC,UAAU,iBAAiB,KAAK,CAAC,CAAC,CAAC,sBAAsB,CAAC,CAAC,CAAC,GAAG,EAAE;QAC5H,OAAO,EAAE,0CAA0C,GAAG,CAAC,UAAU,qBAAqB;QACtF,OAAO,EAAE,4CAA4C,GAAG,CAAC,UAAU,qBAAqB;QACxF,OAAO,EAAE,4GAA4G;QACrH,OAAO,EACL,qGAAqG;YACrG,wGAAwG;YACxG,mFAAmF;QACrF,OAAO,EAAE,eAAe,CAAC,gCAAgC,CAAC;KAC3D,CAAC;IACF,OAAO,QAAQ,CAAC,GAAG,CAAC,MAAM,CAAC,CAAC;AAC9B,CAAC;AAED;;;;;;;;;;;;;;;;;;;;GAoBG;AACH,SAAS,YAAY,CAAC,MAAkB;IACtC,IAAI,MAAM,KAAK,SAAS;QAAE,OAAO;IACjC,MAAM,QAAQ,GAAmD;QAC/D,OAAO,EAAE,SAAS,aAAa,uDAAuD;QACtF,OAAO,EAAE,WAAW,aAAa,uDAAuD;QACxF,OAAO,EAAE,WAAW,aAAa,oGAAoG;QACrI,OAAO,EACL,GAAG,aAAa,4FAA4F;YAC5G,iFAAiF;QACnF,OAAO,EAAE,eAAe,CAAC,aAAa,CAAC;KACxC,CAAC;IACF,OAAO,CAAC,GAAG,CAAC,QAAQ,CAAC,MAAM,CAAC,CAAC,CAAC;AAChC,CAAC;AAED,kEAAkE;AAClE,SAAS,QAAQ,CAAC,GAAmB;IACnC,IAAI,GAAG,CAAC,OAAO,CAAC,MAAM,GAAG,CAAC,EAAE,CAAC;QAC3B,OAAO,CAAC,GAAG,CACT,SAAS,GAAG,CAAC,OAAO,CAAC,MAAM,gFAAgF;YACzG,qGAAqG;YACrG,0FAA0F;YAC1F,+FAA+F,CAClG,CAAC;IACJ,CAAC;IACD,8EAA8E;IAC9E,wEAAwE;IACxE,KAAK,MAAM,IAAI,IAAI,GAAG,CAAC,OAAO;QAAE,OAAO,CAAC,GAAG,CAAC,WAAW,IAAI,EAAE,CAAC,CAAC;IAC/D,IAAI,GAAG,CAAC,OAAO,CAAC,MAAM,GAAG,CAAC,EAAE,CAAC;QAC3B,OAAO,CAAC,GAAG,CACT,uGAAuG;YACrG,sGAAsG;YACtG,wGAAwG;YACxG,iFAAiF,CACpF,CAAC;IACJ,CAAC;IACD,IAAI,CAAC,GAAG,CAAC,IAAI,IAAI,GAAG,CAAC,QAAQ,GAAG,CAAC,EAAE,CAAC;QAClC,OAAO,CAAC,GAAG,CACT,2FAA2F;YACzF,qCAAqC,CACxC,CAAC;IACJ,CAAC;AACH,CAAC;AAED,6EAA6E;AAC7E,SAAS,OAAO,CAAC,GAAmB;IAClC,MAAM,IAAI,GAAG,GAAG,CAAC,OAAO,CAAC;IACzB,IAAI,IAAI,CAAC,OAAO,EAAE,CAAC;QACjB,wEAAwE;QACxE,0EAA0E;QAC1E,kDAAkD;QAClD,MAAM,KAAK,GAAG;YACZ,GAAG,CAAC,IAAI,CAAC,KAAK,CAAC,MAAM,GAAG,CAAC,CAAC,CAAC,CAAC,CAAC,IAAI,IAAI,CAAC,KAAK,CAAC,IAAI,CAAC,IAAI,CAAC,EAAE,CAAC,CAAC,CAAC,CAAC,EAAE,CAAC;YAC/D,GAAG,CAAC,IAAI,CAAC,OAAO,CAAC,MAAM,GAAG,CAAC,CAAC,CAAC,CAAC,CAAC,IAAI,IAAI,CAAC,OAAO,CAAC,IAAI,CAAC,IAAI,CAAC,EAAE,CAAC,CAAC,CAAC,CAAC,EAAE,CAAC;SACpE,CAAC;QACF,OAAO,CAAC,GAAG,CACT,6CAA6C,KAAK,CAAC,IAAI,CAAC,GAAG,CAAC,4CAA4C;YACtG,kHAAkH,CACrH,CAAC;IACJ,CAAC;IACD,IAAI,IAAI,CAAC,UAAU,EAAE,CAAC;QACpB,OAAO,CAAC,GAAG,CACT,yGAAyG;YACvG,8EAA8E,CACjF,CAAC;IACJ,CAAC;AACH,CAAC","sourcesContent":["/**\n * What one `loam subsystem sync` SAYS it did — the `--json` envelope and the\n * human lines, in one place.\n *\n * It sits beside `./views.ts` and `./exclude.ts` because it is the third answer\n * about the same three files, and it moved out of `../sync.ts` for that file's\n * standing reason: `sync.ts` is the lock, the transaction and the plan, and it\n * reached the 400-line limit carrying the output too. Composing a report is not\n * a phase of the commit, so nothing here reads the disk or decides anything — a\n * caller hands over what happened and this spells it.\n */\nimport { emitJson } from \"../../../core/envelope/json.js\";\nimport { authoredRefusal } from \"../../../core/repo/tree/render/service/header.js\";\nimport type { CommitRecovery } from \"../../../core/staging/interrupted.js\";\nimport type { ExcludePlan } from \"./exclude.js\";\nimport { LANDSCAPE_UNREADABLE_NOTE } from \"./views.js\";\n\n/**\n * What one sync did to the views file — and ONLY that file; the projects report\n * separately.\n *\n * `blocked` is the fifth answer: the map cannot be read, a generated file\n * already exists, and this run left it exactly as it is. The tolerant render\n * used to run anyway and rewrote a good file down to title and description\n * (verification 2026-09-04, W5). The project half is unaffected and still runs —\n * neither the per-service files nor the root `exclude` depend on the map.\n *\n * `refused` is the sixth, and it is about OWNERSHIP rather than about the map:\n * the file standing at a path loam generates does not open with loam's mark, so\n * it is somebody's authored document and this command will not take it. It is\n * the only action that also sets a non-zero exit code (`../sync.ts` says why),\n * because unlike `blocked` it is not a state any later run clears on its own.\n */\nexport type SyncAction = \"current\" | \"created\" | \"updated\" | \"removed\" | \"blocked\" | \"refused\";\n\n/** The second generated file's name, spelled once here so the key and the sentence cannot drift. */\nconst SERVICE_VIEWS = \"architecture/services.likec4\";\n\n/** Why a run answered `blocked`. One reason today; a key rather than a bare string so a second one is additive. */\ninterface SyncBlocked {\n reason: \"landscape-invalid\";\n}\n\n/** The additive `projects` payload key, and what the text lines are composed from. */\nexport interface ProjectsReport {\n root: boolean;\n /** POSIX paths relative to the DOCS ROOT, written this run, sorted by path. */\n created: string[];\n /**\n * POSIX paths relative to the DOCS ROOT, DELETED this run, sorted by path — a\n * `likec4.config.json` beside a model that extends the map. `[]` rather than\n * absent on a run that removed none, so a consumer reads the key.\n *\n * SORTED BY PATH rather than by service id, which is the order the survey\n * hands them over in: `../sync.ts` sorts both arrays where it spells them, so\n * the `--json` list and the `removed <path>` lines below can never disagree.\n */\n removed: string[];\n current: number;\n modelled: number;\n exclude: ExcludePlan;\n}\n\n/** Everything one run has to say, as the caller knows it. */\nexport interface SyncReport {\n /** `architecture/subsystems.likec4` — and nothing else; the `--json` `action` key is exactly this. */\n action: SyncAction;\n /**\n * `architecture/services.likec4` — the per-service overviews.\n *\n * A field of its own rather than a folded `action`, for the reason the shared\n * finding code carries the path: the two files drift for different reasons and\n * a reader told only that \"the generated views changed\" would have to diff the\n * repo to find out which one. `blocked` reaches it exactly as it reaches the\n * other, and for a sharper reason — with no readable map no service resolves,\n * so the tolerant render is the empty file rather than a poorer one.\n */\n serviceViews: SyncAction;\n subsystems: number;\n recovered: CommitRecovery | null;\n projects: ProjectsReport;\n /** The map could not be read at all — the note is owed whether or not a file was protected. */\n mapUnreadable: boolean;\n}\n\nexport function reportSync(json: boolean, out: SyncReport): void {\n // One reason today, and it is exactly the `action` it explains — spelled as a\n // record so a second cause is an added value rather than a re-read of the\n // sentence a person sees.\n const blocked: SyncBlocked | null = out.action === \"blocked\" ? { reason: \"landscape-invalid\" } : null;\n if (json) {\n emitJson({\n command: \"subsystem\",\n path: \"architecture/subsystems.likec4\",\n action: out.action,\n subsystems: out.subsystems,\n // ADDITIVE, and emitted on every run including the untouched one: a\n // consumer must be able to read what happened to the second generated file\n // without branching on whether this run happened to change it. `path` and\n // `action` keep their frozen meaning — the subsystem file — so nothing\n // reading them sees a new one.\n serviceViews: { path: SERVICE_VIEWS, action: out.serviceViews },\n ...(blocked === null ? {} : { blocked }),\n ...(out.recovered === null ? {} : { recovered: out.recovered }),\n projects: {\n root: out.projects.root,\n created: out.projects.created,\n removed: out.projects.removed,\n current: out.projects.current,\n // Additive, and emitted in every mode including the untouched one: a\n // consumer must be able to read what the root project now excludes\n // without branching on whether this run happened to change it.\n //\n // `unreadable` travels with it because `entries: []` alone collapses the\n // two answers `readRootExclude` insists must stay apart — \"the file\n // excludes nothing\" and \"loam could not read the file\" are opposite\n // facts about what the renderer will load, and the text view had the\n // distinction while `--json` did not (verification 2026-09-04, review C).\n exclude: {\n updated: out.projects.exclude.updated,\n entries: out.projects.exclude.entries,\n added: out.projects.exclude.added,\n removed: out.projects.exclude.removed,\n unreadable: out.projects.exclude.unreadable,\n },\n },\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 console.log(views(out));\n serviceViews(out.serviceViews);\n // Owed on BOTH arms: the run that declined to rewrite an existing file, and\n // the run that wrote the tolerant shape because there was none to protect.\n if (out.mapUnreadable) console.log(LANDSCAPE_UNREADABLE_NOTE);\n projects(out.projects);\n exclude(out.projects);\n}\n\n/** The one line about the generated views file. */\nfunction views(out: SyncReport): string {\n // \"nothing to write\" is a claim about the WHOLE run, and it is false the moment\n // any of this verb's FOUR writers landed something beside a current subsystem\n // views file: the per-service views file, a project file created, one removed,\n // or the root `exclude` rewritten. The exclude arm was missed when it was\n // added, so the tail contradicted the very next line of output (verification\n // 2026-09-04, review C) — and the per-service file is the same trap one file\n // over, which is why it is in the same expression rather than a second one.\n const quiet =\n out.serviceViews === \"current\" &&\n out.projects.created.length === 0 &&\n out.projects.removed.length === 0 &&\n !out.projects.exclude.updated;\n const sentence: Record<SyncAction, string> = {\n current: `architecture/subsystems.likec4 is current (${out.subsystems} subsystem(s))${quiet ? \" — 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 blocked:\n \"architecture/subsystems.likec4 left as it is — architecture/ does not parse as one project, so the \" +\n \"render's element join is the landscape file's alone (a use-case document that binds an element of its \" +\n \"own is not in it), and rewriting the file would take its `include` lines with it.\",\n refused: authoredRefusal(\"architecture/subsystems.likec4\"),\n };\n return sentence[out.action];\n}\n\n/**\n * The one line about `architecture/services.likec4`, printed on every answer but\n * `current` — so on the two that left the file alone (`blocked`, `refused`) as\n * well as the three that changed it. \"Nothing happened to it, and here is why\"\n * is exactly the sentence a person needs; only \"nothing happened to it because\n * nothing needed to\" is silence worth keeping.\n *\n * SILENT WHEN CURRENT, and that is the difference from the subsystem file's line\n * above: that one is the sentence this command has always ended on and a person\n * reads it as \"the run happened\", while a second unconditional line about a file\n * nothing touched is noise on every run in a fleet that is already in order. The\n * `--json` key is unconditional for the opposite reason — a consumer branches on\n * a value, not on a line's presence.\n *\n * No view COUNT here, deliberately. The subsystem line counts subsystems because\n * the tree answers that without reading anything; the per-service file draws\n * only the services the MAP resolves, and the honest count is the render's, not\n * the tree's. loam never reads a generated file's content (docs/DESIGN.md rule\n * 26), so counting the views in it is the one place the count could come from\n * and the one place it may not.\n */\nfunction serviceViews(action: SyncAction): void {\n if (action === \"current\") return;\n const sentence: Record<Exclude<SyncAction, \"current\">, string> = {\n created: `wrote ${SERVICE_VIEWS} — one overview view per service the fleet map draws.`,\n updated: `updated ${SERVICE_VIEWS} — one overview view per service the fleet map draws.`,\n removed: `removed ${SERVICE_VIEWS} — the fleet map draws none of the services under services/, so the generated file must be absent.`,\n blocked:\n `${SERVICE_VIEWS} left as it is — architecture/ does not parse as one project, so no service resolves to a ` +\n \"map element and the render would have emptied the file rather than improved it.\",\n refused: authoredRefusal(SERVICE_VIEWS),\n };\n console.log(sentence[action]);\n}\n\n/** The per-service project files this run created and removed. */\nfunction projects(out: ProjectsReport): void {\n if (out.created.length > 0) {\n console.log(\n `wrote ${out.created.length} services/<…>/likec4.config.json — one LikeC4 project per service model whose ` +\n \"model stands alone (an existing file is yours and is never rewritten; 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 // Named one per line: this is a DELETE of a file a team may have written, and\n // a count would leave a reader nothing to check against their own diff.\n for (const path of out.removed) console.log(`removed ${path}`);\n if (out.removed.length > 0) {\n console.log(\n \"— that model extends the fleet map, and a project file beside it registers a second renderer project \" +\n \"rooted at the service directory: `likec4 validate .` then refuses without `--project`, and wherever \" +\n \"that project claims the model it loads it alone, where the map's kinds are not, and the fleet project \" +\n \"loses the service's interior. This is what `service.likec4-config-stray` names.\",\n );\n }\n if (!out.root && out.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\n/** The root project's `exclude`, and the one state loam refuses to touch. */\nfunction exclude(out: ProjectsReport): void {\n const plan = out.exclude;\n if (plan.updated) {\n // Named entries rather than a count: this line is the whole record of a\n // rewrite to a file the team owns, and \"updated the exclude list\" gives a\n // reader nothing to check against their own diff.\n const parts = [\n ...(plan.added.length > 0 ? [`+${plan.added.join(\" +\")}`] : []),\n ...(plan.removed.length > 0 ? [`-${plan.removed.join(\" -\")}`] : []),\n ];\n console.log(\n `rewrote likec4.config.json's \\`exclude\\` (${parts.join(\" \")}) — the root project excludes exactly the ` +\n \"models that stand alone; a model that extends the fleet map renders from the docs root and must not be excluded.\",\n );\n }\n if (plan.unreadable) {\n console.log(\n \"note: likec4.config.json at the docs root is not a JSON object with a string `exclude` list, so it was \" +\n \"left exactly as it is — loam does not rewrite a project file it cannot read.\",\n );\n }\n}\n"]}
|
|
@@ -1,12 +1,15 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* The shared commit window every subsystem WRITE verb goes through — `new`,
|
|
3
3
|
* `rm`, `move` and `rename` all land here: lock, recover a predecessor's
|
|
4
|
-
* journal, plan against the walked tree, regenerate
|
|
5
|
-
* PLANNED post-state, commit through the journaled transaction
|
|
6
|
-
* renames included), report. One window, because
|
|
7
|
-
* the same commit as whatever changed the tree — a marker without its
|
|
8
|
-
* or renames without theirs, is exactly the `subsystem.views-stale` state
|
|
9
|
-
* the generated
|
|
4
|
+
* journal, plan against the walked tree, regenerate BOTH generated views files
|
|
5
|
+
* from the PLANNED post-state, commit through the journaled transaction
|
|
6
|
+
* (directory renames included), report. One window, because those files must
|
|
7
|
+
* land in the same commit as whatever changed the tree — a marker without its
|
|
8
|
+
* view, or renames without theirs, is exactly the `subsystem.views-stale` state
|
|
9
|
+
* the generated files' contract refuses to leave between two commits. The
|
|
10
|
+
* per-service file is in that rule and not beside it: its titles are
|
|
11
|
+
* `<subsystem path> / <service id>`, so the placement a verb changes is written
|
|
12
|
+
* into it too.
|
|
10
13
|
*
|
|
11
14
|
* This window does NOT write the per-service project files
|
|
12
15
|
* (`services/<…>/<id>/likec4.config.json`); only `../sync.ts` does, and the
|
|
@@ -26,11 +29,19 @@
|
|
|
26
29
|
* model with no file beside it, and that is the state `validate --all` names
|
|
27
30
|
* as `service.likec4-config-missing`, with `loam subsystem sync` as the
|
|
28
31
|
* repair — one writer, so the create-only rule has one place to hold.
|
|
32
|
+
*
|
|
33
|
+
* NEITHER generated file is written over a document loam did not write, and
|
|
34
|
+
* that guard is checked HERE as well as in `../sync.ts` — see the comment at
|
|
35
|
+
* the check. Both are filenames loam RESERVES inside a directory the team
|
|
36
|
+
* owns, both are rewritten whole, and until the check below existed all four
|
|
37
|
+
* verbs took an authored file at either path without a word. Unlike sync,
|
|
38
|
+
* which reports the one file `refused` and completes the rest, a verb refuses
|
|
39
|
+
* WHOLE: the file has to land in this commit, so half of this commit is not a
|
|
40
|
+
* lesser version of it.
|
|
29
41
|
*/
|
|
30
42
|
import { existsSync } from "node:fs";
|
|
31
43
|
import { readFile } from "node:fs/promises";
|
|
32
|
-
import { fail } from "../../../core/envelope/json.js";
|
|
33
|
-
import { subsystemViewsPath } from "../../../core/repo/paths.js";
|
|
44
|
+
import { fail, repoPath } from "../../../core/envelope/json.js";
|
|
34
45
|
import { listFleetTree } from "../../../core/repo/repo.js";
|
|
35
46
|
import { stageWrites } from "../../../core/staging/commit.js";
|
|
36
47
|
import { InterruptedCommitError } from "../../../core/staging/interrupted.js";
|
|
@@ -38,11 +49,21 @@ import { acquireDocsLock, DocsBusyError } from "../../../core/staging/lock.js";
|
|
|
38
49
|
import { recoverInterruptedCommit } from "../../../core/staging/recovery/recover.js";
|
|
39
50
|
import { commitStaged } from "../../../core/staging/txn/transaction.js";
|
|
40
51
|
import {} from "../../../core/staging/writes.js";
|
|
52
|
+
import { authoredGeneratedPaths, authoredRefusal } from "../../../core/repo/tree/render/service/header.js";
|
|
41
53
|
import { viewsAgree } from "../../../core/repo/tree/render/views.js";
|
|
42
|
-
import { expectedViews } from "./views.js";
|
|
43
|
-
/** The text tail a verb's success line carries about the generated file. */
|
|
54
|
+
import { expectedViews, LANDSCAPE_UNREADABLE_NOTE } from "./views.js";
|
|
55
|
+
/** The text tail a verb's success line carries about the generated files — one clause per file that moved. */
|
|
44
56
|
export function reportViews(views) {
|
|
45
|
-
return
|
|
57
|
+
return ([
|
|
58
|
+
["architecture/subsystems.likec4", views.subsystems],
|
|
59
|
+
["architecture/services.likec4", views.services],
|
|
60
|
+
]
|
|
61
|
+
// `current` says nothing: the tail hangs off a success line, and a verb
|
|
62
|
+
// that renamed one directory must not print two clauses about files that
|
|
63
|
+
// did not move.
|
|
64
|
+
.filter(([, action]) => action !== "current")
|
|
65
|
+
.map(([file, action]) => ` — ${file} ${action}`)
|
|
66
|
+
.join(""));
|
|
46
67
|
}
|
|
47
68
|
/**
|
|
48
69
|
* Lock → recover → plan → stage (views included) → journaled commit → report.
|
|
@@ -74,19 +95,69 @@ export async function commitWindow(docsDir, json, plan) {
|
|
|
74
95
|
const txn = await plan(await listFleetTree(docsDir));
|
|
75
96
|
if (txn === null)
|
|
76
97
|
return;
|
|
77
|
-
const path = subsystemViewsPath(docsDir);
|
|
78
98
|
const expected = await expectedViews(docsDir, txn.tree);
|
|
79
|
-
|
|
80
|
-
//
|
|
81
|
-
//
|
|
82
|
-
//
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
99
|
+
// OWNERSHIP BEFORE ANYTHING IS STAGED, and the WHOLE verb is refused.
|
|
100
|
+
//
|
|
101
|
+
// The guard first shipped in `../sync.ts` alone, and this window is the
|
|
102
|
+
// other writer of exactly the same two reserved paths — for `subsystem
|
|
103
|
+
// new`, `rm`, `move` and `rename` at once. Reproduced before this call
|
|
104
|
+
// existed: with a hand-authored `architecture/services.likec4` in the tree,
|
|
105
|
+
// `loam subsystem new platform` printed "architecture/services.likec4
|
|
106
|
+
// updated" and destroyed it, first line and all. One predicate, one
|
|
107
|
+
// sentence, shared from beneath every writer
|
|
108
|
+
// (`core/repo/tree/render/service/header.ts`).
|
|
109
|
+
//
|
|
110
|
+
// WHY THE WHOLE OPERATION AND NOT JUST THE FILE. `sync` can decline one file
|
|
111
|
+
// and still do the rest, because nothing else it writes depends on that file
|
|
112
|
+
// and every part of it is idempotent. This window cannot: its entire reason
|
|
113
|
+
// for existing is that the generated files land in the SAME commit as the
|
|
114
|
+
// change to the tree, so completing the structural half and reporting the
|
|
115
|
+
// file as refused would MANUFACTURE the `subsystem.views-stale` state the
|
|
116
|
+
// banner above says this window refuses to leave between two commits — and
|
|
117
|
+
// it would do so irreversibly, since `move` and `rename` also stage their
|
|
118
|
+
// renames in git. A half-done move nobody asked for is worse than a refusal
|
|
119
|
+
// a person can act on, and a refusal is re-runnable the moment the authored
|
|
120
|
+
// file is out of the reserved path.
|
|
121
|
+
//
|
|
122
|
+
// It runs AFTER `plan`, which writes nothing and has already answered the
|
|
123
|
+
// verb-specific refusals (`unknown-target`, `subsystem-not-empty`,
|
|
124
|
+
// `move-uncommitted`): a caller whose name is wrong AND whose tree holds an
|
|
125
|
+
// authored views file is better told about the name.
|
|
126
|
+
const authored = await authoredGeneratedPaths([expected.subsystems.path, expected.services.path]);
|
|
127
|
+
if (authored.length > 0) {
|
|
128
|
+
fail(json,
|
|
129
|
+
// The code is `already-exists` and not a new one because it is exactly
|
|
130
|
+
// what that code has always meant here — "refusing to create over
|
|
131
|
+
// something that already exists, because overwriting would destroy work
|
|
132
|
+
// nobody asked to lose" — and it is already this command's documented
|
|
133
|
+
// refusal for the flat namespace. A second code for the same sentence
|
|
134
|
+
// would be a second branch an agent has to learn for no new decision.
|
|
135
|
+
"already-exists", authored.map((path) => authoredRefusal(repoPath(docsDir, path))).join(" ") +
|
|
136
|
+
" NOTHING was written and nothing was renamed: this command lands the generated views file in the same " +
|
|
137
|
+
"commit as the change to the tree, so it refuses whole rather than leaving the tree changed — and, for " +
|
|
138
|
+
"`move` and `rename`, renames staged in git — with a views file it may not write.");
|
|
139
|
+
return;
|
|
89
140
|
}
|
|
141
|
+
const writes = [...txn.writes];
|
|
142
|
+
// BOTH generated files, in the ONE commit that changes the tree. The
|
|
143
|
+
// per-service file is not a bystander here: its view titles are
|
|
144
|
+
// `<subsystem path> / <service id>`, so every `move` and `rename` restales
|
|
145
|
+
// it exactly as it restales the subsystem views, and leaving it for a later
|
|
146
|
+
// `sync` is the failing-between-two-commits state this window exists to
|
|
147
|
+
// refuse.
|
|
148
|
+
const views = {
|
|
149
|
+
subsystems: await stageGenerated(expected.subsystems, writes, false),
|
|
150
|
+
// PROTECTED when the map did not parse as a project, and only this file
|
|
151
|
+
// is: `renderSubsystemViews` answers null off the TREE, which is readable
|
|
152
|
+
// either way, while `renderServiceOverviews` answers null whenever no
|
|
153
|
+
// service resolves to an element — which is what an unreadable map looks
|
|
154
|
+
// like from here. Under `known: false` a null render is therefore not
|
|
155
|
+
// evidence of absence, and writing it would delete the whole file over a
|
|
156
|
+
// sibling document that does not parse. A verb still rewrites a file it
|
|
157
|
+
// CAN render (the tolerant arm this window has always taken), and the
|
|
158
|
+
// person is told by the note at the foot of this function.
|
|
159
|
+
services: await stageGenerated(expected.services, writes, !expected.known),
|
|
160
|
+
};
|
|
90
161
|
const staged = await stageWrites(writes);
|
|
91
162
|
const committed = await commitStaged({
|
|
92
163
|
root: docsDir,
|
|
@@ -105,9 +176,42 @@ export async function commitWindow(docsDir, json, plan) {
|
|
|
105
176
|
return;
|
|
106
177
|
}
|
|
107
178
|
await txn.report(views, recovered);
|
|
179
|
+
// A verb CANNOT leave this file behind the tree — its bytes depend on the
|
|
180
|
+
// placement the verb just changed, so the tolerant render lands even when the
|
|
181
|
+
// map is unreadable and the file loses its includes. `sync` may decline that
|
|
182
|
+
// rewrite (`action: "blocked"`); a verb may not, so the person is told
|
|
183
|
+
// instead, and the same note names the run that puts the includes back.
|
|
184
|
+
if (!json && !expected.known)
|
|
185
|
+
console.log(LANDSCAPE_UNREADABLE_NOTE);
|
|
108
186
|
}
|
|
109
187
|
finally {
|
|
110
188
|
await releaseLock();
|
|
111
189
|
}
|
|
112
190
|
}
|
|
191
|
+
/**
|
|
192
|
+
* Compare ONE generated file against what it must say, append the write when
|
|
193
|
+
* they differ, and name what happened.
|
|
194
|
+
*
|
|
195
|
+
* The compare is over CONTENT, not bytes — `viewsAgree` carries the reason,
|
|
196
|
+
* beside the generator that mints them: an ordinary Windows clone hands a
|
|
197
|
+
* generated file back with CRLF and not one fact in it changed, and without
|
|
198
|
+
* this every subsystem move on such a clone folded a rewrite that changed
|
|
199
|
+
* nothing into its transaction and journalled it. What gets WRITTEN is still
|
|
200
|
+
* the LF render as a Buffer, so the decode here is a comparison only.
|
|
201
|
+
*
|
|
202
|
+
* `protect` is the caller's statement that a NULL render is not evidence: the
|
|
203
|
+
* file is left exactly as it is rather than deleted. It is never a licence to
|
|
204
|
+
* skip a real rewrite — a file loam can render is rendered, which is the
|
|
205
|
+
* difference between this window and `../sync.ts`'s `blocked`.
|
|
206
|
+
*/
|
|
207
|
+
async function stageGenerated(file, writes, protect) {
|
|
208
|
+
const current = existsSync(file.path) ? await readFile(file.path, "utf8") : null;
|
|
209
|
+
if (viewsAgree(current, file.content))
|
|
210
|
+
return "current";
|
|
211
|
+
if (protect && file.content === null)
|
|
212
|
+
return "current";
|
|
213
|
+
const bytes = file.content === null ? null : Buffer.from(file.content, "utf8");
|
|
214
|
+
writes.push({ path: file.path, content: bytes });
|
|
215
|
+
return current === null ? "created" : bytes === null ? "removed" : "updated";
|
|
216
|
+
}
|
|
113
217
|
//# sourceMappingURL=txn.js.map
|