@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 +1 @@
|
|
|
1
|
-
{"version":3,"file":"intent.js","sourceRoot":"","sources":["../../../../src/core/staging/recovery/intent.ts"],"names":[],"mappings":"AAAA;;;;;;;;;GASG;AACH,OAAO,EAAE,UAAU,EAAE,MAAM,SAAS,CAAC;AACrC,OAAO,EAAE,QAAQ,EAAE,MAAM,kBAAkB,CAAC;AAC5C,OAAO,EAAE,QAAQ,EAAE,MAAM,SAAS,CAAC;AACnC,OAAO,EAAE,IAAI,EAAE,MAAM,WAAW,CAAC;AACjC,OAAO,EAAE,QAAQ,EAAE,MAAM,wBAAwB,CAAC;AAClD,OAAO,EAAE,YAAY,EAAE,MAAM,6BAA6B,CAAC;AAC3D,OAAO,EAAE,aAAa,EAAE,YAAY,EAAE,MAAM,mBAAmB,CAAC;AAChE,OAAO,EAAE,QAAQ,EAAE,MAAM,EAAoB,MAAM,cAAc,CAAC;AAClE,OAAO,EAAE,OAAO,EAAE,MAAM,cAAc,CAAC;AAEvC,+EAA+E;AAC/E,MAAM,qBAAqB,GAAG,CAAC,CAAC;AAiChC
|
|
1
|
+
{"version":3,"file":"intent.js","sourceRoot":"","sources":["../../../../src/core/staging/recovery/intent.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * The `.loam-commit` journal — written before the first swap, cleared after the\n * last — and the record that makes an interrupted commit detectable at all.\n *\n * Reading and writing the record is kept apart from acting on it\n * (`./recover.ts`) because a corrupt record has to stay describable: `doctor`\n * reports a `.loam-commit` it cannot parse, and a module that parsed and\n * repaired in one step would have no way to say that without also attempting\n * the repair.\n */\nimport { existsSync } from \"node:fs\";\nimport { readFile } from \"node:fs/promises\";\nimport { hostname } from \"node:os\";\nimport { join } from \"node:path\";\nimport { repoPath } from \"../../envelope/json.js\";\nimport { isPathInside } from \"../../kernel/path-safety.js\";\nimport { COMMIT_INTENT, writeDurable } from \"../interrupted.js\";\nimport { isDigest, sha256, type StagedWrite } from \"../writes.js\";\nimport { quietRm } from \"../commit.js\";\n\n/** The version-1 record: archive's and unarchive's snapshot-backed journal. */\nconst COMMIT_INTENT_VERSION = 1;\n\nexport interface CommitIntentFile {\n /** Docs-repo-relative path, forward slashes. */\n path: string;\n /** sha256 of the bytes before the swap; null when the swap creates the file. */\n before: string | null;\n /** sha256 of the bytes the swap writes; null when the swap deletes the file. */\n after: string | null;\n}\n\nexport interface CommitIntent {\n version: number;\n /** Which command was committing — the sentence a human reads, not a branch. */\n command: \"archive\" | \"unarchive\";\n /**\n * The state a repair returns the docs to. `archive` undoes (its rollback path\n * would have restored the pre-images anyway); `unarchive` finishes (its target\n * bytes ARE the pre-images, and the merged text it was replacing is recorded\n * nowhere else).\n */\n restore: \"before\" | \"after\";\n pid: number;\n host: string;\n at: string;\n feature: string;\n /** Docs-repo-relative feature directory the commit's final move takes FROM. */\n moveFrom: string;\n /** …and TO. Both landed ⇒ every earlier step landed too. */\n moveTo: string;\n files: CommitIntentFile[];\n}\n\n/**\n * Write the intent record and get it onto the disk before the first swap.\n *\n * fsync, not just write — through `writeDurable`, whose doc says why: the\n * point of the record is to survive the crash that interrupts the commit.\n */\nexport async function writeCommitIntent(\n docsDir: string,\n spec: {\n command: CommitIntent[\"command\"];\n restore: CommitIntent[\"restore\"];\n feature: string;\n moveFrom: string;\n moveTo: string;\n },\n staged: StagedWrite[],\n): Promise<void> {\n const intent: CommitIntent = {\n version: COMMIT_INTENT_VERSION,\n command: spec.command,\n restore: spec.restore,\n pid: process.pid,\n host: hostname(),\n at: new Date().toISOString(),\n feature: spec.feature,\n moveFrom: repoPath(docsDir, spec.moveFrom),\n moveTo: repoPath(docsDir, spec.moveTo),\n files: staged.map((s) => ({\n path: repoPath(docsDir, s.write.path),\n before: s.before === null ? null : sha256(s.before),\n after: s.content === null ? null : sha256(s.content),\n })),\n };\n await writeDurable(join(docsDir, COMMIT_INTENT), JSON.stringify(intent, null, 2) + \"\\n\");\n}\n\n/** The commit finished (or was rolled back): there is nothing left to recover from. */\nexport async function clearCommitIntent(docsDir: string): Promise<void> {\n await quietRm(join(docsDir, COMMIT_INTENT));\n}\n\n/** The intent record as written, or null when it is absent or unreadable. */\nexport async function readCommitIntent(docsDir: string): Promise<CommitIntent | null> {\n const path = join(docsDir, COMMIT_INTENT);\n if (!existsSync(path)) return null;\n let parsed: unknown;\n try {\n parsed = JSON.parse(await readFile(path, \"utf8\"));\n } catch {\n return null;\n }\n const i = parsed as Partial<CommitIntent>;\n if (i === null || typeof i !== \"object\") return null;\n if (i.version !== COMMIT_INTENT_VERSION) return null;\n if (i.command !== \"archive\" && i.command !== \"unarchive\") return null;\n if (i.restore !== \"before\" && i.restore !== \"after\") return null;\n if (typeof i.feature !== \"string\" || typeof i.moveFrom !== \"string\" || typeof i.moveTo !== \"string\") return null;\n if (!Array.isArray(i.files)) return null;\n // The journal is untrusted input, and recovery writes over what it names —\n // the same containment the version-2 reader enforces, because an entry\n // escaping the docs repo would turn repair into an arbitrary write.\n const inside = (rel: string): boolean => isPathInside(join(docsDir), join(docsDir, ...rel.split(\"/\")));\n if (!inside(i.moveFrom) || !inside(i.moveTo)) return null;\n for (const f of i.files) {\n if (typeof f?.path !== \"string\") return null;\n if (!isDigestOrNull(f.before) || !isDigestOrNull(f.after)) return null;\n if (!inside(f.path)) return null;\n }\n return i as CommitIntent;\n}\n\nfunction isDigestOrNull(value: unknown): boolean {\n return value === null || isDigest(value);\n}\n\n"]}
|
|
@@ -10,6 +10,12 @@
|
|
|
10
10
|
* way, a file in NEITHER recorded state was written by somebody else since,
|
|
11
11
|
* so recovery refuses and names it rather than choosing which of two truths
|
|
12
12
|
* to destroy.
|
|
13
|
+
*
|
|
14
|
+
* The journaled writers reach it through `openCommitWindow` below — the lock,
|
|
15
|
+
* then this — so the two refusals that can keep a commit window shut are
|
|
16
|
+
* spelled once rather than once per writer. Archive and unarchive are the
|
|
17
|
+
* exception: they call it directly, inside their plan, under a lock they took
|
|
18
|
+
* themselves, and throw its refusal to a handler that maps it.
|
|
13
19
|
*/
|
|
14
20
|
import { existsSync } from "node:fs";
|
|
15
21
|
import { readFile, rm } from "node:fs/promises";
|
|
@@ -17,8 +23,9 @@ import { dirname, join } from "node:path";
|
|
|
17
23
|
import { repoPath } from "../../envelope/json.js";
|
|
18
24
|
import { sha256 } from "../writes.js";
|
|
19
25
|
import { atomicWrite, quietRmdir } from "../commit.js";
|
|
20
|
-
import { SNAPSHOT_DIR } from "../snapshot.js";
|
|
26
|
+
import { SNAPSHOT_DIR } from "../snapshot/manifest.js";
|
|
21
27
|
import { COMMIT_INTENT, InterruptedCommitError, sweepPidTemps, } from "../interrupted.js";
|
|
28
|
+
import { acquireDocsLock, acquireDocsLockWaiting, DocsBusyError, LOCK_WAIT_MS } from "../lock.js";
|
|
22
29
|
import { readTxnIntent } from "../txn/journal.js";
|
|
23
30
|
import { recoverForward } from "../txn/forward.js";
|
|
24
31
|
import { clearCommitIntent, readCommitIntent, } from "./intent.js";
|
|
@@ -123,4 +130,56 @@ async function finishRecovery(docsDir, intent) {
|
|
|
123
130
|
await sweepPidTemps(intent.files.map((f) => dirname(join(docsDir, ...f.path.split("/")))), intent.pid);
|
|
124
131
|
await clearCommitIntent(docsDir);
|
|
125
132
|
}
|
|
133
|
+
/**
|
|
134
|
+
* Open a writer's commit window: take the lock in `root` — the docs repo, or
|
|
135
|
+
* gherkin's emission root in the service repo — then refuse or repair a
|
|
136
|
+
* predecessor's interrupted commit before this run reads a byte.
|
|
137
|
+
*
|
|
138
|
+
* `waitMs` is how long to sit out a live holder before answering `docs-busy`,
|
|
139
|
+
* and 0 refuses on first contact; `acquireDocsLockWaiting` says which writers
|
|
140
|
+
* wait and why archive does not.
|
|
141
|
+
*
|
|
142
|
+
* Each writer here used to spell this sequence out by hand, and the copy in
|
|
143
|
+
* `verify` once let its recovery refusal keep the lock — one bad answer set
|
|
144
|
+
* became a wedged docs repo for everyone else. So the refusal releases HERE,
|
|
145
|
+
* before it is returned, and an error that is neither refusal releases before
|
|
146
|
+
* it propagates.
|
|
147
|
+
*/
|
|
148
|
+
export async function openCommitWindow(root, waitMs) {
|
|
149
|
+
let release;
|
|
150
|
+
try {
|
|
151
|
+
release = waitMs > 0 ? await acquireDocsLockWaiting(root, waitMs) : await acquireDocsLock(root);
|
|
152
|
+
}
|
|
153
|
+
catch (err) {
|
|
154
|
+
if (!(err instanceof DocsBusyError))
|
|
155
|
+
throw err;
|
|
156
|
+
return { ok: false, code: "docs-busy", message: err.message };
|
|
157
|
+
}
|
|
158
|
+
try {
|
|
159
|
+
return { ok: true, recovered: await recoverInterruptedCommit(root), release };
|
|
160
|
+
}
|
|
161
|
+
catch (err) {
|
|
162
|
+
await release();
|
|
163
|
+
if (!(err instanceof InterruptedCommitError))
|
|
164
|
+
throw err;
|
|
165
|
+
return { ok: false, code: "commit-interrupted", message: err.message };
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
/**
|
|
169
|
+
* Recover a predecessor's interrupted commit NOW and hand the lock straight
|
|
170
|
+
* back — for a writer whose slow half runs UNLOCKED over the very bytes a
|
|
171
|
+
* half-commit corrupts (vouch's verification, gherkin's reconcile); each says
|
|
172
|
+
* at its call site why recovery inside its own commit window comes too late.
|
|
173
|
+
* Cheap when there is nothing to do: no journal, no lock, no I/O beyond one
|
|
174
|
+
* existsSync.
|
|
175
|
+
*/
|
|
176
|
+
export async function recoverIfJournaled(root) {
|
|
177
|
+
if (!existsSync(join(root, COMMIT_INTENT)))
|
|
178
|
+
return { ok: true, recovered: null };
|
|
179
|
+
const opened = await openCommitWindow(root, LOCK_WAIT_MS);
|
|
180
|
+
if (!opened.ok)
|
|
181
|
+
return opened;
|
|
182
|
+
await opened.release();
|
|
183
|
+
return { ok: true, recovered: opened.recovered };
|
|
184
|
+
}
|
|
126
185
|
//# sourceMappingURL=recover.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"recover.js","sourceRoot":"","sources":["../../../../src/core/staging/recovery/recover.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * Refuse or repair an interrupted commit, before this run reads anything.\n *\n * Split from `./intent.ts` because this is the only module on the write path\n * that writes bytes nobody asked for in this run. Two journal versions, two\n * doctrines, one dispatch: a version-1 record (archive/unarchive) is repaired\n * BACKWARD from the snapshot pre-images, each verified against the digest the\n * record captured; a version-2 record is handed to `txn/forward.ts`, which\n * rolls FORWARD from the staged temps under the same verification. Either\n * way, a file in NEITHER recorded state was written by somebody else since,\n * so recovery refuses and names it rather than choosing which of two truths\n * to destroy.\n */\nimport { existsSync } from \"node:fs\";\nimport { readFile, rm } from \"node:fs/promises\";\nimport { dirname, join } from \"node:path\";\nimport { repoPath } from \"../../envelope/json.js\";\nimport { sha256 } from \"../writes.js\";\nimport { atomicWrite, quietRmdir } from \"../commit.js\";\nimport { SNAPSHOT_DIR } from \"../snapshot.js\";\nimport {\n COMMIT_INTENT,\n type CommitRecovery,\n InterruptedCommitError,\n sweepPidTemps,\n} from \"../interrupted.js\";\nimport { readTxnIntent } from \"../txn/journal.js\";\nimport { recoverForward } from \"../txn/forward.js\";\nimport {\n clearCommitIntent,\n readCommitIntent,\n type CommitIntent,\n type CommitIntentFile,\n} from \"./intent.js\";\n\n/**\n * Refuse or repair before this run reads anything. Returns null when there is\n * nothing to recover.\n *\n * Repair is only ever the bytes the interrupted run already had on disk: each\n * file still in the wrong state is rewritten from the snapshot pre-image and\n * only after that pre-image's own sha256 matches the digest the record captured\n * before the crash. A file in NEITHER state was written by somebody else since,\n * so this refuses and names it rather than choosing which of two truths to\n * destroy — the same doctrine `rollbackStaged` follows when it leaves a file as\n * found.\n */\nexport async function recoverInterruptedCommit(docsDir: string): Promise<CommitRecovery | null> {\n const path = join(docsDir, COMMIT_INTENT);\n if (!existsSync(path)) return null;\n const intent = await readCommitIntent(docsDir);\n if (intent === null) {\n // Version 2 — the smaller transaction's journal. Same file, different\n // recovery: forward from the staged temps, in txn/forward.ts.\n const txn = await readTxnIntent(docsDir);\n if (txn !== null) return recoverForward(docsDir, txn);\n throw new InterruptedCommitError(\n `${repoPath(docsDir, path)} records a commit this loam cannot read, so it cannot tell a half-merged docs repo ` +\n `from a healthy one. Check the living docs against version control, delete ${COMMIT_INTENT}, then re-run.`,\n );\n }\n const moveFrom = join(docsDir, ...intent.moveFrom.split(\"/\"));\n const moveTo = join(docsDir, ...intent.moveTo.split(\"/\"));\n // The move is the commit's LAST step. If it landed, every swap before it did\n // too, and only the record's own removal was lost.\n if (existsSync(moveTo) && !existsSync(moveFrom)) {\n await finishRecovery(docsDir, intent);\n return { command: intent.command, feature: intent.feature, outcome: \"completed\", repaired: [] };\n }\n\n const target = (rel: string): string => join(docsDir, ...rel.split(\"/\"));\n const wanted = (f: CommitIntentFile): string | null => (intent.restore === \"before\" ? f.before : f.after);\n const other = (f: CommitIntentFile): string | null => (intent.restore === \"before\" ? f.after : f.before);\n const pending: CommitIntentFile[] = [];\n const unknown: string[] = [];\n for (const f of intent.files) {\n const file = target(f.path);\n const now = existsSync(file) ? sha256(await readFile(file)) : null;\n if (now === wanted(f)) continue;\n if (now === other(f)) pending.push(f);\n else unknown.push(f.path);\n }\n if (unknown.length > 0) {\n throw new InterruptedCommitError(\n `a \\`loam ${intent.command} ${intent.feature}\\` was interrupted mid-commit (pid ${intent.pid} on ${intent.host}, ` +\n `${intent.at}) and ${unknown.length} of the file(s) it was writing now hold bytes that are neither what it ` +\n `found nor what it wrote — somebody edited them since: ${unknown.join(\", \")}. loam will not choose between ` +\n `those two truths. Reconcile them against version control, delete ${COMMIT_INTENT}, then re-run.`,\n );\n }\n if (pending.length === 0) {\n await finishRecovery(docsDir, intent);\n return { command: intent.command, feature: intent.feature, outcome: \"consistent\", repaired: [] };\n }\n\n // The pre-images travel inside the feature directory, so they are at one of\n // the two ends of the move depending on how far the interrupted run got.\n // Deliberately NOT the snapshot's (service, artifact) re-keying: the journal\n // and the snapshot were written seconds apart under the same docs lock, so\n // the journal's literal paths and the pre-image keys cannot have diverged —\n // a service directory moved by hand between the crash and this repair leaves\n // its files \"in neither recorded state\", which is the refusal below, not a\n // resolution question.\n const roots = [join(moveFrom, SNAPSHOT_DIR, \"files\"), join(moveTo, SNAPSHOT_DIR, \"files\")];\n const repaired: string[] = [];\n for (const f of pending) {\n const file = target(f.path);\n const want = wanted(f);\n if (want === null) {\n await rm(file, { force: true });\n await quietRmdir(dirname(file));\n repaired.push(f.path);\n continue;\n }\n const source = roots.map((r) => join(r, ...f.path.split(\"/\"))).find((p) => existsSync(p));\n if (source === undefined) {\n throw new InterruptedCommitError(\n `a \\`loam ${intent.command} ${intent.feature}\\` was interrupted mid-commit and ${f.path} is half-written, but ` +\n `the pre-image that would repair it is gone. Restore ${f.path} from version control, delete ` +\n `${COMMIT_INTENT}, then re-run.`,\n );\n }\n const bytes = await readFile(source);\n if (sha256(bytes) !== want) {\n throw new InterruptedCommitError(\n `a \\`loam ${intent.command} ${intent.feature}\\` was interrupted mid-commit and ${f.path} is half-written, but ` +\n `the pre-image that would repair it no longer matches the digest recorded before the commit — it has been ` +\n `edited since. Restore ${f.path} from version control, delete ${COMMIT_INTENT}, then re-run.`,\n );\n }\n await atomicWrite(file, bytes);\n repaired.push(f.path);\n }\n await finishRecovery(docsDir, intent);\n return { command: intent.command, feature: intent.feature, outcome: \"repaired\", repaired };\n}\n\n/** Drop the record, and (via the shared pid-scoped sweep) the killed run's own temps. */\nasync function finishRecovery(docsDir: string, intent: CommitIntent): Promise<void> {\n await sweepPidTemps(\n intent.files.map((f) => dirname(join(docsDir, ...f.path.split(\"/\")))),\n intent.pid,\n );\n await clearCommitIntent(docsDir);\n}\n\n\n"]}
|
|
1
|
+
{"version":3,"file":"recover.js","sourceRoot":"","sources":["../../../../src/core/staging/recovery/recover.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * Refuse or repair an interrupted commit, before this run reads anything.\n *\n * Split from `./intent.ts` because this is the only module on the write path\n * that writes bytes nobody asked for in this run. Two journal versions, two\n * doctrines, one dispatch: a version-1 record (archive/unarchive) is repaired\n * BACKWARD from the snapshot pre-images, each verified against the digest the\n * record captured; a version-2 record is handed to `txn/forward.ts`, which\n * rolls FORWARD from the staged temps under the same verification. Either\n * way, a file in NEITHER recorded state was written by somebody else since,\n * so recovery refuses and names it rather than choosing which of two truths\n * to destroy.\n *\n * The journaled writers reach it through `openCommitWindow` below — the lock,\n * then this — so the two refusals that can keep a commit window shut are\n * spelled once rather than once per writer. Archive and unarchive are the\n * exception: they call it directly, inside their plan, under a lock they took\n * themselves, and throw its refusal to a handler that maps it.\n */\nimport { existsSync } from \"node:fs\";\nimport { readFile, rm } from \"node:fs/promises\";\nimport { dirname, join } from \"node:path\";\nimport { repoPath } from \"../../envelope/json.js\";\nimport { sha256 } from \"../writes.js\";\nimport { atomicWrite, quietRmdir } from \"../commit.js\";\nimport { SNAPSHOT_DIR } from \"../snapshot/manifest.js\";\nimport {\n COMMIT_INTENT,\n type CommitRecovery,\n InterruptedCommitError,\n sweepPidTemps,\n} from \"../interrupted.js\";\nimport { acquireDocsLock, acquireDocsLockWaiting, DocsBusyError, LOCK_WAIT_MS } from \"../lock.js\";\nimport { readTxnIntent } from \"../txn/journal.js\";\nimport { recoverForward } from \"../txn/forward.js\";\nimport {\n clearCommitIntent,\n readCommitIntent,\n type CommitIntent,\n type CommitIntentFile,\n} from \"./intent.js\";\n\n/**\n * Refuse or repair before this run reads anything. Returns null when there is\n * nothing to recover.\n *\n * Repair is only ever the bytes the interrupted run already had on disk: each\n * file still in the wrong state is rewritten from the snapshot pre-image and\n * only after that pre-image's own sha256 matches the digest the record captured\n * before the crash. A file in NEITHER state was written by somebody else since,\n * so this refuses and names it rather than choosing which of two truths to\n * destroy — the same doctrine `rollbackStaged` follows when it leaves a file as\n * found.\n */\nexport async function recoverInterruptedCommit(docsDir: string): Promise<CommitRecovery | null> {\n const path = join(docsDir, COMMIT_INTENT);\n if (!existsSync(path)) return null;\n const intent = await readCommitIntent(docsDir);\n if (intent === null) {\n // Version 2 — the smaller transaction's journal. Same file, different\n // recovery: forward from the staged temps, in txn/forward.ts.\n const txn = await readTxnIntent(docsDir);\n if (txn !== null) return recoverForward(docsDir, txn);\n throw new InterruptedCommitError(\n `${repoPath(docsDir, path)} records a commit this loam cannot read, so it cannot tell a half-merged docs repo ` +\n `from a healthy one. Check the living docs against version control, delete ${COMMIT_INTENT}, then re-run.`,\n );\n }\n const moveFrom = join(docsDir, ...intent.moveFrom.split(\"/\"));\n const moveTo = join(docsDir, ...intent.moveTo.split(\"/\"));\n // The move is the commit's LAST step. If it landed, every swap before it did\n // too, and only the record's own removal was lost.\n if (existsSync(moveTo) && !existsSync(moveFrom)) {\n await finishRecovery(docsDir, intent);\n return { command: intent.command, feature: intent.feature, outcome: \"completed\", repaired: [] };\n }\n\n const target = (rel: string): string => join(docsDir, ...rel.split(\"/\"));\n const wanted = (f: CommitIntentFile): string | null => (intent.restore === \"before\" ? f.before : f.after);\n const other = (f: CommitIntentFile): string | null => (intent.restore === \"before\" ? f.after : f.before);\n const pending: CommitIntentFile[] = [];\n const unknown: string[] = [];\n for (const f of intent.files) {\n const file = target(f.path);\n const now = existsSync(file) ? sha256(await readFile(file)) : null;\n if (now === wanted(f)) continue;\n if (now === other(f)) pending.push(f);\n else unknown.push(f.path);\n }\n if (unknown.length > 0) {\n throw new InterruptedCommitError(\n `a \\`loam ${intent.command} ${intent.feature}\\` was interrupted mid-commit (pid ${intent.pid} on ${intent.host}, ` +\n `${intent.at}) and ${unknown.length} of the file(s) it was writing now hold bytes that are neither what it ` +\n `found nor what it wrote — somebody edited them since: ${unknown.join(\", \")}. loam will not choose between ` +\n `those two truths. Reconcile them against version control, delete ${COMMIT_INTENT}, then re-run.`,\n );\n }\n if (pending.length === 0) {\n await finishRecovery(docsDir, intent);\n return { command: intent.command, feature: intent.feature, outcome: \"consistent\", repaired: [] };\n }\n\n // The pre-images travel inside the feature directory, so they are at one of\n // the two ends of the move depending on how far the interrupted run got.\n // Deliberately NOT the snapshot's (service, artifact) re-keying: the journal\n // and the snapshot were written seconds apart under the same docs lock, so\n // the journal's literal paths and the pre-image keys cannot have diverged —\n // a service directory moved by hand between the crash and this repair leaves\n // its files \"in neither recorded state\", which is the refusal below, not a\n // resolution question.\n const roots = [join(moveFrom, SNAPSHOT_DIR, \"files\"), join(moveTo, SNAPSHOT_DIR, \"files\")];\n const repaired: string[] = [];\n for (const f of pending) {\n const file = target(f.path);\n const want = wanted(f);\n if (want === null) {\n await rm(file, { force: true });\n await quietRmdir(dirname(file));\n repaired.push(f.path);\n continue;\n }\n const source = roots.map((r) => join(r, ...f.path.split(\"/\"))).find((p) => existsSync(p));\n if (source === undefined) {\n throw new InterruptedCommitError(\n `a \\`loam ${intent.command} ${intent.feature}\\` was interrupted mid-commit and ${f.path} is half-written, but ` +\n `the pre-image that would repair it is gone. Restore ${f.path} from version control, delete ` +\n `${COMMIT_INTENT}, then re-run.`,\n );\n }\n const bytes = await readFile(source);\n if (sha256(bytes) !== want) {\n throw new InterruptedCommitError(\n `a \\`loam ${intent.command} ${intent.feature}\\` was interrupted mid-commit and ${f.path} is half-written, but ` +\n `the pre-image that would repair it no longer matches the digest recorded before the commit — it has been ` +\n `edited since. Restore ${f.path} from version control, delete ${COMMIT_INTENT}, then re-run.`,\n );\n }\n await atomicWrite(file, bytes);\n repaired.push(f.path);\n }\n await finishRecovery(docsDir, intent);\n return { command: intent.command, feature: intent.feature, outcome: \"repaired\", repaired };\n}\n\n/** Drop the record, and (via the shared pid-scoped sweep) the killed run's own temps. */\nasync function finishRecovery(docsDir: string, intent: CommitIntent): Promise<void> {\n await sweepPidTemps(\n intent.files.map((f) => dirname(join(docsDir, ...f.path.split(\"/\")))),\n intent.pid,\n );\n await clearCommitIntent(docsDir);\n}\n\n/** The lock is held and a predecessor's interrupted commit is resolved: this run may read, then write. */\nexport interface OpenCommitWindow {\n ok: true;\n recovered: CommitRecovery | null;\n /** Idempotent. The caller's one obligation, in a `finally`. */\n release: () => Promise<void>;\n}\n\n/** The window did not open. Nothing was read or written, and no lock is held. */\nexport interface CommitWindowRefusal {\n ok: false;\n code: \"docs-busy\" | \"commit-interrupted\";\n message: string;\n}\n\n/**\n * Open a writer's commit window: take the lock in `root` — the docs repo, or\n * gherkin's emission root in the service repo — then refuse or repair a\n * predecessor's interrupted commit before this run reads a byte.\n *\n * `waitMs` is how long to sit out a live holder before answering `docs-busy`,\n * and 0 refuses on first contact; `acquireDocsLockWaiting` says which writers\n * wait and why archive does not.\n *\n * Each writer here used to spell this sequence out by hand, and the copy in\n * `verify` once let its recovery refusal keep the lock — one bad answer set\n * became a wedged docs repo for everyone else. So the refusal releases HERE,\n * before it is returned, and an error that is neither refusal releases before\n * it propagates.\n */\nexport async function openCommitWindow(root: string, waitMs: number): Promise<OpenCommitWindow | CommitWindowRefusal> {\n let release: () => Promise<void>;\n try {\n release = waitMs > 0 ? await acquireDocsLockWaiting(root, waitMs) : await acquireDocsLock(root);\n } catch (err) {\n if (!(err instanceof DocsBusyError)) throw err;\n return { ok: false, code: \"docs-busy\", message: err.message };\n }\n try {\n return { ok: true, recovered: await recoverInterruptedCommit(root), release };\n } catch (err) {\n await release();\n if (!(err instanceof InterruptedCommitError)) throw err;\n return { ok: false, code: \"commit-interrupted\", message: err.message };\n }\n}\n\n/**\n * Recover a predecessor's interrupted commit NOW and hand the lock straight\n * back — for a writer whose slow half runs UNLOCKED over the very bytes a\n * half-commit corrupts (vouch's verification, gherkin's reconcile); each says\n * at its call site why recovery inside its own commit window comes too late.\n * Cheap when there is nothing to do: no journal, no lock, no I/O beyond one\n * existsSync.\n */\nexport async function recoverIfJournaled(\n root: string,\n): Promise<{ ok: true; recovered: CommitRecovery | null } | CommitWindowRefusal> {\n if (!existsSync(join(root, COMMIT_INTENT))) return { ok: true, recovered: null };\n const opened = await openCommitWindow(root, LOCK_WAIT_MS);\n if (!opened.ok) return opened;\n await opened.release();\n return { ok: true, recovered: opened.recovered };\n}\n\n"]}
|
|
@@ -0,0 +1,152 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Where the undo snapshot lives and what its manifest says — the half of
|
|
3
|
+
* `./snapshot.ts` a command can know without being able to take a snapshot.
|
|
4
|
+
*
|
|
5
|
+
* Split from that module when the snapshot became a package of its own: taking
|
|
6
|
+
* a snapshot (and refusing to clobber one) is a commit, and the MCP server's
|
|
7
|
+
* read-only claim is DERIVED from the import graph — a served command whose
|
|
8
|
+
* imports reach `./snapshot.ts` is, by that test, a command that can commit.
|
|
9
|
+
* The layout below is pure naming and shape, and the readers that only need
|
|
10
|
+
* to find a snapshot, or read what one recorded, import it from here.
|
|
11
|
+
*/
|
|
12
|
+
import { isUtf8 } from "node:buffer";
|
|
13
|
+
import { readFile } from "node:fs/promises";
|
|
14
|
+
import { join } from "node:path";
|
|
15
|
+
import { resolvePortableFileInsideLexically } from "../../kernel/path-safety.js";
|
|
16
|
+
import { isRecord } from "../../kernel/records.js";
|
|
17
|
+
import { sha256 } from "../writes.js";
|
|
18
|
+
/** Where archive parks the pre-images, inside the feature directory so they travel with it — `./snapshot.ts`'s module doc says why. */
|
|
19
|
+
export const SNAPSHOT_DIR = ".loam-before";
|
|
20
|
+
export const SNAPSHOT_MANIFEST = "manifest.json";
|
|
21
|
+
/**
|
|
22
|
+
* Bumped only when the layout changes in a way `unarchive` must refuse to guess
|
|
23
|
+
* at. 2 added `before`: a version-1 snapshot says nothing about the bytes it
|
|
24
|
+
* would RESTORE, so an edited pre-image read back as an intact one. 3 re-keys
|
|
25
|
+
* `services/` entries by `(service, artifact)`: a literal path pins the service
|
|
26
|
+
* directory where it stood on the day of the archive, and once the subsystem
|
|
27
|
+
* tree lets it move — re-cutting is expected to be routine — an undo addressed
|
|
28
|
+
* by path restores into a location that is gone. The service id never moves.
|
|
29
|
+
*/
|
|
30
|
+
export const SNAPSHOT_VERSION = 3;
|
|
31
|
+
/**
|
|
32
|
+
* Version 2 is still READ, forever: version-2 snapshots already sit inside
|
|
33
|
+
* archived features, which carry them across loam upgrades, and each must keep
|
|
34
|
+
* restoring — and keep being graded for staleness by a retried archive — under
|
|
35
|
+
* its own literal-path rules. Only version 1 is refused (no restore digests).
|
|
36
|
+
*/
|
|
37
|
+
export const SNAPSHOT_COMPAT_VERSION = 2;
|
|
38
|
+
export function snapshotDir(featureDir) {
|
|
39
|
+
return join(featureDir, SNAPSHOT_DIR);
|
|
40
|
+
}
|
|
41
|
+
/**
|
|
42
|
+
* The checklist digest an archived feature's snapshot captured as it merged,
|
|
43
|
+
* or null when it carries none.
|
|
44
|
+
*
|
|
45
|
+
* It lives in the manifest because the manifest is the one file `archive`
|
|
46
|
+
* already writes into the feature at the moment of the merge, travels with it
|
|
47
|
+
* into `features/archive/`, and is taken away by `unarchive` — so a feature
|
|
48
|
+
* archived again gets a fresh one, captured against what it then asked. A
|
|
49
|
+
* frozen verification record cannot be judged against a checklist re-derived
|
|
50
|
+
* after the merge (the merge moved the feature's operations into the living
|
|
51
|
+
* contract, so the derivation can only mismatch); this is the question as it
|
|
52
|
+
* stood when the feature shipped, which is what `loam gate --since` holds the
|
|
53
|
+
* record to. Here rather than beside the writer for the reason this module
|
|
54
|
+
* exists: the gate is a read-only command, served over MCP as one.
|
|
55
|
+
*
|
|
56
|
+
* Tolerant to the last byte, and every failure is "none captured": a manifest
|
|
57
|
+
* an older loam wrote, a feature archived with no record, a manifest that is
|
|
58
|
+
* missing or does not parse. The reader that grades on it reads null as the
|
|
59
|
+
* frozen history it always read, and says so — it never refuses a deploy
|
|
60
|
+
* question over bookkeeping.
|
|
61
|
+
*/
|
|
62
|
+
export async function readSnapshotChecklist(featureDir) {
|
|
63
|
+
const manifest = await manifestRecord(featureDir);
|
|
64
|
+
if (manifest === null)
|
|
65
|
+
return null;
|
|
66
|
+
const { checklist } = manifest;
|
|
67
|
+
return typeof checklist === "string" && checklist !== "" ? checklist : null;
|
|
68
|
+
}
|
|
69
|
+
/**
|
|
70
|
+
* The bytes one service artifact held the moment this feature merged — the
|
|
71
|
+
* pre-image the snapshot parked under `files/` — or one of the two answers
|
|
72
|
+
* that say the merge took nothing out of it ({@link SnapshotPreImage}), or
|
|
73
|
+
* null when there is nothing to trust.
|
|
74
|
+
*
|
|
75
|
+
* Read for the questions only the pre-image can answer, both asked by the
|
|
76
|
+
* retired-operation revival check (core/coherence/retired/): what a merge TOOK
|
|
77
|
+
* that its delta never spelled — a channel-slot removal in an asyncapi delta is
|
|
78
|
+
* a bare marker, while the messages it retired with the channel are written in
|
|
79
|
+
* the living contract it merged into and nowhere else — and whether a removal
|
|
80
|
+
* marker took anything at all, since the merge deletes only what it finds at
|
|
81
|
+
* the marker's slot. Here, beside `readSnapshotChecklist`, for that reader's
|
|
82
|
+
* reason: the readers are read-only commands, and this module is the half of
|
|
83
|
+
* the snapshot they may import.
|
|
84
|
+
*
|
|
85
|
+
* The entry is found by `(service, artifact)` on a version-3 manifest — the
|
|
86
|
+
* key that survives a subsystem re-cut — and by the literal path's last two
|
|
87
|
+
* segments on a version-2 one, which recorded nothing else. Tolerant to the
|
|
88
|
+
* last byte, like the checklist reader: no manifest, a layout this loam would
|
|
89
|
+
* have to guess at, no row for the artifact while some row cannot be read (it
|
|
90
|
+
* might have been this one's, so absence cannot be concluded past it), a path
|
|
91
|
+
* that does not stay inside `files/`, a pre-image that no longer hashes to the digest
|
|
92
|
+
* the manifest recorded for it, or bytes that are not UTF-8 — every one is
|
|
93
|
+
* null, and the caller grades without it. An edited pre-image is refused
|
|
94
|
+
* rather than read for the reason `unarchive` refuses to restore one: its
|
|
95
|
+
* bytes are no longer what the merge saw.
|
|
96
|
+
*/
|
|
97
|
+
export async function readSnapshotPreImage(featureDir, key) {
|
|
98
|
+
const manifest = await manifestRecord(featureDir);
|
|
99
|
+
if (manifest === null || !Array.isArray(manifest.files))
|
|
100
|
+
return null;
|
|
101
|
+
if (manifest.version !== SNAPSHOT_VERSION && manifest.version !== SNAPSHOT_COMPAT_VERSION)
|
|
102
|
+
return null;
|
|
103
|
+
// `Array.isArray` narrows to `any[]`; naming the element type keeps that
|
|
104
|
+
// `any` from spreading past the row being read (snapshot.ts's reason).
|
|
105
|
+
const rows = manifest.files;
|
|
106
|
+
const entry = rows.find((row) => isRecord(row) && entryIs(row, key));
|
|
107
|
+
if (entry === undefined) {
|
|
108
|
+
// Absence is an answer only when every row could be read: a row that is
|
|
109
|
+
// not a record with a path might have been this artifact's.
|
|
110
|
+
return rows.every((row) => isRecord(row) && typeof row.path === "string") ? { kind: "unwritten" } : null;
|
|
111
|
+
}
|
|
112
|
+
if (!isRecord(entry) || typeof entry.path !== "string")
|
|
113
|
+
return null;
|
|
114
|
+
if (entry.existed === false && entry.before === null)
|
|
115
|
+
return { kind: "created" };
|
|
116
|
+
if (typeof entry.before !== "string")
|
|
117
|
+
return null;
|
|
118
|
+
let bytes;
|
|
119
|
+
try {
|
|
120
|
+
// The manifest is data and this path is one we go and read: the lexical
|
|
121
|
+
// containment `unarchive` and the staleness check apply to the same field.
|
|
122
|
+
bytes = await readFile(resolvePortableFileInsideLexically(join(snapshotDir(featureDir), "files"), entry.path, "snapshot pre-image"));
|
|
123
|
+
}
|
|
124
|
+
catch {
|
|
125
|
+
return null;
|
|
126
|
+
}
|
|
127
|
+
if (sha256(bytes) !== entry.before || !isUtf8(bytes))
|
|
128
|
+
return null;
|
|
129
|
+
return { kind: "held", text: bytes.toString("utf8") };
|
|
130
|
+
}
|
|
131
|
+
/** Does this manifest row record `key`'s artifact? The version-3 key when it has one, the literal path otherwise. */
|
|
132
|
+
function entryIs(row, key) {
|
|
133
|
+
if (typeof row.service === "string" && typeof row.artifact === "string") {
|
|
134
|
+
return row.service === key.service && row.artifact === key.artifact;
|
|
135
|
+
}
|
|
136
|
+
if (typeof row.path !== "string")
|
|
137
|
+
return false;
|
|
138
|
+
const parts = row.path.split("/");
|
|
139
|
+
return parts.length >= 3 && parts[0] === "services" && parts.at(-2) === key.service && parts.at(-1) === key.artifact;
|
|
140
|
+
}
|
|
141
|
+
/** The manifest as a record, or null — the one tolerant read behind both readers above. */
|
|
142
|
+
async function manifestRecord(featureDir) {
|
|
143
|
+
let manifest;
|
|
144
|
+
try {
|
|
145
|
+
manifest = JSON.parse(await readFile(join(snapshotDir(featureDir), SNAPSHOT_MANIFEST), "utf8"));
|
|
146
|
+
}
|
|
147
|
+
catch {
|
|
148
|
+
return null;
|
|
149
|
+
}
|
|
150
|
+
return isRecord(manifest) ? manifest : null;
|
|
151
|
+
}
|
|
152
|
+
//# sourceMappingURL=manifest.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"manifest.js","sourceRoot":"","sources":["../../../../src/core/staging/snapshot/manifest.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * Where the undo snapshot lives and what its manifest says — the half of\n * `./snapshot.ts` a command can know without being able to take a snapshot.\n *\n * Split from that module when the snapshot became a package of its own: taking\n * a snapshot (and refusing to clobber one) is a commit, and the MCP server's\n * read-only claim is DERIVED from the import graph — a served command whose\n * imports reach `./snapshot.ts` is, by that test, a command that can commit.\n * The layout below is pure naming and shape, and the readers that only need\n * to find a snapshot, or read what one recorded, import it from here.\n */\nimport { isUtf8 } from \"node:buffer\";\nimport { readFile } from \"node:fs/promises\";\nimport { join } from \"node:path\";\nimport { resolvePortableFileInsideLexically } from \"../../kernel/path-safety.js\";\nimport { isRecord } from \"../../kernel/records.js\";\nimport { sha256 } from \"../writes.js\";\n\n/** Where archive parks the pre-images, inside the feature directory so they travel with it — `./snapshot.ts`'s module doc says why. */\nexport const SNAPSHOT_DIR = \".loam-before\";\nexport const SNAPSHOT_MANIFEST = \"manifest.json\";\n/**\n * Bumped only when the layout changes in a way `unarchive` must refuse to guess\n * at. 2 added `before`: a version-1 snapshot says nothing about the bytes it\n * would RESTORE, so an edited pre-image read back as an intact one. 3 re-keys\n * `services/` entries by `(service, artifact)`: a literal path pins the service\n * directory where it stood on the day of the archive, and once the subsystem\n * tree lets it move — re-cutting is expected to be routine — an undo addressed\n * by path restores into a location that is gone. The service id never moves.\n */\nexport const SNAPSHOT_VERSION = 3;\n/**\n * Version 2 is still READ, forever: version-2 snapshots already sit inside\n * archived features, which carry them across loam upgrades, and each must keep\n * restoring — and keep being graded for staleness by a retried archive — under\n * its own literal-path rules. Only version 1 is refused (no restore digests).\n */\nexport const SNAPSHOT_COMPAT_VERSION = 2;\n\nexport interface SnapshotEntry {\n /**\n * Docs-repo-relative path, forward slashes, AS ARCHIVED. Load-bearing even\n * beside the key below: it names the pre-image under `.loam-before/files/`,\n * and it is the literal fallback when the id stops being enumerated.\n */\n path: string;\n /**\n * For a `services/` entry: the service id (the leaf directory name — the one\n * thing a re-cut never changes) and the artifact's path inside its directory,\n * resolved through the CURRENT enumeration at restore time. Absent on\n * landscape and `features/` entries, which do not move.\n */\n service?: string;\n artifact?: string;\n /** False when archive CREATED the file: restoring it means deleting it again. */\n existed: boolean;\n /** sha256 of what archive wrote, so unarchive can tell its own merge from later edits. */\n after: string;\n /**\n * sha256 of the pre-image beside this manifest — the bytes `unarchive` will\n * RESTORE. Null when archive created the file (there is no pre-image).\n * `after` only describes what archive wrote; while nothing described the\n * restore source, a pre-image edited in the archived feature directory — a\n * bad merge, a rebase, a stray editor — was written back verbatim and every\n * loam surface certified the result.\n */\n before: string | null;\n}\n\nexport interface SnapshotManifest {\n version: number;\n feature: string;\n dirName: string;\n archivedAt: string;\n /**\n * The digest of the checklist a fresh `loam verify` derived for the feature\n * just before this merge — present when the feature carried a record then,\n * absent otherwise and in every manifest an older loam wrote. Additive, so\n * the layout version does not move: `unarchive` never reads it, and a reader\n * that predates it ignores it. See {@link readSnapshotChecklist}.\n */\n checklist?: string;\n files: SnapshotEntry[];\n}\n\nexport function snapshotDir(featureDir: string): string {\n return join(featureDir, SNAPSHOT_DIR);\n}\n\n/**\n * The checklist digest an archived feature's snapshot captured as it merged,\n * or null when it carries none.\n *\n * It lives in the manifest because the manifest is the one file `archive`\n * already writes into the feature at the moment of the merge, travels with it\n * into `features/archive/`, and is taken away by `unarchive` — so a feature\n * archived again gets a fresh one, captured against what it then asked. A\n * frozen verification record cannot be judged against a checklist re-derived\n * after the merge (the merge moved the feature's operations into the living\n * contract, so the derivation can only mismatch); this is the question as it\n * stood when the feature shipped, which is what `loam gate --since` holds the\n * record to. Here rather than beside the writer for the reason this module\n * exists: the gate is a read-only command, served over MCP as one.\n *\n * Tolerant to the last byte, and every failure is \"none captured\": a manifest\n * an older loam wrote, a feature archived with no record, a manifest that is\n * missing or does not parse. The reader that grades on it reads null as the\n * frozen history it always read, and says so — it never refuses a deploy\n * question over bookkeeping.\n */\nexport async function readSnapshotChecklist(featureDir: string): Promise<string | null> {\n const manifest = await manifestRecord(featureDir);\n if (manifest === null) return null;\n const { checklist } = manifest;\n return typeof checklist === \"string\" && checklist !== \"\" ? checklist : null;\n}\n\n/**\n * What a snapshot says about one service artifact as this feature's merge found\n * it. `held` is the pre-image itself. The other two are answers too, and the\n * same one for the question the reader asks — what did the merge take OUT of\n * the artifact: nothing. `created` is a row archive wrote for a file it\n * created (there was nothing before it); `unwritten` is an intact manifest\n * with no row for the artifact at all — archive records every file it writes,\n * so the artifact is what the merge found. Only the EVENT merge leaves a\n * contract unwritten: it returns no text when it changes nothing\n * (asyncapi/merge/merge.ts, `text: null` — a delta whose every marker missed\n * its target is exactly that). The OpenAPI merge returns text whenever the\n * delta has a path entry, so its no-op is an ordinary row whose pre-image\n * equals what it wrote — `held`, which answers the same question the same\n * way: every marker misses against it again.\n */\nexport type SnapshotPreImage =\n | { kind: \"held\"; text: string }\n | { kind: \"created\" }\n | { kind: \"unwritten\" };\n\n/**\n * The bytes one service artifact held the moment this feature merged — the\n * pre-image the snapshot parked under `files/` — or one of the two answers\n * that say the merge took nothing out of it ({@link SnapshotPreImage}), or\n * null when there is nothing to trust.\n *\n * Read for the questions only the pre-image can answer, both asked by the\n * retired-operation revival check (core/coherence/retired/): what a merge TOOK\n * that its delta never spelled — a channel-slot removal in an asyncapi delta is\n * a bare marker, while the messages it retired with the channel are written in\n * the living contract it merged into and nowhere else — and whether a removal\n * marker took anything at all, since the merge deletes only what it finds at\n * the marker's slot. Here, beside `readSnapshotChecklist`, for that reader's\n * reason: the readers are read-only commands, and this module is the half of\n * the snapshot they may import.\n *\n * The entry is found by `(service, artifact)` on a version-3 manifest — the\n * key that survives a subsystem re-cut — and by the literal path's last two\n * segments on a version-2 one, which recorded nothing else. Tolerant to the\n * last byte, like the checklist reader: no manifest, a layout this loam would\n * have to guess at, no row for the artifact while some row cannot be read (it\n * might have been this one's, so absence cannot be concluded past it), a path\n * that does not stay inside `files/`, a pre-image that no longer hashes to the digest\n * the manifest recorded for it, or bytes that are not UTF-8 — every one is\n * null, and the caller grades without it. An edited pre-image is refused\n * rather than read for the reason `unarchive` refuses to restore one: its\n * bytes are no longer what the merge saw.\n */\nexport async function readSnapshotPreImage(\n featureDir: string,\n key: { service: string; artifact: string },\n): Promise<SnapshotPreImage | null> {\n const manifest = await manifestRecord(featureDir);\n if (manifest === null || !Array.isArray(manifest.files)) return null;\n if (manifest.version !== SNAPSHOT_VERSION && manifest.version !== SNAPSHOT_COMPAT_VERSION) return null;\n // `Array.isArray` narrows to `any[]`; naming the element type keeps that\n // `any` from spreading past the row being read (snapshot.ts's reason).\n const rows: unknown[] = manifest.files;\n const entry = rows.find((row) => isRecord(row) && entryIs(row, key));\n if (entry === undefined) {\n // Absence is an answer only when every row could be read: a row that is\n // not a record with a path might have been this artifact's.\n return rows.every((row) => isRecord(row) && typeof row.path === \"string\") ? { kind: \"unwritten\" } : null;\n }\n if (!isRecord(entry) || typeof entry.path !== \"string\") return null;\n if (entry.existed === false && entry.before === null) return { kind: \"created\" };\n if (typeof entry.before !== \"string\") return null;\n let bytes: Buffer;\n try {\n // The manifest is data and this path is one we go and read: the lexical\n // containment `unarchive` and the staleness check apply to the same field.\n bytes = await readFile(\n resolvePortableFileInsideLexically(join(snapshotDir(featureDir), \"files\"), entry.path, \"snapshot pre-image\"),\n );\n } catch {\n return null;\n }\n if (sha256(bytes) !== entry.before || !isUtf8(bytes)) return null;\n return { kind: \"held\", text: bytes.toString(\"utf8\") };\n}\n\n/** Does this manifest row record `key`'s artifact? The version-3 key when it has one, the literal path otherwise. */\nfunction entryIs(row: Record<string, unknown>, key: { service: string; artifact: string }): boolean {\n if (typeof row.service === \"string\" && typeof row.artifact === \"string\") {\n return row.service === key.service && row.artifact === key.artifact;\n }\n if (typeof row.path !== \"string\") return false;\n const parts = row.path.split(\"/\");\n return parts.length >= 3 && parts[0] === \"services\" && parts.at(-2) === key.service && parts.at(-1) === key.artifact;\n}\n\n/** The manifest as a record, or null — the one tolerant read behind both readers above. */\nasync function manifestRecord(featureDir: string): Promise<Record<string, unknown> | null> {\n let manifest: unknown;\n try {\n manifest = JSON.parse(await readFile(join(snapshotDir(featureDir), SNAPSHOT_MANIFEST), \"utf8\"));\n } catch {\n return null;\n }\n return isRecord(manifest) ? manifest : null;\n}\n"]}
|
|
@@ -11,37 +11,15 @@
|
|
|
11
11
|
import { existsSync } from "node:fs";
|
|
12
12
|
import { mkdir, readFile, writeFile } from "node:fs/promises";
|
|
13
13
|
import { dirname, join } from "node:path";
|
|
14
|
-
import { repoPath } from "
|
|
15
|
-
import { resolvePortableFileInsideLexically, UnsafePathError } from "
|
|
16
|
-
// Every `isRecord` below guards
|
|
17
|
-
//
|
|
18
|
-
//
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
import {
|
|
23
|
-
import { isDigest, sha256 } from "./writes.js";
|
|
24
|
-
import { message, quietRm } from "./commit.js";
|
|
25
|
-
/** Where archive parks the pre-images, inside the feature directory so they travel with it — the module doc above says why. */
|
|
26
|
-
export const SNAPSHOT_DIR = ".loam-before";
|
|
27
|
-
export const SNAPSHOT_MANIFEST = "manifest.json";
|
|
28
|
-
/**
|
|
29
|
-
* Bumped only when the layout changes in a way `unarchive` must refuse to guess
|
|
30
|
-
* at. 2 added `before`: a version-1 snapshot says nothing about the bytes it
|
|
31
|
-
* would RESTORE, so an edited pre-image read back as an intact one. 3 re-keys
|
|
32
|
-
* `services/` entries by `(service, artifact)`: a literal path pins the service
|
|
33
|
-
* directory where it stood on the day of the archive, and once the subsystem
|
|
34
|
-
* tree lets it move — re-cutting is expected to be routine — an undo addressed
|
|
35
|
-
* by path restores into a location that is gone. The service id never moves.
|
|
36
|
-
*/
|
|
37
|
-
export const SNAPSHOT_VERSION = 3;
|
|
38
|
-
/**
|
|
39
|
-
* Version 2 is still READ, forever: version-2 snapshots already sit inside
|
|
40
|
-
* archived features, which carry them across loam upgrades, and each must keep
|
|
41
|
-
* restoring — and keep being graded for staleness by a retried archive — under
|
|
42
|
-
* its own literal-path rules. Only version 1 is refused (no restore digests).
|
|
43
|
-
*/
|
|
44
|
-
export const SNAPSHOT_COMPAT_VERSION = 2;
|
|
14
|
+
import { repoPath } from "../../envelope/json.js";
|
|
15
|
+
import { resolvePortableFileInsideLexically, UnsafePathError } from "../../kernel/path-safety.js";
|
|
16
|
+
// Every `isRecord` below guards the manifest loam itself wrote, read back inside
|
|
17
|
+
// a REFUSAL path — `../lock.ts` records why that is where a thrown `TypeError`
|
|
18
|
+
// costs the most.
|
|
19
|
+
import { isRecord } from "../../kernel/records.js";
|
|
20
|
+
import { isDigest, sha256 } from "../writes.js";
|
|
21
|
+
import { message, quietRm } from "../commit.js";
|
|
22
|
+
import { SNAPSHOT_COMPAT_VERSION, SNAPSHOT_DIR, SNAPSHOT_MANIFEST, SNAPSHOT_VERSION, snapshotDir, } from "./manifest.js";
|
|
45
23
|
/** A snapshot directory that must not be replaced, because it is the only copy of the true pre-images. */
|
|
46
24
|
export class SnapshotClobberError extends Error {
|
|
47
25
|
dir;
|
|
@@ -54,9 +32,6 @@ export class SnapshotClobberError extends Error {
|
|
|
54
32
|
this.dir = dir;
|
|
55
33
|
}
|
|
56
34
|
}
|
|
57
|
-
export function snapshotDir(featureDir) {
|
|
58
|
-
return join(featureDir, SNAPSHOT_DIR);
|
|
59
|
-
}
|
|
60
35
|
export async function writeSnapshot(request) {
|
|
61
36
|
const { featureDir, docsDir, staged } = request;
|
|
62
37
|
const { featureId, dirName } = request.feature;
|
|
@@ -89,6 +64,7 @@ export async function writeSnapshot(request) {
|
|
|
89
64
|
feature: featureId,
|
|
90
65
|
dirName,
|
|
91
66
|
archivedAt: new Date().toISOString(),
|
|
67
|
+
...(request.checklist === undefined ? {} : { checklist: request.checklist }),
|
|
92
68
|
files,
|
|
93
69
|
};
|
|
94
70
|
// Manifest last: its presence is what says the pre-images beside it are complete.
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"snapshot.js","sourceRoot":"","sources":["../../../../src/core/staging/snapshot/snapshot.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * The undo snapshot: the bytes `archive` is about to overwrite, parked inside\n * the feature directory so they travel with it into `features/archive/`.\n *\n * Separate from the commit because the merge is not invertible — a MODIFIED\n * requirement's previous text appears nowhere in the delta, and a landscape\n * rewritten by hand since cannot be un-rewritten by re-reading one. `unarchive`\n * puts bytes back; it does not recompute them. Nothing on the commit path reads\n * this module: only `archive` writes a snapshot and only `unarchive` honours one.\n */\nimport { existsSync } from \"node:fs\";\nimport { mkdir, readFile, writeFile } from \"node:fs/promises\";\nimport { dirname, join } from \"node:path\";\nimport { repoPath } from \"../../envelope/json.js\";\nimport { resolvePortableFileInsideLexically, UnsafePathError } from \"../../kernel/path-safety.js\";\n// Every `isRecord` below guards the manifest loam itself wrote, read back inside\n// a REFUSAL path — `../lock.ts` records why that is where a thrown `TypeError`\n// costs the most.\nimport { isRecord } from \"../../kernel/records.js\";\nimport { isDigest, sha256, type StagedWrite } from \"../writes.js\";\nimport { message, quietRm } from \"../commit.js\";\nimport {\n SNAPSHOT_COMPAT_VERSION,\n SNAPSHOT_DIR,\n SNAPSHOT_MANIFEST,\n SNAPSHOT_VERSION,\n snapshotDir,\n type SnapshotEntry,\n type SnapshotManifest,\n} from \"./manifest.js\";\n\n/** A snapshot directory that must not be replaced, because it is the only copy of the true pre-images. */\nexport class SnapshotClobberError extends Error {\n override readonly name = \"SnapshotClobberError\";\n\n constructor(readonly dir: string, detail: string) {\n super(\n `${dir} already holds an undo snapshot that does NOT match the living docs (${detail}) — a previous archive of ` +\n `this feature wrote to the living docs and never took them back, and these are the only copies of what they ` +\n `said before. Overwriting them would make that merge permanent and unreportable. Run \\`loam archive\\` again ` +\n `after restoring the living docs from version control, or delete ${SNAPSHOT_DIR}/ if you know the merge never landed.`,\n );\n }\n}\n\n/** The (service, artifact) key a `services/` path decomposes into. */\nexport interface ServiceKey {\n service: string;\n artifact: string;\n}\n\n/**\n * Everything `writeSnapshot` needs, as one object. The resolvers are INJECTED\n * by the caller — `archive` builds them from its view of the tree — because\n * how a path decomposes into a service identity is repo-layout knowledge, and\n * this module's subject is parking and checking bytes.\n */\nexport interface SnapshotRequest {\n featureDir: string;\n docsDir: string;\n feature: { featureId: string; dirName: string };\n staged: StagedWrite[];\n /** Docs-relative path → the service it belongs to; null for landscape/features paths. */\n serviceKeyOf: (rel: string) => ServiceKey | null;\n /** Service id → its CURRENT docs-relative directory; null when not enumerated. */\n serviceDirOf: (service: string) => string | null;\n /**\n * Recorded verbatim as the manifest's `checklist` when given. Opaque here —\n * this module parks bytes and never learns what a checklist is; `archive`\n * derives it and `gate --since` reads it back (`./manifest.ts`).\n */\n checklist?: string;\n}\n\nexport async function writeSnapshot(request: SnapshotRequest): Promise<void> {\n const { featureDir, docsDir, staged } = request;\n const { featureId, dirName } = request.feature;\n const dir = snapshotDir(featureDir);\n await assertSnapshotReplaceable(dir, docsDir, request.serviceDirOf);\n // A leftover from a rolled-back run would describe a merge that never happened.\n await quietRm(dir);\n\n const files: SnapshotEntry[] = [];\n for (const s of staged) {\n const rel = repoPath(docsDir, s.write.path);\n // services/ entries are re-keyed by (service, artifact) so they survive the\n // service directory moving between archive and unarchive; the literal path\n // is still recorded — it keys the pre-image below, and it is the fallback.\n const key = request.serviceKeyOf(rel);\n files.push({\n path: rel,\n ...key,\n existed: s.before !== null,\n after: sha256(s.content ?? Buffer.alloc(0)),\n before: s.before === null ? null : sha256(s.before),\n });\n if (s.before !== null) {\n const dest = join(dir, \"files\", rel);\n await mkdir(dirname(dest), { recursive: true });\n await writeFile(dest, s.before);\n }\n }\n\n const manifest: SnapshotManifest = {\n version: SNAPSHOT_VERSION,\n feature: featureId,\n dirName,\n archivedAt: new Date().toISOString(),\n ...(request.checklist === undefined ? {} : { checklist: request.checklist }),\n files,\n };\n // Manifest last: its presence is what says the pre-images beside it are complete.\n await mkdir(dir, { recursive: true });\n await writeFile(join(dir, SNAPSHOT_MANIFEST), JSON.stringify(manifest, null, 2) + \"\\n\", \"utf8\");\n}\n\n/**\n * Refuse to replace a snapshot whose pre-images are still the only record of\n * what the living docs said.\n *\n * A snapshot in an ACTIVE feature directory means a previous archive of this\n * same feature wrote it — and since a clean rollback deletes it, finding one\n * means that run either died mid-commit or reported `rollback-incomplete`. If\n * its pre-images still match the living docs, nothing of that run landed and\n * the snapshot is stale bookkeeping. If they do not, the living docs carry\n * that run's writes, these files are the only way back, and the unconditional\n * clear turned a repairable half-merge into a permanent one — after which\n * `unarchive` restored the half-merged text and said the docs were back.\n *\n * Version 2 manifests are graded by the same staleness question over their\n * literal paths — refusing them as \"a different loam\" would turn every leftover\n * version-2 snapshot into a permanent archive refusal on upgrade.\n */\nasync function assertSnapshotReplaceable(\n dir: string,\n docsDir: string,\n serviceDirOf: (service: string) => string | null,\n): Promise<void> {\n const manifestPath = join(dir, SNAPSHOT_MANIFEST);\n // The manifest is written LAST, so a `files/` tree without one is a snapshot\n // whose run died before it could swap anything: it describes no merge.\n if (!existsSync(manifestPath)) return;\n let manifest: unknown;\n try {\n manifest = JSON.parse(await readFile(manifestPath, \"utf8\"));\n } catch {\n throw new SnapshotClobberError(dir, `${SNAPSHOT_MANIFEST} cannot be read, so nothing can say whether it is stale`);\n }\n // The cast that used to stand here asserted the shape rather than checking\n // it: well-formed JSON that is not an object answered `undefined` to every\n // field below and slipped through as \"a different loam\", and a bare `null`\n // threw a TypeError out of the refusal itself. Both left this directory —\n // which may hold the only copies of what the living docs said — one step from\n // being replaced, without the staleness question ever having been asked.\n if (!isRecord(manifest)) {\n throw new SnapshotClobberError(dir, `${SNAPSHOT_MANIFEST} cannot be read, so nothing can say whether it is stale`);\n }\n if (\n (manifest.version !== SNAPSHOT_VERSION && manifest.version !== SNAPSHOT_COMPAT_VERSION) ||\n !Array.isArray(manifest.files)\n ) {\n throw new SnapshotClobberError(dir, `it was written by a different loam and cannot be checked against the living docs`);\n }\n // `Array.isArray` narrows to `any[]`; naming the element type here is what\n // keeps that `any` from spreading past the one row being read.\n const rows: unknown[] = manifest.files;\n const landed: string[] = [];\n for (const row of rows) {\n const entry = readSnapshotClaim(row);\n if (entry === null) {\n throw new SnapshotClobberError(dir, `${SNAPSHOT_MANIFEST} is malformed, so nothing can say whether it is stale`);\n }\n let target: string;\n try {\n // These paths are data, and they are the ones this function goes and\n // reads. They name living documents, so containment is lexical: a docs\n // repo may legitimately mount a service directory through a symlink, and\n // `unarchive` resolves the same field the same way before it writes back.\n target = resolvePortableFileInsideLexically(docsDir, entry.path, `snapshot path '${entry.path}'`);\n // A re-keyed entry's CURRENT location is wherever the enumeration says\n // its service lives today — grading staleness at the old address would\n // call a merely-moved file a landed merge. The literal path was still\n // resolved above on purpose: its containment is part of what \"loam wrote\n // this manifest\" means, and it stays the pre-image's key. An id the\n // enumeration no longer answers falls back to that literal path.\n if (entry.service !== undefined && entry.artifact !== undefined) {\n const current = serviceDirOf(entry.service);\n if (current !== null) {\n target = resolvePortableFileInsideLexically(\n docsDir,\n `${current}/${entry.artifact}`,\n `snapshot path '${entry.path}'`,\n );\n }\n }\n } catch (err) {\n if (!(err instanceof UnsafePathError)) throw err;\n throw new SnapshotClobberError(dir, `${message(err)}, so nothing can say whether it is stale`);\n }\n const now = existsSync(target) ? sha256(await readFile(target)) : null;\n // `existed: false` means archive created the file, so \"nothing landed\"\n // means it is still absent.\n const untouched = entry.existed ? now === entry.before : now === null;\n if (!untouched) landed.push(entry.path);\n }\n if (landed.length > 0) throw new SnapshotClobberError(dir, `${landed.length} file(s) already carry that merge: ${landed.join(\", \")}`);\n}\n\n/**\n * One manifest row read back as the record `writeSnapshot` wrote, or null when\n * the file does not actually say.\n *\n * Every field the staleness question consumes is checked, because an unchecked\n * field does not read as missing here — it reads as an ANSWER. An `existed`\n * that is not a boolean is falsy, and a `false` claim is graded by asking\n * whether the file is ABSENT: a manifest recording a pre-image for a deleted\n * file therefore graded as stale bookkeeping and was cleared, taking the only\n * copy of that pre-image with it. A `before` of the wrong type equals no digest\n * at all, so the opposite row accused an untouched file of carrying a merge.\n *\n * `unarchive` asks a stricter version of the same question of the same file\n * (`after`, `archivedAt`, the feature name, no duplicates) because it is about\n * to write THROUGH it; that check stays where it is, with its own refusals.\n *\n * `service`/`artifact` are read when present (a version-3 `services/` row) and\n * must arrive as a pair: half a key is a manifest that does not say where the\n * file lives, and grading through the literal path anyway would be a guess.\n */\ninterface SnapshotClaim {\n path: string;\n existed: boolean;\n before: string | null;\n service?: string;\n artifact?: string;\n}\n\nfunction readSnapshotClaim(value: unknown): SnapshotClaim | null {\n if (!isRecord(value)) return null;\n const { path, existed, before, service, artifact } = value;\n if (typeof path !== \"string\" || typeof existed !== \"boolean\") return null;\n let key: { service: string; artifact: string } | Record<string, never>;\n if (service === undefined && artifact === undefined) key = {};\n else if (typeof service === \"string\" && typeof artifact === \"string\") key = { service, artifact };\n else return null;\n if (existed) return isDigest(before) ? { path, existed, before, ...key } : null;\n return before === null ? { path, existed, before, ...key } : null;\n}\n\n"]}
|
|
@@ -34,6 +34,17 @@ function execution(step) {
|
|
|
34
34
|
needs: ["compare the living docs with version control"],
|
|
35
35
|
};
|
|
36
36
|
}
|
|
37
|
+
// A vouch is a person's act, never an agent's (`loam vouch` refuses without a
|
|
38
|
+
// terminal or `--yes`), so the step is NOT runnable: an agent prepares the
|
|
39
|
+
// reading list with `loam vouch --pack` and hands the stamp to a person.
|
|
40
|
+
if (step.code === "next.revouch") {
|
|
41
|
+
return {
|
|
42
|
+
kind: "human-review",
|
|
43
|
+
runnable: false,
|
|
44
|
+
cwd: "service-repo",
|
|
45
|
+
needs: ["a person who re-reads the moved document and vouches for it from the service's own repository"],
|
|
46
|
+
};
|
|
47
|
+
}
|
|
37
48
|
const needs = placeholders(step.command);
|
|
38
49
|
const serviceRepo = SERVICE_REPO_STEPS.has(step.code);
|
|
39
50
|
return {
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"execution.js","sourceRoot":"","sources":["../../../../src/core/status/actions/execution.ts"],"names":[],"mappings":"AAOA,MAAM,UAAU,GAAG,IAAI,GAAG,CAAC;IACzB,oBAAoB;IACpB,oBAAoB;IACpB,kBAAkB;IAClB,qBAAqB;IACrB,uBAAuB;CACxB,CAAC,CAAC;AAEH,MAAM,kBAAkB,GAAG,IAAI,GAAG,CAAC;IACjC,mBAAmB;IACnB,kBAAkB;IAClB,qBAAqB;IACrB,yBAAyB;IACzB,qBAAqB;IACrB,sBAAsB;CACvB,CAAC,CAAC;AAEH,SAAS,YAAY,CAAC,OAAe;IACnC,OAAO,CAAC,GAAG,OAAO,CAAC,QAAQ,CAAC,YAAY,CAAC,CAAC,CAAC,GAAG,CAAC,CAAC,KAAK,EAAE,EAAE,CAAC,KAAK,CAAC,CAAC,CAAE,CAAC,CAAC;AACvE,CAAC;AAED,SAAS,SAAS,CAAC,IAAc;IAC/B,IAAI,UAAU,CAAC,GAAG,CAAC,IAAI,CAAC,IAAI,CAAC,EAAE,CAAC;QAC9B,OAAO;YACL,IAAI,EAAE,MAAM;YACZ,QAAQ,EAAE,KAAK;YACf,GAAG,EAAE,WAAW;YAChB,KAAK,EAAE,IAAI,CAAC,OAAO;YACnB,KAAK,EAAE,CAAC,IAAI,CAAC,IAAI,IAAI,iCAAiC,CAAC;SACxD,CAAC;IACJ,CAAC;IACD,IAAI,IAAI,CAAC,IAAI,KAAK,qBAAqB,IAAI,IAAI,CAAC,OAAO,KAAK,oBAAoB,EAAE,CAAC;QACjF,OAAO;YACL,IAAI,EAAE,cAAc;YACpB,QAAQ,EAAE,KAAK;YACf,GAAG,EAAE,WAAW;YAChB,KAAK,EAAE,CAAC,8CAA8C,CAAC;SACxD,CAAC;IACJ,CAAC;IACD,MAAM,KAAK,GAAG,YAAY,CAAC,IAAI,CAAC,OAAO,CAAC,CAAC;IACzC,MAAM,WAAW,GAAG,kBAAkB,CAAC,GAAG,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;IACtD,OAAO;QACL,IAAI,EAAE,WAAW,CAAC,CAAC,CAAC,eAAe,CAAC,CAAC,CAAC,SAAS;QAC/C,QAAQ,EAAE,KAAK,CAAC,MAAM,KAAK,CAAC;QAC5B,GAAG,EAAE,WAAW,CAAC,CAAC,CAAC,cAAc,CAAC,CAAC,CAAC,iBAAiB;QACrD,GAAG,CAAC,KAAK,CAAC,MAAM,KAAK,CAAC,CAAC,CAAC,CAAC,EAAE,OAAO,EAAE,IAAI,CAAC,OAAO,EAAE,CAAC,CAAC,CAAC,EAAE,KAAK,EAAE,CAAC;KAChE,CAAC;AACJ,CAAC;AAED,MAAM,UAAU,cAAc,CAAC,IAAc;IAC3C,OAAO,EAAE,GAAG,IAAI,EAAE,SAAS,EAAE,SAAS,CAAC,IAAI,CAAC,EAAE,CAAC;AACjD,CAAC","sourcesContent":["/** The executable machine contract layered over status's legacy command hint. */\nimport type {\n ExecutableNextStep,\n NextExecution,\n NextStep,\n} from \"../report.js\";\n\nconst EDIT_STEPS = new Set([\n \"next.author-intent\",\n \"next.touch-service\",\n \"next.author-spec\",\n \"next.author-openapi\",\n \"next.author-scenarios\",\n]);\n\nconst SERVICE_REPO_STEPS = new Set([\n \"next.bind-service\",\n \"next.adopt-first\",\n \"next.generate-tests\",\n \"next.verify-unconfirmed\",\n \"next.attest-service\",\n \"next.verify-attested\",\n]);\n\nfunction placeholders(command: string): string[] {\n return [...command.matchAll(/<([^>]+)>/g)].map((match) => match[1]!);\n}\n\nfunction execution(step: NextStep): NextExecution {\n if (EDIT_STEPS.has(step.code)) {\n return {\n kind: \"edit\",\n runnable: false,\n cwd: \"docs-repo\",\n after: step.command,\n needs: [step.path ?? \"the artifact named in statement\"],\n };\n }\n if (step.code === \"next.recover-commit\" && step.command === \"loam doctor --json\") {\n return {\n kind: \"human-review\",\n runnable: false,\n cwd: \"docs-repo\",\n needs: [\"compare the living docs with version control\"],\n };\n }\n const needs = placeholders(step.command);\n const serviceRepo = SERVICE_REPO_STEPS.has(step.code);\n return {\n kind: serviceRepo ? \"external-repo\" : \"command\",\n runnable: needs.length === 0,\n cwd: serviceRepo ? \"service-repo\" : \"configured-repo\",\n ...(needs.length === 0 ? { command: step.command } : { needs }),\n };\n}\n\nexport function executableNext(step: NextStep): ExecutableNextStep {\n return { ...step, execution: execution(step) };\n}\n"]}
|
|
1
|
+
{"version":3,"file":"execution.js","sourceRoot":"","sources":["../../../../src/core/status/actions/execution.ts"],"names":[],"mappings":"AAOA,MAAM,UAAU,GAAG,IAAI,GAAG,CAAC;IACzB,oBAAoB;IACpB,oBAAoB;IACpB,kBAAkB;IAClB,qBAAqB;IACrB,uBAAuB;CACxB,CAAC,CAAC;AAEH,MAAM,kBAAkB,GAAG,IAAI,GAAG,CAAC;IACjC,mBAAmB;IACnB,kBAAkB;IAClB,qBAAqB;IACrB,yBAAyB;IACzB,qBAAqB;IACrB,sBAAsB;CACvB,CAAC,CAAC;AAEH,SAAS,YAAY,CAAC,OAAe;IACnC,OAAO,CAAC,GAAG,OAAO,CAAC,QAAQ,CAAC,YAAY,CAAC,CAAC,CAAC,GAAG,CAAC,CAAC,KAAK,EAAE,EAAE,CAAC,KAAK,CAAC,CAAC,CAAE,CAAC,CAAC;AACvE,CAAC;AAED,SAAS,SAAS,CAAC,IAAc;IAC/B,IAAI,UAAU,CAAC,GAAG,CAAC,IAAI,CAAC,IAAI,CAAC,EAAE,CAAC;QAC9B,OAAO;YACL,IAAI,EAAE,MAAM;YACZ,QAAQ,EAAE,KAAK;YACf,GAAG,EAAE,WAAW;YAChB,KAAK,EAAE,IAAI,CAAC,OAAO;YACnB,KAAK,EAAE,CAAC,IAAI,CAAC,IAAI,IAAI,iCAAiC,CAAC;SACxD,CAAC;IACJ,CAAC;IACD,IAAI,IAAI,CAAC,IAAI,KAAK,qBAAqB,IAAI,IAAI,CAAC,OAAO,KAAK,oBAAoB,EAAE,CAAC;QACjF,OAAO;YACL,IAAI,EAAE,cAAc;YACpB,QAAQ,EAAE,KAAK;YACf,GAAG,EAAE,WAAW;YAChB,KAAK,EAAE,CAAC,8CAA8C,CAAC;SACxD,CAAC;IACJ,CAAC;IACD,8EAA8E;IAC9E,2EAA2E;IAC3E,yEAAyE;IACzE,IAAI,IAAI,CAAC,IAAI,KAAK,cAAc,EAAE,CAAC;QACjC,OAAO;YACL,IAAI,EAAE,cAAc;YACpB,QAAQ,EAAE,KAAK;YACf,GAAG,EAAE,cAAc;YACnB,KAAK,EAAE,CAAC,+FAA+F,CAAC;SACzG,CAAC;IACJ,CAAC;IACD,MAAM,KAAK,GAAG,YAAY,CAAC,IAAI,CAAC,OAAO,CAAC,CAAC;IACzC,MAAM,WAAW,GAAG,kBAAkB,CAAC,GAAG,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;IACtD,OAAO;QACL,IAAI,EAAE,WAAW,CAAC,CAAC,CAAC,eAAe,CAAC,CAAC,CAAC,SAAS;QAC/C,QAAQ,EAAE,KAAK,CAAC,MAAM,KAAK,CAAC;QAC5B,GAAG,EAAE,WAAW,CAAC,CAAC,CAAC,cAAc,CAAC,CAAC,CAAC,iBAAiB;QACrD,GAAG,CAAC,KAAK,CAAC,MAAM,KAAK,CAAC,CAAC,CAAC,CAAC,EAAE,OAAO,EAAE,IAAI,CAAC,OAAO,EAAE,CAAC,CAAC,CAAC,EAAE,KAAK,EAAE,CAAC;KAChE,CAAC;AACJ,CAAC;AAED,MAAM,UAAU,cAAc,CAAC,IAAc;IAC3C,OAAO,EAAE,GAAG,IAAI,EAAE,SAAS,EAAE,SAAS,CAAC,IAAI,CAAC,EAAE,CAAC;AACjD,CAAC","sourcesContent":["/** The executable machine contract layered over status's legacy command hint. */\nimport type {\n ExecutableNextStep,\n NextExecution,\n NextStep,\n} from \"../report.js\";\n\nconst EDIT_STEPS = new Set([\n \"next.author-intent\",\n \"next.touch-service\",\n \"next.author-spec\",\n \"next.author-openapi\",\n \"next.author-scenarios\",\n]);\n\nconst SERVICE_REPO_STEPS = new Set([\n \"next.bind-service\",\n \"next.adopt-first\",\n \"next.generate-tests\",\n \"next.verify-unconfirmed\",\n \"next.attest-service\",\n \"next.verify-attested\",\n]);\n\nfunction placeholders(command: string): string[] {\n return [...command.matchAll(/<([^>]+)>/g)].map((match) => match[1]!);\n}\n\nfunction execution(step: NextStep): NextExecution {\n if (EDIT_STEPS.has(step.code)) {\n return {\n kind: \"edit\",\n runnable: false,\n cwd: \"docs-repo\",\n after: step.command,\n needs: [step.path ?? \"the artifact named in statement\"],\n };\n }\n if (step.code === \"next.recover-commit\" && step.command === \"loam doctor --json\") {\n return {\n kind: \"human-review\",\n runnable: false,\n cwd: \"docs-repo\",\n needs: [\"compare the living docs with version control\"],\n };\n }\n // A vouch is a person's act, never an agent's (`loam vouch` refuses without a\n // terminal or `--yes`), so the step is NOT runnable: an agent prepares the\n // reading list with `loam vouch --pack` and hands the stamp to a person.\n if (step.code === \"next.revouch\") {\n return {\n kind: \"human-review\",\n runnable: false,\n cwd: \"service-repo\",\n needs: [\"a person who re-reads the moved document and vouches for it from the service's own repository\"],\n };\n }\n const needs = placeholders(step.command);\n const serviceRepo = SERVICE_REPO_STEPS.has(step.code);\n return {\n kind: serviceRepo ? \"external-repo\" : \"command\",\n runnable: needs.length === 0,\n cwd: serviceRepo ? \"service-repo\" : \"configured-repo\",\n ...(needs.length === 0 ? { command: step.command } : { needs }),\n };\n}\n\nexport function executableNext(step: NextStep): ExecutableNextStep {\n return { ...step, execution: execution(step) };\n}\n"]}
|
|
@@ -7,7 +7,7 @@
|
|
|
7
7
|
import { existsSync } from "node:fs";
|
|
8
8
|
import { repoPath } from "../../envelope/json.js";
|
|
9
9
|
import { featurePaths, featureSpecPaths } from "../../repo/paths.js";
|
|
10
|
-
import {
|
|
10
|
+
import { recordLocation } from "../../verify/location.js";
|
|
11
11
|
import { owesContract } from "../contracts.js";
|
|
12
12
|
import { verificationStatus } from "../verification.js";
|
|
13
13
|
/**
|
|
@@ -174,7 +174,7 @@ export function featureArtifacts(docsDir, feature, services, grading) {
|
|
|
174
174
|
out.push({
|
|
175
175
|
id: "verification",
|
|
176
176
|
service: null,
|
|
177
|
-
path: rel(
|
|
177
|
+
path: rel(recordLocation(feature.dir)),
|
|
178
178
|
exists: verification.state !== "absent",
|
|
179
179
|
required: true,
|
|
180
180
|
status: feeders.length === 0 ? "blocked" : verificationStatus(verification),
|