@ybotok/loam 0.1.0-beta.3 → 0.2.0-alpha.2
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 +2635 -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 +599 -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 +323 -0
- package/dist/commands/archive/plan/gate.js.map +1 -0
- package/dist/commands/archive/plan/landscape.js +180 -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 +252 -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 +204 -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 +270 -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 +251 -0
- package/dist/commands/validate/service/service.js.map +1 -0
- package/dist/commands/validate/service/specs.js +251 -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 +337 -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 +48 -0
- package/dist/core/brief/unchecked.js.map +1 -0
- package/dist/core/c4/arch.js +174 -0
- package/dist/core/c4/arch.js.map +1 -0
- package/dist/core/c4/likec4.js +90 -0
- package/dist/core/c4/likec4.js.map +1 -0
- package/dist/core/c4/parsed/deployment.js +158 -0
- package/dist/core/c4/parsed/deployment.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 +187 -0
- package/dist/core/c4/project/load.js.map +1 -0
- package/dist/core/c4/project/staged.js +220 -0
- package/dist/core/c4/project/staged.js.map +1 -0
- package/dist/core/c4/resolve/service.js +159 -0
- package/dist/core/c4/resolve/service.js.map +1 -0
- package/dist/core/c4/resolve/steps.js +144 -0
- package/dist/core/c4/resolve/steps.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 +139 -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 +174 -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 +287 -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/deployment/delta.js +188 -0
- package/dist/core/deployment/delta.js.map +1 -0
- package/dist/core/deployment/objects.js +145 -0
- package/dist/core/deployment/objects.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 +137 -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 +210 -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 +277 -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 +196 -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 +197 -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 +72 -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 +58 -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,157 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Re-checking evidence pins against a working tree: pure data out, one grade
|
|
3
|
+
* per pin, and no verdict anywhere in the result on purpose — the lint demotes
|
|
4
|
+
* reviewer confidence, it never promotes or demotes a claim. Prints nothing;
|
|
5
|
+
* the validate command turns the grades into findings.
|
|
6
|
+
*
|
|
7
|
+
* The claim shape is structural rather than an import of `../record.js`'s
|
|
8
|
+
* `RecordedClaim`, and not only for the smallest-parameter rule: this package
|
|
9
|
+
* is imported BY `core/verify` (`../file.ts` asks `./pin.ts` for the read
|
|
10
|
+
* guard, `../answers.ts` for the shape), so importing the parent back would
|
|
11
|
+
* put a cycle in the package graph that `import/no-cycle` cannot see.
|
|
12
|
+
*/
|
|
13
|
+
import { readFile, stat } from "node:fs/promises";
|
|
14
|
+
import { inOrder } from "../../kernel/concurrency.js";
|
|
15
|
+
import { resolvePortableFileInside, UnsafePathError } from "../../kernel/path-safety.js";
|
|
16
|
+
import { pinnedDigest, pinnedText, sourceLines } from "./pin.js";
|
|
17
|
+
/**
|
|
18
|
+
* Grade one record's pins for one service against that service's working tree.
|
|
19
|
+
*
|
|
20
|
+
* Only agent-confirmed claims of `service` are graded: a runner's or contract
|
|
21
|
+
* runner's evidence names a report entry, not a file, so there is nothing to
|
|
22
|
+
* re-check, and an unconfirmed claim pinned nothing. Absent `answered_by`
|
|
23
|
+
* counts as agent — the `attestedClaims` rule.
|
|
24
|
+
*
|
|
25
|
+
* Containment is per citation: one unreadable cited file degrades exactly the
|
|
26
|
+
* pins that cite it (graded `unresolved`), never the sibling pins and never
|
|
27
|
+
* the whole record. And "could not look" is never "looks fine": every failure
|
|
28
|
+
* to read is a drift entry, not a skip — fail closed at the validator.
|
|
29
|
+
*/
|
|
30
|
+
export async function lintEvidencePins(claims, service, repoDir) {
|
|
31
|
+
// The closed side of the answerer test, on purpose: `answered_by` is an open
|
|
32
|
+
// string here (a record is hand-editable YAML, and a newer loam may add
|
|
33
|
+
// answerers), so excluding the known mechanical values would sweep every
|
|
34
|
+
// FUTURE mechanical answerer's report-entry citations into the `unpinned`
|
|
35
|
+
// tally with false "recorded before pins existed" advice. Only what is
|
|
36
|
+
// positively an agent's word is graded; absent counts as agent — the
|
|
37
|
+
// `attestedClaims` rule for records that predate `--results`.
|
|
38
|
+
const mine = claims.filter((c) => c.subject === service &&
|
|
39
|
+
c.verdict === "confirmed" &&
|
|
40
|
+
(c.answered_by === undefined || c.answered_by === "agent"));
|
|
41
|
+
// Join pins to evidence entries by the `path:line` key, never by position:
|
|
42
|
+
// a record is hand-editable YAML, and a positional join would let a deleted
|
|
43
|
+
// evidence line silently shift every later pin onto the wrong citation.
|
|
44
|
+
const joined = [];
|
|
45
|
+
let unpinned = 0;
|
|
46
|
+
for (const claim of mine) {
|
|
47
|
+
const pins = new Map((claim.evidence_pins ?? []).map((pin) => [`${pin.path}:${pin.line}`, pin]));
|
|
48
|
+
// A pin no evidence entry names pins nothing this claim cites (a hand
|
|
49
|
+
// edit); grading it would report drift about a citation the claim does not
|
|
50
|
+
// make, so only the joined pairs are graded. The reverse direction — a
|
|
51
|
+
// citation without a pin — is the `unpinned` tally.
|
|
52
|
+
for (const evidence of claim.evidence) {
|
|
53
|
+
const pin = pins.get(evidence);
|
|
54
|
+
if (pin === undefined)
|
|
55
|
+
unpinned += 1;
|
|
56
|
+
else
|
|
57
|
+
joined.push({ claim: claim.id, evidence, pin });
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
// Each distinct cited file is read and hashed once per call — a local map,
|
|
61
|
+
// per invocation, because module-level state leaks across tests and hosts.
|
|
62
|
+
// The reads are independent, so they go through the bounded pool; grading
|
|
63
|
+
// stays sequential below so `drifts` keeps record order.
|
|
64
|
+
const paths = [...new Set(joined.map((j) => j.pin.path))];
|
|
65
|
+
const states = new Map(await inOrder(paths, async (path) => [path, await readCited(repoDir, path)]));
|
|
66
|
+
let checked = 0;
|
|
67
|
+
const drifts = [];
|
|
68
|
+
for (const { claim, evidence, pin } of joined) {
|
|
69
|
+
const file = states.get(pin.path);
|
|
70
|
+
if (file === undefined || file.state === "unreadable") {
|
|
71
|
+
drifts.push({ claim, evidence, grade: "unresolved", what: file?.what ?? "the cited file was not read" });
|
|
72
|
+
continue;
|
|
73
|
+
}
|
|
74
|
+
const cited = file.lines[pin.line - 1];
|
|
75
|
+
if (cited === undefined) {
|
|
76
|
+
drifts.push({
|
|
77
|
+
claim,
|
|
78
|
+
evidence,
|
|
79
|
+
grade: "unresolved",
|
|
80
|
+
// `continue`, like the unreadable-file arm: an unresolvable CITATION
|
|
81
|
+
// is one finding, and a token scan on top would report the same
|
|
82
|
+
// damaged citation twice under two codes.
|
|
83
|
+
what: `the cited line is past the end — the file now has ${file.lines.length} line(s)`,
|
|
84
|
+
});
|
|
85
|
+
continue;
|
|
86
|
+
}
|
|
87
|
+
if (file.digest === pin.file_sha256) {
|
|
88
|
+
checked += 1;
|
|
89
|
+
}
|
|
90
|
+
else if (pinnedText(cited) === pin.text) {
|
|
91
|
+
// The mildest drift: the file changed around the cited line, which
|
|
92
|
+
// still reads as pinned — "reads as", not "is": both sides of the
|
|
93
|
+
// comparison are capped at PIN_TEXT_CAP, so a change past the cap on a
|
|
94
|
+
// pathological line is invisible to this grade (and visible to the
|
|
95
|
+
// digest, which is why this is a drift and not a clean pin).
|
|
96
|
+
drifts.push({
|
|
97
|
+
claim,
|
|
98
|
+
evidence,
|
|
99
|
+
grade: "moved",
|
|
100
|
+
what: "the file changed around the cited line, which still reads as pinned",
|
|
101
|
+
});
|
|
102
|
+
}
|
|
103
|
+
else {
|
|
104
|
+
drifts.push({
|
|
105
|
+
claim,
|
|
106
|
+
evidence,
|
|
107
|
+
grade: "line-changed",
|
|
108
|
+
what: `the cited line now reads '${pinnedText(cited)}' where the record pinned '${pin.text}'`,
|
|
109
|
+
});
|
|
110
|
+
}
|
|
111
|
+
// Independent of the grades above, clean pins included — a digest match
|
|
112
|
+
// must not read as "the token is still there", because content identical
|
|
113
|
+
// to record time says nothing once the file drifts back. "No longer" is
|
|
114
|
+
// the honest word here: the write side stamps `token` only when the
|
|
115
|
+
// attested blob contained it (EvidencePin.token), so this grade always
|
|
116
|
+
// convicts a disappearance, never re-litigates an absence the record-time
|
|
117
|
+
// notice already reported.
|
|
118
|
+
if (pin.token !== undefined && !file.text.includes(pin.token)) {
|
|
119
|
+
drifts.push({
|
|
120
|
+
claim,
|
|
121
|
+
evidence,
|
|
122
|
+
grade: "token-missing",
|
|
123
|
+
what: `the cited file no longer contains '${pin.token}' — the literal string the claim asserts`,
|
|
124
|
+
});
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
return { checked, drifts, unpinned };
|
|
128
|
+
}
|
|
129
|
+
/** One cited file against the working tree, every failure named rather than thrown. */
|
|
130
|
+
async function readCited(repoDir, path) {
|
|
131
|
+
let absolute;
|
|
132
|
+
try {
|
|
133
|
+
absolute = resolvePortableFileInside(repoDir, path, "pinned evidence");
|
|
134
|
+
}
|
|
135
|
+
catch (err) {
|
|
136
|
+
if (!(err instanceof UnsafePathError))
|
|
137
|
+
throw err;
|
|
138
|
+
return { state: "unreadable", what: `the pinned path is unsafe: ${err.message}` };
|
|
139
|
+
}
|
|
140
|
+
try {
|
|
141
|
+
const info = await stat(absolute);
|
|
142
|
+
if (!info.isFile())
|
|
143
|
+
return { state: "unreadable", what: "the cited path is not a regular file" };
|
|
144
|
+
const text = await readFile(absolute, "utf8");
|
|
145
|
+
return { state: "ok", digest: pinnedDigest(text), lines: sourceLines(text), text };
|
|
146
|
+
}
|
|
147
|
+
catch (err) {
|
|
148
|
+
// ENOENT is the common drift — the cited file is gone from the tree; any
|
|
149
|
+
// other errno (EACCES, EISDIR racing the stat) is named as itself, because
|
|
150
|
+
// "could not read" and "does not exist" call for different repairs.
|
|
151
|
+
if (err.code === "ENOENT") {
|
|
152
|
+
return { state: "unreadable", what: "the cited file does not exist in this working tree" };
|
|
153
|
+
}
|
|
154
|
+
return { state: "unreadable", what: err instanceof Error ? err.message : String(err) };
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
//# sourceMappingURL=lint.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"lint.js","sourceRoot":"","sources":["../../../../src/core/verify/pins/lint.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * Re-checking evidence pins against a working tree: pure data out, one grade\n * per pin, and no verdict anywhere in the result on purpose — the lint demotes\n * reviewer confidence, it never promotes or demotes a claim. Prints nothing;\n * the validate command turns the grades into findings.\n *\n * The claim shape is structural rather than an import of `../record.js`'s\n * `RecordedClaim`, and not only for the smallest-parameter rule: this package\n * is imported BY `core/verify` (`../file.ts` asks `./pin.ts` for the read\n * guard, `../answers.ts` for the shape), so importing the parent back would\n * put a cycle in the package graph that `import/no-cycle` cannot see.\n */\nimport { readFile, stat } from \"node:fs/promises\";\nimport { inOrder } from \"../../kernel/concurrency.js\";\nimport { resolvePortableFileInside, UnsafePathError } from \"../../kernel/path-safety.js\";\nimport { pinnedDigest, pinnedText, sourceLines, type EvidencePin } from \"./pin.js\";\n\n/** What the lint reads off a recorded claim — structurally, see the header. */\nexport interface PinnedClaim {\n id: string;\n /** Present in federated (schema 2) records — the service whose code answers it. */\n subject?: string;\n verdict: string;\n /** Absent counts as agent — the `attestedClaims` rule for records that predate `--results`. */\n answered_by?: string;\n evidence: string[];\n evidence_pins?: EvidencePin[];\n}\n\n/** One pin's disagreement with the working tree. `grade` is what a finding code is made from. */\nexport interface PinDrift {\n claim: string;\n /** The `path:line` citation the pin belongs to. */\n evidence: string;\n grade: \"unresolved\" | \"moved\" | \"line-changed\" | \"token-missing\";\n /** What changed, in one clause — rendered into a finding's detail line. */\n what: string;\n}\n\nexport interface PinLint {\n /** Pins that resolved clean: file digest unchanged against the working tree. */\n checked: number;\n /** In record order — claim by claim, citation by citation — so two runs diff. */\n drifts: PinDrift[];\n /** Agent-confirmed citations carrying no pin, plus claims with no pins at all. */\n unpinned: number;\n}\n\n/** What one cited file turned out to be, read and hashed once per lint call. */\ntype FileState =\n | { state: \"unreadable\"; what: string }\n | { state: \"ok\"; digest: string; lines: string[]; text: string };\n\n/**\n * Grade one record's pins for one service against that service's working tree.\n *\n * Only agent-confirmed claims of `service` are graded: a runner's or contract\n * runner's evidence names a report entry, not a file, so there is nothing to\n * re-check, and an unconfirmed claim pinned nothing. Absent `answered_by`\n * counts as agent — the `attestedClaims` rule.\n *\n * Containment is per citation: one unreadable cited file degrades exactly the\n * pins that cite it (graded `unresolved`), never the sibling pins and never\n * the whole record. And \"could not look\" is never \"looks fine\": every failure\n * to read is a drift entry, not a skip — fail closed at the validator.\n */\nexport async function lintEvidencePins(\n claims: readonly PinnedClaim[],\n service: string,\n repoDir: string,\n): Promise<PinLint> {\n // The closed side of the answerer test, on purpose: `answered_by` is an open\n // string here (a record is hand-editable YAML, and a newer loam may add\n // answerers), so excluding the known mechanical values would sweep every\n // FUTURE mechanical answerer's report-entry citations into the `unpinned`\n // tally with false \"recorded before pins existed\" advice. Only what is\n // positively an agent's word is graded; absent counts as agent — the\n // `attestedClaims` rule for records that predate `--results`.\n const mine = claims.filter(\n (c) =>\n c.subject === service &&\n c.verdict === \"confirmed\" &&\n (c.answered_by === undefined || c.answered_by === \"agent\"),\n );\n\n // Join pins to evidence entries by the `path:line` key, never by position:\n // a record is hand-editable YAML, and a positional join would let a deleted\n // evidence line silently shift every later pin onto the wrong citation.\n const joined: Array<{ claim: string; evidence: string; pin: EvidencePin }> = [];\n let unpinned = 0;\n for (const claim of mine) {\n const pins = new Map((claim.evidence_pins ?? []).map((pin) => [`${pin.path}:${pin.line}`, pin]));\n // A pin no evidence entry names pins nothing this claim cites (a hand\n // edit); grading it would report drift about a citation the claim does not\n // make, so only the joined pairs are graded. The reverse direction — a\n // citation without a pin — is the `unpinned` tally.\n for (const evidence of claim.evidence) {\n const pin = pins.get(evidence);\n if (pin === undefined) unpinned += 1;\n else joined.push({ claim: claim.id, evidence, pin });\n }\n }\n\n // Each distinct cited file is read and hashed once per call — a local map,\n // per invocation, because module-level state leaks across tests and hosts.\n // The reads are independent, so they go through the bounded pool; grading\n // stays sequential below so `drifts` keeps record order.\n const paths = [...new Set(joined.map((j) => j.pin.path))];\n const states = new Map(\n await inOrder(paths, async (path): Promise<[string, FileState]> => [path, await readCited(repoDir, path)]),\n );\n\n let checked = 0;\n const drifts: PinDrift[] = [];\n for (const { claim, evidence, pin } of joined) {\n const file = states.get(pin.path);\n if (file === undefined || file.state === \"unreadable\") {\n drifts.push({ claim, evidence, grade: \"unresolved\", what: file?.what ?? \"the cited file was not read\" });\n continue;\n }\n const cited = file.lines[pin.line - 1];\n if (cited === undefined) {\n drifts.push({\n claim,\n evidence,\n grade: \"unresolved\",\n // `continue`, like the unreadable-file arm: an unresolvable CITATION\n // is one finding, and a token scan on top would report the same\n // damaged citation twice under two codes.\n what: `the cited line is past the end — the file now has ${file.lines.length} line(s)`,\n });\n continue;\n }\n if (file.digest === pin.file_sha256) {\n checked += 1;\n } else if (pinnedText(cited) === pin.text) {\n // The mildest drift: the file changed around the cited line, which\n // still reads as pinned — \"reads as\", not \"is\": both sides of the\n // comparison are capped at PIN_TEXT_CAP, so a change past the cap on a\n // pathological line is invisible to this grade (and visible to the\n // digest, which is why this is a drift and not a clean pin).\n drifts.push({\n claim,\n evidence,\n grade: \"moved\",\n what: \"the file changed around the cited line, which still reads as pinned\",\n });\n } else {\n drifts.push({\n claim,\n evidence,\n grade: \"line-changed\",\n what: `the cited line now reads '${pinnedText(cited)}' where the record pinned '${pin.text}'`,\n });\n }\n // Independent of the grades above, clean pins included — a digest match\n // must not read as \"the token is still there\", because content identical\n // to record time says nothing once the file drifts back. \"No longer\" is\n // the honest word here: the write side stamps `token` only when the\n // attested blob contained it (EvidencePin.token), so this grade always\n // convicts a disappearance, never re-litigates an absence the record-time\n // notice already reported.\n if (pin.token !== undefined && !file.text.includes(pin.token)) {\n drifts.push({\n claim,\n evidence,\n grade: \"token-missing\",\n what: `the cited file no longer contains '${pin.token}' — the literal string the claim asserts`,\n });\n }\n }\n return { checked, drifts, unpinned };\n}\n\n/** One cited file against the working tree, every failure named rather than thrown. */\nasync function readCited(repoDir: string, path: string): Promise<FileState> {\n let absolute: string;\n try {\n absolute = resolvePortableFileInside(repoDir, path, \"pinned evidence\");\n } catch (err) {\n if (!(err instanceof UnsafePathError)) throw err;\n return { state: \"unreadable\", what: `the pinned path is unsafe: ${err.message}` };\n }\n try {\n const info = await stat(absolute);\n if (!info.isFile()) return { state: \"unreadable\", what: \"the cited path is not a regular file\" };\n const text = await readFile(absolute, \"utf8\");\n return { state: \"ok\", digest: pinnedDigest(text), lines: sourceLines(text), text };\n } catch (err) {\n // ENOENT is the common drift — the cited file is gone from the tree; any\n // other errno (EACCES, EISDIR racing the stat) is named as itself, because\n // \"could not read\" and \"does not exist\" call for different repairs.\n if ((err as NodeJS.ErrnoException).code === \"ENOENT\") {\n return { state: \"unreadable\", what: \"the cited file does not exist in this working tree\" };\n }\n return { state: \"unreadable\", what: err instanceof Error ? err.message : String(err) };\n }\n}\n"]}
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The evidence pin's identity: what `verify --service --record` stamps beside a
|
|
3
|
+
* confirmed agent answer so a later `loam validate`, standing in the same
|
|
4
|
+
* service repository, can re-check the citation against the working tree.
|
|
5
|
+
*
|
|
6
|
+
* Be precise about what a pin proves, because the record travels without loam:
|
|
7
|
+
* a pin identifies WHAT was cited at the attested commit — the cited file's
|
|
8
|
+
* normalized sha256, the cited line's text, the literal token the claim
|
|
9
|
+
* asserts. It does not prove the claim true; an agent that cited the wrong
|
|
10
|
+
* line gets a faithful pin of the wrong line. That is why the read side
|
|
11
|
+
* (`./lint.ts`) only ever demotes reviewer confidence: no pin state moves a
|
|
12
|
+
* verdict in either direction.
|
|
13
|
+
*/
|
|
14
|
+
import { createHash } from "node:crypto";
|
|
15
|
+
import { isRecord } from "../../kernel/records.js";
|
|
16
|
+
/**
|
|
17
|
+
* How much of the cited line a pin carries. Enough to recognize a line of
|
|
18
|
+
* source by eye and by equality; a cap because evidence may cite a line of a
|
|
19
|
+
* generated client or a minified bundle, and the record is a YAML file people
|
|
20
|
+
* read and diff — one pathological line must not turn it into one.
|
|
21
|
+
*/
|
|
22
|
+
export const PIN_TEXT_CAP = 200;
|
|
23
|
+
/** The pin's line recipe: trimmed (indentation is formatting), capped at {@link PIN_TEXT_CAP}. */
|
|
24
|
+
export function pinnedText(line) {
|
|
25
|
+
const trimmed = line.trim();
|
|
26
|
+
return trimmed.length > PIN_TEXT_CAP ? trimmed.slice(0, PIN_TEXT_CAP) : trimmed;
|
|
27
|
+
}
|
|
28
|
+
/**
|
|
29
|
+
* The one spelling of where lines fall. Both sides of the pin — the write
|
|
30
|
+
* side's bounds checks and the read side's re-grade — must number lines
|
|
31
|
+
* identically, or a pin recorded at line 12 gets graded against a different
|
|
32
|
+
* line 12; four call sites each spelling the regex themselves is how that
|
|
33
|
+
* divergence would arrive, so the recipe has exactly one home.
|
|
34
|
+
*/
|
|
35
|
+
export function sourceLines(source) {
|
|
36
|
+
return source.split(/\r\n|\n|\r/);
|
|
37
|
+
}
|
|
38
|
+
/**
|
|
39
|
+
* The cited line of a source text, through the pin recipe — or undefined when
|
|
40
|
+
* the line is past the end, which the caller grades (a refusal on the write
|
|
41
|
+
* side, `unresolved` on the read side).
|
|
42
|
+
*/
|
|
43
|
+
export function citedLine(source, line) {
|
|
44
|
+
const raw = sourceLines(source)[line - 1];
|
|
45
|
+
return raw === undefined ? undefined : pinnedText(raw);
|
|
46
|
+
}
|
|
47
|
+
/**
|
|
48
|
+
* The pin's file digest: sha256 hex over the text with CRLF/CR normalized to
|
|
49
|
+
* LF.
|
|
50
|
+
*
|
|
51
|
+
* Normalized because git's eol config makes blob bytes and working-tree bytes
|
|
52
|
+
* legally differ: on a Windows checkout with `core.autocrlf=true`, the blob
|
|
53
|
+
* the attested commit holds is LF while every file on disk is CRLF, so an
|
|
54
|
+
* unnormalized digest would grade every pin `evidence.moved` forever on the
|
|
55
|
+
* checkout config alone. The pin must convict content change, never checkout
|
|
56
|
+
* config. The recipe is over TEXT by construction — the write side digests
|
|
57
|
+
* `git show` output already decoded as utf8 — and the symmetric cost is
|
|
58
|
+
* accepted and documented in SCHEMA.md: a pure line-ending change is invisible
|
|
59
|
+
* to the lint.
|
|
60
|
+
*/
|
|
61
|
+
export function pinnedDigest(source) {
|
|
62
|
+
return createHash("sha256").update(source.replace(/\r\n|\r/g, "\n")).digest("hex");
|
|
63
|
+
}
|
|
64
|
+
/**
|
|
65
|
+
* The read-shape guard `asVerification` (../file.ts) asks per claim, on the
|
|
66
|
+
* `isConsumedContractReport` model to the letter: absent is fine — every
|
|
67
|
+
* record written before pins existed, and every runner-answered claim, has
|
|
68
|
+
* none — but present must be whole for the four fields the lint dereferences.
|
|
69
|
+
* A broken pins block makes the WHOLE record unreadable and therefore never
|
|
70
|
+
* overwritten, exactly the contract-report rule: it is somebody's record, and
|
|
71
|
+
* a lint that silently skipped a malformed pin would read a hand-damaged
|
|
72
|
+
* record as drift-free. `token` stays optional-and-any-string for `format`'s
|
|
73
|
+
* reason — a field a newer loam learned to stamp must not brick the record
|
|
74
|
+
* here.
|
|
75
|
+
*/
|
|
76
|
+
export function isEvidencePinList(v) {
|
|
77
|
+
if (v === undefined)
|
|
78
|
+
return true;
|
|
79
|
+
if (!Array.isArray(v))
|
|
80
|
+
return false;
|
|
81
|
+
for (const pin of v) {
|
|
82
|
+
if (!isRecord(pin))
|
|
83
|
+
return false;
|
|
84
|
+
if (typeof pin["path"] !== "string")
|
|
85
|
+
return false;
|
|
86
|
+
if (typeof pin["line"] !== "number" || !Number.isInteger(pin["line"]) || pin["line"] < 1)
|
|
87
|
+
return false;
|
|
88
|
+
if (typeof pin["file_sha256"] !== "string" || typeof pin["text"] !== "string")
|
|
89
|
+
return false;
|
|
90
|
+
if (pin["token"] !== undefined && typeof pin["token"] !== "string")
|
|
91
|
+
return false;
|
|
92
|
+
}
|
|
93
|
+
return true;
|
|
94
|
+
}
|
|
95
|
+
//# sourceMappingURL=pin.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"pin.js","sourceRoot":"","sources":["../../../../src/core/verify/pins/pin.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;GAYG;AACH,OAAO,EAAE,UAAU,EAAE,MAAM,aAAa,CAAC;AACzC,OAAO,EAAE,QAAQ,EAAE,MAAM,yBAAyB,CAAC;AA4BnD;;;;;GAKG;AACH,MAAM,CAAC,MAAM,YAAY,GAAG,GAAG,CAAC;AAEhC,kGAAkG;AAClG,MAAM,UAAU,UAAU,CAAC,IAAY;IACrC,MAAM,OAAO,GAAG,IAAI,CAAC,IAAI,EAAE,CAAC;IAC5B,OAAO,OAAO,CAAC,MAAM,GAAG,YAAY,CAAC,CAAC,CAAC,OAAO,CAAC,KAAK,CAAC,CAAC,EAAE,YAAY,CAAC,CAAC,CAAC,CAAC,OAAO,CAAC;AAClF,CAAC;AAED;;;;;;GAMG;AACH,MAAM,UAAU,WAAW,CAAC,MAAc;IACxC,OAAO,MAAM,CAAC,KAAK,CAAC,YAAY,CAAC,CAAC;AACpC,CAAC;AAED;;;;GAIG;AACH,MAAM,UAAU,SAAS,CAAC,MAAc,EAAE,IAAY;IACpD,MAAM,GAAG,GAAG,WAAW,CAAC,MAAM,CAAC,CAAC,IAAI,GAAG,CAAC,CAAC,CAAC;IAC1C,OAAO,GAAG,KAAK,SAAS,CAAC,CAAC,CAAC,SAAS,CAAC,CAAC,CAAC,UAAU,CAAC,GAAG,CAAC,CAAC;AACzD,CAAC;AAED;;;;;;;;;;;;;GAaG;AACH,MAAM,UAAU,YAAY,CAAC,MAAc;IACzC,OAAO,UAAU,CAAC,QAAQ,CAAC,CAAC,MAAM,CAAC,MAAM,CAAC,OAAO,CAAC,UAAU,EAAE,IAAI,CAAC,CAAC,CAAC,MAAM,CAAC,KAAK,CAAC,CAAC;AACrF,CAAC;AAED;;;;;;;;;;;GAWG;AACH,MAAM,UAAU,iBAAiB,CAAC,CAAU;IAC1C,IAAI,CAAC,KAAK,SAAS;QAAE,OAAO,IAAI,CAAC;IACjC,IAAI,CAAC,KAAK,CAAC,OAAO,CAAC,CAAC,CAAC;QAAE,OAAO,KAAK,CAAC;IACpC,KAAK,MAAM,GAAG,IAAI,CAAC,EAAE,CAAC;QACpB,IAAI,CAAC,QAAQ,CAAC,GAAG,CAAC;YAAE,OAAO,KAAK,CAAC;QACjC,IAAI,OAAO,GAAG,CAAC,MAAM,CAAC,KAAK,QAAQ;YAAE,OAAO,KAAK,CAAC;QAClD,IAAI,OAAO,GAAG,CAAC,MAAM,CAAC,KAAK,QAAQ,IAAI,CAAC,MAAM,CAAC,SAAS,CAAC,GAAG,CAAC,MAAM,CAAC,CAAC,IAAI,GAAG,CAAC,MAAM,CAAC,GAAG,CAAC;YAAE,OAAO,KAAK,CAAC;QACvG,IAAI,OAAO,GAAG,CAAC,aAAa,CAAC,KAAK,QAAQ,IAAI,OAAO,GAAG,CAAC,MAAM,CAAC,KAAK,QAAQ;YAAE,OAAO,KAAK,CAAC;QAC5F,IAAI,GAAG,CAAC,OAAO,CAAC,KAAK,SAAS,IAAI,OAAO,GAAG,CAAC,OAAO,CAAC,KAAK,QAAQ;YAAE,OAAO,KAAK,CAAC;IACnF,CAAC;IACD,OAAO,IAAI,CAAC;AACd,CAAC","sourcesContent":["/**\n * The evidence pin's identity: what `verify --service --record` stamps beside a\n * confirmed agent answer so a later `loam validate`, standing in the same\n * service repository, can re-check the citation against the working tree.\n *\n * Be precise about what a pin proves, because the record travels without loam:\n * a pin identifies WHAT was cited at the attested commit — the cited file's\n * normalized sha256, the cited line's text, the literal token the claim\n * asserts. It does not prove the claim true; an agent that cited the wrong\n * line gets a faithful pin of the wrong line. That is why the read side\n * (`./lint.ts`) only ever demotes reviewer confidence: no pin state moves a\n * verdict in either direction.\n */\nimport { createHash } from \"node:crypto\";\nimport { isRecord } from \"../../kernel/records.js\";\n\nexport interface EvidencePin {\n /** The cited file, repo-relative in the portable spelling evidence entries use. */\n path: string;\n /** The cited line, 1-based — the `:N` of the evidence entry this pins. */\n line: number;\n /** sha256 hex of the whole file at the attested commit, normalized — see {@link pinnedDigest}. */\n file_sha256: string;\n /** The cited line at the attested commit, trimmed and capped — see {@link pinnedText}. */\n text: string;\n /**\n * The literal string the claim asserts of the cited artifact — the\n * operationId for `api.exposes`, the message name for `event.declares`, the\n * edge's op for `c4.calls` (`Claim.token`, stamped at record time). Carried\n * on the pin so the read side scans the file for it and never has to\n * re-derive a checklist or parse claim prose. Absent for the kinds that\n * assert no token (`service.exists`, `scenario.tested`) — and absent when\n * the attested blob did not CONTAIN the token: that fact is warned once, at\n * record time, where the answerer can still act on it; a pin carrying it\n * would make `loam validate` repeat \"no longer contains\" forever about a\n * file that never did, with a re-record that re-derives the same state as\n * the only offered repair. `token-missing` therefore always means \"was\n * there at the attested commit and is gone\".\n */\n token?: string;\n}\n\n/**\n * How much of the cited line a pin carries. Enough to recognize a line of\n * source by eye and by equality; a cap because evidence may cite a line of a\n * generated client or a minified bundle, and the record is a YAML file people\n * read and diff — one pathological line must not turn it into one.\n */\nexport const PIN_TEXT_CAP = 200;\n\n/** The pin's line recipe: trimmed (indentation is formatting), capped at {@link PIN_TEXT_CAP}. */\nexport function pinnedText(line: string): string {\n const trimmed = line.trim();\n return trimmed.length > PIN_TEXT_CAP ? trimmed.slice(0, PIN_TEXT_CAP) : trimmed;\n}\n\n/**\n * The one spelling of where lines fall. Both sides of the pin — the write\n * side's bounds checks and the read side's re-grade — must number lines\n * identically, or a pin recorded at line 12 gets graded against a different\n * line 12; four call sites each spelling the regex themselves is how that\n * divergence would arrive, so the recipe has exactly one home.\n */\nexport function sourceLines(source: string): string[] {\n return source.split(/\\r\\n|\\n|\\r/);\n}\n\n/**\n * The cited line of a source text, through the pin recipe — or undefined when\n * the line is past the end, which the caller grades (a refusal on the write\n * side, `unresolved` on the read side).\n */\nexport function citedLine(source: string, line: number): string | undefined {\n const raw = sourceLines(source)[line - 1];\n return raw === undefined ? undefined : pinnedText(raw);\n}\n\n/**\n * The pin's file digest: sha256 hex over the text with CRLF/CR normalized to\n * LF.\n *\n * Normalized because git's eol config makes blob bytes and working-tree bytes\n * legally differ: on a Windows checkout with `core.autocrlf=true`, the blob\n * the attested commit holds is LF while every file on disk is CRLF, so an\n * unnormalized digest would grade every pin `evidence.moved` forever on the\n * checkout config alone. The pin must convict content change, never checkout\n * config. The recipe is over TEXT by construction — the write side digests\n * `git show` output already decoded as utf8 — and the symmetric cost is\n * accepted and documented in SCHEMA.md: a pure line-ending change is invisible\n * to the lint.\n */\nexport function pinnedDigest(source: string): string {\n return createHash(\"sha256\").update(source.replace(/\\r\\n|\\r/g, \"\\n\")).digest(\"hex\");\n}\n\n/**\n * The read-shape guard `asVerification` (../file.ts) asks per claim, on the\n * `isConsumedContractReport` model to the letter: absent is fine — every\n * record written before pins existed, and every runner-answered claim, has\n * none — but present must be whole for the four fields the lint dereferences.\n * A broken pins block makes the WHOLE record unreadable and therefore never\n * overwritten, exactly the contract-report rule: it is somebody's record, and\n * a lint that silently skipped a malformed pin would read a hand-damaged\n * record as drift-free. `token` stays optional-and-any-string for `format`'s\n * reason — a field a newer loam learned to stamp must not brick the record\n * here.\n */\nexport function isEvidencePinList(v: unknown): boolean {\n if (v === undefined) return true;\n if (!Array.isArray(v)) return false;\n for (const pin of v) {\n if (!isRecord(pin)) return false;\n if (typeof pin[\"path\"] !== \"string\") return false;\n if (typeof pin[\"line\"] !== \"number\" || !Number.isInteger(pin[\"line\"]) || pin[\"line\"] < 1) return false;\n if (typeof pin[\"file_sha256\"] !== \"string\" || typeof pin[\"text\"] !== \"string\") return false;\n if (pin[\"token\"] !== undefined && typeof pin[\"token\"] !== \"string\") return false;\n }\n return true;\n}\n"]}
|
|
@@ -0,0 +1,142 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* What gets written down, and how a set of answers counts up to a verdict.
|
|
3
|
+
*
|
|
4
|
+
* The shapes and the arithmetic are one module because they are one claim about
|
|
5
|
+
* the same numbers: `tallyRecord` recounts from the answers rather than reading
|
|
6
|
+
* the `summary` field back, so a record whose header disagrees with its own
|
|
7
|
+
* claims is caught rather than believed. A separate counter would be a second
|
|
8
|
+
* opinion, which is precisely what this file exists to prevent.
|
|
9
|
+
*
|
|
10
|
+
* `verified` is reserved for a checklist whose scenarios a RUN answered.
|
|
11
|
+
* Where an agent answered one instead — a legacy service whose suite is months
|
|
12
|
+
* away, which is the fleet loam exists for — the record marks it attested, and
|
|
13
|
+
* `attestedNotice` says so on every surface that reads the record.
|
|
14
|
+
*/
|
|
15
|
+
import { join } from "node:path";
|
|
16
|
+
import {} from "./claims/identity.js";
|
|
17
|
+
import {} from "./answers.js";
|
|
18
|
+
import {} from "./pins/pin.js";
|
|
19
|
+
/**
|
|
20
|
+
* The confirmed `scenario.tested` claims that no test run answered.
|
|
21
|
+
*
|
|
22
|
+
* A scenario claim's whole premise is that a run answers it — its digest IS the
|
|
23
|
+
* tag `loam gherkin` stamps, and `--results` matches the two mechanically. An
|
|
24
|
+
* agent may still answer one, because a legacy service has no runnable suite
|
|
25
|
+
* for months and that fleet is who loam is for; but the answer is somebody's
|
|
26
|
+
* word about a test, and it must not read as the run. A record written before
|
|
27
|
+
* `--results` existed carries no `answered_by` at all: unknown counts as agent
|
|
28
|
+
* here, because the alternative is to credit a run nobody can point at.
|
|
29
|
+
*/
|
|
30
|
+
export function attestedClaims(claims) {
|
|
31
|
+
return claims
|
|
32
|
+
.filter((c) => c.kind === "scenario.tested" && c.verdict === "confirmed" && c.answered_by !== "runner")
|
|
33
|
+
.map((c) => c.id);
|
|
34
|
+
}
|
|
35
|
+
/**
|
|
36
|
+
* Of the CONFIRMED answers, one count per `answered_by` value. Anything that is not a runner spelling
|
|
37
|
+
* counts as the agent's word — a record too old to carry the field says nothing, and crediting a run
|
|
38
|
+
* nobody can point at is the error that matters. Never derive it by subtracting {@link attestedClaims}
|
|
39
|
+
* from the confirmed total: that count is `scenario.tested` ALONE, so the fleet scorecard filed every
|
|
40
|
+
* other kind an agent confirmed under a runner that never ran.
|
|
41
|
+
*/
|
|
42
|
+
export function confirmedProvenance(claims) {
|
|
43
|
+
const done = claims.filter((c) => c.verdict === "confirmed");
|
|
44
|
+
const by = (who) => done.filter((c) => c.answered_by === who).length;
|
|
45
|
+
const ran = { runner: by("runner"), "external-runner": by("external-runner") };
|
|
46
|
+
return { ...ran, agent: done.length - ran.runner - ran["external-runner"] };
|
|
47
|
+
}
|
|
48
|
+
/**
|
|
49
|
+
* `unanswered` is the count the answers themselves cannot show: a federated
|
|
50
|
+
* record holds only what somebody answered, and the claims still owed to other
|
|
51
|
+
* repositories exist on the checklist, not in `claims[]`. Read views that carry
|
|
52
|
+
* an `unanswered` status inside the list pass 0 and are counted in place.
|
|
53
|
+
*/
|
|
54
|
+
export function tallyAnswers(claims, unanswered = 0) {
|
|
55
|
+
const of = (verdict) => claims.filter((c) => c.verdict === verdict).length;
|
|
56
|
+
return {
|
|
57
|
+
claims: claims.length + unanswered,
|
|
58
|
+
confirmed: of("confirmed"),
|
|
59
|
+
unconfirmed: of("unconfirmed"),
|
|
60
|
+
unanswered: of("unanswered") + unanswered,
|
|
61
|
+
attested: attestedClaims(claims).length,
|
|
62
|
+
};
|
|
63
|
+
}
|
|
64
|
+
/**
|
|
65
|
+
* The record's own answers, recounted. Never `v.summary`: the summary is what
|
|
66
|
+
* whoever wrote the file said the answers add up to, and every reader that
|
|
67
|
+
* believed it reported a record full of unconfirmed claims as fully confirmed.
|
|
68
|
+
* `readVerificationState` refuses a record whose summary contradicts this, so a
|
|
69
|
+
* caller holding a Verification may use either — and should use this one.
|
|
70
|
+
*/
|
|
71
|
+
export function tallyRecord(v) {
|
|
72
|
+
return tallyAnswers(v.claims, v.summary.unanswered ?? 0);
|
|
73
|
+
}
|
|
74
|
+
/**
|
|
75
|
+
* Three states, because two were a lie. `verified` is the strong claim — every
|
|
76
|
+
* question answered, every answer confirmed, and every scenario answered by a
|
|
77
|
+
* digest-matched green run. `attested` is the same completeness resting, for at
|
|
78
|
+
* least one scenario, on an agent's word. `unverified` is everything else:
|
|
79
|
+
* nothing recorded, a record gone stale, a claim unanswered or unconfirmed, or
|
|
80
|
+
* a checklist that asks nothing at all.
|
|
81
|
+
*/
|
|
82
|
+
export const VERIFICATION_VERDICTS = ["verified", "attested", "unverified"];
|
|
83
|
+
export function verificationVerdict(t, stale = false) {
|
|
84
|
+
if (stale || t.claims === 0 || t.confirmed !== t.claims)
|
|
85
|
+
return "unverified";
|
|
86
|
+
return t.attested > 0 ? "attested" : "verified";
|
|
87
|
+
}
|
|
88
|
+
/**
|
|
89
|
+
* The one notice `verify`, `list` and `status` all show: this record's scenario
|
|
90
|
+
* claims were confirmed without a run.
|
|
91
|
+
*
|
|
92
|
+
* It is a warning and it gates nothing — verify has never gated, and a service
|
|
93
|
+
* with no suite yet has to be able to ship. What it must never do is read the
|
|
94
|
+
* same as a green run.
|
|
95
|
+
*/
|
|
96
|
+
export function attestedNotice(claims, feature) {
|
|
97
|
+
const ids = attestedClaims(claims);
|
|
98
|
+
if (ids.length === 0)
|
|
99
|
+
return null;
|
|
100
|
+
return {
|
|
101
|
+
code: "verify.scenario-attested",
|
|
102
|
+
severity: "warn",
|
|
103
|
+
message: `${ids.length} scenario claim(s) are confirmed on an agent's word, not on a test run: ${ids.join(", ")}. ` +
|
|
104
|
+
`Answer them mechanically once the suite runs: \`loam verify ${feature} --results <cucumber.json>\`.`,
|
|
105
|
+
claims: ids,
|
|
106
|
+
};
|
|
107
|
+
}
|
|
108
|
+
/**
|
|
109
|
+
* The one-line honesty summary of a record that leaves claims open: partial is
|
|
110
|
+
* not a clean result, and the counts alone did not say so — `summary` carries
|
|
111
|
+
* them on every surface, and a reader skimming for the ⚠ lines shipped a
|
|
112
|
+
* half-answered record as done anyway. Null when nothing is open, and the
|
|
113
|
+
* callers fire it only when a record EXISTS: a feature with no record already
|
|
114
|
+
* says "Not verified", and calling that "not a clean result" would conflate
|
|
115
|
+
* not-started with partial. A pure-attested record (all confirmed, some on an
|
|
116
|
+
* agent's word) never reaches the message either — its counts are closed, and
|
|
117
|
+
* `attestedNotice` above owns that honesty; two notices about one fact is the
|
|
118
|
+
* double-reporting this file's notices exist to prevent.
|
|
119
|
+
*/
|
|
120
|
+
export function openClaimsNotice(tally, feature) {
|
|
121
|
+
const open = tally.unconfirmed + tally.unanswered;
|
|
122
|
+
if (open === 0)
|
|
123
|
+
return null;
|
|
124
|
+
return {
|
|
125
|
+
code: "verify.claims-open",
|
|
126
|
+
severity: "warn",
|
|
127
|
+
message: `not a clean result: of ${tally.claims} claim(s), ${tally.confirmed} confirmed` +
|
|
128
|
+
(tally.attested > 0 ? ` (${tally.attested} on an agent's word)` : "") +
|
|
129
|
+
`, ${tally.unconfirmed} unconfirmed, ${tally.unanswered} unanswered — ` +
|
|
130
|
+
`treat ${feature} as unverified until every claim is answered and confirmed.`,
|
|
131
|
+
};
|
|
132
|
+
}
|
|
133
|
+
/**
|
|
134
|
+
* Where the record lives: inside the feature, so `archive` carries it into
|
|
135
|
+
* `features/archive/` with everything else and a reviewer finds it next to the
|
|
136
|
+
* delta it is about. YAML rather than frontmatter or JSON — it is data, it has
|
|
137
|
+
* to survive without loam, and one claim per block is a diff a person can read.
|
|
138
|
+
*/
|
|
139
|
+
export function verificationPath(featureDir) {
|
|
140
|
+
return join(featureDir, "verification.yaml");
|
|
141
|
+
}
|
|
142
|
+
//# sourceMappingURL=record.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"record.js","sourceRoot":"","sources":["../../../src/core/verify/record.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * What gets written down, and how a set of answers counts up to a verdict.\n *\n * The shapes and the arithmetic are one module because they are one claim about\n * the same numbers: `tallyRecord` recounts from the answers rather than reading\n * the `summary` field back, so a record whose header disagrees with its own\n * claims is caught rather than believed. A separate counter would be a second\n * opinion, which is precisely what this file exists to prevent.\n *\n * `verified` is reserved for a checklist whose scenarios a RUN answered.\n * Where an agent answered one instead — a legacy service whose suite is months\n * away, which is the fleet loam exists for — the record marks it attested, and\n * `attestedNotice` says so on every surface that reads the record.\n */\nimport { join } from \"node:path\";\nimport { type ClaimKind } from \"./claims/identity.js\";\nimport { type AnsweredBy, type Verdict } from \"./answers.js\";\nimport { type EvidencePin } from \"./pins/pin.js\";\n\nexport interface RecordedClaim {\n id: string;\n kind: ClaimKind;\n /** Present in federated records; omitted by legacy records written before schema 2. */\n subject?: string;\n claim: string;\n verdict: Verdict;\n /** Who answered — absent only in records written before `--results` existed. */\n answered_by?: AnsweredBy;\n evidence: string[];\n note?: string;\n /**\n * One pin per file:line citation of a federated agent-confirmed answer: the\n * cited file's normalized sha256 at the attested commit, the cited line's\n * text, and the claim's token where it asserts one. A pin records which\n * text was cited at that commit so `loam validate`, run later in the\n * service's own repo, can convict drift; it does not prove the claim true —\n * a faithful pin of the wrong line is still a wrong answer. Absent on\n * runner-answered claims (their evidence names a report entry, not a file)\n * and on every record written before pins existed.\n */\n evidence_pins?: EvidencePin[];\n}\n\n/**\n * The test report a `--results` run consumed.\n *\n * Nothing can prove a JSON file came from executing a particular commit — a\n * report is bytes, and bytes are writable. What loam can do is stop leaving the\n * question open: it records exactly which file it read (inside the attesting\n * repository, resolved by the same path rules as evidence), the sha256 of the\n * bytes it read, the file's own mtime and how many digest-tagged scenarios it\n * carried. A reviewer holding the repository can tell whether this is that\n * file; before this, `answered_by: runner` named nothing at all.\n */\nexport interface ConsumedReport {\n /** As it was passed to `--results` — repo-relative in a federated record. */\n path: string;\n /** Full sha256 of the bytes read, so `shasum -a 256 <path>` answers the question. */\n digest: string;\n /** ISO-8601 mtime of the report file — when the run that wrote it finished. */\n mtime: string;\n /** How many digest-tagged scenarios the report carried. */\n scenarios: number;\n}\n\n/**\n * The contract-test report a `--contract-results` run consumed — the same pin,\n * the same honesty, for the report that answers `api.exposes` claims by\n * operationId. Nothing here proves the file came from executing the attested\n * commit either; the digest identifies WHICH file was consumed, and the\n * federated form binds a committed one to the commit exactly as `report:` is.\n */\nexport interface ConsumedContractReport {\n /** As it was passed to `--contract-results` — repo-relative in a federated record. */\n path: string;\n /** Full sha256 of the bytes read. */\n digest: string;\n /** ISO-8601 mtime of the report file. */\n mtime: string;\n /** How many distinct operationIds the report's entries name. */\n operations: number;\n /**\n * Which accepted shape the report was read as — `\"generic\"` for the\n * documented `loamContractReport: 1` shape, the only one v1 accepts. loam\n * always writes it, but the READ shape keeps it optional: no reader\n * dereferences it, so a hand-written pin without it stays a readable record\n * (`isConsumedContractReport` in ./file.ts says why) — and it is a string\n * rather than a closed union so a format a NEWER loam learned to read stays\n * readable here too.\n */\n format?: string;\n}\n\n/**\n * Both mechanical answer sheets one recording run may consume, together\n * because they are validated together (`validateServiceEvidence` binds each to\n * the attested commit) and filed together (on the record all-at-once, inside\n * the service's attestation federated). Two loose optional parameters made the\n * inconsistent pair representable at every call site.\n */\nexport interface ConsumedReports {\n results?: ConsumedReport;\n contract?: ConsumedContractReport;\n}\n\n/** A service repository's commit-bound contribution to a federated record. */\nexport interface ServiceAttestation {\n service: string;\n commit: string;\n recorded: string;\n /** Claim ids this commit answered. Kept explicit so stale answers can be pruned safely. */\n claims: string[];\n /**\n * The checklist digest THIS attestation answered.\n *\n * The record carries one `checklist` for the whole file, which is the right\n * field for the all-at-once form and an incomplete one for a federated\n * record: two services attesting a week apart across a changed delta write\n * the same top-level digest, and nothing in the file can say they answered\n * different questions. With this, `verify.checklist-forked` can.\n *\n * Optional forever. An attestation written before this field existed carries\n * no claim about which checklist it answered, and inventing one from the\n * record-level digest would be asserting exactly the thing that cannot be\n * known — the `evidence.unpinned` precedent, one axis over.\n */\n checklist?: string;\n /**\n * The docs-repo commit the checklist was computed from, when the docs repo is\n * a git repository with a committed HEAD.\n *\n * `commit` pins the SERVICE repo — the code the evidence points into.\n * Nothing pinned the side the QUESTION came from, so a reader could not tell\n * a re-record against an unchanged feature from one against a rewritten\n * delta. Absent whenever git cannot answer: a docs repo is not required to be\n * a git checkout, and a missing pin is not a defect.\n */\n docsCommit?: string;\n /** The report that answered this service's scenario claims, when `--results` did. */\n report?: ConsumedReport;\n /** The report that answered this service's api.exposes claims, when `--contract-results` did. */\n contractReport?: ConsumedContractReport;\n}\n\nexport interface Verification {\n /** Federated, partial records use schema 2. Absent means the original all-at-once format. */\n schema?: 2;\n feature: string;\n /** The day the answers were recorded. */\n recorded: string;\n /** The checklist digest they answer — how a later reader spots a record gone stale. */\n checklist: string;\n summary: { claims: number; confirmed: number; unconfirmed: number; unanswered?: number };\n claims: RecordedClaim[];\n /** The all-at-once form's consumed report; the federated form files it per attestation. */\n report?: ConsumedReport;\n /** The all-at-once form's consumed contract report — filed per attestation when federated. */\n contractReport?: ConsumedContractReport;\n attestations?: ServiceAttestation[];\n}\n\n/**\n * Anything carrying an answer, whichever surface holds it: a recorded claim, or\n * the read view's per-claim status. Structural on purpose — the same questions\n * are asked of both, and neither should have to convert into the other to be\n * counted.\n */\nexport interface AnsweredClaim {\n id: string;\n kind: string;\n verdict: string;\n answered_by?: string;\n}\n\n/**\n * The confirmed `scenario.tested` claims that no test run answered.\n *\n * A scenario claim's whole premise is that a run answers it — its digest IS the\n * tag `loam gherkin` stamps, and `--results` matches the two mechanically. An\n * agent may still answer one, because a legacy service has no runnable suite\n * for months and that fleet is who loam is for; but the answer is somebody's\n * word about a test, and it must not read as the run. A record written before\n * `--results` existed carries no `answered_by` at all: unknown counts as agent\n * here, because the alternative is to credit a run nobody can point at.\n */\nexport function attestedClaims(claims: readonly AnsweredClaim[]): string[] {\n return claims\n .filter((c) => c.kind === \"scenario.tested\" && c.verdict === \"confirmed\" && c.answered_by !== \"runner\")\n .map((c) => c.id);\n}\n\n/**\n * Of the CONFIRMED answers, one count per `answered_by` value. Anything that is not a runner spelling\n * counts as the agent's word — a record too old to carry the field says nothing, and crediting a run\n * nobody can point at is the error that matters. Never derive it by subtracting {@link attestedClaims}\n * from the confirmed total: that count is `scenario.tested` ALONE, so the fleet scorecard filed every\n * other kind an agent confirmed under a runner that never ran.\n */\nexport function confirmedProvenance(claims: readonly AnsweredClaim[]): Record<AnsweredBy, number> {\n const done = claims.filter((c) => c.verdict === \"confirmed\");\n const by = (who: AnsweredBy): number => done.filter((c) => c.answered_by === who).length;\n const ran = { runner: by(\"runner\"), \"external-runner\": by(\"external-runner\") };\n return { ...ran, agent: done.length - ran.runner - ran[\"external-runner\"] };\n}\n\n/** How a set of answers counts up — recounted from the answers, never read off a `summary`. */\nexport interface RecordTally {\n /** Questions asked: answered, plus the ones a federated record leaves for other repos. */\n claims: number;\n confirmed: number;\n unconfirmed: number;\n unanswered: number;\n /** Of the confirmed, the scenario claims on an agent's word. See {@link attestedClaims}. */\n attested: number;\n}\n\n/**\n * `unanswered` is the count the answers themselves cannot show: a federated\n * record holds only what somebody answered, and the claims still owed to other\n * repositories exist on the checklist, not in `claims[]`. Read views that carry\n * an `unanswered` status inside the list pass 0 and are counted in place.\n */\nexport function tallyAnswers(claims: readonly AnsweredClaim[], unanswered = 0): RecordTally {\n const of = (verdict: string): number => claims.filter((c) => c.verdict === verdict).length;\n return {\n claims: claims.length + unanswered,\n confirmed: of(\"confirmed\"),\n unconfirmed: of(\"unconfirmed\"),\n unanswered: of(\"unanswered\") + unanswered,\n attested: attestedClaims(claims).length,\n };\n}\n\n/**\n * The record's own answers, recounted. Never `v.summary`: the summary is what\n * whoever wrote the file said the answers add up to, and every reader that\n * believed it reported a record full of unconfirmed claims as fully confirmed.\n * `readVerificationState` refuses a record whose summary contradicts this, so a\n * caller holding a Verification may use either — and should use this one.\n */\nexport function tallyRecord(v: Verification): RecordTally {\n return tallyAnswers(v.claims, v.summary.unanswered ?? 0);\n}\n\n/**\n * Three states, because two were a lie. `verified` is the strong claim — every\n * question answered, every answer confirmed, and every scenario answered by a\n * digest-matched green run. `attested` is the same completeness resting, for at\n * least one scenario, on an agent's word. `unverified` is everything else:\n * nothing recorded, a record gone stale, a claim unanswered or unconfirmed, or\n * a checklist that asks nothing at all.\n */\nexport const VERIFICATION_VERDICTS = [\"verified\", \"attested\", \"unverified\"] as const;\nexport type VerificationVerdict = (typeof VERIFICATION_VERDICTS)[number];\n\nexport function verificationVerdict(t: RecordTally, stale = false): VerificationVerdict {\n if (stale || t.claims === 0 || t.confirmed !== t.claims) return \"unverified\";\n return t.attested > 0 ? \"attested\" : \"verified\";\n}\n\n/** A finding a verify surface reports about a record. Stable code, prose that may change. */\nexport interface VerifyNotice {\n code: string;\n severity: \"warn\";\n message: string;\n /** The claims it is about, when it is about claims. */\n claims?: string[];\n}\n\n/**\n * The one notice `verify`, `list` and `status` all show: this record's scenario\n * claims were confirmed without a run.\n *\n * It is a warning and it gates nothing — verify has never gated, and a service\n * with no suite yet has to be able to ship. What it must never do is read the\n * same as a green run.\n */\nexport function attestedNotice(claims: readonly AnsweredClaim[], feature: string): VerifyNotice | null {\n const ids = attestedClaims(claims);\n if (ids.length === 0) return null;\n return {\n code: \"verify.scenario-attested\",\n severity: \"warn\",\n message:\n `${ids.length} scenario claim(s) are confirmed on an agent's word, not on a test run: ${ids.join(\", \")}. ` +\n `Answer them mechanically once the suite runs: \\`loam verify ${feature} --results <cucumber.json>\\`.`,\n claims: ids,\n };\n}\n\n/**\n * The one-line honesty summary of a record that leaves claims open: partial is\n * not a clean result, and the counts alone did not say so — `summary` carries\n * them on every surface, and a reader skimming for the ⚠ lines shipped a\n * half-answered record as done anyway. Null when nothing is open, and the\n * callers fire it only when a record EXISTS: a feature with no record already\n * says \"Not verified\", and calling that \"not a clean result\" would conflate\n * not-started with partial. A pure-attested record (all confirmed, some on an\n * agent's word) never reaches the message either — its counts are closed, and\n * `attestedNotice` above owns that honesty; two notices about one fact is the\n * double-reporting this file's notices exist to prevent.\n */\nexport function openClaimsNotice(tally: RecordTally, feature: string): VerifyNotice | null {\n const open = tally.unconfirmed + tally.unanswered;\n if (open === 0) return null;\n return {\n code: \"verify.claims-open\",\n severity: \"warn\",\n message:\n `not a clean result: of ${tally.claims} claim(s), ${tally.confirmed} confirmed` +\n (tally.attested > 0 ? ` (${tally.attested} on an agent's word)` : \"\") +\n `, ${tally.unconfirmed} unconfirmed, ${tally.unanswered} unanswered — ` +\n `treat ${feature} as unverified until every claim is answered and confirmed.`,\n };\n}\n\n/**\n * Where the record lives: inside the feature, so `archive` carries it into\n * `features/archive/` with everything else and a reviewer finds it next to the\n * delta it is about. YAML rather than frontmatter or JSON — it is data, it has\n * to survive without loam, and one claim per block is a diff a person can read.\n */\nexport function verificationPath(featureDir: string): string {\n return join(featureDir, \"verification.yaml\");\n}\n\n"]}
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The verification record's commit: staged beside the target, compared against
|
|
3
|
+
* the byte pre-image the merge consumed, swapped in by one rename(2).
|
|
4
|
+
*
|
|
5
|
+
* The record used to go down as a plain `writeFile`, which cost three ways at
|
|
6
|
+
* once: two `--record` runs read the same pre-image and the last writer
|
|
7
|
+
* silently discarded the other service's attestation; a process killed
|
|
8
|
+
* mid-write left the ONLY copy truncated, unreadable, and blocking every later
|
|
9
|
+
* record; and an edit that landed between the read and the write was buried
|
|
10
|
+
* under bytes computed from a document that no longer existed. The docs lock
|
|
11
|
+
* (taken by `loam verify` before its authoritative read) closes the first;
|
|
12
|
+
* this module closes the other two, and backstops the lock itself for a writer
|
|
13
|
+
* that never took it.
|
|
14
|
+
*
|
|
15
|
+
* A sub-package of core/verify rather than a sixth file in it, and a consumer
|
|
16
|
+
* of core/staging's primitives rather than a re-implementation: `stageWrites`
|
|
17
|
+
* and `swapStaged` already do temp-beside-target, whole-content compare-and-set
|
|
18
|
+
* and same-directory rename — vouch commits through exactly this shape.
|
|
19
|
+
*/
|
|
20
|
+
import { message, rollbackMessage, rollbackStaged, stageWrites, StagingRaceError, swapStaged } from "../../staging/commit.js";
|
|
21
|
+
import { repoPath } from "../../envelope/json.js";
|
|
22
|
+
import {} from "../../staging/writes.js";
|
|
23
|
+
import { renderVerification } from "./render.js";
|
|
24
|
+
import { verificationPath } from "../record.js";
|
|
25
|
+
/**
|
|
26
|
+
* `preImage` is the exact bytes the caller's merge was computed from, or null
|
|
27
|
+
* when the record was absent at the authoritative read — in which case the
|
|
28
|
+
* commit may only CREATE (link(2) refuses a file that appeared since), never
|
|
29
|
+
* replace one it has not read.
|
|
30
|
+
*/
|
|
31
|
+
export async function commitVerification(featureDir, v, preImage, docsDir) {
|
|
32
|
+
const path = verificationPath(featureDir);
|
|
33
|
+
const display = repoPath(docsDir, path);
|
|
34
|
+
const write = preImage === null ? { path, content: renderVerification(v), exclusive: true } : { path, content: renderVerification(v) };
|
|
35
|
+
// Staging touches the filesystem — a read-only feature directory, a full
|
|
36
|
+
// disk, a pre-image that will not read. None of that is a bug in loam, and
|
|
37
|
+
// letting it escape as `internal` sends the reader looking for one (archive
|
|
38
|
+
// draws the same line around its own `stageWrites`). Nothing was written.
|
|
39
|
+
let staged;
|
|
40
|
+
try {
|
|
41
|
+
staged = await stageWrites([write]);
|
|
42
|
+
}
|
|
43
|
+
catch (err) {
|
|
44
|
+
return { ok: false, code: "merge-failed", message: `${message(err)} — nothing was recorded; the record is as the last writer left it` };
|
|
45
|
+
}
|
|
46
|
+
// Joined by path, not by index — vouch's rule, for vouch's reason: one entry
|
|
47
|
+
// per planned write in order is true today and promised nowhere, and a
|
|
48
|
+
// compare-and-set over the wrong pair would vouch for bytes the merge never
|
|
49
|
+
// saw. A missing entry fails closed as a merge that never happened.
|
|
50
|
+
const s = staged.find((x) => x.write.path === path);
|
|
51
|
+
if (s === undefined) {
|
|
52
|
+
await rollbackStaged(staged);
|
|
53
|
+
return { ok: false, code: "merge-failed", message: `staging returned no entry for ${display} — nothing was recorded` };
|
|
54
|
+
}
|
|
55
|
+
// The compare the read promised: the file as staging found it must be the
|
|
56
|
+
// file the merge consumed. `swapStaged` re-checks against ITS read just
|
|
57
|
+
// before the rename, so together the two compares cover the whole window
|
|
58
|
+
// from the authoritative read to the swap.
|
|
59
|
+
const edited = preImage === null ? s.before !== null : s.before === null || !s.before.equals(preImage);
|
|
60
|
+
if (edited) {
|
|
61
|
+
await rollbackStaged(staged);
|
|
62
|
+
return { ok: false, code: "record-raced", message: racedMessage(display) };
|
|
63
|
+
}
|
|
64
|
+
try {
|
|
65
|
+
await swapStaged(staged);
|
|
66
|
+
}
|
|
67
|
+
catch (err) {
|
|
68
|
+
const failures = await rollbackStaged(staged);
|
|
69
|
+
if (failures.length > 0) {
|
|
70
|
+
return {
|
|
71
|
+
ok: false,
|
|
72
|
+
code: "rollback-incomplete",
|
|
73
|
+
message: rollbackMessage(err, failures, "written"),
|
|
74
|
+
};
|
|
75
|
+
}
|
|
76
|
+
// EEXIST is the exclusive create losing its race — same fact as the CAS
|
|
77
|
+
// refusal, discovered by link(2) instead of by the compare.
|
|
78
|
+
if (err instanceof StagingRaceError || err.code === "EEXIST") {
|
|
79
|
+
return { ok: false, code: "record-raced", message: racedMessage(display) };
|
|
80
|
+
}
|
|
81
|
+
return { ok: false, code: "merge-failed", message: `${message(err)} — nothing was recorded; the record is as the last writer left it` };
|
|
82
|
+
}
|
|
83
|
+
return { ok: true, path };
|
|
84
|
+
}
|
|
85
|
+
function racedMessage(display) {
|
|
86
|
+
return (`${display} changed while this record was being written — another writer or an editor landed between the read and the write. ` +
|
|
87
|
+
"Nothing was written and their bytes are untouched. Re-run: the merge is recomputed over the record as it now stands.");
|
|
88
|
+
}
|
|
89
|
+
//# sourceMappingURL=commit.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"commit.js","sourceRoot":"","sources":["../../../../src/core/verify/store/commit.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * The verification record's commit: staged beside the target, compared against\n * the byte pre-image the merge consumed, swapped in by one rename(2).\n *\n * The record used to go down as a plain `writeFile`, which cost three ways at\n * once: two `--record` runs read the same pre-image and the last writer\n * silently discarded the other service's attestation; a process killed\n * mid-write left the ONLY copy truncated, unreadable, and blocking every later\n * record; and an edit that landed between the read and the write was buried\n * under bytes computed from a document that no longer existed. The docs lock\n * (taken by `loam verify` before its authoritative read) closes the first;\n * this module closes the other two, and backstops the lock itself for a writer\n * that never took it.\n *\n * A sub-package of core/verify rather than a sixth file in it, and a consumer\n * of core/staging's primitives rather than a re-implementation: `stageWrites`\n * and `swapStaged` already do temp-beside-target, whole-content compare-and-set\n * and same-directory rename — vouch commits through exactly this shape.\n */\nimport { message, rollbackMessage, rollbackStaged, stageWrites, StagingRaceError, swapStaged } from \"../../staging/commit.js\";\nimport { repoPath } from \"../../envelope/json.js\";\nimport { type PlannedWrite, type StagedWrite } from \"../../staging/writes.js\";\nimport { renderVerification } from \"./render.js\";\nimport { verificationPath, type Verification } from \"../record.js\";\n\n/**\n * `record-raced` re-runs cleanly: the merge is recomputed over whatever the\n * other writer left. The other two are staging's own vocabulary, with staging's\n * meanings — `merge-failed` trusts the repo, `rollback-incomplete` does not.\n */\nexport type VerificationCommit =\n | { ok: true; path: string }\n | { ok: false; code: \"record-raced\" | \"merge-failed\" | \"rollback-incomplete\"; message: string };\n\n/**\n * `preImage` is the exact bytes the caller's merge was computed from, or null\n * when the record was absent at the authoritative read — in which case the\n * commit may only CREATE (link(2) refuses a file that appeared since), never\n * replace one it has not read.\n */\nexport async function commitVerification(\n featureDir: string,\n v: Verification,\n preImage: Buffer | null,\n docsDir: string,\n): Promise<VerificationCommit> {\n const path = verificationPath(featureDir);\n const display = repoPath(docsDir, path);\n const write: PlannedWrite =\n preImage === null ? { path, content: renderVerification(v), exclusive: true } : { path, content: renderVerification(v) };\n // Staging touches the filesystem — a read-only feature directory, a full\n // disk, a pre-image that will not read. None of that is a bug in loam, and\n // letting it escape as `internal` sends the reader looking for one (archive\n // draws the same line around its own `stageWrites`). Nothing was written.\n let staged: StagedWrite[];\n try {\n staged = await stageWrites([write]);\n } catch (err) {\n return { ok: false, code: \"merge-failed\", message: `${message(err)} — nothing was recorded; the record is as the last writer left it` };\n }\n // Joined by path, not by index — vouch's rule, for vouch's reason: one entry\n // per planned write in order is true today and promised nowhere, and a\n // compare-and-set over the wrong pair would vouch for bytes the merge never\n // saw. A missing entry fails closed as a merge that never happened.\n const s = staged.find((x) => x.write.path === path);\n if (s === undefined) {\n await rollbackStaged(staged);\n return { ok: false, code: \"merge-failed\", message: `staging returned no entry for ${display} — nothing was recorded` };\n }\n // The compare the read promised: the file as staging found it must be the\n // file the merge consumed. `swapStaged` re-checks against ITS read just\n // before the rename, so together the two compares cover the whole window\n // from the authoritative read to the swap.\n const edited = preImage === null ? s.before !== null : s.before === null || !s.before.equals(preImage);\n if (edited) {\n await rollbackStaged(staged);\n return { ok: false, code: \"record-raced\", message: racedMessage(display) };\n }\n try {\n await swapStaged(staged);\n } catch (err) {\n const failures = await rollbackStaged(staged);\n if (failures.length > 0) {\n return {\n ok: false,\n code: \"rollback-incomplete\",\n message: rollbackMessage(err, failures, \"written\"),\n };\n }\n // EEXIST is the exclusive create losing its race — same fact as the CAS\n // refusal, discovered by link(2) instead of by the compare.\n if (err instanceof StagingRaceError || (err as NodeJS.ErrnoException).code === \"EEXIST\") {\n return { ok: false, code: \"record-raced\", message: racedMessage(display) };\n }\n return { ok: false, code: \"merge-failed\", message: `${message(err)} — nothing was recorded; the record is as the last writer left it` };\n }\n return { ok: true, path };\n}\n\nfunction racedMessage(display: string): string {\n return (\n `${display} changed while this record was being written — another writer or an editor landed between the read and the write. ` +\n \"Nothing was written and their bytes are untouched. Re-run: the merge is recomputed over the record as it now stands.\"\n );\n}\n"]}
|