@ybotok/loam 0.1.0-beta.3 → 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 +2548 -86
- 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 +2355 -256
- 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 +72 -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/{agents-stamp.js → agent/agents-stamp.js} +50 -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.js → asyncapi/read.js} +95 -109
- 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 +7 -64
- 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 +120 -78
- 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 -295
- 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 -262
- package/dist/commands/status.js.map +0 -1
- package/dist/commands/unarchive.js.map +0 -1
- package/dist/commands/validate.js +0 -1773
- 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 -421
- package/dist/commands/vouch.js.map +0 -1
- package/dist/core/agent.js +0 -2173
- package/dist/core/agent.js.map +0 -1
- 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/asyncapi.js.map +0 -1
- package/dist/core/brief.js +0 -755
- 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 -596
- 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 -489
- 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 -787
- package/dist/core/provenance.js.map +0 -1
- package/dist/core/records.js.map +0 -1
- package/dist/core/repo.js +0 -438
- package/dist/core/repo.js.map +0 -1
- package/dist/core/report.js.map +0 -1
- package/dist/core/spec.js +0 -505
- 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 -1132
- 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,246 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The cucumber report, and the repository it has to be evidence FROM.
|
|
3
|
+
*
|
|
4
|
+
* A results file is only evidence if it describes the commit the attestation
|
|
5
|
+
* names, so reading it and checking the repository are one subject: the git
|
|
6
|
+
* helpers below are here because they answer the same question — is this
|
|
7
|
+
* working tree the one the report was produced from.
|
|
8
|
+
*/
|
|
9
|
+
import { execFile } from "node:child_process";
|
|
10
|
+
import { readFile, stat } from "node:fs/promises";
|
|
11
|
+
import { resolvePortableFileInside } from "../../core/kernel/path-safety.js";
|
|
12
|
+
import { isScenarioReport, readCucumberReport, readScenarioReport } from "../../core/results.js";
|
|
13
|
+
import {} from "../../core/verify/answers.js";
|
|
14
|
+
import { citedLine, pinnedDigest, sourceLines } from "../../core/verify/pins/pin.js";
|
|
15
|
+
import {} from "../../core/verify/record.js";
|
|
16
|
+
import {} from "./evidence/pins.js";
|
|
17
|
+
import { readReportArtifact } from "./evidence/read.js";
|
|
18
|
+
/**
|
|
19
|
+
* The report as an artifact, not merely as a parse. The resolve/read/digest
|
|
20
|
+
* plumbing lives in `./evidence/read.ts`, shared with `--contract-results`:
|
|
21
|
+
* the bytes are digested and the file's mtime read, because loam cannot prove
|
|
22
|
+
* a JSON file came from executing this commit and should stop implying
|
|
23
|
+
* otherwise. It can say precisely which file it consumed, and that goes on
|
|
24
|
+
* the record.
|
|
25
|
+
*/
|
|
26
|
+
export async function readResults(spelled, repoDir) {
|
|
27
|
+
const artifact = await readReportArtifact(spelled, repoDir, "test report");
|
|
28
|
+
if (!artifact.ok)
|
|
29
|
+
return artifact;
|
|
30
|
+
// The reader is chosen by MARKER, before either shape has validated
|
|
31
|
+
// anything. A file declaring `loamScenarioReport` must be graded as that
|
|
32
|
+
// shape even when it is malformed — falling through to the cucumber parser
|
|
33
|
+
// would tell an author their report is not a cucumber array, which is true,
|
|
34
|
+
// useless, and hides the actual mistake.
|
|
35
|
+
const parsed = isScenarioReport(artifact.doc)
|
|
36
|
+
? readScenarioReport(artifact.doc, spelled)
|
|
37
|
+
: readCucumberReport(artifact.doc, spelled);
|
|
38
|
+
if (!parsed.ok)
|
|
39
|
+
return { ok: false, code: "answers-unreadable", message: parsed.message };
|
|
40
|
+
return {
|
|
41
|
+
ok: true,
|
|
42
|
+
scenarios: parsed.scenarios,
|
|
43
|
+
report: {
|
|
44
|
+
path: artifact.spelled,
|
|
45
|
+
digest: artifact.digest,
|
|
46
|
+
mtime: artifact.mtime,
|
|
47
|
+
scenarios: parsed.scenarios.length,
|
|
48
|
+
},
|
|
49
|
+
};
|
|
50
|
+
}
|
|
51
|
+
/**
|
|
52
|
+
* In federated mode, a confirmation is accepted only when its evidence holds up
|
|
53
|
+
* in this repository: an agent's `file:line` resolves to a real line that is
|
|
54
|
+
* unchanged at the attested commit, and each runner's evidence names the report
|
|
55
|
+
* loam just read — the cucumber report for `runner`, the contract report for
|
|
56
|
+
* `external-runner`. Legacy global mode deliberately keeps its original, looser
|
|
57
|
+
* evidence contract for backward compatibility.
|
|
58
|
+
*
|
|
59
|
+
* The same pass now STAMPS what it validated: one `EvidencePin` per agent
|
|
60
|
+
* citation, built where `committedFile`'s blob is already in hand (zero new
|
|
61
|
+
* git calls), and one `TokenMiss` per citation whose blob does not contain the
|
|
62
|
+
* claim's token (`tokens`, claim id → the literal the claim asserts). Runner
|
|
63
|
+
* and contract answers get no pins — their evidence names a report entry, not
|
|
64
|
+
* a file — and the legacy all-at-once form never calls this validator, so it
|
|
65
|
+
* stamps nothing.
|
|
66
|
+
*/
|
|
67
|
+
export async function validateServiceEvidence(answers, binding, reports, tokens) {
|
|
68
|
+
const { repoDir, commit } = binding;
|
|
69
|
+
const err = (message) => ({ ok: false, message });
|
|
70
|
+
const pins = new Map();
|
|
71
|
+
const tokenMisses = [];
|
|
72
|
+
for (const report of [reports.results, reports.contract]) {
|
|
73
|
+
if (report === undefined)
|
|
74
|
+
continue;
|
|
75
|
+
// Most reports are build output and untracked, and `git diff` is quiet on
|
|
76
|
+
// those — nothing here invents a rule for them. One the repository DOES
|
|
77
|
+
// carry has to match the commit being attested, like any other file the
|
|
78
|
+
// evidence rests on. Both reports independently: each is its own file
|
|
79
|
+
// making its own claim about this commit.
|
|
80
|
+
const clean = await git(repoDir, ["diff", "--quiet", commit, "--", report.path]);
|
|
81
|
+
if (clean.code !== 0) {
|
|
82
|
+
// Only `1` is git ANSWERING — "that file differs". Every other non-zero
|
|
83
|
+
// code is git failing to answer at all: no git on PATH, a fork that never
|
|
84
|
+
// happened, a child killed by a CI timeout or the OOM killer. Testing for
|
|
85
|
+
// `1` alone made those silently pass the check, so the one run that most
|
|
86
|
+
// needs the binding — the one where the machine is falling over — is the
|
|
87
|
+
// one that would mint an attestation whose report was never bound to the
|
|
88
|
+
// commit. An unanswered check is not a passed one, so the unknown refuses
|
|
89
|
+
// on the same terms as the known, and `clean.stderr` carries whatever
|
|
90
|
+
// account of itself the child managed to leave.
|
|
91
|
+
return err(clean.code === 1
|
|
92
|
+
? `The test report '${report.path}' is committed to this repository and differs from ${commit.slice(0, 12)} — commit it or attest the commit it belongs to.`
|
|
93
|
+
: `Cannot tell whether the test report '${report.path}' is bound to ${commit.slice(0, 12)} — git could not be run to completion: ${clean.stderr || "git diff failed"}`);
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
for (const answer of answers) {
|
|
97
|
+
if (answer.verdict !== "confirmed")
|
|
98
|
+
continue;
|
|
99
|
+
if (answer.answered_by === "runner" || answer.answered_by === "external-runner") {
|
|
100
|
+
// A runner's evidence is an entry inside its report, not a file:line in
|
|
101
|
+
// the source — there is nothing to resolve. What must hold is that it
|
|
102
|
+
// names THAT report: an answer pointing anywhere else is not this run's.
|
|
103
|
+
const report = answer.answered_by === "runner" ? reports.results : reports.contract;
|
|
104
|
+
const stray = answer.evidence.filter((e) => report === undefined || !e.startsWith(`${report.path}: `));
|
|
105
|
+
if (stray.length > 0) {
|
|
106
|
+
return err(`Claim ${answer.id} has runner evidence ${stray.map((e) => `'${e}'`).join(", ")} that does not name the report loam read.`);
|
|
107
|
+
}
|
|
108
|
+
continue;
|
|
109
|
+
}
|
|
110
|
+
for (const evidence of answer.evidence) {
|
|
111
|
+
const match = /^(.+):([1-9]\d*)$/.exec(evidence);
|
|
112
|
+
if (match === null) {
|
|
113
|
+
return err(`Claim ${answer.id} has evidence '${evidence}' — service evidence must be a canonical relative file:line.`);
|
|
114
|
+
}
|
|
115
|
+
const relativePath = match[1];
|
|
116
|
+
const line = Number(match[2]);
|
|
117
|
+
let absolutePath;
|
|
118
|
+
try {
|
|
119
|
+
absolutePath = resolvePortableFileInside(repoDir, relativePath, `evidence for ${answer.id}`);
|
|
120
|
+
}
|
|
121
|
+
catch (cause) {
|
|
122
|
+
return err(`Claim ${answer.id} has unsafe evidence '${evidence}': ${cause instanceof Error ? cause.message : String(cause)}`);
|
|
123
|
+
}
|
|
124
|
+
try {
|
|
125
|
+
const info = await stat(absolutePath);
|
|
126
|
+
if (!info.isFile()) {
|
|
127
|
+
return err(`Claim ${answer.id} has evidence '${evidence}', but '${relativePath}' is not a regular file.`);
|
|
128
|
+
}
|
|
129
|
+
const source = await readFile(absolutePath, "utf8");
|
|
130
|
+
const lines = sourceLines(source).length;
|
|
131
|
+
if (line > lines) {
|
|
132
|
+
return err(`Claim ${answer.id} has evidence '${evidence}', but '${relativePath}' has only ${lines} line(s).`);
|
|
133
|
+
}
|
|
134
|
+
const committed = await committedFile(repoDir, commit, relativePath);
|
|
135
|
+
if (!committed.ok) {
|
|
136
|
+
return err(`Claim ${answer.id} has evidence '${evidence}' that is not bound to ${commit.slice(0, 12)}: ${committed.message}`);
|
|
137
|
+
}
|
|
138
|
+
// The pin, built from the blob just fetched. `citedLine` is undefined
|
|
139
|
+
// exactly when the line is past the committed file's end — the refusal
|
|
140
|
+
// this validator already owed — so the bounds check and the pinned
|
|
141
|
+
// text can never disagree about where lines fall.
|
|
142
|
+
const text = citedLine(committed.source, line);
|
|
143
|
+
if (text === undefined) {
|
|
144
|
+
const committedLines = sourceLines(committed.source).length;
|
|
145
|
+
return err(`Claim ${answer.id} has evidence '${evidence}', but '${relativePath}' has only ${committedLines} line(s) at ${commit.slice(0, 12)}.`);
|
|
146
|
+
}
|
|
147
|
+
// The token scan happens here, per citation, because the blob is in
|
|
148
|
+
// hand and record time is when the answerer can still re-read what
|
|
149
|
+
// they cited. Substring, never a parse: the claim asserts a literal.
|
|
150
|
+
// A blob that does NOT contain the token is warned once (the notice
|
|
151
|
+
// built from `tokenMisses`) and the pin then omits the token — see
|
|
152
|
+
// EvidencePin.token for why re-litigating a never-there token at
|
|
153
|
+
// every later validate would be a false sentence with no repair.
|
|
154
|
+
const token = tokens.get(answer.id);
|
|
155
|
+
const held = token !== undefined && committed.source.includes(token);
|
|
156
|
+
if (token !== undefined && !held)
|
|
157
|
+
tokenMisses.push({ id: answer.id, evidence, token });
|
|
158
|
+
const list = pins.get(answer.id) ?? [];
|
|
159
|
+
list.push({
|
|
160
|
+
path: relativePath,
|
|
161
|
+
line,
|
|
162
|
+
file_sha256: pinnedDigest(committed.source),
|
|
163
|
+
text,
|
|
164
|
+
...(held && token !== undefined ? { token } : {}),
|
|
165
|
+
});
|
|
166
|
+
pins.set(answer.id, list);
|
|
167
|
+
}
|
|
168
|
+
catch (cause) {
|
|
169
|
+
return err(`Claim ${answer.id} has unreadable evidence '${evidence}': ${cause instanceof Error ? cause.message : String(cause)}`);
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
return { ok: true, pins, tokenMisses };
|
|
174
|
+
}
|
|
175
|
+
/** Require the evidence blob to exist at HEAD and have no uncommitted edits. */
|
|
176
|
+
export async function committedFile(repoDir, commit, path) {
|
|
177
|
+
const clean = await git(repoDir, ["diff", "--quiet", commit, "--", path]);
|
|
178
|
+
if (clean.code !== 0) {
|
|
179
|
+
return {
|
|
180
|
+
ok: false,
|
|
181
|
+
message: clean.code === 1 ? `'${path}' has uncommitted changes` : clean.stderr || "git diff failed",
|
|
182
|
+
};
|
|
183
|
+
}
|
|
184
|
+
const blob = await git(repoDir, ["show", `${commit}:${path}`]);
|
|
185
|
+
if (blob.code !== 0) {
|
|
186
|
+
return { ok: false, message: blob.stderr || `'${path}' is not tracked by that commit` };
|
|
187
|
+
}
|
|
188
|
+
return { ok: true, source: blob.stdout };
|
|
189
|
+
}
|
|
190
|
+
/**
|
|
191
|
+
* How long one git question may take, and how much it may say. Both bounds
|
|
192
|
+
* exist because every call in this module now runs while `loam verify --record`
|
|
193
|
+
* HOLDS the docs lock: a `git rev-parse` blocked on a credential-helper prompt
|
|
194
|
+
* used to hang this process forever with the lock in hand, wedging every
|
|
195
|
+
* archive, rebase and record in the fleet behind a live pid that
|
|
196
|
+
* `breakStaleLock` rightly refuses to break. The timeout mirrors
|
|
197
|
+
* `core/provenance/git.ts`; the cap is sized for `git show` of a source file
|
|
198
|
+
* an attestation cites (the default 1 MiB refused sound evidence the moment a
|
|
199
|
+
* generated client crossed it), not for arbitrary blobs.
|
|
200
|
+
*/
|
|
201
|
+
const GIT_TIMEOUT_MS = 10_000;
|
|
202
|
+
const GIT_MAX_OUTPUT_BYTES = 64 * 1024 * 1024;
|
|
203
|
+
function git(repoDir, args) {
|
|
204
|
+
return new Promise((done) => {
|
|
205
|
+
execFile("git", ["-C", repoDir, ...args], { encoding: "utf8", timeout: GIT_TIMEOUT_MS, maxBuffer: GIT_MAX_OUTPUT_BYTES }, (error, stdout, stderr) => {
|
|
206
|
+
done({
|
|
207
|
+
// `error.code` is an exit status only when it is a number: a spawn
|
|
208
|
+
// failure reports an errno string ("ENOENT") and a signal-killed child
|
|
209
|
+
// reports none at all. Folding those onto 1 would state that `git diff`
|
|
210
|
+
// found a difference — a Ctrl-C or an OOM kill during
|
|
211
|
+
// `loam verify --record` would read as an uncommitted edit and refuse
|
|
212
|
+
// a sound attestation. -1 is not a status git assigns, so both diff
|
|
213
|
+
// sites fall to their stderr-carrying arm and say what happened.
|
|
214
|
+
code: error === null ? 0 : typeof error.code === "number" ? error.code : -1,
|
|
215
|
+
stdout,
|
|
216
|
+
// A child that never ran writes nothing to stderr, so its only account
|
|
217
|
+
// of itself is on the error; without this the refusal would name no
|
|
218
|
+
// cause at all. A child WE killed gets its account written for it: the
|
|
219
|
+
// deadline exists for a git blocked on a credential-helper prompt, and
|
|
220
|
+
// the raw "Command failed: git …" says nothing about a deadline — the
|
|
221
|
+
// operator re-runs, waits the same 10 s, and reads the same message,
|
|
222
|
+
// with the actual fix (GIT_TERMINAL_PROMPT=0, or the helper) never
|
|
223
|
+
// reachable from it.
|
|
224
|
+
stderr: error !== null && error.killed && error.signal !== null
|
|
225
|
+
? `git did not answer within ${GIT_TIMEOUT_MS / 1000}s and was stopped — a prompt or a hung remote; GIT_TERMINAL_PROMPT=0 disables credential prompts`
|
|
226
|
+
: stderr.trim() || (error === null ? "" : error.message),
|
|
227
|
+
});
|
|
228
|
+
});
|
|
229
|
+
});
|
|
230
|
+
}
|
|
231
|
+
/** Resolve HEAD without a shell, so repository paths remain data, never code. */
|
|
232
|
+
export async function repositoryCommit(repoDir) {
|
|
233
|
+
const result = await git(repoDir, ["rev-parse", "--verify", "HEAD^{commit}"]);
|
|
234
|
+
if (result.code !== 0) {
|
|
235
|
+
return {
|
|
236
|
+
ok: false,
|
|
237
|
+
message: `Federated verification requires a git repository with a committed HEAD: ${result.stderr || "git rev-parse failed"}`,
|
|
238
|
+
};
|
|
239
|
+
}
|
|
240
|
+
const commit = result.stdout.trim();
|
|
241
|
+
if (!/^[0-9a-f]{40,64}$/i.test(commit)) {
|
|
242
|
+
return { ok: false, message: `Git returned an invalid HEAD commit '${commit}'.` };
|
|
243
|
+
}
|
|
244
|
+
return { ok: true, commit };
|
|
245
|
+
}
|
|
246
|
+
//# sourceMappingURL=results.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"results.js","sourceRoot":"","sources":["../../../src/commands/verify/results.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * The cucumber report, and the repository it has to be evidence FROM.\n *\n * A results file is only evidence if it describes the commit the attestation\n * names, so reading it and checking the repository are one subject: the git\n * helpers below are here because they answer the same question — is this\n * working tree the one the report was produced from.\n */\nimport { execFile } from \"node:child_process\";\nimport { readFile, stat } from \"node:fs/promises\";\nimport { resolvePortableFileInside } from \"../../core/kernel/path-safety.js\";\nimport { isScenarioReport, readCucumberReport, readScenarioReport, type ReportScenario } from \"../../core/results.js\";\nimport { type Answer } from \"../../core/verify/answers.js\";\nimport { citedLine, pinnedDigest, sourceLines, type EvidencePin } from \"../../core/verify/pins/pin.js\";\nimport { type ConsumedReport, type ConsumedReports } from \"../../core/verify/record.js\";\nimport { type EvidencePins, type TokenMiss } from \"./evidence/pins.js\";\nimport { readReportArtifact } from \"./evidence/read.js\";\n\nexport type ResultsRead =\n | { ok: true; report: ConsumedReport; scenarios: ReportScenario[] }\n | { ok: false; code: \"answers-unreadable\"; message: string };\n\n/**\n * The report as an artifact, not merely as a parse. The resolve/read/digest\n * plumbing lives in `./evidence/read.ts`, shared with `--contract-results`:\n * the bytes are digested and the file's mtime read, because loam cannot prove\n * a JSON file came from executing this commit and should stop implying\n * otherwise. It can say precisely which file it consumed, and that goes on\n * the record.\n */\nexport async function readResults(spelled: string, repoDir: string | undefined): Promise<ResultsRead> {\n const artifact = await readReportArtifact(spelled, repoDir, \"test report\");\n if (!artifact.ok) return artifact;\n // The reader is chosen by MARKER, before either shape has validated\n // anything. A file declaring `loamScenarioReport` must be graded as that\n // shape even when it is malformed — falling through to the cucumber parser\n // would tell an author their report is not a cucumber array, which is true,\n // useless, and hides the actual mistake.\n const parsed = isScenarioReport(artifact.doc)\n ? readScenarioReport(artifact.doc, spelled)\n : readCucumberReport(artifact.doc, spelled);\n if (!parsed.ok) return { ok: false, code: \"answers-unreadable\", message: parsed.message };\n return {\n ok: true,\n scenarios: parsed.scenarios,\n report: {\n path: artifact.spelled,\n digest: artifact.digest,\n mtime: artifact.mtime,\n scenarios: parsed.scenarios.length,\n },\n };\n}\n\n/**\n * The repository an attestation binds to and the commit it binds at — two\n * values only ever asked about together, so they travel as one and the\n * inconsistent pair (this repo, that commit) stops being representable.\n */\nexport interface AttestationBinding {\n repoDir: string;\n commit: string;\n}\n\nexport type ServiceEvidenceCheck =\n /** Every citation held up; `pins`/`tokenMisses` are the stamp and the honesty it computed en route. */\n | ({ ok: true } & EvidencePins)\n | { ok: false; message: string };\n\n/**\n * In federated mode, a confirmation is accepted only when its evidence holds up\n * in this repository: an agent's `file:line` resolves to a real line that is\n * unchanged at the attested commit, and each runner's evidence names the report\n * loam just read — the cucumber report for `runner`, the contract report for\n * `external-runner`. Legacy global mode deliberately keeps its original, looser\n * evidence contract for backward compatibility.\n *\n * The same pass now STAMPS what it validated: one `EvidencePin` per agent\n * citation, built where `committedFile`'s blob is already in hand (zero new\n * git calls), and one `TokenMiss` per citation whose blob does not contain the\n * claim's token (`tokens`, claim id → the literal the claim asserts). Runner\n * and contract answers get no pins — their evidence names a report entry, not\n * a file — and the legacy all-at-once form never calls this validator, so it\n * stamps nothing.\n */\nexport async function validateServiceEvidence(\n answers: Answer[],\n binding: AttestationBinding,\n reports: ConsumedReports,\n tokens: ReadonlyMap<string, string>,\n): Promise<ServiceEvidenceCheck> {\n const { repoDir, commit } = binding;\n const err = (message: string): ServiceEvidenceCheck => ({ ok: false, message });\n const pins = new Map<string, EvidencePin[]>();\n const tokenMisses: TokenMiss[] = [];\n for (const report of [reports.results, reports.contract]) {\n if (report === undefined) continue;\n // Most reports are build output and untracked, and `git diff` is quiet on\n // those — nothing here invents a rule for them. One the repository DOES\n // carry has to match the commit being attested, like any other file the\n // evidence rests on. Both reports independently: each is its own file\n // making its own claim about this commit.\n const clean = await git(repoDir, [\"diff\", \"--quiet\", commit, \"--\", report.path]);\n if (clean.code !== 0) {\n // Only `1` is git ANSWERING — \"that file differs\". Every other non-zero\n // code is git failing to answer at all: no git on PATH, a fork that never\n // happened, a child killed by a CI timeout or the OOM killer. Testing for\n // `1` alone made those silently pass the check, so the one run that most\n // needs the binding — the one where the machine is falling over — is the\n // one that would mint an attestation whose report was never bound to the\n // commit. An unanswered check is not a passed one, so the unknown refuses\n // on the same terms as the known, and `clean.stderr` carries whatever\n // account of itself the child managed to leave.\n return err(\n clean.code === 1\n ? `The test report '${report.path}' is committed to this repository and differs from ${commit.slice(0, 12)} — commit it or attest the commit it belongs to.`\n : `Cannot tell whether the test report '${report.path}' is bound to ${commit.slice(0, 12)} — git could not be run to completion: ${clean.stderr || \"git diff failed\"}`,\n );\n }\n }\n for (const answer of answers) {\n if (answer.verdict !== \"confirmed\") continue;\n if (answer.answered_by === \"runner\" || answer.answered_by === \"external-runner\") {\n // A runner's evidence is an entry inside its report, not a file:line in\n // the source — there is nothing to resolve. What must hold is that it\n // names THAT report: an answer pointing anywhere else is not this run's.\n const report = answer.answered_by === \"runner\" ? reports.results : reports.contract;\n const stray = answer.evidence.filter((e) => report === undefined || !e.startsWith(`${report.path}: `));\n if (stray.length > 0) {\n return err(`Claim ${answer.id} has runner evidence ${stray.map((e) => `'${e}'`).join(\", \")} that does not name the report loam read.`);\n }\n continue;\n }\n for (const evidence of answer.evidence) {\n const match = /^(.+):([1-9]\\d*)$/.exec(evidence);\n if (match === null) {\n return err(`Claim ${answer.id} has evidence '${evidence}' — service evidence must be a canonical relative file:line.`);\n }\n const relativePath = match[1]!;\n const line = Number(match[2]);\n let absolutePath: string;\n try {\n absolutePath = resolvePortableFileInside(repoDir, relativePath, `evidence for ${answer.id}`);\n } catch (cause) {\n return err(`Claim ${answer.id} has unsafe evidence '${evidence}': ${cause instanceof Error ? cause.message : String(cause)}`);\n }\n try {\n const info = await stat(absolutePath);\n if (!info.isFile()) {\n return err(`Claim ${answer.id} has evidence '${evidence}', but '${relativePath}' is not a regular file.`);\n }\n const source = await readFile(absolutePath, \"utf8\");\n const lines = sourceLines(source).length;\n if (line > lines) {\n return err(`Claim ${answer.id} has evidence '${evidence}', but '${relativePath}' has only ${lines} line(s).`);\n }\n const committed = await committedFile(repoDir, commit, relativePath);\n if (!committed.ok) {\n return err(`Claim ${answer.id} has evidence '${evidence}' that is not bound to ${commit.slice(0, 12)}: ${committed.message}`);\n }\n // The pin, built from the blob just fetched. `citedLine` is undefined\n // exactly when the line is past the committed file's end — the refusal\n // this validator already owed — so the bounds check and the pinned\n // text can never disagree about where lines fall.\n const text = citedLine(committed.source, line);\n if (text === undefined) {\n const committedLines = sourceLines(committed.source).length;\n return err(`Claim ${answer.id} has evidence '${evidence}', but '${relativePath}' has only ${committedLines} line(s) at ${commit.slice(0, 12)}.`);\n }\n // The token scan happens here, per citation, because the blob is in\n // hand and record time is when the answerer can still re-read what\n // they cited. Substring, never a parse: the claim asserts a literal.\n // A blob that does NOT contain the token is warned once (the notice\n // built from `tokenMisses`) and the pin then omits the token — see\n // EvidencePin.token for why re-litigating a never-there token at\n // every later validate would be a false sentence with no repair.\n const token = tokens.get(answer.id);\n const held = token !== undefined && committed.source.includes(token);\n if (token !== undefined && !held) tokenMisses.push({ id: answer.id, evidence, token });\n const list = pins.get(answer.id) ?? [];\n list.push({\n path: relativePath,\n line,\n file_sha256: pinnedDigest(committed.source),\n text,\n ...(held && token !== undefined ? { token } : {}),\n });\n pins.set(answer.id, list);\n } catch (cause) {\n return err(`Claim ${answer.id} has unreadable evidence '${evidence}': ${cause instanceof Error ? cause.message : String(cause)}`);\n }\n }\n }\n return { ok: true, pins, tokenMisses };\n}\n\ntype CommitResult = { ok: true; commit: string } | { ok: false; message: string };\ntype CommittedFileResult = { ok: true; source: string } | { ok: false; message: string };\n\n/** Require the evidence blob to exist at HEAD and have no uncommitted edits. */\nexport async function committedFile(repoDir: string, commit: string, path: string): Promise<CommittedFileResult> {\n const clean = await git(repoDir, [\"diff\", \"--quiet\", commit, \"--\", path]);\n if (clean.code !== 0) {\n return {\n ok: false,\n message: clean.code === 1 ? `'${path}' has uncommitted changes` : clean.stderr || \"git diff failed\",\n };\n }\n const blob = await git(repoDir, [\"show\", `${commit}:${path}`]);\n if (blob.code !== 0) {\n return { ok: false, message: blob.stderr || `'${path}' is not tracked by that commit` };\n }\n return { ok: true, source: blob.stdout };\n}\n\ninterface GitResult {\n /**\n * Git's own exit status, or `-1` for \"git could not be run to completion\" —\n * no git on PATH, a spawn failure, a child killed by a signal (the deadline\n * below included), or output past the declared cap. Callers read specific\n * meaning into small numbers (`1` from `git diff --quiet` is \"that file\n * differs\"), so a run that never reached an exit status must not borrow one\n * of them.\n */\n code: number;\n stdout: string;\n stderr: string;\n}\n\n/**\n * How long one git question may take, and how much it may say. Both bounds\n * exist because every call in this module now runs while `loam verify --record`\n * HOLDS the docs lock: a `git rev-parse` blocked on a credential-helper prompt\n * used to hang this process forever with the lock in hand, wedging every\n * archive, rebase and record in the fleet behind a live pid that\n * `breakStaleLock` rightly refuses to break. The timeout mirrors\n * `core/provenance/git.ts`; the cap is sized for `git show` of a source file\n * an attestation cites (the default 1 MiB refused sound evidence the moment a\n * generated client crossed it), not for arbitrary blobs.\n */\nconst GIT_TIMEOUT_MS = 10_000;\nconst GIT_MAX_OUTPUT_BYTES = 64 * 1024 * 1024;\n\nfunction git(repoDir: string, args: string[]): Promise<GitResult> {\n return new Promise((done) => {\n execFile(\n \"git\",\n [\"-C\", repoDir, ...args],\n { encoding: \"utf8\", timeout: GIT_TIMEOUT_MS, maxBuffer: GIT_MAX_OUTPUT_BYTES },\n (error, stdout, stderr) => {\n done({\n // `error.code` is an exit status only when it is a number: a spawn\n // failure reports an errno string (\"ENOENT\") and a signal-killed child\n // reports none at all. Folding those onto 1 would state that `git diff`\n // found a difference — a Ctrl-C or an OOM kill during\n // `loam verify --record` would read as an uncommitted edit and refuse\n // a sound attestation. -1 is not a status git assigns, so both diff\n // sites fall to their stderr-carrying arm and say what happened.\n code: error === null ? 0 : typeof error.code === \"number\" ? error.code : -1,\n stdout,\n // A child that never ran writes nothing to stderr, so its only account\n // of itself is on the error; without this the refusal would name no\n // cause at all. A child WE killed gets its account written for it: the\n // deadline exists for a git blocked on a credential-helper prompt, and\n // the raw \"Command failed: git …\" says nothing about a deadline — the\n // operator re-runs, waits the same 10 s, and reads the same message,\n // with the actual fix (GIT_TERMINAL_PROMPT=0, or the helper) never\n // reachable from it.\n stderr:\n error !== null && error.killed && error.signal !== null\n ? `git did not answer within ${GIT_TIMEOUT_MS / 1000}s and was stopped — a prompt or a hung remote; GIT_TERMINAL_PROMPT=0 disables credential prompts`\n : stderr.trim() || (error === null ? \"\" : error.message),\n });\n });\n });\n}\n\n/** Resolve HEAD without a shell, so repository paths remain data, never code. */\nexport async function repositoryCommit(repoDir: string): Promise<CommitResult> {\n const result = await git(repoDir, [\"rev-parse\", \"--verify\", \"HEAD^{commit}\"]);\n if (result.code !== 0) {\n return {\n ok: false,\n message: `Federated verification requires a git repository with a committed HEAD: ${result.stderr || \"git rev-parse failed\"}`,\n };\n }\n const commit = result.stdout.trim();\n if (!/^[0-9a-f]{40,64}$/i.test(commit)) {\n return { ok: false, message: `Git returned an invalid HEAD commit '${commit}'.` };\n }\n return { ok: true, commit };\n}\n"]}
|
|
@@ -0,0 +1,217 @@
|
|
|
1
|
+
import { loadConfig } from "../../core/envelope/config.js";
|
|
2
|
+
import { fail, repoPath, reportNoConfig } from "../../core/envelope/json.js";
|
|
3
|
+
import { missingFeatureMessage, resolveFeature } from "../../core/repo/repo.js";
|
|
4
|
+
import { InterruptedCommitError } from "../../core/staging/interrupted.js";
|
|
5
|
+
import { acquireDocsLockWaiting, DocsBusyError, LOCK_WAIT_MS } from "../../core/staging/lock.js";
|
|
6
|
+
import { recoverInterruptedCommit } from "../../core/staging/recovery/recover.js";
|
|
7
|
+
import { docsRepoReady } from "../policy/gate.js";
|
|
8
|
+
import { featureChecklist } from "../../core/verify/checklist.js";
|
|
9
|
+
import { readVerificationState } from "../../core/verify/file.js";
|
|
10
|
+
import { verificationPath } from "../../core/verify/record.js";
|
|
11
|
+
import { report } from "./report.js";
|
|
12
|
+
import { reportFrozen } from "./frozen.js";
|
|
13
|
+
import { record } from "./record.js";
|
|
14
|
+
import {} from "./record.js";
|
|
15
|
+
import { diffAnswers } from "./cross/diff.js";
|
|
16
|
+
export function registerVerify(program) {
|
|
17
|
+
program
|
|
18
|
+
.command("verify")
|
|
19
|
+
.argument("<featureId>", "feature id, e.g. FEAT-101")
|
|
20
|
+
.description("Check a shipped feature against its own promises: derive the claims, record the answers")
|
|
21
|
+
.option("--record <file>", "record a JSON answer set against the current checklist")
|
|
22
|
+
.option("--results <file>", "answer the scenario.tested claims mechanically from a cucumber JSON test report")
|
|
23
|
+
.option("--contract-results <file>", "answer the api.exposes claims mechanically from an API contract-test report (loam's generic contract-results JSON)")
|
|
24
|
+
.option("--diff-answers <files...>", "cross-examine two independent answer sets against the current checklist (reads only, writes nothing)")
|
|
25
|
+
.option("--service <id>", "record only this service's claims, bound to the current repository commit")
|
|
26
|
+
.option("--json", "emit the machine contract instead of the human view")
|
|
27
|
+
.action(async (featureId, opts) => {
|
|
28
|
+
const json = opts.json === true;
|
|
29
|
+
const loaded = await loadConfig();
|
|
30
|
+
if (loaded.kind !== "loaded") {
|
|
31
|
+
reportNoConfig(json, loaded);
|
|
32
|
+
return;
|
|
33
|
+
}
|
|
34
|
+
const config = loaded.config;
|
|
35
|
+
const { docsDir } = config;
|
|
36
|
+
const recording = opts.record !== undefined || opts.results !== undefined || opts.contractResults !== undefined;
|
|
37
|
+
// --diff-answers is a third READ form (see the WRITING/READING split
|
|
38
|
+
// below): it validates two answer sets against the same checklist and
|
|
39
|
+
// writes nothing, so combining it with any recording flag is a
|
|
40
|
+
// contradiction refused up front — before the binding, before the lock —
|
|
41
|
+
// rather than a mode silently picked between the two.
|
|
42
|
+
let diffFiles = null;
|
|
43
|
+
if (opts.diffAnswers !== undefined) {
|
|
44
|
+
if (recording) {
|
|
45
|
+
return fail(json, "invalid-option", "--diff-answers is a read-only cross-examination: it validates both files and writes nothing, " +
|
|
46
|
+
"so it cannot be combined with --record, --results or --contract-results. " +
|
|
47
|
+
"Diff first, then record the answer set you stand behind.");
|
|
48
|
+
}
|
|
49
|
+
// Commander's variadic form consumes arguments until the next --flag,
|
|
50
|
+
// so a featureId placed AFTER the flag arrives here as a third file —
|
|
51
|
+
// this refusal is what turns that mistake into a sentence instead of a
|
|
52
|
+
// diff of the wrong files.
|
|
53
|
+
const [a, b] = opts.diffAnswers;
|
|
54
|
+
if (opts.diffAnswers.length !== 2 || a === undefined || b === undefined) {
|
|
55
|
+
return fail(json, "invalid-option", "--diff-answers takes exactly two answer files: --diff-answers a.json b.json");
|
|
56
|
+
}
|
|
57
|
+
diffFiles = { a, b };
|
|
58
|
+
}
|
|
59
|
+
// WRITING is bound to the repository; READING is not. An attestation
|
|
60
|
+
// pins claims to this repo's git HEAD and to file:line evidence inside
|
|
61
|
+
// it, so writing (--record, --results or --contract-results) with
|
|
62
|
+
// `--service X` may only run where loam.json says this repo IS X —
|
|
63
|
+
// vouch's and gherkin's refusal, for vouch's reason: from anywhere
|
|
64
|
+
// else, that repository is somebody else's. Reading takes
|
|
65
|
+
// `--service` as a pure lens (which claims are checkout-web's, and what
|
|
66
|
+
// has it said?) and needs no binding at all, because it writes nothing.
|
|
67
|
+
if (recording && opts.service !== undefined) {
|
|
68
|
+
if (config.service === undefined) {
|
|
69
|
+
return fail(json, "repository-unavailable", `Cannot attest for '${opts.service}' from here: this is not a service repo — loam.json declares no \`service\`. ` +
|
|
70
|
+
`A federated attestation binds the answers to this repository's git HEAD and to evidence inside it, so the repository has to say which service it is. ` +
|
|
71
|
+
`Add "service": "${opts.service}" to loam.json, or record it from that service's own repo.`);
|
|
72
|
+
}
|
|
73
|
+
if (config.service !== opts.service) {
|
|
74
|
+
return fail(json, "service-mismatch", `This repository is configured as service '${config.service}', so it cannot attest claims for '${opts.service}'.`);
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
// Before the lock, because the lock is a file INSIDE the docs repo: a
|
|
78
|
+
// `docsDir` pointing at nothing used to surface as the lock file's own
|
|
79
|
+
// ENOENT — an `internal` where the read side of this same command names
|
|
80
|
+
// the misconfiguration. "docs", not "services", in BOTH modes — and that
|
|
81
|
+
// is a pinned contract, not looseness: the legacy all-at-once form
|
|
82
|
+
// records against a features-only docs repo with no `services/` at all,
|
|
83
|
+
// and it has always exited 0 there. The cost is real and accepted: a
|
|
84
|
+
// typo'd `docsDir` pointing at some existing non-docs directory gets a
|
|
85
|
+
// `.loam-lock` created and removed inside it. The gate above only
|
|
86
|
+
// guarantees the directory exists.
|
|
87
|
+
if (!docsRepoReady(json, docsDir, "docs"))
|
|
88
|
+
return;
|
|
89
|
+
// A record is a merge over the docs repo, so recording takes the docs
|
|
90
|
+
// lock — the same one archive, unarchive and rebase hold — and takes it
|
|
91
|
+
// HERE, before the feature is even resolved. Behind the lock, an archive
|
|
92
|
+
// can rename this feature's directory into features/archive/ at any
|
|
93
|
+
// moment; a feature resolved before the lock could have its record
|
|
94
|
+
// written into the pre-move path, a file nothing will ever read again.
|
|
95
|
+
// The waiting form rather than archive's refuse-fast one, because two
|
|
96
|
+
// recorders for different services of one feature are both supposed to
|
|
97
|
+
// land: the second waits out the first's sub-second window and merges
|
|
98
|
+
// over what it left. Reading stays lock-free — it writes nothing.
|
|
99
|
+
let releaseLock = null;
|
|
100
|
+
let recovered = null;
|
|
101
|
+
if (recording) {
|
|
102
|
+
try {
|
|
103
|
+
releaseLock = await acquireDocsLockWaiting(docsDir, LOCK_WAIT_MS);
|
|
104
|
+
}
|
|
105
|
+
catch (err) {
|
|
106
|
+
if (err instanceof DocsBusyError)
|
|
107
|
+
return fail(json, "docs-busy", err.message);
|
|
108
|
+
throw err;
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
try {
|
|
112
|
+
// A journal under the lock means the last writer never finished. The
|
|
113
|
+
// record's own authoritative read is about to happen; reading it over
|
|
114
|
+
// a half-commit would merge against bytes no run ever produced — the
|
|
115
|
+
// last docs-repo writer that could still silently treat a half-commit
|
|
116
|
+
// as healthy. INSIDE the try: this refusal must release the lock like
|
|
117
|
+
// every other path out, and once did not.
|
|
118
|
+
if (recording) {
|
|
119
|
+
try {
|
|
120
|
+
recovered = await recoverInterruptedCommit(docsDir);
|
|
121
|
+
}
|
|
122
|
+
catch (err) {
|
|
123
|
+
if (!(err instanceof InterruptedCommitError))
|
|
124
|
+
throw err;
|
|
125
|
+
return fail(json, "commit-interrupted", err.message);
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
// Archived features resolve too: a shipped feature's verification is worth
|
|
129
|
+
// reading back, and it travelled into the archive with everything else.
|
|
130
|
+
const feature = await resolveFeature(docsDir, featureId, "include");
|
|
131
|
+
if (!feature) {
|
|
132
|
+
// repo.ts's sentence, not a second copy of it. Resolving with "include"
|
|
133
|
+
// means the archived branch inside it cannot fire here — an archived
|
|
134
|
+
// feature resolves — so the text is exactly what it was; what changes is
|
|
135
|
+
// that a reworded miss message can no longer leave this one behind.
|
|
136
|
+
return fail(json, "unknown-target", await missingFeatureMessage(docsDir, featureId));
|
|
137
|
+
}
|
|
138
|
+
// The cross-examination lens leaves BEFORE the record is read below:
|
|
139
|
+
// it never consumes verification.yaml, and the moment a
|
|
140
|
+
// cross-examination is most needed is exactly when the record on disk
|
|
141
|
+
// is too damaged to read and somebody is about to re-record over it —
|
|
142
|
+
// the record-unreadable refusal must not bar the one command that
|
|
143
|
+
// would inform that re-recording. The archived refusal still applies,
|
|
144
|
+
// with the recording branch's code and reason: there is no current
|
|
145
|
+
// checklist to answer, so the wrong thing is the option, not the
|
|
146
|
+
// answer sets.
|
|
147
|
+
if (diffFiles !== null) {
|
|
148
|
+
if (feature.archived) {
|
|
149
|
+
return fail(json, "invalid-option", `${feature.id} is archived — there is no current checklist to answer, so there is nothing to cross-examine; its record is frozen history. \`loam unarchive ${feature.id}\` first if the claims really need re-answering.`);
|
|
150
|
+
}
|
|
151
|
+
await diffAnswers({ docsDir, featureDir: feature.dir, json }, await featureChecklist(docsDir, feature.dir, feature.id), diffFiles, { lens: opts.service });
|
|
152
|
+
return;
|
|
153
|
+
}
|
|
154
|
+
// Before anything else: a verification.yaml that exists but will not read
|
|
155
|
+
// is a refusal, not an absence. It holds somebody's answers and — in a
|
|
156
|
+
// fleet — other repositories' attestations; treating it as "not verified"
|
|
157
|
+
// let the next --record silently overwrite all of it, and made the read
|
|
158
|
+
// view say "no record" about a file sitting right there.
|
|
159
|
+
const existing = await readVerificationState(feature.dir);
|
|
160
|
+
if (existing.state === "unreadable") {
|
|
161
|
+
return fail(json, "record-unreadable", `${repoPath(docsDir, verificationPath(feature.dir))} exists but cannot be read as a verification record: ${existing.reason}. ` +
|
|
162
|
+
(existing.code === "verify.record-miscounted"
|
|
163
|
+
? "Record the answers again rather than editing the counts: a summary made to agree by hand still says nothing about what was checked."
|
|
164
|
+
: "It is plain YAML — repair it by hand, or delete it and record again. loam will not overwrite a record it could not read."));
|
|
165
|
+
}
|
|
166
|
+
const recorded = existing.state === "ok" ? existing.verification : null;
|
|
167
|
+
// But an archived feature never gets a re-derived checklist — see the
|
|
168
|
+
// header. Its record is frozen history, and --record / --results refuse
|
|
169
|
+
// alike: the code is `invalid-option` rather than `answers-mismatch`
|
|
170
|
+
// because the answers were never compared to anything — there is no
|
|
171
|
+
// current checklist to answer, so the wrong thing here is the option,
|
|
172
|
+
// not the answer set.
|
|
173
|
+
if (feature.archived) {
|
|
174
|
+
if (recording) {
|
|
175
|
+
return fail(json, "invalid-option", `${feature.id} is archived — its verification is history now. \`loam unarchive ${feature.id}\` first if the answers really need to change.`);
|
|
176
|
+
}
|
|
177
|
+
reportFrozen({ docsDir, featureDir: feature.dir, json }, feature.id, recorded);
|
|
178
|
+
return;
|
|
179
|
+
}
|
|
180
|
+
const checklist = await featureChecklist(docsDir, feature.dir, feature.id);
|
|
181
|
+
if (recording) {
|
|
182
|
+
// Standing in a service repo, `--service` is what the repo already
|
|
183
|
+
// says: omitting it must not fall back to the legacy all-at-once form,
|
|
184
|
+
// which claims the whole fleet's checklist on this one repo's word.
|
|
185
|
+
//
|
|
186
|
+
// The repo root, not the cwd — `vouch`'s and `gherkin`'s reason. An
|
|
187
|
+
// attestation names a commit and rests on evidence resolved inside
|
|
188
|
+
// "this repository", and `config.root` is the directory loam.json was
|
|
189
|
+
// actually found in. Off `process.cwd()`, running from a subdirectory
|
|
190
|
+
// silently changed all three: evidence resolved against the
|
|
191
|
+
// subdirectory, `--results` had to live in it, and `git rev-parse` ran
|
|
192
|
+
// there — a submodule or nested checkout under it would have named
|
|
193
|
+
// somebody else's commit on this repository's attestation.
|
|
194
|
+
await record({ docsDir, featureDir: feature.dir, json, recovered }, checklist, {
|
|
195
|
+
service: opts.service ?? config.service,
|
|
196
|
+
repoDir: config.root ?? process.cwd(),
|
|
197
|
+
previous: existing.state === "ok" ? { verification: existing.verification, raw: existing.raw } : null,
|
|
198
|
+
}, opts);
|
|
199
|
+
return;
|
|
200
|
+
}
|
|
201
|
+
report({ docsDir, featureDir: feature.dir, json }, checklist, recorded, {
|
|
202
|
+
lens: opts.service,
|
|
203
|
+
bound: config.service,
|
|
204
|
+
});
|
|
205
|
+
}
|
|
206
|
+
finally {
|
|
207
|
+
// Released on every path out — a refusal that kept the lock would turn
|
|
208
|
+
// one bad answer set into a wedged docs repo for everyone else.
|
|
209
|
+
if (releaseLock !== null)
|
|
210
|
+
await releaseLock();
|
|
211
|
+
}
|
|
212
|
+
});
|
|
213
|
+
}
|
|
214
|
+
/* ------------------------------------------------------------------ */
|
|
215
|
+
/* Reading */
|
|
216
|
+
/* ------------------------------------------------------------------ */
|
|
217
|
+
//# sourceMappingURL=verify.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"verify.js","sourceRoot":"","sources":["../../../src/commands/verify/verify.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,UAAU,EAAE,MAAM,+BAA+B,CAAC;AAC3D,OAAO,EAAE,IAAI,EAAE,QAAQ,EAAE,cAAc,EAAE,MAAM,6BAA6B,CAAC;AAC7E,OAAO,EAAE,qBAAqB,EAAE,cAAc,EAAE,MAAM,yBAAyB,CAAC;AAChF,OAAO,EAAuB,sBAAsB,EAAE,MAAM,mCAAmC,CAAC;AAChG,OAAO,EAAE,sBAAsB,EAAE,aAAa,EAAE,YAAY,EAAE,MAAM,4BAA4B,CAAC;AACjG,OAAO,EAAE,wBAAwB,EAAE,MAAM,wCAAwC,CAAC;AAClF,OAAO,EAAE,aAAa,EAAE,MAAM,mBAAmB,CAAC;AAClD,OAAO,EAAE,gBAAgB,EAAE,MAAM,gCAAgC,CAAC;AAClE,OAAO,EAAE,qBAAqB,EAAE,MAAM,2BAA2B,CAAC;AAClE,OAAO,EAAE,gBAAgB,EAAE,MAAM,6BAA6B,CAAC;AAC/D,OAAO,EAAE,MAAM,EAAE,MAAM,aAAa,CAAC;AACrC,OAAO,EAAE,YAAY,EAAE,MAAM,aAAa,CAAC;AAC3C,OAAO,EAAE,MAAM,EAAE,MAAM,aAAa,CAAC;AACrC,OAAO,EAAsB,MAAM,aAAa,CAAC;AACjD,OAAO,EAAE,WAAW,EAAoB,MAAM,iBAAiB,CAAC;AAGhE,MAAM,UAAU,cAAc,CAAC,OAAgB;IAC7C,OAAO;SACJ,OAAO,CAAC,QAAQ,CAAC;SACjB,QAAQ,CAAC,aAAa,EAAE,2BAA2B,CAAC;SACpD,WAAW,CAAC,yFAAyF,CAAC;SACtG,MAAM,CAAC,iBAAiB,EAAE,wDAAwD,CAAC;SACnF,MAAM,CACL,kBAAkB,EAClB,iFAAiF,CAClF;SACA,MAAM,CACL,2BAA2B,EAC3B,oHAAoH,CACrH;SACA,MAAM,CACL,2BAA2B,EAC3B,sGAAsG,CACvG;SACA,MAAM,CACL,gBAAgB,EAChB,2EAA2E,CAC5E;SACA,MAAM,CAAC,QAAQ,EAAE,qDAAqD,CAAC;SACvE,MAAM,CAAC,KAAK,EAAE,SAAiB,EAAE,IAAmB,EAAE,EAAE;QACvD,MAAM,IAAI,GAAG,IAAI,CAAC,IAAI,KAAK,IAAI,CAAC;QAEhC,MAAM,MAAM,GAAG,MAAM,UAAU,EAAE,CAAC;QAClC,IAAI,MAAM,CAAC,IAAI,KAAK,QAAQ,EAAE,CAAC;YAC7B,cAAc,CAAC,IAAI,EAAE,MAAM,CAAC,CAAC;YAC7B,OAAO;QACT,CAAC;QACD,MAAM,MAAM,GAAG,MAAM,CAAC,MAAM,CAAC;QAC7B,MAAM,EAAE,OAAO,EAAE,GAAG,MAAM,CAAC;QAC3B,MAAM,SAAS,GACb,IAAI,CAAC,MAAM,KAAK,SAAS,IAAI,IAAI,CAAC,OAAO,KAAK,SAAS,IAAI,IAAI,CAAC,eAAe,KAAK,SAAS,CAAC;QAEhG,qEAAqE;QACrE,sEAAsE;QACtE,+DAA+D;QAC/D,yEAAyE;QACzE,sDAAsD;QACtD,IAAI,SAAS,GAAuB,IAAI,CAAC;QACzC,IAAI,IAAI,CAAC,WAAW,KAAK,SAAS,EAAE,CAAC;YACnC,IAAI,SAAS,EAAE,CAAC;gBACd,OAAO,IAAI,CACT,IAAI,EACJ,gBAAgB,EAChB,+FAA+F;oBAC7F,2EAA2E;oBAC3E,0DAA0D,CAC7D,CAAC;YACJ,CAAC;YACD,sEAAsE;YACtE,sEAAsE;YACtE,uEAAuE;YACvE,2BAA2B;YAC3B,MAAM,CAAC,CAAC,EAAE,CAAC,CAAC,GAAG,IAAI,CAAC,WAAW,CAAC;YAChC,IAAI,IAAI,CAAC,WAAW,CAAC,MAAM,KAAK,CAAC,IAAI,CAAC,KAAK,SAAS,IAAI,CAAC,KAAK,SAAS,EAAE,CAAC;gBACxE,OAAO,IAAI,CACT,IAAI,EACJ,gBAAgB,EAChB,6EAA6E,CAC9E,CAAC;YACJ,CAAC;YACD,SAAS,GAAG,EAAE,CAAC,EAAE,CAAC,EAAE,CAAC;QACvB,CAAC;QAED,qEAAqE;QACrE,uEAAuE;QACvE,kEAAkE;QAClE,mEAAmE;QACnE,mEAAmE;QACnE,0DAA0D;QAC1D,wEAAwE;QACxE,wEAAwE;QACxE,IAAI,SAAS,IAAI,IAAI,CAAC,OAAO,KAAK,SAAS,EAAE,CAAC;YAC5C,IAAI,MAAM,CAAC,OAAO,KAAK,SAAS,EAAE,CAAC;gBACjC,OAAO,IAAI,CACT,IAAI,EACJ,wBAAwB,EACxB,sBAAsB,IAAI,CAAC,OAAO,+EAA+E;oBAC/G,uJAAuJ;oBACvJ,mBAAmB,IAAI,CAAC,OAAO,4DAA4D,CAC9F,CAAC;YACJ,CAAC;YACD,IAAI,MAAM,CAAC,OAAO,KAAK,IAAI,CAAC,OAAO,EAAE,CAAC;gBACpC,OAAO,IAAI,CACT,IAAI,EACJ,kBAAkB,EAClB,6CAA6C,MAAM,CAAC,OAAO,sCAAsC,IAAI,CAAC,OAAO,IAAI,CAClH,CAAC;YACJ,CAAC;QACH,CAAC;QAED,sEAAsE;QACtE,uEAAuE;QACvE,wEAAwE;QACxE,yEAAyE;QACzE,mEAAmE;QACnE,wEAAwE;QACxE,qEAAqE;QACrE,uEAAuE;QACvE,kEAAkE;QAClE,mCAAmC;QACnC,IAAI,CAAC,aAAa,CAAC,IAAI,EAAE,OAAO,EAAE,MAAM,CAAC;YAAE,OAAO;QAElD,sEAAsE;QACtE,wEAAwE;QACxE,yEAAyE;QACzE,oEAAoE;QACpE,mEAAmE;QACnE,uEAAuE;QACvE,sEAAsE;QACtE,uEAAuE;QACvE,sEAAsE;QACtE,kEAAkE;QAClE,IAAI,WAAW,GAAiC,IAAI,CAAC;QACrD,IAAI,SAAS,GAA0B,IAAI,CAAC;QAC5C,IAAI,SAAS,EAAE,CAAC;YACd,IAAI,CAAC;gBACH,WAAW,GAAG,MAAM,sBAAsB,CAAC,OAAO,EAAE,YAAY,CAAC,CAAC;YACpE,CAAC;YAAC,OAAO,GAAG,EAAE,CAAC;gBACb,IAAI,GAAG,YAAY,aAAa;oBAAE,OAAO,IAAI,CAAC,IAAI,EAAE,WAAW,EAAE,GAAG,CAAC,OAAO,CAAC,CAAC;gBAC9E,MAAM,GAAG,CAAC;YACZ,CAAC;QACH,CAAC;QACD,IAAI,CAAC;YACH,qEAAqE;YACrE,sEAAsE;YACtE,qEAAqE;YACrE,sEAAsE;YACtE,sEAAsE;YACtE,0CAA0C;YAC1C,IAAI,SAAS,EAAE,CAAC;gBACd,IAAI,CAAC;oBACH,SAAS,GAAG,MAAM,wBAAwB,CAAC,OAAO,CAAC,CAAC;gBACtD,CAAC;gBAAC,OAAO,GAAG,EAAE,CAAC;oBACb,IAAI,CAAC,CAAC,GAAG,YAAY,sBAAsB,CAAC;wBAAE,MAAM,GAAG,CAAC;oBACxD,OAAO,IAAI,CAAC,IAAI,EAAE,oBAAoB,EAAE,GAAG,CAAC,OAAO,CAAC,CAAC;gBACvD,CAAC;YACH,CAAC;YACD,2EAA2E;YAC3E,wEAAwE;YACxE,MAAM,OAAO,GAAG,MAAM,cAAc,CAAC,OAAO,EAAE,SAAS,EAAE,SAAS,CAAC,CAAC;YACpE,IAAI,CAAC,OAAO,EAAE,CAAC;gBACb,wEAAwE;gBACxE,qEAAqE;gBACrE,yEAAyE;gBACzE,oEAAoE;gBACpE,OAAO,IAAI,CAAC,IAAI,EAAE,gBAAgB,EAAE,MAAM,qBAAqB,CAAC,OAAO,EAAE,SAAS,CAAC,CAAC,CAAC;YACvF,CAAC;YAED,qEAAqE;YACrE,wDAAwD;YACxD,sEAAsE;YACtE,sEAAsE;YACtE,kEAAkE;YAClE,sEAAsE;YACtE,mEAAmE;YACnE,iEAAiE;YACjE,eAAe;YACf,IAAI,SAAS,KAAK,IAAI,EAAE,CAAC;gBACvB,IAAI,OAAO,CAAC,QAAQ,EAAE,CAAC;oBACrB,OAAO,IAAI,CACT,IAAI,EACJ,gBAAgB,EAChB,GAAG,OAAO,CAAC,EAAE,gJAAgJ,OAAO,CAAC,EAAE,kDAAkD,CAC1N,CAAC;gBACJ,CAAC;gBACD,MAAM,WAAW,CACf,EAAE,OAAO,EAAE,UAAU,EAAE,OAAO,CAAC,GAAG,EAAE,IAAI,EAAE,EAC1C,MAAM,gBAAgB,CAAC,OAAO,EAAE,OAAO,CAAC,GAAG,EAAE,OAAO,CAAC,EAAE,CAAC,EACxD,SAAS,EACT,EAAE,IAAI,EAAE,IAAI,CAAC,OAAO,EAAE,CACvB,CAAC;gBACF,OAAO;YACT,CAAC;YAED,0EAA0E;YAC1E,uEAAuE;YACvE,0EAA0E;YAC1E,wEAAwE;YACxE,yDAAyD;YACzD,MAAM,QAAQ,GAAG,MAAM,qBAAqB,CAAC,OAAO,CAAC,GAAG,CAAC,CAAC;YAC1D,IAAI,QAAQ,CAAC,KAAK,KAAK,YAAY,EAAE,CAAC;gBACpC,OAAO,IAAI,CACT,IAAI,EACJ,mBAAmB,EACnB,GAAG,QAAQ,CAAC,OAAO,EAAE,gBAAgB,CAAC,OAAO,CAAC,GAAG,CAAC,CAAC,wDAAwD,QAAQ,CAAC,MAAM,IAAI;oBAC5H,CAAC,QAAQ,CAAC,IAAI,KAAK,0BAA0B;wBAC3C,CAAC,CAAC,qIAAqI;wBACvI,CAAC,CAAC,0HAA0H,CAAC,CAClI,CAAC;YACJ,CAAC;YACD,MAAM,QAAQ,GAAG,QAAQ,CAAC,KAAK,KAAK,IAAI,CAAC,CAAC,CAAC,QAAQ,CAAC,YAAY,CAAC,CAAC,CAAC,IAAI,CAAC;YAExE,sEAAsE;YACtE,wEAAwE;YACxE,qEAAqE;YACrE,oEAAoE;YACpE,sEAAsE;YACtE,sBAAsB;YACtB,IAAI,OAAO,CAAC,QAAQ,EAAE,CAAC;gBACrB,IAAI,SAAS,EAAE,CAAC;oBACd,OAAO,IAAI,CACT,IAAI,EACJ,gBAAgB,EAChB,GAAG,OAAO,CAAC,EAAE,oEAAoE,OAAO,CAAC,EAAE,gDAAgD,CAC5I,CAAC;gBACJ,CAAC;gBACD,YAAY,CAAC,EAAE,OAAO,EAAE,UAAU,EAAE,OAAO,CAAC,GAAG,EAAE,IAAI,EAAE,EAAE,OAAO,CAAC,EAAE,EAAE,QAAQ,CAAC,CAAC;gBAC/E,OAAO;YACT,CAAC;YAED,MAAM,SAAS,GAAG,MAAM,gBAAgB,CAAC,OAAO,EAAE,OAAO,CAAC,GAAG,EAAE,OAAO,CAAC,EAAE,CAAC,CAAC;YAE3E,IAAI,SAAS,EAAE,CAAC;gBACd,mEAAmE;gBACnE,uEAAuE;gBACvE,oEAAoE;gBACpE,EAAE;gBACF,oEAAoE;gBACpE,mEAAmE;gBACnE,sEAAsE;gBACtE,sEAAsE;gBACtE,4DAA4D;gBAC5D,uEAAuE;gBACvE,mEAAmE;gBACnE,2DAA2D;gBAC3D,MAAM,MAAM,CACV,EAAE,OAAO,EAAE,UAAU,EAAE,OAAO,CAAC,GAAG,EAAE,IAAI,EAAE,SAAS,EAAE,EACrD,SAAS,EACT;oBACE,OAAO,EAAE,IAAI,CAAC,OAAO,IAAI,MAAM,CAAC,OAAO;oBACvC,OAAO,EAAE,MAAM,CAAC,IAAI,IAAI,OAAO,CAAC,GAAG,EAAE;oBACrC,QAAQ,EAAE,QAAQ,CAAC,KAAK,KAAK,IAAI,CAAC,CAAC,CAAC,EAAE,YAAY,EAAE,QAAQ,CAAC,YAAY,EAAE,GAAG,EAAE,QAAQ,CAAC,GAAG,EAAE,CAAC,CAAC,CAAC,IAAI;iBACtG,EACD,IAAI,CACL,CAAC;gBACF,OAAO;YACT,CAAC;YAED,MAAM,CAAC,EAAE,OAAO,EAAE,UAAU,EAAE,OAAO,CAAC,GAAG,EAAE,IAAI,EAAE,EAAE,SAAS,EAAE,QAAQ,EAAE;gBACtE,IAAI,EAAE,IAAI,CAAC,OAAO;gBAClB,KAAK,EAAE,MAAM,CAAC,OAAO;aACtB,CAAC,CAAC;QACL,CAAC;gBAAS,CAAC;YACT,uEAAuE;YACvE,gEAAgE;YAChE,IAAI,WAAW,KAAK,IAAI;gBAAE,MAAM,WAAW,EAAE,CAAC;QAChD,CAAC;IACH,CAAC,CAAC,CAAC;AACP,CAAC;AAED,wEAAwE;AACxE,yEAAyE;AACzE,wEAAwE","sourcesContent":["import type { Command } from \"commander\";\nimport { loadConfig } from \"../../core/envelope/config.js\";\nimport { fail, repoPath, reportNoConfig } from \"../../core/envelope/json.js\";\nimport { missingFeatureMessage, resolveFeature } from \"../../core/repo/repo.js\";\nimport { type CommitRecovery, InterruptedCommitError } from \"../../core/staging/interrupted.js\";\nimport { acquireDocsLockWaiting, DocsBusyError, LOCK_WAIT_MS } from \"../../core/staging/lock.js\";\nimport { recoverInterruptedCommit } from \"../../core/staging/recovery/recover.js\";\nimport { docsRepoReady } from \"../policy/gate.js\";\nimport { featureChecklist } from \"../../core/verify/checklist.js\";\nimport { readVerificationState } from \"../../core/verify/file.js\";\nimport { verificationPath } from \"../../core/verify/record.js\";\nimport { report } from \"./report.js\";\nimport { reportFrozen } from \"./frozen.js\";\nimport { record } from \"./record.js\";\nimport { type VerifyOptions } from \"./record.js\";\nimport { diffAnswers, type AnswerFiles } from \"./cross/diff.js\";\n\n\nexport function registerVerify(program: Command): void {\n program\n .command(\"verify\")\n .argument(\"<featureId>\", \"feature id, e.g. FEAT-101\")\n .description(\"Check a shipped feature against its own promises: derive the claims, record the answers\")\n .option(\"--record <file>\", \"record a JSON answer set against the current checklist\")\n .option(\n \"--results <file>\",\n \"answer the scenario.tested claims mechanically from a cucumber JSON test report\",\n )\n .option(\n \"--contract-results <file>\",\n \"answer the api.exposes claims mechanically from an API contract-test report (loam's generic contract-results JSON)\",\n )\n .option(\n \"--diff-answers <files...>\",\n \"cross-examine two independent answer sets against the current checklist (reads only, writes nothing)\",\n )\n .option(\n \"--service <id>\",\n \"record only this service's claims, bound to the current repository commit\",\n )\n .option(\"--json\", \"emit the machine contract instead of the human view\")\n .action(async (featureId: string, opts: VerifyOptions) => {\n const json = opts.json === true;\n\n const loaded = await loadConfig();\n if (loaded.kind !== \"loaded\") {\n reportNoConfig(json, loaded);\n return;\n }\n const config = loaded.config;\n const { docsDir } = config;\n const recording =\n opts.record !== undefined || opts.results !== undefined || opts.contractResults !== undefined;\n\n // --diff-answers is a third READ form (see the WRITING/READING split\n // below): it validates two answer sets against the same checklist and\n // writes nothing, so combining it with any recording flag is a\n // contradiction refused up front — before the binding, before the lock —\n // rather than a mode silently picked between the two.\n let diffFiles: AnswerFiles | null = null;\n if (opts.diffAnswers !== undefined) {\n if (recording) {\n return fail(\n json,\n \"invalid-option\",\n \"--diff-answers is a read-only cross-examination: it validates both files and writes nothing, \" +\n \"so it cannot be combined with --record, --results or --contract-results. \" +\n \"Diff first, then record the answer set you stand behind.\",\n );\n }\n // Commander's variadic form consumes arguments until the next --flag,\n // so a featureId placed AFTER the flag arrives here as a third file —\n // this refusal is what turns that mistake into a sentence instead of a\n // diff of the wrong files.\n const [a, b] = opts.diffAnswers;\n if (opts.diffAnswers.length !== 2 || a === undefined || b === undefined) {\n return fail(\n json,\n \"invalid-option\",\n \"--diff-answers takes exactly two answer files: --diff-answers a.json b.json\",\n );\n }\n diffFiles = { a, b };\n }\n\n // WRITING is bound to the repository; READING is not. An attestation\n // pins claims to this repo's git HEAD and to file:line evidence inside\n // it, so writing (--record, --results or --contract-results) with\n // `--service X` may only run where loam.json says this repo IS X —\n // vouch's and gherkin's refusal, for vouch's reason: from anywhere\n // else, that repository is somebody else's. Reading takes\n // `--service` as a pure lens (which claims are checkout-web's, and what\n // has it said?) and needs no binding at all, because it writes nothing.\n if (recording && opts.service !== undefined) {\n if (config.service === undefined) {\n return fail(\n json,\n \"repository-unavailable\",\n `Cannot attest for '${opts.service}' from here: this is not a service repo — loam.json declares no \\`service\\`. ` +\n `A federated attestation binds the answers to this repository's git HEAD and to evidence inside it, so the repository has to say which service it is. ` +\n `Add \"service\": \"${opts.service}\" to loam.json, or record it from that service's own repo.`,\n );\n }\n if (config.service !== opts.service) {\n return fail(\n json,\n \"service-mismatch\",\n `This repository is configured as service '${config.service}', so it cannot attest claims for '${opts.service}'.`,\n );\n }\n }\n\n // Before the lock, because the lock is a file INSIDE the docs repo: a\n // `docsDir` pointing at nothing used to surface as the lock file's own\n // ENOENT — an `internal` where the read side of this same command names\n // the misconfiguration. \"docs\", not \"services\", in BOTH modes — and that\n // is a pinned contract, not looseness: the legacy all-at-once form\n // records against a features-only docs repo with no `services/` at all,\n // and it has always exited 0 there. The cost is real and accepted: a\n // typo'd `docsDir` pointing at some existing non-docs directory gets a\n // `.loam-lock` created and removed inside it. The gate above only\n // guarantees the directory exists.\n if (!docsRepoReady(json, docsDir, \"docs\")) return;\n\n // A record is a merge over the docs repo, so recording takes the docs\n // lock — the same one archive, unarchive and rebase hold — and takes it\n // HERE, before the feature is even resolved. Behind the lock, an archive\n // can rename this feature's directory into features/archive/ at any\n // moment; a feature resolved before the lock could have its record\n // written into the pre-move path, a file nothing will ever read again.\n // The waiting form rather than archive's refuse-fast one, because two\n // recorders for different services of one feature are both supposed to\n // land: the second waits out the first's sub-second window and merges\n // over what it left. Reading stays lock-free — it writes nothing.\n let releaseLock: (() => Promise<void>) | null = null;\n let recovered: CommitRecovery | null = null;\n if (recording) {\n try {\n releaseLock = await acquireDocsLockWaiting(docsDir, LOCK_WAIT_MS);\n } catch (err) {\n if (err instanceof DocsBusyError) return fail(json, \"docs-busy\", err.message);\n throw err;\n }\n }\n try {\n // A journal under the lock means the last writer never finished. The\n // record's own authoritative read is about to happen; reading it over\n // a half-commit would merge against bytes no run ever produced — the\n // last docs-repo writer that could still silently treat a half-commit\n // as healthy. INSIDE the try: this refusal must release the lock like\n // every other path out, and once did not.\n if (recording) {\n try {\n recovered = await recoverInterruptedCommit(docsDir);\n } catch (err) {\n if (!(err instanceof InterruptedCommitError)) throw err;\n return fail(json, \"commit-interrupted\", err.message);\n }\n }\n // Archived features resolve too: a shipped feature's verification is worth\n // reading back, and it travelled into the archive with everything else.\n const feature = await resolveFeature(docsDir, featureId, \"include\");\n if (!feature) {\n // repo.ts's sentence, not a second copy of it. Resolving with \"include\"\n // means the archived branch inside it cannot fire here — an archived\n // feature resolves — so the text is exactly what it was; what changes is\n // that a reworded miss message can no longer leave this one behind.\n return fail(json, \"unknown-target\", await missingFeatureMessage(docsDir, featureId));\n }\n\n // The cross-examination lens leaves BEFORE the record is read below:\n // it never consumes verification.yaml, and the moment a\n // cross-examination is most needed is exactly when the record on disk\n // is too damaged to read and somebody is about to re-record over it —\n // the record-unreadable refusal must not bar the one command that\n // would inform that re-recording. The archived refusal still applies,\n // with the recording branch's code and reason: there is no current\n // checklist to answer, so the wrong thing is the option, not the\n // answer sets.\n if (diffFiles !== null) {\n if (feature.archived) {\n return fail(\n json,\n \"invalid-option\",\n `${feature.id} is archived — there is no current checklist to answer, so there is nothing to cross-examine; its record is frozen history. \\`loam unarchive ${feature.id}\\` first if the claims really need re-answering.`,\n );\n }\n await diffAnswers(\n { docsDir, featureDir: feature.dir, json },\n await featureChecklist(docsDir, feature.dir, feature.id),\n diffFiles,\n { lens: opts.service },\n );\n return;\n }\n\n // Before anything else: a verification.yaml that exists but will not read\n // is a refusal, not an absence. It holds somebody's answers and — in a\n // fleet — other repositories' attestations; treating it as \"not verified\"\n // let the next --record silently overwrite all of it, and made the read\n // view say \"no record\" about a file sitting right there.\n const existing = await readVerificationState(feature.dir);\n if (existing.state === \"unreadable\") {\n return fail(\n json,\n \"record-unreadable\",\n `${repoPath(docsDir, verificationPath(feature.dir))} exists but cannot be read as a verification record: ${existing.reason}. ` +\n (existing.code === \"verify.record-miscounted\"\n ? \"Record the answers again rather than editing the counts: a summary made to agree by hand still says nothing about what was checked.\"\n : \"It is plain YAML — repair it by hand, or delete it and record again. loam will not overwrite a record it could not read.\"),\n );\n }\n const recorded = existing.state === \"ok\" ? existing.verification : null;\n\n // But an archived feature never gets a re-derived checklist — see the\n // header. Its record is frozen history, and --record / --results refuse\n // alike: the code is `invalid-option` rather than `answers-mismatch`\n // because the answers were never compared to anything — there is no\n // current checklist to answer, so the wrong thing here is the option,\n // not the answer set.\n if (feature.archived) {\n if (recording) {\n return fail(\n json,\n \"invalid-option\",\n `${feature.id} is archived — its verification is history now. \\`loam unarchive ${feature.id}\\` first if the answers really need to change.`,\n );\n }\n reportFrozen({ docsDir, featureDir: feature.dir, json }, feature.id, recorded);\n return;\n }\n\n const checklist = await featureChecklist(docsDir, feature.dir, feature.id);\n\n if (recording) {\n // Standing in a service repo, `--service` is what the repo already\n // says: omitting it must not fall back to the legacy all-at-once form,\n // which claims the whole fleet's checklist on this one repo's word.\n //\n // The repo root, not the cwd — `vouch`'s and `gherkin`'s reason. An\n // attestation names a commit and rests on evidence resolved inside\n // \"this repository\", and `config.root` is the directory loam.json was\n // actually found in. Off `process.cwd()`, running from a subdirectory\n // silently changed all three: evidence resolved against the\n // subdirectory, `--results` had to live in it, and `git rev-parse` ran\n // there — a submodule or nested checkout under it would have named\n // somebody else's commit on this repository's attestation.\n await record(\n { docsDir, featureDir: feature.dir, json, recovered },\n checklist,\n {\n service: opts.service ?? config.service,\n repoDir: config.root ?? process.cwd(),\n previous: existing.state === \"ok\" ? { verification: existing.verification, raw: existing.raw } : null,\n },\n opts,\n );\n return;\n }\n\n report({ docsDir, featureDir: feature.dir, json }, checklist, recorded, {\n lens: opts.service,\n bound: config.service,\n });\n } finally {\n // Released on every path out — a refusal that kept the lock would turn\n // one bad answer set into a wedged docs repo for everyone else.\n if (releaseLock !== null) await releaseLock();\n }\n });\n}\n\n/* ------------------------------------------------------------------ */\n/* Reading */\n/* ------------------------------------------------------------------ */\n"]}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* What a vouch IS: the request, the stamp it writes, and every way it can
|
|
3
|
+
* refuse.
|
|
4
|
+
*
|
|
5
|
+
* The outcome is a union rather than a result plus an error field because the
|
|
6
|
+
* command prints a different shape for each arm, and the stamp only exists on
|
|
7
|
+
* the arm that wrote one. A flat record would have every reader assert what the
|
|
8
|
+
* writer already knew.
|
|
9
|
+
*/
|
|
10
|
+
import {} from "../../core/envelope/json.js";
|
|
11
|
+
//# sourceMappingURL=contract.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"contract.js","sourceRoot":"","sources":["../../../src/commands/vouch/contract.ts"],"names":[],"mappings":"AAAA;;;;;;;;GAQG;AACH,OAAO,EAAkB,MAAM,6BAA6B,CAAC","sourcesContent":["/**\n * What a vouch IS: the request, the stamp it writes, and every way it can\n * refuse.\n *\n * The outcome is a union rather than a result plus an error field because the\n * command prints a different shape for each arm, and the stamp only exists on\n * the arm that wrote one. A flat record would have every reader assert what the\n * writer already knew.\n */\nimport { type ErrorCode } from \"../../core/envelope/json.js\";\nimport type { DocsDir } from \"../../core/kernel/ids/dirs.js\";\nimport type { PathableService } from \"../../core/kernel/ids/service.js\";\nimport type { CommitRecovery } from \"../../core/staging/interrupted.js\";\nimport type { VouchScope } from \"../../core/provenance/sample/scope.js\";\nimport type { SkippedSource } from \"../../core/provenance/walk.js\";\n// The plan's shapes live with the pass that derives them (`sample/plan.ts`),\n// the way pack's `SectionDelta` lives with `pack/sections.ts`. The import\n// direction is parent-from-child, which is the only one the package graph\n// allows here: a sub-package that reached back up into this file would close\n// the cycle `vet/verify.ts` was moved down to avoid.\nimport type { SamplePlan } from \"./sample/plan.js\";\n\nexport interface VouchRequest {\n docsDir: DocsDir;\n service: PathableService;\n /** The service's own repo — what `sources` resolve against. */\n repoDir: string;\n /** The date to stamp. Injected rather than read off the clock, so it can be pinned. */\n today: string;\n /**\n * Who is vouching, as `vouched_by` is stamped — git's identity for `repoDir`.\n * Injected for the same reason `today` is: this function must be reproducible\n * from its arguments, and the resolution (and its refusal) belongs to the\n * command that can talk to a person about it.\n */\n vouchedBy: string;\n /**\n * The sample a person was shown before they answered, under `--sample <n>`.\n * Absent for an ordinary vouch — and its absence is what CLEARS a prior\n * sampled stamp, since a full run deletes `vouch_scope` from every file it\n * writes.\n */\n sample?: SamplePlan;\n}\n\n/** One spec-axis file's share of a successful vouch. */\nexport interface StampedSpec {\n /** Absolute path of the file that was stamped. */\n path: string;\n /** The axis's filename — \"spec.md\" or \"arch.spec.md\". */\n file: string;\n /** Digest of the sources, as stamped into `sources_digest`. */\n digest: string;\n /** Digest of the document's own body, as stamped into `content_digest`. */\n contentDigest: string;\n /** The `sources` entries, as written in the frontmatter. */\n sources: string[];\n /** How many files those entries expanded to. */\n files: number;\n /**\n * Paths under those entries the digest would not hash. A vouch over a spec\n * with a skipped path is still a vouch — the person read what they read — but\n * they are told, because the stamp cannot go stale over bytes it never saw.\n */\n skipped: SkippedSource[];\n /**\n * What was written into `vouch_scope`, or null when the file was read in\n * full and the field was deleted. The report describes what the stamp\n * ACTUALLY says rather than what the plan asked for: a file whose section\n * count was at or under `--sample <n>` is stamped as a full vouch, and the\n * screen the person is looking at has to say so.\n */\n vouchScope: VouchScope | null;\n}\n\nexport type VouchOutcome =\n | {\n ok: true;\n status: \"verified\";\n lastVerified: string;\n /** The identity stamped into every file this vouch touched. */\n vouchedBy: string;\n /**\n * Non-null when this run first rolled a predecessor's interrupted\n * commit forward — the caller reports it, because docs changing beyond\n * the stamp would otherwise read as this vouch's doing.\n */\n recovered: CommitRecovery | null;\n /**\n * Every spec-axis file stamped. Named, not ordered: spec.md is required\n * for a vouch to happen at all, and arch.spec.md is the only other file\n * one can touch, so the type is what says which is which. An array said it\n * in a comment instead, and every reader had to assert non-emptiness to\n * reach the file that comment promised was there.\n */\n stamped: { spec: StampedSpec; archSpec: StampedSpec | null };\n }\n | {\n ok: false;\n // The last three are the commit phase failing: `vouch-raced` says the\n // document moved under the run and nothing was written at all,\n // `merge-failed` says the rollback held (nothing was stamped, re-running\n // can work), `rollback-incomplete` says it did not and the message lists\n // the files.\n code: Extract<\n ErrorCode,\n | \"unknown-target\"\n | \"sources-absent\"\n | \"sources-path-missing\"\n // The spec is there but is not UTF-8 text: a diagnosable state, not the\n // unexpected throw `internal` is for.\n | \"repository-unavailable\"\n | \"vouch-raced\"\n | \"merge-failed\"\n | \"rollback-incomplete\"\n // The commit window now takes the docs lock and reads the journal:\n // another writer can hold the one, a predecessor's crash can leave\n // the other. Both are the codes every other journaled writer answers.\n | \"docs-busy\"\n | \"commit-interrupted\"\n >;\n message: string;\n };\n"]}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"contract.js","sourceRoot":"","sources":["../../../../src/commands/vouch/pack/contract.ts"],"names":[],"mappings":"","sourcesContent":["/**\n * What a reading pack IS: the request, the per-axis verdicts, and the one way\n * it can refuse. The same seam as `../contract.ts` one level up — the shapes\n * live apart from the derivation so the printer can name what it renders\n * without importing how it was computed.\n *\n * Every verdict is a discriminated union rather than optional-field soup, and\n * the arms are exactly the honest answers: a body is unchanged, or diffable\n * from a NAMED ancestor, or a full read with the reason — never a guessed\n * diff; sources are unchanged, a named delta, a counted-but-nameless move, a\n * state the re-vouch itself would refuse over, or simply unvouched.\n */\nimport type { DocsDir } from \"../../../core/kernel/ids/dirs.js\";\nimport type { PathableService } from \"../../../core/kernel/ids/service.js\";\nimport type { SkippedSource } from \"../../../core/provenance/walk.js\";\nimport type { LandscapeEdge } from \"../../../core/vocabulary/maturity.js\";\nimport type { PackSample, PackVouchScope, SectionDelta } from \"./sections.js\";\n\nexport interface PackRequest {\n docsDir: DocsDir;\n service: PathableService;\n /** The service's own repo — what the source delta resolves `sources` against. */\n repoDir: string;\n /**\n * `--sample <n>`, when the pack is being read as the reading list for a\n * SAMPLED vouch. The pack then prescribes exactly the sections that\n * `loam vouch --sample <n>` will stamp for, through the same seeded\n * derivation — a pack and a stamp that disagreed about which sections were\n * read would make the record worse than no record.\n */\n sample?: number;\n}\n\n/** The body half of one axis's pack: what to read of the document itself. */\nexport type PackBody =\n | { kind: \"unchanged\" }\n | { kind: \"diff\"; ancestorCommit: string; diff: string; sections: SectionDelta }\n | { kind: \"full-read\"; reason: string };\n\n/** The source half: what moved under the code the document was written from. */\nexport type PackSources =\n | { kind: \"unchanged\" }\n | { kind: \"delta\"; added: string[]; changed: string[]; removed: string[] }\n /**\n * The digest moved but the stamp cannot name the paths — the count-only\n * >100-file fallback (`countThen` known) or an index that contradicts its\n * own digest (`countThen` null when even the count is unreadable).\n */\n | { kind: \"uncounted\"; countThen: number | null; countNow: number }\n /** The re-vouch itself would refuse over these sources; the reason IS the worklist. */\n | { kind: \"unavailable\"; reason: string }\n /** Sources resolve but carry no stamp — nothing to diff against yet. */\n | { kind: \"unvouched\" };\n\nexport interface PackAxis {\n /** Docs-relative, `/`-separated — the contract's path spelling. */\n path: string;\n /** The axis's filename — \"spec.md\" or \"arch.spec.md\". */\n file: string;\n /** The stamp's own fields, read back for the \"already covered\" listing. */\n vouchedBy?: string;\n lastVerified?: string;\n body: PackBody;\n sources: PackSources;\n /**\n * Paths under the listed sources the digest recompute would not hash —\n * walk.ts's skip record, carried on the axis because it is orthogonal to\n * the sources verdict (it can accompany unchanged, delta and unvouched\n * alike). Never dropped: these are the parts of the tree the re-vouch's\n * promise will NOT cover, and the pack is now the screen a person reads\n * BEFORE staking that promise. Empty when the vetting itself refused —\n * nobody walked, so nobody can name what a walk would skip.\n */\n skipped: SkippedSource[];\n /**\n * Every H2 heading of the current body — set whenever the whole file is the\n * read (`body.kind === \"full-read\"`: a first vouch, a never-vouched axis, a\n * body whose vouched ancestor cannot be reached), because a full read is\n * exactly where the listing IS the reading plan. Per axis, not per mode:\n * an arch.spec.md created after the last vouch gets its plan inside a\n * re-vouch pack.\n */\n headings?: string[];\n /**\n * The LAST vouch's scope, when that vouch was sampled — present-but-\n * undecodable included, because an unreadable scope on a verified document\n * must never read as a full vouch. Its presence withdraws the \"already\n * covered\" licence for this axis: `unchanged` says the text did not move,\n * and after a sampled vouch that is not the same fact as \"somebody read it\".\n */\n vouchScope?: PackVouchScope;\n /**\n * What a `--sample <n>` vouch would cover, under `--pack --sample <n>`. The\n * forward direction, where `vouchScope` above is the backward one: one says\n * what the next stamp will claim, the other what the last one did.\n */\n sample?: PackSample;\n}\n\n/** What the fleet map says about the service — the first read of a first vouch. */\nexport type PackLandscape =\n | { kind: \"edges\"; inbound: LandscapeEdge[]; outbound: LandscapeEdge[] }\n | { kind: \"silent\"; reason: string };\n\nexport interface PackReport {\n service: string;\n /** \"first-vouch\" when spec.md carries neither digest — nothing exists to diff against. */\n packMode: \"re-vouch\" | \"first-vouch\";\n /**\n * A predecessor's interrupted docs-repo commit journal exists, so what is\n * on disk may predate the write it was killed inside — including the very\n * stamps this pack read. The pack cannot roll the journal forward (recovery\n * is itself a write, and this command may not write), so it says so instead\n * of silently describing a state the next journaled writer will replace.\n */\n pendingCommit: boolean;\n spec: PackAxis;\n archSpec: PackAxis | null;\n /** First-vouch only (null otherwise): landscape claims come first in the reading order. */\n landscape: PackLandscape | null;\n}\n\nexport type PackOutcome =\n | { ok: true; report: PackReport }\n | { ok: false; code: \"unknown-target\"; message: string };\n"]}
|