@ybotok/loam 0.1.0-beta.2 → 0.2.0-alpha.1
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 +2570 -61
- package/COMPARISON.md +80 -63
- package/CONTRIBUTING.md +84 -0
- package/MIGRATING-from-OpenSpec.md +213 -54
- package/QUICKSTART.md +125 -0
- package/README.md +450 -150
- package/ROADMAP.md +494 -0
- package/SCHEMA.md +2362 -245
- package/SECURITY.md +21 -3
- package/WORKFLOW.md +701 -0
- package/dist/cli.js +113 -19
- package/dist/cli.js.map +1 -1
- package/dist/commands/adopt/adopt.js +209 -0
- package/dist/commands/adopt/adopt.js.map +1 -0
- package/dist/commands/adopt/render.js +268 -0
- package/dist/commands/adopt/render.js.map +1 -0
- package/dist/commands/archive/archive.js +77 -0
- package/dist/commands/archive/archive.js.map +1 -0
- package/dist/commands/archive/plan/contracts/asyncapi.js +178 -0
- package/dist/commands/archive/plan/contracts/asyncapi.js.map +1 -0
- package/dist/commands/archive/plan/contracts/openapi.js +159 -0
- package/dist/commands/archive/plan/contracts/openapi.js.map +1 -0
- package/dist/commands/archive/plan/gate.js +317 -0
- package/dist/commands/archive/plan/gate.js.map +1 -0
- package/dist/commands/archive/plan/landscape.js +161 -0
- package/dist/commands/archive/plan/landscape.js.map +1 -0
- package/dist/commands/archive/plan/refusal.js +111 -0
- package/dist/commands/archive/plan/refusal.js.map +1 -0
- package/dist/commands/archive/plan/specs.js +237 -0
- package/dist/commands/archive/plan/specs.js.map +1 -0
- package/dist/commands/archive/plan/state.js +27 -0
- package/dist/commands/archive/plan/state.js.map +1 -0
- package/dist/commands/archive/run.js +251 -0
- package/dist/commands/archive/run.js.map +1 -0
- package/dist/commands/context/context.js +99 -0
- package/dist/commands/context/context.js.map +1 -0
- package/dist/commands/context/print.js +283 -0
- package/dist/commands/context/print.js.map +1 -0
- package/dist/commands/delta/delta.js +279 -0
- package/dist/commands/delta/delta.js.map +1 -0
- package/dist/commands/delta/print.js +205 -0
- package/dist/commands/delta/print.js.map +1 -0
- package/dist/commands/dependencies.js +9 -7
- package/dist/commands/dependencies.js.map +1 -1
- package/dist/commands/diff/diff.js +93 -0
- package/dist/commands/diff/diff.js.map +1 -0
- package/dist/commands/diff/print.js +38 -0
- package/dist/commands/diff/print.js.map +1 -0
- package/dist/commands/doctor.js +5 -4
- package/dist/commands/doctor.js.map +1 -1
- package/dist/commands/explain/explain.js +151 -0
- package/dist/commands/explain/explain.js.map +1 -0
- package/dist/commands/explore.js +29 -24
- package/dist/commands/explore.js.map +1 -1
- package/dist/commands/gate/gate.js +115 -0
- package/dist/commands/gate/gate.js.map +1 -0
- package/dist/commands/gate/print.js +54 -0
- package/dist/commands/gate/print.js.map +1 -0
- package/dist/commands/gherkin/commit.js +146 -0
- package/dist/commands/gherkin/commit.js.map +1 -0
- package/dist/commands/gherkin/gherkin.js +246 -0
- package/dist/commands/gherkin/gherkin.js.map +1 -0
- package/dist/commands/gherkin/reconcile.js +149 -0
- package/dist/commands/gherkin/reconcile.js.map +1 -0
- package/dist/commands/gherkin/render.js +77 -0
- package/dist/commands/gherkin/render.js.map +1 -0
- package/dist/commands/init/example.js +204 -0
- package/dist/commands/init/example.js.map +1 -0
- package/dist/commands/init/first-hour.js +28 -0
- package/dist/commands/init/first-hour.js.map +1 -0
- package/dist/commands/{init.js → init/init.js} +146 -71
- package/dist/commands/init/init.js.map +1 -0
- package/dist/commands/init/mcp.js +131 -0
- package/dist/commands/init/mcp.js.map +1 -0
- package/dist/commands/init/options.js +78 -0
- package/dist/commands/init/options.js.map +1 -0
- package/dist/commands/instructions.js +82 -14
- package/dist/commands/instructions.js.map +1 -1
- package/dist/commands/list/campaign/campaign.js +65 -0
- package/dist/commands/list/campaign/campaign.js.map +1 -0
- package/dist/commands/list/campaign/owners.js +65 -0
- package/dist/commands/list/campaign/owners.js.map +1 -0
- package/dist/commands/list/json.js +157 -0
- package/dist/commands/list/json.js.map +1 -0
- package/dist/commands/list/list.js +343 -0
- package/dist/commands/list/list.js.map +1 -0
- package/dist/commands/list/print.js +301 -0
- package/dist/commands/list/print.js.map +1 -0
- package/dist/commands/list/review.js +120 -0
- package/dist/commands/list/review.js.map +1 -0
- package/dist/commands/list/views.js +105 -0
- package/dist/commands/list/views.js.map +1 -0
- package/dist/commands/mcp/dispatch.js +183 -0
- package/dist/commands/mcp/dispatch.js.map +1 -0
- package/dist/commands/mcp/mcp.js +34 -0
- package/dist/commands/mcp/mcp.js.map +1 -0
- package/dist/commands/mcp/serve.js +55 -0
- package/dist/commands/mcp/serve.js.map +1 -0
- package/dist/commands/migrate-openspec/materialize/active.js +170 -0
- package/dist/commands/migrate-openspec/materialize/active.js.map +1 -0
- package/dist/commands/migrate-openspec/materialize/feature.js +41 -0
- package/dist/commands/migrate-openspec/materialize/feature.js.map +1 -0
- package/dist/commands/migrate-openspec/materialize/living.js +62 -0
- package/dist/commands/migrate-openspec/materialize/living.js.map +1 -0
- package/dist/commands/migrate-openspec/materialize/stage.js +142 -0
- package/dist/commands/migrate-openspec/materialize/stage.js.map +1 -0
- package/dist/commands/migrate-openspec/materialize/target.js +219 -0
- package/dist/commands/migrate-openspec/materialize/target.js.map +1 -0
- package/dist/commands/migrate-openspec/migrate-openspec.js +142 -0
- package/dist/commands/migrate-openspec/migrate-openspec.js.map +1 -0
- package/dist/commands/migrate-openspec/openspec/decisions.js +91 -0
- package/dist/commands/migrate-openspec/openspec/decisions.js.map +1 -0
- package/dist/commands/migrate-openspec/openspec/error.js +19 -0
- package/dist/commands/migrate-openspec/openspec/error.js.map +1 -0
- package/dist/commands/migrate-openspec/openspec/mapping.js +170 -0
- package/dist/commands/migrate-openspec/openspec/mapping.js.map +1 -0
- package/dist/commands/migrate-openspec/openspec/paths.js +43 -0
- package/dist/commands/migrate-openspec/openspec/paths.js.map +1 -0
- package/dist/commands/migrate-openspec/openspec/read.js +49 -0
- package/dist/commands/migrate-openspec/openspec/read.js.map +1 -0
- package/dist/commands/migrate-openspec/print.js +89 -0
- package/dist/commands/migrate-openspec/print.js.map +1 -0
- package/dist/commands/new/commit.js +76 -0
- package/dist/commands/new/commit.js.map +1 -0
- package/dist/commands/new/new.js +283 -0
- package/dist/commands/new/new.js.map +1 -0
- package/dist/commands/new/templates.js +355 -0
- package/dist/commands/new/templates.js.map +1 -0
- package/dist/commands/open.js +203 -0
- package/dist/commands/open.js.map +1 -0
- package/dist/commands/policy/format.js +152 -0
- package/dist/commands/policy/format.js.map +1 -0
- package/dist/commands/{docs-repo-gate.js → policy/gate.js} +11 -11
- package/dist/commands/policy/gate.js.map +1 -0
- package/dist/commands/rebase/edit.js +106 -0
- package/dist/commands/rebase/edit.js.map +1 -0
- package/dist/commands/rebase/living.js +185 -0
- package/dist/commands/rebase/living.js.map +1 -0
- package/dist/commands/rebase/plan.js +245 -0
- package/dist/commands/rebase/plan.js.map +1 -0
- package/dist/commands/rebase/rebase.js +275 -0
- package/dist/commands/rebase/rebase.js.map +1 -0
- package/dist/commands/seed/commit.js +192 -0
- package/dist/commands/seed/commit.js.map +1 -0
- package/dist/commands/seed/plan.js +133 -0
- package/dist/commands/seed/plan.js.map +1 -0
- package/dist/commands/seed/seed.js +172 -0
- package/dist/commands/seed/seed.js.map +1 -0
- package/dist/commands/show/feature.js +178 -0
- package/dist/commands/show/feature.js.map +1 -0
- package/dist/commands/show/marks.js +24 -0
- package/dist/commands/show/marks.js.map +1 -0
- package/dist/commands/{show.js → show/service.js} +63 -152
- package/dist/commands/show/service.js.map +1 -0
- package/dist/commands/show/show.js +77 -0
- package/dist/commands/show/show.js.map +1 -0
- package/dist/commands/status/json.js +45 -0
- package/dist/commands/status/json.js.map +1 -0
- package/dist/commands/status/print.js +157 -0
- package/dist/commands/status/print.js.map +1 -0
- package/dist/commands/status/status.js +119 -0
- package/dist/commands/status/status.js.map +1 -0
- package/dist/commands/steps/steps.js +189 -0
- package/dist/commands/steps/steps.js.map +1 -0
- package/dist/commands/subsystem/edit.js +192 -0
- package/dist/commands/subsystem/edit.js.map +1 -0
- package/dist/commands/subsystem/move.js +265 -0
- package/dist/commands/subsystem/move.js.map +1 -0
- package/dist/commands/subsystem/query.js +115 -0
- package/dist/commands/subsystem/query.js.map +1 -0
- package/dist/commands/subsystem/subsystem.js +101 -0
- package/dist/commands/subsystem/subsystem.js.map +1 -0
- package/dist/commands/subsystem/sync.js +103 -0
- package/dist/commands/subsystem/sync.js.map +1 -0
- package/dist/commands/subsystem/txn/txn.js +94 -0
- package/dist/commands/subsystem/txn/txn.js.map +1 -0
- package/dist/commands/subsystem/txn/views.js +70 -0
- package/dist/commands/subsystem/txn/views.js.map +1 -0
- package/dist/commands/unarchive/manifest.js +160 -0
- package/dist/commands/unarchive/manifest.js.map +1 -0
- package/dist/commands/{unarchive.js → unarchive/restore.js} +47 -160
- package/dist/commands/unarchive/restore.js.map +1 -0
- package/dist/commands/unarchive/unarchive.js +67 -0
- package/dist/commands/unarchive/unarchive.js.map +1 -0
- package/dist/commands/validate/arch-coverage.js +194 -0
- package/dist/commands/validate/arch-coverage.js.map +1 -0
- package/dist/commands/validate/checks/fleet-shape.js +296 -0
- package/dist/commands/validate/checks/fleet-shape.js.map +1 -0
- package/dist/commands/validate/checks/health-deps.js +55 -0
- package/dist/commands/validate/checks/health-deps.js.map +1 -0
- package/dist/commands/validate/checks/requirements.js +254 -0
- package/dist/commands/validate/checks/requirements.js.map +1 -0
- package/dist/commands/validate/checks/sources.js +76 -0
- package/dist/commands/validate/checks/sources.js.map +1 -0
- package/dist/commands/validate/checks/vocabulary.js +31 -0
- package/dist/commands/validate/checks/vocabulary.js.map +1 -0
- package/dist/commands/validate/feature.js +216 -0
- package/dist/commands/validate/feature.js.map +1 -0
- package/dist/commands/validate/fleet/census.js +69 -0
- package/dist/commands/validate/fleet/census.js.map +1 -0
- package/dist/commands/validate/fleet/kind-tags.js +39 -0
- package/dist/commands/validate/fleet/kind-tags.js.map +1 -0
- package/dist/commands/validate/fleet/landscape.js +353 -0
- package/dist/commands/validate/fleet/landscape.js.map +1 -0
- package/dist/commands/validate/fleet/load.js +52 -0
- package/dist/commands/validate/fleet/load.js.map +1 -0
- package/dist/commands/validate/fleet/obligations.js +236 -0
- package/dist/commands/validate/fleet/obligations.js.map +1 -0
- package/dist/commands/validate/fleet/scorecard/adoption.js +102 -0
- package/dist/commands/validate/fleet/scorecard/adoption.js.map +1 -0
- package/dist/commands/validate/fleet/scorecard/contracts.js +98 -0
- package/dist/commands/validate/fleet/scorecard/contracts.js.map +1 -0
- package/dist/commands/validate/fleet/scorecard/print.js +63 -0
- package/dist/commands/validate/fleet/scorecard/print.js.map +1 -0
- package/dist/commands/validate/fleet/scorecard/scorecard.js +156 -0
- package/dist/commands/validate/fleet/scorecard/scorecard.js.map +1 -0
- package/dist/commands/validate/fleet/usecases/capability-tag.js +94 -0
- package/dist/commands/validate/fleet/usecases/capability-tag.js.map +1 -0
- package/dist/commands/validate/fleet/usecases/place.js +13 -0
- package/dist/commands/validate/fleet/usecases/place.js.map +1 -0
- package/dist/commands/validate/fleet/usecases/requirement-tag.js +95 -0
- package/dist/commands/validate/fleet/usecases/requirement-tag.js.map +1 -0
- package/dist/commands/validate/fleet/usecases/steps.js +197 -0
- package/dist/commands/validate/fleet/usecases/steps.js.map +1 -0
- package/dist/commands/validate/fleet/usecases/usecases.js +99 -0
- package/dist/commands/validate/fleet/usecases/usecases.js.map +1 -0
- package/dist/commands/validate/fleet/views/ids.js +40 -0
- package/dist/commands/validate/fleet/views/ids.js.map +1 -0
- package/dist/commands/validate/fleet/views/stale.js +59 -0
- package/dist/commands/validate/fleet/views/stale.js.map +1 -0
- package/dist/commands/validate/links/corpus.js +92 -0
- package/dist/commands/validate/links/corpus.js.map +1 -0
- package/dist/commands/validate/links/glossary.js +75 -0
- package/dist/commands/validate/links/glossary.js.map +1 -0
- package/dist/commands/validate/report.js +269 -0
- package/dist/commands/validate/report.js.map +1 -0
- package/dist/commands/validate/service/api.js +338 -0
- package/dist/commands/validate/service/api.js.map +1 -0
- package/dist/commands/validate/service/events/events.js +219 -0
- package/dist/commands/validate/service/events/events.js.map +1 -0
- package/dist/commands/validate/service/events/message-producers.js +77 -0
- package/dist/commands/validate/service/events/message-producers.js.map +1 -0
- package/dist/commands/validate/service/evidence-pins.js +131 -0
- package/dist/commands/validate/service/evidence-pins.js.map +1 -0
- package/dist/commands/validate/service/service.js +250 -0
- package/dist/commands/validate/service/service.js.map +1 -0
- package/dist/commands/validate/service/specs.js +219 -0
- package/dist/commands/validate/service/specs.js.map +1 -0
- package/dist/commands/validate/service/spine.js +113 -0
- package/dist/commands/validate/service/spine.js.map +1 -0
- package/dist/commands/validate/validate.js +338 -0
- package/dist/commands/validate/validate.js.map +1 -0
- package/dist/commands/verify/cross/diff.js +182 -0
- package/dist/commands/verify/cross/diff.js.map +1 -0
- package/dist/commands/verify/evidence/contract.js +32 -0
- package/dist/commands/verify/evidence/contract.js.map +1 -0
- package/dist/commands/verify/evidence/pins.js +52 -0
- package/dist/commands/verify/evidence/pins.js.map +1 -0
- package/dist/commands/verify/evidence/read.js +72 -0
- package/dist/commands/verify/evidence/read.js.map +1 -0
- package/dist/commands/verify/frozen.js +241 -0
- package/dist/commands/verify/frozen.js.map +1 -0
- package/dist/commands/verify/record.js +226 -0
- package/dist/commands/verify/record.js.map +1 -0
- package/dist/commands/verify/report.js +223 -0
- package/dist/commands/verify/report.js.map +1 -0
- package/dist/commands/verify/results.js +246 -0
- package/dist/commands/verify/results.js.map +1 -0
- package/dist/commands/verify/verify.js +217 -0
- package/dist/commands/verify/verify.js.map +1 -0
- package/dist/commands/vouch/contract.js +11 -0
- package/dist/commands/vouch/contract.js.map +1 -0
- package/dist/commands/vouch/pack/contract.js +2 -0
- package/dist/commands/vouch/pack/contract.js.map +1 -0
- package/dist/commands/vouch/pack/pack.js +266 -0
- package/dist/commands/vouch/pack/pack.js.map +1 -0
- package/dist/commands/vouch/pack/print.js +271 -0
- package/dist/commands/vouch/pack/print.js.map +1 -0
- package/dist/commands/vouch/pack/sections.js +142 -0
- package/dist/commands/vouch/pack/sections.js.map +1 -0
- package/dist/commands/vouch/prompt/confirm.js +46 -0
- package/dist/commands/vouch/prompt/confirm.js.map +1 -0
- package/dist/commands/vouch/report.js +101 -0
- package/dist/commands/vouch/report.js.map +1 -0
- package/dist/commands/vouch/run.js +285 -0
- package/dist/commands/vouch/run.js.map +1 -0
- package/dist/commands/vouch/sample/plan.js +125 -0
- package/dist/commands/vouch/sample/plan.js.map +1 -0
- package/dist/commands/vouch/sample/print.js +109 -0
- package/dist/commands/vouch/sample/print.js.map +1 -0
- package/dist/commands/vouch/vet/verify.js +123 -0
- package/dist/commands/vouch/vet/verify.js.map +1 -0
- package/dist/commands/vouch/vouch.js +176 -0
- package/dist/commands/vouch/vouch.js.map +1 -0
- package/dist/core/agent/agents-md/artifacts.js +220 -0
- package/dist/core/agent/agents-md/artifacts.js.map +1 -0
- package/dist/core/agent/agents-md/command-map.js +324 -0
- package/dist/core/agent/agents-md/command-map.js.map +1 -0
- package/dist/core/agent/agents-md/cycle.js +165 -0
- package/dist/core/agent/agents-md/cycle.js.map +1 -0
- package/dist/core/agent/agents-md/map/explain.js +34 -0
- package/dist/core/agent/agents-md/map/explain.js.map +1 -0
- package/dist/core/agent/agents-md/map/lenses/context.js +41 -0
- package/dist/core/agent/agents-md/map/lenses/context.js.map +1 -0
- package/dist/core/agent/agents-md/map/lenses/diff.js +46 -0
- package/dist/core/agent/agents-md/map/lenses/diff.js.map +1 -0
- package/dist/core/agent/agents-md/map/lenses/gate.js +57 -0
- package/dist/core/agent/agents-md/map/lenses/gate.js.map +1 -0
- package/dist/core/agent/agents-md/map/mcp.js +41 -0
- package/dist/core/agent/agents-md/map/mcp.js.map +1 -0
- package/dist/core/agent/agents-md/map/status.js +62 -0
- package/dist/core/agent/agents-md/map/status.js.map +1 -0
- package/dist/core/agent/agents-md/map/subsystem.js +32 -0
- package/dist/core/agent/agents-md/map/subsystem.js.map +1 -0
- package/dist/core/agent/agents-md/map/usecases.js +60 -0
- package/dist/core/agent/agents-md/map/usecases.js.map +1 -0
- package/dist/core/agent/agents-md/refusals.js +246 -0
- package/dist/core/agent/agents-md/refusals.js.map +1 -0
- package/dist/core/agent/agents-md/shipped/archive-gate.js +98 -0
- package/dist/core/agent/agents-md/shipped/archive-gate.js.map +1 -0
- package/dist/core/agent/agents-md/spine.js +40 -0
- package/dist/core/agent/agents-md/spine.js.map +1 -0
- package/dist/core/agent/agents-md.js +170 -0
- package/dist/core/agent/agents-md.js.map +1 -0
- package/dist/core/agent/agents-stamp.js +155 -0
- package/dist/core/agent/agents-stamp.js.map +1 -0
- package/dist/core/agent/contract.js +2 -0
- package/dist/core/agent/contract.js.map +1 -0
- package/dist/core/agent/protocol.js +193 -0
- package/dist/core/agent/protocol.js.map +1 -0
- package/dist/core/agent/scaffold.js +259 -0
- package/dist/core/agent/scaffold.js.map +1 -0
- package/dist/core/agent/support/report.js +103 -0
- package/dist/core/agent/support/report.js.map +1 -0
- package/dist/core/agent/tools/dialects.js +150 -0
- package/dist/core/agent/tools/dialects.js.map +1 -0
- package/dist/core/agent/tools/registry.js +228 -0
- package/dist/core/agent/tools/registry.js.map +1 -0
- package/dist/core/agent/workflows/adopt.js +133 -0
- package/dist/core/agent/workflows/adopt.js.map +1 -0
- package/dist/core/agent/workflows/check.js +334 -0
- package/dist/core/agent/workflows/check.js.map +1 -0
- package/dist/core/agent/workflows/closing.js +262 -0
- package/dist/core/agent/workflows/closing.js.map +1 -0
- package/dist/core/agent/workflows/feature.js +105 -0
- package/dist/core/agent/workflows/feature.js.map +1 -0
- package/dist/core/agent/workflows/implement.js +105 -0
- package/dist/core/agent/workflows/implement.js.map +1 -0
- package/dist/core/agent/workflows/reference/authoring.js +209 -0
- package/dist/core/agent/workflows/reference/authoring.js.map +1 -0
- package/dist/core/agent/workflows/reference/codes.js +65 -0
- package/dist/core/agent/workflows/reference/codes.js.map +1 -0
- package/dist/core/agent/workflows/reference/done-check.js +188 -0
- package/dist/core/agent/workflows/reference/done-check.js.map +1 -0
- package/dist/core/agent/workflows/reference/reference.js +16 -0
- package/dist/core/agent/workflows/reference/reference.js.map +1 -0
- package/dist/core/agent/workflows/reference/spine.js +297 -0
- package/dist/core/agent/workflows/reference/spine.js.map +1 -0
- package/dist/core/asyncapi/baseline/plan.js +110 -0
- package/dist/core/asyncapi/baseline/plan.js.map +1 -0
- package/dist/core/asyncapi/baseline/surfaces.js +54 -0
- package/dist/core/asyncapi/baseline/surfaces.js.map +1 -0
- package/dist/core/asyncapi/depth.js +164 -0
- package/dist/core/asyncapi/depth.js.map +1 -0
- package/dist/core/asyncapi/digest.js +141 -0
- package/dist/core/asyncapi/digest.js.map +1 -0
- package/dist/core/asyncapi/merge/components.js +91 -0
- package/dist/core/asyncapi/merge/components.js.map +1 -0
- package/dist/core/asyncapi/merge/error.js +33 -0
- package/dist/core/asyncapi/merge/error.js.map +1 -0
- package/dist/core/asyncapi/merge/markers.js +308 -0
- package/dist/core/asyncapi/merge/markers.js.map +1 -0
- package/dist/core/asyncapi/merge/merge.js +215 -0
- package/dist/core/asyncapi/merge/merge.js.map +1 -0
- package/dist/core/asyncapi/merge/pin.js +107 -0
- package/dist/core/asyncapi/merge/pin.js.map +1 -0
- package/dist/core/asyncapi/model.js +11 -0
- package/dist/core/asyncapi/model.js.map +1 -0
- package/dist/core/asyncapi/producers.js +50 -0
- package/dist/core/asyncapi/producers.js.map +1 -0
- package/dist/core/asyncapi/read.js +239 -0
- package/dist/core/asyncapi/read.js.map +1 -0
- package/dist/core/brief/brief.js +225 -0
- package/dist/core/brief/brief.js.map +1 -0
- package/dist/core/brief/checks.js +250 -0
- package/dist/core/brief/checks.js.map +1 -0
- package/dist/core/brief/landscape.js +194 -0
- package/dist/core/brief/landscape.js.map +1 -0
- package/dist/core/brief/targets.js +226 -0
- package/dist/core/brief/targets.js.map +1 -0
- package/dist/core/brief/unchecked.js +47 -0
- package/dist/core/brief/unchecked.js.map +1 -0
- package/dist/core/c4/arch.js +282 -0
- package/dist/core/c4/arch.js.map +1 -0
- package/dist/core/c4/likec4.js +88 -0
- package/dist/core/c4/likec4.js.map +1 -0
- package/dist/core/c4/parsed/dynamic-views.js +132 -0
- package/dist/core/c4/parsed/dynamic-views.js.map +1 -0
- package/dist/core/c4/parsed/specification.js +78 -0
- package/dist/core/c4/parsed/specification.js.map +1 -0
- package/dist/core/c4/parsed/values.js +57 -0
- package/dist/core/c4/parsed/values.js.map +1 -0
- package/dist/core/c4/parsed/view-ids.js +75 -0
- package/dist/core/c4/parsed/view-ids.js.map +1 -0
- package/dist/core/c4/project/architecture.js +45 -0
- package/dist/core/c4/project/architecture.js.map +1 -0
- package/dist/core/c4/project/documents.js +58 -0
- package/dist/core/c4/project/documents.js.map +1 -0
- package/dist/core/c4/project/load.js +183 -0
- package/dist/core/c4/project/load.js.map +1 -0
- package/dist/core/c4/resolve/service.js +113 -0
- package/dist/core/c4/resolve/service.js.map +1 -0
- package/dist/core/c4/seed/fleet-file.js +201 -0
- package/dist/core/c4/seed/fleet-file.js.map +1 -0
- package/dist/core/c4/seed/items.js +91 -0
- package/dist/core/c4/seed/items.js.map +1 -0
- package/dist/core/c4/seed/stamp.js +48 -0
- package/dist/core/c4/seed/stamp.js.map +1 -0
- package/dist/core/c4/seed/template.js +109 -0
- package/dist/core/c4/seed/template.js.map +1 -0
- package/dist/core/c4/source-mask.js +82 -0
- package/dist/core/c4/source-mask.js.map +1 -0
- package/dist/core/c4/source-scan.js +227 -0
- package/dist/core/c4/source-scan.js.map +1 -0
- package/dist/core/c4/splice/authored-source.js +101 -0
- package/dist/core/c4/splice/authored-source.js.map +1 -0
- package/dist/core/c4/splice/contract.js +10 -0
- package/dist/core/c4/splice/contract.js.map +1 -0
- package/dist/core/c4/splice/delta-blocks.js +126 -0
- package/dist/core/c4/splice/delta-blocks.js.map +1 -0
- package/dist/core/c4/splice/identity/edges.js +36 -0
- package/dist/core/c4/splice/identity/edges.js.map +1 -0
- package/dist/core/c4/splice/landscape-merge.js +269 -0
- package/dist/core/c4/splice/landscape-merge.js.map +1 -0
- package/dist/core/c4/splice/placement.js +194 -0
- package/dist/core/c4/splice/placement.js.map +1 -0
- package/dist/core/c4/workspace.js +172 -0
- package/dist/core/c4/workspace.js.map +1 -0
- package/dist/core/capabilities/authoring/notes.js +141 -0
- package/dist/core/capabilities/authoring/notes.js.map +1 -0
- package/dist/core/capabilities/capabilities.js +124 -0
- package/dist/core/capabilities/capabilities.js.map +1 -0
- package/dist/core/capabilities/delta/doc.js +50 -0
- package/dist/core/capabilities/delta/doc.js.map +1 -0
- package/dist/core/capabilities/delta/overlay.js +129 -0
- package/dist/core/capabilities/delta/overlay.js.map +1 -0
- package/dist/core/capabilities/delta/summary.js +57 -0
- package/dist/core/capabilities/delta/summary.js.map +1 -0
- package/dist/core/capabilities/delta/tree.js +43 -0
- package/dist/core/capabilities/delta/tree.js.map +1 -0
- package/dist/core/capabilities/delta/uncovered.js +222 -0
- package/dist/core/capabilities/delta/uncovered.js.map +1 -0
- package/dist/core/capabilities/findings.js +342 -0
- package/dist/core/capabilities/findings.js.map +1 -0
- package/dist/core/capabilities/realizes/findings.js +181 -0
- package/dist/core/capabilities/realizes/findings.js.map +1 -0
- package/dist/core/capabilities/realizes/join.js +143 -0
- package/dist/core/capabilities/realizes/join.js.map +1 -0
- package/dist/core/capabilities/rollup.js +199 -0
- package/dist/core/capabilities/rollup.js.map +1 -0
- package/dist/core/capabilities/tree.js +126 -0
- package/dist/core/capabilities/tree.js.map +1 -0
- package/dist/core/capabilities/usecase-join.js +277 -0
- package/dist/core/capabilities/usecase-join.js.map +1 -0
- package/dist/core/coherence/authoring/scaffold.js +177 -0
- package/dist/core/coherence/authoring/scaffold.js.map +1 -0
- package/dist/core/coherence/authoring/sentinels.js +74 -0
- package/dist/core/coherence/authoring/sentinels.js.map +1 -0
- package/dist/core/coherence/coherence.js +251 -0
- package/dist/core/coherence/coherence.js.map +1 -0
- package/dist/core/coherence/declared.js +269 -0
- package/dist/core/coherence/declared.js.map +1 -0
- package/dist/core/coherence/events/declared.js +206 -0
- package/dist/core/coherence/events/declared.js.map +1 -0
- package/dist/core/coherence/events/events.js +204 -0
- package/dist/core/coherence/events/events.js.map +1 -0
- package/dist/core/coherence/events/grades.js +166 -0
- package/dist/core/coherence/events/grades.js.map +1 -0
- package/dist/core/coherence/events/lookups.js +122 -0
- package/dist/core/coherence/events/lookups.js.map +1 -0
- package/dist/core/coherence/events/merged.js +40 -0
- package/dist/core/coherence/events/merged.js.map +1 -0
- package/dist/core/coherence/living.js +198 -0
- package/dist/core/coherence/living.js.map +1 -0
- package/dist/core/coherence/lookups.js +155 -0
- package/dist/core/coherence/lookups.js.map +1 -0
- package/dist/core/coherence/pending.js +39 -0
- package/dist/core/coherence/pending.js.map +1 -0
- package/dist/core/conflict-markers.js +60 -0
- package/dist/core/conflict-markers.js.map +1 -0
- package/dist/core/delta/claims.js +73 -0
- package/dist/core/delta/claims.js.map +1 -0
- package/dist/core/delta/delta.js +296 -0
- package/dist/core/delta/delta.js.map +1 -0
- package/dist/core/delta/document.js +177 -0
- package/dist/core/delta/document.js.map +1 -0
- package/dist/core/delta/scope.js +48 -0
- package/dist/core/delta/scope.js.map +1 -0
- package/dist/core/delta/select.js +237 -0
- package/dist/core/delta/select.js.map +1 -0
- package/dist/core/dependencies/dependencies.js +141 -0
- package/dist/core/dependencies/dependencies.js.map +1 -0
- package/dist/core/dependencies/facts.js +165 -0
- package/dist/core/dependencies/facts.js.map +1 -0
- package/dist/core/dependencies/fanin.js +88 -0
- package/dist/core/dependencies/fanin.js.map +1 -0
- package/dist/core/dependencies/graph.js +106 -0
- package/dist/core/dependencies/graph.js.map +1 -0
- package/dist/core/diff/base-git.js +148 -0
- package/dist/core/diff/base-git.js.map +1 -0
- package/dist/core/diff/base-state.js +209 -0
- package/dist/core/diff/base-state.js.map +1 -0
- package/dist/core/diff/current-state.js +96 -0
- package/dist/core/diff/current-state.js.map +1 -0
- package/dist/core/diff/scope/changed-paths.js +188 -0
- package/dist/core/diff/scope/changed-paths.js.map +1 -0
- package/dist/core/diff/semantic.js +243 -0
- package/dist/core/diff/semantic.js.map +1 -0
- package/dist/core/diff/victims.js +247 -0
- package/dist/core/diff/victims.js.map +1 -0
- package/dist/core/docs.js +59 -55
- package/dist/core/docs.js.map +1 -1
- package/dist/core/doctor/agents.js +222 -0
- package/dist/core/doctor/agents.js.map +1 -0
- package/dist/core/doctor/config.js +135 -0
- package/dist/core/doctor/config.js.map +1 -0
- package/dist/core/doctor/doctor.js +275 -0
- package/dist/core/doctor/doctor.js.map +1 -0
- package/dist/core/doctor/report.js +13 -0
- package/dist/core/doctor/report.js.map +1 -0
- package/dist/core/doctor/residue.js +110 -0
- package/dist/core/doctor/residue.js.map +1 -0
- package/dist/core/document/apply.js +77 -0
- package/dist/core/document/apply.js.map +1 -0
- package/dist/core/{frontmatter.js → document/frontmatter.js} +48 -2
- package/dist/core/document/frontmatter.js.map +1 -0
- package/dist/core/document/grammar/directives.js +143 -0
- package/dist/core/document/grammar/directives.js.map +1 -0
- package/dist/core/document/parse.js +263 -0
- package/dist/core/document/parse.js.map +1 -0
- package/dist/core/document/scenarios.js +221 -0
- package/dist/core/document/scenarios.js.map +1 -0
- package/dist/core/document/spec.js +241 -0
- package/dist/core/document/spec.js.map +1 -0
- package/dist/core/{config.js → envelope/config.js} +79 -18
- package/dist/core/envelope/config.js.map +1 -0
- package/dist/core/envelope/json.js +175 -0
- package/dist/core/envelope/json.js.map +1 -0
- package/dist/core/{version.js → envelope/version.js} +3 -3
- package/dist/core/envelope/version.js.map +1 -0
- package/dist/core/explain/families.js +175 -0
- package/dist/core/explain/families.js.map +1 -0
- package/dist/core/explain/fix-tables.js +178 -0
- package/dist/core/explain/fix-tables.js.map +1 -0
- package/dist/core/explain/lookup.js +181 -0
- package/dist/core/explain/lookup.js.map +1 -0
- package/dist/core/explain/refusals.js +49 -0
- package/dist/core/explain/refusals.js.map +1 -0
- package/dist/core/explain/terms.js +144 -0
- package/dist/core/explain/terms.js.map +1 -0
- package/dist/core/explore/describe.js +105 -0
- package/dist/core/explore/describe.js.map +1 -0
- package/dist/core/{explore.js → explore/explore.js} +50 -101
- package/dist/core/explore/explore.js.map +1 -0
- package/dist/core/fleet-context.js +132 -77
- package/dist/core/fleet-context.js.map +1 -1
- package/dist/core/gate/checks.js +201 -0
- package/dist/core/gate/checks.js.map +1 -0
- package/dist/core/gate/partners.js +205 -0
- package/dist/core/gate/partners.js.map +1 -0
- package/dist/core/gate/report.js +47 -0
- package/dist/core/gate/report.js.map +1 -0
- package/dist/core/gate/verification.js +113 -0
- package/dist/core/gate/verification.js.map +1 -0
- package/dist/core/gherkin/digest.js +60 -0
- package/dist/core/gherkin/digest.js.map +1 -0
- package/dist/core/gherkin/emit.js +177 -0
- package/dist/core/gherkin/emit.js.map +1 -0
- package/dist/core/gherkin/read.js +76 -0
- package/dist/core/gherkin/read.js.map +1 -0
- package/dist/core/gherkin/stale.js +234 -0
- package/dist/core/gherkin/stale.js.map +1 -0
- package/dist/core/gherkin/stamp.js +77 -0
- package/dist/core/gherkin/stamp.js.map +1 -0
- package/dist/core/gherkin/steps/catalogue.js +154 -0
- package/dist/core/gherkin/steps/catalogue.js.map +1 -0
- package/dist/core/gherkin/steps/inventory.js +98 -0
- package/dist/core/gherkin/steps/inventory.js.map +1 -0
- package/dist/core/gherkin/steps/phrase.js +72 -0
- package/dist/core/gherkin/steps/phrase.js.map +1 -0
- package/dist/core/glossary/delta.js +106 -0
- package/dist/core/glossary/delta.js.map +1 -0
- package/dist/core/glossary/tree.js +86 -0
- package/dist/core/glossary/tree.js.map +1 -0
- package/dist/core/kernel/concurrency.js.map +1 -0
- package/dist/core/{document-bytes.js → kernel/document-bytes.js} +1 -1
- package/dist/core/kernel/document-bytes.js.map +1 -0
- package/dist/core/kernel/fences.js +42 -0
- package/dist/core/kernel/fences.js.map +1 -0
- package/dist/core/kernel/ids/capability.js +72 -0
- package/dist/core/kernel/ids/capability.js.map +1 -0
- package/dist/core/kernel/ids/dirs.js +33 -0
- package/dist/core/kernel/ids/dirs.js.map +1 -0
- package/dist/core/kernel/ids/feature.js +75 -0
- package/dist/core/kernel/ids/feature.js.map +1 -0
- package/dist/core/kernel/ids/service.js +156 -0
- package/dist/core/kernel/ids/service.js.map +1 -0
- package/dist/core/kernel/ids/subsystem.js +63 -0
- package/dist/core/kernel/ids/subsystem.js.map +1 -0
- package/dist/core/{path-safety.js → kernel/path-safety.js} +18 -4
- package/dist/core/kernel/path-safety.js.map +1 -0
- package/dist/core/kernel/records.js.map +1 -0
- package/dist/core/links/backlinks.js +68 -0
- package/dist/core/links/backlinks.js.map +1 -0
- package/dist/core/links/case.js +74 -0
- package/dist/core/links/case.js.map +1 -0
- package/dist/core/links/corpus.js +104 -0
- package/dist/core/links/corpus.js.map +1 -0
- package/dist/core/links/findings.js +110 -0
- package/dist/core/links/findings.js.map +1 -0
- package/dist/core/links/parse.js +236 -0
- package/dist/core/links/parse.js.map +1 -0
- package/dist/core/mcp/argv.js +111 -0
- package/dist/core/mcp/argv.js.map +1 -0
- package/dist/core/mcp/author-tools.js +72 -0
- package/dist/core/mcp/author-tools.js.map +1 -0
- package/dist/core/mcp/framing.js +46 -0
- package/dist/core/mcp/framing.js.map +1 -0
- package/dist/core/mcp/protocol.js +280 -0
- package/dist/core/mcp/protocol.js.map +1 -0
- package/dist/core/mcp/tools.js +314 -0
- package/dist/core/mcp/tools.js.map +1 -0
- package/dist/core/obligations/obligations.js +106 -0
- package/dist/core/obligations/obligations.js.map +1 -0
- package/dist/core/openapi/baseline/gate.js +166 -0
- package/dist/core/openapi/baseline/gate.js.map +1 -0
- package/dist/core/openapi/baseline/plan.js +112 -0
- package/dist/core/openapi/baseline/plan.js.map +1 -0
- package/dist/core/openapi/baseline/record.js +175 -0
- package/dist/core/openapi/baseline/record.js.map +1 -0
- package/dist/core/openapi/depth.js +122 -0
- package/dist/core/openapi/depth.js.map +1 -0
- package/dist/core/openapi/digest.js +189 -0
- package/dist/core/openapi/digest.js.map +1 -0
- package/dist/core/{openapi.js → openapi/doc.js} +44 -106
- package/dist/core/openapi/doc.js.map +1 -0
- package/dist/core/openapi/merge/components.js +234 -0
- package/dist/core/openapi/merge/components.js.map +1 -0
- package/dist/core/openapi/merge/error.js +26 -0
- package/dist/core/openapi/merge/error.js.map +1 -0
- package/dist/core/openapi/merge/markers.js +208 -0
- package/dist/core/openapi/merge/markers.js.map +1 -0
- package/dist/core/openapi/merge/merge.js +256 -0
- package/dist/core/openapi/merge/merge.js.map +1 -0
- package/dist/core/openapi/merge/pin.js +128 -0
- package/dist/core/openapi/merge/pin.js.map +1 -0
- package/dist/core/openspec/decide/capabilities.js +244 -0
- package/dist/core/openspec/decide/capabilities.js.map +1 -0
- package/dist/core/openspec/decide/dispositions.js +63 -0
- package/dist/core/openspec/decide/dispositions.js.map +1 -0
- package/dist/core/openspec/decide/renames.js +179 -0
- package/dist/core/openspec/decide/renames.js.map +1 -0
- package/dist/core/openspec/decide/verdict.js +84 -0
- package/dist/core/openspec/decide/verdict.js.map +1 -0
- package/dist/core/openspec/inventory.js +221 -0
- package/dist/core/openspec/inventory.js.map +1 -0
- package/dist/core/openspec/model/mapping.js +76 -0
- package/dist/core/openspec/model/mapping.js.map +1 -0
- package/dist/core/openspec/model/model.js +65 -0
- package/dist/core/openspec/model/model.js.map +1 -0
- package/dist/core/openspec/scan/changes.js +235 -0
- package/dist/core/openspec/scan/changes.js.map +1 -0
- package/dist/core/openspec/scan/config.js +200 -0
- package/dist/core/openspec/scan/config.js.map +1 -0
- package/dist/core/openspec/scan/shape.js +260 -0
- package/dist/core/openspec/scan/shape.js.map +1 -0
- package/dist/core/openspec/scan/walk.js +133 -0
- package/dist/core/openspec/scan/walk.js.map +1 -0
- package/dist/core/openspec/scan/workspace.js +71 -0
- package/dist/core/openspec/scan/workspace.js.map +1 -0
- package/dist/core/owners/codeowners.js +141 -0
- package/dist/core/owners/codeowners.js.map +1 -0
- package/dist/core/pack/features.js +120 -0
- package/dist/core/pack/features.js.map +1 -0
- package/dist/core/pack/joins.js +112 -0
- package/dist/core/pack/joins.js.map +1 -0
- package/dist/core/pack/living.js +118 -0
- package/dist/core/pack/living.js.map +1 -0
- package/dist/core/pack/pack.js +76 -0
- package/dist/core/pack/pack.js.map +1 -0
- package/dist/core/permissions/permissions.js +87 -0
- package/dist/core/permissions/permissions.js.map +1 -0
- package/dist/core/projection/api.js +84 -0
- package/dist/core/projection/api.js.map +1 -0
- package/dist/core/projection/arch-slice.js +63 -0
- package/dist/core/projection/arch-slice.js.map +1 -0
- package/dist/core/projection/events.js +34 -0
- package/dist/core/projection/events.js.map +1 -0
- package/dist/core/provenance/findings.js +208 -0
- package/dist/core/provenance/findings.js.map +1 -0
- package/dist/core/provenance/git.js +225 -0
- package/dist/core/provenance/git.js.map +1 -0
- package/dist/core/provenance/gitq/moves.js +182 -0
- package/dist/core/provenance/gitq/moves.js.map +1 -0
- package/dist/core/provenance/gitq/vouched-ancestor.js +179 -0
- package/dist/core/provenance/gitq/vouched-ancestor.js.map +1 -0
- package/dist/core/provenance/sample/scope.js +238 -0
- package/dist/core/provenance/sample/scope.js.map +1 -0
- package/dist/core/provenance/sample/sections.js +61 -0
- package/dist/core/provenance/sample/sections.js.map +1 -0
- package/dist/core/provenance/sources.js +263 -0
- package/dist/core/provenance/sources.js.map +1 -0
- package/dist/core/provenance/stamp.js +232 -0
- package/dist/core/provenance/stamp.js.map +1 -0
- package/dist/core/provenance/walk.js +157 -0
- package/dist/core/provenance/walk.js.map +1 -0
- package/dist/core/repo/authored/paths.js +192 -0
- package/dist/core/repo/authored/paths.js.map +1 -0
- package/dist/core/repo/entries.js +126 -0
- package/dist/core/repo/entries.js.map +1 -0
- package/dist/core/repo/paths.js +236 -0
- package/dist/core/repo/paths.js.map +1 -0
- package/dist/core/repo/repo.js +248 -0
- package/dist/core/repo/repo.js.map +1 -0
- package/dist/core/repo/service-target.js +153 -0
- package/dist/core/repo/service-target.js.map +1 -0
- package/dist/core/repo/state.js +58 -0
- package/dist/core/repo/state.js.map +1 -0
- package/dist/core/repo/tree/find.js +52 -0
- package/dist/core/repo/tree/find.js.map +1 -0
- package/dist/core/repo/tree/fs.js +64 -0
- package/dist/core/repo/tree/fs.js.map +1 -0
- package/dist/core/repo/tree/marker.js +61 -0
- package/dist/core/repo/tree/marker.js.map +1 -0
- package/dist/core/repo/tree/views.js +172 -0
- package/dist/core/repo/tree/views.js.map +1 -0
- package/dist/core/repo/tree/walk.js +234 -0
- package/dist/core/repo/tree/walk.js.map +1 -0
- package/dist/core/results.js +99 -2
- package/dist/core/results.js.map +1 -1
- package/dist/core/scaffold/landscape.js +131 -0
- package/dist/core/scaffold/landscape.js.map +1 -0
- package/dist/core/scaffold/readme.js +84 -0
- package/dist/core/scaffold/readme.js.map +1 -0
- package/dist/core/staging/commit.js +241 -0
- package/dist/core/staging/commit.js.map +1 -0
- package/dist/core/staging/interrupted.js +96 -0
- package/dist/core/staging/interrupted.js.map +1 -0
- package/dist/core/staging/lock.js +206 -0
- package/dist/core/staging/lock.js.map +1 -0
- package/dist/core/staging/recovery/finding.js +35 -0
- package/dist/core/staging/recovery/finding.js.map +1 -0
- package/dist/core/staging/recovery/intent.js +98 -0
- package/dist/core/staging/recovery/intent.js.map +1 -0
- package/dist/core/staging/recovery/recover.js +126 -0
- package/dist/core/staging/recovery/recover.js.map +1 -0
- package/dist/core/staging/recovery/residue.js +62 -0
- package/dist/core/staging/recovery/residue.js.map +1 -0
- package/dist/core/staging/snapshot.js +202 -0
- package/dist/core/staging/snapshot.js.map +1 -0
- package/dist/core/staging/txn/forward.js +171 -0
- package/dist/core/staging/txn/forward.js.map +1 -0
- package/dist/core/staging/txn/journal.js +139 -0
- package/dist/core/staging/txn/journal.js.map +1 -0
- package/dist/core/staging/txn/transaction.js +75 -0
- package/dist/core/staging/txn/transaction.js.map +1 -0
- package/dist/core/staging/writes.js +113 -0
- package/dist/core/staging/writes.js.map +1 -0
- package/dist/core/status/actions/execution.js +49 -0
- package/dist/core/status/actions/execution.js.map +1 -0
- package/dist/core/status/contracts.js +102 -0
- package/dist/core/status/contracts.js.map +1 -0
- package/dist/core/status/feature/artifacts.js +171 -0
- package/dist/core/status/feature/artifacts.js.map +1 -0
- package/dist/core/status/feature/feature.js +207 -0
- package/dist/core/status/feature/feature.js.map +1 -0
- package/dist/core/status/feature/next.js +150 -0
- package/dist/core/status/feature/next.js.map +1 -0
- package/dist/core/status/feature/state.js +21 -0
- package/dist/core/status/feature/state.js.map +1 -0
- package/dist/core/status/fleet/fleet.js +187 -0
- package/dist/core/status/fleet/fleet.js.map +1 -0
- package/dist/core/status/fleet/next.js +207 -0
- package/dist/core/status/fleet/next.js.map +1 -0
- package/dist/core/status/interrupted.js +97 -0
- package/dist/core/status/interrupted.js.map +1 -0
- package/dist/core/status/report.js +31 -0
- package/dist/core/status/report.js.map +1 -0
- package/dist/core/status/scan.js +41 -0
- package/dist/core/status/scan.js.map +1 -0
- package/dist/core/status/verification.js +201 -0
- package/dist/core/status/verification.js.map +1 -0
- package/dist/core/usecases/capability.js +169 -0
- package/dist/core/usecases/capability.js.map +1 -0
- package/dist/core/usecases/delta/claims.js +116 -0
- package/dist/core/usecases/delta/claims.js.map +1 -0
- package/dist/core/usecases/delta/flows.js +103 -0
- package/dist/core/usecases/delta/flows.js.map +1 -0
- package/dist/core/usecases/delta/overlay.js +235 -0
- package/dist/core/usecases/delta/overlay.js.map +1 -0
- package/dist/core/usecases/fleet.js +137 -0
- package/dist/core/usecases/fleet.js.map +1 -0
- package/dist/core/usecases/operations.js +111 -0
- package/dist/core/usecases/operations.js.map +1 -0
- package/dist/core/usecases/place.js +44 -0
- package/dist/core/usecases/place.js.map +1 -0
- package/dist/core/usecases/touch.js +101 -0
- package/dist/core/usecases/touch.js.map +1 -0
- package/dist/core/verify/answers.js +173 -0
- package/dist/core/verify/answers.js.map +1 -0
- package/dist/core/verify/build.js +150 -0
- package/dist/core/verify/build.js.map +1 -0
- package/dist/core/verify/checklist.js +201 -0
- package/dist/core/verify/checklist.js.map +1 -0
- package/dist/core/verify/claims/identity.js +61 -0
- package/dist/core/verify/claims/identity.js.map +1 -0
- package/dist/core/verify/cross/diff.js +97 -0
- package/dist/core/verify/cross/diff.js.map +1 -0
- package/dist/core/verify/evidence/contract.js +169 -0
- package/dist/core/verify/evidence/contract.js.map +1 -0
- package/dist/core/verify/file.js +257 -0
- package/dist/core/verify/file.js.map +1 -0
- package/dist/core/verify/pins/lint.js +157 -0
- package/dist/core/verify/pins/lint.js.map +1 -0
- package/dist/core/verify/pins/pin.js +95 -0
- package/dist/core/verify/pins/pin.js.map +1 -0
- package/dist/core/verify/record.js +142 -0
- package/dist/core/verify/record.js.map +1 -0
- package/dist/core/verify/store/commit.js +89 -0
- package/dist/core/verify/store/commit.js.map +1 -0
- package/dist/core/verify/store/render.js +57 -0
- package/dist/core/verify/store/render.js.map +1 -0
- package/dist/core/vocabulary/health.js +91 -0
- package/dist/core/vocabulary/health.js.map +1 -0
- package/dist/core/vocabulary/issue.js +55 -0
- package/dist/core/vocabulary/issue.js.map +1 -0
- package/dist/core/vocabulary/maturity.js +145 -0
- package/dist/core/vocabulary/maturity.js.map +1 -0
- package/dist/core/{report.js → vocabulary/report.js} +30 -2
- package/dist/core/vocabulary/report.js.map +1 -0
- package/dist/core/vocabulary/steps.js +266 -0
- package/dist/core/vocabulary/steps.js.map +1 -0
- package/dist/core/workspace/discover.js +239 -0
- package/dist/core/workspace/discover.js.map +1 -0
- package/dist/core/workspace/render.js +52 -0
- package/dist/core/workspace/render.js.map +1 -0
- package/examples/README.md +189 -0
- package/examples/docs/architecture/adrs/0001-transactional-outbox.md +33 -0
- package/examples/docs/architecture/capabilities.yaml +31 -0
- package/examples/docs/architecture/landscape.likec4 +280 -0
- package/examples/docs/architecture/obligations.yaml +23 -0
- package/examples/docs/architecture/permissions.yaml +50 -0
- package/examples/docs/architecture/subsystems.likec4 +11 -0
- package/examples/docs/architecture/usecases/order-notification.likec4 +65 -0
- package/examples/docs/capabilities/checkout/spec.md +55 -0
- package/examples/docs/capabilities/identity/tokens/spec.md +42 -0
- package/examples/docs/capabilities/order-notifications/spec.md +43 -0
- package/examples/docs/features/FEAT-101-payment-splitting/adrs/0001-dedicated-split-service.md +18 -0
- package/examples/docs/features/FEAT-101-payment-splitting/delta.likec4 +94 -0
- package/examples/docs/features/FEAT-101-payment-splitting/intent.md +44 -0
- package/examples/docs/features/FEAT-101-payment-splitting/specs/checkout-web/spec.md +21 -0
- package/examples/docs/features/FEAT-101-payment-splitting/specs/payment-service/asyncapi.yaml +129 -0
- package/examples/docs/features/FEAT-101-payment-splitting/specs/payment-service/spec.md +36 -0
- package/examples/docs/features/FEAT-101-payment-splitting/specs/payment-split-service/arch.spec.md +40 -0
- package/examples/docs/features/FEAT-101-payment-splitting/specs/payment-split-service/openapi.yaml +116 -0
- package/examples/docs/features/FEAT-101-payment-splitting/specs/payment-split-service/spec.md +55 -0
- package/examples/docs/features/FEAT-112-retire-order-v1/intent.md +35 -0
- package/examples/docs/features/FEAT-112-retire-order-v1/specs/order-service/openapi.yaml +28 -0
- package/examples/docs/features/FEAT-112-retire-order-v1/specs/order-service/spec.md +17 -0
- package/examples/docs/features/archive/FEAT-088-refunds/.loam-before/files/architecture/landscape.likec4 +253 -0
- package/examples/docs/features/archive/FEAT-088-refunds/.loam-before/files/services/order-service/spec.md +121 -0
- package/examples/docs/features/archive/FEAT-088-refunds/.loam-before/files/services/payment-service/openapi.yaml +111 -0
- package/examples/docs/features/archive/FEAT-088-refunds/.loam-before/files/services/payment-service/spec.md +58 -0
- package/examples/docs/features/archive/FEAT-088-refunds/.loam-before/manifest.json +32 -0
- package/examples/docs/features/archive/FEAT-088-refunds/adrs/0001-refunds-stay-in-payment-service.md +26 -0
- package/examples/docs/features/archive/FEAT-088-refunds/delta.likec4 +28 -0
- package/examples/docs/features/archive/FEAT-088-refunds/intent.md +26 -0
- package/examples/docs/features/archive/FEAT-088-refunds/specs/order-service/spec.md +37 -0
- package/examples/docs/features/archive/FEAT-088-refunds/specs/payment-service/openapi.yaml +70 -0
- package/examples/docs/features/archive/FEAT-088-refunds/specs/payment-service/spec.md +23 -0
- package/examples/docs/features/archive/FEAT-088-refunds/verification.yaml +76 -0
- package/examples/docs/features/archive/FEAT-120-refund-notification/.loam-before/files/architecture/landscape.likec4 +275 -0
- package/examples/docs/features/archive/FEAT-120-refund-notification/.loam-before/files/services/payment-service/asyncapi.yaml +96 -0
- package/examples/docs/features/archive/FEAT-120-refund-notification/.loam-before/files/services/payment-service/spec.md +82 -0
- package/examples/docs/features/archive/FEAT-120-refund-notification/.loam-before/files/services/platform/notification-service/asyncapi.yaml +164 -0
- package/examples/docs/features/archive/FEAT-120-refund-notification/.loam-before/files/services/platform/notification-service/spec.md +91 -0
- package/examples/docs/features/archive/FEAT-120-refund-notification/.loam-before/manifest.json +46 -0
- package/examples/docs/features/archive/FEAT-120-refund-notification/delta.likec4 +36 -0
- package/examples/docs/features/archive/FEAT-120-refund-notification/intent.md +22 -0
- package/examples/docs/features/archive/FEAT-120-refund-notification/scenario-report.json +10 -0
- package/examples/docs/features/archive/FEAT-120-refund-notification/specs/notification-service/asyncapi.yaml +208 -0
- package/examples/docs/features/archive/FEAT-120-refund-notification/specs/notification-service/spec.md +24 -0
- package/examples/docs/features/archive/FEAT-120-refund-notification/specs/payment-service/asyncapi.yaml +137 -0
- package/examples/docs/features/archive/FEAT-120-refund-notification/specs/payment-service/spec.md +31 -0
- package/examples/docs/features/archive/FEAT-120-refund-notification/verification.yaml +84 -0
- package/examples/docs/glossary/order.md +15 -0
- package/examples/docs/glossary/payments/authorization.md +14 -0
- package/examples/docs/likec4.config.json +9 -0
- package/examples/docs/loam.json +3 -0
- package/examples/docs/services/checkout-web/model.likec4 +109 -0
- package/examples/docs/services/checkout-web/spec.md +91 -0
- package/examples/docs/services/checkout-web/ui/pages/checkout.page.yaml +40 -0
- package/examples/docs/services/checkout-web/ui/pages/order-status.page.yaml +24 -0
- package/examples/docs/services/checkout-web/ui/pages/split-payment.page.yaml +26 -0
- package/examples/docs/services/order-service/adrs/0001-orchestrated-order-lifecycle.md +52 -0
- package/examples/docs/services/order-service/arch.spec.md +127 -0
- package/examples/docs/services/order-service/asyncapi.yaml +108 -0
- package/examples/docs/services/order-service/health.yaml +57 -0
- package/examples/docs/services/order-service/model.likec4 +138 -0
- package/examples/docs/services/order-service/openapi.yaml +257 -0
- package/examples/docs/services/order-service/runbook.md +62 -0
- package/examples/docs/services/order-service/spec.md +134 -0
- package/examples/docs/services/payment-service/adrs/0001-payments-outbox.md +18 -0
- package/examples/docs/services/payment-service/adrs/0002-idempotency-keys-on-authorization.md +27 -0
- package/examples/docs/services/payment-service/arch.spec.md +73 -0
- package/examples/docs/services/payment-service/asyncapi.yaml +130 -0
- package/examples/docs/services/payment-service/health.yaml +39 -0
- package/examples/docs/services/payment-service/model.likec4 +67 -0
- package/examples/docs/services/payment-service/openapi.yaml +151 -0
- package/examples/docs/services/payment-service/runbook.md +37 -0
- package/examples/docs/services/payment-service/spec.md +88 -0
- package/examples/docs/services/platform/identity-service/adrs/0001-token-introspection-over-local-validation.md +60 -0
- package/examples/docs/services/platform/identity-service/arch.spec.md +117 -0
- package/examples/docs/services/platform/identity-service/health.yaml +70 -0
- package/examples/docs/services/platform/identity-service/model.likec4 +83 -0
- package/examples/docs/services/platform/identity-service/openapi.yaml +197 -0
- package/examples/docs/services/platform/identity-service/runbook.md +95 -0
- package/examples/docs/services/platform/identity-service/spec.md +119 -0
- package/examples/docs/services/platform/notification-service/asyncapi.yaml +199 -0
- package/examples/docs/services/platform/notification-service/health.yaml +35 -0
- package/examples/docs/services/platform/notification-service/model.likec4 +118 -0
- package/examples/docs/services/platform/notification-service/runbook.md +48 -0
- package/examples/docs/services/platform/notification-service/spec.md +112 -0
- package/examples/docs/services/platform/subsystem.yaml +2 -0
- package/examples/fleet.yaml +57 -0
- package/package.json +19 -6
- package/dist/commands/adopt.js +0 -240
- package/dist/commands/adopt.js.map +0 -1
- package/dist/commands/archive.js +0 -1313
- package/dist/commands/archive.js.map +0 -1
- package/dist/commands/delta.js +0 -388
- package/dist/commands/delta.js.map +0 -1
- package/dist/commands/docs-repo-gate.js.map +0 -1
- package/dist/commands/format.js +0 -17
- package/dist/commands/format.js.map +0 -1
- package/dist/commands/gherkin.js +0 -309
- package/dist/commands/gherkin.js.map +0 -1
- package/dist/commands/init.js.map +0 -1
- package/dist/commands/list.js +0 -291
- package/dist/commands/list.js.map +0 -1
- package/dist/commands/migrate-openspec.js +0 -950
- package/dist/commands/migrate-openspec.js.map +0 -1
- package/dist/commands/new.js +0 -413
- package/dist/commands/new.js.map +0 -1
- package/dist/commands/rebase.js +0 -356
- package/dist/commands/rebase.js.map +0 -1
- package/dist/commands/show.js.map +0 -1
- package/dist/commands/status.js +0 -254
- package/dist/commands/status.js.map +0 -1
- package/dist/commands/unarchive.js.map +0 -1
- package/dist/commands/validate.js +0 -1591
- package/dist/commands/validate.js.map +0 -1
- package/dist/commands/verify.js +0 -775
- package/dist/commands/verify.js.map +0 -1
- package/dist/commands/vouch.js +0 -343
- package/dist/commands/vouch.js.map +0 -1
- package/dist/core/agent.js +0 -2065
- package/dist/core/agent.js.map +0 -1
- package/dist/core/agents-stamp.js +0 -68
- package/dist/core/agents-stamp.js.map +0 -1
- package/dist/core/arch.js +0 -130
- package/dist/core/arch.js.map +0 -1
- package/dist/core/brief.js +0 -733
- package/dist/core/brief.js.map +0 -1
- package/dist/core/coherence.js +0 -577
- package/dist/core/coherence.js.map +0 -1
- package/dist/core/concurrency.js.map +0 -1
- package/dist/core/config.js.map +0 -1
- package/dist/core/delta.js +0 -476
- package/dist/core/delta.js.map +0 -1
- package/dist/core/dependencies.js +0 -356
- package/dist/core/dependencies.js.map +0 -1
- package/dist/core/doctor.js +0 -559
- package/dist/core/doctor.js.map +0 -1
- package/dist/core/document-bytes.js.map +0 -1
- package/dist/core/explore.js.map +0 -1
- package/dist/core/frontmatter.js.map +0 -1
- package/dist/core/gherkin.js +0 -463
- package/dist/core/gherkin.js.map +0 -1
- package/dist/core/health.js +0 -67
- package/dist/core/health.js.map +0 -1
- package/dist/core/ids.js +0 -122
- package/dist/core/ids.js.map +0 -1
- package/dist/core/issue.js +0 -9
- package/dist/core/issue.js.map +0 -1
- package/dist/core/json.js +0 -74
- package/dist/core/json.js.map +0 -1
- package/dist/core/likec4.js +0 -479
- package/dist/core/likec4.js.map +0 -1
- package/dist/core/maturity.js +0 -66
- package/dist/core/maturity.js.map +0 -1
- package/dist/core/openapi-merge.js +0 -552
- package/dist/core/openapi-merge.js.map +0 -1
- package/dist/core/openapi.js.map +0 -1
- package/dist/core/openspec-inventory.js +0 -1440
- package/dist/core/openspec-inventory.js.map +0 -1
- package/dist/core/path-safety.js.map +0 -1
- package/dist/core/provenance.js +0 -720
- package/dist/core/provenance.js.map +0 -1
- package/dist/core/records.js.map +0 -1
- package/dist/core/repo.js +0 -436
- package/dist/core/repo.js.map +0 -1
- package/dist/core/report.js.map +0 -1
- package/dist/core/spec.js +0 -481
- package/dist/core/spec.js.map +0 -1
- package/dist/core/staging.js +0 -890
- package/dist/core/staging.js.map +0 -1
- package/dist/core/status.js +0 -1104
- package/dist/core/status.js.map +0 -1
- package/dist/core/steps.js +0 -72
- package/dist/core/steps.js.map +0 -1
- package/dist/core/verify.js +0 -807
- package/dist/core/verify.js.map +0 -1
- package/dist/core/version.js.map +0 -1
- /package/dist/core/{concurrency.js → kernel/concurrency.js} +0 -0
- /package/dist/core/{records.js → kernel/records.js} +0 -0
|
@@ -0,0 +1,202 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The undo snapshot: the bytes `archive` is about to overwrite, parked inside
|
|
3
|
+
* the feature directory so they travel with it into `features/archive/`.
|
|
4
|
+
*
|
|
5
|
+
* Separate from the commit because the merge is not invertible — a MODIFIED
|
|
6
|
+
* requirement's previous text appears nowhere in the delta, and a landscape
|
|
7
|
+
* rewritten by hand since cannot be un-rewritten by re-reading one. `unarchive`
|
|
8
|
+
* puts bytes back; it does not recompute them. Nothing on the commit path reads
|
|
9
|
+
* this module: only `archive` writes a snapshot and only `unarchive` honours one.
|
|
10
|
+
*/
|
|
11
|
+
import { existsSync } from "node:fs";
|
|
12
|
+
import { mkdir, readFile, writeFile } from "node:fs/promises";
|
|
13
|
+
import { dirname, join } from "node:path";
|
|
14
|
+
import { repoPath } from "../envelope/json.js";
|
|
15
|
+
import { resolvePortableFileInsideLexically, UnsafePathError } from "../kernel/path-safety.js";
|
|
16
|
+
// Every `isRecord` below guards a record loam itself wrote and is reading back
|
|
17
|
+
// — the lock, the snapshot manifest — whose fields are asked for on the very
|
|
18
|
+
// next line, inside a REFUSAL path. `JSON.parse` answers with any JSON value,
|
|
19
|
+
// not with an object, and that is where a thrown `TypeError` costs the most: it
|
|
20
|
+
// does not make the command fail safely, it replaces a designed refusal with
|
|
21
|
+
// `internal`.
|
|
22
|
+
import { isRecord } from "../kernel/records.js";
|
|
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;
|
|
45
|
+
/** A snapshot directory that must not be replaced, because it is the only copy of the true pre-images. */
|
|
46
|
+
export class SnapshotClobberError extends Error {
|
|
47
|
+
dir;
|
|
48
|
+
name = "SnapshotClobberError";
|
|
49
|
+
constructor(dir, detail) {
|
|
50
|
+
super(`${dir} already holds an undo snapshot that does NOT match the living docs (${detail}) — a previous archive of ` +
|
|
51
|
+
`this feature wrote to the living docs and never took them back, and these are the only copies of what they ` +
|
|
52
|
+
`said before. Overwriting them would make that merge permanent and unreportable. Run \`loam archive\` again ` +
|
|
53
|
+
`after restoring the living docs from version control, or delete ${SNAPSHOT_DIR}/ if you know the merge never landed.`);
|
|
54
|
+
this.dir = dir;
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
export function snapshotDir(featureDir) {
|
|
58
|
+
return join(featureDir, SNAPSHOT_DIR);
|
|
59
|
+
}
|
|
60
|
+
export async function writeSnapshot(request) {
|
|
61
|
+
const { featureDir, docsDir, staged } = request;
|
|
62
|
+
const { featureId, dirName } = request.feature;
|
|
63
|
+
const dir = snapshotDir(featureDir);
|
|
64
|
+
await assertSnapshotReplaceable(dir, docsDir, request.serviceDirOf);
|
|
65
|
+
// A leftover from a rolled-back run would describe a merge that never happened.
|
|
66
|
+
await quietRm(dir);
|
|
67
|
+
const files = [];
|
|
68
|
+
for (const s of staged) {
|
|
69
|
+
const rel = repoPath(docsDir, s.write.path);
|
|
70
|
+
// services/ entries are re-keyed by (service, artifact) so they survive the
|
|
71
|
+
// service directory moving between archive and unarchive; the literal path
|
|
72
|
+
// is still recorded — it keys the pre-image below, and it is the fallback.
|
|
73
|
+
const key = request.serviceKeyOf(rel);
|
|
74
|
+
files.push({
|
|
75
|
+
path: rel,
|
|
76
|
+
...key,
|
|
77
|
+
existed: s.before !== null,
|
|
78
|
+
after: sha256(s.content ?? Buffer.alloc(0)),
|
|
79
|
+
before: s.before === null ? null : sha256(s.before),
|
|
80
|
+
});
|
|
81
|
+
if (s.before !== null) {
|
|
82
|
+
const dest = join(dir, "files", rel);
|
|
83
|
+
await mkdir(dirname(dest), { recursive: true });
|
|
84
|
+
await writeFile(dest, s.before);
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
const manifest = {
|
|
88
|
+
version: SNAPSHOT_VERSION,
|
|
89
|
+
feature: featureId,
|
|
90
|
+
dirName,
|
|
91
|
+
archivedAt: new Date().toISOString(),
|
|
92
|
+
files,
|
|
93
|
+
};
|
|
94
|
+
// Manifest last: its presence is what says the pre-images beside it are complete.
|
|
95
|
+
await mkdir(dir, { recursive: true });
|
|
96
|
+
await writeFile(join(dir, SNAPSHOT_MANIFEST), JSON.stringify(manifest, null, 2) + "\n", "utf8");
|
|
97
|
+
}
|
|
98
|
+
/**
|
|
99
|
+
* Refuse to replace a snapshot whose pre-images are still the only record of
|
|
100
|
+
* what the living docs said.
|
|
101
|
+
*
|
|
102
|
+
* A snapshot in an ACTIVE feature directory means a previous archive of this
|
|
103
|
+
* same feature wrote it — and since a clean rollback deletes it, finding one
|
|
104
|
+
* means that run either died mid-commit or reported `rollback-incomplete`. If
|
|
105
|
+
* its pre-images still match the living docs, nothing of that run landed and
|
|
106
|
+
* the snapshot is stale bookkeeping. If they do not, the living docs carry
|
|
107
|
+
* that run's writes, these files are the only way back, and the unconditional
|
|
108
|
+
* clear turned a repairable half-merge into a permanent one — after which
|
|
109
|
+
* `unarchive` restored the half-merged text and said the docs were back.
|
|
110
|
+
*
|
|
111
|
+
* Version 2 manifests are graded by the same staleness question over their
|
|
112
|
+
* literal paths — refusing them as "a different loam" would turn every leftover
|
|
113
|
+
* version-2 snapshot into a permanent archive refusal on upgrade.
|
|
114
|
+
*/
|
|
115
|
+
async function assertSnapshotReplaceable(dir, docsDir, serviceDirOf) {
|
|
116
|
+
const manifestPath = join(dir, SNAPSHOT_MANIFEST);
|
|
117
|
+
// The manifest is written LAST, so a `files/` tree without one is a snapshot
|
|
118
|
+
// whose run died before it could swap anything: it describes no merge.
|
|
119
|
+
if (!existsSync(manifestPath))
|
|
120
|
+
return;
|
|
121
|
+
let manifest;
|
|
122
|
+
try {
|
|
123
|
+
manifest = JSON.parse(await readFile(manifestPath, "utf8"));
|
|
124
|
+
}
|
|
125
|
+
catch {
|
|
126
|
+
throw new SnapshotClobberError(dir, `${SNAPSHOT_MANIFEST} cannot be read, so nothing can say whether it is stale`);
|
|
127
|
+
}
|
|
128
|
+
// The cast that used to stand here asserted the shape rather than checking
|
|
129
|
+
// it: well-formed JSON that is not an object answered `undefined` to every
|
|
130
|
+
// field below and slipped through as "a different loam", and a bare `null`
|
|
131
|
+
// threw a TypeError out of the refusal itself. Both left this directory —
|
|
132
|
+
// which may hold the only copies of what the living docs said — one step from
|
|
133
|
+
// being replaced, without the staleness question ever having been asked.
|
|
134
|
+
if (!isRecord(manifest)) {
|
|
135
|
+
throw new SnapshotClobberError(dir, `${SNAPSHOT_MANIFEST} cannot be read, so nothing can say whether it is stale`);
|
|
136
|
+
}
|
|
137
|
+
if ((manifest.version !== SNAPSHOT_VERSION && manifest.version !== SNAPSHOT_COMPAT_VERSION) ||
|
|
138
|
+
!Array.isArray(manifest.files)) {
|
|
139
|
+
throw new SnapshotClobberError(dir, `it was written by a different loam and cannot be checked against the living docs`);
|
|
140
|
+
}
|
|
141
|
+
// `Array.isArray` narrows to `any[]`; naming the element type here is what
|
|
142
|
+
// keeps that `any` from spreading past the one row being read.
|
|
143
|
+
const rows = manifest.files;
|
|
144
|
+
const landed = [];
|
|
145
|
+
for (const row of rows) {
|
|
146
|
+
const entry = readSnapshotClaim(row);
|
|
147
|
+
if (entry === null) {
|
|
148
|
+
throw new SnapshotClobberError(dir, `${SNAPSHOT_MANIFEST} is malformed, so nothing can say whether it is stale`);
|
|
149
|
+
}
|
|
150
|
+
let target;
|
|
151
|
+
try {
|
|
152
|
+
// These paths are data, and they are the ones this function goes and
|
|
153
|
+
// reads. They name living documents, so containment is lexical: a docs
|
|
154
|
+
// repo may legitimately mount a service directory through a symlink, and
|
|
155
|
+
// `unarchive` resolves the same field the same way before it writes back.
|
|
156
|
+
target = resolvePortableFileInsideLexically(docsDir, entry.path, `snapshot path '${entry.path}'`);
|
|
157
|
+
// A re-keyed entry's CURRENT location is wherever the enumeration says
|
|
158
|
+
// its service lives today — grading staleness at the old address would
|
|
159
|
+
// call a merely-moved file a landed merge. The literal path was still
|
|
160
|
+
// resolved above on purpose: its containment is part of what "loam wrote
|
|
161
|
+
// this manifest" means, and it stays the pre-image's key. An id the
|
|
162
|
+
// enumeration no longer answers falls back to that literal path.
|
|
163
|
+
if (entry.service !== undefined && entry.artifact !== undefined) {
|
|
164
|
+
const current = serviceDirOf(entry.service);
|
|
165
|
+
if (current !== null) {
|
|
166
|
+
target = resolvePortableFileInsideLexically(docsDir, `${current}/${entry.artifact}`, `snapshot path '${entry.path}'`);
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
catch (err) {
|
|
171
|
+
if (!(err instanceof UnsafePathError))
|
|
172
|
+
throw err;
|
|
173
|
+
throw new SnapshotClobberError(dir, `${message(err)}, so nothing can say whether it is stale`);
|
|
174
|
+
}
|
|
175
|
+
const now = existsSync(target) ? sha256(await readFile(target)) : null;
|
|
176
|
+
// `existed: false` means archive created the file, so "nothing landed"
|
|
177
|
+
// means it is still absent.
|
|
178
|
+
const untouched = entry.existed ? now === entry.before : now === null;
|
|
179
|
+
if (!untouched)
|
|
180
|
+
landed.push(entry.path);
|
|
181
|
+
}
|
|
182
|
+
if (landed.length > 0)
|
|
183
|
+
throw new SnapshotClobberError(dir, `${landed.length} file(s) already carry that merge: ${landed.join(", ")}`);
|
|
184
|
+
}
|
|
185
|
+
function readSnapshotClaim(value) {
|
|
186
|
+
if (!isRecord(value))
|
|
187
|
+
return null;
|
|
188
|
+
const { path, existed, before, service, artifact } = value;
|
|
189
|
+
if (typeof path !== "string" || typeof existed !== "boolean")
|
|
190
|
+
return null;
|
|
191
|
+
let key;
|
|
192
|
+
if (service === undefined && artifact === undefined)
|
|
193
|
+
key = {};
|
|
194
|
+
else if (typeof service === "string" && typeof artifact === "string")
|
|
195
|
+
key = { service, artifact };
|
|
196
|
+
else
|
|
197
|
+
return null;
|
|
198
|
+
if (existed)
|
|
199
|
+
return isDigest(before) ? { path, existed, before, ...key } : null;
|
|
200
|
+
return before === null ? { path, existed, before, ...key } : null;
|
|
201
|
+
}
|
|
202
|
+
//# sourceMappingURL=snapshot.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"snapshot.js","sourceRoot":"","sources":["../../../src/core/staging/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 a record loam itself wrote and is reading back\n// — the lock, the snapshot manifest — whose fields are asked for on the very\n// next line, inside a REFUSAL path. `JSON.parse` answers with any JSON value,\n// not with an object, and that is where a thrown `TypeError` costs the most: it\n// does not make the command fail safely, it replaces a designed refusal with\n// `internal`.\nimport { isRecord } from \"../kernel/records.js\";\nimport { isDigest, sha256, type StagedWrite } from \"./writes.js\";\nimport { message, quietRm } from \"./commit.js\";\n\n/** Where archive parks the pre-images, inside the feature directory so they travel with it — the module doc above 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\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\nexport interface SnapshotManifest {\n version: number;\n feature: string;\n dirName: string;\n archivedAt: string;\n files: SnapshotEntry[];\n}\n\nexport function snapshotDir(featureDir: string): string {\n return join(featureDir, SNAPSHOT_DIR);\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\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 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"]}
|
|
@@ -0,0 +1,171 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Roll a version-2 or version-3 journal FORWARD: finish the renames a killed
|
|
3
|
+
* commit did not reach, from the staged temps that are already durable on
|
|
4
|
+
* disk (and, for a version-3 record, the directory renames after them).
|
|
5
|
+
*
|
|
6
|
+
* Forward and not undo, because that is what is actually durable: the
|
|
7
|
+
* after-bytes were written and (per stageWrites) fully on disk before the
|
|
8
|
+
* journal existed, while the OVERWRITTEN files' pre-images are durable
|
|
9
|
+
* nowhere — the rename that replaced them destroyed the only copy. The plan
|
|
10
|
+
* was validated completely before the first rename, so the post-state is
|
|
11
|
+
* exactly what a successful run would have produced; unarchive's
|
|
12
|
+
* finish-never-undo recovery is the precedent.
|
|
13
|
+
*
|
|
14
|
+
* The refuse-to-choose doctrine is rollbackStaged's, verbatim: a file whose
|
|
15
|
+
* bytes are neither the before- nor the after-digest was edited by somebody
|
|
16
|
+
* since the crash, and completing over it would destroy one of two truths.
|
|
17
|
+
*/
|
|
18
|
+
import { existsSync, readFileSync } from "node:fs";
|
|
19
|
+
import { readFile, rename, rm } from "node:fs/promises";
|
|
20
|
+
import { dirname, join } from "node:path";
|
|
21
|
+
import { quietRmdir } from "../commit.js";
|
|
22
|
+
import { sha256 } from "../writes.js";
|
|
23
|
+
import { COMMIT_INTENT, InterruptedCommitError, sweepPidTemps } from "../interrupted.js";
|
|
24
|
+
import { clearTxnIntent } from "./journal.js";
|
|
25
|
+
export async function recoverForward(root, intent) {
|
|
26
|
+
const target = (f) => join(root, ...f.path.split("/"));
|
|
27
|
+
const abs = (rel) => join(root, ...rel.split("/"));
|
|
28
|
+
const done = [];
|
|
29
|
+
const pending = [];
|
|
30
|
+
const unknown = [];
|
|
31
|
+
for (const f of intent.files) {
|
|
32
|
+
const file = target(f);
|
|
33
|
+
const now = existsSync(file) ? sha256(await readFile(file)) : null;
|
|
34
|
+
if (now === f.after)
|
|
35
|
+
done.push(f);
|
|
36
|
+
else if (now === f.before)
|
|
37
|
+
pending.push(f);
|
|
38
|
+
else
|
|
39
|
+
unknown.push(f.path);
|
|
40
|
+
}
|
|
41
|
+
// A directory rename is classified by pure existence — a rename moves the
|
|
42
|
+
// inode, so "which end exists" is exactly as discriminating as a digest is
|
|
43
|
+
// for a file. Both ends existing (or neither) is a state no step of the
|
|
44
|
+
// commit produces: somebody made a directory since the crash, and choosing
|
|
45
|
+
// which tree to keep is the refuse-to-choose case below.
|
|
46
|
+
const movesDone = [];
|
|
47
|
+
const movesPending = [];
|
|
48
|
+
const unknownMoves = [];
|
|
49
|
+
for (const m of intent.moves) {
|
|
50
|
+
const fromExists = existsSync(abs(m.from));
|
|
51
|
+
const toExists = existsSync(abs(m.to));
|
|
52
|
+
if (toExists && !fromExists)
|
|
53
|
+
movesDone.push(m);
|
|
54
|
+
else if (fromExists && !toExists)
|
|
55
|
+
movesPending.push(m);
|
|
56
|
+
else
|
|
57
|
+
unknownMoves.push(`${m.from} -> ${m.to}`);
|
|
58
|
+
}
|
|
59
|
+
if (unknown.length > 0 || unknownMoves.length > 0) {
|
|
60
|
+
// Two vocabularies on purpose: a FILE in neither recorded state holds
|
|
61
|
+
// edited bytes, while a DIRECTORY rename in neither state means both ends
|
|
62
|
+
// exist (or neither does) — nothing was "edited", somebody made or
|
|
63
|
+
// restored a directory since the crash. One sentence covering both told
|
|
64
|
+
// the operator to hunt for edited bytes in a directory pair. And the
|
|
65
|
+
// interrupted work is named by what it WAS (`command` + `target`), not by
|
|
66
|
+
// `intent.rerun` — that string is the REPAIR spelling (`loam subsystem
|
|
67
|
+
// sync` repairs all four subsystem verbs), so "a `loam subsystem sync`
|
|
68
|
+
// was interrupted" named a command nobody ran.
|
|
69
|
+
const parts = [
|
|
70
|
+
...(unknown.length > 0
|
|
71
|
+
? [
|
|
72
|
+
`${unknown.length} of the file(s) it was writing now hold bytes that are neither what it found ` +
|
|
73
|
+
`nor what it wrote — somebody edited them since: ${unknown.join(", ")}`,
|
|
74
|
+
]
|
|
75
|
+
: []),
|
|
76
|
+
...(unknownMoves.length > 0
|
|
77
|
+
? [
|
|
78
|
+
`the directory rename(s) ${unknownMoves.join(", ")} are in neither recorded state — both ends ` +
|
|
79
|
+
`exist, or neither does, so loam cannot tell which tree to keep`,
|
|
80
|
+
]
|
|
81
|
+
: []),
|
|
82
|
+
];
|
|
83
|
+
throw new InterruptedCommitError(`a \`${intent.command}\` commit for '${intent.target}' was interrupted mid-commit ` +
|
|
84
|
+
`(pid ${intent.pid} on ${intent.host}, ${intent.at}) and ${parts.join("; ")}. ` +
|
|
85
|
+
`loam will not choose between those two truths. ` +
|
|
86
|
+
`Reconcile them against version control, delete ${COMMIT_INTENT}, then re-run \`${intent.rerun}\`.`);
|
|
87
|
+
}
|
|
88
|
+
// Whether forward is still POSSIBLE is decided before a single byte moves:
|
|
89
|
+
// completing some files and then refusing on a missing temp would leave the
|
|
90
|
+
// repo in a state neither the old nor the new tree ever had.
|
|
91
|
+
const missing = pending.filter((f) => f.after !== null && !usableTemp(root, f));
|
|
92
|
+
if (missing.length > 0) {
|
|
93
|
+
// A file whose before and after digests are EQUAL — an unchanged file
|
|
94
|
+
// carried through a commit — reads as "done" in whichever state it is in,
|
|
95
|
+
// so it is not evidence that any rename landed. Only a file that could
|
|
96
|
+
// tell the two states apart counts as one — and a landed directory MOVE
|
|
97
|
+
// always can, so it counts too.
|
|
98
|
+
const evidence = done.filter((f) => f.before !== f.after);
|
|
99
|
+
if (evidence.length === 0 && movesDone.length === 0) {
|
|
100
|
+
// Nothing ever swapped and the temps are gone — this is the tree a
|
|
101
|
+
// completed ROLLBACK leaves when the crash landed between the rollback
|
|
102
|
+
// and the journal's removal. The complete pre-state is on disk, which
|
|
103
|
+
// is one of the two states recovery promises; declaring it beats
|
|
104
|
+
// refusing over bytes that are already consistent.
|
|
105
|
+
await finish(root, intent);
|
|
106
|
+
return { command: intent.command, feature: intent.target, outcome: "consistent", repaired: [] };
|
|
107
|
+
}
|
|
108
|
+
throw new InterruptedCommitError(`a \`${intent.command}\` commit for '${intent.target}' was interrupted and ${missing.map((f) => f.path).join(", ")} ` +
|
|
109
|
+
`${missing.length === 1 ? "is" : "are"} still in the pre-commit state, but the staged bytes that would ` +
|
|
110
|
+
`finish ${missing.length === 1 ? "it" : "them"} are gone or no longer match the digest recorded before the ` +
|
|
111
|
+
`crash. Restore the tree from version control, delete ${COMMIT_INTENT}, then re-run \`${intent.rerun}\`.`);
|
|
112
|
+
}
|
|
113
|
+
const repaired = [];
|
|
114
|
+
for (const f of pending) {
|
|
115
|
+
const file = target(f);
|
|
116
|
+
if (f.after === null) {
|
|
117
|
+
await rm(file, { force: true });
|
|
118
|
+
await quietRmdir(dirname(file));
|
|
119
|
+
}
|
|
120
|
+
else {
|
|
121
|
+
await rename(join(root, ...f.tmp.split("/")), file);
|
|
122
|
+
}
|
|
123
|
+
repaired.push(f.path);
|
|
124
|
+
}
|
|
125
|
+
// Renames after the file swaps, in recorded order — the order the commit
|
|
126
|
+
// itself performs them. One that cannot land now (the destination's parent
|
|
127
|
+
// deleted since the crash, a Windows EPERM on an open handle) is a refusal
|
|
128
|
+
// naming the pair, not a half-finished loop: nothing before it is undone,
|
|
129
|
+
// and the journal stays for the next attempt.
|
|
130
|
+
for (const m of movesPending) {
|
|
131
|
+
try {
|
|
132
|
+
await rename(abs(m.from), abs(m.to));
|
|
133
|
+
}
|
|
134
|
+
catch (err) {
|
|
135
|
+
throw new InterruptedCommitError(`a \`${intent.command}\` commit for '${intent.target}' was interrupted and the rename ${m.from} -> ${m.to} ` +
|
|
136
|
+
`that would finish it cannot be performed now (${err instanceof Error ? err.message : String(err)}). ` +
|
|
137
|
+
`Complete or undo it by hand against version control, delete ${COMMIT_INTENT}, then re-run \`${intent.rerun}\`.`);
|
|
138
|
+
}
|
|
139
|
+
repaired.push(m.to);
|
|
140
|
+
}
|
|
141
|
+
await finish(root, intent);
|
|
142
|
+
return {
|
|
143
|
+
command: intent.command,
|
|
144
|
+
feature: intent.target,
|
|
145
|
+
outcome: repaired.length > 0 ? "repaired" : "completed",
|
|
146
|
+
repaired,
|
|
147
|
+
};
|
|
148
|
+
}
|
|
149
|
+
/** Is the recorded temp still there with exactly the bytes the journal promised? */
|
|
150
|
+
function usableTemp(root, f) {
|
|
151
|
+
if (f.tmp === null)
|
|
152
|
+
return false;
|
|
153
|
+
const path = join(root, ...f.tmp.split("/"));
|
|
154
|
+
try {
|
|
155
|
+
return sha256(readFileSync(path)) === f.after;
|
|
156
|
+
}
|
|
157
|
+
catch {
|
|
158
|
+
return false;
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
async function finish(root, intent) {
|
|
162
|
+
const dirs = intent.files.flatMap((f) => {
|
|
163
|
+
const out = [dirname(join(root, ...f.path.split("/")))];
|
|
164
|
+
if (f.tmp !== null)
|
|
165
|
+
out.push(dirname(join(root, ...f.tmp.split("/"))));
|
|
166
|
+
return out;
|
|
167
|
+
});
|
|
168
|
+
await sweepPidTemps(dirs, intent.pid);
|
|
169
|
+
await clearTxnIntent(root);
|
|
170
|
+
}
|
|
171
|
+
//# sourceMappingURL=forward.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"forward.js","sourceRoot":"","sources":["../../../../src/core/staging/txn/forward.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;GAgBG;AACH,OAAO,EAAE,UAAU,EAAE,YAAY,EAAE,MAAM,SAAS,CAAC;AACnD,OAAO,EAAE,QAAQ,EAAE,MAAM,EAAE,EAAE,EAAE,MAAM,kBAAkB,CAAC;AACxD,OAAO,EAAE,OAAO,EAAE,IAAI,EAAE,MAAM,WAAW,CAAC;AAC1C,OAAO,EAAE,UAAU,EAAE,MAAM,cAAc,CAAC;AAC1C,OAAO,EAAE,MAAM,EAAE,MAAM,cAAc,CAAC;AACtC,OAAO,EAAE,aAAa,EAAuB,sBAAsB,EAAE,aAAa,EAAE,MAAM,mBAAmB,CAAC;AAC9G,OAAO,EAAE,cAAc,EAA0D,MAAM,cAAc,CAAC;AAEtG,MAAM,CAAC,KAAK,UAAU,cAAc,CAAC,IAAY,EAAE,MAAiB;IAClE,MAAM,MAAM,GAAG,CAAC,CAAgB,EAAU,EAAE,CAAC,IAAI,CAAC,IAAI,EAAE,GAAG,CAAC,CAAC,IAAI,CAAC,KAAK,CAAC,GAAG,CAAC,CAAC,CAAC;IAC9E,MAAM,GAAG,GAAG,CAAC,GAAW,EAAU,EAAE,CAAC,IAAI,CAAC,IAAI,EAAE,GAAG,GAAG,CAAC,KAAK,CAAC,GAAG,CAAC,CAAC,CAAC;IACnE,MAAM,IAAI,GAAoB,EAAE,CAAC;IACjC,MAAM,OAAO,GAAoB,EAAE,CAAC;IACpC,MAAM,OAAO,GAAa,EAAE,CAAC;IAC7B,KAAK,MAAM,CAAC,IAAI,MAAM,CAAC,KAAK,EAAE,CAAC;QAC7B,MAAM,IAAI,GAAG,MAAM,CAAC,CAAC,CAAC,CAAC;QACvB,MAAM,GAAG,GAAG,UAAU,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC,MAAM,CAAC,MAAM,QAAQ,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC,CAAC,IAAI,CAAC;QACnE,IAAI,GAAG,KAAK,CAAC,CAAC,KAAK;YAAE,IAAI,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC;aAC7B,IAAI,GAAG,KAAK,CAAC,CAAC,MAAM;YAAE,OAAO,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC;;YACtC,OAAO,CAAC,IAAI,CAAC,CAAC,CAAC,IAAI,CAAC,CAAC;IAC5B,CAAC;IACD,0EAA0E;IAC1E,2EAA2E;IAC3E,wEAAwE;IACxE,2EAA2E;IAC3E,yDAAyD;IACzD,MAAM,SAAS,GAAoB,EAAE,CAAC;IACtC,MAAM,YAAY,GAAoB,EAAE,CAAC;IACzC,MAAM,YAAY,GAAa,EAAE,CAAC;IAClC,KAAK,MAAM,CAAC,IAAI,MAAM,CAAC,KAAK,EAAE,CAAC;QAC7B,MAAM,UAAU,GAAG,UAAU,CAAC,GAAG,CAAC,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC;QAC3C,MAAM,QAAQ,GAAG,UAAU,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC;QACvC,IAAI,QAAQ,IAAI,CAAC,UAAU;YAAE,SAAS,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC;aAC1C,IAAI,UAAU,IAAI,CAAC,QAAQ;YAAE,YAAY,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC;;YAClD,YAAY,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC,IAAI,OAAO,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC;IACjD,CAAC;IACD,IAAI,OAAO,CAAC,MAAM,GAAG,CAAC,IAAI,YAAY,CAAC,MAAM,GAAG,CAAC,EAAE,CAAC;QAClD,sEAAsE;QACtE,0EAA0E;QAC1E,mEAAmE;QACnE,wEAAwE;QACxE,qEAAqE;QACrE,0EAA0E;QAC1E,uEAAuE;QACvE,uEAAuE;QACvE,+CAA+C;QAC/C,MAAM,KAAK,GAAG;YACZ,GAAG,CAAC,OAAO,CAAC,MAAM,GAAG,CAAC;gBACpB,CAAC,CAAC;oBACE,GAAG,OAAO,CAAC,MAAM,+EAA+E;wBAC9F,mDAAmD,OAAO,CAAC,IAAI,CAAC,IAAI,CAAC,EAAE;iBAC1E;gBACH,CAAC,CAAC,EAAE,CAAC;YACP,GAAG,CAAC,YAAY,CAAC,MAAM,GAAG,CAAC;gBACzB,CAAC,CAAC;oBACE,2BAA2B,YAAY,CAAC,IAAI,CAAC,IAAI,CAAC,6CAA6C;wBAC7F,gEAAgE;iBACnE;gBACH,CAAC,CAAC,EAAE,CAAC;SACR,CAAC;QACF,MAAM,IAAI,sBAAsB,CAC9B,OAAO,MAAM,CAAC,OAAO,kBAAkB,MAAM,CAAC,MAAM,+BAA+B;YACjF,QAAQ,MAAM,CAAC,GAAG,OAAO,MAAM,CAAC,IAAI,KAAK,MAAM,CAAC,EAAE,SAAS,KAAK,CAAC,IAAI,CAAC,IAAI,CAAC,IAAI;YAC/E,iDAAiD;YACjD,kDAAkD,aAAa,mBAAmB,MAAM,CAAC,KAAK,KAAK,CACtG,CAAC;IACJ,CAAC;IAED,2EAA2E;IAC3E,4EAA4E;IAC5E,6DAA6D;IAC7D,MAAM,OAAO,GAAG,OAAO,CAAC,MAAM,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,KAAK,KAAK,IAAI,IAAI,CAAC,UAAU,CAAC,IAAI,EAAE,CAAC,CAAC,CAAC,CAAC;IAChF,IAAI,OAAO,CAAC,MAAM,GAAG,CAAC,EAAE,CAAC;QACvB,sEAAsE;QACtE,0EAA0E;QAC1E,uEAAuE;QACvE,wEAAwE;QACxE,gCAAgC;QAChC,MAAM,QAAQ,GAAG,IAAI,CAAC,MAAM,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,MAAM,KAAK,CAAC,CAAC,KAAK,CAAC,CAAC;QAC1D,IAAI,QAAQ,CAAC,MAAM,KAAK,CAAC,IAAI,SAAS,CAAC,MAAM,KAAK,CAAC,EAAE,CAAC;YACpD,mEAAmE;YACnE,uEAAuE;YACvE,sEAAsE;YACtE,iEAAiE;YACjE,mDAAmD;YACnD,MAAM,MAAM,CAAC,IAAI,EAAE,MAAM,CAAC,CAAC;YAC3B,OAAO,EAAE,OAAO,EAAE,MAAM,CAAC,OAAO,EAAE,OAAO,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,EAAE,YAAY,EAAE,QAAQ,EAAE,EAAE,EAAE,CAAC;QAClG,CAAC;QACD,MAAM,IAAI,sBAAsB,CAC9B,OAAO,MAAM,CAAC,OAAO,kBAAkB,MAAM,CAAC,MAAM,yBAAyB,OAAO,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,IAAI,CAAC,CAAC,IAAI,CAAC,IAAI,CAAC,GAAG;YACnH,GAAG,OAAO,CAAC,MAAM,KAAK,CAAC,CAAC,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC,KAAK,kEAAkE;YACxG,UAAU,OAAO,CAAC,MAAM,KAAK,CAAC,CAAC,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC,MAAM,8DAA8D;YAC5G,wDAAwD,aAAa,mBAAmB,MAAM,CAAC,KAAK,KAAK,CAC5G,CAAC;IACJ,CAAC;IAED,MAAM,QAAQ,GAAa,EAAE,CAAC;IAC9B,KAAK,MAAM,CAAC,IAAI,OAAO,EAAE,CAAC;QACxB,MAAM,IAAI,GAAG,MAAM,CAAC,CAAC,CAAC,CAAC;QACvB,IAAI,CAAC,CAAC,KAAK,KAAK,IAAI,EAAE,CAAC;YACrB,MAAM,EAAE,CAAC,IAAI,EAAE,EAAE,KAAK,EAAE,IAAI,EAAE,CAAC,CAAC;YAChC,MAAM,UAAU,CAAC,OAAO,CAAC,IAAI,CAAC,CAAC,CAAC;QAClC,CAAC;aAAM,CAAC;YACN,MAAM,MAAM,CAAC,IAAI,CAAC,IAAI,EAAE,GAAG,CAAC,CAAC,GAAI,CAAC,KAAK,CAAC,GAAG,CAAC,CAAC,EAAE,IAAI,CAAC,CAAC;QACvD,CAAC;QACD,QAAQ,CAAC,IAAI,CAAC,CAAC,CAAC,IAAI,CAAC,CAAC;IACxB,CAAC;IACD,yEAAyE;IACzE,2EAA2E;IAC3E,2EAA2E;IAC3E,0EAA0E;IAC1E,8CAA8C;IAC9C,KAAK,MAAM,CAAC,IAAI,YAAY,EAAE,CAAC;QAC7B,IAAI,CAAC;YACH,MAAM,MAAM,CAAC,GAAG,CAAC,CAAC,CAAC,IAAI,CAAC,EAAE,GAAG,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC;QACvC,CAAC;QAAC,OAAO,GAAG,EAAE,CAAC;YACb,MAAM,IAAI,sBAAsB,CAC9B,OAAO,MAAM,CAAC,OAAO,kBAAkB,MAAM,CAAC,MAAM,oCAAoC,CAAC,CAAC,IAAI,OAAO,CAAC,CAAC,EAAE,GAAG;gBAC1G,iDAAiD,GAAG,YAAY,KAAK,CAAC,CAAC,CAAC,GAAG,CAAC,OAAO,CAAC,CAAC,CAAC,MAAM,CAAC,GAAG,CAAC,KAAK;gBACtG,+DAA+D,aAAa,mBAAmB,MAAM,CAAC,KAAK,KAAK,CACnH,CAAC;QACJ,CAAC;QACD,QAAQ,CAAC,IAAI,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC;IACtB,CAAC;IACD,MAAM,MAAM,CAAC,IAAI,EAAE,MAAM,CAAC,CAAC;IAC3B,OAAO;QACL,OAAO,EAAE,MAAM,CAAC,OAAO;QACvB,OAAO,EAAE,MAAM,CAAC,MAAM;QACtB,OAAO,EAAE,QAAQ,CAAC,MAAM,GAAG,CAAC,CAAC,CAAC,CAAC,UAAU,CAAC,CAAC,CAAC,WAAW;QACvD,QAAQ;KACT,CAAC;AACJ,CAAC;AAED,oFAAoF;AACpF,SAAS,UAAU,CAAC,IAAY,EAAE,CAAgB;IAChD,IAAI,CAAC,CAAC,GAAG,KAAK,IAAI;QAAE,OAAO,KAAK,CAAC;IACjC,MAAM,IAAI,GAAG,IAAI,CAAC,IAAI,EAAE,GAAG,CAAC,CAAC,GAAG,CAAC,KAAK,CAAC,GAAG,CAAC,CAAC,CAAC;IAC7C,IAAI,CAAC;QACH,OAAO,MAAM,CAAC,YAAY,CAAC,IAAI,CAAC,CAAC,KAAK,CAAC,CAAC,KAAK,CAAC;IAChD,CAAC;IAAC,MAAM,CAAC;QACP,OAAO,KAAK,CAAC;IACf,CAAC;AACH,CAAC;AAED,KAAK,UAAU,MAAM,CAAC,IAAY,EAAE,MAAiB;IACnD,MAAM,IAAI,GAAG,MAAM,CAAC,KAAK,CAAC,OAAO,CAAC,CAAC,CAAC,EAAE,EAAE;QACtC,MAAM,GAAG,GAAG,CAAC,OAAO,CAAC,IAAI,CAAC,IAAI,EAAE,GAAG,CAAC,CAAC,IAAI,CAAC,KAAK,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC,CAAC;QACxD,IAAI,CAAC,CAAC,GAAG,KAAK,IAAI;YAAE,GAAG,CAAC,IAAI,CAAC,OAAO,CAAC,IAAI,CAAC,IAAI,EAAE,GAAG,CAAC,CAAC,GAAG,CAAC,KAAK,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC,CAAC;QACvE,OAAO,GAAG,CAAC;IACb,CAAC,CAAC,CAAC;IACH,MAAM,aAAa,CAAC,IAAI,EAAE,MAAM,CAAC,GAAG,CAAC,CAAC;IACtC,MAAM,cAAc,CAAC,IAAI,CAAC,CAAC;AAC7B,CAAC","sourcesContent":["/**\n * Roll a version-2 or version-3 journal FORWARD: finish the renames a killed\n * commit did not reach, from the staged temps that are already durable on\n * disk (and, for a version-3 record, the directory renames after them).\n *\n * Forward and not undo, because that is what is actually durable: the\n * after-bytes were written and (per stageWrites) fully on disk before the\n * journal existed, while the OVERWRITTEN files' pre-images are durable\n * nowhere — the rename that replaced them destroyed the only copy. The plan\n * was validated completely before the first rename, so the post-state is\n * exactly what a successful run would have produced; unarchive's\n * finish-never-undo recovery is the precedent.\n *\n * The refuse-to-choose doctrine is rollbackStaged's, verbatim: a file whose\n * bytes are neither the before- nor the after-digest was edited by somebody\n * since the crash, and completing over it would destroy one of two truths.\n */\nimport { existsSync, readFileSync } from \"node:fs\";\nimport { readFile, rename, rm } from \"node:fs/promises\";\nimport { dirname, join } from \"node:path\";\nimport { quietRmdir } from \"../commit.js\";\nimport { sha256 } from \"../writes.js\";\nimport { COMMIT_INTENT, type CommitRecovery, InterruptedCommitError, sweepPidTemps } from \"../interrupted.js\";\nimport { clearTxnIntent, type TxnIntent, type TxnIntentFile, type TxnIntentMove } from \"./journal.js\";\n\nexport async function recoverForward(root: string, intent: TxnIntent): Promise<CommitRecovery> {\n const target = (f: TxnIntentFile): string => join(root, ...f.path.split(\"/\"));\n const abs = (rel: string): string => join(root, ...rel.split(\"/\"));\n const done: TxnIntentFile[] = [];\n const pending: TxnIntentFile[] = [];\n const unknown: string[] = [];\n for (const f of intent.files) {\n const file = target(f);\n const now = existsSync(file) ? sha256(await readFile(file)) : null;\n if (now === f.after) done.push(f);\n else if (now === f.before) pending.push(f);\n else unknown.push(f.path);\n }\n // A directory rename is classified by pure existence — a rename moves the\n // inode, so \"which end exists\" is exactly as discriminating as a digest is\n // for a file. Both ends existing (or neither) is a state no step of the\n // commit produces: somebody made a directory since the crash, and choosing\n // which tree to keep is the refuse-to-choose case below.\n const movesDone: TxnIntentMove[] = [];\n const movesPending: TxnIntentMove[] = [];\n const unknownMoves: string[] = [];\n for (const m of intent.moves) {\n const fromExists = existsSync(abs(m.from));\n const toExists = existsSync(abs(m.to));\n if (toExists && !fromExists) movesDone.push(m);\n else if (fromExists && !toExists) movesPending.push(m);\n else unknownMoves.push(`${m.from} -> ${m.to}`);\n }\n if (unknown.length > 0 || unknownMoves.length > 0) {\n // Two vocabularies on purpose: a FILE in neither recorded state holds\n // edited bytes, while a DIRECTORY rename in neither state means both ends\n // exist (or neither does) — nothing was \"edited\", somebody made or\n // restored a directory since the crash. One sentence covering both told\n // the operator to hunt for edited bytes in a directory pair. And the\n // interrupted work is named by what it WAS (`command` + `target`), not by\n // `intent.rerun` — that string is the REPAIR spelling (`loam subsystem\n // sync` repairs all four subsystem verbs), so \"a `loam subsystem sync`\n // was interrupted\" named a command nobody ran.\n const parts = [\n ...(unknown.length > 0\n ? [\n `${unknown.length} of the file(s) it was writing now hold bytes that are neither what it found ` +\n `nor what it wrote — somebody edited them since: ${unknown.join(\", \")}`,\n ]\n : []),\n ...(unknownMoves.length > 0\n ? [\n `the directory rename(s) ${unknownMoves.join(\", \")} are in neither recorded state — both ends ` +\n `exist, or neither does, so loam cannot tell which tree to keep`,\n ]\n : []),\n ];\n throw new InterruptedCommitError(\n `a \\`${intent.command}\\` commit for '${intent.target}' was interrupted mid-commit ` +\n `(pid ${intent.pid} on ${intent.host}, ${intent.at}) and ${parts.join(\"; \")}. ` +\n `loam will not choose between those two truths. ` +\n `Reconcile them against version control, delete ${COMMIT_INTENT}, then re-run \\`${intent.rerun}\\`.`,\n );\n }\n\n // Whether forward is still POSSIBLE is decided before a single byte moves:\n // completing some files and then refusing on a missing temp would leave the\n // repo in a state neither the old nor the new tree ever had.\n const missing = pending.filter((f) => f.after !== null && !usableTemp(root, f));\n if (missing.length > 0) {\n // A file whose before and after digests are EQUAL — an unchanged file\n // carried through a commit — reads as \"done\" in whichever state it is in,\n // so it is not evidence that any rename landed. Only a file that could\n // tell the two states apart counts as one — and a landed directory MOVE\n // always can, so it counts too.\n const evidence = done.filter((f) => f.before !== f.after);\n if (evidence.length === 0 && movesDone.length === 0) {\n // Nothing ever swapped and the temps are gone — this is the tree a\n // completed ROLLBACK leaves when the crash landed between the rollback\n // and the journal's removal. The complete pre-state is on disk, which\n // is one of the two states recovery promises; declaring it beats\n // refusing over bytes that are already consistent.\n await finish(root, intent);\n return { command: intent.command, feature: intent.target, outcome: \"consistent\", repaired: [] };\n }\n throw new InterruptedCommitError(\n `a \\`${intent.command}\\` commit for '${intent.target}' was interrupted and ${missing.map((f) => f.path).join(\", \")} ` +\n `${missing.length === 1 ? \"is\" : \"are\"} still in the pre-commit state, but the staged bytes that would ` +\n `finish ${missing.length === 1 ? \"it\" : \"them\"} are gone or no longer match the digest recorded before the ` +\n `crash. Restore the tree from version control, delete ${COMMIT_INTENT}, then re-run \\`${intent.rerun}\\`.`,\n );\n }\n\n const repaired: string[] = [];\n for (const f of pending) {\n const file = target(f);\n if (f.after === null) {\n await rm(file, { force: true });\n await quietRmdir(dirname(file));\n } else {\n await rename(join(root, ...f.tmp!.split(\"/\")), file);\n }\n repaired.push(f.path);\n }\n // Renames after the file swaps, in recorded order — the order the commit\n // itself performs them. One that cannot land now (the destination's parent\n // deleted since the crash, a Windows EPERM on an open handle) is a refusal\n // naming the pair, not a half-finished loop: nothing before it is undone,\n // and the journal stays for the next attempt.\n for (const m of movesPending) {\n try {\n await rename(abs(m.from), abs(m.to));\n } catch (err) {\n throw new InterruptedCommitError(\n `a \\`${intent.command}\\` commit for '${intent.target}' was interrupted and the rename ${m.from} -> ${m.to} ` +\n `that would finish it cannot be performed now (${err instanceof Error ? err.message : String(err)}). ` +\n `Complete or undo it by hand against version control, delete ${COMMIT_INTENT}, then re-run \\`${intent.rerun}\\`.`,\n );\n }\n repaired.push(m.to);\n }\n await finish(root, intent);\n return {\n command: intent.command,\n feature: intent.target,\n outcome: repaired.length > 0 ? \"repaired\" : \"completed\",\n repaired,\n };\n}\n\n/** Is the recorded temp still there with exactly the bytes the journal promised? */\nfunction usableTemp(root: string, f: TxnIntentFile): boolean {\n if (f.tmp === null) return false;\n const path = join(root, ...f.tmp.split(\"/\"));\n try {\n return sha256(readFileSync(path)) === f.after;\n } catch {\n return false;\n }\n}\n\nasync function finish(root: string, intent: TxnIntent): Promise<void> {\n const dirs = intent.files.flatMap((f) => {\n const out = [dirname(join(root, ...f.path.split(\"/\")))];\n if (f.tmp !== null) out.push(dirname(join(root, ...f.tmp.split(\"/\"))));\n return out;\n });\n await sweepPidTemps(dirs, intent.pid);\n await clearTxnIntent(root);\n}\n\n"]}
|
|
@@ -0,0 +1,139 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The version-2 `.loam-commit` journal: the record a SMALL journaled
|
|
3
|
+
* transaction writes before its first rename, for the writers whose commits
|
|
4
|
+
* have no snapshot and no final directory move — rebase, vouch, new, gherkin.
|
|
5
|
+
*
|
|
6
|
+
* Version 1 (archive/unarchive) restores from a snapshot that travels in the
|
|
7
|
+
* feature directory; none of the smaller writers has one, which is exactly
|
|
8
|
+
* why the roadmap says extract a smaller transaction rather than reuse — this
|
|
9
|
+
* journal therefore records, per file, the digest of both states AND the
|
|
10
|
+
* root-relative staged temp still holding the after-bytes, so recovery can
|
|
11
|
+
* roll FORWARD from what is already durable instead of pretending pre-images
|
|
12
|
+
* exist somewhere. `rerun` is the exact `loam …` line doctor prints as the
|
|
13
|
+
* fix: recovery runs at the start of the next invocation of the same command.
|
|
14
|
+
*
|
|
15
|
+
* A txn/ sub-package importing ONLY the staging root, never `recovery/`:
|
|
16
|
+
* recovery/ already imports the root and will import txn/ for dispatch, so an
|
|
17
|
+
* edge back from here would be the repo's first package cycle.
|
|
18
|
+
*/
|
|
19
|
+
import { existsSync } from "node:fs";
|
|
20
|
+
import { readFile } from "node:fs/promises";
|
|
21
|
+
import { hostname } from "node:os";
|
|
22
|
+
import { join, relative } from "node:path";
|
|
23
|
+
import { isPathInside } from "../../kernel/path-safety.js";
|
|
24
|
+
import { quietRm } from "../commit.js";
|
|
25
|
+
import { COMMIT_INTENT, writeDurable } from "../interrupted.js";
|
|
26
|
+
import { isDigest, sha256 } from "../writes.js";
|
|
27
|
+
const TXN_VERSION = 2;
|
|
28
|
+
/**
|
|
29
|
+
* The version whose records CARRY DIRECTORY MOVES — `subsystem move`'s
|
|
30
|
+
* journal, the first consumer whose commit renames directories beside its one
|
|
31
|
+
* staged file. A version of its own, and version 2 is still what every
|
|
32
|
+
* move-less writer stamps, deliberately: an old binary must keep recovering
|
|
33
|
+
* the version-2 records it always could, and it must REFUSE a record with
|
|
34
|
+
* moves rather than roll the file half forward and leave the renames behind —
|
|
35
|
+
* an unknown version already reads as "a commit this loam cannot read", which
|
|
36
|
+
* is `doctor`'s honest blocker, not a silent half-repair.
|
|
37
|
+
*/
|
|
38
|
+
const TXN_MOVES_VERSION = 3;
|
|
39
|
+
/** Root-relative with forward slashes — the journal must survive being read on another machine. */
|
|
40
|
+
function rel(root, abs) {
|
|
41
|
+
return relative(root, abs).split(/[\\/]/).join("/");
|
|
42
|
+
}
|
|
43
|
+
export async function writeTxnIntent(spec, staged) {
|
|
44
|
+
const moves = (spec.moves ?? []).map((m) => ({ from: rel(spec.root, m.from), to: rel(spec.root, m.to) }));
|
|
45
|
+
const intent = {
|
|
46
|
+
version: moves.length > 0 ? TXN_MOVES_VERSION : TXN_VERSION,
|
|
47
|
+
command: spec.command,
|
|
48
|
+
rerun: spec.rerun,
|
|
49
|
+
target: spec.target,
|
|
50
|
+
pid: process.pid,
|
|
51
|
+
host: hostname(),
|
|
52
|
+
at: new Date().toISOString(),
|
|
53
|
+
files: staged.map((s) => ({
|
|
54
|
+
path: rel(spec.root, s.write.path),
|
|
55
|
+
before: s.before === null ? null : sha256(s.before),
|
|
56
|
+
after: s.content === null ? null : sha256(s.content),
|
|
57
|
+
tmp: s.tmp === null ? null : rel(spec.root, s.tmp),
|
|
58
|
+
})),
|
|
59
|
+
moves,
|
|
60
|
+
};
|
|
61
|
+
await writeDurable(join(spec.root, COMMIT_INTENT), JSON.stringify(intent, null, 2) + "\n");
|
|
62
|
+
}
|
|
63
|
+
export async function clearTxnIntent(root) {
|
|
64
|
+
await quietRm(join(root, COMMIT_INTENT));
|
|
65
|
+
}
|
|
66
|
+
/**
|
|
67
|
+
* The journal as written, or null when absent, unreadable, or not version 2 —
|
|
68
|
+
* the same strict grading version 1 gets: a record that cannot be trusted
|
|
69
|
+
* whole is not a record, and `doctor` describes the difference.
|
|
70
|
+
*/
|
|
71
|
+
export async function readTxnIntent(root) {
|
|
72
|
+
const path = join(root, COMMIT_INTENT);
|
|
73
|
+
if (!existsSync(path))
|
|
74
|
+
return null;
|
|
75
|
+
let parsed;
|
|
76
|
+
try {
|
|
77
|
+
parsed = JSON.parse(await readFile(path, "utf8"));
|
|
78
|
+
}
|
|
79
|
+
catch {
|
|
80
|
+
return null;
|
|
81
|
+
}
|
|
82
|
+
const i = parsed;
|
|
83
|
+
if (i === null || typeof i !== "object")
|
|
84
|
+
return null;
|
|
85
|
+
if (i.version !== TXN_VERSION && i.version !== TXN_MOVES_VERSION)
|
|
86
|
+
return null;
|
|
87
|
+
if (typeof i.command !== "string" || typeof i.rerun !== "string" || typeof i.target !== "string")
|
|
88
|
+
return null;
|
|
89
|
+
if (typeof i.pid !== "number" || typeof i.host !== "string" || typeof i.at !== "string")
|
|
90
|
+
return null;
|
|
91
|
+
if (!Array.isArray(i.files))
|
|
92
|
+
return null;
|
|
93
|
+
// A version-2 record predates the field; normalise so recovery reads one
|
|
94
|
+
// shape. A version-3 record's moves are validated exactly as its files are:
|
|
95
|
+
// recovery RENAMES what they name, so an entry escaping the root fails the
|
|
96
|
+
// whole record closed.
|
|
97
|
+
if (i.moves === undefined)
|
|
98
|
+
i.moves = [];
|
|
99
|
+
if (!Array.isArray(i.moves))
|
|
100
|
+
return null;
|
|
101
|
+
if (i.version === TXN_VERSION && i.moves.length > 0)
|
|
102
|
+
return null;
|
|
103
|
+
for (const m of i.moves) {
|
|
104
|
+
if (typeof m?.from !== "string" || typeof m?.to !== "string")
|
|
105
|
+
return null;
|
|
106
|
+
if (!insideRoot(root, m.from) || !insideRoot(root, m.to))
|
|
107
|
+
return null;
|
|
108
|
+
}
|
|
109
|
+
for (const f of i.files) {
|
|
110
|
+
if (typeof f?.path !== "string")
|
|
111
|
+
return null;
|
|
112
|
+
if (!digestOrNull(f.before) || !digestOrNull(f.after))
|
|
113
|
+
return null;
|
|
114
|
+
if (f.tmp !== null && typeof f.tmp !== "string")
|
|
115
|
+
return null;
|
|
116
|
+
// A write (after non-null) with no temp recorded could never be rolled
|
|
117
|
+
// forward; refusing the whole record here keeps that impossibility a
|
|
118
|
+
// `doctor` sentence instead of a mid-recovery surprise.
|
|
119
|
+
if (f.after !== null && f.tmp === null)
|
|
120
|
+
return null;
|
|
121
|
+
// The journal is untrusted input — a plain file that travels in a git
|
|
122
|
+
// checkout — and recovery RENAMES OVER and DELETES what these entries
|
|
123
|
+
// name. An entry escaping the root (`../outside`, an absolute path) would
|
|
124
|
+
// turn roll-forward into an arbitrary write anywhere on the machine, so
|
|
125
|
+
// it fails the whole record closed: `doctor` grades it exactly as any
|
|
126
|
+
// other unreadable journal, a blocker naming the file for a human.
|
|
127
|
+
if (!insideRoot(root, f.path) || (f.tmp !== null && !insideRoot(root, f.tmp)))
|
|
128
|
+
return null;
|
|
129
|
+
}
|
|
130
|
+
return i;
|
|
131
|
+
}
|
|
132
|
+
function digestOrNull(value) {
|
|
133
|
+
return value === null || isDigest(value);
|
|
134
|
+
}
|
|
135
|
+
/** Lexical containment of a recorded root-relative path — recovery acts on the join. */
|
|
136
|
+
function insideRoot(root, rel) {
|
|
137
|
+
return isPathInside(join(root), join(root, ...rel.split("/")));
|
|
138
|
+
}
|
|
139
|
+
//# sourceMappingURL=journal.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"journal.js","sourceRoot":"","sources":["../../../../src/core/staging/txn/journal.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * The version-2 `.loam-commit` journal: the record a SMALL journaled\n * transaction writes before its first rename, for the writers whose commits\n * have no snapshot and no final directory move — rebase, vouch, new, gherkin.\n *\n * Version 1 (archive/unarchive) restores from a snapshot that travels in the\n * feature directory; none of the smaller writers has one, which is exactly\n * why the roadmap says extract a smaller transaction rather than reuse — this\n * journal therefore records, per file, the digest of both states AND the\n * root-relative staged temp still holding the after-bytes, so recovery can\n * roll FORWARD from what is already durable instead of pretending pre-images\n * exist somewhere. `rerun` is the exact `loam …` line doctor prints as the\n * fix: recovery runs at the start of the next invocation of the same command.\n *\n * A txn/ sub-package importing ONLY the staging root, never `recovery/`:\n * recovery/ already imports the root and will import txn/ for dispatch, so an\n * edge back from here would be the repo's first package cycle.\n */\nimport { existsSync } from \"node:fs\";\nimport { readFile } from \"node:fs/promises\";\nimport { hostname } from \"node:os\";\nimport { join, relative } from \"node:path\";\nimport { isPathInside } from \"../../kernel/path-safety.js\";\nimport { quietRm } from \"../commit.js\";\nimport { COMMIT_INTENT, writeDurable } from \"../interrupted.js\";\nimport { isDigest, sha256, type StagedWrite } from \"../writes.js\";\n\nconst TXN_VERSION = 2;\n/**\n * The version whose records CARRY DIRECTORY MOVES — `subsystem move`'s\n * journal, the first consumer whose commit renames directories beside its one\n * staged file. A version of its own, and version 2 is still what every\n * move-less writer stamps, deliberately: an old binary must keep recovering\n * the version-2 records it always could, and it must REFUSE a record with\n * moves rather than roll the file half forward and leave the renames behind —\n * an unknown version already reads as \"a commit this loam cannot read\", which\n * is `doctor`'s honest blocker, not a silent half-repair.\n */\nconst TXN_MOVES_VERSION = 3;\n\n/** One directory rename inside a transaction: root-relative, forward slashes. */\nexport interface TxnIntentMove {\n from: string;\n to: string;\n}\n\nexport interface TxnIntentFile {\n /** Root-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 /** Root-relative staged temp holding the after-bytes; null for a deletion. */\n tmp: string | null;\n}\n\nexport interface TxnIntent {\n version: number;\n /** Which command was committing — a sentence a human reads, not a branch. */\n command: string;\n /** The exact `loam …` invocation whose next run recovers this journal. */\n rerun: string;\n /** What the command was committing for: a feature id, or a service. */\n target: string;\n pid: number;\n host: string;\n at: string;\n files: TxnIntentFile[];\n /**\n * Directory renames the commit performs AFTER its file swaps, in this\n * order. `[]` on every version-2 record (the reader normalises absence):\n * only `subsystem move`/`rename` write any, and their presence is what\n * bumps the record to version 3.\n */\n moves: TxnIntentMove[];\n}\n\n/** What a transaction tells the journal about itself; the caller's one spelling of its identity. */\nexport interface TxnSpec {\n /** The repo root the journal lives in — the docs repo, or gherkin's owned root. */\n root: string;\n command: string;\n rerun: string;\n target: string;\n /** Directory renames to perform after the swaps — ABSOLUTE paths; the journal stores them root-relative. */\n moves?: { from: string; to: string }[];\n}\n\n/** Root-relative with forward slashes — the journal must survive being read on another machine. */\nfunction rel(root: string, abs: string): string {\n return relative(root, abs).split(/[\\\\/]/).join(\"/\");\n}\n\nexport async function writeTxnIntent(spec: TxnSpec, staged: StagedWrite[]): Promise<void> {\n const moves = (spec.moves ?? []).map((m) => ({ from: rel(spec.root, m.from), to: rel(spec.root, m.to) }));\n const intent: TxnIntent = {\n version: moves.length > 0 ? TXN_MOVES_VERSION : TXN_VERSION,\n command: spec.command,\n rerun: spec.rerun,\n target: spec.target,\n pid: process.pid,\n host: hostname(),\n at: new Date().toISOString(),\n files: staged.map((s) => ({\n path: rel(spec.root, s.write.path),\n before: s.before === null ? null : sha256(s.before),\n after: s.content === null ? null : sha256(s.content),\n tmp: s.tmp === null ? null : rel(spec.root, s.tmp),\n })),\n moves,\n };\n await writeDurable(join(spec.root, COMMIT_INTENT), JSON.stringify(intent, null, 2) + \"\\n\");\n}\n\nexport async function clearTxnIntent(root: string): Promise<void> {\n await quietRm(join(root, COMMIT_INTENT));\n}\n\n/**\n * The journal as written, or null when absent, unreadable, or not version 2 —\n * the same strict grading version 1 gets: a record that cannot be trusted\n * whole is not a record, and `doctor` describes the difference.\n */\nexport async function readTxnIntent(root: string): Promise<TxnIntent | null> {\n const path = join(root, 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<TxnIntent>;\n if (i === null || typeof i !== \"object\") return null;\n if (i.version !== TXN_VERSION && i.version !== TXN_MOVES_VERSION) return null;\n if (typeof i.command !== \"string\" || typeof i.rerun !== \"string\" || typeof i.target !== \"string\") return null;\n if (typeof i.pid !== \"number\" || typeof i.host !== \"string\" || typeof i.at !== \"string\") return null;\n if (!Array.isArray(i.files)) return null;\n // A version-2 record predates the field; normalise so recovery reads one\n // shape. A version-3 record's moves are validated exactly as its files are:\n // recovery RENAMES what they name, so an entry escaping the root fails the\n // whole record closed.\n if (i.moves === undefined) i.moves = [];\n if (!Array.isArray(i.moves)) return null;\n if (i.version === TXN_VERSION && i.moves.length > 0) return null;\n for (const m of i.moves) {\n if (typeof m?.from !== \"string\" || typeof m?.to !== \"string\") return null;\n if (!insideRoot(root, m.from) || !insideRoot(root, m.to)) return null;\n }\n for (const f of i.files) {\n if (typeof f?.path !== \"string\") return null;\n if (!digestOrNull(f.before) || !digestOrNull(f.after)) return null;\n if (f.tmp !== null && typeof f.tmp !== \"string\") return null;\n // A write (after non-null) with no temp recorded could never be rolled\n // forward; refusing the whole record here keeps that impossibility a\n // `doctor` sentence instead of a mid-recovery surprise.\n if (f.after !== null && f.tmp === null) return null;\n // The journal is untrusted input — a plain file that travels in a git\n // checkout — and recovery RENAMES OVER and DELETES what these entries\n // name. An entry escaping the root (`../outside`, an absolute path) would\n // turn roll-forward into an arbitrary write anywhere on the machine, so\n // it fails the whole record closed: `doctor` grades it exactly as any\n // other unreadable journal, a blocker naming the file for a human.\n if (!insideRoot(root, f.path) || (f.tmp !== null && !insideRoot(root, f.tmp))) return null;\n }\n return i as TxnIntent;\n}\n\nfunction digestOrNull(value: unknown): boolean {\n return value === null || isDigest(value);\n}\n\n/** Lexical containment of a recorded root-relative path — recovery acts on the join. */\nfunction insideRoot(root: string, rel: string): boolean {\n return isPathInside(join(root), join(root, ...rel.split(\"/\")));\n}\n"]}
|