@ybotok/loam 0.2.0-alpha.6 → 0.2.0-alpha.9
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +1477 -1
- package/COMPARISON.md +91 -3
- package/CONTRIBUTING.md +37 -0
- package/MIGRATING-from-OpenSpec.md +40 -22
- package/QUICKSTART.md +181 -9
- package/README.md +256 -57
- package/ROADMAP.md +108 -11
- package/SCHEMA.md +721 -130
- package/WORKFLOW.md +272 -62
- package/dist/cli.js +26 -8
- package/dist/cli.js.map +1 -1
- package/dist/commands/adopt/adopt.js +5 -10
- package/dist/commands/adopt/adopt.js.map +1 -1
- package/dist/commands/archive/archive.js +15 -18
- package/dist/commands/archive/archive.js.map +1 -1
- package/dist/commands/archive/plan/contracts/asyncapi.js +27 -41
- package/dist/commands/archive/plan/contracts/asyncapi.js.map +1 -1
- package/dist/commands/archive/plan/contracts/consumers.js +56 -0
- package/dist/commands/archive/plan/contracts/consumers.js.map +1 -0
- package/dist/commands/archive/plan/contracts/openapi.js +25 -42
- package/dist/commands/archive/plan/contracts/openapi.js.map +1 -1
- package/dist/commands/archive/plan/contracts/verdict-lines.js +41 -0
- package/dist/commands/archive/plan/contracts/verdict-lines.js.map +1 -0
- package/dist/commands/archive/plan/gate.js +3 -7
- package/dist/commands/archive/plan/gate.js.map +1 -1
- package/dist/commands/archive/plan/landscape.js +4 -4
- package/dist/commands/archive/plan/landscape.js.map +1 -1
- package/dist/commands/archive/plan/refusal.js +8 -18
- package/dist/commands/archive/plan/refusal.js.map +1 -1
- package/dist/commands/archive/plan/specs.js +134 -25
- package/dist/commands/archive/plan/specs.js.map +1 -1
- package/dist/commands/archive/plan/state.js +1 -0
- package/dist/commands/archive/plan/state.js.map +1 -1
- package/dist/commands/archive/run.js +82 -33
- package/dist/commands/archive/run.js.map +1 -1
- package/dist/commands/archive/shipped.js +49 -0
- package/dist/commands/archive/shipped.js.map +1 -0
- package/dist/commands/context/context.js +4 -7
- package/dist/commands/context/context.js.map +1 -1
- package/dist/commands/context/print.js +2 -2
- package/dist/commands/context/print.js.map +1 -1
- package/dist/commands/delta/delta.js +44 -12
- package/dist/commands/delta/delta.js.map +1 -1
- package/dist/commands/delta/print.js +1 -1
- package/dist/commands/delta/print.js.map +1 -1
- package/dist/commands/dependencies.js +3 -7
- package/dist/commands/dependencies.js.map +1 -1
- package/dist/commands/diff/diff.js +20 -27
- package/dist/commands/diff/diff.js.map +1 -1
- package/dist/commands/explore.js +4 -8
- package/dist/commands/explore.js.map +1 -1
- package/dist/commands/gate/gate.js +45 -8
- package/dist/commands/gate/gate.js.map +1 -1
- package/dist/commands/gate/print.js +15 -3
- package/dist/commands/gate/print.js.map +1 -1
- package/dist/commands/gherkin/commit.js +13 -54
- package/dist/commands/gherkin/commit.js.map +1 -1
- package/dist/commands/gherkin/gherkin.js +36 -20
- package/dist/commands/gherkin/gherkin.js.map +1 -1
- package/dist/commands/gherkin/reconcile.js +10 -13
- package/dist/commands/gherkin/reconcile.js.map +1 -1
- package/dist/commands/init/first-hour.js +44 -0
- package/dist/commands/init/first-hour.js.map +1 -1
- package/dist/commands/init/init.js +18 -22
- package/dist/commands/init/init.js.map +1 -1
- package/dist/commands/list/campaign/campaign.js +4 -6
- package/dist/commands/list/campaign/campaign.js.map +1 -1
- package/dist/commands/list/json.js +0 -4
- package/dist/commands/list/json.js.map +1 -1
- package/dist/commands/list/list.js +38 -23
- package/dist/commands/list/list.js.map +1 -1
- package/dist/commands/list/print.js +38 -6
- package/dist/commands/list/print.js.map +1 -1
- package/dist/commands/list/suites/suites.js +140 -0
- package/dist/commands/list/suites/suites.js.map +1 -0
- package/dist/commands/list/views.js +28 -33
- package/dist/commands/list/views.js.map +1 -1
- package/dist/commands/mcp/dispatch.js +91 -36
- package/dist/commands/mcp/dispatch.js.map +1 -1
- package/dist/commands/mcp/mcp.js +56 -15
- package/dist/commands/mcp/mcp.js.map +1 -1
- package/dist/commands/mcp/serve.js.map +1 -1
- package/dist/commands/migrate-openspec/materialize/active.js +27 -22
- package/dist/commands/migrate-openspec/materialize/active.js.map +1 -1
- package/dist/commands/migrate-openspec/materialize/stage.js +4 -3
- package/dist/commands/migrate-openspec/materialize/stage.js.map +1 -1
- package/dist/commands/migrate-openspec/materialize/target.js +2 -2
- package/dist/commands/migrate-openspec/materialize/target.js.map +1 -1
- package/dist/commands/migrate-openspec/openspec/decisions.js +9 -14
- package/dist/commands/migrate-openspec/openspec/decisions.js.map +1 -1
- package/dist/commands/migrate-openspec/openspec/error.js +7 -8
- package/dist/commands/migrate-openspec/openspec/error.js.map +1 -1
- package/dist/commands/migrate-openspec/openspec/mapping.js +1 -9
- package/dist/commands/migrate-openspec/openspec/mapping.js.map +1 -1
- package/dist/commands/migrate-openspec/openspec/paths.js +3 -6
- package/dist/commands/migrate-openspec/openspec/paths.js.map +1 -1
- package/dist/commands/new/capability-template.js +118 -0
- package/dist/commands/new/capability-template.js.map +1 -0
- package/dist/commands/new/commit.js +8 -22
- package/dist/commands/new/commit.js.map +1 -1
- package/dist/commands/new/modifies.js +206 -0
- package/dist/commands/new/modifies.js.map +1 -0
- package/dist/commands/new/new.js +59 -14
- package/dist/commands/new/new.js.map +1 -1
- package/dist/commands/new/templates.js +48 -124
- package/dist/commands/new/templates.js.map +1 -1
- package/dist/commands/open.js +3 -7
- package/dist/commands/open.js.map +1 -1
- package/dist/commands/policy/gate.js +27 -0
- package/dist/commands/policy/gate.js.map +1 -1
- package/dist/commands/rebase/plan.js +2 -2
- package/dist/commands/rebase/plan.js.map +1 -1
- package/dist/commands/rebase/rebase.js +60 -35
- package/dist/commands/rebase/rebase.js.map +1 -1
- package/dist/commands/seed/commit.js +55 -26
- package/dist/commands/seed/commit.js.map +1 -1
- package/dist/commands/seed/plan.js +64 -13
- package/dist/commands/seed/plan.js.map +1 -1
- package/dist/commands/seed/seed.js +6 -9
- package/dist/commands/seed/seed.js.map +1 -1
- package/dist/commands/show/show.js +3 -7
- package/dist/commands/show/show.js.map +1 -1
- package/dist/commands/status/print.js +13 -4
- package/dist/commands/status/print.js.map +1 -1
- package/dist/commands/status/status.js +3 -7
- package/dist/commands/status/status.js.map +1 -1
- package/dist/commands/steps/steps.js +3 -7
- package/dist/commands/steps/steps.js.map +1 -1
- package/dist/commands/subsystem/edit.js +7 -7
- package/dist/commands/subsystem/edit.js.map +1 -1
- package/dist/commands/subsystem/move.js +60 -25
- package/dist/commands/subsystem/move.js.map +1 -1
- package/dist/commands/subsystem/query.js +2 -4
- package/dist/commands/subsystem/query.js.map +1 -1
- package/dist/commands/subsystem/subsystem.js +4 -7
- package/dist/commands/subsystem/subsystem.js.map +1 -1
- package/dist/commands/subsystem/sync.js +141 -71
- package/dist/commands/subsystem/sync.js.map +1 -1
- package/dist/commands/subsystem/txn/report.js +55 -5
- package/dist/commands/subsystem/txn/report.js.map +1 -1
- package/dist/commands/subsystem/txn/txn.js +121 -45
- package/dist/commands/subsystem/txn/txn.js.map +1 -1
- package/dist/commands/subsystem/txn/views.js +82 -12
- package/dist/commands/subsystem/txn/views.js.map +1 -1
- package/dist/commands/tasks/render.js +116 -0
- package/dist/commands/tasks/render.js.map +1 -0
- package/dist/commands/tasks/tasks.js +113 -0
- package/dist/commands/tasks/tasks.js.map +1 -0
- package/dist/commands/unarchive/manifest.js +6 -9
- package/dist/commands/unarchive/manifest.js.map +1 -1
- package/dist/commands/unarchive/restore.js +17 -28
- package/dist/commands/unarchive/restore.js.map +1 -1
- package/dist/commands/unarchive/unarchive.js +22 -30
- package/dist/commands/unarchive/unarchive.js.map +1 -1
- package/dist/commands/validate/arch-coverage.js +6 -15
- package/dist/commands/validate/arch-coverage.js.map +1 -1
- package/dist/commands/validate/checks/fleet-shape.js +4 -49
- package/dist/commands/validate/checks/fleet-shape.js.map +1 -1
- package/dist/commands/validate/checks/health-deps.js +9 -6
- package/dist/commands/validate/checks/health-deps.js.map +1 -1
- package/dist/commands/validate/checks/requirements.js +18 -8
- package/dist/commands/validate/checks/requirements.js.map +1 -1
- package/dist/commands/validate/checks/sources.js +6 -2
- package/dist/commands/validate/checks/sources.js.map +1 -1
- package/dist/commands/validate/feature.js +23 -19
- package/dist/commands/validate/feature.js.map +1 -1
- package/dist/commands/validate/fleet/census.js +26 -3
- package/dist/commands/validate/fleet/census.js.map +1 -1
- package/dist/commands/validate/fleet/landscape.js +35 -32
- package/dist/commands/validate/fleet/landscape.js.map +1 -1
- package/dist/commands/validate/fleet/map/attest.js +7 -12
- package/dist/commands/validate/fleet/map/attest.js.map +1 -1
- package/dist/commands/validate/fleet/map/bindings.js +16 -5
- package/dist/commands/validate/fleet/map/bindings.js.map +1 -1
- package/dist/commands/validate/fleet/map/consumers.js +43 -122
- package/dist/commands/validate/fleet/map/consumers.js.map +1 -1
- package/dist/commands/validate/fleet/map/usecases.js +5 -11
- package/dist/commands/validate/fleet/map/usecases.js.map +1 -1
- package/dist/commands/validate/fleet/obligations.js +1 -5
- package/dist/commands/validate/fleet/obligations.js.map +1 -1
- package/dist/commands/validate/fleet/scorecard/print.js +5 -1
- package/dist/commands/validate/fleet/scorecard/print.js.map +1 -1
- package/dist/commands/validate/fleet/scorecard/scorecard.js +17 -11
- package/dist/commands/validate/fleet/scorecard/scorecard.js.map +1 -1
- package/dist/commands/validate/fleet/usecases/usecases.js +6 -4
- package/dist/commands/validate/fleet/usecases/usecases.js.map +1 -1
- package/dist/commands/validate/fleet/views/fleet-project.js +1 -1
- package/dist/commands/validate/fleet/views/fleet-project.js.map +1 -1
- package/dist/commands/validate/fleet/views/ids.js +33 -7
- package/dist/commands/validate/fleet/views/ids.js.map +1 -1
- package/dist/commands/validate/fleet/views/root-documents.js +17 -35
- package/dist/commands/validate/fleet/views/root-documents.js.map +1 -1
- package/dist/commands/validate/fleet/views/stale.js +31 -14
- package/dist/commands/validate/fleet/views/stale.js.map +1 -1
- package/dist/commands/validate/links/corpus.js +1 -4
- package/dist/commands/validate/links/corpus.js.map +1 -1
- package/dist/commands/validate/links/glossary.js.map +1 -1
- package/dist/commands/validate/report.js +5 -4
- package/dist/commands/validate/report.js.map +1 -1
- package/dist/commands/validate/scope.js +113 -0
- package/dist/commands/validate/scope.js.map +1 -0
- package/dist/commands/validate/service/api.js +16 -104
- package/dist/commands/validate/service/api.js.map +1 -1
- package/dist/commands/validate/service/contracts/generated.js +134 -0
- package/dist/commands/validate/service/contracts/generated.js.map +1 -0
- package/dist/commands/validate/service/events/events.js +1 -1
- package/dist/commands/validate/service/events/events.js.map +1 -1
- package/dist/commands/validate/service/events/message-producers.js +1 -1
- package/dist/commands/validate/service/events/message-producers.js.map +1 -1
- package/dist/commands/validate/service/evidence-pins.js +56 -10
- package/dist/commands/validate/service/evidence-pins.js.map +1 -1
- package/dist/commands/validate/service/service.js +16 -26
- package/dist/commands/validate/service/service.js.map +1 -1
- package/dist/commands/validate/service/specs.js +31 -38
- package/dist/commands/validate/service/specs.js.map +1 -1
- package/dist/commands/validate/service/spine.js +8 -11
- package/dist/commands/validate/service/spine.js.map +1 -1
- package/dist/commands/validate/service/usecases/usecases.js +3 -3
- package/dist/commands/validate/service/usecases/usecases.js.map +1 -1
- package/dist/commands/validate/validate.js +44 -26
- package/dist/commands/validate/validate.js.map +1 -1
- package/dist/commands/verify/cross/diff.js +2 -6
- package/dist/commands/verify/cross/diff.js.map +1 -1
- package/dist/commands/verify/evidence/contract.js +1 -1
- package/dist/commands/verify/evidence/contract.js.map +1 -1
- package/dist/commands/verify/evidence/read.js +12 -3
- package/dist/commands/verify/evidence/read.js.map +1 -1
- package/dist/commands/verify/frozen.js +45 -23
- package/dist/commands/verify/frozen.js.map +1 -1
- package/dist/commands/verify/record.js +69 -17
- package/dist/commands/verify/record.js.map +1 -1
- package/dist/commands/verify/report.js +27 -44
- package/dist/commands/verify/report.js.map +1 -1
- package/dist/commands/verify/results.js +112 -30
- package/dist/commands/verify/results.js.map +1 -1
- package/dist/commands/verify/verify.js +54 -51
- package/dist/commands/verify/verify.js.map +1 -1
- package/dist/commands/vouch/pack/pack.js +2 -2
- package/dist/commands/vouch/pack/pack.js.map +1 -1
- package/dist/commands/vouch/pack/print.js +1 -6
- package/dist/commands/vouch/pack/print.js.map +1 -1
- package/dist/commands/vouch/pack/sections.js +1 -2
- package/dist/commands/vouch/pack/sections.js.map +1 -1
- package/dist/commands/vouch/run.js +26 -81
- package/dist/commands/vouch/run.js.map +1 -1
- package/dist/commands/vouch/sample/plan.js +20 -34
- package/dist/commands/vouch/sample/plan.js.map +1 -1
- package/dist/commands/vouch/vet/verify.js +39 -7
- package/dist/commands/vouch/vet/verify.js.map +1 -1
- package/dist/commands/vouch/vouch.js +10 -17
- package/dist/commands/vouch/vouch.js.map +1 -1
- package/dist/core/agent/agents-md/command-map.js +57 -102
- package/dist/core/agent/agents-md/command-map.js.map +1 -1
- package/dist/core/agent/agents-md/map/explain.js +4 -3
- package/dist/core/agent/agents-md/map/explain.js.map +1 -1
- package/dist/core/agent/agents-md/map/lenses/context.js +4 -3
- package/dist/core/agent/agents-md/map/lenses/context.js.map +1 -1
- package/dist/core/agent/agents-md/map/lenses/diff.js +33 -20
- package/dist/core/agent/agents-md/map/lenses/diff.js.map +1 -1
- package/dist/core/agent/agents-md/map/lenses/gate.js +13 -5
- package/dist/core/agent/agents-md/map/lenses/gate.js.map +1 -1
- package/dist/core/agent/agents-md/map/mcp.js +10 -7
- package/dist/core/agent/agents-md/map/mcp.js.map +1 -1
- package/dist/core/agent/agents-md/map/status.js +9 -3
- package/dist/core/agent/agents-md/map/status.js.map +1 -1
- package/dist/core/agent/agents-md/map/subsystem.js +31 -17
- package/dist/core/agent/agents-md/map/subsystem.js.map +1 -1
- package/dist/core/agent/agents-md/map/usecases.js +24 -6
- package/dist/core/agent/agents-md/map/usecases.js.map +1 -1
- package/dist/core/agent/agents-md/refusals.js +37 -21
- package/dist/core/agent/agents-md/refusals.js.map +1 -1
- package/dist/core/agent/agents-md.js +25 -10
- package/dist/core/agent/agents-md.js.map +1 -1
- package/dist/core/agent/agents-stamp.js +10 -0
- package/dist/core/agent/agents-stamp.js.map +1 -1
- package/dist/core/agent/scaffold.js +10 -15
- package/dist/core/agent/scaffold.js.map +1 -1
- package/dist/core/agent/tools/dialects.js +1 -0
- package/dist/core/agent/tools/dialects.js.map +1 -1
- package/dist/core/agent/tools/registry.js +41 -76
- package/dist/core/agent/tools/registry.js.map +1 -1
- package/dist/core/agent/workflows/adopt.js +5 -3
- package/dist/core/agent/workflows/adopt.js.map +1 -1
- package/dist/core/agent/workflows/check.js +25 -22
- package/dist/core/agent/workflows/check.js.map +1 -1
- package/dist/core/agent/workflows/closing.js +54 -21
- package/dist/core/agent/workflows/closing.js.map +1 -1
- package/dist/core/agent/workflows/feature.js +79 -11
- package/dist/core/agent/workflows/feature.js.map +1 -1
- package/dist/core/agent/workflows/implement.js +91 -13
- package/dist/core/agent/workflows/implement.js.map +1 -1
- package/dist/core/agent/workflows/reference/authoring.js +3 -1
- package/dist/core/agent/workflows/reference/authoring.js.map +1 -1
- package/dist/core/agent/workflows/reference/codes.js +8 -6
- package/dist/core/agent/workflows/reference/codes.js.map +1 -1
- package/dist/core/agent/workflows/reference/done-check.js +37 -15
- package/dist/core/agent/workflows/reference/done-check.js.map +1 -1
- package/dist/core/agent/workflows/reference/reference.js.map +1 -1
- package/dist/core/agent/workflows/reference/spine.js +62 -24
- package/dist/core/agent/workflows/reference/spine.js.map +1 -1
- package/dist/core/asyncapi/baseline/plan.js +14 -81
- package/dist/core/asyncapi/baseline/plan.js.map +1 -1
- package/dist/core/asyncapi/digest.js +30 -25
- package/dist/core/asyncapi/digest.js.map +1 -1
- package/dist/core/asyncapi/merge/components.js +5 -4
- package/dist/core/asyncapi/merge/components.js.map +1 -1
- package/dist/core/asyncapi/merge/error.js +7 -2
- package/dist/core/asyncapi/merge/error.js.map +1 -1
- package/dist/core/asyncapi/merge/markers.js +30 -50
- package/dist/core/asyncapi/merge/markers.js.map +1 -1
- package/dist/core/asyncapi/merge/merge.js +31 -53
- package/dist/core/asyncapi/merge/merge.js.map +1 -1
- package/dist/core/asyncapi/merge/pin.js +10 -36
- package/dist/core/asyncapi/merge/pin.js.map +1 -1
- package/dist/core/asyncapi/model.js.map +1 -1
- package/dist/core/asyncapi/read.js +36 -12
- package/dist/core/asyncapi/read.js.map +1 -1
- package/dist/core/brief/brief.js +30 -25
- package/dist/core/brief/brief.js.map +1 -1
- package/dist/core/brief/landscape.js +8 -12
- package/dist/core/brief/landscape.js.map +1 -1
- package/dist/core/brief/map/extend.js +8 -5
- package/dist/core/brief/map/extend.js.map +1 -1
- package/dist/core/brief/map/owed.js +8 -26
- package/dist/core/brief/map/owed.js.map +1 -1
- package/dist/core/brief/targets.js +79 -5
- package/dist/core/brief/targets.js.map +1 -1
- package/dist/core/brief/unchecked.js +1 -1
- package/dist/core/brief/unchecked.js.map +1 -1
- package/dist/core/c4/likec4.js +147 -16
- package/dist/core/c4/likec4.js.map +1 -1
- package/dist/core/c4/locate/unlocated.js +150 -0
- package/dist/core/c4/locate/unlocated.js.map +1 -0
- package/dist/core/c4/parsed/values.js +54 -1
- package/dist/core/c4/parsed/values.js.map +1 -1
- package/dist/core/c4/prefetch/fleet.js +1 -9
- package/dist/core/c4/prefetch/fleet.js.map +1 -1
- package/dist/core/c4/project/architecture.js +31 -9
- package/dist/core/c4/project/architecture.js.map +1 -1
- package/dist/core/c4/project/documents.js +36 -10
- package/dist/core/c4/project/documents.js.map +1 -1
- package/dist/core/c4/project/load.js +14 -21
- package/dist/core/c4/project/load.js.map +1 -1
- package/dist/core/c4/project/staged.js +7 -3
- package/dist/core/c4/project/staged.js.map +1 -1
- package/dist/core/c4/resolve/external.js +51 -0
- package/dist/core/c4/resolve/external.js.map +1 -0
- package/dist/core/c4/resolve/service.js +25 -1
- package/dist/core/c4/resolve/service.js.map +1 -1
- package/dist/core/c4/seed/template.js +15 -1
- package/dist/core/c4/seed/template.js.map +1 -1
- package/dist/core/c4/service-model/shape.js +2 -22
- package/dist/core/c4/service-model/shape.js.map +1 -1
- package/dist/core/c4/source-mask.js +288 -4
- package/dist/core/c4/source-mask.js.map +1 -1
- package/dist/core/c4/source-scan.js +203 -147
- package/dist/core/c4/source-scan.js.map +1 -1
- package/dist/core/c4/splice/authored-source.js +19 -0
- package/dist/core/c4/splice/authored-source.js.map +1 -1
- package/dist/core/c4/splice/delta-blocks.js +5 -18
- package/dist/core/c4/splice/delta-blocks.js.map +1 -1
- package/dist/core/c4/splice/landscape-merge.js +13 -8
- package/dist/core/c4/splice/landscape-merge.js.map +1 -1
- package/dist/core/c4/splice/model/merge.js +10 -6
- package/dist/core/c4/splice/model/merge.js.map +1 -1
- package/dist/core/c4/splice/placement.js +108 -18
- package/dist/core/c4/splice/placement.js.map +1 -1
- package/dist/core/c4/workspace.js +13 -22
- package/dist/core/c4/workspace.js.map +1 -1
- package/dist/core/capabilities/capabilities.js +6 -6
- package/dist/core/capabilities/capabilities.js.map +1 -1
- package/dist/core/capabilities/delta/doc.js +7 -10
- package/dist/core/capabilities/delta/doc.js.map +1 -1
- package/dist/core/capabilities/findings.js +15 -20
- package/dist/core/capabilities/findings.js.map +1 -1
- package/dist/core/capabilities/realizes/findings.js +12 -19
- package/dist/core/capabilities/realizes/findings.js.map +1 -1
- package/dist/core/capabilities/realizes/join.js +0 -20
- package/dist/core/capabilities/realizes/join.js.map +1 -1
- package/dist/core/capabilities/rollup.js +6 -0
- package/dist/core/capabilities/rollup.js.map +1 -1
- package/dist/core/capabilities/tree.js +6 -28
- package/dist/core/capabilities/tree.js.map +1 -1
- package/dist/core/capabilities/usecase-join.js +3 -13
- package/dist/core/capabilities/usecase-join.js.map +1 -1
- package/dist/core/coherence/authoring/scaffold.js +11 -3
- package/dist/core/coherence/authoring/scaffold.js.map +1 -1
- package/dist/core/coherence/authoring/sentinels.js +2 -1
- package/dist/core/coherence/authoring/sentinels.js.map +1 -1
- package/dist/core/coherence/coherence.js +30 -16
- package/dist/core/coherence/coherence.js.map +1 -1
- package/dist/core/coherence/consumed/operations.js +70 -0
- package/dist/core/coherence/consumed/operations.js.map +1 -0
- package/dist/core/coherence/consumed/refusal.js +38 -0
- package/dist/core/coherence/consumed/refusal.js.map +1 -0
- package/dist/core/coherence/declared.js +60 -34
- package/dist/core/coherence/declared.js.map +1 -1
- package/dist/core/coherence/events/declared.js +44 -59
- package/dist/core/coherence/events/declared.js.map +1 -1
- package/dist/core/coherence/events/events.js +119 -8
- package/dist/core/coherence/events/events.js.map +1 -1
- package/dist/core/coherence/events/grades.js +31 -8
- package/dist/core/coherence/events/grades.js.map +1 -1
- package/dist/core/coherence/events/lookups.js +14 -82
- package/dist/core/coherence/events/lookups.js.map +1 -1
- package/dist/core/coherence/lookups.js +18 -78
- package/dist/core/coherence/lookups.js.map +1 -1
- package/dist/core/coherence/retired/archived.js +285 -0
- package/dist/core/coherence/retired/archived.js.map +1 -0
- package/dist/core/coherence/retired/event-pair.js +62 -0
- package/dist/core/coherence/retired/event-pair.js.map +1 -0
- package/dist/core/coherence/retired/merged.js +79 -0
- package/dist/core/coherence/retired/merged.js.map +1 -0
- package/dist/core/coherence/retired/retirement.js +49 -0
- package/dist/core/coherence/retired/retirement.js.map +1 -0
- package/dist/core/coherence/retired/revival/arrivals.js +131 -0
- package/dist/core/coherence/retired/revival/arrivals.js.map +1 -0
- package/dist/core/coherence/retired/revival/revived.js +297 -0
- package/dist/core/coherence/retired/revival/revived.js.map +1 -0
- package/dist/core/consumers/after.js +113 -0
- package/dist/core/consumers/after.js.map +1 -0
- package/dist/core/consumers/consumers.js +296 -0
- package/dist/core/consumers/consumers.js.map +1 -0
- package/dist/core/consumers/current.js +211 -0
- package/dist/core/consumers/current.js.map +1 -0
- package/dist/core/consumers/scan.js +46 -0
- package/dist/core/consumers/scan.js.map +1 -0
- package/dist/core/consumers/state.js +27 -0
- package/dist/core/consumers/state.js.map +1 -0
- package/dist/core/delta/delta.js +1 -1
- package/dist/core/delta/delta.js.map +1 -1
- package/dist/core/deployment/delta.js +15 -16
- package/dist/core/deployment/delta.js.map +1 -1
- package/dist/core/diff/base-git.js +94 -12
- package/dist/core/diff/base-git.js.map +1 -1
- package/dist/core/diff/base-state.js +28 -16
- package/dist/core/diff/base-state.js.map +1 -1
- package/dist/core/diff/joins.js +136 -0
- package/dist/core/diff/joins.js.map +1 -0
- package/dist/core/diff/scope/changed-paths.js +22 -6
- package/dist/core/diff/scope/changed-paths.js.map +1 -1
- package/dist/core/diff/scope/edges.js +82 -0
- package/dist/core/diff/scope/edges.js.map +1 -0
- package/dist/core/diff/semantic.js +100 -31
- package/dist/core/diff/semantic.js.map +1 -1
- package/dist/core/doctor/agents.js +34 -18
- package/dist/core/doctor/agents.js.map +1 -1
- package/dist/core/doctor/report.js.map +1 -1
- package/dist/core/document/apply.js +10 -2
- package/dist/core/document/apply.js.map +1 -1
- package/dist/core/document/frontmatter.js +55 -0
- package/dist/core/document/frontmatter.js.map +1 -1
- package/dist/core/document/parse.js +29 -6
- package/dist/core/document/parse.js.map +1 -1
- package/dist/core/document/scenarios.js +17 -0
- package/dist/core/document/scenarios.js.map +1 -1
- package/dist/core/document/spec.js +4 -11
- package/dist/core/document/spec.js.map +1 -1
- package/dist/core/envelope/config.js +10 -0
- package/dist/core/envelope/config.js.map +1 -1
- package/dist/core/envelope/json.js +34 -0
- package/dist/core/envelope/json.js.map +1 -1
- package/dist/core/explain/families.js +17 -12
- package/dist/core/explain/families.js.map +1 -1
- package/dist/core/explain/refusals.js +3 -3
- package/dist/core/explain/refusals.js.map +1 -1
- package/dist/core/explain/terms.js +18 -9
- package/dist/core/explain/terms.js.map +1 -1
- package/dist/core/explore/explore.js +2 -7
- package/dist/core/explore/explore.js.map +1 -1
- package/dist/core/fleet-context.js +34 -95
- package/dist/core/fleet-context.js.map +1 -1
- package/dist/core/gate/checks.js +10 -3
- package/dist/core/gate/checks.js.map +1 -1
- package/dist/core/gate/partners.js +64 -19
- package/dist/core/gate/partners.js.map +1 -1
- package/dist/core/gate/report.js.map +1 -1
- package/dist/core/gate/verification.js +75 -12
- package/dist/core/gate/verification.js.map +1 -1
- package/dist/core/gherkin/stale.js +13 -37
- package/dist/core/gherkin/stale.js.map +1 -1
- package/dist/core/glossary/tree.js +6 -19
- package/dist/core/glossary/tree.js.map +1 -1
- package/dist/core/kernel/concurrency.js +19 -0
- package/dist/core/kernel/concurrency.js.map +1 -1
- package/dist/core/kernel/records.js +11 -0
- package/dist/core/kernel/records.js.map +1 -1
- package/dist/core/links/backlinks.js +1 -3
- package/dist/core/links/backlinks.js.map +1 -1
- package/dist/core/links/corpus.js +5 -36
- package/dist/core/links/corpus.js.map +1 -1
- package/dist/core/mcp/argv.js +101 -0
- package/dist/core/mcp/argv.js.map +1 -1
- package/dist/core/mcp/author-tools.js +2 -1
- package/dist/core/mcp/author-tools.js.map +1 -1
- package/dist/core/mcp/protocol.js +2 -2
- package/dist/core/mcp/protocol.js.map +1 -1
- package/dist/core/mcp/tools.js +43 -33
- package/dist/core/mcp/tools.js.map +1 -1
- package/dist/core/openapi/baseline/gate.js +53 -12
- package/dist/core/openapi/baseline/gate.js.map +1 -1
- package/dist/core/openapi/baseline/plan.js +22 -8
- package/dist/core/openapi/baseline/plan.js.map +1 -1
- package/dist/core/openapi/baseline/record.js +0 -1
- package/dist/core/openapi/baseline/record.js.map +1 -1
- package/dist/core/openapi/contract-text.js +33 -0
- package/dist/core/openapi/contract-text.js.map +1 -0
- package/dist/core/openapi/contract-tree.js +56 -0
- package/dist/core/openapi/contract-tree.js.map +1 -0
- package/dist/core/openapi/depth.js +38 -24
- package/dist/core/openapi/depth.js.map +1 -1
- package/dist/core/openapi/digest.js +52 -2
- package/dist/core/openapi/digest.js.map +1 -1
- package/dist/core/openapi/doc.js +12 -4
- package/dist/core/openapi/doc.js.map +1 -1
- package/dist/core/openapi/merge/components.js +25 -60
- package/dist/core/openapi/merge/components.js.map +1 -1
- package/dist/core/openapi/merge/error.js +3 -13
- package/dist/core/openapi/merge/error.js.map +1 -1
- package/dist/core/openapi/merge/markers.js +26 -30
- package/dist/core/openapi/merge/markers.js.map +1 -1
- package/dist/core/openapi/merge/merge.js +26 -52
- package/dist/core/openapi/merge/merge.js.map +1 -1
- package/dist/core/openapi/merge/pin.js +15 -35
- package/dist/core/openapi/merge/pin.js.map +1 -1
- package/dist/core/openspec/decide/verdict.js.map +1 -1
- package/dist/core/openspec/inventory.js +4 -5
- package/dist/core/openspec/inventory.js.map +1 -1
- package/dist/core/openspec/model/mapping.js +30 -28
- package/dist/core/openspec/model/mapping.js.map +1 -1
- package/dist/core/openspec/model/model.js +12 -7
- package/dist/core/openspec/model/model.js.map +1 -1
- package/dist/core/openspec/scan/changes.js +10 -4
- package/dist/core/openspec/scan/changes.js.map +1 -1
- package/dist/core/openspec/scan/config.js +7 -16
- package/dist/core/openspec/scan/config.js.map +1 -1
- package/dist/core/openspec/scan/shape.js +63 -19
- package/dist/core/openspec/scan/shape.js.map +1 -1
- package/dist/core/openspec/scan/walk.js +21 -38
- package/dist/core/openspec/scan/walk.js.map +1 -1
- package/dist/core/pack/living.js +2 -16
- package/dist/core/pack/living.js.map +1 -1
- package/dist/core/provenance/findings.js +48 -12
- package/dist/core/provenance/findings.js.map +1 -1
- package/dist/core/provenance/git.js +33 -57
- package/dist/core/provenance/git.js.map +1 -1
- package/dist/core/provenance/gitq/moves.js +71 -114
- package/dist/core/provenance/gitq/moves.js.map +1 -1
- package/dist/core/provenance/gitq/vouched-ancestor.js +2 -2
- package/dist/core/provenance/gitq/vouched-ancestor.js.map +1 -1
- package/dist/core/provenance/sources.js +23 -6
- package/dist/core/provenance/sources.js.map +1 -1
- package/dist/core/provenance/stamp.js +5 -17
- package/dist/core/provenance/stamp.js.map +1 -1
- package/dist/core/repo/entries.js.map +1 -1
- package/dist/core/repo/paths.js +34 -29
- package/dist/core/repo/paths.js.map +1 -1
- package/dist/core/repo/repo.js +24 -14
- package/dist/core/repo/repo.js.map +1 -1
- package/dist/core/repo/tree/find.js +12 -3
- package/dist/core/repo/tree/find.js.map +1 -1
- package/dist/core/repo/tree/fs.js +27 -1
- package/dist/core/repo/tree/fs.js.map +1 -1
- package/dist/core/repo/tree/render/body.js +45 -6
- package/dist/core/repo/tree/render/body.js.map +1 -1
- package/dist/core/repo/tree/render/service/header.js +125 -0
- package/dist/core/repo/tree/render/service/header.js.map +1 -0
- package/dist/core/repo/tree/render/service/peers.js +28 -0
- package/dist/core/repo/tree/render/service/peers.js.map +1 -0
- package/dist/core/repo/tree/render/service/view-id.js +67 -0
- package/dist/core/repo/tree/render/service/view-id.js.map +1 -0
- package/dist/core/repo/tree/render/service/views.js +78 -0
- package/dist/core/repo/tree/render/service/views.js.map +1 -0
- package/dist/core/repo/tree/render/stale.js +19 -2
- package/dist/core/repo/tree/render/stale.js.map +1 -1
- package/dist/core/repo/tree/render/views.js +102 -33
- package/dist/core/repo/tree/render/views.js.map +1 -1
- package/dist/core/repo/tree/walk.js +8 -30
- package/dist/core/repo/tree/walk.js.map +1 -1
- package/dist/core/results.js +47 -12
- package/dist/core/results.js.map +1 -1
- package/dist/core/staging/commit.js +10 -12
- package/dist/core/staging/commit.js.map +1 -1
- package/dist/core/staging/lock.js +74 -12
- package/dist/core/staging/lock.js.map +1 -1
- package/dist/core/staging/recovery/intent.js +2 -5
- package/dist/core/staging/recovery/intent.js.map +1 -1
- package/dist/core/staging/recovery/recover.js +60 -1
- package/dist/core/staging/recovery/recover.js.map +1 -1
- package/dist/core/staging/snapshot/manifest.js +152 -0
- package/dist/core/staging/snapshot/manifest.js.map +1 -0
- package/dist/core/staging/{snapshot.js → snapshot/snapshot.js} +10 -34
- package/dist/core/staging/snapshot/snapshot.js.map +1 -0
- package/dist/core/status/actions/execution.js +11 -0
- package/dist/core/status/actions/execution.js.map +1 -1
- package/dist/core/status/feature/artifacts.js +2 -2
- package/dist/core/status/feature/artifacts.js.map +1 -1
- package/dist/core/status/feature/feature.js +5 -11
- package/dist/core/status/feature/feature.js.map +1 -1
- package/dist/core/status/fleet/fleet.js +6 -2
- package/dist/core/status/fleet/fleet.js.map +1 -1
- package/dist/core/status/fleet/next.js +26 -3
- package/dist/core/status/fleet/next.js.map +1 -1
- package/dist/core/status/report.js.map +1 -1
- package/dist/core/status/verification.js +30 -15
- package/dist/core/status/verification.js.map +1 -1
- package/dist/core/tasks/packet.js +174 -0
- package/dist/core/tasks/packet.js.map +1 -0
- package/dist/core/tasks/record.js +43 -0
- package/dist/core/tasks/record.js.map +1 -0
- package/dist/core/tasks/sources.js +181 -0
- package/dist/core/tasks/sources.js.map +1 -0
- package/dist/core/tasks/stamps.js +76 -0
- package/dist/core/tasks/stamps.js.map +1 -0
- package/dist/core/tasks/unit.js +0 -0
- package/dist/core/tasks/unit.js.map +1 -0
- package/dist/core/usecases/capability.js +11 -14
- package/dist/core/usecases/capability.js.map +1 -1
- package/dist/core/usecases/delta/flows.js +5 -7
- package/dist/core/usecases/delta/flows.js.map +1 -1
- package/dist/core/usecases/fleet.js +178 -31
- package/dist/core/usecases/fleet.js.map +1 -1
- package/dist/core/usecases/operations.js +5 -4
- package/dist/core/usecases/operations.js.map +1 -1
- package/dist/core/usecases/service/flows.js +9 -6
- package/dist/core/usecases/service/flows.js.map +1 -1
- package/dist/core/usecases/suites/participants.js +83 -0
- package/dist/core/usecases/suites/participants.js.map +1 -0
- package/dist/core/usecases/suites/suites.js +45 -0
- package/dist/core/usecases/suites/suites.js.map +1 -0
- package/dist/core/verify/answers.js +12 -5
- package/dist/core/verify/answers.js.map +1 -1
- package/dist/core/verify/build.js +102 -82
- package/dist/core/verify/build.js.map +1 -1
- package/dist/core/verify/checklist.js +45 -10
- package/dist/core/verify/checklist.js.map +1 -1
- package/dist/core/verify/location.js +67 -0
- package/dist/core/verify/location.js.map +1 -0
- package/dist/core/verify/pins/lint.js +3 -2
- package/dist/core/verify/pins/lint.js.map +1 -1
- package/dist/core/verify/pins/pin.js +1 -1
- package/dist/core/verify/pins/pin.js.map +1 -1
- package/dist/core/verify/read/file.js +224 -0
- package/dist/core/verify/read/file.js.map +1 -0
- package/dist/core/verify/{file.js → read/shape.js} +16 -104
- package/dist/core/verify/read/shape.js.map +1 -0
- package/dist/core/verify/read/union.js +105 -0
- package/dist/core/verify/read/union.js.map +1 -0
- package/dist/core/verify/record.js +35 -12
- package/dist/core/verify/record.js.map +1 -1
- package/dist/core/verify/store/commit.js +40 -30
- package/dist/core/verify/store/commit.js.map +1 -1
- package/dist/core/verify/store/render.js +8 -4
- package/dist/core/verify/store/render.js.map +1 -1
- package/dist/core/vocabulary/issue.js +1 -0
- package/dist/core/vocabulary/issue.js.map +1 -1
- package/dist/core/vocabulary/maturity.js +53 -3
- package/dist/core/vocabulary/maturity.js.map +1 -1
- package/examples/README.md +28 -9
- package/examples/docs/architecture/landscape.likec4 +21 -7
- package/examples/docs/architecture/services.likec4 +70 -0
- package/examples/docs/architecture/subsystems.likec4 +3 -0
- package/examples/docs/architecture/usecases/order-notification.likec4 +6 -2
- package/examples/docs/features/FEAT-101-payment-splitting/specs/payment-service/asyncapi.yaml +11 -8
- package/examples/docs/features/FEAT-101-payment-splitting/specs/payment-split-service/openapi.yaml +6 -6
- package/examples/docs/features/archive/FEAT-088-refunds/specs/payment-service/openapi.yaml +13 -10
- package/examples/docs/services/order-service/asyncapi.yaml +3 -2
- package/examples/docs/services/order-service/model.likec4 +10 -8
- package/examples/docs/services/payment-service/model.likec4 +6 -4
- package/examples/docs/services/platform/identity-service/model.likec4 +9 -7
- package/examples/docs/services/platform/notification-service/asyncapi.yaml +2 -1
- package/examples/docs/services/platform/notification-service/model.likec4 +4 -5
- package/package.json +1 -1
- package/dist/commands/validate/checks/vocabulary.js +0 -16
- package/dist/commands/validate/checks/vocabulary.js.map +0 -1
- package/dist/core/agent/agents-md/artifacts.js +0 -221
- package/dist/core/agent/agents-md/artifacts.js.map +0 -1
- package/dist/core/agent/agents-md/cycle.js +0 -180
- package/dist/core/agent/agents-md/cycle.js.map +0 -1
- package/dist/core/agent/agents-md/shipped/archive-gate.js +0 -98
- package/dist/core/agent/agents-md/shipped/archive-gate.js.map +0 -1
- package/dist/core/agent/agents-md/spine.js +0 -40
- package/dist/core/agent/agents-md/spine.js.map +0 -1
- package/dist/core/coherence/events/merged.js +0 -40
- package/dist/core/coherence/events/merged.js.map +0 -1
- package/dist/core/diff/current-state.js +0 -96
- package/dist/core/diff/current-state.js.map +0 -1
- package/dist/core/diff/victims.js +0 -256
- package/dist/core/diff/victims.js.map +0 -1
- package/dist/core/staging/snapshot.js.map +0 -1
- package/dist/core/verify/file.js.map +0 -1
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"sections.js","sourceRoot":"","sources":["../../../../src/commands/vouch/pack/sections.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * The reading pack's section-level derivations: pure text in, lists of\n * headings out. Three of them, and the last two are why a sampled vouch does\n * not quietly weaken this command —\n *\n * - `sectionDelta`, the heading summary printed beside the raw git diff and\n * the source of the \"already covered\" list;\n * - `forwardSample`, what the next `--sample <n>` vouch will read;\n * - `priorScope`, what the last sampled vouch actually did read, so the\n * covered list cannot count sections nobody opened.\n *\n * It lives in the vouch command package rather than `core/document` because\n * that package sits at its five-file limit and this command package already\n * holds vouch's non-printing logic (`../vet/verify.ts` precedent); the one\n * structural question — what is an H2 heading — is still answered by\n * `core/document/parse.ts`'s `sectionHeadings`, fence- and BOM-aware, so this\n * module can never disagree with the rest of loam about whether a `## ` line\n * inside a code fence is structure.\n */\nimport { rawBody, type Frontmatter } from \"../../../core/document/frontmatter.js\";\nimport { sectionHeadings } from \"../../../core/document/parse.js\";\nimport { splitSections } from \"../../../core/provenance/sample/sections.js\";\nimport { planSample, readVouchScope, sampledSections, scopeText, type VouchScope } from \"../../../core/provenance/sample/scope.js\";\nimport { contentDigest } from \"../../../core/provenance/stamp.js\";\n\n/** How the delta names the text above the first H2 — a section with no heading to call it by. */\nconst PREAMBLE_LABEL = \"(before the first heading)\";\n\n/** The four verdicts, each a list of H2 heading lines as the document spells them. */\nexport interface SectionDelta {\n /** Present in both bodies with different text — in the new body's order. */\n changed: string[];\n /** Present only in the new body, in its order. */\n added: string[];\n /** Present only in the old body, in its order. */\n removed: string[];\n /** Present in both with identical text — the \"already covered\" list. */\n unchanged: string[];\n}\n\n/**\n * CRLF/CR to LF, the `core/verify/pins/pin.ts` recipe: the old body comes out\n * of a git blob (LF) while the new one comes off a checkout that may be CRLF,\n * and a comparison that let the line endings vote would report every section\n * changed on exactly the machines whose config differs from the repo's.\n * A pure line-ending change is invisible here — the same cost the pins\n * accept, and for a reading list it is the right one: there is nothing to\n * re-read in a CR.\n */\nfunction normalized(text: string): string {\n return text.replace(/\\r\\n|\\r/g, \"\\n\");\n}\n\n/**\n * One body as heading -> section text. The heading LINE is the key and is not\n * part of the value, so a section whose only change is its own heading reads\n * as removed+added — which is what a reader must do about it: find it under\n * its new name. A duplicated heading concatenates its sections under the one\n * key rather than letting the last occurrence shadow the rest: the compare\n * then sees all of the text, so an edit under either twin still reads as\n * changed. Text before the first heading files under PREAMBLE_LABEL when any\n * of it is more than whitespace.\n */\nfunction sectionMap(body: string): Map<string, string> {\n const text = normalized(body);\n const lines = text.split(\"\\n\");\n const headings = sectionHeadings(text);\n const map = new Map<string, string>();\n const file = (key: string, segment: string): void => {\n map.set(key, map.has(key) ? `${map.get(key)}\\n${segment}` : segment);\n };\n const firstHeadingLine = headings[0]?.line ?? lines.length + 1;\n const preamble = lines.slice(0, firstHeadingLine - 1).join(\"\\n\");\n if (preamble.trim() !== \"\") map.set(PREAMBLE_LABEL, preamble);\n headings.forEach((heading, i) => {\n const end = headings[i + 1]?.line ?? lines.length + 1;\n file(heading.text, lines.slice(heading.line, end - 1).join(\"\\n\"));\n });\n return map;\n}\n\n/** The forward-looking sample: what a `--sample <n>` vouch of this file would cover. */\nexport interface PackSample {\n /** The seed the stamp will carry, derivable now because the body it hashes will not change. */\n seed: string;\n /** Every section the body has. */\n of: number;\n /** The sections to read, in document order. Headings only — the pack does not renumber lines. */\n headings: string[];\n /** True when `<n>` reached every section: the read is the whole file, and no scope is stamped. */\n covers: boolean;\n}\n\n/**\n * What `loam vouch --sample <n>` would read of this file — computed through\n * the same `planSample` the stamp uses, so a pack and the vouch that follows\n * it cannot prescribe different samples of one document. Both digests are the\n * ones the stamp will carry: `contentDigest` survives stamping because the\n * body does, and the sources digest is the vetting's own.\n */\nexport function forwardSample(doc: {\n service: string;\n /** The file as read, frontmatter included — the same string the stamp is applied to. */\n raw: string;\n sourcesDigest: string;\n n: number;\n}): PackSample {\n const planned = planSample({\n service: doc.service,\n contentDigest: contentDigest(doc.raw),\n sourcesDigest: doc.sourcesDigest,\n body: rawBody(doc.raw),\n n: doc.n,\n });\n return {\n seed: planned.seed,\n of: planned.of,\n headings: planned.picked.map((section) => section.heading),\n covers: planned.picked.length >= planned.of,\n };\n}\n\n/**\n * What the LAST vouch actually covered, when it was a sampled one.\n *\n * This is the pack's answer to the one way sampling could quietly undo the\n * whole point of it: the \"already covered\" list licenses a person to NOT\n * re-read a section, on the strength of somebody having read it before. After\n * a sampled vouch that strength is not there — most of those sections were\n * never read by anyone — so the licence has to be withdrawn, and withdrawn\n * with the detail that makes it actionable rather than as a blanket warning.\n *\n * The detail is recomputable because the stamp records the seed: run the same\n * pick over the body that vouch was taken over and you get back the exact\n * list that person was shown. `read` is that list; `unread` is every other\n * section of that body — text that has now survived a vouch without anybody\n * looking at it, which is precisely what a re-voucher most needs to know.\n *\n * Three things come back null-but-still-sampled, and all three fail the same\n * way, closed: a scope that does not decode (mangled by hand), no vouched\n * body to recompute over (the document has changed since the stamp, so the\n * pick cannot be reproduced), and a body whose section count no longer\n * matches what the scope claims. In each case the pack reports the scope and\n * refuses to name a covered set — never the reverse.\n */\nexport interface PackVouchScope {\n /** The `vouch_scope` value as a reader is shown it — verbatim where it is text at all. */\n stamped: string;\n /** Decoded, or null when the field is not one this loam wrote. */\n scope: VouchScope | null;\n /** The headings that vouch read, in document order — null when they cannot be recomputed. */\n read: string[] | null;\n /** The headings it did not read: present at that stamp, and never read by anyone. */\n unread: string[] | null;\n}\n\nexport function priorScope(fm: Frontmatter, vouchedBody: string | null): PackVouchScope | null {\n // One reader, `readVouchScope`, decides what \"sampled\" means — so the pack\n // cannot be the surface that quietly grades a mangled scope as a full vouch\n // while `loam validate` grades it sampled.\n const stamp = readVouchScope(fm);\n if (stamp.kind === \"none\") return null;\n const stamped = scopeText(fm) ?? \"\";\n const scope = stamp.kind === \"sampled\" ? stamp.scope : null;\n const blind: PackVouchScope = { stamped, scope, read: null, unread: null };\n if (scope === null || vouchedBody === null) return blind;\n const picked = sampledSections(vouchedBody, scope);\n if (picked === null) return blind;\n // Partitioned by LINE, not by heading text: a body with two `## Notes`\n // sections has two units, and matching on the text would report both as\n // read when one was picked — turning a section nobody opened into a covered\n // one, which is the exact misreading this function exists to prevent.\n const read = new Set(picked.map((section) => section.line));\n return {\n stamped,\n scope,\n read: picked.map((section) => section.heading),\n unread: splitSections(vouchedBody)\n .filter((section) => !read.has(section.line))\n .map((section) => section.heading),\n };\n}\n\n/**\n * Compare two bodies section by section. Exact text per heading after line\n * endings are normalized — no reflow tolerance, because \"unchanged\" here\n * licenses a person to NOT re-read a section, and the only claim safe to make\n * at that stakes is byte equality.\n */\nexport function sectionDelta(oldBody: string, newBody: string): SectionDelta {\n const before = sectionMap(oldBody);\n const after = sectionMap(newBody);\n const changed: string[] = [];\n const added: string[] = [];\n const unchanged: string[] = [];\n for (const [heading, text] of after) {\n const prior = before.get(heading);\n if (prior === undefined) added.push(heading);\n else if (prior === text) unchanged.push(heading);\n else changed.push(heading);\n }\n const removed = [...before.keys()].filter((heading) => !after.has(heading));\n return { changed, added, removed, unchanged };\n}\n"]}
|
|
1
|
+
{"version":3,"file":"sections.js","sourceRoot":"","sources":["../../../../src/commands/vouch/pack/sections.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * The reading pack's section-level derivations: pure text in, lists of\n * headings out. Three of them, and the last two are why a sampled vouch does\n * not quietly weaken this command —\n *\n * - `sectionDelta`, the heading summary printed beside the raw git diff and\n * the source of the \"already covered\" list;\n * - `forwardSample`, what the next `--sample <n>` vouch will read;\n * - `priorScope`, what the last sampled vouch actually did read, so the\n * covered list cannot count sections nobody opened.\n *\n * It lives in the vouch command package rather than `core/document` because\n * that package sits at its five-file limit and this command package already\n * holds vouch's non-printing logic (`../vet/verify.ts` precedent); the one\n * structural question — what is an H2 heading — is still answered by\n * `core/document/parse.ts`'s `sectionHeadings`, fence- and BOM-aware, so this\n * module can never disagree with the rest of loam about whether a `## ` line\n * inside a code fence is structure.\n */\nimport { contentDigest, rawBody, type Frontmatter } from \"../../../core/document/frontmatter.js\";\nimport { sectionHeadings } from \"../../../core/document/parse.js\";\nimport { splitSections } from \"../../../core/provenance/sample/sections.js\";\nimport { planSample, readVouchScope, sampledSections, scopeText, type VouchScope } from \"../../../core/provenance/sample/scope.js\";\n\n/** How the delta names the text above the first H2 — a section with no heading to call it by. */\nconst PREAMBLE_LABEL = \"(before the first heading)\";\n\n/** The four verdicts, each a list of H2 heading lines as the document spells them. */\nexport interface SectionDelta {\n /** Present in both bodies with different text — in the new body's order. */\n changed: string[];\n /** Present only in the new body, in its order. */\n added: string[];\n /** Present only in the old body, in its order. */\n removed: string[];\n /** Present in both with identical text — the \"already covered\" list. */\n unchanged: string[];\n}\n\n/**\n * CRLF/CR to LF, the `core/verify/pins/pin.ts` recipe: the old body comes out\n * of a git blob (LF) while the new one comes off a checkout that may be CRLF,\n * and a comparison that let the line endings vote would report every section\n * changed on exactly the machines whose config differs from the repo's.\n * A pure line-ending change is invisible here — the same cost the pins\n * accept, and for a reading list it is the right one: there is nothing to\n * re-read in a CR.\n */\nfunction normalized(text: string): string {\n return text.replace(/\\r\\n|\\r/g, \"\\n\");\n}\n\n/**\n * One body as heading -> section text. The heading LINE is the key and is not\n * part of the value, so a section whose only change is its own heading reads\n * as removed+added — which is what a reader must do about it: find it under\n * its new name. A duplicated heading concatenates its sections under the one\n * key rather than letting the last occurrence shadow the rest: the compare\n * then sees all of the text, so an edit under either twin still reads as\n * changed. Text before the first heading files under PREAMBLE_LABEL when any\n * of it is more than whitespace.\n */\nfunction sectionMap(body: string): Map<string, string> {\n const text = normalized(body);\n const lines = text.split(\"\\n\");\n const headings = sectionHeadings(text);\n const map = new Map<string, string>();\n const file = (key: string, segment: string): void => {\n map.set(key, map.has(key) ? `${map.get(key)}\\n${segment}` : segment);\n };\n const firstHeadingLine = headings[0]?.line ?? lines.length + 1;\n const preamble = lines.slice(0, firstHeadingLine - 1).join(\"\\n\");\n if (preamble.trim() !== \"\") map.set(PREAMBLE_LABEL, preamble);\n headings.forEach((heading, i) => {\n const end = headings[i + 1]?.line ?? lines.length + 1;\n file(heading.text, lines.slice(heading.line, end - 1).join(\"\\n\"));\n });\n return map;\n}\n\n/** The forward-looking sample: what a `--sample <n>` vouch of this file would cover. */\nexport interface PackSample {\n /** The seed the stamp will carry, derivable now because the body it hashes will not change. */\n seed: string;\n /** Every section the body has. */\n of: number;\n /** The sections to read, in document order. Headings only — the pack does not renumber lines. */\n headings: string[];\n /** True when `<n>` reached every section: the read is the whole file, and no scope is stamped. */\n covers: boolean;\n}\n\n/**\n * What `loam vouch --sample <n>` would read of this file — computed through\n * the same `planSample` the stamp uses, so a pack and the vouch that follows\n * it cannot prescribe different samples of one document. Both digests are the\n * ones the stamp will carry: `contentDigest` survives stamping because the\n * body does, and the sources digest is the vetting's own.\n */\nexport function forwardSample(doc: {\n service: string;\n /** The file as read, frontmatter included — the same string the stamp is applied to. */\n raw: string;\n sourcesDigest: string;\n n: number;\n}): PackSample {\n const planned = planSample({\n service: doc.service,\n contentDigest: contentDigest(doc.raw),\n sourcesDigest: doc.sourcesDigest,\n body: rawBody(doc.raw),\n n: doc.n,\n });\n return {\n seed: planned.seed,\n of: planned.of,\n headings: planned.picked.map((section) => section.heading),\n covers: planned.picked.length >= planned.of,\n };\n}\n\n/**\n * What the LAST vouch actually covered, when it was a sampled one.\n *\n * This is the pack's answer to the one way sampling could quietly undo the\n * whole point of it: the \"already covered\" list licenses a person to NOT\n * re-read a section, on the strength of somebody having read it before. After\n * a sampled vouch that strength is not there — most of those sections were\n * never read by anyone — so the licence has to be withdrawn, and withdrawn\n * with the detail that makes it actionable rather than as a blanket warning.\n *\n * The detail is recomputable because the stamp records the seed: run the same\n * pick over the body that vouch was taken over and you get back the exact\n * list that person was shown. `read` is that list; `unread` is every other\n * section of that body — text that has now survived a vouch without anybody\n * looking at it, which is precisely what a re-voucher most needs to know.\n *\n * Three things come back null-but-still-sampled, and all three fail the same\n * way, closed: a scope that does not decode (mangled by hand), no vouched\n * body to recompute over (the document has changed since the stamp, so the\n * pick cannot be reproduced), and a body whose section count no longer\n * matches what the scope claims. In each case the pack reports the scope and\n * refuses to name a covered set — never the reverse.\n */\nexport interface PackVouchScope {\n /** The `vouch_scope` value as a reader is shown it — verbatim where it is text at all. */\n stamped: string;\n /** Decoded, or null when the field is not one this loam wrote. */\n scope: VouchScope | null;\n /** The headings that vouch read, in document order — null when they cannot be recomputed. */\n read: string[] | null;\n /** The headings it did not read: present at that stamp, and never read by anyone. */\n unread: string[] | null;\n}\n\nexport function priorScope(fm: Frontmatter, vouchedBody: string | null): PackVouchScope | null {\n // One reader, `readVouchScope`, decides what \"sampled\" means — so the pack\n // cannot be the surface that quietly grades a mangled scope as a full vouch\n // while `loam validate` grades it sampled.\n const stamp = readVouchScope(fm);\n if (stamp.kind === \"none\") return null;\n const stamped = scopeText(fm) ?? \"\";\n const scope = stamp.kind === \"sampled\" ? stamp.scope : null;\n const blind: PackVouchScope = { stamped, scope, read: null, unread: null };\n if (scope === null || vouchedBody === null) return blind;\n const picked = sampledSections(vouchedBody, scope);\n if (picked === null) return blind;\n // Partitioned by LINE, not by heading text: a body with two `## Notes`\n // sections has two units, and matching on the text would report both as\n // read when one was picked — turning a section nobody opened into a covered\n // one, which is the exact misreading this function exists to prevent.\n const read = new Set(picked.map((section) => section.line));\n return {\n stamped,\n scope,\n read: picked.map((section) => section.heading),\n unread: splitSections(vouchedBody)\n .filter((section) => !read.has(section.line))\n .map((section) => section.heading),\n };\n}\n\n/**\n * Compare two bodies section by section. Exact text per heading after line\n * endings are normalized — no reflow tolerance, because \"unchanged\" here\n * licenses a person to NOT re-read a section, and the only claim safe to make\n * at that stakes is byte equality.\n */\nexport function sectionDelta(oldBody: string, newBody: string): SectionDelta {\n const before = sectionMap(oldBody);\n const after = sectionMap(newBody);\n const changed: string[] = [];\n const added: string[] = [];\n const unchanged: string[] = [];\n for (const [heading, text] of after) {\n const prior = before.get(heading);\n if (prior === undefined) added.push(heading);\n else if (prior === text) unchanged.push(heading);\n else changed.push(heading);\n }\n const removed = [...before.keys()].filter((heading) => !after.has(heading));\n return { changed, added, removed, unchanged };\n}\n"]}
|
|
@@ -21,22 +21,18 @@
|
|
|
21
21
|
* `loam validate` can see either side move. (This paragraph came from a
|
|
22
22
|
* docblock stranded at the foot of `vouch.ts` when the stamp moved down here.)
|
|
23
23
|
*/
|
|
24
|
-
import {
|
|
25
|
-
import { withFrontmatterFields } from "../../core/document/frontmatter.js";
|
|
24
|
+
import { contentDigest, withFrontmatterFields } from "../../core/document/frontmatter.js";
|
|
26
25
|
import { encodeVouchScope } from "../../core/provenance/sample/scope.js";
|
|
27
|
-
import {
|
|
28
|
-
import { SPEC_AXES } from "../../core/repo/paths.js";
|
|
29
|
-
import { locateServicePaths } from "../../core/repo/service-target.js";
|
|
26
|
+
import { encodeSourceIndex } from "../../core/provenance/stamp.js";
|
|
30
27
|
import { rollbackStaged, stageWrites } from "../../core/staging/commit.js";
|
|
31
|
-
import {
|
|
32
|
-
import {
|
|
33
|
-
import {
|
|
34
|
-
import { recoverInterruptedCommit } from "../../core/staging/recovery/recover.js";
|
|
28
|
+
import {} from "../../core/staging/interrupted.js";
|
|
29
|
+
import { LOCK_WAIT_MS } from "../../core/staging/lock.js";
|
|
30
|
+
import { openCommitWindow, recoverIfJournaled } from "../../core/staging/recovery/recover.js";
|
|
35
31
|
import { commitStaged } from "../../core/staging/txn/transaction.js";
|
|
36
32
|
import {} from "../../core/staging/writes.js";
|
|
37
33
|
import {} from "./contract.js";
|
|
38
34
|
import { axisScope, sampleDrift } from "./sample/plan.js";
|
|
39
|
-
import {
|
|
35
|
+
import { verifyAxes } from "./vet/verify.js";
|
|
40
36
|
export async function vouch(req) {
|
|
41
37
|
// A predecessor's journal is rolled forward BEFORE verification reads a
|
|
42
38
|
// byte. Recovery under the commit lock alone was tried first and kept as
|
|
@@ -44,54 +40,18 @@ export async function vouch(req) {
|
|
|
44
40
|
// run's own raced check then refused a sound stamp and the fix doctor
|
|
45
41
|
// prints took two runs instead of one. Cheap when there is nothing to do:
|
|
46
42
|
// one existsSync.
|
|
47
|
-
|
|
48
|
-
if (
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
recoveredEarly = await recoverInterruptedCommit(req.docsDir);
|
|
60
|
-
}
|
|
61
|
-
catch (err) {
|
|
62
|
-
if (err instanceof InterruptedCommitError)
|
|
63
|
-
return { ok: false, code: "commit-interrupted", message: err.message };
|
|
64
|
-
throw err;
|
|
65
|
-
}
|
|
66
|
-
finally {
|
|
67
|
-
await release();
|
|
68
|
-
}
|
|
69
|
-
}
|
|
70
|
-
const paths = await locateServicePaths(req.docsDir, req.service);
|
|
71
|
-
if (!existsSync(paths.spec)) {
|
|
72
|
-
return {
|
|
73
|
-
ok: false,
|
|
74
|
-
code: "unknown-target",
|
|
75
|
-
message: noLivingSpecMessage(paths.spec, req.service),
|
|
76
|
-
};
|
|
77
|
-
}
|
|
78
|
-
// Verify first, stamp after: spec.md is required (checked above), arch.spec.md
|
|
79
|
-
// rides when it exists, and one file that cannot be verified refuses the run
|
|
80
|
-
// before anything is written. The two axes are walked by name rather than
|
|
81
|
-
// accumulated in a loop, because everything downstream needs to know WHICH
|
|
82
|
-
// file it is holding — the required one or the optional one — and a loop can
|
|
83
|
-
// only say that positionally.
|
|
84
|
-
const [specAxis, archAxis] = SPEC_AXES;
|
|
85
|
-
const specVerified = await verifySpec(req, paths[specAxis.key], specAxis.file);
|
|
86
|
-
if (!specVerified.ok)
|
|
87
|
-
return specVerified;
|
|
88
|
-
let archVerified = null;
|
|
89
|
-
if (existsSync(paths[archAxis.key])) {
|
|
90
|
-
const outcome = await verifySpec(req, paths[archAxis.key], archAxis.file);
|
|
91
|
-
if (!outcome.ok)
|
|
92
|
-
return outcome;
|
|
93
|
-
archVerified = outcome;
|
|
94
|
-
}
|
|
43
|
+
const early = await recoverIfJournaled(req.docsDir);
|
|
44
|
+
if (!early.ok)
|
|
45
|
+
return early;
|
|
46
|
+
// Verify first, stamp after: spec.md is required (a missing one is
|
|
47
|
+
// `unknown-target`), arch.spec.md rides when it exists, and one file that
|
|
48
|
+
// cannot be verified refuses the run before anything is written. The walk is
|
|
49
|
+
// `verifyAxes` (vet/verify.ts), shared with the reading list, and it hands
|
|
50
|
+
// the two files back by name — vet/verify.ts says why not a loop.
|
|
51
|
+
const axes = await verifyAxes(req);
|
|
52
|
+
if (!axes.ok)
|
|
53
|
+
return axes;
|
|
54
|
+
const { spec: specVerified, arch: archVerified } = axes;
|
|
95
55
|
const verified = [specVerified, ...(archVerified === null ? [] : [archVerified])];
|
|
96
56
|
// The READING window's race, which the commit window's byte compare cannot
|
|
97
57
|
// see. Under `--sample` a person spends minutes on the list before they
|
|
@@ -114,31 +74,17 @@ export async function vouch(req) {
|
|
|
114
74
|
// could stage against bytes an archive was mid-swap over, and the byte
|
|
115
75
|
// compare below would refuse a perfectly sound stamp — or worse, an
|
|
116
76
|
// interrupted commit's journal would be silently written over.
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
return { ok: false, code: "docs-busy", message: err.message };
|
|
124
|
-
throw err;
|
|
125
|
-
}
|
|
77
|
+
//
|
|
78
|
+
// A journal under the lock means the last writer never finished; the window
|
|
79
|
+
// recovers or refuses before reading the pre-images this commit will compare.
|
|
80
|
+
const opened = await openCommitWindow(req.docsDir, LOCK_WAIT_MS);
|
|
81
|
+
if (!opened.ok)
|
|
82
|
+
return opened;
|
|
126
83
|
try {
|
|
127
|
-
|
|
128
|
-
// or refuse before reading the pre-images this commit will compare.
|
|
129
|
-
let recovered;
|
|
130
|
-
try {
|
|
131
|
-
recovered = await recoverInterruptedCommit(req.docsDir);
|
|
132
|
-
}
|
|
133
|
-
catch (err) {
|
|
134
|
-
if (err instanceof InterruptedCommitError)
|
|
135
|
-
return { ok: false, code: "commit-interrupted", message: err.message };
|
|
136
|
-
throw err;
|
|
137
|
-
}
|
|
138
|
-
return await commitStamp(req, { writes, specPlan, archPlan, verified }, recovered ?? recoveredEarly);
|
|
84
|
+
return await commitStamp(req, { writes, specPlan, archPlan, verified }, opened.recovered ?? early.recovered);
|
|
139
85
|
}
|
|
140
86
|
finally {
|
|
141
|
-
await
|
|
87
|
+
await opened.release();
|
|
142
88
|
}
|
|
143
89
|
}
|
|
144
90
|
/**
|
|
@@ -281,5 +227,4 @@ export function planStamp(v, req) {
|
|
|
281
227
|
},
|
|
282
228
|
};
|
|
283
229
|
}
|
|
284
|
-
/** A spec-axis file whose sources all check out, carrying what the stamp needs. */
|
|
285
230
|
//# sourceMappingURL=run.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"run.js","sourceRoot":"","sources":["../../../src/commands/vouch/run.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * The stamp itself: verify every spec, then write them all or none.\n *\n * Separate from the command because this half has nothing to say to a terminal.\n * It refuses on any spec it cannot verify and stamps none of them — a vouch is\n * a person's claim that the documents match the code, and a partial one would\n * put that claim on files nobody checked.\n *\n * What lands in each file: `status`, `last_verified`, `vouched_by`,\n * `sources_digest`, the `sources_files` index, `content_digest` — and\n * `vouch_scope` when the person read a sample rather than the document — with\n * every byte below the frontmatter left identical. Nothing is written unless\n * all of it can be stamped truthfully for EVERY file: a half-stamp (verified\n * with no digest behind it, or one file stamped and its sibling not) is\n * exactly the claim this command exists to stop being possible, so every\n * present file is verified before any is written, and the writing itself is\n * staged and swapped in like archive's merge, so a failure between the pair's\n * two writes rolls the first back instead of leaving it. The two digests are\n * the two halves of one promise — `sources_digest` pins the code that was\n * read, `content_digest` pins the words it was read against — so\n * `loam validate` can see either side move. (This paragraph came from a\n * docblock stranded at the foot of `vouch.ts` when the stamp moved down here.)\n */\nimport { existsSync } from \"node:fs\";\nimport { withFrontmatterFields } from \"../../core/document/frontmatter.js\";\nimport { encodeVouchScope } from \"../../core/provenance/sample/scope.js\";\nimport { contentDigest, encodeSourceIndex } from \"../../core/provenance/stamp.js\";\nimport { SPEC_AXES } from \"../../core/repo/paths.js\";\nimport { locateServicePaths } from \"../../core/repo/service-target.js\";\nimport { rollbackStaged, stageWrites } from \"../../core/staging/commit.js\";\nimport { join } from \"node:path\";\nimport { COMMIT_INTENT, type CommitRecovery, InterruptedCommitError } from \"../../core/staging/interrupted.js\";\nimport { acquireDocsLockWaiting, DocsBusyError, LOCK_WAIT_MS } from \"../../core/staging/lock.js\";\nimport { recoverInterruptedCommit } from \"../../core/staging/recovery/recover.js\";\nimport { commitStaged } from \"../../core/staging/txn/transaction.js\";\nimport { type PlannedWrite } from \"../../core/staging/writes.js\";\nimport { type StampedSpec, type VouchOutcome, type VouchRequest } from \"./contract.js\";\nimport { axisScope, sampleDrift } from \"./sample/plan.js\";\nimport { noLivingSpecMessage, verifySpec, type VerifiedSpec } from \"./vet/verify.js\";\n\nexport async function vouch(req: VouchRequest): Promise<VouchOutcome> {\n // A predecessor's journal is rolled forward BEFORE verification reads a\n // byte. Recovery under the commit lock alone was tried first and kept as\n // the backstop, but it changes files verification has already read, so the\n // run's own raced check then refused a sound stamp and the fix doctor\n // prints took two runs instead of one. Cheap when there is nothing to do:\n // one existsSync.\n let recoveredEarly: CommitRecovery | null = null;\n if (existsSync(join(req.docsDir, COMMIT_INTENT))) {\n let release: () => Promise<void>;\n try {\n release = await acquireDocsLockWaiting(req.docsDir, LOCK_WAIT_MS);\n } catch (err) {\n if (err instanceof DocsBusyError) return { ok: false, code: \"docs-busy\", message: err.message };\n throw err;\n }\n try {\n recoveredEarly = await recoverInterruptedCommit(req.docsDir);\n } catch (err) {\n if (err instanceof InterruptedCommitError) return { ok: false, code: \"commit-interrupted\", message: err.message };\n throw err;\n } finally {\n await release();\n }\n }\n const paths = await locateServicePaths(req.docsDir, req.service);\n if (!existsSync(paths.spec)) {\n return {\n ok: false,\n code: \"unknown-target\",\n message: noLivingSpecMessage(paths.spec, req.service),\n };\n }\n\n // Verify first, stamp after: spec.md is required (checked above), arch.spec.md\n // rides when it exists, and one file that cannot be verified refuses the run\n // before anything is written. The two axes are walked by name rather than\n // accumulated in a loop, because everything downstream needs to know WHICH\n // file it is holding — the required one or the optional one — and a loop can\n // only say that positionally.\n const [specAxis, archAxis] = SPEC_AXES;\n const specVerified = await verifySpec(req, paths[specAxis.key], specAxis.file);\n if (!specVerified.ok) return specVerified;\n let archVerified: VerifiedSpec | null = null;\n if (existsSync(paths[archAxis.key])) {\n const outcome = await verifySpec(req, paths[archAxis.key], archAxis.file);\n if (!outcome.ok) return outcome;\n archVerified = outcome;\n }\n const verified: VerifiedSpec[] = [specVerified, ...(archVerified === null ? [] : [archVerified])];\n\n // The READING window's race, which the commit window's byte compare cannot\n // see. Under `--sample` a person spends minutes on the list before they\n // answer, and the sample they were shown was chosen from digests taken\n // before that. If either digest moved — or a second axis file appeared —\n // the sections they read are not the sections this stamp would claim, and\n // `vouch_scope` would record a read that did not happen. Nothing is written.\n const raced = req.sample === undefined ? null : sampleDrift(req.sample, verified, req.service);\n if (raced !== null) return { ok: false, code: \"vouch-raced\", message: raced };\n\n const specPlan = planStamp(specVerified, req);\n const archPlan = archVerified === null ? null : planStamp(archVerified, req);\n const writes: PlannedWrite[] = [specPlan.write, ...(archPlan === null ? [] : [archPlan.write])];\n\n // The slow half — reading specs, hashing sources, composing stamps — ran\n // UNLOCKED above, so vouches for independent services do not serialise\n // behind one repo-wide mutex. Only the commit window takes the docs lock:\n // the same one archive, rebase and verify --record hold, in the waiting\n // form, because two vouches for different services are both supposed to\n // land. This is the lock the roadmap called missing: without it a vouch\n // could stage against bytes an archive was mid-swap over, and the byte\n // compare below would refuse a perfectly sound stamp — or worse, an\n // interrupted commit's journal would be silently written over.\n let releaseLock: () => Promise<void>;\n try {\n releaseLock = await acquireDocsLockWaiting(req.docsDir, LOCK_WAIT_MS);\n } catch (err) {\n if (err instanceof DocsBusyError) return { ok: false, code: \"docs-busy\", message: err.message };\n throw err;\n }\n try {\n // A journal under the lock means the last writer never finished; recover\n // or refuse before reading the pre-images this commit will compare.\n let recovered: CommitRecovery | null;\n try {\n recovered = await recoverInterruptedCommit(req.docsDir);\n } catch (err) {\n if (err instanceof InterruptedCommitError) return { ok: false, code: \"commit-interrupted\", message: err.message };\n throw err;\n }\n return await commitStamp(req, { writes, specPlan, archPlan, verified }, recovered ?? recoveredEarly);\n } finally {\n await releaseLock();\n }\n}\n\n/**\n * The commit window: stage, compare against what verification read, journal,\n * swap. Split from `vouch` so the lock's extent is visible in the code shape —\n * everything in here holds it, nothing above does.\n */\nasync function commitStamp(\n req: VouchRequest,\n plan: {\n writes: PlannedWrite[];\n specPlan: { write: PlannedWrite; stamped: StampedSpec };\n archPlan: { write: PlannedWrite; stamped: StampedSpec } | null;\n verified: VerifiedSpec[];\n },\n recovered: CommitRecovery | null,\n): Promise<VouchOutcome> {\n const { writes, specPlan, archPlan, verified } = plan;\n const staged = await stageWrites(writes);\n\n // One shared docs repo, ten service repos, and nothing stopping two of them\n // from vouching at once. Between reading a spec and swapping the stamp in\n // there is a window in which somebody else's vouch (or an editor, or a merge)\n // can land in the same file — and because the new bytes were computed from\n // the OLD ones, swapping them in would take that stamp back out without a\n // word. `stageWrites` has already read what is on disk right now, so the\n // check is a comparison, not another read: if the file is not what was\n // verified, this run is describing a document that no longer exists.\n // Bytes, not text: `stageWrites` reads the file as a Buffer (a string\n // comparison against it is never equal, which made every vouch report\n // `vouch-raced` and stamp nothing). A file that is GONE is still a race —\n // `null` is not \"empty\", it is \"the document this run described is not\n // there any more\".\n // Joined by path, not by index: pairing the two lists positionally assumed\n // `stageWrites` returns one entry per planned write in order — true today,\n // promised nowhere, and a compare-and-set that silently compares the wrong\n // pair would stamp over somebody else's vouch. A staged write no verified\n // file claims has nothing to be compared against, so it counts as raced;\n // fail-closed is the only safe direction here.\n const verifiedByPath = new Map(verified.map((v) => [v.path, v] as const));\n const raced = staged.filter((s) => {\n const v = verifiedByPath.get(s.write.path);\n return v === undefined || s.before === null || !s.before.equals(Buffer.from(v.raw, \"utf8\"));\n });\n if (raced.length > 0) {\n // Nothing has swapped yet, so the rollback is only the temp files going\n // away — the other writer's stamp is left exactly as it landed.\n await rollbackStaged(staged);\n return {\n ok: false,\n code: \"vouch-raced\",\n message:\n `${req.service}: ${raced.map((s) => s.write.path).join(\", \")} changed while this vouch was running — ` +\n `another vouch or an edit landed first. Nothing was stamped: re-read the document and re-run.`,\n };\n }\n\n // The journaled commit: intent fsynced before the first rename, so a kill\n // between the pair's two swaps is recoverable instead of invisible — the\n // exact half-stamped state the all-or-nothing verification exists to rule\n // out used to be reachable through that one window. The codes are archive's,\n // reused rather than minted: rolled back → nothing changed, re-running can\n // work; incomplete → the files listed need a human.\n const committed = await commitStaged(\n {\n root: req.docsDir,\n command: \"vouch\",\n // Runnable where vouch runs — the SERVICE repo, with --yes because the\n // journal's reader may be unattended. Any other journaled docs-repo\n // writer recovers this journal on its next run too.\n rerun: `loam vouch --service ${req.service} --yes`,\n target: req.service,\n },\n staged,\n \"stamped\",\n );\n if (!committed.ok) {\n return committed.raced\n ? {\n ok: false,\n code: \"vouch-raced\",\n message:\n `${req.service}: a document changed while this vouch was committing — ` +\n `another writer landed first. Nothing was stamped: re-read and re-run.`,\n }\n : { ok: false, code: committed.code, message: committed.message };\n }\n return {\n ok: true,\n status: \"verified\",\n lastVerified: req.today,\n vouchedBy: req.vouchedBy,\n recovered,\n stamped: { spec: specPlan.stamped, archSpec: archPlan === null ? null : archPlan.stamped },\n };\n}\n\n/**\n * One verified file's share of the commit: the bytes to write, and the record\n * of what was stamped into them. Kept beside each other because they are the\n * same computation — the report must describe the file that was actually\n * written, not a second guess at it.\n */\nexport function planStamp(v: VerifiedSpec, req: VouchRequest): { write: PlannedWrite; stamped: StampedSpec } {\n // Two passes on purpose: `content_digest` hashes the body BELOW the\n // frontmatter, and withFrontmatterFields promises that body byte-identical —\n // so hashing after the first stamp and writing the hash in a second one\n // yields a digest that is true of the file exactly as written. A re-vouch\n // takes the same road and refreshes every field, this one included.\n //\n // The scope this file's stamp will carry — null both when the run is an\n // ordinary vouch and when a sampled run's `--sample <n>` covered the whole\n // file. Joined to the plan by FILENAME rather than taken positionally: the\n // two axes are stamped by two separate calls, and a scope written onto the\n // wrong file would claim a person read four sections of a document they\n // never opened.\n const planned = req.sample?.axes.find((axis) => axis.file === v.file);\n // A verified file with no planned axis is unreachable — `sampleDrift`\n // refuses that run before this line — and null is the safe direction if it\n // ever were: a full stamp that CLEARS any prior scope, never one claiming a\n // sample nobody was shown.\n const scope = planned === undefined ? null : axisScope(planned);\n const restamped = withFrontmatterFields(\n v.raw,\n {\n status: \"verified\",\n last_verified: req.today,\n // WHO, beside when. Without it `status: verified` recorded that the word\n // had been written and nothing about who wrote it, so a vouch and an\n // agent's own draft left the same trace in the document — the one\n // distinction this command exists to make.\n vouched_by: req.vouchedBy,\n sources_digest: v.digest,\n // Beside the digest, what it was taken over. `sources_digest` alone can\n // only ever say THAT the code moved; the next `loam validate` reads this\n // back to say which files did.\n sources_files: encodeSourceIndex(v.index),\n // Beside `vouched_by`, never inside `status`: the rung string `vouched`\n // and the status `verified` are a published contract, and a sampled read\n // is not a fourth status — it is a qualification of this one.\n ...(scope === null ? {} : { vouch_scope: encodeVouchScope(scope) }),\n },\n // A full vouch CLEARS any scope a previous sampled one left, rather than\n // stranding a claim that would keep a fully-read document reading as\n // partial forever. Removing a key that was never there is a no-op, so\n // this rides on every run that is not itself sampling.\n scope === null ? [\"vouch_scope\"] : [],\n );\n const bodyDigest = contentDigest(restamped);\n return {\n write: { path: v.path, content: withFrontmatterFields(restamped, { content_digest: bodyDigest }) },\n stamped: {\n path: v.path,\n file: v.file,\n digest: v.digest,\n contentDigest: bodyDigest,\n sources: v.sources,\n files: v.index.length,\n skipped: v.skipped,\n vouchScope: scope,\n },\n };\n}\n\n/** A spec-axis file whose sources all check out, carrying what the stamp needs. */\n"]}
|
|
1
|
+
{"version":3,"file":"run.js","sourceRoot":"","sources":["../../../src/commands/vouch/run.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * The stamp itself: verify every spec, then write them all or none.\n *\n * Separate from the command because this half has nothing to say to a terminal.\n * It refuses on any spec it cannot verify and stamps none of them — a vouch is\n * a person's claim that the documents match the code, and a partial one would\n * put that claim on files nobody checked.\n *\n * What lands in each file: `status`, `last_verified`, `vouched_by`,\n * `sources_digest`, the `sources_files` index, `content_digest` — and\n * `vouch_scope` when the person read a sample rather than the document — with\n * every byte below the frontmatter left identical. Nothing is written unless\n * all of it can be stamped truthfully for EVERY file: a half-stamp (verified\n * with no digest behind it, or one file stamped and its sibling not) is\n * exactly the claim this command exists to stop being possible, so every\n * present file is verified before any is written, and the writing itself is\n * staged and swapped in like archive's merge, so a failure between the pair's\n * two writes rolls the first back instead of leaving it. The two digests are\n * the two halves of one promise — `sources_digest` pins the code that was\n * read, `content_digest` pins the words it was read against — so\n * `loam validate` can see either side move. (This paragraph came from a\n * docblock stranded at the foot of `vouch.ts` when the stamp moved down here.)\n */\nimport { contentDigest, withFrontmatterFields } from \"../../core/document/frontmatter.js\";\nimport { encodeVouchScope } from \"../../core/provenance/sample/scope.js\";\nimport { encodeSourceIndex } from \"../../core/provenance/stamp.js\";\nimport { rollbackStaged, stageWrites } from \"../../core/staging/commit.js\";\nimport { type CommitRecovery } from \"../../core/staging/interrupted.js\";\nimport { LOCK_WAIT_MS } from \"../../core/staging/lock.js\";\nimport { openCommitWindow, recoverIfJournaled } from \"../../core/staging/recovery/recover.js\";\nimport { commitStaged } from \"../../core/staging/txn/transaction.js\";\nimport { type PlannedWrite } from \"../../core/staging/writes.js\";\nimport { type StampedSpec, type VouchOutcome, type VouchRequest } from \"./contract.js\";\nimport { axisScope, sampleDrift } from \"./sample/plan.js\";\nimport { verifyAxes, type VerifiedSpec } from \"./vet/verify.js\";\n\nexport async function vouch(req: VouchRequest): Promise<VouchOutcome> {\n // A predecessor's journal is rolled forward BEFORE verification reads a\n // byte. Recovery under the commit lock alone was tried first and kept as\n // the backstop, but it changes files verification has already read, so the\n // run's own raced check then refused a sound stamp and the fix doctor\n // prints took two runs instead of one. Cheap when there is nothing to do:\n // one existsSync.\n const early = await recoverIfJournaled(req.docsDir);\n if (!early.ok) return early;\n // Verify first, stamp after: spec.md is required (a missing one is\n // `unknown-target`), arch.spec.md rides when it exists, and one file that\n // cannot be verified refuses the run before anything is written. The walk is\n // `verifyAxes` (vet/verify.ts), shared with the reading list, and it hands\n // the two files back by name — vet/verify.ts says why not a loop.\n const axes = await verifyAxes(req);\n if (!axes.ok) return axes;\n const { spec: specVerified, arch: archVerified } = axes;\n const verified: VerifiedSpec[] = [specVerified, ...(archVerified === null ? [] : [archVerified])];\n\n // The READING window's race, which the commit window's byte compare cannot\n // see. Under `--sample` a person spends minutes on the list before they\n // answer, and the sample they were shown was chosen from digests taken\n // before that. If either digest moved — or a second axis file appeared —\n // the sections they read are not the sections this stamp would claim, and\n // `vouch_scope` would record a read that did not happen. Nothing is written.\n const raced = req.sample === undefined ? null : sampleDrift(req.sample, verified, req.service);\n if (raced !== null) return { ok: false, code: \"vouch-raced\", message: raced };\n\n const specPlan = planStamp(specVerified, req);\n const archPlan = archVerified === null ? null : planStamp(archVerified, req);\n const writes: PlannedWrite[] = [specPlan.write, ...(archPlan === null ? [] : [archPlan.write])];\n\n // The slow half — reading specs, hashing sources, composing stamps — ran\n // UNLOCKED above, so vouches for independent services do not serialise\n // behind one repo-wide mutex. Only the commit window takes the docs lock:\n // the same one archive, rebase and verify --record hold, in the waiting\n // form, because two vouches for different services are both supposed to\n // land. This is the lock the roadmap called missing: without it a vouch\n // could stage against bytes an archive was mid-swap over, and the byte\n // compare below would refuse a perfectly sound stamp — or worse, an\n // interrupted commit's journal would be silently written over.\n //\n // A journal under the lock means the last writer never finished; the window\n // recovers or refuses before reading the pre-images this commit will compare.\n const opened = await openCommitWindow(req.docsDir, LOCK_WAIT_MS);\n if (!opened.ok) return opened;\n try {\n return await commitStamp(req, { writes, specPlan, archPlan, verified }, opened.recovered ?? early.recovered);\n } finally {\n await opened.release();\n }\n}\n\n/**\n * The commit window: stage, compare against what verification read, journal,\n * swap. Split from `vouch` so the lock's extent is visible in the code shape —\n * everything in here holds it, nothing above does.\n */\nasync function commitStamp(\n req: VouchRequest,\n plan: {\n writes: PlannedWrite[];\n specPlan: { write: PlannedWrite; stamped: StampedSpec };\n archPlan: { write: PlannedWrite; stamped: StampedSpec } | null;\n verified: VerifiedSpec[];\n },\n recovered: CommitRecovery | null,\n): Promise<VouchOutcome> {\n const { writes, specPlan, archPlan, verified } = plan;\n const staged = await stageWrites(writes);\n\n // One shared docs repo, ten service repos, and nothing stopping two of them\n // from vouching at once. Between reading a spec and swapping the stamp in\n // there is a window in which somebody else's vouch (or an editor, or a merge)\n // can land in the same file — and because the new bytes were computed from\n // the OLD ones, swapping them in would take that stamp back out without a\n // word. `stageWrites` has already read what is on disk right now, so the\n // check is a comparison, not another read: if the file is not what was\n // verified, this run is describing a document that no longer exists.\n // Bytes, not text: `stageWrites` reads the file as a Buffer (a string\n // comparison against it is never equal, which made every vouch report\n // `vouch-raced` and stamp nothing). A file that is GONE is still a race —\n // `null` is not \"empty\", it is \"the document this run described is not\n // there any more\".\n // Joined by path, not by index: pairing the two lists positionally assumed\n // `stageWrites` returns one entry per planned write in order — true today,\n // promised nowhere, and a compare-and-set that silently compares the wrong\n // pair would stamp over somebody else's vouch. A staged write no verified\n // file claims has nothing to be compared against, so it counts as raced;\n // fail-closed is the only safe direction here.\n const verifiedByPath = new Map(verified.map((v) => [v.path, v] as const));\n const raced = staged.filter((s) => {\n const v = verifiedByPath.get(s.write.path);\n return v === undefined || s.before === null || !s.before.equals(Buffer.from(v.raw, \"utf8\"));\n });\n if (raced.length > 0) {\n // Nothing has swapped yet, so the rollback is only the temp files going\n // away — the other writer's stamp is left exactly as it landed.\n await rollbackStaged(staged);\n return {\n ok: false,\n code: \"vouch-raced\",\n message:\n `${req.service}: ${raced.map((s) => s.write.path).join(\", \")} changed while this vouch was running — ` +\n `another vouch or an edit landed first. Nothing was stamped: re-read the document and re-run.`,\n };\n }\n\n // The journaled commit: intent fsynced before the first rename, so a kill\n // between the pair's two swaps is recoverable instead of invisible — the\n // exact half-stamped state the all-or-nothing verification exists to rule\n // out used to be reachable through that one window. The codes are archive's,\n // reused rather than minted: rolled back → nothing changed, re-running can\n // work; incomplete → the files listed need a human.\n const committed = await commitStaged(\n {\n root: req.docsDir,\n command: \"vouch\",\n // Runnable where vouch runs — the SERVICE repo, with --yes because the\n // journal's reader may be unattended. Any other journaled docs-repo\n // writer recovers this journal on its next run too.\n rerun: `loam vouch --service ${req.service} --yes`,\n target: req.service,\n },\n staged,\n \"stamped\",\n );\n if (!committed.ok) {\n return committed.raced\n ? {\n ok: false,\n code: \"vouch-raced\",\n message:\n `${req.service}: a document changed while this vouch was committing — ` +\n `another writer landed first. Nothing was stamped: re-read and re-run.`,\n }\n : { ok: false, code: committed.code, message: committed.message };\n }\n return {\n ok: true,\n status: \"verified\",\n lastVerified: req.today,\n vouchedBy: req.vouchedBy,\n recovered,\n stamped: { spec: specPlan.stamped, archSpec: archPlan === null ? null : archPlan.stamped },\n };\n}\n\n/**\n * One verified file's share of the commit: the bytes to write, and the record\n * of what was stamped into them. Kept beside each other because they are the\n * same computation — the report must describe the file that was actually\n * written, not a second guess at it.\n */\nexport function planStamp(v: VerifiedSpec, req: VouchRequest): { write: PlannedWrite; stamped: StampedSpec } {\n // Two passes on purpose: `content_digest` hashes the body BELOW the\n // frontmatter, and withFrontmatterFields promises that body byte-identical —\n // so hashing after the first stamp and writing the hash in a second one\n // yields a digest that is true of the file exactly as written. A re-vouch\n // takes the same road and refreshes every field, this one included.\n //\n // The scope this file's stamp will carry — null both when the run is an\n // ordinary vouch and when a sampled run's `--sample <n>` covered the whole\n // file. Joined to the plan by FILENAME rather than taken positionally: the\n // two axes are stamped by two separate calls, and a scope written onto the\n // wrong file would claim a person read four sections of a document they\n // never opened.\n const planned = req.sample?.axes.find((axis) => axis.file === v.file);\n // A verified file with no planned axis is unreachable — `sampleDrift`\n // refuses that run before this line — and null is the safe direction if it\n // ever were: a full stamp that CLEARS any prior scope, never one claiming a\n // sample nobody was shown.\n const scope = planned === undefined ? null : axisScope(planned);\n const restamped = withFrontmatterFields(\n v.raw,\n {\n status: \"verified\",\n last_verified: req.today,\n // WHO, beside when. Without it `status: verified` recorded that the word\n // had been written and nothing about who wrote it, so a vouch and an\n // agent's own draft left the same trace in the document — the one\n // distinction this command exists to make.\n vouched_by: req.vouchedBy,\n sources_digest: v.digest,\n // Beside the digest, what it was taken over. `sources_digest` alone can\n // only ever say THAT the code moved; the next `loam validate` reads this\n // back to say which files did.\n sources_files: encodeSourceIndex(v.index),\n // Beside `vouched_by`, never inside `status`: the rung string `vouched`\n // and the status `verified` are a published contract, and a sampled read\n // is not a fourth status — it is a qualification of this one.\n ...(scope === null ? {} : { vouch_scope: encodeVouchScope(scope) }),\n },\n // A full vouch CLEARS any scope a previous sampled one left, rather than\n // stranding a claim that would keep a fully-read document reading as\n // partial forever. Removing a key that was never there is a no-op, so\n // this rides on every run that is not itself sampling.\n scope === null ? [\"vouch_scope\"] : [],\n );\n const bodyDigest = contentDigest(restamped);\n return {\n write: { path: v.path, content: withFrontmatterFields(restamped, { content_digest: bodyDigest }) },\n stamped: {\n path: v.path,\n file: v.file,\n digest: v.digest,\n contentDigest: bodyDigest,\n sources: v.sources,\n files: v.index.length,\n skipped: v.skipped,\n vouchScope: scope,\n },\n };\n}\n"]}
|
|
@@ -9,12 +9,13 @@
|
|
|
9
9
|
* command exists to interrupt. So the reading list is computed first, printed,
|
|
10
10
|
* and only then is the person asked.
|
|
11
11
|
*
|
|
12
|
-
* It runs the vouch's own vetting (`../vet/verify.ts
|
|
13
|
-
* `run.ts`
|
|
14
|
-
* raise about a document — an unreadable header, no
|
|
15
|
-
* pattern, a source that is gone, an expansion covering no
|
|
16
|
-
* before the prompt, with the same code and the same
|
|
17
|
-
* not read four sections and then be told the run was
|
|
12
|
+
* It runs the vouch's own vetting (`../vet/verify.ts`: the same axis walk
|
|
13
|
+
* `run.ts` makes, over the call the reading pack makes), which means every
|
|
14
|
+
* refusal a vouch can raise about a document — an unreadable header, no
|
|
15
|
+
* `sources`, a glob pattern, a source that is gone, an expansion covering no
|
|
16
|
+
* files — fires HERE, before the prompt, with the same code and the same
|
|
17
|
+
* words. A person should not read four sections and then be told the run was
|
|
18
|
+
* never going to stamp.
|
|
18
19
|
*
|
|
19
20
|
* The cost is that the sources are walked and hashed twice: once to seed the
|
|
20
21
|
* sample, once at the stamp. That is the honesty price of the seed being
|
|
@@ -22,13 +23,9 @@
|
|
|
22
23
|
* second walk buys is the drift check, which refuses a stamp whose sample was
|
|
23
24
|
* chosen from a document that has since moved.
|
|
24
25
|
*/
|
|
25
|
-
import {
|
|
26
|
-
import { rawBody } from "../../../core/document/frontmatter.js";
|
|
26
|
+
import { contentDigest, rawBody } from "../../../core/document/frontmatter.js";
|
|
27
27
|
import { planSample } from "../../../core/provenance/sample/scope.js";
|
|
28
|
-
import {
|
|
29
|
-
import { SPEC_AXES } from "../../../core/repo/paths.js";
|
|
30
|
-
import { locateServicePaths } from "../../../core/repo/service-target.js";
|
|
31
|
-
import { noLivingSpecMessage, verifySpec } from "../vet/verify.js";
|
|
28
|
+
import { verifyAxes } from "../vet/verify.js";
|
|
32
29
|
/** The scope a planned axis will stamp, or null for one that was read in full. */
|
|
33
30
|
export function axisScope(axis) {
|
|
34
31
|
return axis.mode === "full" ? null : { sections: axis.picked.length, of: axis.of, seed: axis.seed };
|
|
@@ -69,29 +66,18 @@ export function sampleDrift(plan, verified, service) {
|
|
|
69
66
|
`not the sections a stamp would now cover. Nothing was stamped: re-run for a fresh reading list.`);
|
|
70
67
|
}
|
|
71
68
|
/**
|
|
72
|
-
* Build the reading list.
|
|
73
|
-
*
|
|
74
|
-
*
|
|
75
|
-
*
|
|
69
|
+
* Build the reading list. The axis walk is `run.ts`'s own — `verifyAxes`, which
|
|
70
|
+
* refuses a missing spec.md in vet/verify.ts's one sentence (the same absence
|
|
71
|
+
* and the same fix; three spellings would read as three different problems) —
|
|
72
|
+
* and so is the all-or-nothing discipline: one file that cannot be verified
|
|
73
|
+
* refuses the run before a person is asked to read anything.
|
|
76
74
|
*/
|
|
77
75
|
export async function buildSamplePlan(req) {
|
|
78
|
-
const
|
|
79
|
-
if (!
|
|
80
|
-
|
|
81
|
-
// and three spellings of it would read as three different problems.
|
|
82
|
-
return { ok: false, code: "unknown-target", message: noLivingSpecMessage(paths.spec, req.service) };
|
|
83
|
-
}
|
|
76
|
+
const verified = await verifyAxes(req);
|
|
77
|
+
if (!verified.ok)
|
|
78
|
+
return verified;
|
|
84
79
|
const axes = [];
|
|
85
|
-
for (const
|
|
86
|
-
const path = paths[axis.key];
|
|
87
|
-
if (!existsSync(path))
|
|
88
|
-
continue;
|
|
89
|
-
// Sequential, not Promise.all: each iteration hashes a whole source tree
|
|
90
|
-
// through the batched digest, and two of those in flight at once doubles
|
|
91
|
-
// the file descriptors that batching exists to bound. Two files.
|
|
92
|
-
const vetted = await verifySpec({ service: req.service, repoDir: req.repoDir }, path, axis.file);
|
|
93
|
-
if (!vetted.ok)
|
|
94
|
-
return vetted;
|
|
80
|
+
for (const vetted of verified.arch === null ? [verified.spec] : [verified.spec, verified.arch]) {
|
|
95
81
|
const digest = contentDigest(vetted.raw);
|
|
96
82
|
// The BODY, so no frontmatter line can be picked as a section — and the
|
|
97
83
|
// same slice `content_digest` is taken over, which is what makes the
|
|
@@ -105,8 +91,8 @@ export async function buildSamplePlan(req) {
|
|
|
105
91
|
n: req.n,
|
|
106
92
|
});
|
|
107
93
|
const base = {
|
|
108
|
-
file:
|
|
109
|
-
path,
|
|
94
|
+
file: vetted.file,
|
|
95
|
+
path: vetted.path,
|
|
110
96
|
contentDigest: digest,
|
|
111
97
|
sourcesDigest: vetted.digest,
|
|
112
98
|
of: sample.of,
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"plan.js","sourceRoot":"","sources":["../../../../src/commands/vouch/sample/plan.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * The pre-pass behind `loam vouch --sample <n>`: decide, before anyone is\n * asked anything, which sections of which files this vouch will actually\n * cover.\n *\n * It has to happen before the confirmation because a sampled vouch's question\n * is different in kind — \"have you read THESE four sections\", not \"have you\n * read the code\" — and a question that names nothing is the reflex this\n * command exists to interrupt. So the reading list is computed first, printed,\n * and only then is the person asked.\n *\n * It runs the vouch's own vetting (`../vet/verify.ts`, the same call\n * `run.ts` and the reading pack make), which means every refusal a vouch can\n * raise about a document — an unreadable header, no `sources`, a glob\n * pattern, a source that is gone, an expansion covering no files — fires HERE,\n * before the prompt, with the same code and the same words. A person should\n * not read four sections and then be told the run was never going to stamp.\n *\n * The cost is that the sources are walked and hashed twice: once to seed the\n * sample, once at the stamp. That is the honesty price of the seed being\n * content-derived, and `run.ts` spends the second walk anyway — what the\n * second walk buys is the drift check, which refuses a stamp whose sample was\n * chosen from a document that has since moved.\n */\nimport { existsSync } from \"node:fs\";\nimport { rawBody } from \"../../../core/document/frontmatter.js\";\nimport type { DocsDir } from \"../../../core/kernel/ids/dirs.js\";\nimport type { PathableService } from \"../../../core/kernel/ids/service.js\";\nimport type { DocSection } from \"../../../core/provenance/sample/sections.js\";\nimport { planSample, type VouchScope } from \"../../../core/provenance/sample/scope.js\";\nimport { contentDigest } from \"../../../core/provenance/stamp.js\";\nimport { SPEC_AXES } from \"../../../core/repo/paths.js\";\nimport { locateServicePaths } from \"../../../core/repo/service-target.js\";\nimport { noLivingSpecMessage, verifySpec, type SpecRefusal, type VerifiedSpec } from \"../vet/verify.js\";\n\n/**\n * One spec-axis file's share of a `--sample <n>` run, as the pre-pass decided\n * it and the stamp must honour it.\n *\n * A variant rather than an optional `picked?:`, because \"read in full\" and\n * \"sampled\" are two different claims and the difference decides whether the\n * file gets a `vouch_scope` at all. The two digests ride on both arms: they\n * are what the seed was derived from, and the stamp re-checks them against a\n * fresh verification, because a person reads for minutes and nothing locks\n * the document while they do.\n */\ninterface PlannedAxisBase {\n /** The axis's filename — \"spec.md\" or \"arch.spec.md\". The key the stamp joins on. */\n file: string;\n /** Absolute path, for the reading list's own line. */\n path: string;\n /** `contentDigest(raw)` when the reading list was built. */\n contentDigest: string;\n /** The sources digest when the reading list was built. */\n sourcesDigest: string;\n /** Every section the body had. */\n of: number;\n /**\n * How many lines of frontmatter sit above the body. `DocSection.line` is\n * counted within the BODY — that is what the splitter can promise, since it\n * is handed a body — and a reading list that printed those numbers as file\n * lines would send a person to the wrong place in every stamped document.\n * Added once, at the display edge.\n */\n lineOffset: number;\n /** The `sources` entries as the frontmatter spells them, and how many files they expanded to. */\n sources: { entries: string[]; files: number };\n}\n\nexport type PlannedAxis =\n /** `of <= n`: the sample covers the whole file, so this axis is an ordinary full vouch. */\n | (PlannedAxisBase & { mode: \"full\" })\n | (PlannedAxisBase & { mode: \"sampled\"; seed: string; picked: readonly DocSection[] });\n\nexport interface SamplePlan {\n /** `--sample <n>` as the person typed it — carried so the output can say what was asked for. */\n n: number;\n /**\n * Every spec-axis file the pre-pass verified, sampled or not. Complete on\n * purpose: the stamp compares this roster against what it re-verifies, so an\n * arch.spec.md that appeared while the person was reading refuses the run\n * instead of riding along unread.\n */\n axes: PlannedAxis[];\n}\n\n/** The scope a planned axis will stamp, or null for one that was read in full. */\nexport function axisScope(axis: PlannedAxis): VouchScope | null {\n return axis.mode === \"full\" ? null : { sections: axis.picked.length, of: axis.of, seed: axis.seed };\n}\n\n/**\n * What moved between the reading list and the stamp, as a phrase for the\n * refusal — or null when the plan and the fresh verification describe the same\n * documents.\n *\n * Joined by FILENAME both ways, and both directions matter: a plan entry with\n * no verified file means an axis vanished mid-read, a verified file with no\n * plan entry means one appeared and nobody was shown it. Comparing counts, or\n * zipping the two lists, would call the second case fine.\n *\n * The digests, not the bytes: `content_digest` and `sources_digest` are the\n * seed's own inputs, so a change in either is exactly a change in which\n * sections a sample would pick. It lives here rather than in `run.ts` because\n * it is a question about the PLAN — whether the reading list still describes\n * these files — and the answer has to be computed the same way the plan was.\n */\nexport function sampleDrift(\n plan: SamplePlan,\n verified: readonly VerifiedSpec[],\n service: string,\n): string | null {\n const now = new Map(verified.map((v) => [v.file, v] as const));\n const moved = plan.axes\n .filter((axis) => {\n const v = now.get(axis.file);\n return v === undefined || contentDigest(v.raw) !== axis.contentDigest || v.digest !== axis.sourcesDigest;\n })\n .map((axis) => axis.file);\n const planned = new Set(plan.axes.map((axis) => axis.file));\n const appeared = verified.filter((v) => !planned.has(v.file)).map((v) => v.file);\n const files = [...moved, ...appeared];\n if (files.length === 0) return null;\n // The whole sentence, not the file list: `run.ts` returns it verbatim under\n // `vouch-raced`, and the diagnosis — which is about a READING window nothing\n // locks — belongs beside the check that can explain it.\n return (\n `${service}: ${files.join(\", \")} changed while you were reading the sample — the sections you were shown are ` +\n `not the sections a stamp would now cover. Nothing was stamped: re-run for a fresh reading list.`\n );\n}\n\nexport interface SampleRequest {\n docsDir: DocsDir;\n service: PathableService;\n /** The service's own repo — what `sources` resolve against. */\n repoDir: string;\n /** How many sections to read per spec-axis file. A whole number of at least 1; the caller validates. */\n n: number;\n}\n\nexport type SampleOutcome =\n | { ok: true; plan: SamplePlan }\n | { ok: false; code: SpecRefusal[\"code\"] | \"unknown-target\"; message: string };\n\n/**\n * Build the reading list. Same axis walk as `run.ts` — spec.md required,\n * arch.spec.md when it exists — and the same all-or-nothing discipline: one\n * file that cannot be verified refuses the run before a person is asked to\n * read anything.\n */\nexport async function buildSamplePlan(req: SampleRequest): Promise<SampleOutcome> {\n const paths = await locateServicePaths(req.docsDir, req.service);\n if (!existsSync(paths.spec)) {\n // One sentence, from `vet/verify.ts`: the same absence and the same fix,\n // and three spellings of it would read as three different problems.\n return { ok: false, code: \"unknown-target\", message: noLivingSpecMessage(paths.spec, req.service) };\n }\n const axes: PlannedAxis[] = [];\n for (const axis of SPEC_AXES) {\n const path = paths[axis.key];\n if (!existsSync(path)) continue;\n // Sequential, not Promise.all: each iteration hashes a whole source tree\n // through the batched digest, and two of those in flight at once doubles\n // the file descriptors that batching exists to bound. Two files.\n const vetted = await verifySpec({ service: req.service, repoDir: req.repoDir }, path, axis.file);\n if (!vetted.ok) return vetted;\n const digest = contentDigest(vetted.raw);\n // The BODY, so no frontmatter line can be picked as a section — and the\n // same slice `content_digest` is taken over, which is what makes the\n // seed's inputs and the sampled text describe one document.\n const body = rawBody(vetted.raw);\n const sample = planSample({\n service: req.service,\n contentDigest: digest,\n sourcesDigest: vetted.digest,\n body,\n n: req.n,\n });\n const base = {\n file: axis.file,\n path,\n contentDigest: digest,\n sourcesDigest: vetted.digest,\n of: sample.of,\n // The header's line count, by subtraction rather than a second parse:\n // `rawBody` is the one definition of where the body starts, and asking\n // it twice is how the two answers drift.\n lineOffset: vetted.raw.slice(0, vetted.raw.length - body.length).split(\"\\n\").length - 1,\n sources: { entries: vetted.sources, files: vetted.index.length },\n };\n axes.push(\n sample.picked.length >= sample.of\n ? { ...base, mode: \"full\" }\n : { ...base, mode: \"sampled\", seed: sample.seed, picked: sample.picked },\n );\n }\n return { ok: true, plan: { n: req.n, axes } };\n}\n"]}
|
|
1
|
+
{"version":3,"file":"plan.js","sourceRoot":"","sources":["../../../../src/commands/vouch/sample/plan.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * The pre-pass behind `loam vouch --sample <n>`: decide, before anyone is\n * asked anything, which sections of which files this vouch will actually\n * cover.\n *\n * It has to happen before the confirmation because a sampled vouch's question\n * is different in kind — \"have you read THESE four sections\", not \"have you\n * read the code\" — and a question that names nothing is the reflex this\n * command exists to interrupt. So the reading list is computed first, printed,\n * and only then is the person asked.\n *\n * It runs the vouch's own vetting (`../vet/verify.ts`: the same axis walk\n * `run.ts` makes, over the call the reading pack makes), which means every\n * refusal a vouch can raise about a document — an unreadable header, no\n * `sources`, a glob pattern, a source that is gone, an expansion covering no\n * files — fires HERE, before the prompt, with the same code and the same\n * words. A person should not read four sections and then be told the run was\n * never going to stamp.\n *\n * The cost is that the sources are walked and hashed twice: once to seed the\n * sample, once at the stamp. That is the honesty price of the seed being\n * content-derived, and `run.ts` spends the second walk anyway — what the\n * second walk buys is the drift check, which refuses a stamp whose sample was\n * chosen from a document that has since moved.\n */\nimport { contentDigest, rawBody } from \"../../../core/document/frontmatter.js\";\nimport type { DocsDir } from \"../../../core/kernel/ids/dirs.js\";\nimport type { PathableService } from \"../../../core/kernel/ids/service.js\";\nimport type { DocSection } from \"../../../core/provenance/sample/sections.js\";\nimport { planSample, type VouchScope } from \"../../../core/provenance/sample/scope.js\";\nimport { verifyAxes, type AxesRefusal, type VerifiedSpec } from \"../vet/verify.js\";\n\n/**\n * One spec-axis file's share of a `--sample <n>` run, as the pre-pass decided\n * it and the stamp must honour it.\n *\n * A variant rather than an optional `picked?:`, because \"read in full\" and\n * \"sampled\" are two different claims and the difference decides whether the\n * file gets a `vouch_scope` at all. The two digests ride on both arms: they\n * are what the seed was derived from, and the stamp re-checks them against a\n * fresh verification, because a person reads for minutes and nothing locks\n * the document while they do.\n */\ninterface PlannedAxisBase {\n /** The axis's filename — \"spec.md\" or \"arch.spec.md\". The key the stamp joins on. */\n file: string;\n /** Absolute path, for the reading list's own line. */\n path: string;\n /** `contentDigest(raw)` when the reading list was built. */\n contentDigest: string;\n /** The sources digest when the reading list was built. */\n sourcesDigest: string;\n /** Every section the body had. */\n of: number;\n /**\n * How many lines of frontmatter sit above the body. `DocSection.line` is\n * counted within the BODY — that is what the splitter can promise, since it\n * is handed a body — and a reading list that printed those numbers as file\n * lines would send a person to the wrong place in every stamped document.\n * Added once, at the display edge.\n */\n lineOffset: number;\n /** The `sources` entries as the frontmatter spells them, and how many files they expanded to. */\n sources: { entries: string[]; files: number };\n}\n\nexport type PlannedAxis =\n /** `of <= n`: the sample covers the whole file, so this axis is an ordinary full vouch. */\n | (PlannedAxisBase & { mode: \"full\" })\n | (PlannedAxisBase & { mode: \"sampled\"; seed: string; picked: readonly DocSection[] });\n\nexport interface SamplePlan {\n /** `--sample <n>` as the person typed it — carried so the output can say what was asked for. */\n n: number;\n /**\n * Every spec-axis file the pre-pass verified, sampled or not. Complete on\n * purpose: the stamp compares this roster against what it re-verifies, so an\n * arch.spec.md that appeared while the person was reading refuses the run\n * instead of riding along unread.\n */\n axes: PlannedAxis[];\n}\n\n/** The scope a planned axis will stamp, or null for one that was read in full. */\nexport function axisScope(axis: PlannedAxis): VouchScope | null {\n return axis.mode === \"full\" ? null : { sections: axis.picked.length, of: axis.of, seed: axis.seed };\n}\n\n/**\n * What moved between the reading list and the stamp, as a phrase for the\n * refusal — or null when the plan and the fresh verification describe the same\n * documents.\n *\n * Joined by FILENAME both ways, and both directions matter: a plan entry with\n * no verified file means an axis vanished mid-read, a verified file with no\n * plan entry means one appeared and nobody was shown it. Comparing counts, or\n * zipping the two lists, would call the second case fine.\n *\n * The digests, not the bytes: `content_digest` and `sources_digest` are the\n * seed's own inputs, so a change in either is exactly a change in which\n * sections a sample would pick. It lives here rather than in `run.ts` because\n * it is a question about the PLAN — whether the reading list still describes\n * these files — and the answer has to be computed the same way the plan was.\n */\nexport function sampleDrift(\n plan: SamplePlan,\n verified: readonly VerifiedSpec[],\n service: string,\n): string | null {\n const now = new Map(verified.map((v) => [v.file, v] as const));\n const moved = plan.axes\n .filter((axis) => {\n const v = now.get(axis.file);\n return v === undefined || contentDigest(v.raw) !== axis.contentDigest || v.digest !== axis.sourcesDigest;\n })\n .map((axis) => axis.file);\n const planned = new Set(plan.axes.map((axis) => axis.file));\n const appeared = verified.filter((v) => !planned.has(v.file)).map((v) => v.file);\n const files = [...moved, ...appeared];\n if (files.length === 0) return null;\n // The whole sentence, not the file list: `run.ts` returns it verbatim under\n // `vouch-raced`, and the diagnosis — which is about a READING window nothing\n // locks — belongs beside the check that can explain it.\n return (\n `${service}: ${files.join(\", \")} changed while you were reading the sample — the sections you were shown are ` +\n `not the sections a stamp would now cover. Nothing was stamped: re-run for a fresh reading list.`\n );\n}\n\nexport interface SampleRequest {\n docsDir: DocsDir;\n service: PathableService;\n /** The service's own repo — what `sources` resolve against. */\n repoDir: string;\n /** How many sections to read per spec-axis file. A whole number of at least 1; the caller validates. */\n n: number;\n}\n\nexport type SampleOutcome = { ok: true; plan: SamplePlan } | AxesRefusal;\n\n/**\n * Build the reading list. The axis walk is `run.ts`'s own — `verifyAxes`, which\n * refuses a missing spec.md in vet/verify.ts's one sentence (the same absence\n * and the same fix; three spellings would read as three different problems) —\n * and so is the all-or-nothing discipline: one file that cannot be verified\n * refuses the run before a person is asked to read anything.\n */\nexport async function buildSamplePlan(req: SampleRequest): Promise<SampleOutcome> {\n const verified = await verifyAxes(req);\n if (!verified.ok) return verified;\n const axes: PlannedAxis[] = [];\n for (const vetted of verified.arch === null ? [verified.spec] : [verified.spec, verified.arch]) {\n const digest = contentDigest(vetted.raw);\n // The BODY, so no frontmatter line can be picked as a section — and the\n // same slice `content_digest` is taken over, which is what makes the\n // seed's inputs and the sampled text describe one document.\n const body = rawBody(vetted.raw);\n const sample = planSample({\n service: req.service,\n contentDigest: digest,\n sourcesDigest: vetted.digest,\n body,\n n: req.n,\n });\n const base = {\n file: vetted.file,\n path: vetted.path,\n contentDigest: digest,\n sourcesDigest: vetted.digest,\n of: sample.of,\n // The header's line count, by subtraction rather than a second parse:\n // `rawBody` is the one definition of where the body starts, and asking\n // it twice is how the two answers drift.\n lineOffset: vetted.raw.slice(0, vetted.raw.length - body.length).split(\"\\n\").length - 1,\n sources: { entries: vetted.sources, files: vetted.index.length },\n };\n axes.push(\n sample.picked.length >= sample.of\n ? { ...base, mode: \"full\" }\n : { ...base, mode: \"sampled\", seed: sample.seed, picked: sample.picked },\n );\n }\n return { ok: true, plan: { n: req.n, axes } };\n}\n"]}
|
|
@@ -5,25 +5,57 @@
|
|
|
5
5
|
* could check, and says which sources it skipped — a vouch over a spec whose
|
|
6
6
|
* sources could not be read would be the claim this command exists to prevent.
|
|
7
7
|
*/
|
|
8
|
+
import { existsSync } from "node:fs";
|
|
8
9
|
import { listField, parseFrontmatter } from "../../../core/document/frontmatter.js";
|
|
9
10
|
import { missingSources, patternSources, unsafeSources } from "../../../core/provenance/sources.js";
|
|
10
11
|
import { emptySourcesMessage, sourcesDigest } from "../../../core/provenance/stamp.js";
|
|
12
|
+
import { SPEC_AXES } from "../../../core/repo/paths.js";
|
|
13
|
+
import { locateServicePaths } from "../../../core/repo/service-target.js";
|
|
11
14
|
import { NotUtf8Error, readUtf8 } from "../../../core/staging/writes.js";
|
|
12
15
|
/**
|
|
13
16
|
* "There is no living spec here", in one sentence.
|
|
14
17
|
*
|
|
15
18
|
* Three callers reach this state independently — the stamp (`../run.ts`), the
|
|
16
|
-
* reading list (`../sample/plan.ts`)
|
|
17
|
-
* — and two of them carried a comment
|
|
18
|
-
* to the third, which is a promise
|
|
19
|
-
*
|
|
20
|
-
*
|
|
21
|
-
*
|
|
22
|
-
*
|
|
19
|
+
* reading list (`../sample/plan.ts`), both now through `verifyAxes` below, and
|
|
20
|
+
* the reading pack (`../pack/pack.ts`) — and two of them carried a comment
|
|
21
|
+
* promising to keep the wording identical to the third, which is a promise
|
|
22
|
+
* nothing could enforce. It lives in the leaf all three already import,
|
|
23
|
+
* because the failure this repo has actually shipped is four copies of an
|
|
24
|
+
* enumeration `catch` that looked identical for years while a fix landed in
|
|
25
|
+
* one of them: a `--sample` run refusing a missing spec with older advice than
|
|
26
|
+
* a bare run gives is the same defect, one flag wide.
|
|
23
27
|
*/
|
|
24
28
|
export function noLivingSpecMessage(specPath, service) {
|
|
25
29
|
return `No living spec at ${specPath}. Run \`loam adopt\` for '${service}' first.`;
|
|
26
30
|
}
|
|
31
|
+
/**
|
|
32
|
+
* The axis walk the stamp (`../run.ts`) and the reading list
|
|
33
|
+
* (`../sample/plan.ts`) both make, spelled once: spec.md is required,
|
|
34
|
+
* arch.spec.md rides when it exists, and one file that cannot be verified
|
|
35
|
+
* refuses the walk before the next is read — so a stamp writes nothing and a
|
|
36
|
+
* person is asked to read nothing.
|
|
37
|
+
*
|
|
38
|
+
* Walked by name rather than accumulated in a loop, because every caller
|
|
39
|
+
* needs to know WHICH file it is holding — the required one or the optional
|
|
40
|
+
* one — and a loop can only say that positionally. Sequential, not
|
|
41
|
+
* Promise.all: each verification hashes a whole source tree through the
|
|
42
|
+
* batched digest, and two of those in flight at once doubles the file
|
|
43
|
+
* descriptors that batching exists to bound. Two files.
|
|
44
|
+
*/
|
|
45
|
+
export async function verifyAxes(req) {
|
|
46
|
+
const paths = await locateServicePaths(req.docsDir, req.service);
|
|
47
|
+
if (!existsSync(paths.spec)) {
|
|
48
|
+
return { ok: false, code: "unknown-target", message: noLivingSpecMessage(paths.spec, req.service) };
|
|
49
|
+
}
|
|
50
|
+
const [specAxis, archAxis] = SPEC_AXES;
|
|
51
|
+
const spec = await verifySpec(req, paths[specAxis.key], specAxis.file);
|
|
52
|
+
if (!spec.ok)
|
|
53
|
+
return spec;
|
|
54
|
+
if (!existsSync(paths[archAxis.key]))
|
|
55
|
+
return { ok: true, spec, arch: null };
|
|
56
|
+
const arch = await verifySpec(req, paths[archAxis.key], archAxis.file);
|
|
57
|
+
return arch.ok ? { ok: true, spec, arch } : arch;
|
|
58
|
+
}
|
|
27
59
|
/**
|
|
28
60
|
* The per-file half of the refusal discipline: everything vouch cannot verify
|
|
29
61
|
* about ONE spec file, checked the same way for spec.md and arch.spec.md.
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"verify.js","sourceRoot":"","sources":["../../../../src/commands/vouch/vet/verify.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * Does this spec still describe the code it points at?\n *\n * One spec, read against the sources it declares. It answers only about what it\n * could check, and says which sources it skipped — a vouch over a spec whose\n * sources could not be read would be the claim this command exists to prevent.\n */\nimport { listField, parseFrontmatter } from \"../../../core/document/frontmatter.js\";\nimport type { PathableService } from \"../../../core/kernel/ids/service.js\";\nimport { missingSources, patternSources, unsafeSources } from \"../../../core/provenance/sources.js\";\nimport { emptySourcesMessage, sourcesDigest, type SourceIndexEntry } from \"../../../core/provenance/stamp.js\";\nimport type { SkippedSource } from \"../../../core/provenance/walk.js\";\nimport { NotUtf8Error, readUtf8 } from \"../../../core/staging/writes.js\";\n\nexport interface VerifiedSpec {\n ok: true;\n path: string;\n file: string;\n /** The file exactly as read — what the stamp is applied to. */\n raw: string;\n sources: string[];\n digest: string;\n /** Per-file shas, in digest order — what `sources_files` is stamped from. */\n index: SourceIndexEntry[];\n skipped: SkippedSource[];\n}\n\n/**\n * The two facts the vetting reads — not the whole VouchRequest, because the\n * reading pack (`../pack/pack.ts`) asks the same questions with no date or\n * identity to offer: a pack stamps nothing.\n */\nexport interface SpecVetting {\n service: PathableService;\n repoDir: string;\n}\n\n/**\n * The refusal half, spelled here rather than imported from `../contract.ts`:\n * this module is a LEAF two sibling consumers share — `run.ts` (the stamp) and\n * `pack/pack.ts` (the worklist) — which is why it lives in its own package,\n * and an import from the vouch package above it would close the exact\n * package-graph cycle the move exists to avoid. The codes are a subset of\n * VouchOutcome's refusal codes, so run.ts returns one of these unchanged.\n */\nexport interface SpecRefusal {\n ok: false;\n code: \"sources-absent\" | \"sources-path-missing\" | \"repository-unavailable\";\n message: string;\n}\n\n/**\n * \"There is no living spec here\", in one sentence.\n *\n * Three callers reach this state independently — the stamp (`../run.ts`), the\n * reading list (`../sample/plan.ts`) and the reading pack (`../pack/pack.ts`)\n * — and two of them carried a comment promising to keep the wording identical\n * to the third, which is a promise nothing could enforce. It lives in the leaf\n * all three already import, because the failure this repo has actually shipped\n * is four copies of an enumeration `catch` that looked identical for years\n * while a fix landed in one of them: a `--sample` run refusing a missing spec\n * with older advice than a bare run gives is the same defect, one flag wide.\n */\nexport function noLivingSpecMessage(specPath: string, service: string): string {\n return `No living spec at ${specPath}. Run \\`loam adopt\\` for '${service}' first.`;\n}\n\n/**\n * The per-file half of the refusal discipline: everything vouch cannot verify\n * about ONE spec file, checked the same way for spec.md and arch.spec.md.\n */\nexport async function verifySpec(\n req: SpecVetting,\n path: string,\n file: string,\n): Promise<VerifiedSpec | SpecRefusal> {\n // Through readUtf8, so the round trip below is exact: the stamp is computed\n // from `raw` and the race check compares `Buffer.from(raw)` against the bytes\n // on disk, which only agree if the file decoded without substitutions. A\n // non-UTF-8 spec is refused by name rather than stamped as verified with\n // every undecodable byte replaced. Refused HERE rather than left to the CLI's\n // top-level handler, which would report a diagnosable state as `internal`.\n // What it replaces is a diagnosis that was worse than none: a UTF-16 spec\n // decoded to a document with no frontmatter loam could find, so vouch refused\n // it as `sources-absent` — \"names no sources\" — over a file whose `sources:`\n // line is right there.\n let raw: string;\n try {\n raw = await readUtf8(path);\n } catch (err) {\n if (!(err instanceof NotUtf8Error)) throw err;\n return {\n ok: false,\n code: \"repository-unavailable\",\n message: `${req.service}: ${file} cannot be vouched for — ${err.message}`,\n };\n }\n const fm = parseFrontmatter(raw);\n // Before any sources reasoning: a header that does not parse hides whatever\n // fields the author wrote, so \"names no sources\" would be a false diagnosis —\n // and if the file ever reached the stamp, withFrontmatterFields' rule for\n // unreadable headers is replace-don't-merge, which would silently discard the\n // author's owner/service/sources lines. Refuse before the write path can see\n // the file. The code stays `sources-absent` — for vouch's purposes the\n // sources ARE unreadable-hence-absent, and the message carries the real\n // diagnosis — rather than minting a new ErrorCode for one refusal.\n if (fm.malformed) {\n return {\n ok: false,\n code: \"sources-absent\",\n message:\n `${req.service}: ${file} has a frontmatter block that cannot be read as YAML — ` +\n `its fields (\\`sources\\` included) are unreadable, and stamping would rewrite the header wholesale, ` +\n `losing what the author wrote. Fix the YAML between the \\`---\\` fences ` +\n `(\\`loam validate\\` reports it as \\`frontmatter.malformed\\`), then re-run.`,\n };\n }\n const sources = listField(fm, \"sources\");\n if (sources.length === 0) {\n return {\n ok: false,\n code: \"sources-absent\",\n message: `${req.service}: ${file} names no sources — there is nothing to vouch it against. Add \\`sources:\\` naming the code it was written from.`,\n };\n }\n\n // spec.md's refusals predate the second axis and keep their exact wording;\n // where they did not name the file, only the newer axis adds it.\n const label = file === \"spec.md\" ? req.service : `${req.service}: ${file}`;\n\n // Before the existence check: a pattern is not a path that \"does not exist\",\n // it is an entry loam no longer reads at all, and the refusal has to say so.\n const patterns = patternSources(sources);\n if (patterns.length > 0) {\n return {\n ok: false,\n code: \"sources-path-missing\",\n message: `${label}: ${patterns.length} source(s) are glob patterns — ${patterns.join(\", \")}. Patterns are no longer supported: name files or directories (a directory already covers everything beneath it). A stamp over a pattern would vouch for a file set nobody can be sure of.`,\n };\n }\n\n const unsafe = unsafeSources(req.repoDir, sources);\n if (unsafe.length > 0) {\n return {\n ok: false,\n code: \"sources-path-missing\",\n message: `${label}: ${unsafe.length} source(s) escape the service repo — ${unsafe.join(\", \")}. Sources must be relative paths contained by this repository, including through symlinks; vouch will not hash files outside it.`,\n };\n }\n\n const missing = missingSources(req.repoDir, sources);\n if (missing.length > 0) {\n return {\n ok: false,\n code: \"sources-path-missing\",\n message: `${label}: ${missing.length} source(s) do not exist — ${missing.join(\", \")}. Vouching now would stamp a claim about code that is not there.`,\n };\n }\n\n const { digest, index, skipped } = await sourcesDigest(req.repoDir, sources);\n if (index.length === 0) {\n // A directory that exists but holds no files — or only ones the walk leaves\n // out: dot-entries, `node_modules`, anything the repository itself ignores.\n // The paths \"resolve\", but a digest over nothing never changes, so the stamp\n // would read as current forever. The sentence comes from provenance/stamp.ts so\n // that `loam validate` can grade the same state in the same words instead of\n // going green on a document this command refuses.\n return { ok: false, code: \"sources-absent\", message: emptySourcesMessage(label, sources) };\n }\n return { ok: true, path, file, raw, sources, digest, index, skipped };\n}\n"]}
|
|
1
|
+
{"version":3,"file":"verify.js","sourceRoot":"","sources":["../../../../src/commands/vouch/vet/verify.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * Does this spec still describe the code it points at?\n *\n * One spec, read against the sources it declares. It answers only about what it\n * could check, and says which sources it skipped — a vouch over a spec whose\n * sources could not be read would be the claim this command exists to prevent.\n */\nimport { existsSync } from \"node:fs\";\nimport { listField, parseFrontmatter } from \"../../../core/document/frontmatter.js\";\nimport type { DocsDir } from \"../../../core/kernel/ids/dirs.js\";\nimport type { PathableService } from \"../../../core/kernel/ids/service.js\";\nimport { missingSources, patternSources, unsafeSources } from \"../../../core/provenance/sources.js\";\nimport { emptySourcesMessage, sourcesDigest, type SourceIndexEntry } from \"../../../core/provenance/stamp.js\";\nimport type { SkippedSource } from \"../../../core/provenance/walk.js\";\nimport { SPEC_AXES } from \"../../../core/repo/paths.js\";\nimport { locateServicePaths } from \"../../../core/repo/service-target.js\";\nimport { NotUtf8Error, readUtf8 } from \"../../../core/staging/writes.js\";\n\n/** A spec-axis file whose sources all check out, carrying what the stamp needs. */\nexport interface VerifiedSpec {\n ok: true;\n path: string;\n file: string;\n /** The file exactly as read — what the stamp is applied to. */\n raw: string;\n sources: string[];\n digest: string;\n /** Per-file shas, in digest order — what `sources_files` is stamped from. */\n index: SourceIndexEntry[];\n skipped: SkippedSource[];\n}\n\n/**\n * The two facts the vetting reads — not the whole VouchRequest, because the\n * reading pack (`../pack/pack.ts`) asks the same questions with no date or\n * identity to offer: a pack stamps nothing.\n */\nexport interface SpecVetting {\n service: PathableService;\n repoDir: string;\n}\n\n/**\n * The refusal half, spelled here rather than imported from `../contract.ts`:\n * this module is a LEAF two sibling consumers share — `run.ts` (the stamp) and\n * `pack/pack.ts` (the worklist) — which is why it lives in its own package,\n * and an import from the vouch package above it would close the exact\n * package-graph cycle the move exists to avoid. The codes are a subset of\n * VouchOutcome's refusal codes, so run.ts returns one of these unchanged.\n */\nexport interface SpecRefusal {\n ok: false;\n code: \"sources-absent\" | \"sources-path-missing\" | \"repository-unavailable\";\n message: string;\n}\n\n/**\n * \"There is no living spec here\", in one sentence.\n *\n * Three callers reach this state independently — the stamp (`../run.ts`), the\n * reading list (`../sample/plan.ts`), both now through `verifyAxes` below, and\n * the reading pack (`../pack/pack.ts`) — and two of them carried a comment\n * promising to keep the wording identical to the third, which is a promise\n * nothing could enforce. It lives in the leaf all three already import,\n * because the failure this repo has actually shipped is four copies of an\n * enumeration `catch` that looked identical for years while a fix landed in\n * one of them: a `--sample` run refusing a missing spec with older advice than\n * a bare run gives is the same defect, one flag wide.\n */\nexport function noLivingSpecMessage(specPath: string, service: string): string {\n return `No living spec at ${specPath}. Run \\`loam adopt\\` for '${service}' first.`;\n}\n\n/** Every spec-axis file of one service, verified: spec.md always, arch.spec.md when it exists. */\nexport interface VerifiedAxes {\n ok: true;\n spec: VerifiedSpec;\n arch: VerifiedSpec | null;\n}\n\n/** The walk's refusals: one file's vetting, or no spec.md to vet at all. */\nexport type AxesRefusal = SpecRefusal | { ok: false; code: \"unknown-target\"; message: string };\n\n/**\n * The axis walk the stamp (`../run.ts`) and the reading list\n * (`../sample/plan.ts`) both make, spelled once: spec.md is required,\n * arch.spec.md rides when it exists, and one file that cannot be verified\n * refuses the walk before the next is read — so a stamp writes nothing and a\n * person is asked to read nothing.\n *\n * Walked by name rather than accumulated in a loop, because every caller\n * needs to know WHICH file it is holding — the required one or the optional\n * one — and a loop can only say that positionally. Sequential, not\n * Promise.all: each verification hashes a whole source tree through the\n * batched digest, and two of those in flight at once doubles the file\n * descriptors that batching exists to bound. Two files.\n */\nexport async function verifyAxes(req: SpecVetting & { docsDir: DocsDir }): Promise<VerifiedAxes | AxesRefusal> {\n const paths = await locateServicePaths(req.docsDir, req.service);\n if (!existsSync(paths.spec)) {\n return { ok: false, code: \"unknown-target\", message: noLivingSpecMessage(paths.spec, req.service) };\n }\n const [specAxis, archAxis] = SPEC_AXES;\n const spec = await verifySpec(req, paths[specAxis.key], specAxis.file);\n if (!spec.ok) return spec;\n if (!existsSync(paths[archAxis.key])) return { ok: true, spec, arch: null };\n const arch = await verifySpec(req, paths[archAxis.key], archAxis.file);\n return arch.ok ? { ok: true, spec, arch } : arch;\n}\n\n/**\n * The per-file half of the refusal discipline: everything vouch cannot verify\n * about ONE spec file, checked the same way for spec.md and arch.spec.md.\n */\nexport async function verifySpec(\n req: SpecVetting,\n path: string,\n file: string,\n): Promise<VerifiedSpec | SpecRefusal> {\n // Through readUtf8, so the round trip below is exact: the stamp is computed\n // from `raw` and the race check compares `Buffer.from(raw)` against the bytes\n // on disk, which only agree if the file decoded without substitutions. A\n // non-UTF-8 spec is refused by name rather than stamped as verified with\n // every undecodable byte replaced. Refused HERE rather than left to the CLI's\n // top-level handler, which would report a diagnosable state as `internal`.\n // What it replaces is a diagnosis that was worse than none: a UTF-16 spec\n // decoded to a document with no frontmatter loam could find, so vouch refused\n // it as `sources-absent` — \"names no sources\" — over a file whose `sources:`\n // line is right there.\n let raw: string;\n try {\n raw = await readUtf8(path);\n } catch (err) {\n if (!(err instanceof NotUtf8Error)) throw err;\n return {\n ok: false,\n code: \"repository-unavailable\",\n message: `${req.service}: ${file} cannot be vouched for — ${err.message}`,\n };\n }\n const fm = parseFrontmatter(raw);\n // Before any sources reasoning: a header that does not parse hides whatever\n // fields the author wrote, so \"names no sources\" would be a false diagnosis —\n // and if the file ever reached the stamp, withFrontmatterFields' rule for\n // unreadable headers is replace-don't-merge, which would silently discard the\n // author's owner/service/sources lines. Refuse before the write path can see\n // the file. The code stays `sources-absent` — for vouch's purposes the\n // sources ARE unreadable-hence-absent, and the message carries the real\n // diagnosis — rather than minting a new ErrorCode for one refusal.\n if (fm.malformed) {\n return {\n ok: false,\n code: \"sources-absent\",\n message:\n `${req.service}: ${file} has a frontmatter block that cannot be read as YAML — ` +\n `its fields (\\`sources\\` included) are unreadable, and stamping would rewrite the header wholesale, ` +\n `losing what the author wrote. Fix the YAML between the \\`---\\` fences ` +\n `(\\`loam validate\\` reports it as \\`frontmatter.malformed\\`), then re-run.`,\n };\n }\n const sources = listField(fm, \"sources\");\n if (sources.length === 0) {\n return {\n ok: false,\n code: \"sources-absent\",\n message: `${req.service}: ${file} names no sources — there is nothing to vouch it against. Add \\`sources:\\` naming the code it was written from.`,\n };\n }\n\n // spec.md's refusals predate the second axis and keep their exact wording;\n // where they did not name the file, only the newer axis adds it.\n const label = file === \"spec.md\" ? req.service : `${req.service}: ${file}`;\n\n // Before the existence check: a pattern is not a path that \"does not exist\",\n // it is an entry loam no longer reads at all, and the refusal has to say so.\n const patterns = patternSources(sources);\n if (patterns.length > 0) {\n return {\n ok: false,\n code: \"sources-path-missing\",\n message: `${label}: ${patterns.length} source(s) are glob patterns — ${patterns.join(\", \")}. Patterns are no longer supported: name files or directories (a directory already covers everything beneath it). A stamp over a pattern would vouch for a file set nobody can be sure of.`,\n };\n }\n\n const unsafe = unsafeSources(req.repoDir, sources);\n if (unsafe.length > 0) {\n return {\n ok: false,\n code: \"sources-path-missing\",\n message: `${label}: ${unsafe.length} source(s) escape the service repo — ${unsafe.join(\", \")}. Sources must be relative paths contained by this repository, including through symlinks; vouch will not hash files outside it.`,\n };\n }\n\n const missing = missingSources(req.repoDir, sources);\n if (missing.length > 0) {\n return {\n ok: false,\n code: \"sources-path-missing\",\n message: `${label}: ${missing.length} source(s) do not exist — ${missing.join(\", \")}. Vouching now would stamp a claim about code that is not there.`,\n };\n }\n\n const { digest, index, skipped } = await sourcesDigest(req.repoDir, sources);\n if (index.length === 0) {\n // A directory that exists but holds no files — or only ones the walk leaves\n // out: dot-entries, `node_modules`, anything the repository itself ignores.\n // The paths \"resolve\", but a digest over nothing never changes, so the stamp\n // would read as current forever. The sentence comes from provenance/stamp.ts so\n // that `loam validate` can grade the same state in the same words instead of\n // going green on a document this command refuses.\n return { ok: false, code: \"sources-absent\", message: emptySourcesMessage(label, sources) };\n }\n return { ok: true, path, file, raw, sources, digest, index, skipped };\n}\n"]}
|