@ybotok/loam 0.2.0-alpha.7 → 0.2.0-alpha.9
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 +1315 -1
- package/COMPARISON.md +91 -3
- package/CONTRIBUTING.md +37 -0
- package/MIGRATING-from-OpenSpec.md +40 -22
- package/QUICKSTART.md +181 -9
- package/README.md +256 -57
- package/ROADMAP.md +83 -7
- package/SCHEMA.md +569 -98
- package/WORKFLOW.md +272 -62
- package/dist/cli.js +26 -8
- package/dist/cli.js.map +1 -1
- package/dist/commands/adopt/adopt.js +5 -10
- package/dist/commands/adopt/adopt.js.map +1 -1
- package/dist/commands/archive/archive.js +15 -18
- package/dist/commands/archive/archive.js.map +1 -1
- package/dist/commands/archive/plan/contracts/asyncapi.js +27 -41
- package/dist/commands/archive/plan/contracts/asyncapi.js.map +1 -1
- package/dist/commands/archive/plan/contracts/consumers.js +56 -0
- package/dist/commands/archive/plan/contracts/consumers.js.map +1 -0
- package/dist/commands/archive/plan/contracts/openapi.js +25 -42
- package/dist/commands/archive/plan/contracts/openapi.js.map +1 -1
- package/dist/commands/archive/plan/contracts/verdict-lines.js +41 -0
- package/dist/commands/archive/plan/contracts/verdict-lines.js.map +1 -0
- package/dist/commands/archive/plan/gate.js +3 -7
- package/dist/commands/archive/plan/gate.js.map +1 -1
- package/dist/commands/archive/plan/landscape.js +4 -4
- package/dist/commands/archive/plan/landscape.js.map +1 -1
- package/dist/commands/archive/plan/refusal.js +8 -18
- package/dist/commands/archive/plan/refusal.js.map +1 -1
- package/dist/commands/archive/plan/specs.js +134 -25
- package/dist/commands/archive/plan/specs.js.map +1 -1
- package/dist/commands/archive/plan/state.js +1 -0
- package/dist/commands/archive/plan/state.js.map +1 -1
- package/dist/commands/archive/run.js +33 -11
- package/dist/commands/archive/run.js.map +1 -1
- package/dist/commands/archive/shipped.js +49 -0
- package/dist/commands/archive/shipped.js.map +1 -0
- package/dist/commands/context/context.js +4 -7
- package/dist/commands/context/context.js.map +1 -1
- package/dist/commands/context/print.js +2 -2
- package/dist/commands/context/print.js.map +1 -1
- package/dist/commands/delta/delta.js +44 -12
- package/dist/commands/delta/delta.js.map +1 -1
- package/dist/commands/delta/print.js +1 -1
- package/dist/commands/delta/print.js.map +1 -1
- package/dist/commands/dependencies.js +3 -7
- package/dist/commands/dependencies.js.map +1 -1
- package/dist/commands/diff/diff.js +20 -27
- package/dist/commands/diff/diff.js.map +1 -1
- package/dist/commands/explore.js +4 -8
- package/dist/commands/explore.js.map +1 -1
- package/dist/commands/gate/gate.js +45 -8
- package/dist/commands/gate/gate.js.map +1 -1
- package/dist/commands/gate/print.js +15 -3
- package/dist/commands/gate/print.js.map +1 -1
- package/dist/commands/gherkin/commit.js +13 -54
- package/dist/commands/gherkin/commit.js.map +1 -1
- package/dist/commands/gherkin/gherkin.js +36 -20
- package/dist/commands/gherkin/gherkin.js.map +1 -1
- package/dist/commands/gherkin/reconcile.js +10 -13
- package/dist/commands/gherkin/reconcile.js.map +1 -1
- package/dist/commands/init/first-hour.js +44 -0
- package/dist/commands/init/first-hour.js.map +1 -1
- package/dist/commands/init/init.js +18 -22
- package/dist/commands/init/init.js.map +1 -1
- package/dist/commands/list/campaign/campaign.js +4 -6
- package/dist/commands/list/campaign/campaign.js.map +1 -1
- package/dist/commands/list/json.js +0 -4
- package/dist/commands/list/json.js.map +1 -1
- package/dist/commands/list/list.js +38 -23
- package/dist/commands/list/list.js.map +1 -1
- package/dist/commands/list/print.js +38 -6
- package/dist/commands/list/print.js.map +1 -1
- package/dist/commands/list/suites/suites.js +140 -0
- package/dist/commands/list/suites/suites.js.map +1 -0
- package/dist/commands/list/views.js +28 -33
- package/dist/commands/list/views.js.map +1 -1
- package/dist/commands/mcp/dispatch.js +91 -36
- package/dist/commands/mcp/dispatch.js.map +1 -1
- package/dist/commands/mcp/mcp.js +56 -15
- package/dist/commands/mcp/mcp.js.map +1 -1
- package/dist/commands/mcp/serve.js.map +1 -1
- package/dist/commands/migrate-openspec/materialize/active.js +27 -22
- package/dist/commands/migrate-openspec/materialize/active.js.map +1 -1
- package/dist/commands/migrate-openspec/materialize/stage.js +4 -3
- package/dist/commands/migrate-openspec/materialize/stage.js.map +1 -1
- package/dist/commands/migrate-openspec/materialize/target.js +2 -2
- package/dist/commands/migrate-openspec/materialize/target.js.map +1 -1
- package/dist/commands/migrate-openspec/openspec/decisions.js +9 -14
- package/dist/commands/migrate-openspec/openspec/decisions.js.map +1 -1
- package/dist/commands/migrate-openspec/openspec/error.js +7 -8
- package/dist/commands/migrate-openspec/openspec/error.js.map +1 -1
- package/dist/commands/migrate-openspec/openspec/mapping.js +1 -9
- package/dist/commands/migrate-openspec/openspec/mapping.js.map +1 -1
- package/dist/commands/migrate-openspec/openspec/paths.js +3 -6
- package/dist/commands/migrate-openspec/openspec/paths.js.map +1 -1
- package/dist/commands/new/capability-template.js +118 -0
- package/dist/commands/new/capability-template.js.map +1 -0
- package/dist/commands/new/commit.js +8 -22
- package/dist/commands/new/commit.js.map +1 -1
- package/dist/commands/new/modifies.js +206 -0
- package/dist/commands/new/modifies.js.map +1 -0
- package/dist/commands/new/new.js +59 -14
- package/dist/commands/new/new.js.map +1 -1
- package/dist/commands/new/templates.js +48 -124
- package/dist/commands/new/templates.js.map +1 -1
- package/dist/commands/open.js +3 -7
- package/dist/commands/open.js.map +1 -1
- package/dist/commands/policy/gate.js +27 -0
- package/dist/commands/policy/gate.js.map +1 -1
- package/dist/commands/rebase/plan.js +2 -2
- package/dist/commands/rebase/plan.js.map +1 -1
- package/dist/commands/rebase/rebase.js +60 -35
- package/dist/commands/rebase/rebase.js.map +1 -1
- package/dist/commands/seed/commit.js +21 -25
- package/dist/commands/seed/commit.js.map +1 -1
- package/dist/commands/seed/seed.js +6 -9
- package/dist/commands/seed/seed.js.map +1 -1
- package/dist/commands/show/show.js +3 -7
- package/dist/commands/show/show.js.map +1 -1
- package/dist/commands/status/print.js +13 -4
- package/dist/commands/status/print.js.map +1 -1
- package/dist/commands/status/status.js +3 -7
- package/dist/commands/status/status.js.map +1 -1
- package/dist/commands/steps/steps.js +3 -7
- package/dist/commands/steps/steps.js.map +1 -1
- package/dist/commands/subsystem/edit.js +3 -5
- package/dist/commands/subsystem/edit.js.map +1 -1
- package/dist/commands/subsystem/move.js +5 -8
- package/dist/commands/subsystem/move.js.map +1 -1
- package/dist/commands/subsystem/query.js +2 -4
- package/dist/commands/subsystem/query.js.map +1 -1
- package/dist/commands/subsystem/subsystem.js +4 -7
- package/dist/commands/subsystem/subsystem.js.map +1 -1
- package/dist/commands/subsystem/sync.js +14 -41
- package/dist/commands/subsystem/sync.js.map +1 -1
- package/dist/commands/subsystem/txn/txn.js +17 -38
- package/dist/commands/subsystem/txn/txn.js.map +1 -1
- package/dist/commands/subsystem/txn/views.js +32 -5
- package/dist/commands/subsystem/txn/views.js.map +1 -1
- package/dist/commands/tasks/render.js +116 -0
- package/dist/commands/tasks/render.js.map +1 -0
- package/dist/commands/tasks/tasks.js +113 -0
- package/dist/commands/tasks/tasks.js.map +1 -0
- package/dist/commands/unarchive/manifest.js +6 -9
- package/dist/commands/unarchive/manifest.js.map +1 -1
- package/dist/commands/unarchive/restore.js +17 -28
- package/dist/commands/unarchive/restore.js.map +1 -1
- package/dist/commands/unarchive/unarchive.js +22 -30
- package/dist/commands/unarchive/unarchive.js.map +1 -1
- package/dist/commands/validate/arch-coverage.js +6 -15
- package/dist/commands/validate/arch-coverage.js.map +1 -1
- package/dist/commands/validate/checks/fleet-shape.js +4 -49
- package/dist/commands/validate/checks/fleet-shape.js.map +1 -1
- package/dist/commands/validate/checks/requirements.js +18 -8
- package/dist/commands/validate/checks/requirements.js.map +1 -1
- package/dist/commands/validate/checks/sources.js +6 -2
- package/dist/commands/validate/checks/sources.js.map +1 -1
- package/dist/commands/validate/feature.js +23 -19
- package/dist/commands/validate/feature.js.map +1 -1
- package/dist/commands/validate/fleet/landscape.js +23 -23
- package/dist/commands/validate/fleet/landscape.js.map +1 -1
- package/dist/commands/validate/fleet/map/attest.js +6 -10
- package/dist/commands/validate/fleet/map/attest.js.map +1 -1
- package/dist/commands/validate/fleet/map/bindings.js +1 -3
- package/dist/commands/validate/fleet/map/bindings.js.map +1 -1
- package/dist/commands/validate/fleet/map/usecases.js +5 -11
- package/dist/commands/validate/fleet/map/usecases.js.map +1 -1
- package/dist/commands/validate/fleet/obligations.js +1 -5
- package/dist/commands/validate/fleet/obligations.js.map +1 -1
- package/dist/commands/validate/fleet/scorecard/print.js +5 -1
- package/dist/commands/validate/fleet/scorecard/print.js.map +1 -1
- package/dist/commands/validate/fleet/scorecard/scorecard.js +17 -11
- package/dist/commands/validate/fleet/scorecard/scorecard.js.map +1 -1
- package/dist/commands/validate/fleet/usecases/usecases.js +6 -4
- package/dist/commands/validate/fleet/usecases/usecases.js.map +1 -1
- package/dist/commands/validate/fleet/views/fleet-project.js.map +1 -1
- package/dist/commands/validate/fleet/views/root-documents.js +8 -28
- package/dist/commands/validate/fleet/views/root-documents.js.map +1 -1
- package/dist/commands/validate/links/corpus.js +1 -4
- package/dist/commands/validate/links/corpus.js.map +1 -1
- package/dist/commands/validate/links/glossary.js.map +1 -1
- package/dist/commands/validate/report.js +5 -4
- package/dist/commands/validate/report.js.map +1 -1
- package/dist/commands/validate/scope.js +113 -0
- package/dist/commands/validate/scope.js.map +1 -0
- package/dist/commands/validate/service/api.js +16 -104
- package/dist/commands/validate/service/api.js.map +1 -1
- package/dist/commands/validate/service/contracts/generated.js +134 -0
- package/dist/commands/validate/service/contracts/generated.js.map +1 -0
- package/dist/commands/validate/service/events/events.js +1 -1
- package/dist/commands/validate/service/events/events.js.map +1 -1
- package/dist/commands/validate/service/events/message-producers.js +1 -1
- package/dist/commands/validate/service/events/message-producers.js.map +1 -1
- package/dist/commands/validate/service/evidence-pins.js +56 -10
- package/dist/commands/validate/service/evidence-pins.js.map +1 -1
- package/dist/commands/validate/service/service.js +16 -26
- package/dist/commands/validate/service/service.js.map +1 -1
- package/dist/commands/validate/service/specs.js +31 -38
- package/dist/commands/validate/service/specs.js.map +1 -1
- package/dist/commands/validate/service/spine.js +8 -11
- package/dist/commands/validate/service/spine.js.map +1 -1
- package/dist/commands/validate/service/usecases/usecases.js +3 -3
- package/dist/commands/validate/service/usecases/usecases.js.map +1 -1
- package/dist/commands/validate/validate.js +44 -26
- package/dist/commands/validate/validate.js.map +1 -1
- package/dist/commands/verify/cross/diff.js +2 -6
- package/dist/commands/verify/cross/diff.js.map +1 -1
- package/dist/commands/verify/evidence/contract.js +1 -1
- package/dist/commands/verify/evidence/contract.js.map +1 -1
- package/dist/commands/verify/evidence/read.js +12 -3
- package/dist/commands/verify/evidence/read.js.map +1 -1
- package/dist/commands/verify/frozen.js +45 -23
- package/dist/commands/verify/frozen.js.map +1 -1
- package/dist/commands/verify/record.js +69 -17
- package/dist/commands/verify/record.js.map +1 -1
- package/dist/commands/verify/report.js +27 -44
- package/dist/commands/verify/report.js.map +1 -1
- package/dist/commands/verify/results.js +112 -30
- package/dist/commands/verify/results.js.map +1 -1
- package/dist/commands/verify/verify.js +54 -51
- package/dist/commands/verify/verify.js.map +1 -1
- package/dist/commands/vouch/pack/pack.js +2 -2
- package/dist/commands/vouch/pack/pack.js.map +1 -1
- package/dist/commands/vouch/pack/print.js +1 -6
- package/dist/commands/vouch/pack/print.js.map +1 -1
- package/dist/commands/vouch/pack/sections.js +1 -2
- package/dist/commands/vouch/pack/sections.js.map +1 -1
- package/dist/commands/vouch/run.js +26 -81
- package/dist/commands/vouch/run.js.map +1 -1
- package/dist/commands/vouch/sample/plan.js +20 -34
- package/dist/commands/vouch/sample/plan.js.map +1 -1
- package/dist/commands/vouch/vet/verify.js +39 -7
- package/dist/commands/vouch/vet/verify.js.map +1 -1
- package/dist/commands/vouch/vouch.js +10 -17
- package/dist/commands/vouch/vouch.js.map +1 -1
- package/dist/core/agent/agents-md/command-map.js +56 -101
- package/dist/core/agent/agents-md/command-map.js.map +1 -1
- package/dist/core/agent/agents-md/map/explain.js +4 -3
- package/dist/core/agent/agents-md/map/explain.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/lenses/diff.js +33 -20
- package/dist/core/agent/agents-md/map/lenses/diff.js.map +1 -1
- package/dist/core/agent/agents-md/map/lenses/gate.js +13 -5
- package/dist/core/agent/agents-md/map/lenses/gate.js.map +1 -1
- package/dist/core/agent/agents-md/map/mcp.js +10 -7
- package/dist/core/agent/agents-md/map/mcp.js.map +1 -1
- package/dist/core/agent/agents-md/map/status.js +9 -3
- package/dist/core/agent/agents-md/map/status.js.map +1 -1
- package/dist/core/agent/agents-md/map/subsystem.js +4 -3
- package/dist/core/agent/agents-md/map/subsystem.js.map +1 -1
- package/dist/core/agent/agents-md/map/usecases.js +24 -6
- package/dist/core/agent/agents-md/map/usecases.js.map +1 -1
- package/dist/core/agent/agents-md/refusals.js +32 -18
- package/dist/core/agent/agents-md/refusals.js.map +1 -1
- package/dist/core/agent/agents-md.js +9 -3
- package/dist/core/agent/agents-md.js.map +1 -1
- package/dist/core/agent/agents-stamp.js +10 -0
- package/dist/core/agent/agents-stamp.js.map +1 -1
- package/dist/core/agent/scaffold.js +10 -15
- package/dist/core/agent/scaffold.js.map +1 -1
- package/dist/core/agent/tools/dialects.js +1 -0
- package/dist/core/agent/tools/dialects.js.map +1 -1
- package/dist/core/agent/tools/registry.js +41 -76
- package/dist/core/agent/tools/registry.js.map +1 -1
- package/dist/core/agent/workflows/adopt.js +5 -3
- package/dist/core/agent/workflows/adopt.js.map +1 -1
- package/dist/core/agent/workflows/check.js +20 -16
- package/dist/core/agent/workflows/check.js.map +1 -1
- package/dist/core/agent/workflows/closing.js +54 -21
- package/dist/core/agent/workflows/closing.js.map +1 -1
- package/dist/core/agent/workflows/feature.js +79 -11
- package/dist/core/agent/workflows/feature.js.map +1 -1
- package/dist/core/agent/workflows/implement.js +91 -13
- package/dist/core/agent/workflows/implement.js.map +1 -1
- package/dist/core/agent/workflows/reference/authoring.js +3 -1
- package/dist/core/agent/workflows/reference/authoring.js.map +1 -1
- package/dist/core/agent/workflows/reference/codes.js +8 -6
- package/dist/core/agent/workflows/reference/codes.js.map +1 -1
- package/dist/core/agent/workflows/reference/done-check.js +37 -15
- package/dist/core/agent/workflows/reference/done-check.js.map +1 -1
- package/dist/core/agent/workflows/reference/reference.js.map +1 -1
- package/dist/core/agent/workflows/reference/spine.js +50 -18
- package/dist/core/agent/workflows/reference/spine.js.map +1 -1
- package/dist/core/asyncapi/baseline/plan.js +14 -81
- package/dist/core/asyncapi/baseline/plan.js.map +1 -1
- package/dist/core/asyncapi/digest.js +30 -25
- package/dist/core/asyncapi/digest.js.map +1 -1
- package/dist/core/asyncapi/merge/components.js +5 -4
- package/dist/core/asyncapi/merge/components.js.map +1 -1
- package/dist/core/asyncapi/merge/error.js +7 -2
- package/dist/core/asyncapi/merge/error.js.map +1 -1
- package/dist/core/asyncapi/merge/markers.js +30 -50
- package/dist/core/asyncapi/merge/markers.js.map +1 -1
- package/dist/core/asyncapi/merge/merge.js +31 -53
- package/dist/core/asyncapi/merge/merge.js.map +1 -1
- package/dist/core/asyncapi/merge/pin.js +10 -36
- package/dist/core/asyncapi/merge/pin.js.map +1 -1
- package/dist/core/asyncapi/model.js.map +1 -1
- package/dist/core/asyncapi/read.js +36 -12
- package/dist/core/asyncapi/read.js.map +1 -1
- package/dist/core/brief/brief.js +6 -18
- package/dist/core/brief/brief.js.map +1 -1
- package/dist/core/brief/landscape.js +8 -12
- package/dist/core/brief/landscape.js.map +1 -1
- package/dist/core/brief/map/extend.js +8 -5
- package/dist/core/brief/map/extend.js.map +1 -1
- package/dist/core/brief/map/owed.js +8 -26
- package/dist/core/brief/map/owed.js.map +1 -1
- package/dist/core/brief/targets.js +15 -5
- package/dist/core/brief/targets.js.map +1 -1
- package/dist/core/c4/likec4.js +147 -16
- package/dist/core/c4/likec4.js.map +1 -1
- package/dist/core/c4/locate/unlocated.js +150 -0
- package/dist/core/c4/locate/unlocated.js.map +1 -0
- package/dist/core/c4/parsed/values.js +54 -1
- package/dist/core/c4/parsed/values.js.map +1 -1
- package/dist/core/c4/prefetch/fleet.js +1 -9
- package/dist/core/c4/prefetch/fleet.js.map +1 -1
- package/dist/core/c4/project/documents.js +31 -6
- package/dist/core/c4/project/documents.js.map +1 -1
- package/dist/core/c4/project/load.js +6 -17
- package/dist/core/c4/project/load.js.map +1 -1
- package/dist/core/c4/resolve/external.js +51 -0
- package/dist/core/c4/resolve/external.js.map +1 -0
- package/dist/core/c4/resolve/service.js +25 -1
- package/dist/core/c4/resolve/service.js.map +1 -1
- package/dist/core/c4/seed/template.js +15 -1
- package/dist/core/c4/seed/template.js.map +1 -1
- package/dist/core/c4/service-model/shape.js +2 -22
- package/dist/core/c4/service-model/shape.js.map +1 -1
- package/dist/core/c4/source-mask.js +288 -4
- package/dist/core/c4/source-mask.js.map +1 -1
- package/dist/core/c4/source-scan.js +203 -147
- package/dist/core/c4/source-scan.js.map +1 -1
- package/dist/core/c4/splice/authored-source.js +19 -0
- package/dist/core/c4/splice/authored-source.js.map +1 -1
- package/dist/core/c4/splice/delta-blocks.js +5 -18
- package/dist/core/c4/splice/delta-blocks.js.map +1 -1
- package/dist/core/c4/splice/landscape-merge.js +13 -8
- package/dist/core/c4/splice/landscape-merge.js.map +1 -1
- package/dist/core/c4/splice/model/merge.js +10 -6
- package/dist/core/c4/splice/model/merge.js.map +1 -1
- package/dist/core/c4/splice/placement.js +108 -18
- package/dist/core/c4/splice/placement.js.map +1 -1
- package/dist/core/c4/workspace.js +13 -22
- package/dist/core/c4/workspace.js.map +1 -1
- package/dist/core/capabilities/capabilities.js +6 -6
- package/dist/core/capabilities/capabilities.js.map +1 -1
- package/dist/core/capabilities/delta/doc.js +7 -10
- package/dist/core/capabilities/delta/doc.js.map +1 -1
- package/dist/core/capabilities/findings.js +15 -20
- package/dist/core/capabilities/findings.js.map +1 -1
- package/dist/core/capabilities/realizes/findings.js +12 -19
- package/dist/core/capabilities/realizes/findings.js.map +1 -1
- package/dist/core/capabilities/realizes/join.js +0 -20
- package/dist/core/capabilities/realizes/join.js.map +1 -1
- package/dist/core/capabilities/rollup.js +6 -0
- package/dist/core/capabilities/rollup.js.map +1 -1
- package/dist/core/capabilities/tree.js +6 -28
- package/dist/core/capabilities/tree.js.map +1 -1
- package/dist/core/capabilities/usecase-join.js +3 -13
- package/dist/core/capabilities/usecase-join.js.map +1 -1
- package/dist/core/coherence/authoring/scaffold.js +11 -3
- package/dist/core/coherence/authoring/scaffold.js.map +1 -1
- package/dist/core/coherence/authoring/sentinels.js +2 -1
- package/dist/core/coherence/authoring/sentinels.js.map +1 -1
- package/dist/core/coherence/coherence.js +30 -16
- package/dist/core/coherence/coherence.js.map +1 -1
- package/dist/core/coherence/consumed/operations.js +70 -0
- package/dist/core/coherence/consumed/operations.js.map +1 -0
- package/dist/core/coherence/consumed/refusal.js +38 -0
- package/dist/core/coherence/consumed/refusal.js.map +1 -0
- package/dist/core/coherence/declared.js +60 -34
- package/dist/core/coherence/declared.js.map +1 -1
- package/dist/core/coherence/events/declared.js +44 -59
- package/dist/core/coherence/events/declared.js.map +1 -1
- package/dist/core/coherence/events/events.js +119 -8
- package/dist/core/coherence/events/events.js.map +1 -1
- package/dist/core/coherence/events/grades.js +31 -8
- package/dist/core/coherence/events/grades.js.map +1 -1
- package/dist/core/coherence/events/lookups.js +14 -82
- package/dist/core/coherence/events/lookups.js.map +1 -1
- package/dist/core/coherence/lookups.js +18 -78
- package/dist/core/coherence/lookups.js.map +1 -1
- package/dist/core/coherence/retired/archived.js +285 -0
- package/dist/core/coherence/retired/archived.js.map +1 -0
- package/dist/core/coherence/retired/event-pair.js +62 -0
- package/dist/core/coherence/retired/event-pair.js.map +1 -0
- package/dist/core/coherence/retired/merged.js +79 -0
- package/dist/core/coherence/retired/merged.js.map +1 -0
- package/dist/core/coherence/retired/retirement.js +49 -0
- package/dist/core/coherence/retired/retirement.js.map +1 -0
- package/dist/core/coherence/retired/revival/arrivals.js +131 -0
- package/dist/core/coherence/retired/revival/arrivals.js.map +1 -0
- package/dist/core/coherence/retired/revival/revived.js +297 -0
- package/dist/core/coherence/retired/revival/revived.js.map +1 -0
- package/dist/core/consumers/after.js +113 -0
- package/dist/core/consumers/after.js.map +1 -0
- package/dist/core/consumers/consumers.js +296 -0
- package/dist/core/consumers/consumers.js.map +1 -0
- package/dist/core/consumers/current.js +211 -0
- package/dist/core/consumers/current.js.map +1 -0
- package/dist/core/consumers/scan.js +46 -0
- package/dist/core/consumers/scan.js.map +1 -0
- package/dist/core/consumers/state.js +27 -0
- package/dist/core/consumers/state.js.map +1 -0
- package/dist/core/delta/delta.js +1 -1
- package/dist/core/delta/delta.js.map +1 -1
- package/dist/core/deployment/delta.js +3 -6
- package/dist/core/deployment/delta.js.map +1 -1
- package/dist/core/diff/base-git.js +94 -12
- package/dist/core/diff/base-git.js.map +1 -1
- package/dist/core/diff/base-state.js +28 -16
- package/dist/core/diff/base-state.js.map +1 -1
- package/dist/core/diff/joins.js +136 -0
- package/dist/core/diff/joins.js.map +1 -0
- package/dist/core/diff/scope/changed-paths.js +22 -6
- package/dist/core/diff/scope/changed-paths.js.map +1 -1
- package/dist/core/diff/scope/edges.js +82 -0
- package/dist/core/diff/scope/edges.js.map +1 -0
- package/dist/core/diff/semantic.js +100 -31
- package/dist/core/diff/semantic.js.map +1 -1
- package/dist/core/doctor/agents.js +34 -18
- package/dist/core/doctor/agents.js.map +1 -1
- package/dist/core/doctor/report.js.map +1 -1
- package/dist/core/document/apply.js +10 -2
- package/dist/core/document/apply.js.map +1 -1
- package/dist/core/document/frontmatter.js +55 -0
- package/dist/core/document/frontmatter.js.map +1 -1
- package/dist/core/document/parse.js +29 -6
- package/dist/core/document/parse.js.map +1 -1
- package/dist/core/document/scenarios.js +17 -0
- package/dist/core/document/scenarios.js.map +1 -1
- package/dist/core/document/spec.js +4 -11
- package/dist/core/document/spec.js.map +1 -1
- package/dist/core/envelope/config.js +10 -0
- package/dist/core/envelope/config.js.map +1 -1
- package/dist/core/envelope/json.js +34 -0
- package/dist/core/envelope/json.js.map +1 -1
- package/dist/core/explain/families.js +17 -12
- package/dist/core/explain/families.js.map +1 -1
- package/dist/core/explain/refusals.js +3 -3
- package/dist/core/explain/refusals.js.map +1 -1
- package/dist/core/explain/terms.js +18 -9
- package/dist/core/explain/terms.js.map +1 -1
- package/dist/core/explore/explore.js +2 -7
- package/dist/core/explore/explore.js.map +1 -1
- package/dist/core/fleet-context.js +34 -95
- package/dist/core/fleet-context.js.map +1 -1
- package/dist/core/gate/checks.js +5 -2
- package/dist/core/gate/checks.js.map +1 -1
- package/dist/core/gate/partners.js +4 -2
- package/dist/core/gate/partners.js.map +1 -1
- package/dist/core/gate/report.js.map +1 -1
- package/dist/core/gate/verification.js +75 -12
- package/dist/core/gate/verification.js.map +1 -1
- package/dist/core/gherkin/stale.js +13 -37
- package/dist/core/gherkin/stale.js.map +1 -1
- package/dist/core/glossary/tree.js +6 -19
- package/dist/core/glossary/tree.js.map +1 -1
- package/dist/core/kernel/concurrency.js +19 -0
- package/dist/core/kernel/concurrency.js.map +1 -1
- package/dist/core/kernel/records.js +11 -0
- package/dist/core/kernel/records.js.map +1 -1
- package/dist/core/links/backlinks.js +1 -3
- package/dist/core/links/backlinks.js.map +1 -1
- package/dist/core/links/corpus.js +5 -36
- package/dist/core/links/corpus.js.map +1 -1
- package/dist/core/mcp/argv.js +101 -0
- package/dist/core/mcp/argv.js.map +1 -1
- package/dist/core/mcp/author-tools.js +2 -1
- package/dist/core/mcp/author-tools.js.map +1 -1
- package/dist/core/mcp/protocol.js +2 -2
- package/dist/core/mcp/protocol.js.map +1 -1
- package/dist/core/mcp/tools.js +43 -33
- package/dist/core/mcp/tools.js.map +1 -1
- package/dist/core/openapi/baseline/gate.js +53 -12
- package/dist/core/openapi/baseline/gate.js.map +1 -1
- package/dist/core/openapi/baseline/plan.js +22 -8
- package/dist/core/openapi/baseline/plan.js.map +1 -1
- package/dist/core/openapi/baseline/record.js +0 -1
- package/dist/core/openapi/baseline/record.js.map +1 -1
- package/dist/core/openapi/contract-text.js +33 -0
- package/dist/core/openapi/contract-text.js.map +1 -0
- package/dist/core/openapi/contract-tree.js +56 -0
- package/dist/core/openapi/contract-tree.js.map +1 -0
- package/dist/core/openapi/depth.js +38 -24
- package/dist/core/openapi/depth.js.map +1 -1
- package/dist/core/openapi/digest.js +52 -2
- package/dist/core/openapi/digest.js.map +1 -1
- package/dist/core/openapi/doc.js +12 -4
- package/dist/core/openapi/doc.js.map +1 -1
- package/dist/core/openapi/merge/components.js +25 -60
- package/dist/core/openapi/merge/components.js.map +1 -1
- package/dist/core/openapi/merge/error.js +3 -13
- package/dist/core/openapi/merge/error.js.map +1 -1
- package/dist/core/openapi/merge/markers.js +26 -30
- package/dist/core/openapi/merge/markers.js.map +1 -1
- package/dist/core/openapi/merge/merge.js +26 -52
- package/dist/core/openapi/merge/merge.js.map +1 -1
- package/dist/core/openapi/merge/pin.js +15 -35
- package/dist/core/openapi/merge/pin.js.map +1 -1
- package/dist/core/openspec/decide/verdict.js.map +1 -1
- package/dist/core/openspec/inventory.js +4 -5
- package/dist/core/openspec/inventory.js.map +1 -1
- package/dist/core/openspec/model/mapping.js +30 -28
- package/dist/core/openspec/model/mapping.js.map +1 -1
- package/dist/core/openspec/model/model.js +12 -7
- package/dist/core/openspec/model/model.js.map +1 -1
- package/dist/core/openspec/scan/changes.js +10 -4
- package/dist/core/openspec/scan/changes.js.map +1 -1
- package/dist/core/openspec/scan/config.js +7 -16
- package/dist/core/openspec/scan/config.js.map +1 -1
- package/dist/core/openspec/scan/shape.js +63 -19
- package/dist/core/openspec/scan/shape.js.map +1 -1
- package/dist/core/openspec/scan/walk.js +21 -38
- package/dist/core/openspec/scan/walk.js.map +1 -1
- package/dist/core/pack/living.js +2 -16
- package/dist/core/pack/living.js.map +1 -1
- package/dist/core/provenance/findings.js +48 -12
- package/dist/core/provenance/findings.js.map +1 -1
- package/dist/core/provenance/git.js +33 -57
- package/dist/core/provenance/git.js.map +1 -1
- package/dist/core/provenance/gitq/moves.js +71 -114
- package/dist/core/provenance/gitq/moves.js.map +1 -1
- package/dist/core/provenance/gitq/vouched-ancestor.js +2 -2
- package/dist/core/provenance/gitq/vouched-ancestor.js.map +1 -1
- package/dist/core/provenance/sources.js +23 -6
- package/dist/core/provenance/sources.js.map +1 -1
- package/dist/core/provenance/stamp.js +5 -17
- package/dist/core/provenance/stamp.js.map +1 -1
- package/dist/core/repo/entries.js.map +1 -1
- package/dist/core/repo/repo.js +24 -14
- package/dist/core/repo/repo.js.map +1 -1
- package/dist/core/repo/tree/find.js +12 -3
- package/dist/core/repo/tree/find.js.map +1 -1
- package/dist/core/repo/tree/fs.js +27 -1
- package/dist/core/repo/tree/fs.js.map +1 -1
- package/dist/core/repo/tree/walk.js +8 -30
- package/dist/core/repo/tree/walk.js.map +1 -1
- package/dist/core/results.js +47 -12
- package/dist/core/results.js.map +1 -1
- package/dist/core/staging/commit.js +10 -12
- package/dist/core/staging/commit.js.map +1 -1
- package/dist/core/staging/lock.js +74 -12
- package/dist/core/staging/lock.js.map +1 -1
- package/dist/core/staging/recovery/intent.js +2 -5
- package/dist/core/staging/recovery/intent.js.map +1 -1
- package/dist/core/staging/recovery/recover.js +60 -1
- package/dist/core/staging/recovery/recover.js.map +1 -1
- package/dist/core/staging/snapshot/manifest.js +152 -0
- package/dist/core/staging/snapshot/manifest.js.map +1 -0
- package/dist/core/staging/{snapshot.js → snapshot/snapshot.js} +10 -34
- package/dist/core/staging/snapshot/snapshot.js.map +1 -0
- package/dist/core/status/actions/execution.js +11 -0
- package/dist/core/status/actions/execution.js.map +1 -1
- package/dist/core/status/feature/artifacts.js +2 -2
- package/dist/core/status/feature/artifacts.js.map +1 -1
- package/dist/core/status/feature/feature.js +5 -11
- package/dist/core/status/feature/feature.js.map +1 -1
- package/dist/core/status/fleet/fleet.js +6 -2
- package/dist/core/status/fleet/fleet.js.map +1 -1
- package/dist/core/status/fleet/next.js +26 -3
- package/dist/core/status/fleet/next.js.map +1 -1
- package/dist/core/status/report.js.map +1 -1
- package/dist/core/status/verification.js +30 -15
- package/dist/core/status/verification.js.map +1 -1
- package/dist/core/tasks/packet.js +174 -0
- package/dist/core/tasks/packet.js.map +1 -0
- package/dist/core/tasks/record.js +43 -0
- package/dist/core/tasks/record.js.map +1 -0
- package/dist/core/tasks/sources.js +181 -0
- package/dist/core/tasks/sources.js.map +1 -0
- package/dist/core/tasks/stamps.js +76 -0
- package/dist/core/tasks/stamps.js.map +1 -0
- package/dist/core/tasks/unit.js +0 -0
- package/dist/core/tasks/unit.js.map +1 -0
- package/dist/core/usecases/capability.js +11 -14
- package/dist/core/usecases/capability.js.map +1 -1
- package/dist/core/usecases/delta/flows.js +5 -7
- package/dist/core/usecases/delta/flows.js.map +1 -1
- package/dist/core/usecases/fleet.js +178 -31
- package/dist/core/usecases/fleet.js.map +1 -1
- package/dist/core/usecases/operations.js +5 -4
- package/dist/core/usecases/operations.js.map +1 -1
- package/dist/core/usecases/service/flows.js +9 -6
- package/dist/core/usecases/service/flows.js.map +1 -1
- package/dist/core/usecases/suites/participants.js +83 -0
- package/dist/core/usecases/suites/participants.js.map +1 -0
- package/dist/core/usecases/suites/suites.js +45 -0
- package/dist/core/usecases/suites/suites.js.map +1 -0
- package/dist/core/verify/answers.js +12 -5
- package/dist/core/verify/answers.js.map +1 -1
- package/dist/core/verify/build.js +102 -82
- package/dist/core/verify/build.js.map +1 -1
- package/dist/core/verify/checklist.js +3 -0
- package/dist/core/verify/checklist.js.map +1 -1
- package/dist/core/verify/location.js +67 -0
- package/dist/core/verify/location.js.map +1 -0
- package/dist/core/verify/pins/lint.js +3 -2
- package/dist/core/verify/pins/lint.js.map +1 -1
- package/dist/core/verify/pins/pin.js +1 -1
- package/dist/core/verify/pins/pin.js.map +1 -1
- package/dist/core/verify/read/file.js +224 -0
- package/dist/core/verify/read/file.js.map +1 -0
- package/dist/core/verify/{file.js → read/shape.js} +16 -104
- package/dist/core/verify/read/shape.js.map +1 -0
- package/dist/core/verify/read/union.js +105 -0
- package/dist/core/verify/read/union.js.map +1 -0
- package/dist/core/verify/record.js +35 -12
- package/dist/core/verify/record.js.map +1 -1
- package/dist/core/verify/store/commit.js +40 -30
- package/dist/core/verify/store/commit.js.map +1 -1
- package/dist/core/verify/store/render.js +8 -4
- package/dist/core/verify/store/render.js.map +1 -1
- package/dist/core/vocabulary/issue.js +1 -0
- package/dist/core/vocabulary/issue.js.map +1 -1
- package/dist/core/vocabulary/maturity.js +12 -2
- package/dist/core/vocabulary/maturity.js.map +1 -1
- package/examples/README.md +21 -6
- package/examples/docs/architecture/landscape.likec4 +8 -0
- package/examples/docs/architecture/usecases/order-notification.likec4 +6 -2
- package/examples/docs/features/FEAT-101-payment-splitting/specs/payment-service/asyncapi.yaml +11 -8
- package/examples/docs/features/FEAT-101-payment-splitting/specs/payment-split-service/openapi.yaml +6 -6
- package/examples/docs/features/archive/FEAT-088-refunds/specs/payment-service/openapi.yaml +13 -10
- package/examples/docs/services/order-service/asyncapi.yaml +3 -2
- package/examples/docs/services/platform/notification-service/asyncapi.yaml +2 -1
- package/package.json +1 -1
- package/dist/commands/validate/checks/vocabulary.js +0 -16
- package/dist/commands/validate/checks/vocabulary.js.map +0 -1
- package/dist/core/agent/agents-md/artifacts.js +0 -221
- package/dist/core/agent/agents-md/artifacts.js.map +0 -1
- package/dist/core/agent/agents-md/cycle.js +0 -180
- package/dist/core/agent/agents-md/cycle.js.map +0 -1
- package/dist/core/agent/agents-md/shipped/archive-gate.js +0 -98
- package/dist/core/agent/agents-md/shipped/archive-gate.js.map +0 -1
- package/dist/core/agent/agents-md/spine.js +0 -40
- package/dist/core/agent/agents-md/spine.js.map +0 -1
- package/dist/core/coherence/events/merged.js +0 -40
- package/dist/core/coherence/events/merged.js.map +0 -1
- package/dist/core/diff/current-state.js +0 -96
- package/dist/core/diff/current-state.js.map +0 -1
- package/dist/core/diff/victims.js +0 -256
- package/dist/core/diff/victims.js.map +0 -1
- package/dist/core/staging/snapshot.js.map +0 -1
- package/dist/core/verify/file.js.map +0 -1
|
@@ -106,40 +106,24 @@ import { LIKEC4_ROOT_PROJECT, rootProjectPath, serviceRenderPaths } from "../../
|
|
|
106
106
|
import { listFleetTree } from "../../core/repo/repo.js";
|
|
107
107
|
import { renderServiceProject, surveyProjects, } from "../../core/repo/tree/render/projects.js";
|
|
108
108
|
import { stageWrites } from "../../core/staging/commit.js";
|
|
109
|
-
import {
|
|
110
|
-
import {
|
|
111
|
-
import { recoverInterruptedCommit } from "../../core/staging/recovery/recover.js";
|
|
109
|
+
import { LOCK_WAIT_MS } from "../../core/staging/lock.js";
|
|
110
|
+
import { openCommitWindow } from "../../core/staging/recovery/recover.js";
|
|
112
111
|
import { commitStaged } from "../../core/staging/txn/transaction.js";
|
|
113
112
|
import { planWrite } from "../../core/staging/writes.js";
|
|
114
113
|
import { readGeneratedFile } from "../../core/repo/tree/render/service/header.js";
|
|
115
|
-
import { viewsAgree } from "../../core/repo/tree/render/views.js";
|
|
116
114
|
import { NO_EXCLUDE, planExclude } from "./txn/exclude.js";
|
|
117
115
|
import { reportSync } from "./txn/report.js";
|
|
118
|
-
import { expectedViews } from "./txn/views.js";
|
|
116
|
+
import { expectedViews, planGenerated } from "./txn/views.js";
|
|
119
117
|
export async function runSync(docsDir, json) {
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
if (!(err instanceof DocsBusyError))
|
|
126
|
-
throw err;
|
|
127
|
-
fail(json, "docs-busy", err.message);
|
|
118
|
+
// A predecessor's journal is recovered (or refused) before the tree is
|
|
119
|
+
// read: the views file itself may be the half-written one.
|
|
120
|
+
const opened = await openCommitWindow(docsDir, LOCK_WAIT_MS);
|
|
121
|
+
if (!opened.ok) {
|
|
122
|
+
fail(json, opened.code, opened.message);
|
|
128
123
|
return;
|
|
129
124
|
}
|
|
125
|
+
const { recovered } = opened;
|
|
130
126
|
try {
|
|
131
|
-
// A predecessor's journal is recovered (or refused) before the tree is
|
|
132
|
-
// read: the views file itself may be the half-written one.
|
|
133
|
-
let recovered;
|
|
134
|
-
try {
|
|
135
|
-
recovered = await recoverInterruptedCommit(docsDir);
|
|
136
|
-
}
|
|
137
|
-
catch (err) {
|
|
138
|
-
if (!(err instanceof InterruptedCommitError))
|
|
139
|
-
throw err;
|
|
140
|
-
fail(json, "commit-interrupted", err.message);
|
|
141
|
-
return;
|
|
142
|
-
}
|
|
143
127
|
const tree = await listFleetTree(docsDir);
|
|
144
128
|
// BOTH generated files, from ONE read of the map — `./txn/views.ts` says why
|
|
145
129
|
// the pair is one answer and why the parsed document, never a literal, is
|
|
@@ -237,7 +221,7 @@ export async function runSync(docsDir, json) {
|
|
|
237
221
|
say();
|
|
238
222
|
}
|
|
239
223
|
finally {
|
|
240
|
-
await
|
|
224
|
+
await opened.release();
|
|
241
225
|
}
|
|
242
226
|
}
|
|
243
227
|
/**
|
|
@@ -246,14 +230,9 @@ export async function runSync(docsDir, json) {
|
|
|
246
230
|
*
|
|
247
231
|
* ONE function for both generated files, because the rule is one rule and a
|
|
248
232
|
* 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
|
-
*
|
|
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.
|
|
233
|
+
* `services.likec4` to answer differently about the same map. Past the two
|
|
234
|
+
* vetoes below, the compare is `planGenerated` (`./txn/views.ts`), the one the
|
|
235
|
+
* tree-changing verbs make too, which says why it is over CONTENT, not bytes.
|
|
257
236
|
*
|
|
258
237
|
* `blocked` is the map's veto and it is the whole reason `known` is threaded
|
|
259
238
|
* this far. The render cannot be trusted against a file that already exists when
|
|
@@ -294,13 +273,7 @@ async function planFile(file, known) {
|
|
|
294
273
|
return { action: "refused", write: null };
|
|
295
274
|
if (!known && actual !== null)
|
|
296
275
|
return { action: "blocked", write: null };
|
|
297
|
-
|
|
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
|
-
};
|
|
276
|
+
return planGenerated(file, actual);
|
|
304
277
|
}
|
|
305
278
|
/**
|
|
306
279
|
* What the project half of this run owes, from ONE predicate. `surveyProjects`
|
|
@@ -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. 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"]}
|
|
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 { LOCK_WAIT_MS } from \"../../core/staging/lock.js\";\nimport { openCommitWindow } 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 { NO_EXCLUDE, planExclude } from \"./txn/exclude.js\";\nimport { type ProjectsReport, reportSync, type SyncAction } from \"./txn/report.js\";\nimport { type ExpectedFile, expectedViews, planGenerated } 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 // 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 const opened = await openCommitWindow(docsDir, LOCK_WAIT_MS);\n if (!opened.ok) {\n fail(json, opened.code, opened.message);\n return;\n }\n const { recovered } = opened;\n try {\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 opened.release();\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. Past the two\n * vetoes below, the compare is `planGenerated` (`./txn/views.ts`), the one the\n * tree-changing verbs make too, which says why it is over CONTENT, not bytes.\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 return planGenerated(file, actual);\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"]}
|
|
@@ -44,14 +44,12 @@ import { readFile } from "node:fs/promises";
|
|
|
44
44
|
import { fail, repoPath } from "../../../core/envelope/json.js";
|
|
45
45
|
import { listFleetTree } from "../../../core/repo/repo.js";
|
|
46
46
|
import { stageWrites } from "../../../core/staging/commit.js";
|
|
47
|
-
import {
|
|
48
|
-
import {
|
|
49
|
-
import { recoverInterruptedCommit } from "../../../core/staging/recovery/recover.js";
|
|
47
|
+
import {} from "../../../core/staging/interrupted.js";
|
|
48
|
+
import { openCommitWindow } from "../../../core/staging/recovery/recover.js";
|
|
50
49
|
import { commitStaged } from "../../../core/staging/txn/transaction.js";
|
|
51
50
|
import {} from "../../../core/staging/writes.js";
|
|
52
51
|
import { authoredGeneratedPaths, authoredRefusal } from "../../../core/repo/tree/render/service/header.js";
|
|
53
|
-
import {
|
|
54
|
-
import { expectedViews, LANDSCAPE_UNREADABLE_NOTE } from "./views.js";
|
|
52
|
+
import { expectedViews, LANDSCAPE_UNREADABLE_NOTE, planGenerated } from "./views.js";
|
|
55
53
|
/** The text tail a verb's success line carries about the generated files — one clause per file that moved. */
|
|
56
54
|
export function reportViews(views) {
|
|
57
55
|
return ([
|
|
@@ -71,27 +69,15 @@ export function reportViews(views) {
|
|
|
71
69
|
* staged and nothing is written.
|
|
72
70
|
*/
|
|
73
71
|
export async function commitWindow(docsDir, json, plan) {
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
if (!(err instanceof DocsBusyError))
|
|
80
|
-
throw err;
|
|
81
|
-
fail(json, "docs-busy", err.message);
|
|
72
|
+
// Refuse-fast (0), as archive takes the lock: two concurrent verbs answer
|
|
73
|
+
// `docs-busy` rather than queue behind each other.
|
|
74
|
+
const opened = await openCommitWindow(docsDir, 0);
|
|
75
|
+
if (!opened.ok) {
|
|
76
|
+
fail(json, opened.code, opened.message);
|
|
82
77
|
return;
|
|
83
78
|
}
|
|
79
|
+
const { recovered } = opened;
|
|
84
80
|
try {
|
|
85
|
-
let recovered;
|
|
86
|
-
try {
|
|
87
|
-
recovered = await recoverInterruptedCommit(docsDir);
|
|
88
|
-
}
|
|
89
|
-
catch (err) {
|
|
90
|
-
if (!(err instanceof InterruptedCommitError))
|
|
91
|
-
throw err;
|
|
92
|
-
fail(json, "commit-interrupted", err.message);
|
|
93
|
-
return;
|
|
94
|
-
}
|
|
95
81
|
const txn = await plan(await listFleetTree(docsDir));
|
|
96
82
|
if (txn === null)
|
|
97
83
|
return;
|
|
@@ -185,19 +171,14 @@ export async function commitWindow(docsDir, json, plan) {
|
|
|
185
171
|
console.log(LANDSCAPE_UNREADABLE_NOTE);
|
|
186
172
|
}
|
|
187
173
|
finally {
|
|
188
|
-
await
|
|
174
|
+
await opened.release();
|
|
189
175
|
}
|
|
190
176
|
}
|
|
191
177
|
/**
|
|
192
178
|
* Compare ONE generated file against what it must say, append the write when
|
|
193
|
-
* they differ, and name what happened.
|
|
194
|
-
*
|
|
195
|
-
*
|
|
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.
|
|
179
|
+
* they differ, and name what happened. The compare is `planGenerated`'s, the
|
|
180
|
+
* one `../sync.ts` makes too — `./views.ts` says why it is over CONTENT, not
|
|
181
|
+
* bytes, and why there is one spelling of it.
|
|
201
182
|
*
|
|
202
183
|
* `protect` is the caller's statement that a NULL render is not evidence: the
|
|
203
184
|
* file is left exactly as it is rather than deleted. It is never a licence to
|
|
@@ -206,12 +187,10 @@ export async function commitWindow(docsDir, json, plan) {
|
|
|
206
187
|
*/
|
|
207
188
|
async function stageGenerated(file, writes, protect) {
|
|
208
189
|
const current = existsSync(file.path) ? await readFile(file.path, "utf8") : null;
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
if (protect && file.content === null)
|
|
190
|
+
const planned = planGenerated(file, current);
|
|
191
|
+
if (planned.write === null || (protect && file.content === null))
|
|
212
192
|
return "current";
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
return current === null ? "created" : bytes === null ? "removed" : "updated";
|
|
193
|
+
writes.push(planned.write);
|
|
194
|
+
return planned.action;
|
|
216
195
|
}
|
|
217
196
|
//# sourceMappingURL=txn.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"txn.js","sourceRoot":"","sources":["../../../../src/commands/subsystem/txn/txn.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * The shared commit window every subsystem WRITE verb goes through — `new`,\n * `rm`, `move` and `rename` all land here: lock, recover a predecessor's\n * journal, plan against the walked tree, regenerate BOTH generated views files\n * from the PLANNED post-state, commit through the journaled transaction\n * (directory renames included), report. One window, because those files must\n * land in the same commit as whatever changed the tree — a marker without its\n * view, or renames without theirs, is exactly the `subsystem.views-stale` state\n * the generated files' contract refuses to leave between two commits. The\n * per-service file is in that rule and not beside it: its titles are\n * `<subsystem path> / <service id>`, so the placement a verb changes is written\n * into it too.\n *\n * This window does NOT write the per-service project files\n * (`services/<…>/<id>/likec4.config.json`); only `../sync.ts` does, and the\n * asymmetry is deliberate rather than an omission. The views file must ride\n * in the verb's commit because its bytes depend on the PLACEMENT the verb is\n * changing. A project file's bytes depend on the service id alone, which no\n * verb here changes — and the file lives INSIDE the service directory, so a\n * `move` carries it to the new location as part of the rename it already\n * journals (in a git-backed repo that move REFUSES first, `move-uncommitted`,\n * until the files `sync` created are committed — the refusal names them, and\n * `sync`'s own output says to commit them), and `rm` never reaches one,\n * because it refuses any subsystem that still holds a member\n * (`subsystem-not-empty`, `../edit.ts`). No verb can\n * therefore leave a project file stale or orphaned, and a verb that filled\n * gaps in passing would silently write files a `--json` reader of `new` or\n * `rename` was never told to expect. The one state a verb can leave is a\n * model with no file beside it, and that is the state `validate --all` names\n * as `service.likec4-config-missing`, with `loam subsystem sync` as the\n * repair — one writer, so the create-only rule has one place to hold.\n *\n * NEITHER generated file is written over a document loam did not write, and\n * that guard is checked HERE as well as in `../sync.ts` — see the comment at\n * the check. Both are filenames loam RESERVES inside a directory the team\n * owns, both are rewritten whole, and until the check below existed all four\n * verbs took an authored file at either path without a word. Unlike sync,\n * which reports the one file `refused` and completes the rest, a verb refuses\n * WHOLE: the file has to land in this commit, so half of this commit is not a\n * lesser version of it.\n */\nimport { existsSync } from \"node:fs\";\nimport { readFile } from \"node:fs/promises\";\nimport type { DocsDir } from \"../../../core/kernel/ids/dirs.js\";\nimport { fail, repoPath, type ErrorCode } from \"../../../core/envelope/json.js\";\nimport { listFleetTree } from \"../../../core/repo/repo.js\";\nimport type { FleetTree } from \"../../../core/repo/tree/walk.js\";\nimport { stageWrites } from \"../../../core/staging/commit.js\";\nimport { type CommitRecovery, InterruptedCommitError } from \"../../../core/staging/interrupted.js\";\nimport { acquireDocsLock, DocsBusyError } from \"../../../core/staging/lock.js\";\nimport { recoverInterruptedCommit } from \"../../../core/staging/recovery/recover.js\";\nimport { commitStaged } from \"../../../core/staging/txn/transaction.js\";\nimport { type PlannedWrite } from \"../../../core/staging/writes.js\";\nimport { authoredGeneratedPaths, authoredRefusal } from \"../../../core/repo/tree/render/service/header.js\";\nimport { viewsAgree } from \"../../../core/repo/tree/render/views.js\";\nimport { type ExpectedFile, expectedViews, LANDSCAPE_UNREADABLE_NOTE } from \"./views.js\";\n\nexport type ViewsAction = \"created\" | \"updated\" | \"removed\" | \"current\";\n\n/**\n * What one commit did to EACH generated file, kept apart.\n *\n * One action per file rather than one folded answer, for the reason\n * `./views.ts` gives about the expected bytes: the two files change for\n * different reasons and are named separately everywhere a person or a `--json`\n * reader meets them. The `views` key a verb has always emitted still carries\n * `subsystems` and nothing else — a payload key's meaning is frozen, so the\n * second file arrives as a key of its own beside it.\n */\nexport interface ViewsActions {\n /** `architecture/subsystems.likec4` — the verb's `--json` `views` key, unchanged. */\n subsystems: ViewsAction;\n /** `architecture/services.likec4` — the per-service overviews, one per service the map draws. */\n services: ViewsAction;\n}\n\n/** The text tail a verb's success line carries about the generated files — one clause per file that moved. */\nexport function reportViews(views: ViewsActions): string {\n return (\n [\n [\"architecture/subsystems.likec4\", views.subsystems] as const,\n [\"architecture/services.likec4\", views.services] as const,\n ]\n // `current` says nothing: the tail hangs off a success line, and a verb\n // that renamed one directory must not print two clauses about files that\n // did not move.\n .filter(([, action]) => action !== \"current\")\n .map(([file, action]) => ` — ${file} ${action}`)\n .join(\"\")\n );\n}\n\n/** What one subsystem verb asks the shared commit window to land. */\nexport interface SubsystemTxn {\n /** The journal's `target` — what this commit is FOR. */\n target: string;\n /** Marker writes (create/delete); the views write is derived and appended. */\n writes: PlannedWrite[];\n /** The tree AS THE COMMIT LEAVES IT — the views file is rendered from this. */\n tree: FleetTree;\n /** Directory renames, performed after the swaps, in this order. */\n moves?: { from: string; to: string }[];\n /** Remap of a RACED commit failure (new: a lost marker create is `already-exists`). */\n racedCode?: ErrorCode;\n /** Remap of a rolled-back commit failure (move: `move-failed` — no merge was computed). */\n failedCode?: ErrorCode;\n /** The rollback sentence's verb for half-state files. */\n what: string;\n report: (views: ViewsActions, recovered: CommitRecovery | null) => void | Promise<void>;\n}\n\n/**\n * Lock → recover → plan → stage (views included) → journaled commit → report.\n * `plan` returning null means it already reported a refusal; nothing was\n * staged and nothing is written.\n */\nexport async function commitWindow(\n docsDir: DocsDir,\n json: boolean,\n plan: (tree: FleetTree) => Promise<SubsystemTxn | null>,\n): Promise<void> {\n let releaseLock: () => Promise<void>;\n try {\n releaseLock = await acquireDocsLock(docsDir);\n } catch (err) {\n if (!(err instanceof DocsBusyError)) throw err;\n fail(json, \"docs-busy\", err.message);\n return;\n }\n try {\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 const txn = await plan(await listFleetTree(docsDir));\n if (txn === null) return;\n\n const expected = await expectedViews(docsDir, txn.tree);\n // OWNERSHIP BEFORE ANYTHING IS STAGED, and the WHOLE verb is refused.\n //\n // The guard first shipped in `../sync.ts` alone, and this window is the\n // other writer of exactly the same two reserved paths — for `subsystem\n // new`, `rm`, `move` and `rename` at once. Reproduced before this call\n // existed: with a hand-authored `architecture/services.likec4` in the tree,\n // `loam subsystem new platform` printed \"architecture/services.likec4\n // updated\" and destroyed it, first line and all. One predicate, one\n // sentence, shared from beneath every writer\n // (`core/repo/tree/render/service/header.ts`).\n //\n // WHY THE WHOLE OPERATION AND NOT JUST THE FILE. `sync` can decline one file\n // and still do the rest, because nothing else it writes depends on that file\n // and every part of it is idempotent. This window cannot: its entire reason\n // for existing is that the generated files land in the SAME commit as the\n // change to the tree, so completing the structural half and reporting the\n // file as refused would MANUFACTURE the `subsystem.views-stale` state the\n // banner above says this window refuses to leave between two commits — and\n // it would do so irreversibly, since `move` and `rename` also stage their\n // renames in git. A half-done move nobody asked for is worse than a refusal\n // a person can act on, and a refusal is re-runnable the moment the authored\n // file is out of the reserved path.\n //\n // It runs AFTER `plan`, which writes nothing and has already answered the\n // verb-specific refusals (`unknown-target`, `subsystem-not-empty`,\n // `move-uncommitted`): a caller whose name is wrong AND whose tree holds an\n // authored views file is better told about the name.\n const authored = await authoredGeneratedPaths([expected.subsystems.path, expected.services.path]);\n if (authored.length > 0) {\n fail(\n json,\n // The code is `already-exists` and not a new one because it is exactly\n // what that code has always meant here — \"refusing to create over\n // something that already exists, because overwriting would destroy work\n // nobody asked to lose\" — and it is already this command's documented\n // refusal for the flat namespace. A second code for the same sentence\n // would be a second branch an agent has to learn for no new decision.\n \"already-exists\",\n authored.map((path) => authoredRefusal(repoPath(docsDir, path))).join(\" \") +\n \" NOTHING was written and nothing was renamed: this command lands the generated views file in the same \" +\n \"commit as the change to the tree, so it refuses whole rather than leaving the tree changed — and, for \" +\n \"`move` and `rename`, renames staged in git — with a views file it may not write.\",\n );\n return;\n }\n const writes = [...txn.writes];\n // BOTH generated files, in the ONE commit that changes the tree. The\n // per-service file is not a bystander here: its view titles are\n // `<subsystem path> / <service id>`, so every `move` and `rename` restales\n // it exactly as it restales the subsystem views, and leaving it for a later\n // `sync` is the failing-between-two-commits state this window exists to\n // refuse.\n const views: ViewsActions = {\n subsystems: await stageGenerated(expected.subsystems, writes, false),\n // PROTECTED when the map did not parse as a project, and only this file\n // is: `renderSubsystemViews` answers null off the TREE, which is readable\n // either way, while `renderServiceOverviews` answers null whenever no\n // service resolves to an element — which is what an unreadable map looks\n // like from here. Under `known: false` a null render is therefore not\n // evidence of absence, and writing it would delete the whole file over a\n // sibling document that does not parse. A verb still rewrites a file it\n // CAN render (the tolerant arm this window has always taken), and the\n // person is told by the note at the foot of this function.\n services: await stageGenerated(expected.services, writes, !expected.known),\n };\n const staged = await stageWrites(writes);\n const committed = await commitStaged(\n {\n root: docsDir,\n command: \"subsystem\",\n rerun: \"loam subsystem sync\",\n target: txn.target,\n ...(txn.moves === undefined ? {} : { moves: txn.moves }),\n },\n staged,\n txn.what,\n );\n if (!committed.ok) {\n const code =\n committed.code === \"rollback-incomplete\"\n ? committed.code\n : committed.raced\n ? (txn.racedCode ?? txn.failedCode ?? committed.code)\n : (txn.failedCode ?? committed.code);\n fail(json, code, committed.message);\n return;\n }\n await txn.report(views, recovered);\n // A verb CANNOT leave this file behind the tree — its bytes depend on the\n // placement the verb just changed, so the tolerant render lands even when the\n // map is unreadable and the file loses its includes. `sync` may decline that\n // rewrite (`action: \"blocked\"`); a verb may not, so the person is told\n // instead, and the same note names the run that puts the includes back.\n if (!json && !expected.known) console.log(LANDSCAPE_UNREADABLE_NOTE);\n } finally {\n await releaseLock();\n }\n}\n\n/**\n * Compare ONE generated file against what it must say, append the write when\n * they differ, and name what happened.\n *\n * The compare is over CONTENT, not bytes — `viewsAgree` carries the reason,\n * beside the generator that mints them: an ordinary Windows clone hands a\n * generated file back with CRLF and not one fact in it changed, and without\n * this every subsystem move on such a clone folded a rewrite that changed\n * nothing into its transaction and journalled it. What gets WRITTEN is still\n * the LF render as a Buffer, so the decode here is a comparison only.\n *\n * `protect` is the caller's statement that a NULL render is not evidence: the\n * file is left exactly as it is rather than deleted. It is never a licence to\n * skip a real rewrite — a file loam can render is rendered, which is the\n * difference between this window and `../sync.ts`'s `blocked`.\n */\nasync function stageGenerated(file: ExpectedFile, writes: PlannedWrite[], protect: boolean): Promise<ViewsAction> {\n const current = existsSync(file.path) ? await readFile(file.path, \"utf8\") : null;\n if (viewsAgree(current, file.content)) return \"current\";\n if (protect && file.content === null) return \"current\";\n const bytes = file.content === null ? null : Buffer.from(file.content, \"utf8\");\n writes.push({ path: file.path, content: bytes });\n return current === null ? \"created\" : bytes === null ? \"removed\" : \"updated\";\n}\n"]}
|
|
1
|
+
{"version":3,"file":"txn.js","sourceRoot":"","sources":["../../../../src/commands/subsystem/txn/txn.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * The shared commit window every subsystem WRITE verb goes through — `new`,\n * `rm`, `move` and `rename` all land here: lock, recover a predecessor's\n * journal, plan against the walked tree, regenerate BOTH generated views files\n * from the PLANNED post-state, commit through the journaled transaction\n * (directory renames included), report. One window, because those files must\n * land in the same commit as whatever changed the tree — a marker without its\n * view, or renames without theirs, is exactly the `subsystem.views-stale` state\n * the generated files' contract refuses to leave between two commits. The\n * per-service file is in that rule and not beside it: its titles are\n * `<subsystem path> / <service id>`, so the placement a verb changes is written\n * into it too.\n *\n * This window does NOT write the per-service project files\n * (`services/<…>/<id>/likec4.config.json`); only `../sync.ts` does, and the\n * asymmetry is deliberate rather than an omission. The views file must ride\n * in the verb's commit because its bytes depend on the PLACEMENT the verb is\n * changing. A project file's bytes depend on the service id alone, which no\n * verb here changes — and the file lives INSIDE the service directory, so a\n * `move` carries it to the new location as part of the rename it already\n * journals (in a git-backed repo that move REFUSES first, `move-uncommitted`,\n * until the files `sync` created are committed — the refusal names them, and\n * `sync`'s own output says to commit them), and `rm` never reaches one,\n * because it refuses any subsystem that still holds a member\n * (`subsystem-not-empty`, `../edit.ts`). No verb can\n * therefore leave a project file stale or orphaned, and a verb that filled\n * gaps in passing would silently write files a `--json` reader of `new` or\n * `rename` was never told to expect. The one state a verb can leave is a\n * model with no file beside it, and that is the state `validate --all` names\n * as `service.likec4-config-missing`, with `loam subsystem sync` as the\n * repair — one writer, so the create-only rule has one place to hold.\n *\n * NEITHER generated file is written over a document loam did not write, and\n * that guard is checked HERE as well as in `../sync.ts` — see the comment at\n * the check. Both are filenames loam RESERVES inside a directory the team\n * owns, both are rewritten whole, and until the check below existed all four\n * verbs took an authored file at either path without a word. Unlike sync,\n * which reports the one file `refused` and completes the rest, a verb refuses\n * WHOLE: the file has to land in this commit, so half of this commit is not a\n * lesser version of it.\n */\nimport { existsSync } from \"node:fs\";\nimport { readFile } from \"node:fs/promises\";\nimport type { DocsDir } from \"../../../core/kernel/ids/dirs.js\";\nimport { fail, repoPath, type ErrorCode } from \"../../../core/envelope/json.js\";\nimport { listFleetTree } from \"../../../core/repo/repo.js\";\nimport type { FleetTree } from \"../../../core/repo/tree/walk.js\";\nimport { stageWrites } from \"../../../core/staging/commit.js\";\nimport { type CommitRecovery } from \"../../../core/staging/interrupted.js\";\nimport { openCommitWindow } from \"../../../core/staging/recovery/recover.js\";\nimport { commitStaged } from \"../../../core/staging/txn/transaction.js\";\nimport { type PlannedWrite } from \"../../../core/staging/writes.js\";\nimport { authoredGeneratedPaths, authoredRefusal } from \"../../../core/repo/tree/render/service/header.js\";\nimport { type ExpectedFile, expectedViews, LANDSCAPE_UNREADABLE_NOTE, planGenerated, type ViewsAction } from \"./views.js\";\n\n/**\n * What one commit did to EACH generated file, kept apart.\n *\n * One action per file rather than one folded answer, for the reason\n * `./views.ts` gives about the expected bytes: the two files change for\n * different reasons and are named separately everywhere a person or a `--json`\n * reader meets them. The `views` key a verb has always emitted still carries\n * `subsystems` and nothing else — a payload key's meaning is frozen, so the\n * second file arrives as a key of its own beside it.\n */\nexport interface ViewsActions {\n /** `architecture/subsystems.likec4` — the verb's `--json` `views` key, unchanged. */\n subsystems: ViewsAction;\n /** `architecture/services.likec4` — the per-service overviews, one per service the map draws. */\n services: ViewsAction;\n}\n\n/** The text tail a verb's success line carries about the generated files — one clause per file that moved. */\nexport function reportViews(views: ViewsActions): string {\n return (\n [\n [\"architecture/subsystems.likec4\", views.subsystems] as const,\n [\"architecture/services.likec4\", views.services] as const,\n ]\n // `current` says nothing: the tail hangs off a success line, and a verb\n // that renamed one directory must not print two clauses about files that\n // did not move.\n .filter(([, action]) => action !== \"current\")\n .map(([file, action]) => ` — ${file} ${action}`)\n .join(\"\")\n );\n}\n\n/** What one subsystem verb asks the shared commit window to land. */\nexport interface SubsystemTxn {\n /** The journal's `target` — what this commit is FOR. */\n target: string;\n /** Marker writes (create/delete); the views write is derived and appended. */\n writes: PlannedWrite[];\n /** The tree AS THE COMMIT LEAVES IT — the views file is rendered from this. */\n tree: FleetTree;\n /** Directory renames, performed after the swaps, in this order. */\n moves?: { from: string; to: string }[];\n /** Remap of a RACED commit failure (new: a lost marker create is `already-exists`). */\n racedCode?: ErrorCode;\n /** Remap of a rolled-back commit failure (move: `move-failed` — no merge was computed). */\n failedCode?: ErrorCode;\n /** The rollback sentence's verb for half-state files. */\n what: string;\n report: (views: ViewsActions, recovered: CommitRecovery | null) => void | Promise<void>;\n}\n\n/**\n * Lock → recover → plan → stage (views included) → journaled commit → report.\n * `plan` returning null means it already reported a refusal; nothing was\n * staged and nothing is written.\n */\nexport async function commitWindow(\n docsDir: DocsDir,\n json: boolean,\n plan: (tree: FleetTree) => Promise<SubsystemTxn | null>,\n): Promise<void> {\n // Refuse-fast (0), as archive takes the lock: two concurrent verbs answer\n // `docs-busy` rather than queue behind each other.\n const opened = await openCommitWindow(docsDir, 0);\n if (!opened.ok) {\n fail(json, opened.code, opened.message);\n return;\n }\n const { recovered } = opened;\n try {\n const txn = await plan(await listFleetTree(docsDir));\n if (txn === null) return;\n\n const expected = await expectedViews(docsDir, txn.tree);\n // OWNERSHIP BEFORE ANYTHING IS STAGED, and the WHOLE verb is refused.\n //\n // The guard first shipped in `../sync.ts` alone, and this window is the\n // other writer of exactly the same two reserved paths — for `subsystem\n // new`, `rm`, `move` and `rename` at once. Reproduced before this call\n // existed: with a hand-authored `architecture/services.likec4` in the tree,\n // `loam subsystem new platform` printed \"architecture/services.likec4\n // updated\" and destroyed it, first line and all. One predicate, one\n // sentence, shared from beneath every writer\n // (`core/repo/tree/render/service/header.ts`).\n //\n // WHY THE WHOLE OPERATION AND NOT JUST THE FILE. `sync` can decline one file\n // and still do the rest, because nothing else it writes depends on that file\n // and every part of it is idempotent. This window cannot: its entire reason\n // for existing is that the generated files land in the SAME commit as the\n // change to the tree, so completing the structural half and reporting the\n // file as refused would MANUFACTURE the `subsystem.views-stale` state the\n // banner above says this window refuses to leave between two commits — and\n // it would do so irreversibly, since `move` and `rename` also stage their\n // renames in git. A half-done move nobody asked for is worse than a refusal\n // a person can act on, and a refusal is re-runnable the moment the authored\n // file is out of the reserved path.\n //\n // It runs AFTER `plan`, which writes nothing and has already answered the\n // verb-specific refusals (`unknown-target`, `subsystem-not-empty`,\n // `move-uncommitted`): a caller whose name is wrong AND whose tree holds an\n // authored views file is better told about the name.\n const authored = await authoredGeneratedPaths([expected.subsystems.path, expected.services.path]);\n if (authored.length > 0) {\n fail(\n json,\n // The code is `already-exists` and not a new one because it is exactly\n // what that code has always meant here — \"refusing to create over\n // something that already exists, because overwriting would destroy work\n // nobody asked to lose\" — and it is already this command's documented\n // refusal for the flat namespace. A second code for the same sentence\n // would be a second branch an agent has to learn for no new decision.\n \"already-exists\",\n authored.map((path) => authoredRefusal(repoPath(docsDir, path))).join(\" \") +\n \" NOTHING was written and nothing was renamed: this command lands the generated views file in the same \" +\n \"commit as the change to the tree, so it refuses whole rather than leaving the tree changed — and, for \" +\n \"`move` and `rename`, renames staged in git — with a views file it may not write.\",\n );\n return;\n }\n const writes = [...txn.writes];\n // BOTH generated files, in the ONE commit that changes the tree. The\n // per-service file is not a bystander here: its view titles are\n // `<subsystem path> / <service id>`, so every `move` and `rename` restales\n // it exactly as it restales the subsystem views, and leaving it for a later\n // `sync` is the failing-between-two-commits state this window exists to\n // refuse.\n const views: ViewsActions = {\n subsystems: await stageGenerated(expected.subsystems, writes, false),\n // PROTECTED when the map did not parse as a project, and only this file\n // is: `renderSubsystemViews` answers null off the TREE, which is readable\n // either way, while `renderServiceOverviews` answers null whenever no\n // service resolves to an element — which is what an unreadable map looks\n // like from here. Under `known: false` a null render is therefore not\n // evidence of absence, and writing it would delete the whole file over a\n // sibling document that does not parse. A verb still rewrites a file it\n // CAN render (the tolerant arm this window has always taken), and the\n // person is told by the note at the foot of this function.\n services: await stageGenerated(expected.services, writes, !expected.known),\n };\n const staged = await stageWrites(writes);\n const committed = await commitStaged(\n {\n root: docsDir,\n command: \"subsystem\",\n rerun: \"loam subsystem sync\",\n target: txn.target,\n ...(txn.moves === undefined ? {} : { moves: txn.moves }),\n },\n staged,\n txn.what,\n );\n if (!committed.ok) {\n const code =\n committed.code === \"rollback-incomplete\"\n ? committed.code\n : committed.raced\n ? (txn.racedCode ?? txn.failedCode ?? committed.code)\n : (txn.failedCode ?? committed.code);\n fail(json, code, committed.message);\n return;\n }\n await txn.report(views, recovered);\n // A verb CANNOT leave this file behind the tree — its bytes depend on the\n // placement the verb just changed, so the tolerant render lands even when the\n // map is unreadable and the file loses its includes. `sync` may decline that\n // rewrite (`action: \"blocked\"`); a verb may not, so the person is told\n // instead, and the same note names the run that puts the includes back.\n if (!json && !expected.known) console.log(LANDSCAPE_UNREADABLE_NOTE);\n } finally {\n await opened.release();\n }\n}\n\n/**\n * Compare ONE generated file against what it must say, append the write when\n * they differ, and name what happened. The compare is `planGenerated`'s, the\n * one `../sync.ts` makes too — `./views.ts` says why it is over CONTENT, not\n * bytes, and why there is one spelling of it.\n *\n * `protect` is the caller's statement that a NULL render is not evidence: the\n * file is left exactly as it is rather than deleted. It is never a licence to\n * skip a real rewrite — a file loam can render is rendered, which is the\n * difference between this window and `../sync.ts`'s `blocked`.\n */\nasync function stageGenerated(file: ExpectedFile, writes: PlannedWrite[], protect: boolean): Promise<ViewsAction> {\n const current = existsSync(file.path) ? await readFile(file.path, \"utf8\") : null;\n const planned = planGenerated(file, current);\n if (planned.write === null || (protect && file.content === null)) return \"current\";\n writes.push(planned.write);\n return planned.action;\n}\n"]}
|
|
@@ -1,9 +1,10 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* What the generated views files MUST say — the shared answer every subsystem
|
|
3
|
-
* writer and `subsystem sync` render from
|
|
4
|
-
*
|
|
5
|
-
*
|
|
6
|
-
*
|
|
3
|
+
* writer and `subsystem sync` render from, and the one compare both make against
|
|
4
|
+
* it (`planGenerated`). A module beneath the verbs (in the txn/ subpackage)
|
|
5
|
+
* because the commit window and sync both consume it, and a second spelling of
|
|
6
|
+
* "what should the views say" would be a second chance to disagree with
|
|
7
|
+
* `validate`'s staleness check.
|
|
7
8
|
*
|
|
8
9
|
* BOTH generated files come out of ONE read of the map, and that is the reason
|
|
9
10
|
* this module answers about the pair rather than about one file: they are
|
|
@@ -21,7 +22,7 @@ import { loadFile } from "../../../core/c4/likec4.js";
|
|
|
21
22
|
import { architectureProjectDocuments, loadArchitecture } from "../../../core/c4/project/architecture.js";
|
|
22
23
|
import { serviceDirOf } from "../../../core/kernel/ids/dirs.js";
|
|
23
24
|
import { landscapePath, serviceViewsPath, subsystemViewsPath } from "../../../core/repo/paths.js";
|
|
24
|
-
import { renderServiceOverviews, renderSubsystemViews, } from "../../../core/repo/tree/render/views.js";
|
|
25
|
+
import { renderServiceOverviews, renderSubsystemViews, viewsAgree, } from "../../../core/repo/tree/render/views.js";
|
|
25
26
|
/**
|
|
26
27
|
* The elements, the relationships and the declared global style ids of the whole
|
|
27
28
|
* `architecture/` PROJECT — the member join, the edges every per-service view
|
|
@@ -157,6 +158,32 @@ async function declaredStyles(docsDir, landscape) {
|
|
|
157
158
|
}
|
|
158
159
|
return [...ids].sort();
|
|
159
160
|
}
|
|
161
|
+
/**
|
|
162
|
+
* Compare ONE generated file against what it must say: the write when they
|
|
163
|
+
* differ, and what that write is. Both writers of these files — `../sync.ts`'s
|
|
164
|
+
* `planFile` and the verbs' `stageGenerated` (`./txn.ts`) — put their own veto
|
|
165
|
+
* in front of this and nothing else, because the rule is one rule, and a second
|
|
166
|
+
* spelling of it would be a second chance for the two writers to answer
|
|
167
|
+
* differently about the same file.
|
|
168
|
+
*
|
|
169
|
+
* The compare is over CONTENT, not bytes: `viewsAgree` records why, beside the
|
|
170
|
+
* generator. A Windows clone hands a generated file back CRLF with not one fact
|
|
171
|
+
* changed, and a byte compare made `sync` answer `updated` and rewrite it LF —
|
|
172
|
+
* after which git shows the file permanently modified and the next checkout puts
|
|
173
|
+
* the CRLF back — and folded the same rewrite into every subsystem move's
|
|
174
|
+
* transaction and journal. `validate` stopped demanding that rewrite; the
|
|
175
|
+
* writers must stop offering it, or they disagree about the same file. What gets
|
|
176
|
+
* WRITTEN is still the LF render as a Buffer, so the decode is a comparison only.
|
|
177
|
+
*/
|
|
178
|
+
export function planGenerated(file, current) {
|
|
179
|
+
if (viewsAgree(current, file.content))
|
|
180
|
+
return { action: "current", write: null };
|
|
181
|
+
const bytes = file.content === null ? null : Buffer.from(file.content, "utf8");
|
|
182
|
+
return {
|
|
183
|
+
action: current === null ? "created" : bytes === null ? "removed" : "updated",
|
|
184
|
+
write: { path: file.path, content: bytes },
|
|
185
|
+
};
|
|
186
|
+
}
|
|
160
187
|
export async function expectedViews(docsDir, tree) {
|
|
161
188
|
const read = await landscapeFacts(docsDir);
|
|
162
189
|
// The parsed DOCUMENT goes across, never a literal composed here: `MapFacts`
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"views.js","sourceRoot":"","sources":["../../../../src/commands/subsystem/txn/views.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * What the generated views files MUST say — the shared answer every subsystem\n * writer and `subsystem sync` render from. A module beneath the verbs (in the\n * txn/ subpackage) because the commit window and sync both consume it, and a\n * second spelling of \"what should the views say\" would be a second chance to\n * disagree with `validate`'s staleness check.\n *\n * BOTH generated files come out of ONE read of the map, and that is the reason\n * this module answers about the pair rather than about one file: they are\n * rendered from the same parsed `architecture/` project, they are repaired by\n * the same command, and they must land in the same commit — a verb that changed\n * a service's placement changed a subsystem view's members AND that service's\n * overview TITLE (`<subsystem path> / <service id>`), so shipping one without\n * the other is the \"stale between two commits\" state the generated files'\n * contract exists to refuse. Reading the project twice would also parse the\n * whole fleet map twice per run for one answer.\n */\nimport { existsSync } from \"node:fs\";\nimport { relative, resolve, sep } from \"node:path\";\nimport { loadFile } from \"../../../core/c4/likec4.js\";\nimport { architectureProjectDocuments, loadArchitecture } from \"../../../core/c4/project/architecture.js\";\nimport { serviceDirOf, type DocsDir } from \"../../../core/kernel/ids/dirs.js\";\nimport { landscapePath, serviceViewsPath, subsystemViewsPath } from \"../../../core/repo/paths.js\";\nimport {\n type MapFacts,\n renderServiceOverviews,\n renderSubsystemViews,\n} from \"../../../core/repo/tree/render/views.js\";\nimport type { FleetTree } from \"../../../core/repo/tree/walk.js\";\n\n/** What one read of the map yielded, and whether it came from the whole project. */\nexport interface LandscapeRead {\n /** The element join and the style census, or null when neither reading was available. */\n facts: MapFacts | null;\n /**\n * The `architecture/` PROJECT parsed. FALSE means the facts below, if there\n * are any, were assembled document by document — the member join from the\n * landscape file alone (so an element a use-case document binds is missing\n * from it), the style census from every document that parses on its own.\n */\n known: boolean;\n}\n\n/**\n * The elements, the relationships and the declared global style ids of the whole\n * `architecture/` PROJECT — the member join, the edges every per-service view\n * computes its peer set from, and the one line a view may borrow a palette\n * with — with the provenance of that answer beside it.\n *\n * The PROJECT, not the landscape file alone, and that is a correctness rule\n * rather than a preference: `validate` grades this file against\n * `loadArchitecture` — the landscape merged with every\n * `architecture/usecases/*.likec4`, which may `extend` the model and bind\n * elements of its own. While this side read the landscape alone, a use-case\n * document declaring one element bound to a filed service was enough to make\n * `sync` answer `current` while `validate --all` reported\n * `subsystem.views-stale` and named `sync` as the repair — a loop no command\n * could clear, reproduced on `examples/docs` before this call changed. Two\n * readers of \"what should the views say\" must read the same documents, or the\n * shared render in `core/repo/tree/render/views.ts` is shared in name only.\n *\n * WHY THE FALLBACK AND THE FLAG ARE TWO ANSWERS RATHER THAN ONE. The fallback\n * exists because one broken use-case document must not cost the render its\n * member join: the landscape's own elements are still the truth about who\n * belongs in which subsystem, and re-rendering the file without them takes every\n * `include` line off it (verification 2026-09-04, W5). But the STYLE CENSUS is\n * not the landscape file's alone — SCHEMA blesses a palette in any `.likec4`\n * under `architecture/` — so the fallback takes it over EVERY document that\n * parses on its own (`declaredStyles`). Reading it off the landscape alone was\n * enough to drop `global style subsystems` from a good generated file whenever\n * the palette sat in a sibling and an unrelated sibling was broken\n * (re-verification 2026-09-04, area C item 8).\n *\n * `known` still means the PROJECT parsed, and nothing weaker, because the MEMBER\n * JOIN is what it licences: a use-case document that binds an element of its own\n * contributes to the join and cannot be read document by document. It is\n * `../sync.ts`'s licence to leave an existing file alone.\n *\n * An ABSENT map is not a failure: there is nothing to read, the empty facts are\n * the truth, and the tolerant render is what a fleet being scaffolded needs. An\n * EMPTY project is no longer reachable here — `architectureProjectDocuments`\n * keeps the landscape whatever the root `exclude` says, and its banner records\n * what an empty one cost.\n */\nexport async function landscapeFacts(docsDir: DocsDir): Promise<LandscapeRead> {\n const path = landscapePath(docsDir);\n // No map at all: no elements and NO EDGES, said outright. `relationships` is\n // required of every `MapFacts` precisely so an empty answer has to be written\n // down rather than dropped — see the type.\n if (!existsSync(path)) return { facts: { elements: [], relationships: [] }, known: true };\n try {\n const doc = await loadArchitecture(docsDir);\n // The whole document on success, so the render reads the same record\n // `validate --all` grades against; `globalStyles` rides along without\n // being named here, and a field the loaders add later does too.\n if (doc.errors.length === 0) return { facts: doc, known: true };\n } catch {\n // A throw and a parse error are the same evidence: fall through to the file.\n }\n try {\n const file = await loadFile(path);\n // THE STYLE CENSUS IS TAKEN OVER EVERY DOCUMENT, not over the landscape, and\n // that is what makes the fallback safe for a verb that has to write. A\n // palette declared in a SIBLING under `architecture/` is invisible to a read\n // of the landscape alone, so a `subsystem new` run standing beside one\n // unrelated broken sibling silently dropped `global style subsystems` from a\n // good generated file — the landscape parsed, the element join survived, and\n // nothing graded the loss because `sync` answers `blocked` for as long as the\n // broken file stands (re-verification 2026-09-04, area C item 8). Reading\n // each document ON ITS OWN is the widening the fallback is entitled to: it\n // cannot merge them (that is what failed one line up), but a document that\n // parses alone certainly declares the style groups it declares.\n if (file.errors.length === 0) {\n // THE LANDSCAPE'S OWN RELATIONSHIPS TRAVEL WITH ITS ELEMENTS. This arm\n // composes a record rather than handing the document across, because the\n // style census below is widened past what this one document declares — and\n // the first version of it listed the fields it happened to be thinking\n // about and dropped the edges the document was carrying all along. That is\n // silent: every per-service view then renders with no peers, the service\n // and its own interior and nothing it touches, and no check anywhere says\n // so. `MapFacts` now REQUIRES the field for exactly this reason; the copy\n // stays explicit so the widening above it stays visible.\n return {\n facts: {\n elements: file.elements,\n relationships: file.relationships,\n globalStyles: await declaredStyles(docsDir, file),\n },\n known: false,\n };\n }\n } catch {\n // Unreadable bytes; the caller is told loam could not say.\n }\n return { facts: null, known: false };\n}\n\n/**\n * Every global style id declared by an `architecture/` document that parses ON\n * ITS OWN, the landscape's included — the union, sorted, so the render is a\n * function of the set rather than of readdir order.\n *\n * The document set is `architectureProjectDocuments`: the same list the PROJECT\n * read uses, so the fallback can never consider a file the merged read would\n * have excluded — the generated views file itself, or one the root `exclude`\n * hides from the renderer. A document that does not parse alone contributes\n * nothing and is not an error here: it is named by `loam validate --all`, and\n * the note this module carries already tells the reader the census came from\n * outside a project read.\n */\nasync function declaredStyles(docsDir: DocsDir, landscape: MapFacts): Promise<string[]> {\n const ids = new Set(landscape.globalStyles ?? []);\n let paths: string[];\n try {\n paths = await architectureProjectDocuments(docsDir);\n } catch {\n return [...ids].sort();\n }\n const map = resolve(landscapePath(docsDir));\n for (const path of paths) {\n if (resolve(path) === map) continue;\n try {\n const doc = await loadFile(path);\n if (doc.errors.length === 0) for (const id of doc.globalStyles ?? []) ids.add(id);\n } catch {\n // One unreadable sibling costs its own styles and nothing else.\n }\n }\n return [...ids].sort();\n}\n\n/**\n * ONE generated file: where it lives, and the bytes it must hold — `null` for\n * \"this file must not exist\", which is a state of its own and not a blank file\n * (`render/stale.ts` keeps the same three apart on the reading side).\n */\nexport interface ExpectedFile {\n path: string;\n content: string | null;\n}\n\n/**\n * The bytes BOTH generated files must hold for this tree, and whether the map\n * they were computed from was read as a PROJECT.\n *\n * The content is always renderable — from the landscape file alone, or from\n * nothing — because a fleet with no generated file yet must still get one, and\n * the tree-changing verbs must still land theirs in the commit that changes the\n * tree. `known: false` is the caller's licence to leave an EXISTING file alone\n * instead (`../sync.ts`, `action: \"blocked\"`).\n *\n * NAMED FIELDS, never an array, and never one folded answer: the two files go\n * stale for different reasons and each is named in its own finding and its own\n * report line, so a caller that could not tell them apart would have nothing to\n * say. `subsystem.views-stale` carries the path precisely so one code covers\n * both honestly.\n *\n * THE SERVICE FILE IS THE ONE THE MAP CAN EMPTY. `renderSubsystemViews` answers\n * null only when the TREE has no subsystems — a fact no map can change — while\n * `renderServiceOverviews` answers null whenever no service resolves to an\n * element, which is exactly what an unreadable map looks like from here (`facts`\n * falls back to an empty element list and an empty edge list alike). So for that\n * file \"must be absent\" and \"loam could not read the map\" are the same bytes,\n * and only `known` tells them apart:\n * a writer must not treat a null service render under `known: false` as evidence\n * that the file should go. Both callers hold that line, and say so.\n */\nexport interface ExpectedViews {\n /**\n * The `architecture/` PROJECT was read. False means the content was rendered\n * from the landscape file alone — or from nothing at all — so its style census\n * is not one an existing file may be rewritten from, and its element join is\n * not one the per-service file may be deleted on.\n */\n known: boolean;\n /** `architecture/subsystems.likec4` — one view per subsystem, from the tree. */\n subsystems: ExpectedFile;\n /** `architecture/services.likec4` — one overview view per service the map draws. */\n services: ExpectedFile;\n}\n\nexport async function expectedViews(docsDir: DocsDir, tree: FleetTree): Promise<ExpectedViews> {\n const read = await landscapeFacts(docsDir);\n // The parsed DOCUMENT goes across, never a literal composed here: `MapFacts`\n // carries the relationships the per-service peer sets are computed from, and a\n // caller that dropped them would render every service view peerless — the\n // service and its interior, and nothing it touches — with no error anywhere.\n // The type now REQUIRES the field so that mistake cannot be made silently\n // (`render/views.ts` records what it cost); this last-resort literal is the one\n // place an empty answer is the TRUE one — loam could not read the map at\n // all — and it says so rather than leaving the field off.\n const map = read.facts ?? { elements: [], relationships: [] };\n return {\n known: read.known,\n subsystems: { path: subsystemViewsPath(docsDir), content: renderSubsystemViews(tree, map) },\n services: { path: serviceViewsPath(docsDir), content: renderServiceOverviews(tree, map) },\n };\n}\n\n/**\n * The one line a person is owed when the map cannot be read — the same sentence\n * from `../sync.ts` (which then leaves an existing file alone) and from a\n * tree-changing verb (which renders the tolerant shape anyway, because its own\n * contract is that the file lands in the verb's commit). It names the gating\n * surface rather than repeating its findings: `loam validate --all` is where the\n * broken document is named. It lives here, beside the facts it is about, because\n * both callers are on this side of the `txn/` boundary and a constant in\n * `../sync.ts` would point the package edge back up.\n */\nexport const LANDSCAPE_UNREADABLE_NOTE =\n \"note: architecture/ does not parse as one project, so the element join is whatever \" +\n \"architecture/landscape.likec4 alone says and the style census is the union of the documents that parse on \" +\n \"their own — fix the map (`loam validate --all` names the file), then re-run `loam subsystem sync`.\";\n\n/**\n * The tree AS THE RENAMES LEAVE IT, computed in memory: every dir under a\n * moved root is remapped by prefix, and each entry's placement chain is\n * re-derived from its remapped dir. The views file is rendered from this, so\n * the commit lands file and renames agreeing — never from a re-walk that\n * would have to happen between two states. Beside `expectedViews` because the\n * two are one promise: what the commit renames and what the file says must\n * come from the same in-memory answer.\n */\nexport function movedTree(tree: FleetTree, renames: { from: string; to: string }[], servicesRoot: string): FleetTree {\n const remap = (dir: string): string => {\n for (const r of renames) {\n if (dir === r.from || dir.startsWith(r.from + sep)) return r.to + dir.slice(r.from.length);\n }\n return dir;\n };\n const chainOf = (dir: string): string[] => relative(servicesRoot, dir).split(sep);\n return {\n findings: tree.findings,\n services: tree.services.map((s) => {\n const dir = remap(s.dir);\n // The constructor call records provenance: a readdir-established\n // directory carried through a rename this same commit performs.\n return { ...s, dir: serviceDirOf(dir), subsystem: chainOf(dir).slice(0, -1) };\n }),\n subsystems: tree.subsystems.map((s) => {\n const dir = remap(s.dir);\n return { ...s, dir, path: chainOf(dir) };\n }),\n };\n}\n"]}
|
|
1
|
+
{"version":3,"file":"views.js","sourceRoot":"","sources":["../../../../src/commands/subsystem/txn/views.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * What the generated views files MUST say — the shared answer every subsystem\n * writer and `subsystem sync` render from, and the one compare both make against\n * it (`planGenerated`). A module beneath the verbs (in the txn/ subpackage)\n * because the commit window and sync both consume it, and a second spelling of\n * \"what should the views say\" would be a second chance to disagree with\n * `validate`'s staleness check.\n *\n * BOTH generated files come out of ONE read of the map, and that is the reason\n * this module answers about the pair rather than about one file: they are\n * rendered from the same parsed `architecture/` project, they are repaired by\n * the same command, and they must land in the same commit — a verb that changed\n * a service's placement changed a subsystem view's members AND that service's\n * overview TITLE (`<subsystem path> / <service id>`), so shipping one without\n * the other is the \"stale between two commits\" state the generated files'\n * contract exists to refuse. Reading the project twice would also parse the\n * whole fleet map twice per run for one answer.\n */\nimport { existsSync } from \"node:fs\";\nimport { relative, resolve, sep } from \"node:path\";\nimport { loadFile } from \"../../../core/c4/likec4.js\";\nimport { architectureProjectDocuments, loadArchitecture } from \"../../../core/c4/project/architecture.js\";\nimport { serviceDirOf, type DocsDir } from \"../../../core/kernel/ids/dirs.js\";\nimport { landscapePath, serviceViewsPath, subsystemViewsPath } from \"../../../core/repo/paths.js\";\nimport {\n type MapFacts,\n renderServiceOverviews,\n renderSubsystemViews,\n viewsAgree,\n} from \"../../../core/repo/tree/render/views.js\";\nimport type { FleetTree } from \"../../../core/repo/tree/walk.js\";\nimport type { PlannedWrite } from \"../../../core/staging/writes.js\";\n\n/** What one read of the map yielded, and whether it came from the whole project. */\nexport interface LandscapeRead {\n /** The element join and the style census, or null when neither reading was available. */\n facts: MapFacts | null;\n /**\n * The `architecture/` PROJECT parsed. FALSE means the facts below, if there\n * are any, were assembled document by document — the member join from the\n * landscape file alone (so an element a use-case document binds is missing\n * from it), the style census from every document that parses on its own.\n */\n known: boolean;\n}\n\n/**\n * The elements, the relationships and the declared global style ids of the whole\n * `architecture/` PROJECT — the member join, the edges every per-service view\n * computes its peer set from, and the one line a view may borrow a palette\n * with — with the provenance of that answer beside it.\n *\n * The PROJECT, not the landscape file alone, and that is a correctness rule\n * rather than a preference: `validate` grades this file against\n * `loadArchitecture` — the landscape merged with every\n * `architecture/usecases/*.likec4`, which may `extend` the model and bind\n * elements of its own. While this side read the landscape alone, a use-case\n * document declaring one element bound to a filed service was enough to make\n * `sync` answer `current` while `validate --all` reported\n * `subsystem.views-stale` and named `sync` as the repair — a loop no command\n * could clear, reproduced on `examples/docs` before this call changed. Two\n * readers of \"what should the views say\" must read the same documents, or the\n * shared render in `core/repo/tree/render/views.ts` is shared in name only.\n *\n * WHY THE FALLBACK AND THE FLAG ARE TWO ANSWERS RATHER THAN ONE. The fallback\n * exists because one broken use-case document must not cost the render its\n * member join: the landscape's own elements are still the truth about who\n * belongs in which subsystem, and re-rendering the file without them takes every\n * `include` line off it (verification 2026-09-04, W5). But the STYLE CENSUS is\n * not the landscape file's alone — SCHEMA blesses a palette in any `.likec4`\n * under `architecture/` — so the fallback takes it over EVERY document that\n * parses on its own (`declaredStyles`). Reading it off the landscape alone was\n * enough to drop `global style subsystems` from a good generated file whenever\n * the palette sat in a sibling and an unrelated sibling was broken\n * (re-verification 2026-09-04, area C item 8).\n *\n * `known` still means the PROJECT parsed, and nothing weaker, because the MEMBER\n * JOIN is what it licences: a use-case document that binds an element of its own\n * contributes to the join and cannot be read document by document. It is\n * `../sync.ts`'s licence to leave an existing file alone.\n *\n * An ABSENT map is not a failure: there is nothing to read, the empty facts are\n * the truth, and the tolerant render is what a fleet being scaffolded needs. An\n * EMPTY project is no longer reachable here — `architectureProjectDocuments`\n * keeps the landscape whatever the root `exclude` says, and its banner records\n * what an empty one cost.\n */\nexport async function landscapeFacts(docsDir: DocsDir): Promise<LandscapeRead> {\n const path = landscapePath(docsDir);\n // No map at all: no elements and NO EDGES, said outright. `relationships` is\n // required of every `MapFacts` precisely so an empty answer has to be written\n // down rather than dropped — see the type.\n if (!existsSync(path)) return { facts: { elements: [], relationships: [] }, known: true };\n try {\n const doc = await loadArchitecture(docsDir);\n // The whole document on success, so the render reads the same record\n // `validate --all` grades against; `globalStyles` rides along without\n // being named here, and a field the loaders add later does too.\n if (doc.errors.length === 0) return { facts: doc, known: true };\n } catch {\n // A throw and a parse error are the same evidence: fall through to the file.\n }\n try {\n const file = await loadFile(path);\n // THE STYLE CENSUS IS TAKEN OVER EVERY DOCUMENT, not over the landscape, and\n // that is what makes the fallback safe for a verb that has to write. A\n // palette declared in a SIBLING under `architecture/` is invisible to a read\n // of the landscape alone, so a `subsystem new` run standing beside one\n // unrelated broken sibling silently dropped `global style subsystems` from a\n // good generated file — the landscape parsed, the element join survived, and\n // nothing graded the loss because `sync` answers `blocked` for as long as the\n // broken file stands (re-verification 2026-09-04, area C item 8). Reading\n // each document ON ITS OWN is the widening the fallback is entitled to: it\n // cannot merge them (that is what failed one line up), but a document that\n // parses alone certainly declares the style groups it declares.\n if (file.errors.length === 0) {\n // THE LANDSCAPE'S OWN RELATIONSHIPS TRAVEL WITH ITS ELEMENTS. This arm\n // composes a record rather than handing the document across, because the\n // style census below is widened past what this one document declares — and\n // the first version of it listed the fields it happened to be thinking\n // about and dropped the edges the document was carrying all along. That is\n // silent: every per-service view then renders with no peers, the service\n // and its own interior and nothing it touches, and no check anywhere says\n // so. `MapFacts` now REQUIRES the field for exactly this reason; the copy\n // stays explicit so the widening above it stays visible.\n return {\n facts: {\n elements: file.elements,\n relationships: file.relationships,\n globalStyles: await declaredStyles(docsDir, file),\n },\n known: false,\n };\n }\n } catch {\n // Unreadable bytes; the caller is told loam could not say.\n }\n return { facts: null, known: false };\n}\n\n/**\n * Every global style id declared by an `architecture/` document that parses ON\n * ITS OWN, the landscape's included — the union, sorted, so the render is a\n * function of the set rather than of readdir order.\n *\n * The document set is `architectureProjectDocuments`: the same list the PROJECT\n * read uses, so the fallback can never consider a file the merged read would\n * have excluded — the generated views file itself, or one the root `exclude`\n * hides from the renderer. A document that does not parse alone contributes\n * nothing and is not an error here: it is named by `loam validate --all`, and\n * the note this module carries already tells the reader the census came from\n * outside a project read.\n */\nasync function declaredStyles(docsDir: DocsDir, landscape: MapFacts): Promise<string[]> {\n const ids = new Set(landscape.globalStyles ?? []);\n let paths: string[];\n try {\n paths = await architectureProjectDocuments(docsDir);\n } catch {\n return [...ids].sort();\n }\n const map = resolve(landscapePath(docsDir));\n for (const path of paths) {\n if (resolve(path) === map) continue;\n try {\n const doc = await loadFile(path);\n if (doc.errors.length === 0) for (const id of doc.globalStyles ?? []) ids.add(id);\n } catch {\n // One unreadable sibling costs its own styles and nothing else.\n }\n }\n return [...ids].sort();\n}\n\n/**\n * ONE generated file: where it lives, and the bytes it must hold — `null` for\n * \"this file must not exist\", which is a state of its own and not a blank file\n * (`render/stale.ts` keeps the same three apart on the reading side).\n */\nexport interface ExpectedFile {\n path: string;\n content: string | null;\n}\n\n/** What a commit does to one generated file once every writer's own veto has passed. */\nexport type ViewsAction = \"created\" | \"updated\" | \"removed\" | \"current\";\n\n/**\n * Compare ONE generated file against what it must say: the write when they\n * differ, and what that write is. Both writers of these files — `../sync.ts`'s\n * `planFile` and the verbs' `stageGenerated` (`./txn.ts`) — put their own veto\n * in front of this and nothing else, because the rule is one rule, and a second\n * spelling of it would be a second chance for the two writers to answer\n * differently about the same file.\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 — and folded the same rewrite into every subsystem move's\n * transaction and journal. `validate` stopped demanding that rewrite; the\n * writers must stop offering it, or they disagree about the same file. What gets\n * WRITTEN is still the LF render as a Buffer, so the decode is a comparison only.\n */\nexport function planGenerated(file: ExpectedFile, current: string | null): { action: ViewsAction; write: PlannedWrite | null } {\n if (viewsAgree(current, file.content)) return { action: \"current\", write: null };\n const bytes = file.content === null ? null : Buffer.from(file.content, \"utf8\");\n return {\n action: current === null ? \"created\" : bytes === null ? \"removed\" : \"updated\",\n write: { path: file.path, content: bytes },\n };\n}\n\n/**\n * The bytes BOTH generated files must hold for this tree, and whether the map\n * they were computed from was read as a PROJECT.\n *\n * The content is always renderable — from the landscape file alone, or from\n * nothing — because a fleet with no generated file yet must still get one, and\n * the tree-changing verbs must still land theirs in the commit that changes the\n * tree. `known: false` is the caller's licence to leave an EXISTING file alone\n * instead (`../sync.ts`, `action: \"blocked\"`).\n *\n * NAMED FIELDS, never an array, and never one folded answer: the two files go\n * stale for different reasons and each is named in its own finding and its own\n * report line, so a caller that could not tell them apart would have nothing to\n * say. `subsystem.views-stale` carries the path precisely so one code covers\n * both honestly.\n *\n * THE SERVICE FILE IS THE ONE THE MAP CAN EMPTY. `renderSubsystemViews` answers\n * null only when the TREE has no subsystems — a fact no map can change — while\n * `renderServiceOverviews` answers null whenever no service resolves to an\n * element, which is exactly what an unreadable map looks like from here (`facts`\n * falls back to an empty element list and an empty edge list alike). So for that\n * file \"must be absent\" and \"loam could not read the map\" are the same bytes,\n * and only `known` tells them apart:\n * a writer must not treat a null service render under `known: false` as evidence\n * that the file should go. Both callers hold that line, and say so.\n */\nexport interface ExpectedViews {\n /**\n * The `architecture/` PROJECT was read. False means the content was rendered\n * from the landscape file alone — or from nothing at all — so its style census\n * is not one an existing file may be rewritten from, and its element join is\n * not one the per-service file may be deleted on.\n */\n known: boolean;\n /** `architecture/subsystems.likec4` — one view per subsystem, from the tree. */\n subsystems: ExpectedFile;\n /** `architecture/services.likec4` — one overview view per service the map draws. */\n services: ExpectedFile;\n}\n\nexport async function expectedViews(docsDir: DocsDir, tree: FleetTree): Promise<ExpectedViews> {\n const read = await landscapeFacts(docsDir);\n // The parsed DOCUMENT goes across, never a literal composed here: `MapFacts`\n // carries the relationships the per-service peer sets are computed from, and a\n // caller that dropped them would render every service view peerless — the\n // service and its interior, and nothing it touches — with no error anywhere.\n // The type now REQUIRES the field so that mistake cannot be made silently\n // (`render/views.ts` records what it cost); this last-resort literal is the one\n // place an empty answer is the TRUE one — loam could not read the map at\n // all — and it says so rather than leaving the field off.\n const map = read.facts ?? { elements: [], relationships: [] };\n return {\n known: read.known,\n subsystems: { path: subsystemViewsPath(docsDir), content: renderSubsystemViews(tree, map) },\n services: { path: serviceViewsPath(docsDir), content: renderServiceOverviews(tree, map) },\n };\n}\n\n/**\n * The one line a person is owed when the map cannot be read — the same sentence\n * from `../sync.ts` (which then leaves an existing file alone) and from a\n * tree-changing verb (which renders the tolerant shape anyway, because its own\n * contract is that the file lands in the verb's commit). It names the gating\n * surface rather than repeating its findings: `loam validate --all` is where the\n * broken document is named. It lives here, beside the facts it is about, because\n * both callers are on this side of the `txn/` boundary and a constant in\n * `../sync.ts` would point the package edge back up.\n */\nexport const LANDSCAPE_UNREADABLE_NOTE =\n \"note: architecture/ does not parse as one project, so the element join is whatever \" +\n \"architecture/landscape.likec4 alone says and the style census is the union of the documents that parse on \" +\n \"their own — fix the map (`loam validate --all` names the file), then re-run `loam subsystem sync`.\";\n\n/**\n * The tree AS THE RENAMES LEAVE IT, computed in memory: every dir under a\n * moved root is remapped by prefix, and each entry's placement chain is\n * re-derived from its remapped dir. The views file is rendered from this, so\n * the commit lands file and renames agreeing — never from a re-walk that\n * would have to happen between two states. Beside `expectedViews` because the\n * two are one promise: what the commit renames and what the file says must\n * come from the same in-memory answer.\n */\nexport function movedTree(tree: FleetTree, renames: { from: string; to: string }[], servicesRoot: string): FleetTree {\n const remap = (dir: string): string => {\n for (const r of renames) {\n if (dir === r.from || dir.startsWith(r.from + sep)) return r.to + dir.slice(r.from.length);\n }\n return dir;\n };\n const chainOf = (dir: string): string[] => relative(servicesRoot, dir).split(sep);\n return {\n findings: tree.findings,\n services: tree.services.map((s) => {\n const dir = remap(s.dir);\n // The constructor call records provenance: a readdir-established\n // directory carried through a rename this same commit performs.\n return { ...s, dir: serviceDirOf(dir), subsystem: chainOf(dir).slice(0, -1) };\n }),\n subsystems: tree.subsystems.map((s) => {\n const dir = remap(s.dir);\n return { ...s, dir, path: chainOf(dir) };\n }),\n };\n}\n"]}
|