@ybotok/loam 0.2.0-alpha.6 → 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 +1477 -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 +108 -11
- package/SCHEMA.md +721 -130
- 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 +82 -33
- 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 +55 -26
- package/dist/commands/seed/commit.js.map +1 -1
- package/dist/commands/seed/plan.js +64 -13
- package/dist/commands/seed/plan.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 +7 -7
- package/dist/commands/subsystem/edit.js.map +1 -1
- package/dist/commands/subsystem/move.js +60 -25
- 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 +141 -71
- package/dist/commands/subsystem/sync.js.map +1 -1
- package/dist/commands/subsystem/txn/report.js +55 -5
- package/dist/commands/subsystem/txn/report.js.map +1 -1
- package/dist/commands/subsystem/txn/txn.js +121 -45
- package/dist/commands/subsystem/txn/txn.js.map +1 -1
- package/dist/commands/subsystem/txn/views.js +82 -12
- 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/health-deps.js +9 -6
- package/dist/commands/validate/checks/health-deps.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/census.js +26 -3
- package/dist/commands/validate/fleet/census.js.map +1 -1
- package/dist/commands/validate/fleet/landscape.js +35 -32
- package/dist/commands/validate/fleet/landscape.js.map +1 -1
- package/dist/commands/validate/fleet/map/attest.js +7 -12
- package/dist/commands/validate/fleet/map/attest.js.map +1 -1
- package/dist/commands/validate/fleet/map/bindings.js +16 -5
- package/dist/commands/validate/fleet/map/bindings.js.map +1 -1
- package/dist/commands/validate/fleet/map/consumers.js +43 -122
- package/dist/commands/validate/fleet/map/consumers.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 +1 -1
- package/dist/commands/validate/fleet/views/fleet-project.js.map +1 -1
- package/dist/commands/validate/fleet/views/ids.js +33 -7
- package/dist/commands/validate/fleet/views/ids.js.map +1 -1
- package/dist/commands/validate/fleet/views/root-documents.js +17 -35
- package/dist/commands/validate/fleet/views/root-documents.js.map +1 -1
- package/dist/commands/validate/fleet/views/stale.js +31 -14
- package/dist/commands/validate/fleet/views/stale.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 +57 -102
- 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 +31 -17
- 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 +37 -21
- package/dist/core/agent/agents-md/refusals.js.map +1 -1
- package/dist/core/agent/agents-md.js +25 -10
- 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 +25 -22
- 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 +62 -24
- 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 +30 -25
- 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 +79 -5
- package/dist/core/brief/targets.js.map +1 -1
- package/dist/core/brief/unchecked.js +1 -1
- package/dist/core/brief/unchecked.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/architecture.js +31 -9
- package/dist/core/c4/project/architecture.js.map +1 -1
- package/dist/core/c4/project/documents.js +36 -10
- package/dist/core/c4/project/documents.js.map +1 -1
- package/dist/core/c4/project/load.js +14 -21
- package/dist/core/c4/project/load.js.map +1 -1
- package/dist/core/c4/project/staged.js +7 -3
- package/dist/core/c4/project/staged.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 +15 -16
- 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 +10 -3
- package/dist/core/gate/checks.js.map +1 -1
- package/dist/core/gate/partners.js +64 -19
- 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/paths.js +34 -29
- package/dist/core/repo/paths.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/render/body.js +45 -6
- package/dist/core/repo/tree/render/body.js.map +1 -1
- package/dist/core/repo/tree/render/service/header.js +125 -0
- package/dist/core/repo/tree/render/service/header.js.map +1 -0
- package/dist/core/repo/tree/render/service/peers.js +28 -0
- package/dist/core/repo/tree/render/service/peers.js.map +1 -0
- package/dist/core/repo/tree/render/service/view-id.js +67 -0
- package/dist/core/repo/tree/render/service/view-id.js.map +1 -0
- package/dist/core/repo/tree/render/service/views.js +78 -0
- package/dist/core/repo/tree/render/service/views.js.map +1 -0
- package/dist/core/repo/tree/render/stale.js +19 -2
- package/dist/core/repo/tree/render/stale.js.map +1 -1
- package/dist/core/repo/tree/render/views.js +102 -33
- package/dist/core/repo/tree/render/views.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 +45 -10
- 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 +53 -3
- package/dist/core/vocabulary/maturity.js.map +1 -1
- package/examples/README.md +28 -9
- package/examples/docs/architecture/landscape.likec4 +21 -7
- package/examples/docs/architecture/services.likec4 +70 -0
- package/examples/docs/architecture/subsystems.likec4 +3 -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/order-service/model.likec4 +10 -8
- package/examples/docs/services/payment-service/model.likec4 +6 -4
- package/examples/docs/services/platform/identity-service/model.likec4 +9 -7
- package/examples/docs/services/platform/notification-service/asyncapi.yaml +2 -1
- package/examples/docs/services/platform/notification-service/model.likec4 +4 -5
- 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
|
@@ -1,12 +1,15 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* The shared commit window every subsystem WRITE verb goes through — `new`,
|
|
3
3
|
* `rm`, `move` and `rename` all land here: lock, recover a predecessor's
|
|
4
|
-
* journal, plan against the walked tree, regenerate
|
|
5
|
-
* PLANNED post-state, commit through the journaled transaction
|
|
6
|
-
* renames included), report. One window, because
|
|
7
|
-
* the same commit as whatever changed the tree — a marker without its
|
|
8
|
-
* or renames without theirs, is exactly the `subsystem.views-stale` state
|
|
9
|
-
* the generated
|
|
4
|
+
* journal, plan against the walked tree, regenerate BOTH generated views files
|
|
5
|
+
* from the PLANNED post-state, commit through the journaled transaction
|
|
6
|
+
* (directory renames included), report. One window, because those files must
|
|
7
|
+
* land in the same commit as whatever changed the tree — a marker without its
|
|
8
|
+
* view, or renames without theirs, is exactly the `subsystem.views-stale` state
|
|
9
|
+
* the generated files' contract refuses to leave between two commits. The
|
|
10
|
+
* per-service file is in that rule and not beside it: its titles are
|
|
11
|
+
* `<subsystem path> / <service id>`, so the placement a verb changes is written
|
|
12
|
+
* into it too.
|
|
10
13
|
*
|
|
11
14
|
* This window does NOT write the per-service project files
|
|
12
15
|
* (`services/<…>/<id>/likec4.config.json`); only `../sync.ts` does, and the
|
|
@@ -26,23 +29,39 @@
|
|
|
26
29
|
* model with no file beside it, and that is the state `validate --all` names
|
|
27
30
|
* as `service.likec4-config-missing`, with `loam subsystem sync` as the
|
|
28
31
|
* repair — one writer, so the create-only rule has one place to hold.
|
|
32
|
+
*
|
|
33
|
+
* NEITHER generated file is written over a document loam did not write, and
|
|
34
|
+
* that guard is checked HERE as well as in `../sync.ts` — see the comment at
|
|
35
|
+
* the check. Both are filenames loam RESERVES inside a directory the team
|
|
36
|
+
* owns, both are rewritten whole, and until the check below existed all four
|
|
37
|
+
* verbs took an authored file at either path without a word. Unlike sync,
|
|
38
|
+
* which reports the one file `refused` and completes the rest, a verb refuses
|
|
39
|
+
* WHOLE: the file has to land in this commit, so half of this commit is not a
|
|
40
|
+
* lesser version of it.
|
|
29
41
|
*/
|
|
30
42
|
import { existsSync } from "node:fs";
|
|
31
43
|
import { readFile } from "node:fs/promises";
|
|
32
|
-
import { fail } from "../../../core/envelope/json.js";
|
|
33
|
-
import { subsystemViewsPath } from "../../../core/repo/paths.js";
|
|
44
|
+
import { fail, repoPath } from "../../../core/envelope/json.js";
|
|
34
45
|
import { listFleetTree } from "../../../core/repo/repo.js";
|
|
35
46
|
import { stageWrites } from "../../../core/staging/commit.js";
|
|
36
|
-
import {
|
|
37
|
-
import {
|
|
38
|
-
import { recoverInterruptedCommit } from "../../../core/staging/recovery/recover.js";
|
|
47
|
+
import {} from "../../../core/staging/interrupted.js";
|
|
48
|
+
import { openCommitWindow } from "../../../core/staging/recovery/recover.js";
|
|
39
49
|
import { commitStaged } from "../../../core/staging/txn/transaction.js";
|
|
40
50
|
import {} from "../../../core/staging/writes.js";
|
|
41
|
-
import {
|
|
42
|
-
import { expectedViews, LANDSCAPE_UNREADABLE_NOTE } from "./views.js";
|
|
43
|
-
/** The text tail a verb's success line carries about the generated file. */
|
|
51
|
+
import { authoredGeneratedPaths, authoredRefusal } from "../../../core/repo/tree/render/service/header.js";
|
|
52
|
+
import { expectedViews, LANDSCAPE_UNREADABLE_NOTE, planGenerated } from "./views.js";
|
|
53
|
+
/** The text tail a verb's success line carries about the generated files — one clause per file that moved. */
|
|
44
54
|
export function reportViews(views) {
|
|
45
|
-
return
|
|
55
|
+
return ([
|
|
56
|
+
["architecture/subsystems.likec4", views.subsystems],
|
|
57
|
+
["architecture/services.likec4", views.services],
|
|
58
|
+
]
|
|
59
|
+
// `current` says nothing: the tail hangs off a success line, and a verb
|
|
60
|
+
// that renamed one directory must not print two clauses about files that
|
|
61
|
+
// did not move.
|
|
62
|
+
.filter(([, action]) => action !== "current")
|
|
63
|
+
.map(([file, action]) => ` — ${file} ${action}`)
|
|
64
|
+
.join(""));
|
|
46
65
|
}
|
|
47
66
|
/**
|
|
48
67
|
* Lock → recover → plan → stage (views included) → journaled commit → report.
|
|
@@ -50,43 +69,81 @@ export function reportViews(views) {
|
|
|
50
69
|
* staged and nothing is written.
|
|
51
70
|
*/
|
|
52
71
|
export async function commitWindow(docsDir, json, plan) {
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
if (!(err instanceof DocsBusyError))
|
|
59
|
-
throw err;
|
|
60
|
-
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);
|
|
61
77
|
return;
|
|
62
78
|
}
|
|
79
|
+
const { recovered } = opened;
|
|
63
80
|
try {
|
|
64
|
-
let recovered;
|
|
65
|
-
try {
|
|
66
|
-
recovered = await recoverInterruptedCommit(docsDir);
|
|
67
|
-
}
|
|
68
|
-
catch (err) {
|
|
69
|
-
if (!(err instanceof InterruptedCommitError))
|
|
70
|
-
throw err;
|
|
71
|
-
fail(json, "commit-interrupted", err.message);
|
|
72
|
-
return;
|
|
73
|
-
}
|
|
74
81
|
const txn = await plan(await listFleetTree(docsDir));
|
|
75
82
|
if (txn === null)
|
|
76
83
|
return;
|
|
77
|
-
const path = subsystemViewsPath(docsDir);
|
|
78
84
|
const expected = await expectedViews(docsDir, txn.tree);
|
|
79
|
-
|
|
80
|
-
//
|
|
81
|
-
//
|
|
82
|
-
//
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
85
|
+
// OWNERSHIP BEFORE ANYTHING IS STAGED, and the WHOLE verb is refused.
|
|
86
|
+
//
|
|
87
|
+
// The guard first shipped in `../sync.ts` alone, and this window is the
|
|
88
|
+
// other writer of exactly the same two reserved paths — for `subsystem
|
|
89
|
+
// new`, `rm`, `move` and `rename` at once. Reproduced before this call
|
|
90
|
+
// existed: with a hand-authored `architecture/services.likec4` in the tree,
|
|
91
|
+
// `loam subsystem new platform` printed "architecture/services.likec4
|
|
92
|
+
// updated" and destroyed it, first line and all. One predicate, one
|
|
93
|
+
// sentence, shared from beneath every writer
|
|
94
|
+
// (`core/repo/tree/render/service/header.ts`).
|
|
95
|
+
//
|
|
96
|
+
// WHY THE WHOLE OPERATION AND NOT JUST THE FILE. `sync` can decline one file
|
|
97
|
+
// and still do the rest, because nothing else it writes depends on that file
|
|
98
|
+
// and every part of it is idempotent. This window cannot: its entire reason
|
|
99
|
+
// for existing is that the generated files land in the SAME commit as the
|
|
100
|
+
// change to the tree, so completing the structural half and reporting the
|
|
101
|
+
// file as refused would MANUFACTURE the `subsystem.views-stale` state the
|
|
102
|
+
// banner above says this window refuses to leave between two commits — and
|
|
103
|
+
// it would do so irreversibly, since `move` and `rename` also stage their
|
|
104
|
+
// renames in git. A half-done move nobody asked for is worse than a refusal
|
|
105
|
+
// a person can act on, and a refusal is re-runnable the moment the authored
|
|
106
|
+
// file is out of the reserved path.
|
|
107
|
+
//
|
|
108
|
+
// It runs AFTER `plan`, which writes nothing and has already answered the
|
|
109
|
+
// verb-specific refusals (`unknown-target`, `subsystem-not-empty`,
|
|
110
|
+
// `move-uncommitted`): a caller whose name is wrong AND whose tree holds an
|
|
111
|
+
// authored views file is better told about the name.
|
|
112
|
+
const authored = await authoredGeneratedPaths([expected.subsystems.path, expected.services.path]);
|
|
113
|
+
if (authored.length > 0) {
|
|
114
|
+
fail(json,
|
|
115
|
+
// The code is `already-exists` and not a new one because it is exactly
|
|
116
|
+
// what that code has always meant here — "refusing to create over
|
|
117
|
+
// something that already exists, because overwriting would destroy work
|
|
118
|
+
// nobody asked to lose" — and it is already this command's documented
|
|
119
|
+
// refusal for the flat namespace. A second code for the same sentence
|
|
120
|
+
// would be a second branch an agent has to learn for no new decision.
|
|
121
|
+
"already-exists", authored.map((path) => authoredRefusal(repoPath(docsDir, path))).join(" ") +
|
|
122
|
+
" NOTHING was written and nothing was renamed: this command lands the generated views file in the same " +
|
|
123
|
+
"commit as the change to the tree, so it refuses whole rather than leaving the tree changed — and, for " +
|
|
124
|
+
"`move` and `rename`, renames staged in git — with a views file it may not write.");
|
|
125
|
+
return;
|
|
89
126
|
}
|
|
127
|
+
const writes = [...txn.writes];
|
|
128
|
+
// BOTH generated files, in the ONE commit that changes the tree. The
|
|
129
|
+
// per-service file is not a bystander here: its view titles are
|
|
130
|
+
// `<subsystem path> / <service id>`, so every `move` and `rename` restales
|
|
131
|
+
// it exactly as it restales the subsystem views, and leaving it for a later
|
|
132
|
+
// `sync` is the failing-between-two-commits state this window exists to
|
|
133
|
+
// refuse.
|
|
134
|
+
const views = {
|
|
135
|
+
subsystems: await stageGenerated(expected.subsystems, writes, false),
|
|
136
|
+
// PROTECTED when the map did not parse as a project, and only this file
|
|
137
|
+
// is: `renderSubsystemViews` answers null off the TREE, which is readable
|
|
138
|
+
// either way, while `renderServiceOverviews` answers null whenever no
|
|
139
|
+
// service resolves to an element — which is what an unreadable map looks
|
|
140
|
+
// like from here. Under `known: false` a null render is therefore not
|
|
141
|
+
// evidence of absence, and writing it would delete the whole file over a
|
|
142
|
+
// sibling document that does not parse. A verb still rewrites a file it
|
|
143
|
+
// CAN render (the tolerant arm this window has always taken), and the
|
|
144
|
+
// person is told by the note at the foot of this function.
|
|
145
|
+
services: await stageGenerated(expected.services, writes, !expected.known),
|
|
146
|
+
};
|
|
90
147
|
const staged = await stageWrites(writes);
|
|
91
148
|
const committed = await commitStaged({
|
|
92
149
|
root: docsDir,
|
|
@@ -114,7 +171,26 @@ export async function commitWindow(docsDir, json, plan) {
|
|
|
114
171
|
console.log(LANDSCAPE_UNREADABLE_NOTE);
|
|
115
172
|
}
|
|
116
173
|
finally {
|
|
117
|
-
await
|
|
174
|
+
await opened.release();
|
|
118
175
|
}
|
|
119
176
|
}
|
|
177
|
+
/**
|
|
178
|
+
* Compare ONE generated file against what it must say, append the write when
|
|
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.
|
|
182
|
+
*
|
|
183
|
+
* `protect` is the caller's statement that a NULL render is not evidence: the
|
|
184
|
+
* file is left exactly as it is rather than deleted. It is never a licence to
|
|
185
|
+
* skip a real rewrite — a file loam can render is rendered, which is the
|
|
186
|
+
* difference between this window and `../sync.ts`'s `blocked`.
|
|
187
|
+
*/
|
|
188
|
+
async function stageGenerated(file, writes, protect) {
|
|
189
|
+
const current = existsSync(file.path) ? await readFile(file.path, "utf8") : null;
|
|
190
|
+
const planned = planGenerated(file, current);
|
|
191
|
+
if (planned.write === null || (protect && file.content === null))
|
|
192
|
+
return "current";
|
|
193
|
+
writes.push(planned.write);
|
|
194
|
+
return planned.action;
|
|
195
|
+
}
|
|
120
196
|
//# sourceMappingURL=txn.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"txn.js","sourceRoot":"","sources":["../../../../src/commands/subsystem/txn/txn.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA4BG;AACH,OAAO,EAAE,UAAU,EAAE,MAAM,SAAS,CAAC;AACrC,OAAO,EAAE,QAAQ,EAAE,MAAM,kBAAkB,CAAC;AAE5C,OAAO,EAAE,IAAI,EAAkB,MAAM,gCAAgC,CAAC;AACtE,OAAO,EAAE,kBAAkB,EAAE,MAAM,6BAA6B,CAAC;AACjE,OAAO,EAAE,aAAa,EAAE,MAAM,4BAA4B,CAAC;AAE3D,OAAO,EAAE,WAAW,EAAE,MAAM,iCAAiC,CAAC;AAC9D,OAAO,EAAuB,sBAAsB,EAAE,MAAM,sCAAsC,CAAC;AACnG,OAAO,EAAE,eAAe,EAAE,aAAa,EAAE,MAAM,+BAA+B,CAAC;AAC/E,OAAO,EAAE,wBAAwB,EAAE,MAAM,2CAA2C,CAAC;AACrF,OAAO,EAAE,YAAY,EAAE,MAAM,0CAA0C,CAAC;AACxE,OAAO,EAAqB,MAAM,iCAAiC,CAAC;AACpE,OAAO,EAAE,UAAU,EAAE,MAAM,yCAAyC,CAAC;AACrE,OAAO,EAAE,aAAa,EAAE,yBAAyB,EAAE,MAAM,YAAY,CAAC;AAItE,4EAA4E;AAC5E,MAAM,UAAU,WAAW,CAAC,KAAkB;IAC5C,OAAO,KAAK,KAAK,SAAS,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,qCAAqC,KAAK,EAAE,CAAC;AACjF,CAAC;AAqBD;;;;GAIG;AACH,MAAM,CAAC,KAAK,UAAU,YAAY,CAChC,OAAgB,EAChB,IAAa,EACb,IAAuD;IAEvD,IAAI,WAAgC,CAAC;IACrC,IAAI,CAAC;QACH,WAAW,GAAG,MAAM,eAAe,CAAC,OAAO,CAAC,CAAC;IAC/C,CAAC;IAAC,OAAO,GAAG,EAAE,CAAC;QACb,IAAI,CAAC,CAAC,GAAG,YAAY,aAAa,CAAC;YAAE,MAAM,GAAG,CAAC;QAC/C,IAAI,CAAC,IAAI,EAAE,WAAW,EAAE,GAAG,CAAC,OAAO,CAAC,CAAC;QACrC,OAAO;IACT,CAAC;IACD,IAAI,CAAC;QACH,IAAI,SAAgC,CAAC;QACrC,IAAI,CAAC;YACH,SAAS,GAAG,MAAM,wBAAwB,CAAC,OAAO,CAAC,CAAC;QACtD,CAAC;QAAC,OAAO,GAAG,EAAE,CAAC;YACb,IAAI,CAAC,CAAC,GAAG,YAAY,sBAAsB,CAAC;gBAAE,MAAM,GAAG,CAAC;YACxD,IAAI,CAAC,IAAI,EAAE,oBAAoB,EAAE,GAAG,CAAC,OAAO,CAAC,CAAC;YAC9C,OAAO;QACT,CAAC;QACD,MAAM,GAAG,GAAG,MAAM,IAAI,CAAC,MAAM,aAAa,CAAC,OAAO,CAAC,CAAC,CAAC;QACrD,IAAI,GAAG,KAAK,IAAI;YAAE,OAAO;QAEzB,MAAM,IAAI,GAAG,kBAAkB,CAAC,OAAO,CAAC,CAAC;QACzC,MAAM,QAAQ,GAAG,MAAM,aAAa,CAAC,OAAO,EAAE,GAAG,CAAC,IAAI,CAAC,CAAC;QACxD,MAAM,KAAK,GAAG,QAAQ,CAAC,OAAO,KAAK,IAAI,CAAC,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC,MAAM,CAAC,IAAI,CAAC,QAAQ,CAAC,OAAO,EAAE,MAAM,CAAC,CAAC;QACvF,yEAAyE;QACzE,2EAA2E;QAC3E,4DAA4D;QAC5D,MAAM,OAAO,GAAG,UAAU,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC,MAAM,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC,CAAC,CAAC,CAAC,IAAI,CAAC;QACvE,IAAI,KAAK,GAAgB,SAAS,CAAC;QACnC,MAAM,MAAM,GAAG,CAAC,GAAG,GAAG,CAAC,MAAM,CAAC,CAAC;QAC/B,IAAI,CAAC,UAAU,CAAC,OAAO,EAAE,QAAQ,CAAC,OAAO,CAAC,EAAE,CAAC;YAC3C,KAAK,GAAG,OAAO,KAAK,IAAI,CAAC,CAAC,CAAC,SAAS,CAAC,CAAC,CAAC,KAAK,KAAK,IAAI,CAAC,CAAC,CAAC,SAAS,CAAC,CAAC,CAAC,SAAS,CAAC;YAC9E,MAAM,CAAC,IAAI,CAAC,EAAE,IAAI,EAAE,OAAO,EAAE,KAAK,EAAE,CAAC,CAAC;QACxC,CAAC;QACD,MAAM,MAAM,GAAG,MAAM,WAAW,CAAC,MAAM,CAAC,CAAC;QACzC,MAAM,SAAS,GAAG,MAAM,YAAY,CAClC;YACE,IAAI,EAAE,OAAO;YACb,OAAO,EAAE,WAAW;YACpB,KAAK,EAAE,qBAAqB;YAC5B,MAAM,EAAE,GAAG,CAAC,MAAM;YAClB,GAAG,CAAC,GAAG,CAAC,KAAK,KAAK,SAAS,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,EAAE,KAAK,EAAE,GAAG,CAAC,KAAK,EAAE,CAAC;SACzD,EACD,MAAM,EACN,GAAG,CAAC,IAAI,CACT,CAAC;QACF,IAAI,CAAC,SAAS,CAAC,EAAE,EAAE,CAAC;YAClB,MAAM,IAAI,GACR,SAAS,CAAC,IAAI,KAAK,qBAAqB;gBACtC,CAAC,CAAC,SAAS,CAAC,IAAI;gBAChB,CAAC,CAAC,SAAS,CAAC,KAAK;oBACf,CAAC,CAAC,CAAC,GAAG,CAAC,SAAS,IAAI,GAAG,CAAC,UAAU,IAAI,SAAS,CAAC,IAAI,CAAC;oBACrD,CAAC,CAAC,CAAC,GAAG,CAAC,UAAU,IAAI,SAAS,CAAC,IAAI,CAAC,CAAC;YAC3C,IAAI,CAAC,IAAI,EAAE,IAAI,EAAE,SAAS,CAAC,OAAO,CAAC,CAAC;YACpC,OAAO;QACT,CAAC;QACD,MAAM,GAAG,CAAC,MAAM,CAAC,KAAK,EAAE,SAAS,CAAC,CAAC;QACnC,0EAA0E;QAC1E,8EAA8E;QAC9E,6EAA6E;QAC7E,uEAAuE;QACvE,wEAAwE;QACxE,IAAI,CAAC,IAAI,IAAI,CAAC,QAAQ,CAAC,KAAK;YAAE,OAAO,CAAC,GAAG,CAAC,yBAAyB,CAAC,CAAC;IACvE,CAAC;YAAS,CAAC;QACT,MAAM,WAAW,EAAE,CAAC;IACtB,CAAC;AACH,CAAC","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 the views file from the\n * PLANNED post-state, commit through the journaled transaction (directory\n * renames included), report. One window, because the views file must land in\n * the same commit as whatever changed the tree — a marker without its view,\n * or renames without theirs, is exactly the `subsystem.views-stale` state\n * the generated file's contract refuses to leave between two commits.\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 */\nimport { existsSync } from \"node:fs\";\nimport { readFile } from \"node:fs/promises\";\nimport type { DocsDir } from \"../../../core/kernel/ids/dirs.js\";\nimport { fail, type ErrorCode } from \"../../../core/envelope/json.js\";\nimport { subsystemViewsPath } from \"../../../core/repo/paths.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 { viewsAgree } from \"../../../core/repo/tree/render/views.js\";\nimport { expectedViews, LANDSCAPE_UNREADABLE_NOTE } from \"./views.js\";\n\nexport type ViewsAction = \"created\" | \"updated\" | \"removed\" | \"current\";\n\n/** The text tail a verb's success line carries about the generated file. */\nexport function reportViews(views: ViewsAction): string {\n return views === \"current\" ? \"\" : ` — architecture/subsystems.likec4 ${views}`;\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: ViewsAction, 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 path = subsystemViewsPath(docsDir);\n const expected = await expectedViews(docsDir, txn.tree);\n const bytes = expected.content === null ? null : Buffer.from(expected.content, \"utf8\");\n // Content, not bytes — `viewsAgree` carries the reason. Without it every\n // subsystem move on a Windows clone folded a rewrite of this file into its\n // transaction that changed not one fact, and journalled it.\n const current = existsSync(path) ? await readFile(path, \"utf8\") : null;\n let views: ViewsAction = \"current\";\n const writes = [...txn.writes];\n if (!viewsAgree(current, expected.content)) {\n views = current === null ? \"created\" : bytes === null ? \"removed\" : \"updated\";\n writes.push({ path, content: bytes });\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"]}
|
|
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,20 +1,32 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* What the generated views
|
|
3
|
-
* writer and `subsystem sync` render from
|
|
4
|
-
*
|
|
5
|
-
*
|
|
6
|
-
*
|
|
2
|
+
* What the generated views files MUST say — the shared answer every subsystem
|
|
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.
|
|
8
|
+
*
|
|
9
|
+
* BOTH generated files come out of ONE read of the map, and that is the reason
|
|
10
|
+
* this module answers about the pair rather than about one file: they are
|
|
11
|
+
* rendered from the same parsed `architecture/` project, they are repaired by
|
|
12
|
+
* the same command, and they must land in the same commit — a verb that changed
|
|
13
|
+
* a service's placement changed a subsystem view's members AND that service's
|
|
14
|
+
* overview TITLE (`<subsystem path> / <service id>`), so shipping one without
|
|
15
|
+
* the other is the "stale between two commits" state the generated files'
|
|
16
|
+
* contract exists to refuse. Reading the project twice would also parse the
|
|
17
|
+
* whole fleet map twice per run for one answer.
|
|
7
18
|
*/
|
|
8
19
|
import { existsSync } from "node:fs";
|
|
9
20
|
import { relative, resolve, sep } from "node:path";
|
|
10
21
|
import { loadFile } from "../../../core/c4/likec4.js";
|
|
11
22
|
import { architectureProjectDocuments, loadArchitecture } from "../../../core/c4/project/architecture.js";
|
|
12
23
|
import { serviceDirOf } from "../../../core/kernel/ids/dirs.js";
|
|
13
|
-
import { landscapePath } from "../../../core/repo/paths.js";
|
|
14
|
-
import { renderSubsystemViews } from "../../../core/repo/tree/render/views.js";
|
|
24
|
+
import { landscapePath, serviceViewsPath, subsystemViewsPath } from "../../../core/repo/paths.js";
|
|
25
|
+
import { renderServiceOverviews, renderSubsystemViews, viewsAgree, } from "../../../core/repo/tree/render/views.js";
|
|
15
26
|
/**
|
|
16
|
-
* The elements and declared global style ids of the whole
|
|
17
|
-
* PROJECT — the member join,
|
|
27
|
+
* The elements, the relationships and the declared global style ids of the whole
|
|
28
|
+
* `architecture/` PROJECT — the member join, the edges every per-service view
|
|
29
|
+
* computes its peer set from, and the one line a view may borrow a palette
|
|
18
30
|
* with — with the provenance of that answer beside it.
|
|
19
31
|
*
|
|
20
32
|
* The PROJECT, not the landscape file alone, and that is a correctness rule
|
|
@@ -54,8 +66,11 @@ import { renderSubsystemViews } from "../../../core/repo/tree/render/views.js";
|
|
|
54
66
|
*/
|
|
55
67
|
export async function landscapeFacts(docsDir) {
|
|
56
68
|
const path = landscapePath(docsDir);
|
|
69
|
+
// No map at all: no elements and NO EDGES, said outright. `relationships` is
|
|
70
|
+
// required of every `MapFacts` precisely so an empty answer has to be written
|
|
71
|
+
// down rather than dropped — see the type.
|
|
57
72
|
if (!existsSync(path))
|
|
58
|
-
return { facts: { elements: [] }, known: true };
|
|
73
|
+
return { facts: { elements: [], relationships: [] }, known: true };
|
|
59
74
|
try {
|
|
60
75
|
const doc = await loadArchitecture(docsDir);
|
|
61
76
|
// The whole document on success, so the render reads the same record
|
|
@@ -81,7 +96,23 @@ export async function landscapeFacts(docsDir) {
|
|
|
81
96
|
// cannot merge them (that is what failed one line up), but a document that
|
|
82
97
|
// parses alone certainly declares the style groups it declares.
|
|
83
98
|
if (file.errors.length === 0) {
|
|
84
|
-
|
|
99
|
+
// THE LANDSCAPE'S OWN RELATIONSHIPS TRAVEL WITH ITS ELEMENTS. This arm
|
|
100
|
+
// composes a record rather than handing the document across, because the
|
|
101
|
+
// style census below is widened past what this one document declares — and
|
|
102
|
+
// the first version of it listed the fields it happened to be thinking
|
|
103
|
+
// about and dropped the edges the document was carrying all along. That is
|
|
104
|
+
// silent: every per-service view then renders with no peers, the service
|
|
105
|
+
// and its own interior and nothing it touches, and no check anywhere says
|
|
106
|
+
// so. `MapFacts` now REQUIRES the field for exactly this reason; the copy
|
|
107
|
+
// stays explicit so the widening above it stays visible.
|
|
108
|
+
return {
|
|
109
|
+
facts: {
|
|
110
|
+
elements: file.elements,
|
|
111
|
+
relationships: file.relationships,
|
|
112
|
+
globalStyles: await declaredStyles(docsDir, file),
|
|
113
|
+
},
|
|
114
|
+
known: false,
|
|
115
|
+
};
|
|
85
116
|
}
|
|
86
117
|
}
|
|
87
118
|
catch {
|
|
@@ -127,9 +158,48 @@ async function declaredStyles(docsDir, landscape) {
|
|
|
127
158
|
}
|
|
128
159
|
return [...ids].sort();
|
|
129
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
|
+
}
|
|
130
187
|
export async function expectedViews(docsDir, tree) {
|
|
131
188
|
const read = await landscapeFacts(docsDir);
|
|
132
|
-
|
|
189
|
+
// The parsed DOCUMENT goes across, never a literal composed here: `MapFacts`
|
|
190
|
+
// carries the relationships the per-service peer sets are computed from, and a
|
|
191
|
+
// caller that dropped them would render every service view peerless — the
|
|
192
|
+
// service and its interior, and nothing it touches — with no error anywhere.
|
|
193
|
+
// The type now REQUIRES the field so that mistake cannot be made silently
|
|
194
|
+
// (`render/views.ts` records what it cost); this last-resort literal is the one
|
|
195
|
+
// place an empty answer is the TRUE one — loam could not read the map at
|
|
196
|
+
// all — and it says so rather than leaving the field off.
|
|
197
|
+
const map = read.facts ?? { elements: [], relationships: [] };
|
|
198
|
+
return {
|
|
199
|
+
known: read.known,
|
|
200
|
+
subsystems: { path: subsystemViewsPath(docsDir), content: renderSubsystemViews(tree, map) },
|
|
201
|
+
services: { path: serviceViewsPath(docsDir), content: renderServiceOverviews(tree, map) },
|
|
202
|
+
};
|
|
133
203
|
}
|
|
134
204
|
/**
|
|
135
205
|
* The one line a person is owed when the map cannot be read — the same sentence
|
|
@@ -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 file 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 */\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 } from \"../../../core/repo/paths.js\";\nimport { type MapFacts, renderSubsystemViews } 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 and declared global style ids of the whole `architecture/`\n * PROJECT — the member join, 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 if (!existsSync(path)) return { facts: { elements: [] }, 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 return { facts: { elements: file.elements, globalStyles: await declaredStyles(docsDir, file) }, known: false };\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 * The bytes the views file must hold for this tree, and whether the map they\n * 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 */\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.\n */\n known: boolean;\n /** The bytes the file must hold, or null for \"must be absent\". */\n content: string | null;\n}\n\nexport async function expectedViews(docsDir: DocsDir, tree: FleetTree): Promise<ExpectedViews> {\n const read = await landscapeFacts(docsDir);\n return { known: read.known, content: renderSubsystemViews(tree, read.facts ?? { elements: [] }) };\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"]}
|
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
/** How many units one service shows before the rest are elided behind its own lens. */
|
|
2
|
+
const PER_SERVICE_LIMIT = 12;
|
|
3
|
+
/** `path:line`, or just the path — a source with no line is still worth opening. */
|
|
4
|
+
function at(unit) {
|
|
5
|
+
const source = unit.source;
|
|
6
|
+
if (source === undefined)
|
|
7
|
+
return "";
|
|
8
|
+
return source.line === undefined ? ` (${source.path})` : ` (${source.path}:${source.line})`;
|
|
9
|
+
}
|
|
10
|
+
function line(unit) {
|
|
11
|
+
return ` [${unit.at}] ${unit.kind} ${unit.claim}${at(unit)}`;
|
|
12
|
+
}
|
|
13
|
+
export function renderPacket(packet) {
|
|
14
|
+
console.log(`${packet.feature} — ${packet.counts.open} open · ${packet.counts.done} done · ${packet.counts.attested} attested`);
|
|
15
|
+
console.log(` record: ${packet.record} · stamps: ${packet.stamps}${packet.service === null ? "" : ` · lens: ${packet.service}`}`);
|
|
16
|
+
// The blind spots FIRST, and before any list: an empty packet over a document
|
|
17
|
+
// nobody could read is the one output an implementer must never take at face
|
|
18
|
+
// value, and a warning under fifty lines of units is a warning nobody reads.
|
|
19
|
+
for (const warning of blindSpots(packet))
|
|
20
|
+
console.log(` ⚠ ${warning}`);
|
|
21
|
+
console.log("");
|
|
22
|
+
const subjects = [...new Set(packet.units.map((unit) => unit.subject))];
|
|
23
|
+
if (packet.units.length === 0) {
|
|
24
|
+
console.log(packet.counts.attested > 0 || packet.counts.done > 0
|
|
25
|
+
? " Nothing open. Every unit is recorded confirmed, or attested and waiting on a run."
|
|
26
|
+
: " Nothing open — this feature's delta, specs and contracts promise nothing yet.");
|
|
27
|
+
}
|
|
28
|
+
for (const subject of subjects) {
|
|
29
|
+
const units = packet.units.filter((unit) => unit.subject === subject);
|
|
30
|
+
console.log(` ${subject}`);
|
|
31
|
+
for (const unit of units.slice(0, PER_SERVICE_LIMIT))
|
|
32
|
+
console.log(line(unit));
|
|
33
|
+
if (units.length > PER_SERVICE_LIMIT) {
|
|
34
|
+
console.log(` +${units.length - PER_SERVICE_LIMIT} more — loam tasks ${packet.feature} --service ${subject} --json`);
|
|
35
|
+
}
|
|
36
|
+
console.log("");
|
|
37
|
+
}
|
|
38
|
+
if (packet.frontier.length > 0) {
|
|
39
|
+
console.log(` frontier: ${packet.frontier.join(", ")}${packet.frontierElided > 0 ? ` (+${packet.frontierElided})` : ""}`);
|
|
40
|
+
}
|
|
41
|
+
for (const waiting of packet.waitingOn ?? []) {
|
|
42
|
+
console.log(` waiting on ${waiting.subject}: ${waiting.id} ${waiting.claim}`);
|
|
43
|
+
}
|
|
44
|
+
console.log("\n Review:");
|
|
45
|
+
for (const note of packet.review)
|
|
46
|
+
console.log(` - ${note}`);
|
|
47
|
+
}
|
|
48
|
+
/**
|
|
49
|
+
* The leaf, in full. The follow-up command is literal rather than a shape to
|
|
50
|
+
* fill in, because the reader is usually a model cheap enough that "the
|
|
51
|
+
* appropriate verify invocation" is a guess it will get wrong.
|
|
52
|
+
*/
|
|
53
|
+
export function renderNext(packet) {
|
|
54
|
+
const unit = packet.units[0];
|
|
55
|
+
if (unit === undefined) {
|
|
56
|
+
console.log(`${packet.feature} — nothing open (${packet.counts.done} done · ${packet.counts.attested} attested)`);
|
|
57
|
+
console.log(` record: ${packet.record} · stamps: ${packet.stamps}`);
|
|
58
|
+
for (const warning of blindSpots(packet))
|
|
59
|
+
console.log(` ⚠ ${warning}`);
|
|
60
|
+
return;
|
|
61
|
+
}
|
|
62
|
+
console.log(`${packet.feature} — next: ${unit.id}`);
|
|
63
|
+
for (const warning of blindSpots(packet))
|
|
64
|
+
console.log(` ⚠ ${warning}`);
|
|
65
|
+
console.log("");
|
|
66
|
+
console.log(` [${unit.at}] ${unit.kind} [${unit.subject}]`);
|
|
67
|
+
console.log(` ${unit.claim}`);
|
|
68
|
+
const source = unit.source;
|
|
69
|
+
if (source !== undefined) {
|
|
70
|
+
console.log(` ${source.path}${source.line === undefined ? "" : `:${source.line}`}`);
|
|
71
|
+
// Verbatim, never reflowed: these are the bytes the scenario digest was
|
|
72
|
+
// taken over, and a test written from reworded criteria answers a claim
|
|
73
|
+
// nobody made.
|
|
74
|
+
if ("lines" in source)
|
|
75
|
+
for (const body of source.lines)
|
|
76
|
+
console.log(` ${body}`);
|
|
77
|
+
}
|
|
78
|
+
if (unit.dependsOn.length > 0)
|
|
79
|
+
console.log(` depends on: ${unit.dependsOn.join(", ")}`);
|
|
80
|
+
console.log(`\n done: ${unit.done.check}`);
|
|
81
|
+
console.log(`\n ${followUp(packet.feature, unit)}`);
|
|
82
|
+
}
|
|
83
|
+
/**
|
|
84
|
+
* The command that closes this unit.
|
|
85
|
+
*
|
|
86
|
+
* A `describe` unit is the one arm that is NOT a `loam verify --record`: there
|
|
87
|
+
* is no claim to answer, because a requirement with no scenario asks no
|
|
88
|
+
* question — the work is writing the Given/When/Then into the file named above,
|
|
89
|
+
* and `loam validate --feature` is what grades that it landed. Pointing it at
|
|
90
|
+
* `--record` would invite an answer to a question the checklist does not carry.
|
|
91
|
+
*/
|
|
92
|
+
function followUp(feature, unit) {
|
|
93
|
+
if (unit.at === "describe")
|
|
94
|
+
return `loam validate --feature ${feature}`;
|
|
95
|
+
if (unit.at === "generate")
|
|
96
|
+
return `loam gherkin ${feature}`;
|
|
97
|
+
if (unit.kind === "scenario.tested") {
|
|
98
|
+
return `loam verify ${feature} --service ${unit.subject} --results report.json`;
|
|
99
|
+
}
|
|
100
|
+
return `loam verify ${feature} --record answers.json --service ${unit.subject}`;
|
|
101
|
+
}
|
|
102
|
+
/** What the packet could not look at — one sentence per document class. */
|
|
103
|
+
function blindSpots(packet) {
|
|
104
|
+
const out = [];
|
|
105
|
+
if (packet.unreadable.architecture !== undefined) {
|
|
106
|
+
out.push(`delta.likec4 does not parse (${packet.unreadable.architecture}) — no c4.calls unit could be derived, ` +
|
|
107
|
+
"so this list is short because loam could not look, not because the work is done");
|
|
108
|
+
}
|
|
109
|
+
const contracts = packet.unreadable.contracts;
|
|
110
|
+
if (contracts !== undefined) {
|
|
111
|
+
out.push(`contract delta unreadable for ${contracts.join(", ")} — operations and messages are missing from this packet, ` +
|
|
112
|
+
"not absent from the feature");
|
|
113
|
+
}
|
|
114
|
+
return out;
|
|
115
|
+
}
|
|
116
|
+
//# sourceMappingURL=render.js.map
|