@ybotok/loam 0.2.0-alpha.7 → 0.2.0-alpha.9
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +1315 -1
- package/COMPARISON.md +91 -3
- package/CONTRIBUTING.md +37 -0
- package/MIGRATING-from-OpenSpec.md +40 -22
- package/QUICKSTART.md +181 -9
- package/README.md +256 -57
- package/ROADMAP.md +83 -7
- package/SCHEMA.md +569 -98
- package/WORKFLOW.md +272 -62
- package/dist/cli.js +26 -8
- package/dist/cli.js.map +1 -1
- package/dist/commands/adopt/adopt.js +5 -10
- package/dist/commands/adopt/adopt.js.map +1 -1
- package/dist/commands/archive/archive.js +15 -18
- package/dist/commands/archive/archive.js.map +1 -1
- package/dist/commands/archive/plan/contracts/asyncapi.js +27 -41
- package/dist/commands/archive/plan/contracts/asyncapi.js.map +1 -1
- package/dist/commands/archive/plan/contracts/consumers.js +56 -0
- package/dist/commands/archive/plan/contracts/consumers.js.map +1 -0
- package/dist/commands/archive/plan/contracts/openapi.js +25 -42
- package/dist/commands/archive/plan/contracts/openapi.js.map +1 -1
- package/dist/commands/archive/plan/contracts/verdict-lines.js +41 -0
- package/dist/commands/archive/plan/contracts/verdict-lines.js.map +1 -0
- package/dist/commands/archive/plan/gate.js +3 -7
- package/dist/commands/archive/plan/gate.js.map +1 -1
- package/dist/commands/archive/plan/landscape.js +4 -4
- package/dist/commands/archive/plan/landscape.js.map +1 -1
- package/dist/commands/archive/plan/refusal.js +8 -18
- package/dist/commands/archive/plan/refusal.js.map +1 -1
- package/dist/commands/archive/plan/specs.js +134 -25
- package/dist/commands/archive/plan/specs.js.map +1 -1
- package/dist/commands/archive/plan/state.js +1 -0
- package/dist/commands/archive/plan/state.js.map +1 -1
- package/dist/commands/archive/run.js +33 -11
- package/dist/commands/archive/run.js.map +1 -1
- package/dist/commands/archive/shipped.js +49 -0
- package/dist/commands/archive/shipped.js.map +1 -0
- package/dist/commands/context/context.js +4 -7
- package/dist/commands/context/context.js.map +1 -1
- package/dist/commands/context/print.js +2 -2
- package/dist/commands/context/print.js.map +1 -1
- package/dist/commands/delta/delta.js +44 -12
- package/dist/commands/delta/delta.js.map +1 -1
- package/dist/commands/delta/print.js +1 -1
- package/dist/commands/delta/print.js.map +1 -1
- package/dist/commands/dependencies.js +3 -7
- package/dist/commands/dependencies.js.map +1 -1
- package/dist/commands/diff/diff.js +20 -27
- package/dist/commands/diff/diff.js.map +1 -1
- package/dist/commands/explore.js +4 -8
- package/dist/commands/explore.js.map +1 -1
- package/dist/commands/gate/gate.js +45 -8
- package/dist/commands/gate/gate.js.map +1 -1
- package/dist/commands/gate/print.js +15 -3
- package/dist/commands/gate/print.js.map +1 -1
- package/dist/commands/gherkin/commit.js +13 -54
- package/dist/commands/gherkin/commit.js.map +1 -1
- package/dist/commands/gherkin/gherkin.js +36 -20
- package/dist/commands/gherkin/gherkin.js.map +1 -1
- package/dist/commands/gherkin/reconcile.js +10 -13
- package/dist/commands/gherkin/reconcile.js.map +1 -1
- package/dist/commands/init/first-hour.js +44 -0
- package/dist/commands/init/first-hour.js.map +1 -1
- package/dist/commands/init/init.js +18 -22
- package/dist/commands/init/init.js.map +1 -1
- package/dist/commands/list/campaign/campaign.js +4 -6
- package/dist/commands/list/campaign/campaign.js.map +1 -1
- package/dist/commands/list/json.js +0 -4
- package/dist/commands/list/json.js.map +1 -1
- package/dist/commands/list/list.js +38 -23
- package/dist/commands/list/list.js.map +1 -1
- package/dist/commands/list/print.js +38 -6
- package/dist/commands/list/print.js.map +1 -1
- package/dist/commands/list/suites/suites.js +140 -0
- package/dist/commands/list/suites/suites.js.map +1 -0
- package/dist/commands/list/views.js +28 -33
- package/dist/commands/list/views.js.map +1 -1
- package/dist/commands/mcp/dispatch.js +91 -36
- package/dist/commands/mcp/dispatch.js.map +1 -1
- package/dist/commands/mcp/mcp.js +56 -15
- package/dist/commands/mcp/mcp.js.map +1 -1
- package/dist/commands/mcp/serve.js.map +1 -1
- package/dist/commands/migrate-openspec/materialize/active.js +27 -22
- package/dist/commands/migrate-openspec/materialize/active.js.map +1 -1
- package/dist/commands/migrate-openspec/materialize/stage.js +4 -3
- package/dist/commands/migrate-openspec/materialize/stage.js.map +1 -1
- package/dist/commands/migrate-openspec/materialize/target.js +2 -2
- package/dist/commands/migrate-openspec/materialize/target.js.map +1 -1
- package/dist/commands/migrate-openspec/openspec/decisions.js +9 -14
- package/dist/commands/migrate-openspec/openspec/decisions.js.map +1 -1
- package/dist/commands/migrate-openspec/openspec/error.js +7 -8
- package/dist/commands/migrate-openspec/openspec/error.js.map +1 -1
- package/dist/commands/migrate-openspec/openspec/mapping.js +1 -9
- package/dist/commands/migrate-openspec/openspec/mapping.js.map +1 -1
- package/dist/commands/migrate-openspec/openspec/paths.js +3 -6
- package/dist/commands/migrate-openspec/openspec/paths.js.map +1 -1
- package/dist/commands/new/capability-template.js +118 -0
- package/dist/commands/new/capability-template.js.map +1 -0
- package/dist/commands/new/commit.js +8 -22
- package/dist/commands/new/commit.js.map +1 -1
- package/dist/commands/new/modifies.js +206 -0
- package/dist/commands/new/modifies.js.map +1 -0
- package/dist/commands/new/new.js +59 -14
- package/dist/commands/new/new.js.map +1 -1
- package/dist/commands/new/templates.js +48 -124
- package/dist/commands/new/templates.js.map +1 -1
- package/dist/commands/open.js +3 -7
- package/dist/commands/open.js.map +1 -1
- package/dist/commands/policy/gate.js +27 -0
- package/dist/commands/policy/gate.js.map +1 -1
- package/dist/commands/rebase/plan.js +2 -2
- package/dist/commands/rebase/plan.js.map +1 -1
- package/dist/commands/rebase/rebase.js +60 -35
- package/dist/commands/rebase/rebase.js.map +1 -1
- package/dist/commands/seed/commit.js +21 -25
- package/dist/commands/seed/commit.js.map +1 -1
- package/dist/commands/seed/seed.js +6 -9
- package/dist/commands/seed/seed.js.map +1 -1
- package/dist/commands/show/show.js +3 -7
- package/dist/commands/show/show.js.map +1 -1
- package/dist/commands/status/print.js +13 -4
- package/dist/commands/status/print.js.map +1 -1
- package/dist/commands/status/status.js +3 -7
- package/dist/commands/status/status.js.map +1 -1
- package/dist/commands/steps/steps.js +3 -7
- package/dist/commands/steps/steps.js.map +1 -1
- package/dist/commands/subsystem/edit.js +3 -5
- package/dist/commands/subsystem/edit.js.map +1 -1
- package/dist/commands/subsystem/move.js +5 -8
- package/dist/commands/subsystem/move.js.map +1 -1
- package/dist/commands/subsystem/query.js +2 -4
- package/dist/commands/subsystem/query.js.map +1 -1
- package/dist/commands/subsystem/subsystem.js +4 -7
- package/dist/commands/subsystem/subsystem.js.map +1 -1
- package/dist/commands/subsystem/sync.js +14 -41
- package/dist/commands/subsystem/sync.js.map +1 -1
- package/dist/commands/subsystem/txn/txn.js +17 -38
- package/dist/commands/subsystem/txn/txn.js.map +1 -1
- package/dist/commands/subsystem/txn/views.js +32 -5
- package/dist/commands/subsystem/txn/views.js.map +1 -1
- package/dist/commands/tasks/render.js +116 -0
- package/dist/commands/tasks/render.js.map +1 -0
- package/dist/commands/tasks/tasks.js +113 -0
- package/dist/commands/tasks/tasks.js.map +1 -0
- package/dist/commands/unarchive/manifest.js +6 -9
- package/dist/commands/unarchive/manifest.js.map +1 -1
- package/dist/commands/unarchive/restore.js +17 -28
- package/dist/commands/unarchive/restore.js.map +1 -1
- package/dist/commands/unarchive/unarchive.js +22 -30
- package/dist/commands/unarchive/unarchive.js.map +1 -1
- package/dist/commands/validate/arch-coverage.js +6 -15
- package/dist/commands/validate/arch-coverage.js.map +1 -1
- package/dist/commands/validate/checks/fleet-shape.js +4 -49
- package/dist/commands/validate/checks/fleet-shape.js.map +1 -1
- package/dist/commands/validate/checks/requirements.js +18 -8
- package/dist/commands/validate/checks/requirements.js.map +1 -1
- package/dist/commands/validate/checks/sources.js +6 -2
- package/dist/commands/validate/checks/sources.js.map +1 -1
- package/dist/commands/validate/feature.js +23 -19
- package/dist/commands/validate/feature.js.map +1 -1
- package/dist/commands/validate/fleet/landscape.js +23 -23
- package/dist/commands/validate/fleet/landscape.js.map +1 -1
- package/dist/commands/validate/fleet/map/attest.js +6 -10
- package/dist/commands/validate/fleet/map/attest.js.map +1 -1
- package/dist/commands/validate/fleet/map/bindings.js +1 -3
- package/dist/commands/validate/fleet/map/bindings.js.map +1 -1
- package/dist/commands/validate/fleet/map/usecases.js +5 -11
- package/dist/commands/validate/fleet/map/usecases.js.map +1 -1
- package/dist/commands/validate/fleet/obligations.js +1 -5
- package/dist/commands/validate/fleet/obligations.js.map +1 -1
- package/dist/commands/validate/fleet/scorecard/print.js +5 -1
- package/dist/commands/validate/fleet/scorecard/print.js.map +1 -1
- package/dist/commands/validate/fleet/scorecard/scorecard.js +17 -11
- package/dist/commands/validate/fleet/scorecard/scorecard.js.map +1 -1
- package/dist/commands/validate/fleet/usecases/usecases.js +6 -4
- package/dist/commands/validate/fleet/usecases/usecases.js.map +1 -1
- package/dist/commands/validate/fleet/views/fleet-project.js.map +1 -1
- package/dist/commands/validate/fleet/views/root-documents.js +8 -28
- package/dist/commands/validate/fleet/views/root-documents.js.map +1 -1
- package/dist/commands/validate/links/corpus.js +1 -4
- package/dist/commands/validate/links/corpus.js.map +1 -1
- package/dist/commands/validate/links/glossary.js.map +1 -1
- package/dist/commands/validate/report.js +5 -4
- package/dist/commands/validate/report.js.map +1 -1
- package/dist/commands/validate/scope.js +113 -0
- package/dist/commands/validate/scope.js.map +1 -0
- package/dist/commands/validate/service/api.js +16 -104
- package/dist/commands/validate/service/api.js.map +1 -1
- package/dist/commands/validate/service/contracts/generated.js +134 -0
- package/dist/commands/validate/service/contracts/generated.js.map +1 -0
- package/dist/commands/validate/service/events/events.js +1 -1
- package/dist/commands/validate/service/events/events.js.map +1 -1
- package/dist/commands/validate/service/events/message-producers.js +1 -1
- package/dist/commands/validate/service/events/message-producers.js.map +1 -1
- package/dist/commands/validate/service/evidence-pins.js +56 -10
- package/dist/commands/validate/service/evidence-pins.js.map +1 -1
- package/dist/commands/validate/service/service.js +16 -26
- package/dist/commands/validate/service/service.js.map +1 -1
- package/dist/commands/validate/service/specs.js +31 -38
- package/dist/commands/validate/service/specs.js.map +1 -1
- package/dist/commands/validate/service/spine.js +8 -11
- package/dist/commands/validate/service/spine.js.map +1 -1
- package/dist/commands/validate/service/usecases/usecases.js +3 -3
- package/dist/commands/validate/service/usecases/usecases.js.map +1 -1
- package/dist/commands/validate/validate.js +44 -26
- package/dist/commands/validate/validate.js.map +1 -1
- package/dist/commands/verify/cross/diff.js +2 -6
- package/dist/commands/verify/cross/diff.js.map +1 -1
- package/dist/commands/verify/evidence/contract.js +1 -1
- package/dist/commands/verify/evidence/contract.js.map +1 -1
- package/dist/commands/verify/evidence/read.js +12 -3
- package/dist/commands/verify/evidence/read.js.map +1 -1
- package/dist/commands/verify/frozen.js +45 -23
- package/dist/commands/verify/frozen.js.map +1 -1
- package/dist/commands/verify/record.js +69 -17
- package/dist/commands/verify/record.js.map +1 -1
- package/dist/commands/verify/report.js +27 -44
- package/dist/commands/verify/report.js.map +1 -1
- package/dist/commands/verify/results.js +112 -30
- package/dist/commands/verify/results.js.map +1 -1
- package/dist/commands/verify/verify.js +54 -51
- package/dist/commands/verify/verify.js.map +1 -1
- package/dist/commands/vouch/pack/pack.js +2 -2
- package/dist/commands/vouch/pack/pack.js.map +1 -1
- package/dist/commands/vouch/pack/print.js +1 -6
- package/dist/commands/vouch/pack/print.js.map +1 -1
- package/dist/commands/vouch/pack/sections.js +1 -2
- package/dist/commands/vouch/pack/sections.js.map +1 -1
- package/dist/commands/vouch/run.js +26 -81
- package/dist/commands/vouch/run.js.map +1 -1
- package/dist/commands/vouch/sample/plan.js +20 -34
- package/dist/commands/vouch/sample/plan.js.map +1 -1
- package/dist/commands/vouch/vet/verify.js +39 -7
- package/dist/commands/vouch/vet/verify.js.map +1 -1
- package/dist/commands/vouch/vouch.js +10 -17
- package/dist/commands/vouch/vouch.js.map +1 -1
- package/dist/core/agent/agents-md/command-map.js +56 -101
- package/dist/core/agent/agents-md/command-map.js.map +1 -1
- package/dist/core/agent/agents-md/map/explain.js +4 -3
- package/dist/core/agent/agents-md/map/explain.js.map +1 -1
- package/dist/core/agent/agents-md/map/lenses/context.js +4 -3
- package/dist/core/agent/agents-md/map/lenses/context.js.map +1 -1
- package/dist/core/agent/agents-md/map/lenses/diff.js +33 -20
- package/dist/core/agent/agents-md/map/lenses/diff.js.map +1 -1
- package/dist/core/agent/agents-md/map/lenses/gate.js +13 -5
- package/dist/core/agent/agents-md/map/lenses/gate.js.map +1 -1
- package/dist/core/agent/agents-md/map/mcp.js +10 -7
- package/dist/core/agent/agents-md/map/mcp.js.map +1 -1
- package/dist/core/agent/agents-md/map/status.js +9 -3
- package/dist/core/agent/agents-md/map/status.js.map +1 -1
- package/dist/core/agent/agents-md/map/subsystem.js +4 -3
- package/dist/core/agent/agents-md/map/subsystem.js.map +1 -1
- package/dist/core/agent/agents-md/map/usecases.js +24 -6
- package/dist/core/agent/agents-md/map/usecases.js.map +1 -1
- package/dist/core/agent/agents-md/refusals.js +32 -18
- package/dist/core/agent/agents-md/refusals.js.map +1 -1
- package/dist/core/agent/agents-md.js +9 -3
- package/dist/core/agent/agents-md.js.map +1 -1
- package/dist/core/agent/agents-stamp.js +10 -0
- package/dist/core/agent/agents-stamp.js.map +1 -1
- package/dist/core/agent/scaffold.js +10 -15
- package/dist/core/agent/scaffold.js.map +1 -1
- package/dist/core/agent/tools/dialects.js +1 -0
- package/dist/core/agent/tools/dialects.js.map +1 -1
- package/dist/core/agent/tools/registry.js +41 -76
- package/dist/core/agent/tools/registry.js.map +1 -1
- package/dist/core/agent/workflows/adopt.js +5 -3
- package/dist/core/agent/workflows/adopt.js.map +1 -1
- package/dist/core/agent/workflows/check.js +20 -16
- package/dist/core/agent/workflows/check.js.map +1 -1
- package/dist/core/agent/workflows/closing.js +54 -21
- package/dist/core/agent/workflows/closing.js.map +1 -1
- package/dist/core/agent/workflows/feature.js +79 -11
- package/dist/core/agent/workflows/feature.js.map +1 -1
- package/dist/core/agent/workflows/implement.js +91 -13
- package/dist/core/agent/workflows/implement.js.map +1 -1
- package/dist/core/agent/workflows/reference/authoring.js +3 -1
- package/dist/core/agent/workflows/reference/authoring.js.map +1 -1
- package/dist/core/agent/workflows/reference/codes.js +8 -6
- package/dist/core/agent/workflows/reference/codes.js.map +1 -1
- package/dist/core/agent/workflows/reference/done-check.js +37 -15
- package/dist/core/agent/workflows/reference/done-check.js.map +1 -1
- package/dist/core/agent/workflows/reference/reference.js.map +1 -1
- package/dist/core/agent/workflows/reference/spine.js +50 -18
- package/dist/core/agent/workflows/reference/spine.js.map +1 -1
- package/dist/core/asyncapi/baseline/plan.js +14 -81
- package/dist/core/asyncapi/baseline/plan.js.map +1 -1
- package/dist/core/asyncapi/digest.js +30 -25
- package/dist/core/asyncapi/digest.js.map +1 -1
- package/dist/core/asyncapi/merge/components.js +5 -4
- package/dist/core/asyncapi/merge/components.js.map +1 -1
- package/dist/core/asyncapi/merge/error.js +7 -2
- package/dist/core/asyncapi/merge/error.js.map +1 -1
- package/dist/core/asyncapi/merge/markers.js +30 -50
- package/dist/core/asyncapi/merge/markers.js.map +1 -1
- package/dist/core/asyncapi/merge/merge.js +31 -53
- package/dist/core/asyncapi/merge/merge.js.map +1 -1
- package/dist/core/asyncapi/merge/pin.js +10 -36
- package/dist/core/asyncapi/merge/pin.js.map +1 -1
- package/dist/core/asyncapi/model.js.map +1 -1
- package/dist/core/asyncapi/read.js +36 -12
- package/dist/core/asyncapi/read.js.map +1 -1
- package/dist/core/brief/brief.js +6 -18
- package/dist/core/brief/brief.js.map +1 -1
- package/dist/core/brief/landscape.js +8 -12
- package/dist/core/brief/landscape.js.map +1 -1
- package/dist/core/brief/map/extend.js +8 -5
- package/dist/core/brief/map/extend.js.map +1 -1
- package/dist/core/brief/map/owed.js +8 -26
- package/dist/core/brief/map/owed.js.map +1 -1
- package/dist/core/brief/targets.js +15 -5
- package/dist/core/brief/targets.js.map +1 -1
- package/dist/core/c4/likec4.js +147 -16
- package/dist/core/c4/likec4.js.map +1 -1
- package/dist/core/c4/locate/unlocated.js +150 -0
- package/dist/core/c4/locate/unlocated.js.map +1 -0
- package/dist/core/c4/parsed/values.js +54 -1
- package/dist/core/c4/parsed/values.js.map +1 -1
- package/dist/core/c4/prefetch/fleet.js +1 -9
- package/dist/core/c4/prefetch/fleet.js.map +1 -1
- package/dist/core/c4/project/documents.js +31 -6
- package/dist/core/c4/project/documents.js.map +1 -1
- package/dist/core/c4/project/load.js +6 -17
- package/dist/core/c4/project/load.js.map +1 -1
- package/dist/core/c4/resolve/external.js +51 -0
- package/dist/core/c4/resolve/external.js.map +1 -0
- package/dist/core/c4/resolve/service.js +25 -1
- package/dist/core/c4/resolve/service.js.map +1 -1
- package/dist/core/c4/seed/template.js +15 -1
- package/dist/core/c4/seed/template.js.map +1 -1
- package/dist/core/c4/service-model/shape.js +2 -22
- package/dist/core/c4/service-model/shape.js.map +1 -1
- package/dist/core/c4/source-mask.js +288 -4
- package/dist/core/c4/source-mask.js.map +1 -1
- package/dist/core/c4/source-scan.js +203 -147
- package/dist/core/c4/source-scan.js.map +1 -1
- package/dist/core/c4/splice/authored-source.js +19 -0
- package/dist/core/c4/splice/authored-source.js.map +1 -1
- package/dist/core/c4/splice/delta-blocks.js +5 -18
- package/dist/core/c4/splice/delta-blocks.js.map +1 -1
- package/dist/core/c4/splice/landscape-merge.js +13 -8
- package/dist/core/c4/splice/landscape-merge.js.map +1 -1
- package/dist/core/c4/splice/model/merge.js +10 -6
- package/dist/core/c4/splice/model/merge.js.map +1 -1
- package/dist/core/c4/splice/placement.js +108 -18
- package/dist/core/c4/splice/placement.js.map +1 -1
- package/dist/core/c4/workspace.js +13 -22
- package/dist/core/c4/workspace.js.map +1 -1
- package/dist/core/capabilities/capabilities.js +6 -6
- package/dist/core/capabilities/capabilities.js.map +1 -1
- package/dist/core/capabilities/delta/doc.js +7 -10
- package/dist/core/capabilities/delta/doc.js.map +1 -1
- package/dist/core/capabilities/findings.js +15 -20
- package/dist/core/capabilities/findings.js.map +1 -1
- package/dist/core/capabilities/realizes/findings.js +12 -19
- package/dist/core/capabilities/realizes/findings.js.map +1 -1
- package/dist/core/capabilities/realizes/join.js +0 -20
- package/dist/core/capabilities/realizes/join.js.map +1 -1
- package/dist/core/capabilities/rollup.js +6 -0
- package/dist/core/capabilities/rollup.js.map +1 -1
- package/dist/core/capabilities/tree.js +6 -28
- package/dist/core/capabilities/tree.js.map +1 -1
- package/dist/core/capabilities/usecase-join.js +3 -13
- package/dist/core/capabilities/usecase-join.js.map +1 -1
- package/dist/core/coherence/authoring/scaffold.js +11 -3
- package/dist/core/coherence/authoring/scaffold.js.map +1 -1
- package/dist/core/coherence/authoring/sentinels.js +2 -1
- package/dist/core/coherence/authoring/sentinels.js.map +1 -1
- package/dist/core/coherence/coherence.js +30 -16
- package/dist/core/coherence/coherence.js.map +1 -1
- package/dist/core/coherence/consumed/operations.js +70 -0
- package/dist/core/coherence/consumed/operations.js.map +1 -0
- package/dist/core/coherence/consumed/refusal.js +38 -0
- package/dist/core/coherence/consumed/refusal.js.map +1 -0
- package/dist/core/coherence/declared.js +60 -34
- package/dist/core/coherence/declared.js.map +1 -1
- package/dist/core/coherence/events/declared.js +44 -59
- package/dist/core/coherence/events/declared.js.map +1 -1
- package/dist/core/coherence/events/events.js +119 -8
- package/dist/core/coherence/events/events.js.map +1 -1
- package/dist/core/coherence/events/grades.js +31 -8
- package/dist/core/coherence/events/grades.js.map +1 -1
- package/dist/core/coherence/events/lookups.js +14 -82
- package/dist/core/coherence/events/lookups.js.map +1 -1
- package/dist/core/coherence/lookups.js +18 -78
- package/dist/core/coherence/lookups.js.map +1 -1
- package/dist/core/coherence/retired/archived.js +285 -0
- package/dist/core/coherence/retired/archived.js.map +1 -0
- package/dist/core/coherence/retired/event-pair.js +62 -0
- package/dist/core/coherence/retired/event-pair.js.map +1 -0
- package/dist/core/coherence/retired/merged.js +79 -0
- package/dist/core/coherence/retired/merged.js.map +1 -0
- package/dist/core/coherence/retired/retirement.js +49 -0
- package/dist/core/coherence/retired/retirement.js.map +1 -0
- package/dist/core/coherence/retired/revival/arrivals.js +131 -0
- package/dist/core/coherence/retired/revival/arrivals.js.map +1 -0
- package/dist/core/coherence/retired/revival/revived.js +297 -0
- package/dist/core/coherence/retired/revival/revived.js.map +1 -0
- package/dist/core/consumers/after.js +113 -0
- package/dist/core/consumers/after.js.map +1 -0
- package/dist/core/consumers/consumers.js +296 -0
- package/dist/core/consumers/consumers.js.map +1 -0
- package/dist/core/consumers/current.js +211 -0
- package/dist/core/consumers/current.js.map +1 -0
- package/dist/core/consumers/scan.js +46 -0
- package/dist/core/consumers/scan.js.map +1 -0
- package/dist/core/consumers/state.js +27 -0
- package/dist/core/consumers/state.js.map +1 -0
- package/dist/core/delta/delta.js +1 -1
- package/dist/core/delta/delta.js.map +1 -1
- package/dist/core/deployment/delta.js +3 -6
- package/dist/core/deployment/delta.js.map +1 -1
- package/dist/core/diff/base-git.js +94 -12
- package/dist/core/diff/base-git.js.map +1 -1
- package/dist/core/diff/base-state.js +28 -16
- package/dist/core/diff/base-state.js.map +1 -1
- package/dist/core/diff/joins.js +136 -0
- package/dist/core/diff/joins.js.map +1 -0
- package/dist/core/diff/scope/changed-paths.js +22 -6
- package/dist/core/diff/scope/changed-paths.js.map +1 -1
- package/dist/core/diff/scope/edges.js +82 -0
- package/dist/core/diff/scope/edges.js.map +1 -0
- package/dist/core/diff/semantic.js +100 -31
- package/dist/core/diff/semantic.js.map +1 -1
- package/dist/core/doctor/agents.js +34 -18
- package/dist/core/doctor/agents.js.map +1 -1
- package/dist/core/doctor/report.js.map +1 -1
- package/dist/core/document/apply.js +10 -2
- package/dist/core/document/apply.js.map +1 -1
- package/dist/core/document/frontmatter.js +55 -0
- package/dist/core/document/frontmatter.js.map +1 -1
- package/dist/core/document/parse.js +29 -6
- package/dist/core/document/parse.js.map +1 -1
- package/dist/core/document/scenarios.js +17 -0
- package/dist/core/document/scenarios.js.map +1 -1
- package/dist/core/document/spec.js +4 -11
- package/dist/core/document/spec.js.map +1 -1
- package/dist/core/envelope/config.js +10 -0
- package/dist/core/envelope/config.js.map +1 -1
- package/dist/core/envelope/json.js +34 -0
- package/dist/core/envelope/json.js.map +1 -1
- package/dist/core/explain/families.js +17 -12
- package/dist/core/explain/families.js.map +1 -1
- package/dist/core/explain/refusals.js +3 -3
- package/dist/core/explain/refusals.js.map +1 -1
- package/dist/core/explain/terms.js +18 -9
- package/dist/core/explain/terms.js.map +1 -1
- package/dist/core/explore/explore.js +2 -7
- package/dist/core/explore/explore.js.map +1 -1
- package/dist/core/fleet-context.js +34 -95
- package/dist/core/fleet-context.js.map +1 -1
- package/dist/core/gate/checks.js +5 -2
- package/dist/core/gate/checks.js.map +1 -1
- package/dist/core/gate/partners.js +4 -2
- package/dist/core/gate/partners.js.map +1 -1
- package/dist/core/gate/report.js.map +1 -1
- package/dist/core/gate/verification.js +75 -12
- package/dist/core/gate/verification.js.map +1 -1
- package/dist/core/gherkin/stale.js +13 -37
- package/dist/core/gherkin/stale.js.map +1 -1
- package/dist/core/glossary/tree.js +6 -19
- package/dist/core/glossary/tree.js.map +1 -1
- package/dist/core/kernel/concurrency.js +19 -0
- package/dist/core/kernel/concurrency.js.map +1 -1
- package/dist/core/kernel/records.js +11 -0
- package/dist/core/kernel/records.js.map +1 -1
- package/dist/core/links/backlinks.js +1 -3
- package/dist/core/links/backlinks.js.map +1 -1
- package/dist/core/links/corpus.js +5 -36
- package/dist/core/links/corpus.js.map +1 -1
- package/dist/core/mcp/argv.js +101 -0
- package/dist/core/mcp/argv.js.map +1 -1
- package/dist/core/mcp/author-tools.js +2 -1
- package/dist/core/mcp/author-tools.js.map +1 -1
- package/dist/core/mcp/protocol.js +2 -2
- package/dist/core/mcp/protocol.js.map +1 -1
- package/dist/core/mcp/tools.js +43 -33
- package/dist/core/mcp/tools.js.map +1 -1
- package/dist/core/openapi/baseline/gate.js +53 -12
- package/dist/core/openapi/baseline/gate.js.map +1 -1
- package/dist/core/openapi/baseline/plan.js +22 -8
- package/dist/core/openapi/baseline/plan.js.map +1 -1
- package/dist/core/openapi/baseline/record.js +0 -1
- package/dist/core/openapi/baseline/record.js.map +1 -1
- package/dist/core/openapi/contract-text.js +33 -0
- package/dist/core/openapi/contract-text.js.map +1 -0
- package/dist/core/openapi/contract-tree.js +56 -0
- package/dist/core/openapi/contract-tree.js.map +1 -0
- package/dist/core/openapi/depth.js +38 -24
- package/dist/core/openapi/depth.js.map +1 -1
- package/dist/core/openapi/digest.js +52 -2
- package/dist/core/openapi/digest.js.map +1 -1
- package/dist/core/openapi/doc.js +12 -4
- package/dist/core/openapi/doc.js.map +1 -1
- package/dist/core/openapi/merge/components.js +25 -60
- package/dist/core/openapi/merge/components.js.map +1 -1
- package/dist/core/openapi/merge/error.js +3 -13
- package/dist/core/openapi/merge/error.js.map +1 -1
- package/dist/core/openapi/merge/markers.js +26 -30
- package/dist/core/openapi/merge/markers.js.map +1 -1
- package/dist/core/openapi/merge/merge.js +26 -52
- package/dist/core/openapi/merge/merge.js.map +1 -1
- package/dist/core/openapi/merge/pin.js +15 -35
- package/dist/core/openapi/merge/pin.js.map +1 -1
- package/dist/core/openspec/decide/verdict.js.map +1 -1
- package/dist/core/openspec/inventory.js +4 -5
- package/dist/core/openspec/inventory.js.map +1 -1
- package/dist/core/openspec/model/mapping.js +30 -28
- package/dist/core/openspec/model/mapping.js.map +1 -1
- package/dist/core/openspec/model/model.js +12 -7
- package/dist/core/openspec/model/model.js.map +1 -1
- package/dist/core/openspec/scan/changes.js +10 -4
- package/dist/core/openspec/scan/changes.js.map +1 -1
- package/dist/core/openspec/scan/config.js +7 -16
- package/dist/core/openspec/scan/config.js.map +1 -1
- package/dist/core/openspec/scan/shape.js +63 -19
- package/dist/core/openspec/scan/shape.js.map +1 -1
- package/dist/core/openspec/scan/walk.js +21 -38
- package/dist/core/openspec/scan/walk.js.map +1 -1
- package/dist/core/pack/living.js +2 -16
- package/dist/core/pack/living.js.map +1 -1
- package/dist/core/provenance/findings.js +48 -12
- package/dist/core/provenance/findings.js.map +1 -1
- package/dist/core/provenance/git.js +33 -57
- package/dist/core/provenance/git.js.map +1 -1
- package/dist/core/provenance/gitq/moves.js +71 -114
- package/dist/core/provenance/gitq/moves.js.map +1 -1
- package/dist/core/provenance/gitq/vouched-ancestor.js +2 -2
- package/dist/core/provenance/gitq/vouched-ancestor.js.map +1 -1
- package/dist/core/provenance/sources.js +23 -6
- package/dist/core/provenance/sources.js.map +1 -1
- package/dist/core/provenance/stamp.js +5 -17
- package/dist/core/provenance/stamp.js.map +1 -1
- package/dist/core/repo/entries.js.map +1 -1
- package/dist/core/repo/repo.js +24 -14
- package/dist/core/repo/repo.js.map +1 -1
- package/dist/core/repo/tree/find.js +12 -3
- package/dist/core/repo/tree/find.js.map +1 -1
- package/dist/core/repo/tree/fs.js +27 -1
- package/dist/core/repo/tree/fs.js.map +1 -1
- package/dist/core/repo/tree/walk.js +8 -30
- package/dist/core/repo/tree/walk.js.map +1 -1
- package/dist/core/results.js +47 -12
- package/dist/core/results.js.map +1 -1
- package/dist/core/staging/commit.js +10 -12
- package/dist/core/staging/commit.js.map +1 -1
- package/dist/core/staging/lock.js +74 -12
- package/dist/core/staging/lock.js.map +1 -1
- package/dist/core/staging/recovery/intent.js +2 -5
- package/dist/core/staging/recovery/intent.js.map +1 -1
- package/dist/core/staging/recovery/recover.js +60 -1
- package/dist/core/staging/recovery/recover.js.map +1 -1
- package/dist/core/staging/snapshot/manifest.js +152 -0
- package/dist/core/staging/snapshot/manifest.js.map +1 -0
- package/dist/core/staging/{snapshot.js → snapshot/snapshot.js} +10 -34
- package/dist/core/staging/snapshot/snapshot.js.map +1 -0
- package/dist/core/status/actions/execution.js +11 -0
- package/dist/core/status/actions/execution.js.map +1 -1
- package/dist/core/status/feature/artifacts.js +2 -2
- package/dist/core/status/feature/artifacts.js.map +1 -1
- package/dist/core/status/feature/feature.js +5 -11
- package/dist/core/status/feature/feature.js.map +1 -1
- package/dist/core/status/fleet/fleet.js +6 -2
- package/dist/core/status/fleet/fleet.js.map +1 -1
- package/dist/core/status/fleet/next.js +26 -3
- package/dist/core/status/fleet/next.js.map +1 -1
- package/dist/core/status/report.js.map +1 -1
- package/dist/core/status/verification.js +30 -15
- package/dist/core/status/verification.js.map +1 -1
- package/dist/core/tasks/packet.js +174 -0
- package/dist/core/tasks/packet.js.map +1 -0
- package/dist/core/tasks/record.js +43 -0
- package/dist/core/tasks/record.js.map +1 -0
- package/dist/core/tasks/sources.js +181 -0
- package/dist/core/tasks/sources.js.map +1 -0
- package/dist/core/tasks/stamps.js +76 -0
- package/dist/core/tasks/stamps.js.map +1 -0
- package/dist/core/tasks/unit.js +0 -0
- package/dist/core/tasks/unit.js.map +1 -0
- package/dist/core/usecases/capability.js +11 -14
- package/dist/core/usecases/capability.js.map +1 -1
- package/dist/core/usecases/delta/flows.js +5 -7
- package/dist/core/usecases/delta/flows.js.map +1 -1
- package/dist/core/usecases/fleet.js +178 -31
- package/dist/core/usecases/fleet.js.map +1 -1
- package/dist/core/usecases/operations.js +5 -4
- package/dist/core/usecases/operations.js.map +1 -1
- package/dist/core/usecases/service/flows.js +9 -6
- package/dist/core/usecases/service/flows.js.map +1 -1
- package/dist/core/usecases/suites/participants.js +83 -0
- package/dist/core/usecases/suites/participants.js.map +1 -0
- package/dist/core/usecases/suites/suites.js +45 -0
- package/dist/core/usecases/suites/suites.js.map +1 -0
- package/dist/core/verify/answers.js +12 -5
- package/dist/core/verify/answers.js.map +1 -1
- package/dist/core/verify/build.js +102 -82
- package/dist/core/verify/build.js.map +1 -1
- package/dist/core/verify/checklist.js +3 -0
- package/dist/core/verify/checklist.js.map +1 -1
- package/dist/core/verify/location.js +67 -0
- package/dist/core/verify/location.js.map +1 -0
- package/dist/core/verify/pins/lint.js +3 -2
- package/dist/core/verify/pins/lint.js.map +1 -1
- package/dist/core/verify/pins/pin.js +1 -1
- package/dist/core/verify/pins/pin.js.map +1 -1
- package/dist/core/verify/read/file.js +224 -0
- package/dist/core/verify/read/file.js.map +1 -0
- package/dist/core/verify/{file.js → read/shape.js} +16 -104
- package/dist/core/verify/read/shape.js.map +1 -0
- package/dist/core/verify/read/union.js +105 -0
- package/dist/core/verify/read/union.js.map +1 -0
- package/dist/core/verify/record.js +35 -12
- package/dist/core/verify/record.js.map +1 -1
- package/dist/core/verify/store/commit.js +40 -30
- package/dist/core/verify/store/commit.js.map +1 -1
- package/dist/core/verify/store/render.js +8 -4
- package/dist/core/verify/store/render.js.map +1 -1
- package/dist/core/vocabulary/issue.js +1 -0
- package/dist/core/vocabulary/issue.js.map +1 -1
- package/dist/core/vocabulary/maturity.js +12 -2
- package/dist/core/vocabulary/maturity.js.map +1 -1
- package/examples/README.md +21 -6
- package/examples/docs/architecture/landscape.likec4 +8 -0
- package/examples/docs/architecture/usecases/order-notification.likec4 +6 -2
- package/examples/docs/features/FEAT-101-payment-splitting/specs/payment-service/asyncapi.yaml +11 -8
- package/examples/docs/features/FEAT-101-payment-splitting/specs/payment-split-service/openapi.yaml +6 -6
- package/examples/docs/features/archive/FEAT-088-refunds/specs/payment-service/openapi.yaml +13 -10
- package/examples/docs/services/order-service/asyncapi.yaml +3 -2
- package/examples/docs/services/platform/notification-service/asyncapi.yaml +2 -1
- package/package.json +1 -1
- package/dist/commands/validate/checks/vocabulary.js +0 -16
- package/dist/commands/validate/checks/vocabulary.js.map +0 -1
- package/dist/core/agent/agents-md/artifacts.js +0 -221
- package/dist/core/agent/agents-md/artifacts.js.map +0 -1
- package/dist/core/agent/agents-md/cycle.js +0 -180
- package/dist/core/agent/agents-md/cycle.js.map +0 -1
- package/dist/core/agent/agents-md/shipped/archive-gate.js +0 -98
- package/dist/core/agent/agents-md/shipped/archive-gate.js.map +0 -1
- package/dist/core/agent/agents-md/spine.js +0 -40
- package/dist/core/agent/agents-md/spine.js.map +0 -1
- package/dist/core/coherence/events/merged.js +0 -40
- package/dist/core/coherence/events/merged.js.map +0 -1
- package/dist/core/diff/current-state.js +0 -96
- package/dist/core/diff/current-state.js.map +0 -1
- package/dist/core/diff/victims.js +0 -256
- package/dist/core/diff/victims.js.map +0 -1
- package/dist/core/staging/snapshot.js.map +0 -1
- package/dist/core/verify/file.js.map +0 -1
|
@@ -1,83 +1,17 @@
|
|
|
1
1
|
/**
|
|
2
|
-
*
|
|
2
|
+
* What a file must hold to be a verification record — the shape check and the
|
|
3
|
+
* self-consistency check every file of the record passes on the way in.
|
|
3
4
|
*
|
|
4
|
-
*
|
|
5
|
-
*
|
|
6
|
-
*
|
|
7
|
-
*
|
|
8
|
-
*
|
|
9
|
-
* produces exactly the bytes `store/commit.ts` swaps into place; races and
|
|
10
|
-
* kills mid-write live there too), and the lock that serialises writers is
|
|
11
|
-
* taken by `commands/verify/verify.ts` before this module's authoritative
|
|
12
|
-
* read. Nothing here touches the filesystem except to read.
|
|
5
|
+
* Moved out of `./file.ts` when the record became several files: that module
|
|
6
|
+
* now owns WHICH files make up a record and how a damaged one is reported,
|
|
7
|
+
* while this one owns what any single file has to say to be read at all. The
|
|
8
|
+
* two are phases — find the files, then grade each — and the grading never
|
|
9
|
+
* needed to know where its bytes came from.
|
|
13
10
|
*/
|
|
14
|
-
import {
|
|
15
|
-
import {
|
|
16
|
-
import {
|
|
17
|
-
import {
|
|
18
|
-
import { isEvidencePinList } from "./pins/pin.js";
|
|
19
|
-
import { tallyAnswers, verificationPath } from "./record.js";
|
|
20
|
-
export async function readVerificationState(featureDir) {
|
|
21
|
-
const path = verificationPath(featureDir);
|
|
22
|
-
let raw;
|
|
23
|
-
try {
|
|
24
|
-
raw = await readFile(path);
|
|
25
|
-
}
|
|
26
|
-
catch (err) {
|
|
27
|
-
if (err.code === "ENOENT") {
|
|
28
|
-
// ENOENT is "absent" — except from a symlink whose target is gone, which
|
|
29
|
-
// `existsSync` used to fold into the same answer. That mislabel was a
|
|
30
|
-
// trap: the read said "no record" while the commit's exclusive link(2)
|
|
31
|
-
// saw the link itself and refused EEXIST, so every `--record` answered
|
|
32
|
-
// `record-raced` with advice (re-run) that could never work. The lstat
|
|
33
|
-
// must CONFIRM the link before the message asserts one: a real file can
|
|
34
|
-
// also appear between the two calls, and telling its owner to "remove
|
|
35
|
-
// the link" would delete a valid record.
|
|
36
|
-
let entry;
|
|
37
|
-
try {
|
|
38
|
-
entry = await lstat(path);
|
|
39
|
-
}
|
|
40
|
-
catch {
|
|
41
|
-
return { state: "absent" };
|
|
42
|
-
}
|
|
43
|
-
if (entry.isSymbolicLink()) {
|
|
44
|
-
return {
|
|
45
|
-
state: "unreadable",
|
|
46
|
-
reason: "it is a symlink whose target does not exist — replace the link with a real record, or remove it",
|
|
47
|
-
};
|
|
48
|
-
}
|
|
49
|
-
return {
|
|
50
|
-
state: "unreadable",
|
|
51
|
-
reason: "it appeared while being read — another writer is landing; re-run",
|
|
52
|
-
};
|
|
53
|
-
}
|
|
54
|
-
return { state: "unreadable", reason: err instanceof Error ? err.message : String(err) };
|
|
55
|
-
}
|
|
56
|
-
let doc;
|
|
57
|
-
try {
|
|
58
|
-
doc = parse(raw.toString("utf8"));
|
|
59
|
-
}
|
|
60
|
-
catch (err) {
|
|
61
|
-
return { state: "unreadable", reason: yamlReason(err) };
|
|
62
|
-
}
|
|
63
|
-
const verification = asVerification(doc);
|
|
64
|
-
if (verification === null) {
|
|
65
|
-
return {
|
|
66
|
-
state: "unreadable",
|
|
67
|
-
reason: "it parses as YAML but does not have a verification record's shape (feature, recorded, checklist, summary and claims)",
|
|
68
|
-
};
|
|
69
|
-
}
|
|
70
|
-
const miscount = summaryDisagreement(verification);
|
|
71
|
-
if (miscount === null)
|
|
72
|
-
return { state: "ok", verification, raw };
|
|
73
|
-
return {
|
|
74
|
-
state: "unreadable",
|
|
75
|
-
// Spelled twice on purpose: the field is what a caller branches on, the
|
|
76
|
-
// prose is what a terminal prints and what somebody greps for.
|
|
77
|
-
code: "verify.record-miscounted",
|
|
78
|
-
reason: `verify.record-miscounted: ${miscount}`,
|
|
79
|
-
};
|
|
80
|
-
}
|
|
11
|
+
import { isRecord } from "../../kernel/records.js";
|
|
12
|
+
import { VERDICTS } from "../answers.js";
|
|
13
|
+
import { isEvidencePinList } from "../pins/pin.js";
|
|
14
|
+
import { tallyAnswers } from "../record.js";
|
|
81
15
|
/**
|
|
82
16
|
* Does the record's `summary` add up from its own `claims[]`?
|
|
83
17
|
*
|
|
@@ -89,7 +23,7 @@ export async function readVerificationState(featureDir) {
|
|
|
89
23
|
* record that contradicts itself is refused like any other file loam cannot
|
|
90
24
|
* use. The code is in the reason: it travels wherever the reason is printed.
|
|
91
25
|
*/
|
|
92
|
-
function summaryDisagreement(v) {
|
|
26
|
+
export function summaryDisagreement(v) {
|
|
93
27
|
const t = tallyAnswers(v.claims);
|
|
94
28
|
const unanswered = v.summary.unanswered ?? 0;
|
|
95
29
|
const says = [];
|
|
@@ -114,31 +48,9 @@ function summaryDisagreement(v) {
|
|
|
114
48
|
? null
|
|
115
49
|
: `its summary ${says.join(", ")} — the record contradicts itself, so neither half can be believed`;
|
|
116
50
|
}
|
|
117
|
-
/** A YAML failure with its line, when the parser located one: that line is the fix. */
|
|
118
|
-
function yamlReason(err) {
|
|
119
|
-
const message = err instanceof Error ? err.message.split("\n")[0].trim() : String(err);
|
|
120
|
-
const pos = err?.linePos;
|
|
121
|
-
const line = Array.isArray(pos) ? pos[0]?.line : undefined;
|
|
122
|
-
return line === undefined ? `YAML error: ${message}` : `YAML error at line ${line}: ${message}`;
|
|
123
|
-
}
|
|
124
|
-
/**
|
|
125
|
-
* The record beside a feature, or null when there is none loam can use. Kept
|
|
126
|
-
* for readers that have nothing to say about the difference (`loam list`'s
|
|
127
|
-
* verification column shows one glyph either way); anything that WRITES must
|
|
128
|
-
* use {@link readVerificationState} instead, or it overwrites what it could not
|
|
129
|
-
* read.
|
|
130
|
-
*
|
|
131
|
-
* A record whose summary contradicts its own claims is one of the nulls now, so
|
|
132
|
-
* no reader can print its counts as fact. Reading it as ABSENT is the safe half
|
|
133
|
-
* of the truth and not the whole of it: a reader that can say why should take
|
|
134
|
-
* {@link readVerificationState} and its `code`.
|
|
135
|
-
*/
|
|
136
|
-
export async function readVerification(featureDir) {
|
|
137
|
-
const read = await readVerificationState(featureDir);
|
|
138
|
-
return read.state === "ok" ? read.verification : null;
|
|
139
|
-
}
|
|
140
51
|
/**
|
|
141
|
-
* The shape check `
|
|
52
|
+
* The shape check `readVerificationState` (`./file.ts`) stands behind:
|
|
53
|
+
* everything its readers
|
|
142
54
|
* dereference (verify's report/frozen views, list's verification column) must
|
|
143
55
|
* be present and typed, or the cast would manufacture a Verification that
|
|
144
56
|
* crashes them. Deliberately no tighter than the readers need: extra keys pass
|
|
@@ -146,7 +58,7 @@ export async function readVerification(featureDir) {
|
|
|
146
58
|
* predate `--results`) and is any string, like `kind` — the readers only ever
|
|
147
59
|
* compare them, and a record written by a newer loam must not read as absent.
|
|
148
60
|
*/
|
|
149
|
-
function asVerification(doc) {
|
|
61
|
+
export function asVerification(doc) {
|
|
150
62
|
if (!isRecord(doc))
|
|
151
63
|
return null;
|
|
152
64
|
const summary = doc["summary"];
|
|
@@ -254,4 +166,4 @@ function isConsumedContractReport(v) {
|
|
|
254
166
|
function isCount(v) {
|
|
255
167
|
return typeof v === "number" && Number.isInteger(v);
|
|
256
168
|
}
|
|
257
|
-
//# sourceMappingURL=
|
|
169
|
+
//# sourceMappingURL=shape.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"shape.js","sourceRoot":"","sources":["../../../../src/core/verify/read/shape.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * What a file must hold to be a verification record — the shape check and the\n * self-consistency check every file of the record passes on the way in.\n *\n * Moved out of `./file.ts` when the record became several files: that module\n * now owns WHICH files make up a record and how a damaged one is reported,\n * while this one owns what any single file has to say to be read at all. The\n * two are phases — find the files, then grade each — and the grading never\n * needed to know where its bytes came from.\n */\nimport { isRecord } from \"../../kernel/records.js\";\nimport { VERDICTS } from \"../answers.js\";\nimport { isEvidencePinList } from \"../pins/pin.js\";\nimport { tallyAnswers, type Verification } from \"../record.js\";\n\n/**\n * Does the record's `summary` add up from its own `claims[]`?\n *\n * The summary is what wrote the file; the claims are what it answered. Nothing\n * ever compared them, so every reader that took the shortcut — verify's frozen\n * verdict for a shipped feature, list's column, status's block — reported a\n * record full of unconfirmed claims as fully confirmed. Believing either side\n * over the other would be a guess about which half was tampered with, so a\n * record that contradicts itself is refused like any other file loam cannot\n * use. The code is in the reason: it travels wherever the reason is printed.\n */\nexport function summaryDisagreement(v: Verification): string | null {\n const t = tallyAnswers(v.claims);\n const unanswered = v.summary.unanswered ?? 0;\n const says: string[] = [];\n // Before the arithmetic, because a negative count defeats it: `unanswered:\n // -2` is never something loam wrote, and it is the one value that lets a\n // hand-edited summary balance its own books — every comparison below then\n // agrees, and a record contradicting its claims reported `verified`.\n for (const [field, value] of Object.entries(v.summary)) {\n if (typeof value === \"number\" && value < 0) says.push(`carries a negative count (${field}: ${value})`);\n }\n if (v.summary.confirmed !== t.confirmed) {\n says.push(`says ${v.summary.confirmed} confirmed where claims[] holds ${t.confirmed}`);\n }\n if (v.summary.unconfirmed !== t.unconfirmed) {\n says.push(`says ${v.summary.unconfirmed} unconfirmed where claims[] holds ${t.unconfirmed}`);\n }\n if (v.summary.claims !== v.claims.length + unanswered) {\n says.push(\n `says ${v.summary.claims} claim(s)${unanswered === 0 ? \"\" : ` including ${unanswered} unanswered`} where claims[] holds ${v.claims.length}`,\n );\n }\n return says.length === 0\n ? null\n : `its summary ${says.join(\", \")} — the record contradicts itself, so neither half can be believed`;\n}\n\n/**\n * The shape check `readVerificationState` (`./file.ts`) stands behind:\n * everything its readers\n * dereference (verify's report/frozen views, list's verification column) must\n * be present and typed, or the cast would manufacture a Verification that\n * crashes them. Deliberately no tighter than the readers need: extra keys pass\n * (a human may annotate), `answered_by` stays optional (absent in records that\n * predate `--results`) and is any string, like `kind` — the readers only ever\n * compare them, and a record written by a newer loam must not read as absent.\n */\nexport function asVerification(doc: unknown): Verification | null {\n if (!isRecord(doc)) return null;\n const summary = doc[\"summary\"];\n if (\n typeof doc[\"feature\"] !== \"string\" ||\n typeof doc[\"recorded\"] !== \"string\" ||\n typeof doc[\"checklist\"] !== \"string\" ||\n !isRecord(summary) ||\n !isCount(summary[\"claims\"]) ||\n !isCount(summary[\"confirmed\"]) ||\n !isCount(summary[\"unconfirmed\"]) ||\n (summary[\"unanswered\"] !== undefined && !isCount(summary[\"unanswered\"])) ||\n !Array.isArray(doc[\"claims\"])\n ) {\n return null;\n }\n for (const c of doc[\"claims\"]) {\n if (!isRecord(c)) return null;\n if (typeof c[\"id\"] !== \"string\" || typeof c[\"kind\"] !== \"string\" || typeof c[\"claim\"] !== \"string\") return null;\n if (c[\"subject\"] !== undefined && typeof c[\"subject\"] !== \"string\") return null;\n if (typeof c[\"verdict\"] !== \"string\" || !(VERDICTS as readonly string[]).includes(c[\"verdict\"])) return null;\n // The frozen view iterates evidence per claim — a scalar here would crash it.\n if (!Array.isArray(c[\"evidence\"]) || c[\"evidence\"].some((e) => typeof e !== \"string\")) return null;\n if (c[\"note\"] !== undefined && typeof c[\"note\"] !== \"string\") return null;\n if (c[\"answered_by\"] !== undefined && typeof c[\"answered_by\"] !== \"string\") return null;\n // Absent fine, present whole — the isConsumedContractReport precedent, and\n // the guard body lives with the pin shape it vouches for (pins/pin.ts). A\n // broken pins block makes the whole record unreadable and therefore never\n // overwritten: skipping it instead would read a hand-damaged record as\n // drift-free.\n if (!isEvidencePinList(c[\"evidence_pins\"])) return null;\n }\n if (doc[\"schema\"] !== undefined && doc[\"schema\"] !== 2) return null;\n if (!isConsumedReport(doc[\"report\"])) return null;\n if (!isConsumedContractReport(doc[\"contractReport\"])) return null;\n if (doc[\"attestations\"] !== undefined) {\n if (!Array.isArray(doc[\"attestations\"])) return null;\n for (const a of doc[\"attestations\"]) {\n if (!isRecord(a)) return null;\n if (typeof a[\"service\"] !== \"string\" || typeof a[\"commit\"] !== \"string\" || typeof a[\"recorded\"] !== \"string\") return null;\n if (!/^[0-9a-f]{40,64}$/i.test(a[\"commit\"])) return null;\n if (!Array.isArray(a[\"claims\"]) || a[\"claims\"].some((id) => typeof id !== \"string\")) return null;\n if (!isConsumedReport(a[\"report\"])) return null;\n if (!isConsumedContractReport(a[\"contractReport\"])) return null;\n }\n }\n return doc as unknown as Verification;\n}\n\n/** Absent is fine — most records answer nothing from a report. Present must be whole: it is printed. */\nfunction isConsumedReport(v: unknown): boolean {\n if (v === undefined) return true;\n return (\n isRecord(v) &&\n typeof v[\"path\"] === \"string\" &&\n typeof v[\"digest\"] === \"string\" &&\n typeof v[\"mtime\"] === \"string\" &&\n isCount(v[\"scenarios\"])\n );\n}\n\n/**\n * The contract report's pin, exactly as loose as {@link isConsumedReport}:\n * absent fine, present whole — for the fields the readers dereference.\n * `format` is OPTIONAL here even though loam always writes it: nothing in the\n * readers dereferences it (it is provenance for a person, echoed only inside\n * whole-object payloads), and the generated AGENTS.md teaches the pin as\n * path/sha256/mtime/operation-count — so a hand-written block faithful to that\n * teaching must not make the WHOLE record unreadable over a field no reader\n * uses. When present it must be a string, and any string: a record written by\n * a newer loam that learned a second report format must not read as\n * unreadable here.\n */\nfunction isConsumedContractReport(v: unknown): boolean {\n if (v === undefined) return true;\n return (\n isRecord(v) &&\n typeof v[\"path\"] === \"string\" &&\n typeof v[\"digest\"] === \"string\" &&\n typeof v[\"mtime\"] === \"string\" &&\n isCount(v[\"operations\"]) &&\n (v[\"format\"] === undefined || typeof v[\"format\"] === \"string\")\n );\n}\n\n/**\n * A summary count: an integer. YAML hands back strings for anything quoted.\n * Negativity is deliberately NOT checked here: a negative count is a summary\n * that disagrees with the claims below it, and `summaryDisagreement` owns that\n * refusal — failing the SHAPE check instead made the message list five keys\n * that were all present, while the miscount branch one call away holds the\n * advice the case actually needs.\n */\nfunction isCount(v: unknown): v is number {\n return typeof v === \"number\" && Number.isInteger(v);\n}\n"]}
|
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The record as the union of its files: the one-file `verification.yaml` and
|
|
3
|
+
* one `verification/<svc>.yaml` per attesting service, added up into the single
|
|
4
|
+
* `Verification` every reader already takes.
|
|
5
|
+
*
|
|
6
|
+
* Pure on purpose — `./file.ts` reads and grades the bytes, the writer in
|
|
7
|
+
* `commands/verify/record.ts` hands in the files as they will stand after its
|
|
8
|
+
* commit, and both get the same answer from the same arithmetic. A writer that
|
|
9
|
+
* computed the post-record verdict its own way would be the second opinion
|
|
10
|
+
* `../record.ts` exists to prevent; this way what `--record` prints is exactly
|
|
11
|
+
* what the next read will say.
|
|
12
|
+
*
|
|
13
|
+
* Nothing here reads a `summary` as fact. Each file's own `summary` was graded
|
|
14
|
+
* against its own `claims[]` on the way in, and the union's is recomputed here
|
|
15
|
+
* from the claims it actually counts — so the block that used to be the
|
|
16
|
+
* hottest conflict in a fleet's docs PRs is now derived, never merged.
|
|
17
|
+
*/
|
|
18
|
+
import { tallyAnswers } from "../record.js";
|
|
19
|
+
/**
|
|
20
|
+
* Add the files up. `services` are the graded per-service files, each a
|
|
21
|
+
* schema-2 record holding exactly its own service's attestation (the reader
|
|
22
|
+
* refuses one that does not). Null only when there is nothing at all.
|
|
23
|
+
*
|
|
24
|
+
* With no per-service file the one-file record is returned VERBATIM — a record
|
|
25
|
+
* written before the directory existed, or by the all-at-once form, reads
|
|
26
|
+
* exactly as it always did, retained answers and all.
|
|
27
|
+
*
|
|
28
|
+
* Otherwise the record answers ONE version of the checklist. A file that
|
|
29
|
+
* answered a different version keeps its attestation on the list — that is
|
|
30
|
+
* what `verify.checklist-forked` reads to name it — but its answers do not
|
|
31
|
+
* count. The single shared file used to settle this by rewriting: the last
|
|
32
|
+
* writer pruned everybody's answers to ITS checklist. Per-service files are
|
|
33
|
+
* never rewritten by anyone else, so nothing can prune them, and counting an
|
|
34
|
+
* answer filed against another question set would let an old id that the
|
|
35
|
+
* feature no longer asks fill the slot of a new one it does — a record reading
|
|
36
|
+
* `verified` over a question nobody answered. Excluding is the honest half:
|
|
37
|
+
* the service behind is named, and recording it again is the whole fix.
|
|
38
|
+
*
|
|
39
|
+
* WHICH version is `live`'s to say whenever the caller knows it: the digest of
|
|
40
|
+
* the checklist the feature asks now (`loam verify`, its writer, `tasks`, and
|
|
41
|
+
* `status`/`list`/`gate` for an in-flight feature all derive it anyway), or
|
|
42
|
+
* the one `loam archive` captured as it merged. The files that answered it
|
|
43
|
+
* count. Only when no caller knows — the frozen view of an archived feature
|
|
44
|
+
* with no capture — or when no file answered it does the newest file decide
|
|
45
|
+
* (by `recorded`, a later service id breaking a same-day tie, and any
|
|
46
|
+
* per-service file outranking the one-file record). Newest-by-date used to
|
|
47
|
+
* decide always, and `recorded` is a DATE: a service recording this morning
|
|
48
|
+
* against the current checklist lost the tie to a service with a later id that
|
|
49
|
+
* recorded an older checklist an hour before — or a day later, from a docs
|
|
50
|
+
* branch that was behind — so the current answers were dropped, every reader
|
|
51
|
+
* called the record stale, and the writer printed a tally its own answers were
|
|
52
|
+
* not in.
|
|
53
|
+
*/
|
|
54
|
+
export function assembleRecord(legacy, services, live) {
|
|
55
|
+
if (services.length === 0)
|
|
56
|
+
return legacy;
|
|
57
|
+
const filed = new Set(services.flatMap((s) => (s.attestations ?? []).map((a) => a.service)));
|
|
58
|
+
const parts = [...legacyRemnant(legacy, filed), ...services];
|
|
59
|
+
// The question being asked now, when somebody answered it; every part only
|
|
60
|
+
// when nobody did — then the record is stale under every reader whichever
|
|
61
|
+
// part represents it, and the newest is the one a reader recognises.
|
|
62
|
+
const answering = live === undefined ? [] : parts.filter((part) => part.checklist === live);
|
|
63
|
+
const pool = answering.length > 0 ? answering : parts;
|
|
64
|
+
// `>=` so a later part wins a same-day tie: services arrive sorted by id,
|
|
65
|
+
// and after the one-file remnant, which is exactly the precedence above.
|
|
66
|
+
const current = pool.reduce((newest, part) => (part.recorded >= newest.recorded ? part : newest));
|
|
67
|
+
const claims = parts.filter((part) => part.checklist === current.checklist).flatMap((part) => part.claims);
|
|
68
|
+
const tally = tallyAnswers(claims);
|
|
69
|
+
return {
|
|
70
|
+
schema: 2,
|
|
71
|
+
feature: current.feature,
|
|
72
|
+
recorded: current.recorded,
|
|
73
|
+
checklist: current.checklist,
|
|
74
|
+
summary: {
|
|
75
|
+
claims: current.summary.claims,
|
|
76
|
+
confirmed: tally.confirmed,
|
|
77
|
+
unconfirmed: tally.unconfirmed,
|
|
78
|
+
unanswered: current.summary.claims - claims.length,
|
|
79
|
+
},
|
|
80
|
+
claims,
|
|
81
|
+
attestations: parts.flatMap((part) => part.attestations ?? []).sort((a, b) => a.service.localeCompare(b.service)),
|
|
82
|
+
};
|
|
83
|
+
}
|
|
84
|
+
/**
|
|
85
|
+
* What of the one-file record still counts once services have files of their
|
|
86
|
+
* own: the attestations of the services that have NOT re-recorded since, and
|
|
87
|
+
* their answers.
|
|
88
|
+
*
|
|
89
|
+
* A schema-1 (all-at-once) file contributes nothing. Its answers carry no
|
|
90
|
+
* commit attestation, so a federated record could never count them — the rule
|
|
91
|
+
* the first federated write applied when it used to overwrite the file, and the
|
|
92
|
+
* reason that write now deletes it. A schema-2 file's attestation for a service
|
|
93
|
+
* that has a file of its own is superseded by that file and drops out, answers
|
|
94
|
+
* with it; when every attestation in it is superseded, nothing in it counts.
|
|
95
|
+
*/
|
|
96
|
+
function legacyRemnant(legacy, filed) {
|
|
97
|
+
if (legacy === null || legacy.schema !== 2)
|
|
98
|
+
return [];
|
|
99
|
+
const attestations = (legacy.attestations ?? []).filter((a) => !filed.has(a.service));
|
|
100
|
+
if (attestations.length === 0)
|
|
101
|
+
return [];
|
|
102
|
+
const kept = new Set(attestations.map((a) => a.service));
|
|
103
|
+
return [{ ...legacy, attestations, claims: legacy.claims.filter((c) => c.subject !== undefined && kept.has(c.subject)) }];
|
|
104
|
+
}
|
|
105
|
+
//# sourceMappingURL=union.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"union.js","sourceRoot":"","sources":["../../../../src/core/verify/read/union.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * The record as the union of its files: the one-file `verification.yaml` and\n * one `verification/<svc>.yaml` per attesting service, added up into the single\n * `Verification` every reader already takes.\n *\n * Pure on purpose — `./file.ts` reads and grades the bytes, the writer in\n * `commands/verify/record.ts` hands in the files as they will stand after its\n * commit, and both get the same answer from the same arithmetic. A writer that\n * computed the post-record verdict its own way would be the second opinion\n * `../record.ts` exists to prevent; this way what `--record` prints is exactly\n * what the next read will say.\n *\n * Nothing here reads a `summary` as fact. Each file's own `summary` was graded\n * against its own `claims[]` on the way in, and the union's is recomputed here\n * from the claims it actually counts — so the block that used to be the\n * hottest conflict in a fleet's docs PRs is now derived, never merged.\n */\nimport { tallyAnswers, type Verification } from \"../record.js\";\n\n/**\n * Add the files up. `services` are the graded per-service files, each a\n * schema-2 record holding exactly its own service's attestation (the reader\n * refuses one that does not). Null only when there is nothing at all.\n *\n * With no per-service file the one-file record is returned VERBATIM — a record\n * written before the directory existed, or by the all-at-once form, reads\n * exactly as it always did, retained answers and all.\n *\n * Otherwise the record answers ONE version of the checklist. A file that\n * answered a different version keeps its attestation on the list — that is\n * what `verify.checklist-forked` reads to name it — but its answers do not\n * count. The single shared file used to settle this by rewriting: the last\n * writer pruned everybody's answers to ITS checklist. Per-service files are\n * never rewritten by anyone else, so nothing can prune them, and counting an\n * answer filed against another question set would let an old id that the\n * feature no longer asks fill the slot of a new one it does — a record reading\n * `verified` over a question nobody answered. Excluding is the honest half:\n * the service behind is named, and recording it again is the whole fix.\n *\n * WHICH version is `live`'s to say whenever the caller knows it: the digest of\n * the checklist the feature asks now (`loam verify`, its writer, `tasks`, and\n * `status`/`list`/`gate` for an in-flight feature all derive it anyway), or\n * the one `loam archive` captured as it merged. The files that answered it\n * count. Only when no caller knows — the frozen view of an archived feature\n * with no capture — or when no file answered it does the newest file decide\n * (by `recorded`, a later service id breaking a same-day tie, and any\n * per-service file outranking the one-file record). Newest-by-date used to\n * decide always, and `recorded` is a DATE: a service recording this morning\n * against the current checklist lost the tie to a service with a later id that\n * recorded an older checklist an hour before — or a day later, from a docs\n * branch that was behind — so the current answers were dropped, every reader\n * called the record stale, and the writer printed a tally its own answers were\n * not in.\n */\nexport function assembleRecord(\n legacy: Verification | null,\n services: readonly Verification[],\n live?: string,\n): Verification | null {\n if (services.length === 0) return legacy;\n const filed = new Set(services.flatMap((s) => (s.attestations ?? []).map((a) => a.service)));\n const parts = [...legacyRemnant(legacy, filed), ...services];\n // The question being asked now, when somebody answered it; every part only\n // when nobody did — then the record is stale under every reader whichever\n // part represents it, and the newest is the one a reader recognises.\n const answering = live === undefined ? [] : parts.filter((part) => part.checklist === live);\n const pool = answering.length > 0 ? answering : parts;\n // `>=` so a later part wins a same-day tie: services arrive sorted by id,\n // and after the one-file remnant, which is exactly the precedence above.\n const current = pool.reduce((newest, part) => (part.recorded >= newest.recorded ? part : newest));\n const claims = parts.filter((part) => part.checklist === current.checklist).flatMap((part) => part.claims);\n const tally = tallyAnswers(claims);\n return {\n schema: 2,\n feature: current.feature,\n recorded: current.recorded,\n checklist: current.checklist,\n summary: {\n claims: current.summary.claims,\n confirmed: tally.confirmed,\n unconfirmed: tally.unconfirmed,\n unanswered: current.summary.claims - claims.length,\n },\n claims,\n attestations: parts.flatMap((part) => part.attestations ?? []).sort((a, b) => a.service.localeCompare(b.service)),\n };\n}\n\n/**\n * What of the one-file record still counts once services have files of their\n * own: the attestations of the services that have NOT re-recorded since, and\n * their answers.\n *\n * A schema-1 (all-at-once) file contributes nothing. Its answers carry no\n * commit attestation, so a federated record could never count them — the rule\n * the first federated write applied when it used to overwrite the file, and the\n * reason that write now deletes it. A schema-2 file's attestation for a service\n * that has a file of its own is superseded by that file and drops out, answers\n * with it; when every attestation in it is superseded, nothing in it counts.\n */\nfunction legacyRemnant(legacy: Verification | null, filed: ReadonlySet<string>): Verification[] {\n if (legacy === null || legacy.schema !== 2) return [];\n const attestations = (legacy.attestations ?? []).filter((a) => !filed.has(a.service));\n if (attestations.length === 0) return [];\n const kept = new Set(attestations.map((a) => a.service));\n return [{ ...legacy, attestations, claims: legacy.claims.filter((c) => c.subject !== undefined && kept.has(c.subject)) }];\n}\n"]}
|
|
@@ -12,9 +12,8 @@
|
|
|
12
12
|
* away, which is the fleet loam exists for — the record marks it attested, and
|
|
13
13
|
* `attestedNotice` says so on every surface that reads the record.
|
|
14
14
|
*/
|
|
15
|
-
import { join } from "node:path";
|
|
16
15
|
import {} from "./claims/identity.js";
|
|
17
|
-
import {} from "./answers.js";
|
|
16
|
+
import { ANSWERED_BY } from "./answers.js";
|
|
18
17
|
import {} from "./pins/pin.js";
|
|
19
18
|
/**
|
|
20
19
|
* The confirmed `scenario.tested` claims that no test run answered.
|
|
@@ -45,6 +44,20 @@ export function confirmedProvenance(claims) {
|
|
|
45
44
|
const ran = { runner: by("runner"), "external-runner": by("external-runner") };
|
|
46
45
|
return { ...ran, agent: done.length - ran.runner - ran["external-runner"] };
|
|
47
46
|
}
|
|
47
|
+
/**
|
|
48
|
+
* The confirmed split as the per-feature human views print it — `13 runner · 6 agent`, empty
|
|
49
|
+
* buckets left out. `loam status <FEAT>` and `list`'s verification column print it beside the
|
|
50
|
+
* verdict because the verdict turns on the scenario claims alone ({@link verificationVerdict}), so
|
|
51
|
+
* `verified 19/19 confirmed` read as nineteen runs over a record where six were an agent's word —
|
|
52
|
+
* the split `--json` and the fleet scorecard already carried. The scorecard keeps all three keys
|
|
53
|
+
* for its stable-shape reason (validate's scorecard/print.ts); a one-feature line has no shape to
|
|
54
|
+
* keep, and a column of zeros is width the reader pays for nothing.
|
|
55
|
+
*/
|
|
56
|
+
export function answeredSplit(answered) {
|
|
57
|
+
return ANSWERED_BY.filter((who) => answered[who] > 0)
|
|
58
|
+
.map((who) => `${answered[who]} ${who}`)
|
|
59
|
+
.join(" · ");
|
|
60
|
+
}
|
|
48
61
|
/**
|
|
49
62
|
* `unanswered` is the count the answers themselves cannot show: a federated
|
|
50
63
|
* record holds only what somebody answered, and the claims still owed to other
|
|
@@ -71,6 +84,24 @@ export function tallyAnswers(claims, unanswered = 0) {
|
|
|
71
84
|
export function tallyRecord(v) {
|
|
72
85
|
return tallyAnswers(v.claims, v.summary.unanswered ?? 0);
|
|
73
86
|
}
|
|
87
|
+
/**
|
|
88
|
+
* The recorded rows that may answer a checklist, keyed by claim id — the one
|
|
89
|
+
* spelling of the attestation-scoping rule. A legacy (no `schema`) record is
|
|
90
|
+
* all-at-once, so every row answers. In a federated record a row only counts
|
|
91
|
+
* where an attestation from the claim's OWN subject (per `claims`, the live
|
|
92
|
+
* checklist) listed it, so one repository cannot answer another's claims by
|
|
93
|
+
* writing their ids into its own attestation. `verify`'s read view and `loam
|
|
94
|
+
* tasks` each used to carry a copy, the second labelled "a deliberate mirror"
|
|
95
|
+
* because core could not import the command module the first lived in; the
|
|
96
|
+
* rule lives here now and both import it.
|
|
97
|
+
*/
|
|
98
|
+
export function countedAnswers(claims, recorded) {
|
|
99
|
+
const subjects = new Map(claims.map((claim) => [claim.id, claim.subject]));
|
|
100
|
+
const attested = recorded.schema !== 2
|
|
101
|
+
? null
|
|
102
|
+
: new Set((recorded.attestations ?? []).flatMap((a) => a.claims.filter((id) => subjects.get(id) === a.service)));
|
|
103
|
+
return new Map(recorded.claims.filter((c) => attested === null || attested.has(c.id)).map((c) => [c.id, c]));
|
|
104
|
+
}
|
|
74
105
|
/**
|
|
75
106
|
* Three states, because two were a lie. `verified` is the strong claim — every
|
|
76
107
|
* question answered, every answer confirmed, and every scenario answered by a
|
|
@@ -101,7 +132,8 @@ export function attestedNotice(claims, feature) {
|
|
|
101
132
|
code: "verify.scenario-attested",
|
|
102
133
|
severity: "warn",
|
|
103
134
|
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 <
|
|
135
|
+
`Answer them mechanically once the suite runs: \`loam verify ${feature} --results <report.json>\` — ` +
|
|
136
|
+
"a cucumber JSON report, or loam's runner-neutral loamScenarioReport shape for any other runner.",
|
|
105
137
|
claims: ids,
|
|
106
138
|
};
|
|
107
139
|
}
|
|
@@ -130,13 +162,4 @@ export function openClaimsNotice(tally, feature) {
|
|
|
130
162
|
`treat ${feature} as unverified until every claim is answered and confirmed.`,
|
|
131
163
|
};
|
|
132
164
|
}
|
|
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
165
|
//# sourceMappingURL=record.js.map
|
|
@@ -1 +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"]}
|
|
1
|
+
{"version":3,"file":"record.js","sourceRoot":"","sources":["../../../src/core/verify/record.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;GAaG;AACH,OAAO,EAAkB,MAAM,sBAAsB,CAAC;AACtD,OAAO,EAAE,WAAW,EAAiC,MAAM,cAAc,CAAC;AAC1E,OAAO,EAAoB,MAAM,eAAe,CAAC;AA6JjD;;;;;;;;;;GAUG;AACH,MAAM,UAAU,cAAc,CAAC,MAAgC;IAC7D,OAAO,MAAM;SACV,MAAM,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,IAAI,KAAK,iBAAiB,IAAI,CAAC,CAAC,OAAO,KAAK,WAAW,IAAI,CAAC,CAAC,WAAW,KAAK,QAAQ,CAAC;SACtG,GAAG,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC;AACtB,CAAC;AAED;;;;;;GAMG;AACH,MAAM,UAAU,mBAAmB,CAAC,MAAgC;IAClE,MAAM,IAAI,GAAG,MAAM,CAAC,MAAM,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,OAAO,KAAK,WAAW,CAAC,CAAC;IAC7D,MAAM,EAAE,GAAG,CAAC,GAAe,EAAU,EAAE,CAAC,IAAI,CAAC,MAAM,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,WAAW,KAAK,GAAG,CAAC,CAAC,MAAM,CAAC;IACzF,MAAM,GAAG,GAAG,EAAE,MAAM,EAAE,EAAE,CAAC,QAAQ,CAAC,EAAE,iBAAiB,EAAE,EAAE,CAAC,iBAAiB,CAAC,EAAE,CAAC;IAC/E,OAAO,EAAE,GAAG,GAAG,EAAE,KAAK,EAAE,IAAI,CAAC,MAAM,GAAG,GAAG,CAAC,MAAM,GAAG,GAAG,CAAC,iBAAiB,CAAC,EAAE,CAAC;AAC9E,CAAC;AAED;;;;;;;;GAQG;AACH,MAAM,UAAU,aAAa,CAAC,QAAoC;IAChE,OAAO,WAAW,CAAC,MAAM,CAAC,CAAC,GAAG,EAAE,EAAE,CAAC,QAAQ,CAAC,GAAG,CAAC,GAAG,CAAC,CAAC;SAClD,GAAG,CAAC,CAAC,GAAG,EAAE,EAAE,CAAC,GAAG,QAAQ,CAAC,GAAG,CAAC,IAAI,GAAG,EAAE,CAAC;SACvC,IAAI,CAAC,KAAK,CAAC,CAAC;AACjB,CAAC;AAaD;;;;;GAKG;AACH,MAAM,UAAU,YAAY,CAAC,MAAgC,EAAE,UAAU,GAAG,CAAC;IAC3E,MAAM,EAAE,GAAG,CAAC,OAAe,EAAU,EAAE,CAAC,MAAM,CAAC,MAAM,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,OAAO,KAAK,OAAO,CAAC,CAAC,MAAM,CAAC;IAC3F,OAAO;QACL,MAAM,EAAE,MAAM,CAAC,MAAM,GAAG,UAAU;QAClC,SAAS,EAAE,EAAE,CAAC,WAAW,CAAC;QAC1B,WAAW,EAAE,EAAE,CAAC,aAAa,CAAC;QAC9B,UAAU,EAAE,EAAE,CAAC,YAAY,CAAC,GAAG,UAAU;QACzC,QAAQ,EAAE,cAAc,CAAC,MAAM,CAAC,CAAC,MAAM;KACxC,CAAC;AACJ,CAAC;AAED;;;;;;GAMG;AACH,MAAM,UAAU,WAAW,CAAC,CAAe;IACzC,OAAO,YAAY,CAAC,CAAC,CAAC,MAAM,EAAE,CAAC,CAAC,OAAO,CAAC,UAAU,IAAI,CAAC,CAAC,CAAC;AAC3D,CAAC;AAED;;;;;;;;;;GAUG;AACH,MAAM,UAAU,cAAc,CAC5B,MAAkD,EAClD,QAAsB;IAEtB,MAAM,QAAQ,GAAG,IAAI,GAAG,CAAC,MAAM,CAAC,GAAG,CAAC,CAAC,KAAK,EAAE,EAAE,CAAC,CAAC,KAAK,CAAC,EAAE,EAAE,KAAK,CAAC,OAAO,CAAC,CAAC,CAAC,CAAC;IAC3E,MAAM,QAAQ,GACZ,QAAQ,CAAC,MAAM,KAAK,CAAC;QACnB,CAAC,CAAC,IAAI;QACN,CAAC,CAAC,IAAI,GAAG,CACL,CAAC,QAAQ,CAAC,YAAY,IAAI,EAAE,CAAC,CAAC,OAAO,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,MAAM,CAAC,MAAM,CAAC,CAAC,EAAE,EAAE,EAAE,CAAC,QAAQ,CAAC,GAAG,CAAC,EAAE,CAAC,KAAK,CAAC,CAAC,OAAO,CAAC,CAAC,CACtG,CAAC;IACR,OAAO,IAAI,GAAG,CAAC,QAAQ,CAAC,MAAM,CAAC,MAAM,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,QAAQ,KAAK,IAAI,IAAI,QAAQ,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,CAAC,CAAC;AAC/G,CAAC;AAED;;;;;;;GAOG;AACH,MAAM,CAAC,MAAM,qBAAqB,GAAG,CAAC,UAAU,EAAE,UAAU,EAAE,YAAY,CAAU,CAAC;AAGrF,MAAM,UAAU,mBAAmB,CAAC,CAAc,EAAE,KAAK,GAAG,KAAK;IAC/D,IAAI,KAAK,IAAI,CAAC,CAAC,MAAM,KAAK,CAAC,IAAI,CAAC,CAAC,SAAS,KAAK,CAAC,CAAC,MAAM;QAAE,OAAO,YAAY,CAAC;IAC7E,OAAO,CAAC,CAAC,QAAQ,GAAG,CAAC,CAAC,CAAC,CAAC,UAAU,CAAC,CAAC,CAAC,UAAU,CAAC;AAClD,CAAC;AAWD;;;;;;;GAOG;AACH,MAAM,UAAU,cAAc,CAAC,MAAgC,EAAE,OAAe;IAC9E,MAAM,GAAG,GAAG,cAAc,CAAC,MAAM,CAAC,CAAC;IACnC,IAAI,GAAG,CAAC,MAAM,KAAK,CAAC;QAAE,OAAO,IAAI,CAAC;IAClC,OAAO;QACL,IAAI,EAAE,0BAA0B;QAChC,QAAQ,EAAE,MAAM;QAChB,OAAO,EACL,GAAG,GAAG,CAAC,MAAM,2EAA2E,GAAG,CAAC,IAAI,CAAC,IAAI,CAAC,IAAI;YAC1G,+DAA+D,OAAO,+BAA+B;YACrG,iGAAiG;QACnG,MAAM,EAAE,GAAG;KACZ,CAAC;AACJ,CAAC;AAED;;;;;;;;;;;GAWG;AACH,MAAM,UAAU,gBAAgB,CAAC,KAAkB,EAAE,OAAe;IAClE,MAAM,IAAI,GAAG,KAAK,CAAC,WAAW,GAAG,KAAK,CAAC,UAAU,CAAC;IAClD,IAAI,IAAI,KAAK,CAAC;QAAE,OAAO,IAAI,CAAC;IAC5B,OAAO;QACL,IAAI,EAAE,oBAAoB;QAC1B,QAAQ,EAAE,MAAM;QAChB,OAAO,EACL,0BAA0B,KAAK,CAAC,MAAM,cAAc,KAAK,CAAC,SAAS,YAAY;YAC/E,CAAC,KAAK,CAAC,QAAQ,GAAG,CAAC,CAAC,CAAC,CAAC,KAAK,KAAK,CAAC,QAAQ,sBAAsB,CAAC,CAAC,CAAC,EAAE,CAAC;YACrE,KAAK,KAAK,CAAC,WAAW,iBAAiB,KAAK,CAAC,UAAU,gBAAgB;YACvE,SAAS,OAAO,6DAA6D;KAChF,CAAC;AACJ,CAAC","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 { type ClaimKind } from \"./claims/identity.js\";\nimport { ANSWERED_BY, 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 ./read/shape.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/**\n * The confirmed split as the per-feature human views print it — `13 runner · 6 agent`, empty\n * buckets left out. `loam status <FEAT>` and `list`'s verification column print it beside the\n * verdict because the verdict turns on the scenario claims alone ({@link verificationVerdict}), so\n * `verified 19/19 confirmed` read as nineteen runs over a record where six were an agent's word —\n * the split `--json` and the fleet scorecard already carried. The scorecard keeps all three keys\n * for its stable-shape reason (validate's scorecard/print.ts); a one-feature line has no shape to\n * keep, and a column of zeros is width the reader pays for nothing.\n */\nexport function answeredSplit(answered: Record<AnsweredBy, number>): string {\n return ANSWERED_BY.filter((who) => answered[who] > 0)\n .map((who) => `${answered[who]} ${who}`)\n .join(\" · \");\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 * The recorded rows that may answer a checklist, keyed by claim id — the one\n * spelling of the attestation-scoping rule. A legacy (no `schema`) record is\n * all-at-once, so every row answers. In a federated record a row only counts\n * where an attestation from the claim's OWN subject (per `claims`, the live\n * checklist) listed it, so one repository cannot answer another's claims by\n * writing their ids into its own attestation. `verify`'s read view and `loam\n * tasks` each used to carry a copy, the second labelled \"a deliberate mirror\"\n * because core could not import the command module the first lived in; the\n * rule lives here now and both import it.\n */\nexport function countedAnswers(\n claims: readonly { id: string; subject: string }[],\n recorded: Verification,\n): Map<string, RecordedClaim> {\n const subjects = new Map(claims.map((claim) => [claim.id, claim.subject]));\n const attested =\n recorded.schema !== 2\n ? null\n : new Set(\n (recorded.attestations ?? []).flatMap((a) => a.claims.filter((id) => subjects.get(id) === a.service)),\n );\n return new Map(recorded.claims.filter((c) => attested === null || attested.has(c.id)).map((c) => [c.id, c]));\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 <report.json>\\` — ` +\n \"a cucumber JSON report, or loam's runner-neutral loamScenarioReport shape for any other runner.\",\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"]}
|
|
@@ -1,6 +1,9 @@
|
|
|
1
1
|
/**
|
|
2
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).
|
|
3
|
+
* the byte pre-image the merge consumed, swapped in by one rename(2) per file.
|
|
4
|
+
* A write is one file, or two: a service's own `verification/<svc>.yaml`, and
|
|
5
|
+
* — once nothing in it counts any more — the removal of the one-file record it
|
|
6
|
+
* supersedes. Never another service's file: those are not this run's to touch.
|
|
4
7
|
*
|
|
5
8
|
* The record used to go down as a plain `writeFile`, which cost three ways at
|
|
6
9
|
* once: two `--record` runs read the same pre-image and the last writer
|
|
@@ -21,45 +24,51 @@ import { message, rollbackMessage, rollbackStaged, stageWrites, StagingRaceError
|
|
|
21
24
|
import { repoPath } from "../../envelope/json.js";
|
|
22
25
|
import {} from "../../staging/writes.js";
|
|
23
26
|
import { renderVerification } from "./render.js";
|
|
24
|
-
import {
|
|
27
|
+
import {} from "../record.js";
|
|
25
28
|
/**
|
|
26
|
-
*
|
|
27
|
-
*
|
|
28
|
-
*
|
|
29
|
-
*
|
|
29
|
+
* Writes land in the order given, so a caller puts what must survive a kill
|
|
30
|
+
* FIRST: a service's own file before the one-file record it supersedes, so a
|
|
31
|
+
* process killed between the two renames leaves a record that still reads
|
|
32
|
+
* whole — the new file counted, the old one superseded by it — rather than one
|
|
33
|
+
* with the old file gone and nothing in its place.
|
|
30
34
|
*/
|
|
31
|
-
export async function commitVerification(
|
|
32
|
-
const
|
|
33
|
-
|
|
34
|
-
|
|
35
|
+
export async function commitVerification(writes, docsDir) {
|
|
36
|
+
const planned = writes.map((w) => w.record === null
|
|
37
|
+
? { path: w.path, content: null }
|
|
38
|
+
: w.preImage === null
|
|
39
|
+
? { path: w.path, content: renderVerification(w.record), exclusive: true }
|
|
40
|
+
: { path: w.path, content: renderVerification(w.record) });
|
|
35
41
|
// Staging touches the filesystem — a read-only feature directory, a full
|
|
36
42
|
// disk, a pre-image that will not read. None of that is a bug in loam, and
|
|
37
43
|
// letting it escape as `internal` sends the reader looking for one (archive
|
|
38
44
|
// draws the same line around its own `stageWrites`). Nothing was written.
|
|
39
45
|
let staged;
|
|
40
46
|
try {
|
|
41
|
-
staged = await stageWrites(
|
|
47
|
+
staged = await stageWrites(planned);
|
|
42
48
|
}
|
|
43
49
|
catch (err) {
|
|
44
50
|
return { ok: false, code: "merge-failed", message: `${message(err)} — nothing was recorded; the record is as the last writer left it` };
|
|
45
51
|
}
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
52
|
+
for (const w of writes) {
|
|
53
|
+
const display = repoPath(docsDir, w.path);
|
|
54
|
+
// Joined by path, not by index — vouch's rule, for vouch's reason: one
|
|
55
|
+
// entry per planned write in order is true today and promised nowhere, and
|
|
56
|
+
// a compare-and-set over the wrong pair would vouch for bytes the merge
|
|
57
|
+
// never saw. A missing entry fails closed as a merge that never happened.
|
|
58
|
+
const s = staged.find((x) => x.write.path === w.path);
|
|
59
|
+
if (s === undefined) {
|
|
60
|
+
await rollbackStaged(staged);
|
|
61
|
+
return { ok: false, code: "merge-failed", message: `staging returned no entry for ${display} — nothing was recorded` };
|
|
62
|
+
}
|
|
63
|
+
// The compare the read promised: the file as staging found it must be the
|
|
64
|
+
// file the merge consumed. `swapStaged` re-checks against ITS read just
|
|
65
|
+
// before the rename, so together the two compares cover the whole window
|
|
66
|
+
// from the authoritative read to the swap.
|
|
67
|
+
const edited = w.preImage === null ? s.before !== null : s.before === null || !s.before.equals(w.preImage);
|
|
68
|
+
if (edited) {
|
|
69
|
+
await rollbackStaged(staged);
|
|
70
|
+
return { ok: false, code: "record-raced", message: racedMessage(display) };
|
|
71
|
+
}
|
|
63
72
|
}
|
|
64
73
|
try {
|
|
65
74
|
await swapStaged(staged);
|
|
@@ -76,11 +85,12 @@ export async function commitVerification(featureDir, v, preImage, docsDir) {
|
|
|
76
85
|
// EEXIST is the exclusive create losing its race — same fact as the CAS
|
|
77
86
|
// refusal, discovered by link(2) instead of by the compare.
|
|
78
87
|
if (err instanceof StagingRaceError || err.code === "EEXIST") {
|
|
79
|
-
|
|
88
|
+
const path = err instanceof StagingRaceError ? err.path : (writes.find((w) => w.preImage === null)?.path ?? writes[0].path);
|
|
89
|
+
return { ok: false, code: "record-raced", message: racedMessage(repoPath(docsDir, path)) };
|
|
80
90
|
}
|
|
81
91
|
return { ok: false, code: "merge-failed", message: `${message(err)} — nothing was recorded; the record is as the last writer left it` };
|
|
82
92
|
}
|
|
83
|
-
return { ok: true
|
|
93
|
+
return { ok: true };
|
|
84
94
|
}
|
|
85
95
|
function racedMessage(display) {
|
|
86
96
|
return (`${display} changed while this record was being written — another writer or an editor landed between the read and the write. ` +
|