@ybotok/loam 0.2.0-alpha.1 → 0.2.0-alpha.13
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 +3448 -0
- package/COMPARISON.md +91 -3
- package/CONTRIBUTING.md +37 -0
- package/MIGRATING-from-OpenSpec.md +47 -23
- package/QUICKSTART.md +189 -12
- package/README.md +274 -65
- package/ROADMAP.md +572 -31
- package/SCHEMA.md +1709 -155
- package/WORKFLOW.md +298 -72
- 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/adopt/render.js +50 -9
- package/dist/commands/adopt/render.js.map +1 -1
- package/dist/commands/archive/archive.js +16 -19
- package/dist/commands/archive/archive.js.map +1 -1
- package/dist/commands/archive/closing.js +42 -0
- package/dist/commands/archive/closing.js.map +1 -0
- package/dist/commands/archive/plan/contracts/asyncapi.js +36 -42
- 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 +77 -43
- package/dist/commands/archive/plan/contracts/openapi.js.map +1 -1
- package/dist/commands/archive/plan/contracts/verdict-lines.js +103 -0
- package/dist/commands/archive/plan/contracts/verdict-lines.js.map +1 -0
- package/dist/commands/archive/plan/document/head.js +151 -0
- package/dist/commands/archive/plan/document/head.js.map +1 -0
- package/dist/commands/archive/plan/document/links.js +284 -0
- package/dist/commands/archive/plan/document/links.js.map +1 -0
- package/dist/commands/archive/plan/document/run.js +136 -0
- package/dist/commands/archive/plan/document/run.js.map +1 -0
- package/dist/commands/archive/plan/gate.js +21 -19
- package/dist/commands/archive/plan/gate.js.map +1 -1
- package/dist/commands/archive/plan/landscape.js +167 -30
- package/dist/commands/archive/plan/landscape.js.map +1 -1
- package/dist/commands/archive/plan/model/extending.js +85 -0
- package/dist/commands/archive/plan/model/extending.js.map +1 -0
- package/dist/commands/archive/plan/refusal.js +14 -19
- package/dist/commands/archive/plan/refusal.js.map +1 -1
- package/dist/commands/archive/plan/specs.js +137 -56
- package/dist/commands/archive/plan/specs.js.map +1 -1
- package/dist/commands/archive/plan/state.js +3 -0
- package/dist/commands/archive/plan/state.js.map +1 -1
- package/dist/commands/archive/run.js +134 -30
- 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 +85 -18
- package/dist/commands/context/context.js.map +1 -1
- package/dist/commands/context/print.js +29 -7
- package/dist/commands/context/print.js.map +1 -1
- package/dist/commands/delta/delta.js +102 -14
- package/dist/commands/delta/delta.js.map +1 -1
- package/dist/commands/delta/print.js +17 -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/doctor.js +60 -1
- package/dist/commands/doctor.js.map +1 -1
- package/dist/commands/explain/explain.js +89 -13
- package/dist/commands/explain/explain.js.map +1 -1
- package/dist/commands/explore.js +16 -10
- 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 +5 -7
- package/dist/commands/list/json.js.map +1 -1
- package/dist/commands/list/list.js +44 -29
- package/dist/commands/list/list.js.map +1 -1
- package/dist/commands/list/print.js +52 -9
- 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 +37 -34
- 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 +158 -13
- package/dist/commands/seed/plan.js.map +1 -1
- package/dist/commands/seed/seed.js +13 -11
- package/dist/commands/seed/seed.js.map +1 -1
- package/dist/commands/show/feature.js +9 -5
- package/dist/commands/show/feature.js.map +1 -1
- package/dist/commands/show/introduced.js +157 -0
- package/dist/commands/show/introduced.js.map +1 -0
- package/dist/commands/show/marks.js +35 -6
- package/dist/commands/show/marks.js.map +1 -1
- package/dist/commands/show/service.js +41 -33
- package/dist/commands/show/service.js.map +1 -1
- package/dist/commands/show/show.js +17 -7
- package/dist/commands/show/show.js.map +1 -1
- package/dist/commands/status/fleet-next.js +90 -0
- package/dist/commands/status/fleet-next.js.map +1 -0
- package/dist/commands/status/json.js +2 -0
- package/dist/commands/status/json.js.map +1 -1
- package/dist/commands/status/print.js +89 -37
- 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 +103 -23
- 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 +106 -46
- 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 +289 -74
- package/dist/commands/subsystem/sync.js.map +1 -1
- package/dist/commands/subsystem/txn/exclude.js +132 -0
- package/dist/commands/subsystem/txn/exclude.js.map +1 -0
- package/dist/commands/subsystem/txn/links.js +128 -0
- package/dist/commands/subsystem/txn/links.js.map +1 -0
- package/dist/commands/subsystem/txn/report.js +174 -0
- package/dist/commands/subsystem/txn/report.js.map +1 -0
- package/dist/commands/subsystem/txn/txn.js +147 -45
- package/dist/commands/subsystem/txn/txn.js.map +1 -1
- package/dist/commands/subsystem/txn/views.js +200 -21
- package/dist/commands/subsystem/txn/views.js.map +1 -1
- package/dist/commands/tasks/actions.js +107 -0
- package/dist/commands/tasks/actions.js.map +1 -0
- package/dist/commands/tasks/render.js +253 -0
- package/dist/commands/tasks/render.js.map +1 -0
- package/dist/commands/tasks/tasks.js +119 -0
- package/dist/commands/tasks/tasks.js.map +1 -0
- package/dist/commands/unarchive/manifest.js +42 -26
- package/dist/commands/unarchive/manifest.js.map +1 -1
- package/dist/commands/unarchive/restore.js +83 -33
- 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 +78 -27
- package/dist/commands/validate/arch-coverage.js.map +1 -1
- package/dist/commands/validate/checks/fleet-shape.js +34 -126
- 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/introduced.js +102 -0
- package/dist/commands/validate/checks/introduced.js.map +1 -0
- 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 +11 -2
- package/dist/commands/validate/checks/sources.js.map +1 -1
- package/dist/commands/validate/coverage/covers-after.js +105 -0
- package/dist/commands/validate/coverage/covers-after.js.map +1 -0
- package/dist/commands/validate/feature.js +26 -20
- package/dist/commands/validate/feature.js.map +1 -1
- package/dist/commands/validate/fleet/census.js +37 -13
- package/dist/commands/validate/fleet/census.js.map +1 -1
- package/dist/commands/validate/fleet/landscape.js +144 -126
- package/dist/commands/validate/fleet/landscape.js.map +1 -1
- package/dist/commands/validate/fleet/load.js +66 -0
- package/dist/commands/validate/fleet/load.js.map +1 -1
- package/dist/commands/validate/fleet/map/attest.js +98 -0
- package/dist/commands/validate/fleet/map/attest.js.map +1 -0
- package/dist/commands/validate/fleet/map/bindings.js +107 -0
- package/dist/commands/validate/fleet/map/bindings.js.map +1 -0
- package/dist/commands/validate/fleet/map/consumers.js +244 -0
- package/dist/commands/validate/fleet/map/consumers.js.map +1 -0
- package/dist/commands/validate/fleet/map/isolation.js +116 -0
- package/dist/commands/validate/fleet/map/isolation.js.map +1 -0
- package/dist/commands/validate/fleet/map/usecases.js +54 -0
- package/dist/commands/validate/fleet/map/usecases.js.map +1 -0
- package/dist/commands/validate/fleet/obligations.js +44 -14
- package/dist/commands/validate/fleet/obligations.js.map +1 -1
- package/dist/commands/validate/fleet/scorecard/print.js +10 -2
- 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/place.js.map +1 -1
- package/dist/commands/validate/fleet/views/fleet-project.js +217 -0
- package/dist/commands/validate/fleet/views/fleet-project.js.map +1 -0
- package/dist/commands/validate/fleet/views/ids.js +50 -15
- package/dist/commands/validate/fleet/views/ids.js.map +1 -1
- package/dist/commands/validate/fleet/views/projects.js +278 -0
- package/dist/commands/validate/fleet/views/projects.js.map +1 -0
- package/dist/commands/validate/fleet/views/root-documents.js +83 -0
- package/dist/commands/validate/fleet/views/root-documents.js.map +1 -0
- package/dist/commands/validate/fleet/views/stale.js +40 -47
- 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/render/repeats.js +63 -0
- package/dist/commands/validate/render/repeats.js.map +1 -0
- package/dist/commands/validate/render/rollups.js +115 -0
- package/dist/commands/validate/render/rollups.js.map +1 -0
- package/dist/commands/validate/render/text.js +199 -0
- package/dist/commands/validate/render/text.js.map +1 -0
- package/dist/commands/validate/report.js +36 -160
- 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 +21 -107
- 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 +36 -14
- 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/model/diverged.js +226 -0
- package/dist/commands/validate/service/model/diverged.js.map +1 -0
- package/dist/commands/validate/service/model/grade.js +163 -0
- package/dist/commands/validate/service/model/grade.js.map +1 -0
- package/dist/commands/validate/service/service.js +121 -61
- package/dist/commands/validate/service/service.js.map +1 -1
- package/dist/commands/validate/service/specs.js +88 -28
- package/dist/commands/validate/service/specs.js.map +1 -1
- package/dist/commands/validate/service/spine.js +38 -3
- package/dist/commands/validate/service/spine.js.map +1 -1
- package/dist/commands/validate/service/usecases/tags.js +113 -0
- package/dist/commands/validate/service/usecases/tags.js.map +1 -0
- package/dist/commands/validate/service/usecases/usecases.js +209 -0
- package/dist/commands/validate/service/usecases/usecases.js.map +1 -0
- package/dist/commands/validate/validate.js +85 -60
- 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 +55 -52
- 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 +81 -106
- package/dist/core/agent/agents-md/command-map.js.map +1 -1
- package/dist/core/agent/agents-md/map/explain.js +21 -9
- package/dist/core/agent/agents-md/map/explain.js.map +1 -1
- package/dist/core/agent/agents-md/map/lenses/context.js +17 -8
- 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 +24 -8
- package/dist/core/agent/agents-md/map/status.js.map +1 -1
- package/dist/core/agent/agents-md/map/subsystem.js +80 -8
- package/dist/core/agent/agents-md/map/subsystem.js.map +1 -1
- package/dist/core/agent/agents-md/map/usecases.js +88 -10
- package/dist/core/agent/agents-md/map/usecases.js.map +1 -1
- package/dist/core/agent/agents-md/refusals.js +49 -22
- package/dist/core/agent/agents-md/refusals.js.map +1 -1
- package/dist/core/agent/agents-md.js +39 -6
- 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/protocol.js +1 -1
- package/dist/core/agent/protocol.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/support/report.js +46 -5
- package/dist/core/agent/support/report.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 +158 -11
- package/dist/core/agent/workflows/adopt.js.map +1 -1
- package/dist/core/agent/workflows/check.js +78 -39
- package/dist/core/agent/workflows/check.js.map +1 -1
- package/dist/core/agent/workflows/closing.js +88 -27
- package/dist/core/agent/workflows/closing.js.map +1 -1
- package/dist/core/agent/workflows/feature.js +128 -13
- package/dist/core/agent/workflows/feature.js.map +1 -1
- package/dist/core/agent/workflows/implement.js +96 -13
- package/dist/core/agent/workflows/implement.js.map +1 -1
- package/dist/core/agent/workflows/reference/authoring.js +106 -4
- 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/drawing/drawing.js +253 -0
- package/dist/core/agent/workflows/reference/drawing/drawing.js.map +1 -0
- package/dist/core/agent/workflows/reference/reference.js +6 -3
- package/dist/core/agent/workflows/reference/reference.js.map +1 -1
- package/dist/core/agent/workflows/reference/spine.js +68 -27
- 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 +32 -50
- package/dist/core/asyncapi/merge/markers.js.map +1 -1
- package/dist/core/asyncapi/merge/merge.js +33 -53
- package/dist/core/asyncapi/merge/merge.js.map +1 -1
- package/dist/core/asyncapi/merge/pin.js +12 -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 +96 -36
- package/dist/core/brief/brief.js.map +1 -1
- package/dist/core/brief/checks.js +138 -3
- package/dist/core/brief/checks.js.map +1 -1
- package/dist/core/brief/landscape.js +209 -130
- package/dist/core/brief/landscape.js.map +1 -1
- package/dist/core/brief/map/extend.js +84 -0
- package/dist/core/brief/map/extend.js.map +1 -0
- package/dist/core/brief/map/owed.js +266 -0
- package/dist/core/brief/map/owed.js.map +1 -0
- package/dist/core/brief/targets.js +109 -31
- package/dist/core/brief/targets.js.map +1 -1
- package/dist/core/brief/unchecked.js +14 -3
- package/dist/core/brief/unchecked.js.map +1 -1
- package/dist/core/c4/arch.js +90 -182
- package/dist/core/c4/arch.js.map +1 -1
- package/dist/core/c4/direction/direction.js +114 -0
- package/dist/core/c4/direction/direction.js.map +1 -0
- package/dist/core/c4/likec4.js +169 -14
- 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/deployment.js +160 -0
- package/dist/core/c4/parsed/deployment.js.map +1 -0
- package/dist/core/c4/parsed/dynamic-views.js.map +1 -1
- package/dist/core/c4/parsed/values.js +54 -1
- package/dist/core/c4/parsed/values.js.map +1 -1
- package/dist/core/c4/parsed/view-ids.js +39 -1
- package/dist/core/c4/parsed/view-ids.js.map +1 -1
- package/dist/core/c4/prefetch/fleet.js +134 -0
- package/dist/core/c4/prefetch/fleet.js.map +1 -0
- package/dist/core/c4/project/architecture.js +123 -7
- package/dist/core/c4/project/architecture.js.map +1 -1
- package/dist/core/c4/project/batch.js +140 -0
- package/dist/core/c4/project/batch.js.map +1 -0
- package/dist/core/c4/project/documents.js +43 -12
- package/dist/core/c4/project/documents.js.map +1 -1
- package/dist/core/c4/project/load.js +79 -43
- package/dist/core/c4/project/load.js.map +1 -1
- package/dist/core/c4/project/staged.js +277 -0
- package/dist/core/c4/project/staged.js.map +1 -0
- package/dist/core/c4/resolve/attested.js +0 -0
- package/dist/core/c4/resolve/attested.js.map +1 -0
- 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 +78 -17
- package/dist/core/c4/resolve/service.js.map +1 -1
- package/dist/core/c4/resolve/steps.js +290 -0
- package/dist/core/c4/resolve/steps.js.map +1 -0
- package/dist/core/c4/resolve/systems.js +154 -0
- package/dist/core/c4/resolve/systems.js.map +1 -0
- package/dist/core/c4/root-project/exclude.js +352 -0
- package/dist/core/c4/root-project/exclude.js.map +1 -0
- package/dist/core/c4/seed/template.js +65 -1
- package/dist/core/c4/seed/template.js.map +1 -1
- package/dist/core/c4/service-model/created/models.js +96 -0
- package/dist/core/c4/service-model/created/models.js.map +1 -0
- package/dist/core/c4/service-model/documents.js +47 -0
- package/dist/core/c4/service-model/documents.js.map +1 -0
- package/dist/core/c4/service-model/fleet/extending.js +82 -0
- package/dist/core/c4/service-model/fleet/extending.js.map +1 -0
- package/dist/core/c4/service-model/load.js +165 -0
- package/dist/core/c4/service-model/load.js.map +1 -0
- package/dist/core/c4/service-model/renderer.js +128 -0
- package/dist/core/c4/service-model/renderer.js.map +1 -0
- package/dist/core/c4/service-model/shape.js +106 -0
- package/dist/core/c4/service-model/shape.js.map +1 -0
- package/dist/core/c4/service-model/slice.js +188 -0
- package/dist/core/c4/service-model/slice.js.map +1 -0
- package/dist/core/c4/source-mask.js +297 -10
- package/dist/core/c4/source-mask.js.map +1 -1
- package/dist/core/c4/source-scan.js +222 -141
- package/dist/core/c4/source-scan.js.map +1 -1
- package/dist/core/c4/splice/authored-source.js +79 -13
- package/dist/core/c4/splice/authored-source.js.map +1 -1
- package/dist/core/c4/splice/contract.js +20 -0
- package/dist/core/c4/splice/contract.js.map +1 -1
- package/dist/core/c4/splice/delta-blocks.js +20 -20
- package/dist/core/c4/splice/delta-blocks.js.map +1 -1
- package/dist/core/c4/splice/identity/declared.js +87 -0
- package/dist/core/c4/splice/identity/declared.js.map +1 -0
- package/dist/core/c4/splice/identity/edges.js +37 -0
- package/dist/core/c4/splice/identity/edges.js.map +1 -1
- package/dist/core/c4/splice/identity/existing.js +80 -0
- package/dist/core/c4/splice/identity/existing.js.map +1 -0
- package/dist/core/c4/splice/identity/interior.js +80 -0
- package/dist/core/c4/splice/identity/interior.js.map +1 -0
- package/dist/core/c4/splice/identity/owner.js +50 -0
- package/dist/core/c4/splice/identity/owner.js.map +1 -0
- package/dist/core/c4/splice/landscape-merge.js +182 -114
- package/dist/core/c4/splice/landscape-merge.js.map +1 -1
- package/dist/core/c4/splice/model/merge.js +151 -0
- package/dist/core/c4/splice/model/merge.js.map +1 -0
- package/dist/core/c4/splice/placement.js +160 -56
- package/dist/core/c4/splice/placement.js.map +1 -1
- package/dist/core/c4/splice/text/spot.js +131 -0
- package/dist/core/c4/splice/text/spot.js.map +1 -0
- package/dist/core/c4/workspace.js +13 -19
- 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/delta/uncovered.js +2 -2
- package/dist/core/capabilities/delta/uncovered.js.map +1 -1
- package/dist/core/capabilities/findings.js +18 -22
- 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/calls/called.js +52 -0
- package/dist/core/coherence/calls/called.js.map +1 -0
- package/dist/core/coherence/coherence.js +103 -22
- 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/covers/health.js +85 -0
- package/dist/core/coherence/covers/health.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 +147 -16
- 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 +28 -87
- package/dist/core/coherence/events/lookups.js.map +1 -1
- package/dist/core/coherence/events/produced.js +100 -0
- package/dist/core/coherence/events/produced.js.map +1 -0
- 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 +51 -6
- package/dist/core/delta/delta.js.map +1 -1
- package/dist/core/dependencies/fanin.js +2 -2
- package/dist/core/dependencies/fanin.js.map +1 -1
- package/dist/core/deployment/delta.js +191 -0
- package/dist/core/deployment/delta.js.map +1 -0
- package/dist/core/deployment/objects.js +145 -0
- package/dist/core/deployment/objects.js.map +1 -0
- package/dist/core/diff/base-git.js +94 -12
- package/dist/core/diff/base-git.js.map +1 -1
- package/dist/core/diff/base-state.js +49 -21
- 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/docs.js +31 -22
- package/dist/core/docs.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/config.js +7 -3
- package/dist/core/doctor/config.js.map +1 -1
- package/dist/core/doctor/doctor.js +91 -11
- package/dist/core/doctor/doctor.js.map +1 -1
- package/dist/core/doctor/report.js.map +1 -1
- package/dist/core/doctor/reports/scan.js +242 -0
- package/dist/core/doctor/reports/scan.js.map +1 -0
- package/dist/core/document/apply.js +158 -11
- 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 +26 -21
- package/dist/core/explain/families.js.map +1 -1
- package/dist/core/explain/lookup.js +14 -1
- package/dist/core/explain/lookup.js.map +1 -1
- package/dist/core/explain/refusals.js +5 -5
- package/dist/core/explain/refusals.js.map +1 -1
- package/dist/core/explain/terms/documents.js +227 -0
- package/dist/core/explain/terms/documents.js.map +1 -0
- package/dist/core/explain/terms/done-check.js +144 -0
- package/dist/core/explain/terms/done-check.js.map +1 -0
- package/dist/core/explain/terms/entry.js +14 -0
- package/dist/core/explain/terms/entry.js.map +1 -0
- package/dist/core/explain/terms/stages.js +71 -0
- package/dist/core/explain/terms/stages.js.map +1 -0
- package/dist/core/explain/terms/tasks.js +97 -0
- package/dist/core/explain/terms/tasks.js.map +1 -0
- package/dist/core/explain/terms.js +68 -109
- package/dist/core/explain/terms.js.map +1 -1
- package/dist/core/explore/explore.js +40 -10
- package/dist/core/explore/explore.js.map +1 -1
- package/dist/core/fleet-context.js +81 -95
- package/dist/core/fleet-context.js.map +1 -1
- package/dist/core/gate/checks.js +11 -4
- package/dist/core/gate/checks.js.map +1 -1
- package/dist/core/gate/partners.js +69 -28
- 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/delta.js +10 -7
- package/dist/core/glossary/delta.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/ids/fqn/ancestors.js +65 -0
- package/dist/core/kernel/ids/fqn/ancestors.js.map +1 -0
- package/dist/core/kernel/path-safety.js +38 -0
- package/dist/core/kernel/path-safety.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 +51 -37
- package/dist/core/links/corpus.js.map +1 -1
- package/dist/core/links/findings.js.map +1 -1
- package/dist/core/links/parse.js +38 -9
- package/dist/core/links/parse.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 +40 -10
- 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 +301 -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 +28 -52
- package/dist/core/openapi/merge/merge.js.map +1 -1
- package/dist/core/openapi/merge/pin.js +17 -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 +43 -4
- package/dist/core/pack/living.js.map +1 -1
- package/dist/core/pack/pack.js +50 -6
- package/dist/core/pack/pack.js.map +1 -1
- package/dist/core/projection/arch-slice.js +2 -2
- package/dist/core/projection/arch-slice.js.map +1 -1
- package/dist/core/projection/introduced.js +14 -0
- package/dist/core/projection/introduced.js.map +1 -0
- 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/authored/paths.js +18 -0
- package/dist/core/repo/authored/paths.js.map +1 -1
- package/dist/core/repo/entries.js.map +1 -1
- package/dist/core/repo/paths.js +92 -6
- package/dist/core/repo/paths.js.map +1 -1
- package/dist/core/repo/repo.js +74 -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 +258 -0
- package/dist/core/repo/tree/render/body.js.map +1 -0
- package/dist/core/repo/tree/render/projects.js +182 -0
- package/dist/core/repo/tree/render/projects.js.map +1 -0
- 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 +42 -0
- package/dist/core/repo/tree/render/stale.js.map +1 -0
- package/dist/core/repo/tree/render/text.js +84 -0
- package/dist/core/repo/tree/render/text.js.map +1 -0
- package/dist/core/repo/tree/render/views.js +293 -0
- package/dist/core/repo/tree/render/views.js.map +1 -0
- package/dist/core/repo/tree/walk.js +53 -32
- 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/scaffold/landscape.js +121 -13
- package/dist/core/scaffold/landscape.js.map +1 -1
- package/dist/core/scaffold/legacy.js +271 -0
- package/dist/core/scaffold/legacy.js.map +1 -0
- package/dist/core/staging/commit.js +26 -15
- 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 +84 -6
- package/dist/core/staging/recovery/recover.js.map +1 -1
- package/dist/core/staging/snapshot/manifest.js +188 -0
- package/dist/core/staging/snapshot/manifest.js.map +1 -0
- package/dist/core/staging/{snapshot.js → snapshot/snapshot.js} +39 -45
- package/dist/core/staging/snapshot/snapshot.js.map +1 -0
- package/dist/core/staging/txn/forward.js +24 -14
- package/dist/core/staging/txn/forward.js.map +1 -1
- package/dist/core/staging/writes.js.map +1 -1
- package/dist/core/status/actions/execution.js +11 -0
- package/dist/core/status/actions/execution.js.map +1 -1
- package/dist/core/status/contracts.js +39 -15
- package/dist/core/status/contracts.js.map +1 -1
- package/dist/core/status/feature/artifacts.js +33 -8
- package/dist/core/status/feature/artifacts.js.map +1 -1
- package/dist/core/status/feature/feature.js +24 -17
- package/dist/core/status/feature/feature.js.map +1 -1
- package/dist/core/status/feature/next.js +20 -7
- package/dist/core/status/feature/next.js.map +1 -1
- package/dist/core/status/feature/words.js +96 -0
- package/dist/core/status/feature/words.js.map +1 -0
- package/dist/core/status/fleet/fleet.js +43 -14
- package/dist/core/status/fleet/fleet.js.map +1 -1
- package/dist/core/status/fleet/next.js +153 -58
- package/dist/core/status/fleet/next.js.map +1 -1
- package/dist/core/status/report.js.map +1 -1
- package/dist/core/status/scan.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 +199 -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/claims.js +31 -7
- package/dist/core/usecases/delta/claims.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/delta/modified.js +315 -0
- package/dist/core/usecases/delta/modified.js.map +1 -0
- package/dist/core/usecases/delta/overlay.js +32 -191
- package/dist/core/usecases/delta/overlay.js.map +1 -1
- package/dist/core/usecases/fleet.js +195 -36
- package/dist/core/usecases/fleet.js.map +1 -1
- package/dist/{commands/validate/fleet/usecases → core/usecases/grade}/capability-tag.js +8 -9
- package/dist/core/usecases/grade/capability-tag.js.map +1 -0
- package/dist/core/usecases/grade/feature.js +88 -0
- package/dist/core/usecases/grade/feature.js.map +1 -0
- package/dist/{commands/validate/fleet/usecases → core/usecases/grade}/requirement-tag.js +3 -4
- package/dist/core/usecases/grade/requirement-tag.js.map +1 -0
- package/dist/{commands/validate/fleet/usecases → core/usecases/grade}/steps.js +102 -33
- package/dist/core/usecases/grade/steps.js.map +1 -0
- package/dist/{commands/validate/fleet/usecases → core/usecases/grade}/usecases.js +23 -10
- package/dist/core/usecases/grade/usecases.js.map +1 -0
- package/dist/core/usecases/operations.js +26 -14
- package/dist/core/usecases/operations.js.map +1 -1
- package/dist/core/usecases/place.js.map +1 -1
- package/dist/core/usecases/service/exclude.js +119 -0
- package/dist/core/usecases/service/exclude.js.map +1 -0
- package/dist/core/usecases/service/flows.js +164 -0
- package/dist/core/usecases/service/flows.js.map +1 -0
- 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 +70 -12
- 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 +4 -0
- package/dist/core/vocabulary/issue.js.map +1 -1
- package/dist/core/vocabulary/maturity.js +75 -4
- package/dist/core/vocabulary/maturity.js.map +1 -1
- package/dist/core/vocabulary/report.js +16 -1
- package/dist/core/vocabulary/report.js.map +1 -1
- package/examples/README.md +54 -11
- package/examples/docs/architecture/landscape.likec4 +62 -6
- package/examples/docs/architecture/services.likec4 +70 -0
- package/examples/docs/architecture/subsystems.likec4 +7 -0
- package/examples/docs/architecture/usecases/order-notification.likec4 +16 -7
- 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/FEAT-112-retire-order-v1/specs/order-service/openapi.yaml +4 -1
- package/examples/docs/features/archive/FEAT-088-refunds/specs/payment-service/openapi.yaml +13 -10
- package/examples/docs/likec4.config.json +0 -1
- package/examples/docs/services/checkout-web/model.likec4 +39 -56
- package/examples/docs/services/order-service/arch.spec.md +13 -6
- package/examples/docs/services/order-service/asyncapi.yaml +3 -2
- package/examples/docs/services/order-service/model.likec4 +58 -70
- package/examples/docs/services/payment-service/arch.spec.md +7 -4
- package/examples/docs/services/payment-service/model.likec4 +38 -37
- package/examples/docs/services/platform/identity-service/arch.spec.md +7 -4
- package/examples/docs/services/platform/identity-service/model.likec4 +35 -37
- package/examples/docs/services/platform/notification-service/asyncapi.yaml +2 -1
- package/examples/docs/services/platform/notification-service/health.yaml +8 -3
- package/examples/docs/services/platform/notification-service/model.likec4 +43 -64
- package/package.json +1 -1
- package/dist/commands/validate/checks/vocabulary.js +0 -31
- package/dist/commands/validate/checks/vocabulary.js.map +0 -1
- package/dist/commands/validate/fleet/usecases/capability-tag.js.map +0 -1
- package/dist/commands/validate/fleet/usecases/requirement-tag.js.map +0 -1
- package/dist/commands/validate/fleet/usecases/steps.js.map +0 -1
- package/dist/commands/validate/fleet/usecases/usecases.js.map +0 -1
- package/dist/core/agent/agents-md/artifacts.js +0 -220
- package/dist/core/agent/agents-md/artifacts.js.map +0 -1
- package/dist/core/agent/agents-md/cycle.js +0 -165
- 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 -247
- package/dist/core/diff/victims.js.map +0 -1
- package/dist/core/repo/tree/views.js +0 -172
- package/dist/core/repo/tree/views.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":"checklist.js","sourceRoot":"","sources":["../../../src/core/verify/checklist.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * The questions: what a feature's own artifacts promised, derived mechanically.\n *\n * The original plan was to extract C4 from the built code and diff it against\n * the delta. There is no such extractor and there will not be one — nothing\n * deterministic reads a service and says what its architecture MEANS, and two\n * generated models of the same code disagree in wording every run, so the diff\n * would flap and be switched off inside a week.\n *\n * What survives of the idea is the part that was always deterministic, and it\n * is this module: a feature says this service will exist, it will expose this\n * operation, that service will call it, this scenario will have a test. The\n * list comes from the same files `validate` already reads.\n *\n * The claim ids are a function of the claim and of nothing else — so two runs\n * are diffable, reordering the delta renames nothing, and rewording a scenario\n * DOES rename its claim, because an answer about text nobody wrote must not\n * carry over.\n *\n * `scenarioBodyHash` lives in `core/gherkin/digest.ts` — one recipe shared\n * with `core/gherkin/stamp.ts`, which stamps the same digest as a tag on every\n * generated scenario so `--results` can match a cucumber report back to a\n * claim. A second spelling of the hash would silently stop every scenario\n * claim from being answerable by a run.\n */\nimport { existsSync } from \"node:fs\";\nimport { readFile } from \"node:fs/promises\";\nimport { readAsyncapi } from \"../asyncapi/read.js\";\nimport { type Elem } from \"../c4/likec4.js\";\nimport { elementService, serviceResolver } from \"../c4/resolve/service.js\";\nimport { ACTOR_KINDS } from \"../vocabulary/maturity.js\";\nimport { operationIds, operations } from \"../openapi/doc.js\";\nimport { featurePaths, featureSpecPaths } from \"../repo/paths.js\";\nimport { locateServicePaths } from \"../repo/service-target.js\";\nimport { featureSpecServices } from \"../repo/repo.js\";\nimport { enumeratedServiceIds } from \"../repo/service-target.js\";\nimport { FleetContext } from \"../fleet-context.js\";\nimport { parseRequirements } from \"../document/parse.js\";\nimport { scenarioBodyHash } from \"../gherkin/digest.js\";\nimport type { DocsDir, FeatureDir } from \"../kernel/ids/dirs.js\";\n// The kind vocabulary and both hash recipes live in ./claims/identity.ts —\n// one module for everything that decides whether two runs ask the same\n// question, and this one for how the questions are derived.\nimport {\n checklistDigest,\n claimId,\n DIGEST_LENGTH,\n ID_LENGTH,\n type ClaimKind,\n} from \"./claims/identity.js\";\n\nexport interface Claim {\n /** `<kind>-<8 hex>` — stable for the life of the claim. See `claimId`. */\n id: string;\n kind: ClaimKind;\n /** The service whose code answers this. A caller's claim is filed under the caller. */\n subject: string;\n /** The claim in one line, answerable without reading the feature's files. */\n claim: string;\n /**\n * `scenario.tested` only: the first {@link DIGEST_LENGTH} hex of the\n * scenario's body hash — the exact digest `loam gherkin` stamps as\n * `@loam-digest-…`, which is what a cucumber report scenario carries and\n * what `--results` matches on. Absent on every other kind.\n */\n digest?: string;\n /**\n * `api.exposes` only: the operationId the claim asserts, structurally — the\n * join key `--contract-results` matches a contract report's entries on,\n * exactly as `digest` is `--results`'s. Never parsed back out of the claim\n * text, which is prose and may be reworded. Absent on every other kind.\n */\n operation?: string;\n /**\n * The literal string the claim asserts of the cited artifact — the\n * operationId for `api.exposes`, the message name for `event.declares`, the\n * edge's op for `c4.calls` — carried structurally so evidence pins\n * (`./pins/pin.ts`) can stamp it at record time and the read side never\n * parses it back out of claim prose. A separate field from `operation` on\n * purpose: `contestedOperations` filters on `operation !== undefined` and\n * must not start seeing `c4.calls` claims. Absent where a claim asserts no\n * literal (`service.exists`, `scenario.tested`).\n */\n token?: string;\n}\n\nexport interface Checklist {\n feature: string;\n claims: Claim[];\n /** A digest of the claim id SET — what says a record still answers this feature. */\n digest: string;\n}\n\n/**\n * Derive the checklist from a feature's own artifacts. No code is read here and\n * none should be: the whole design rests on the questions being computed from\n * documents that do not change between runs.\n */\nexport async function featureChecklist(\n docsDir: DocsDir,\n featureDir: FeatureDir,\n featureId: string,\n fleet?: FleetContext,\n): Promise<Checklist> {\n // The per-service loop below resolves living contracts through the\n // enumeration twice per service; without a shared context each of those is\n // a full fleet walk, so deriving one checklist cost O(services²) reads. A\n // caller-supplied context is used when given; otherwise one is created for\n // this derivation alone — the loop still pays a single walk either way.\n const ctx = fleet ?? new FleetContext();\n const seen = new Map<string, number>();\n const claim = (kind: ClaimKind, subject: string, parts: string[], text: string): Claim => ({\n id: claimId(featureId, kind, parts, seen),\n kind,\n subject,\n claim: text,\n });\n\n const exists: Claim[] = [];\n const exposes: Claim[] = [];\n const declares: Claim[] = [];\n const calls: Claim[] = [];\n const scenarios: Claim[] = [];\n\n const specServices = await featureSpecServices(featureDir, ctx);\n\n // Architecture — what the delta promised the fleet would look like. Loaded\n // through the context rather than a bare parse, so a caller that derives\n // several checklists in one invocation (`loam gate`'s verification check,\n // the fleet forms) shares one parse per delta — and a batch prefetch\n // (`FleetContext.prefetchLikeC4`) genuinely seeds this load.\n const deltaPath = featurePaths(featureDir).delta;\n if (existsSync(deltaPath)) {\n const res = await ctx.loadLikeC4(deltaPath);\n // A delta nobody can parse promises nothing checkable. `validate` already\n // reports it; inventing claims out of a broken document would only add noise.\n if (res.errors.length === 0) {\n const elements: Elem[] = res.elements;\n for (const e of elements) {\n if (!e.tags.includes(featureId)) continue;\n // A dotted id is a container INSIDE a service, and an actor is nobody's\n // repository. Neither is a service that has to exist.\n if (e.id.includes(\".\") || ACTOR_KINDS.has(e.kind.toLowerCase())) continue;\n const svc = elementService(e);\n exists.push(claim(\"service.exists\", svc, [svc], `service '${svc}' exists`));\n }\n // The enumerated fleet — `services/` plus this feature's own `specs/`\n // directories, the only place a service the feature INTRODUCES has a\n // directory at all — rides into the resolver so a claim about an edge\n // drawn into a modelled container names the service that owns it. A\n // claim against a service called \"api\" that has never existed is one no\n // evidence in any repository could ever answer, and the fleet half alone\n // still derived exactly that claim for an introduced service's own\n // container. Unenumerable services/ degrades to the feature's names.\n const svcOf = serviceResolver(\n elements,\n new Set<string>([...(await enumeratedServiceIds(docsDir, ctx)), ...specServices]),\n );\n for (const r of res.relationships) {\n // Untagged edges are context for the diagram; an edge with no operation\n // names nothing specific enough to look for in the code.\n if (!r.tags.includes(featureId) || r.op === undefined) continue;\n const from = svcOf(r.source);\n const to = svcOf(r.target);\n calls.push({\n ...claim(\"c4.calls\", from, [from, to, r.op], `${from} calls '${r.op}' on ${to}`),\n // The same string as the provider's api.exposes token, scanned in the\n // CALLER's cited file — a call site plausibly spells the operationId.\n token: r.op,\n });\n }\n }\n }\n\n for (const svc of specServices) {\n const paths = featureSpecPaths(featureDir, svc);\n\n // Contract — only what is NEW. A delta's openapi.yaml is a whole document,\n // not a patch, so authors restate the living API inside it; asking whether a\n // service still exposes what it already exposed is noise.\n const featOps = (await operations(paths.openapi))\n .filter((operation) => !operation.remove)\n .map((operation) => operation.id);\n if (featOps.length > 0) {\n const living = new Set(await operationIds((await locateServicePaths(docsDir, svc, ctx)).openapi));\n for (const op of featOps) {\n if (living.has(op)) continue;\n exposes.push({\n ...claim(\"api.exposes\", svc, [svc, op], `${svc} exposes operationId '${op}'`),\n operation: op,\n token: op,\n });\n }\n }\n\n // Events — the same NEW-only discipline on the async axis: a delta's\n // asyncapi.yaml is a complete document too, so what the living contract\n // already sends or receives is restatement, not a promise. Removals are\n // not questions either — the reader already keeps marked declarations out\n // of the sent/received sets — and an unreadable delta promises nothing\n // checkable, exactly like the broken C4 delta above.\n const events = await readAsyncapi(paths.asyncapi);\n if (events.sent.length > 0 || events.received.length > 0) {\n const living = await readAsyncapi((await locateServicePaths(docsDir, svc, ctx)).asyncapi);\n const directions = [\n { direction: \"sends\", feat: events.sent, known: new Set(living.sent) },\n { direction: \"receives\", feat: events.received, known: new Set(living.received) },\n ] as const;\n for (const d of directions) {\n for (const name of d.feat) {\n if (d.known.has(name)) continue;\n declares.push({\n ...claim(\n \"event.declares\",\n svc,\n [svc, d.direction, name],\n `${svc} declares it ${d.direction} message '${name}'`,\n ),\n token: name,\n });\n }\n }\n }\n\n // Behaviour — the scenarios of the requirements this feature CHANGES. A BASE\n // requirement is the living state quoted inside the delta, and a REMOVED one\n // is being retired: neither is work anybody has to have done.\n if (existsSync(paths.spec)) {\n for (const r of parseRequirements(await readFile(paths.spec, \"utf8\"))) {\n if (r.kind === \"BASE\" || r.kind === \"REMOVED\") continue;\n for (const s of r.scenarios) {\n const body = scenarioBodyHash(svc, s.lines);\n scenarios.push({\n ...claim(\n \"scenario.tested\",\n svc,\n [svc, r.id ?? r.name, s.name, body.slice(0, ID_LENGTH)],\n `scenario '${s.name}' of requirement '${r.name}' (${svc}) is covered by a test`,\n ),\n digest: body.slice(0, DIGEST_LENGTH),\n });\n }\n }\n }\n\n // The architecture spec axis, same walk: an arch scenario is a promise like\n // any other — the outbox test nobody was going to write IS the point of the\n // axis. The source file rides in the id tuple AND in the body hash itself\n // (two axes are two namespaces: identically-worded scenarios must stay two\n // questions, and the axis-salted digest is what keeps a business-only test\n // run from answering the arch claim) and in the claim text, so the\n // answering agent knows it is being asked for an integration/ops test, not\n // an acceptance test.\n if (existsSync(paths.archSpec)) {\n for (const r of parseRequirements(await readFile(paths.archSpec, \"utf8\"))) {\n if (r.kind === \"BASE\" || r.kind === \"REMOVED\") continue;\n for (const s of r.scenarios) {\n const body = scenarioBodyHash(svc, s.lines, \"arch\");\n scenarios.push({\n ...claim(\n \"scenario.tested\",\n svc,\n [svc, \"arch.spec.md\", r.id ?? r.name, s.name, body.slice(0, ID_LENGTH)],\n `scenario '${s.name}' of arch requirement '${r.name}' (${svc}, arch.spec.md) is covered by a test`,\n ),\n digest: body.slice(0, DIGEST_LENGTH),\n });\n }\n }\n }\n }\n\n const claims = [...exists, ...exposes, ...declares, ...calls, ...scenarios];\n return { feature: featureId, claims, digest: checklistDigest(claims.map((c) => c.id)) };\n}\n"]}
|
|
1
|
+
{"version":3,"file":"checklist.js","sourceRoot":"","sources":["../../../src/core/verify/checklist.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * The questions: what a feature's own artifacts promised, derived mechanically.\n *\n * The original plan was to extract C4 from the built code and diff it against\n * the delta. There is no such extractor and there will not be one — nothing\n * deterministic reads a service and says what its architecture MEANS, and two\n * generated models of the same code disagree in wording every run, so the diff\n * would flap and be switched off inside a week.\n *\n * What survives of the idea is the part that was always deterministic, and it\n * is this module: a feature says this service will exist, it will expose this\n * operation, that service will call it, this scenario will have a test. The\n * list comes from the same files `validate` already reads.\n *\n * The claim ids are a function of the claim and of nothing else — so two runs\n * are diffable, reordering the delta renames nothing, and rewording a scenario\n * DOES rename its claim, because an answer about text nobody wrote must not\n * carry over.\n *\n * `scenarioBodyHash` lives in `core/gherkin/digest.ts` — one recipe shared\n * with `core/gherkin/stamp.ts`, which stamps the same digest as a tag on every\n * generated scenario so `--results` can match a cucumber report back to a\n * claim. A second spelling of the hash would silently stop every scenario\n * claim from being answerable by a run.\n */\nimport { existsSync } from \"node:fs\";\nimport { readFile } from \"node:fs/promises\";\nimport { readAsyncapi } from \"../asyncapi/read.js\";\nimport { type Elem } from \"../c4/likec4.js\";\nimport { elementService, serviceResolver } from \"../c4/resolve/service.js\";\nimport { livingMapOf, readDeltaSystem } from \"../c4/resolve/systems.js\";\nimport { scenarioGherkin } from \"../vocabulary/steps.js\";\nimport { operationIds, operations } from \"../openapi/doc.js\";\nimport { featurePaths, featureSpecPaths, landscapePath } from \"../repo/paths.js\";\nimport { locateServicePaths } from \"../repo/service-target.js\";\nimport { featureSpecServices } from \"../repo/repo.js\";\nimport { enumeratedServiceIds } from \"../repo/service-target.js\";\nimport { FleetContext } from \"../fleet-context.js\";\nimport { parseRequirements } from \"../document/parse.js\";\nimport { scenarioBodyHash } from \"../gherkin/digest.js\";\nimport type { DocsDir, FeatureDir } from \"../kernel/ids/dirs.js\";\n// The kind vocabulary and both hash recipes live in ./claims/identity.ts —\n// one module for everything that decides whether two runs ask the same\n// question, and this one for how the questions are derived.\nimport {\n checklistDigest,\n claimId,\n DIGEST_LENGTH,\n ID_LENGTH,\n type ClaimKind,\n} from \"./claims/identity.js\";\n\nexport interface Claim {\n /** `<kind>-<8 hex>` — stable for the life of the claim. See `claimId`. */\n id: string;\n kind: ClaimKind;\n /** The service whose code answers this. A caller's claim is filed under the caller. */\n subject: string;\n /** The claim in one line, answerable without reading the feature's files. */\n claim: string;\n /**\n * `scenario.tested` only: the first {@link DIGEST_LENGTH} hex of the\n * scenario's body hash — the exact digest `loam gherkin` stamps as\n * `@loam-digest-…`, which is what a cucumber report scenario carries and\n * what `--results` matches on. Absent on every other kind.\n */\n digest?: string;\n /**\n * `scenario.tested` only: how many steps `loam gherkin` emits for the\n * scenario — `scenarioGherkin`'s own count, the one the generated `.feature`\n * carries. `--results` holds a cucumber run to it: the digest says WHICH\n * scenario a report entry claims to be, and only the step count can say the\n * entry is that scenario's run rather than a one-step stub tagged by hand\n * (which confirmed five-step scenarios before this existed). The count, not\n * the step text: an outline's steps reach the report with their\n * `<placeholders>` filled in, and dialects disagree about keyword spacing and\n * localisation, while every one of them reports one entry per step.\n */\n steps?: number;\n /**\n * `api.exposes` only: the operationId the claim asserts, structurally — the\n * join key `--contract-results` matches a contract report's entries on,\n * exactly as `digest` is `--results`'s. Never parsed back out of the claim\n * text, which is prose and may be reworded. Absent on every other kind.\n */\n operation?: string;\n /**\n * The literal string the claim asserts of the cited artifact — the\n * operationId for `api.exposes`, the message name for `event.declares`, the\n * edge's op for `c4.calls` — carried structurally so evidence pins\n * (`./pins/pin.ts`) can stamp it at record time and the read side never\n * parses it back out of claim prose. A separate field from `operation` on\n * purpose: `contestedOperations` filters on `operation !== undefined` and\n * must not start seeing `c4.calls` claims. Absent where a claim asserts no\n * literal (`service.exists`, `scenario.tested`).\n */\n token?: string;\n}\n\nexport interface Checklist {\n feature: string;\n claims: Claim[];\n /** A digest of the claim id SET — what says a record still answers this feature. */\n digest: string;\n}\n\n/**\n * Derive the checklist from a feature's own artifacts. No code is read here and\n * none should be: the whole design rests on the questions being computed from\n * documents that do not change between runs. Those are the feature's own\n * files, read against the fleet they merge into — the `services/`\n * enumeration, which says which elements stand for a service, and, for a\n * tagged element no directory answers for, the living map, whose tags the\n * merge keeps (`../c4/resolve/systems.ts`). A map that does not parse\n * leaves the delta to answer alone, which is the answer given before the map\n * was asked.\n */\nexport async function featureChecklist(\n docsDir: DocsDir,\n featureDir: FeatureDir,\n featureId: string,\n fleet?: FleetContext,\n): Promise<Checklist> {\n // The per-service loop below resolves living contracts through the\n // enumeration twice per service; without a shared context each of those is\n // a full fleet walk, so deriving one checklist cost O(services²) reads. A\n // caller-supplied context is used when given; otherwise one is created for\n // this derivation alone — the loop still pays a single walk either way.\n const ctx = fleet ?? new FleetContext();\n const seen = new Map<string, number>();\n const claim = (kind: ClaimKind, subject: string, parts: string[], text: string): Claim => ({\n id: claimId(featureId, kind, parts, seen),\n kind,\n subject,\n claim: text,\n });\n\n const exists: Claim[] = [];\n const exposes: Claim[] = [];\n const declares: Claim[] = [];\n const calls: Claim[] = [];\n const scenarios: Claim[] = [];\n\n const specServices = await featureSpecServices(featureDir, ctx);\n\n // Architecture — what the delta promised the fleet would look like. Loaded\n // through the context rather than a bare parse, so a caller that derives\n // several checklists in one invocation (`loam gate`'s verification check,\n // the fleet forms) shares one parse per delta — and a batch prefetch\n // (`FleetContext.prefetchLikeC4`) genuinely seeds this load.\n const deltaPath = featurePaths(featureDir).delta;\n if (existsSync(deltaPath)) {\n const res = await ctx.loadLikeC4(deltaPath);\n // A delta nobody can parse promises nothing checkable. `validate` already\n // reports it; inventing claims out of a broken document would only add noise.\n if (res.errors.length === 0) {\n const elements: Elem[] = res.elements;\n // The enumerated fleet — `services/` plus this feature's own `specs/`\n // directories, the only place a service the feature INTRODUCES has a\n // directory at all. Built once, above both readers below, because they\n // must not disagree: the skip loop asks whether an element STANDS FOR a\n // service, and the resolver asks WHICH service an endpoint belongs to,\n // and a set that answered the second question only left the first one\n // reading a title as unknown that the very next line resolved.\n const living = new Set<string>(await enumeratedServiceIds(docsDir, ctx));\n const known = new Set<string>([...living, ...specServices]);\n // The living map, parsed only for a tagged element no directory answers for.\n const systems = { elements, known, living, livingMap: livingMapOf(landscapePath(docsDir), (path) => ctx.loadLikeC4(path)) };\n for (const e of elements) {\n if (!e.tags.includes(featureId)) continue;\n // Only a SYSTEM of ours is a service that has to exist — the fleet\n // census's reading, applied to the map this merge leaves\n // (`../c4/resolve/systems.ts` holds it and says what it skips). An\n // element inside a service is its container, an actor is nobody's\n // repository, an `#external` system is somebody else's, a grouping is\n // scenery for the services under it, and the fleet's own\n // infrastructure — a `database`, `queue`, `topic`, `cache` or\n // `bucket` — has no repository either. None of them is a service that\n // has to exist.\n //\n // The infra skip closes a claim no repository could ever answer: a\n // delta that draws a fleet-level `database` minted `service.exists` for\n // it, and `loam verify` then asked somebody to point at the code that\n // proves a Postgres has a services/ directory. The `#external` and\n // grouping skips close the same kind of claim: a tagged producer\n // outside the fleet, or the domain a new service is drawn inside, each\n // became a `loam tasks` unit asking for a service to be built.\n //\n // STANDING FOR A SERVICE still wins over the infra kind, because that is\n // a claim about this repo rather than a guess at one. Both of its rungs\n // matter: a `database` carrying `metadata { service '<id>' }` names a\n // directory the author says exists, and one whose TITLE names a real\n // `services/<id>/` names a directory the enumeration just read. Testing\n // the binding alone made an adopted store a service on every other\n // surface and infrastructure here, so its `service.exists` claim\n // silently went missing from the checklist that is supposed to hold\n // every promise the feature made.\n //\n // DEPTH IS NOT A FACT ABOUT A SERVICE. This used to skip every dotted\n // id as a container, so the new service a delta draws inside the map's\n // own group — FEAT-101 in the shipped example — was never asked about\n // at all. A grouping the delta declares is looked through; any other\n // level is what its children belong to.\n //\n // The stub of a service that already has a directory keeps exactly the\n // answer it always had — asked about when tagged at top level, never\n // when nested — because the question is not this module's to reopen\n // here: nesting it inside the map's group must not turn a service\n // somebody adopted into \"create service\" at the head of `loam tasks`,\n // and the top-level claim is pinned on purpose (an adopted store named\n // by its title is a promise the feature makes about it).\n const reading = await readDeltaSystem(e, systems);\n if (reading === \"none\" || (reading === \"living\" && e.id.includes(\".\"))) continue;\n const svc = elementService(e);\n exists.push(claim(\"service.exists\", svc, [svc], `service '${svc}' exists`));\n }\n // That same set rides into the resolver so a claim about an edge drawn\n // into a modelled container names the service that owns it. A claim\n // against a service called \"api\" that has never existed is one no\n // evidence in any repository could ever answer, and the fleet half alone\n // still derived exactly that claim for an introduced service's own\n // container. Unenumerable services/ degrades to the feature's names.\n const svcOf = serviceResolver(elements, known);\n for (const r of res.relationships) {\n // Untagged edges are context for the diagram; an edge with no operation\n // names nothing specific enough to look for in the code.\n if (!r.tags.includes(featureId) || r.op === undefined) continue;\n const from = svcOf(r.source);\n const to = svcOf(r.target);\n calls.push({\n ...claim(\"c4.calls\", from, [from, to, r.op], `${from} calls '${r.op}' on ${to}`),\n // The same string as the provider's api.exposes token, scanned in the\n // CALLER's cited file — a call site plausibly spells the operationId.\n token: r.op,\n });\n }\n }\n }\n\n for (const svc of specServices) {\n const paths = featureSpecPaths(featureDir, svc);\n\n // Contract — only what is NEW. A delta's openapi.yaml is a whole document,\n // not a patch, so authors restate the living API inside it; asking whether a\n // service still exposes what it already exposed is noise.\n const featOps = (await operations(paths.openapi))\n .filter((operation) => !operation.remove)\n .map((operation) => operation.id);\n if (featOps.length > 0) {\n const living = new Set(await operationIds((await locateServicePaths(docsDir, svc, ctx)).openapi));\n for (const op of featOps) {\n if (living.has(op)) continue;\n exposes.push({\n ...claim(\"api.exposes\", svc, [svc, op], `${svc} exposes operationId '${op}'`),\n operation: op,\n token: op,\n });\n }\n }\n\n // Events — the same NEW-only discipline on the async axis: a delta's\n // asyncapi.yaml is a complete document too, so what the living contract\n // already sends or receives is restatement, not a promise. Removals are\n // not questions either — the reader already keeps marked declarations out\n // of the sent/received sets — and an unreadable delta promises nothing\n // checkable, exactly like the broken C4 delta above.\n const events = await readAsyncapi(paths.asyncapi);\n if (events.sent.length > 0 || events.received.length > 0) {\n const living = await readAsyncapi((await locateServicePaths(docsDir, svc, ctx)).asyncapi);\n const directions = [\n { direction: \"sends\", feat: events.sent, known: new Set(living.sent) },\n { direction: \"receives\", feat: events.received, known: new Set(living.received) },\n ] as const;\n for (const d of directions) {\n for (const name of d.feat) {\n if (d.known.has(name)) continue;\n declares.push({\n ...claim(\n \"event.declares\",\n svc,\n [svc, d.direction, name],\n `${svc} declares it ${d.direction} message '${name}'`,\n ),\n token: name,\n });\n }\n }\n }\n\n // Behaviour — the scenarios of the requirements this feature CHANGES. A BASE\n // requirement is the living state quoted inside the delta, and a REMOVED one\n // is being retired: neither is work anybody has to have done.\n if (existsSync(paths.spec)) {\n for (const r of parseRequirements(await readFile(paths.spec, \"utf8\"))) {\n if (r.kind === \"BASE\" || r.kind === \"REMOVED\") continue;\n for (const s of r.scenarios) {\n const body = scenarioBodyHash(svc, s.lines);\n scenarios.push({\n ...claim(\n \"scenario.tested\",\n svc,\n [svc, r.id ?? r.name, s.name, body.slice(0, ID_LENGTH)],\n `scenario '${s.name}' of requirement '${r.name}' (${svc}) is covered by a test`,\n ),\n digest: body.slice(0, DIGEST_LENGTH),\n steps: scenarioGherkin(s.lines).steps.length,\n });\n }\n }\n }\n\n // The architecture spec axis, same walk: an arch scenario is a promise like\n // any other — the outbox test nobody was going to write IS the point of the\n // axis. The source file rides in the id tuple AND in the body hash itself\n // (two axes are two namespaces: identically-worded scenarios must stay two\n // questions, and the axis-salted digest is what keeps a business-only test\n // run from answering the arch claim) and in the claim text, so the\n // answering agent knows it is being asked for an integration/ops test, not\n // an acceptance test.\n if (existsSync(paths.archSpec)) {\n for (const r of parseRequirements(await readFile(paths.archSpec, \"utf8\"))) {\n if (r.kind === \"BASE\" || r.kind === \"REMOVED\") continue;\n for (const s of r.scenarios) {\n const body = scenarioBodyHash(svc, s.lines, \"arch\");\n scenarios.push({\n ...claim(\n \"scenario.tested\",\n svc,\n [svc, \"arch.spec.md\", r.id ?? r.name, s.name, body.slice(0, ID_LENGTH)],\n `scenario '${s.name}' of arch requirement '${r.name}' (${svc}, arch.spec.md) is covered by a test`,\n ),\n digest: body.slice(0, DIGEST_LENGTH),\n steps: scenarioGherkin(s.lines).steps.length,\n });\n }\n }\n }\n }\n\n const claims = [...exists, ...exposes, ...declares, ...calls, ...scenarios];\n return { feature: featureId, claims, digest: checklistDigest(claims.map((c) => c.id)) };\n}\n"]}
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Where the verification record lives on disk — the one-file form, the
|
|
3
|
+
* per-service directory, and which of the two a reader is pointed at.
|
|
4
|
+
*
|
|
5
|
+
* Split out of `./record.ts`, which keeps what gets written down and how a set
|
|
6
|
+
* of answers counts up to a verdict: the addresses are a different subject from
|
|
7
|
+
* the shapes and the arithmetic, and the per-service directory pushed that
|
|
8
|
+
* module over the line limit. The readers (`./read/file.ts`), the writers
|
|
9
|
+
* (`loam verify`'s record step) and `status`'s artifact row all ask where the
|
|
10
|
+
* record is; none of them needs the record's shape to ask it.
|
|
11
|
+
*/
|
|
12
|
+
import { existsSync } from "node:fs";
|
|
13
|
+
import { join } from "node:path";
|
|
14
|
+
/**
|
|
15
|
+
* Where the record lives: inside the feature, so `archive` carries it into
|
|
16
|
+
* `features/archive/` with everything else and a reviewer finds it next to the
|
|
17
|
+
* delta it is about. YAML rather than frontmatter or JSON — it is data, it has
|
|
18
|
+
* to survive without loam, and one claim per block is a diff a person can read.
|
|
19
|
+
*
|
|
20
|
+
* This is the ONE-FILE form: what the all-at-once `--record` (no `--service`)
|
|
21
|
+
* writes, and what every federated record written before the per-service
|
|
22
|
+
* directory existed still is. It is read forever, active and archived alike.
|
|
23
|
+
* A federated attestation no longer goes here — see {@link serviceRecordPath}.
|
|
24
|
+
*/
|
|
25
|
+
export function verificationPath(featureDir) {
|
|
26
|
+
return join(featureDir, "verification.yaml");
|
|
27
|
+
}
|
|
28
|
+
/**
|
|
29
|
+
* The federated record: one file per attesting service, and the DIRECTORY is
|
|
30
|
+
* the list — the `glossary/` and `capabilities/` rule.
|
|
31
|
+
*
|
|
32
|
+
* One shared file was the wrong shape for a fleet. Every service's attestation
|
|
33
|
+
* rewrote the same `summary`, `recorded` and checklist-ordered `claims`, so two
|
|
34
|
+
* docs-repo branches recording two DIFFERENT services always conflicted in git
|
|
35
|
+
* — add/add from an empty start, content conflicts on the summary once one
|
|
36
|
+
* attestation had merged — and sorting the attestations did not help, because
|
|
37
|
+
* two insertions at one anchor are a conflict whatever the order. The lock that
|
|
38
|
+
* serialises writers covers one checkout; a fleet records from many. With one
|
|
39
|
+
* file per service, two teams' records touch two different paths and merge
|
|
40
|
+
* cleanly, and the verdict is computed from the union at read time — which it
|
|
41
|
+
* always was: nothing ever trusted the stored `summary`.
|
|
42
|
+
*/
|
|
43
|
+
export function verificationDir(featureDir) {
|
|
44
|
+
return join(featureDir, "verification");
|
|
45
|
+
}
|
|
46
|
+
/**
|
|
47
|
+
* One service's attestation file. The service id grammar admits no `/`, so this
|
|
48
|
+
* never leaves the directory — and the parameter type is what makes that a
|
|
49
|
+
* guarantee rather than a hope: a `PathableService` is a name whose provenance
|
|
50
|
+
* is the repository (a validated id, or a directory that exists), so a name a
|
|
51
|
+
* document merely asserts, or a raw flag nobody checked, does not compile here.
|
|
52
|
+
*/
|
|
53
|
+
export function serviceRecordPath(featureDir, service) {
|
|
54
|
+
return join(verificationDir(featureDir), `${service}.yaml`);
|
|
55
|
+
}
|
|
56
|
+
/**
|
|
57
|
+
* Where a reader should look: the directory once it exists, the one file
|
|
58
|
+
* otherwise. A single path is lossy on purpose — a record mid-migration is
|
|
59
|
+
* both — and what a reader is pointed at is where the next attestation will
|
|
60
|
+
* land. A legacy file beside the directory is still read (see `./read/file.ts`);
|
|
61
|
+
* it is simply no longer where anything is written for a service.
|
|
62
|
+
*/
|
|
63
|
+
export function recordLocation(featureDir) {
|
|
64
|
+
const dir = verificationDir(featureDir);
|
|
65
|
+
return existsSync(dir) ? dir : verificationPath(featureDir);
|
|
66
|
+
}
|
|
67
|
+
//# sourceMappingURL=location.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"location.js","sourceRoot":"","sources":["../../../src/core/verify/location.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;GAUG;AACH,OAAO,EAAE,UAAU,EAAE,MAAM,SAAS,CAAC;AACrC,OAAO,EAAE,IAAI,EAAE,MAAM,WAAW,CAAC;AAGjC;;;;;;;;;;GAUG;AACH,MAAM,UAAU,gBAAgB,CAAC,UAAkB;IACjD,OAAO,IAAI,CAAC,UAAU,EAAE,mBAAmB,CAAC,CAAC;AAC/C,CAAC;AAED;;;;;;;;;;;;;;GAcG;AACH,MAAM,UAAU,eAAe,CAAC,UAAkB;IAChD,OAAO,IAAI,CAAC,UAAU,EAAE,cAAc,CAAC,CAAC;AAC1C,CAAC;AAED;;;;;;GAMG;AACH,MAAM,UAAU,iBAAiB,CAAC,UAAkB,EAAE,OAAwB;IAC5E,OAAO,IAAI,CAAC,eAAe,CAAC,UAAU,CAAC,EAAE,GAAG,OAAO,OAAO,CAAC,CAAC;AAC9D,CAAC;AAED;;;;;;GAMG;AACH,MAAM,UAAU,cAAc,CAAC,UAAkB;IAC/C,MAAM,GAAG,GAAG,eAAe,CAAC,UAAU,CAAC,CAAC;IACxC,OAAO,UAAU,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC,GAAG,CAAC,CAAC,CAAC,gBAAgB,CAAC,UAAU,CAAC,CAAC;AAC9D,CAAC","sourcesContent":["/**\n * Where the verification record lives on disk — the one-file form, the\n * per-service directory, and which of the two a reader is pointed at.\n *\n * Split out of `./record.ts`, which keeps what gets written down and how a set\n * of answers counts up to a verdict: the addresses are a different subject from\n * the shapes and the arithmetic, and the per-service directory pushed that\n * module over the line limit. The readers (`./read/file.ts`), the writers\n * (`loam verify`'s record step) and `status`'s artifact row all ask where the\n * record is; none of them needs the record's shape to ask it.\n */\nimport { existsSync } from \"node:fs\";\nimport { join } from \"node:path\";\nimport type { PathableService } from \"../kernel/ids/service.js\";\n\n/**\n * Where the record lives: inside the feature, so `archive` carries it into\n * `features/archive/` with everything else and a reviewer finds it next to the\n * delta it is about. YAML rather than frontmatter or JSON — it is data, it has\n * to survive without loam, and one claim per block is a diff a person can read.\n *\n * This is the ONE-FILE form: what the all-at-once `--record` (no `--service`)\n * writes, and what every federated record written before the per-service\n * directory existed still is. It is read forever, active and archived alike.\n * A federated attestation no longer goes here — see {@link serviceRecordPath}.\n */\nexport function verificationPath(featureDir: string): string {\n return join(featureDir, \"verification.yaml\");\n}\n\n/**\n * The federated record: one file per attesting service, and the DIRECTORY is\n * the list — the `glossary/` and `capabilities/` rule.\n *\n * One shared file was the wrong shape for a fleet. Every service's attestation\n * rewrote the same `summary`, `recorded` and checklist-ordered `claims`, so two\n * docs-repo branches recording two DIFFERENT services always conflicted in git\n * — add/add from an empty start, content conflicts on the summary once one\n * attestation had merged — and sorting the attestations did not help, because\n * two insertions at one anchor are a conflict whatever the order. The lock that\n * serialises writers covers one checkout; a fleet records from many. With one\n * file per service, two teams' records touch two different paths and merge\n * cleanly, and the verdict is computed from the union at read time — which it\n * always was: nothing ever trusted the stored `summary`.\n */\nexport function verificationDir(featureDir: string): string {\n return join(featureDir, \"verification\");\n}\n\n/**\n * One service's attestation file. The service id grammar admits no `/`, so this\n * never leaves the directory — and the parameter type is what makes that a\n * guarantee rather than a hope: a `PathableService` is a name whose provenance\n * is the repository (a validated id, or a directory that exists), so a name a\n * document merely asserts, or a raw flag nobody checked, does not compile here.\n */\nexport function serviceRecordPath(featureDir: string, service: PathableService): string {\n return join(verificationDir(featureDir), `${service}.yaml`);\n}\n\n/**\n * Where a reader should look: the directory once it exists, the one file\n * otherwise. A single path is lossy on purpose — a record mid-migration is\n * both — and what a reader is pointed at is where the next attestation will\n * land. A legacy file beside the directory is still read (see `./read/file.ts`);\n * it is simply no longer where anything is written for a service.\n */\nexport function recordLocation(featureDir: string): string {\n const dir = verificationDir(featureDir);\n return existsSync(dir) ? dir : verificationPath(featureDir);\n}\n"]}
|
|
@@ -6,8 +6,9 @@
|
|
|
6
6
|
*
|
|
7
7
|
* The claim shape is structural rather than an import of `../record.js`'s
|
|
8
8
|
* `RecordedClaim`, and not only for the smallest-parameter rule: this package
|
|
9
|
-
* is imported BY `core/verify` (`../
|
|
10
|
-
* guard, `../answers.ts` for the shape), so importing
|
|
9
|
+
* is imported BY `core/verify` and its `read/` sibling (`../read/shape.ts` asks
|
|
10
|
+
* `./pin.ts` for the read guard, `../answers.ts` for the shape), so importing
|
|
11
|
+
* the parent back would
|
|
11
12
|
* put a cycle in the package graph that `import/no-cycle` cannot see.
|
|
12
13
|
*/
|
|
13
14
|
import { readFile, stat } from "node:fs/promises";
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"lint.js","sourceRoot":"","sources":["../../../../src/core/verify/pins/lint.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * Re-checking evidence pins against a working tree: pure data out, one grade\n * per pin, and no verdict anywhere in the result on purpose — the lint demotes\n * reviewer confidence, it never promotes or demotes a claim. Prints nothing;\n * the validate command turns the grades into findings.\n *\n * The claim shape is structural rather than an import of `../record.js`'s\n * `RecordedClaim`, and not only for the smallest-parameter rule: this package\n * is imported BY `core/verify` (`../file.ts` asks `./pin.ts` for the read\n * guard, `../answers.ts` for the shape), so importing the parent back would\n * put a cycle in the package graph that `import/no-cycle` cannot see.\n */\nimport { readFile, stat } from \"node:fs/promises\";\nimport { inOrder } from \"../../kernel/concurrency.js\";\nimport { resolvePortableFileInside, UnsafePathError } from \"../../kernel/path-safety.js\";\nimport { pinnedDigest, pinnedText, sourceLines, type EvidencePin } from \"./pin.js\";\n\n/** What the lint reads off a recorded claim — structurally, see the header. */\nexport interface PinnedClaim {\n id: string;\n /** Present in federated (schema 2) records — the service whose code answers it. */\n subject?: string;\n verdict: string;\n /** Absent counts as agent — the `attestedClaims` rule for records that predate `--results`. */\n answered_by?: string;\n evidence: string[];\n evidence_pins?: EvidencePin[];\n}\n\n/** One pin's disagreement with the working tree. `grade` is what a finding code is made from. */\nexport interface PinDrift {\n claim: string;\n /** The `path:line` citation the pin belongs to. */\n evidence: string;\n grade: \"unresolved\" | \"moved\" | \"line-changed\" | \"token-missing\";\n /** What changed, in one clause — rendered into a finding's detail line. */\n what: string;\n}\n\nexport interface PinLint {\n /** Pins that resolved clean: file digest unchanged against the working tree. */\n checked: number;\n /** In record order — claim by claim, citation by citation — so two runs diff. */\n drifts: PinDrift[];\n /** Agent-confirmed citations carrying no pin, plus claims with no pins at all. */\n unpinned: number;\n}\n\n/** What one cited file turned out to be, read and hashed once per lint call. */\ntype FileState =\n | { state: \"unreadable\"; what: string }\n | { state: \"ok\"; digest: string; lines: string[]; text: string };\n\n/**\n * Grade one record's pins for one service against that service's working tree.\n *\n * Only agent-confirmed claims of `service` are graded: a runner's or contract\n * runner's evidence names a report entry, not a file, so there is nothing to\n * re-check, and an unconfirmed claim pinned nothing. Absent `answered_by`\n * counts as agent — the `attestedClaims` rule.\n *\n * Containment is per citation: one unreadable cited file degrades exactly the\n * pins that cite it (graded `unresolved`), never the sibling pins and never\n * the whole record. And \"could not look\" is never \"looks fine\": every failure\n * to read is a drift entry, not a skip — fail closed at the validator.\n */\nexport async function lintEvidencePins(\n claims: readonly PinnedClaim[],\n service: string,\n repoDir: string,\n): Promise<PinLint> {\n // The closed side of the answerer test, on purpose: `answered_by` is an open\n // string here (a record is hand-editable YAML, and a newer loam may add\n // answerers), so excluding the known mechanical values would sweep every\n // FUTURE mechanical answerer's report-entry citations into the `unpinned`\n // tally with false \"recorded before pins existed\" advice. Only what is\n // positively an agent's word is graded; absent counts as agent — the\n // `attestedClaims` rule for records that predate `--results`.\n const mine = claims.filter(\n (c) =>\n c.subject === service &&\n c.verdict === \"confirmed\" &&\n (c.answered_by === undefined || c.answered_by === \"agent\"),\n );\n\n // Join pins to evidence entries by the `path:line` key, never by position:\n // a record is hand-editable YAML, and a positional join would let a deleted\n // evidence line silently shift every later pin onto the wrong citation.\n const joined: Array<{ claim: string; evidence: string; pin: EvidencePin }> = [];\n let unpinned = 0;\n for (const claim of mine) {\n const pins = new Map((claim.evidence_pins ?? []).map((pin) => [`${pin.path}:${pin.line}`, pin]));\n // A pin no evidence entry names pins nothing this claim cites (a hand\n // edit); grading it would report drift about a citation the claim does not\n // make, so only the joined pairs are graded. The reverse direction — a\n // citation without a pin — is the `unpinned` tally.\n for (const evidence of claim.evidence) {\n const pin = pins.get(evidence);\n if (pin === undefined) unpinned += 1;\n else joined.push({ claim: claim.id, evidence, pin });\n }\n }\n\n // Each distinct cited file is read and hashed once per call — a local map,\n // per invocation, because module-level state leaks across tests and hosts.\n // The reads are independent, so they go through the bounded pool; grading\n // stays sequential below so `drifts` keeps record order.\n const paths = [...new Set(joined.map((j) => j.pin.path))];\n const states = new Map(\n await inOrder(paths, async (path): Promise<[string, FileState]> => [path, await readCited(repoDir, path)]),\n );\n\n let checked = 0;\n const drifts: PinDrift[] = [];\n for (const { claim, evidence, pin } of joined) {\n const file = states.get(pin.path);\n if (file === undefined || file.state === \"unreadable\") {\n drifts.push({ claim, evidence, grade: \"unresolved\", what: file?.what ?? \"the cited file was not read\" });\n continue;\n }\n const cited = file.lines[pin.line - 1];\n if (cited === undefined) {\n drifts.push({\n claim,\n evidence,\n grade: \"unresolved\",\n // `continue`, like the unreadable-file arm: an unresolvable CITATION\n // is one finding, and a token scan on top would report the same\n // damaged citation twice under two codes.\n what: `the cited line is past the end — the file now has ${file.lines.length} line(s)`,\n });\n continue;\n }\n if (file.digest === pin.file_sha256) {\n checked += 1;\n } else if (pinnedText(cited) === pin.text) {\n // The mildest drift: the file changed around the cited line, which\n // still reads as pinned — \"reads as\", not \"is\": both sides of the\n // comparison are capped at PIN_TEXT_CAP, so a change past the cap on a\n // pathological line is invisible to this grade (and visible to the\n // digest, which is why this is a drift and not a clean pin).\n drifts.push({\n claim,\n evidence,\n grade: \"moved\",\n what: \"the file changed around the cited line, which still reads as pinned\",\n });\n } else {\n drifts.push({\n claim,\n evidence,\n grade: \"line-changed\",\n what: `the cited line now reads '${pinnedText(cited)}' where the record pinned '${pin.text}'`,\n });\n }\n // Independent of the grades above, clean pins included — a digest match\n // must not read as \"the token is still there\", because content identical\n // to record time says nothing once the file drifts back. \"No longer\" is\n // the honest word here: the write side stamps `token` only when the\n // attested blob contained it (EvidencePin.token), so this grade always\n // convicts a disappearance, never re-litigates an absence the record-time\n // notice already reported.\n if (pin.token !== undefined && !file.text.includes(pin.token)) {\n drifts.push({\n claim,\n evidence,\n grade: \"token-missing\",\n what: `the cited file no longer contains '${pin.token}' — the literal string the claim asserts`,\n });\n }\n }\n return { checked, drifts, unpinned };\n}\n\n/** One cited file against the working tree, every failure named rather than thrown. */\nasync function readCited(repoDir: string, path: string): Promise<FileState> {\n let absolute: string;\n try {\n absolute = resolvePortableFileInside(repoDir, path, \"pinned evidence\");\n } catch (err) {\n if (!(err instanceof UnsafePathError)) throw err;\n return { state: \"unreadable\", what: `the pinned path is unsafe: ${err.message}` };\n }\n try {\n const info = await stat(absolute);\n if (!info.isFile()) return { state: \"unreadable\", what: \"the cited path is not a regular file\" };\n const text = await readFile(absolute, \"utf8\");\n return { state: \"ok\", digest: pinnedDigest(text), lines: sourceLines(text), text };\n } catch (err) {\n // ENOENT is the common drift — the cited file is gone from the tree; any\n // other errno (EACCES, EISDIR racing the stat) is named as itself, because\n // \"could not read\" and \"does not exist\" call for different repairs.\n if ((err as NodeJS.ErrnoException).code === \"ENOENT\") {\n return { state: \"unreadable\", what: \"the cited file does not exist in this working tree\" };\n }\n return { state: \"unreadable\", what: err instanceof Error ? err.message : String(err) };\n }\n}\n"]}
|
|
1
|
+
{"version":3,"file":"lint.js","sourceRoot":"","sources":["../../../../src/core/verify/pins/lint.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * Re-checking evidence pins against a working tree: pure data out, one grade\n * per pin, and no verdict anywhere in the result on purpose — the lint demotes\n * reviewer confidence, it never promotes or demotes a claim. Prints nothing;\n * the validate command turns the grades into findings.\n *\n * The claim shape is structural rather than an import of `../record.js`'s\n * `RecordedClaim`, and not only for the smallest-parameter rule: this package\n * is imported BY `core/verify` and its `read/` sibling (`../read/shape.ts` asks\n * `./pin.ts` for the read guard, `../answers.ts` for the shape), so importing\n * the parent back would\n * put a cycle in the package graph that `import/no-cycle` cannot see.\n */\nimport { readFile, stat } from \"node:fs/promises\";\nimport { inOrder } from \"../../kernel/concurrency.js\";\nimport { resolvePortableFileInside, UnsafePathError } from \"../../kernel/path-safety.js\";\nimport { pinnedDigest, pinnedText, sourceLines, type EvidencePin } from \"./pin.js\";\n\n/** What the lint reads off a recorded claim — structurally, see the header. */\nexport interface PinnedClaim {\n id: string;\n /** Present in federated (schema 2) records — the service whose code answers it. */\n subject?: string;\n verdict: string;\n /** Absent counts as agent — the `attestedClaims` rule for records that predate `--results`. */\n answered_by?: string;\n evidence: string[];\n evidence_pins?: EvidencePin[];\n}\n\n/** One pin's disagreement with the working tree. `grade` is what a finding code is made from. */\nexport interface PinDrift {\n claim: string;\n /** The `path:line` citation the pin belongs to. */\n evidence: string;\n grade: \"unresolved\" | \"moved\" | \"line-changed\" | \"token-missing\";\n /** What changed, in one clause — rendered into a finding's detail line. */\n what: string;\n}\n\nexport interface PinLint {\n /** Pins that resolved clean: file digest unchanged against the working tree. */\n checked: number;\n /** In record order — claim by claim, citation by citation — so two runs diff. */\n drifts: PinDrift[];\n /** Agent-confirmed citations carrying no pin, plus claims with no pins at all. */\n unpinned: number;\n}\n\n/** What one cited file turned out to be, read and hashed once per lint call. */\ntype FileState =\n | { state: \"unreadable\"; what: string }\n | { state: \"ok\"; digest: string; lines: string[]; text: string };\n\n/**\n * Grade one record's pins for one service against that service's working tree.\n *\n * Only agent-confirmed claims of `service` are graded: a runner's or contract\n * runner's evidence names a report entry, not a file, so there is nothing to\n * re-check, and an unconfirmed claim pinned nothing. Absent `answered_by`\n * counts as agent — the `attestedClaims` rule.\n *\n * Containment is per citation: one unreadable cited file degrades exactly the\n * pins that cite it (graded `unresolved`), never the sibling pins and never\n * the whole record. And \"could not look\" is never \"looks fine\": every failure\n * to read is a drift entry, not a skip — fail closed at the validator.\n */\nexport async function lintEvidencePins(\n claims: readonly PinnedClaim[],\n service: string,\n repoDir: string,\n): Promise<PinLint> {\n // The closed side of the answerer test, on purpose: `answered_by` is an open\n // string here (a record is hand-editable YAML, and a newer loam may add\n // answerers), so excluding the known mechanical values would sweep every\n // FUTURE mechanical answerer's report-entry citations into the `unpinned`\n // tally with false \"recorded before pins existed\" advice. Only what is\n // positively an agent's word is graded; absent counts as agent — the\n // `attestedClaims` rule for records that predate `--results`.\n const mine = claims.filter(\n (c) =>\n c.subject === service &&\n c.verdict === \"confirmed\" &&\n (c.answered_by === undefined || c.answered_by === \"agent\"),\n );\n\n // Join pins to evidence entries by the `path:line` key, never by position:\n // a record is hand-editable YAML, and a positional join would let a deleted\n // evidence line silently shift every later pin onto the wrong citation.\n const joined: Array<{ claim: string; evidence: string; pin: EvidencePin }> = [];\n let unpinned = 0;\n for (const claim of mine) {\n const pins = new Map((claim.evidence_pins ?? []).map((pin) => [`${pin.path}:${pin.line}`, pin]));\n // A pin no evidence entry names pins nothing this claim cites (a hand\n // edit); grading it would report drift about a citation the claim does not\n // make, so only the joined pairs are graded. The reverse direction — a\n // citation without a pin — is the `unpinned` tally.\n for (const evidence of claim.evidence) {\n const pin = pins.get(evidence);\n if (pin === undefined) unpinned += 1;\n else joined.push({ claim: claim.id, evidence, pin });\n }\n }\n\n // Each distinct cited file is read and hashed once per call — a local map,\n // per invocation, because module-level state leaks across tests and hosts.\n // The reads are independent, so they go through the bounded pool; grading\n // stays sequential below so `drifts` keeps record order.\n const paths = [...new Set(joined.map((j) => j.pin.path))];\n const states = new Map(\n await inOrder(paths, async (path): Promise<[string, FileState]> => [path, await readCited(repoDir, path)]),\n );\n\n let checked = 0;\n const drifts: PinDrift[] = [];\n for (const { claim, evidence, pin } of joined) {\n const file = states.get(pin.path);\n if (file === undefined || file.state === \"unreadable\") {\n drifts.push({ claim, evidence, grade: \"unresolved\", what: file?.what ?? \"the cited file was not read\" });\n continue;\n }\n const cited = file.lines[pin.line - 1];\n if (cited === undefined) {\n drifts.push({\n claim,\n evidence,\n grade: \"unresolved\",\n // `continue`, like the unreadable-file arm: an unresolvable CITATION\n // is one finding, and a token scan on top would report the same\n // damaged citation twice under two codes.\n what: `the cited line is past the end — the file now has ${file.lines.length} line(s)`,\n });\n continue;\n }\n if (file.digest === pin.file_sha256) {\n checked += 1;\n } else if (pinnedText(cited) === pin.text) {\n // The mildest drift: the file changed around the cited line, which\n // still reads as pinned — \"reads as\", not \"is\": both sides of the\n // comparison are capped at PIN_TEXT_CAP, so a change past the cap on a\n // pathological line is invisible to this grade (and visible to the\n // digest, which is why this is a drift and not a clean pin).\n drifts.push({\n claim,\n evidence,\n grade: \"moved\",\n what: \"the file changed around the cited line, which still reads as pinned\",\n });\n } else {\n drifts.push({\n claim,\n evidence,\n grade: \"line-changed\",\n what: `the cited line now reads '${pinnedText(cited)}' where the record pinned '${pin.text}'`,\n });\n }\n // Independent of the grades above, clean pins included — a digest match\n // must not read as \"the token is still there\", because content identical\n // to record time says nothing once the file drifts back. \"No longer\" is\n // the honest word here: the write side stamps `token` only when the\n // attested blob contained it (EvidencePin.token), so this grade always\n // convicts a disappearance, never re-litigates an absence the record-time\n // notice already reported.\n if (pin.token !== undefined && !file.text.includes(pin.token)) {\n drifts.push({\n claim,\n evidence,\n grade: \"token-missing\",\n what: `the cited file no longer contains '${pin.token}' — the literal string the claim asserts`,\n });\n }\n }\n return { checked, drifts, unpinned };\n}\n\n/** One cited file against the working tree, every failure named rather than thrown. */\nasync function readCited(repoDir: string, path: string): Promise<FileState> {\n let absolute: string;\n try {\n absolute = resolvePortableFileInside(repoDir, path, \"pinned evidence\");\n } catch (err) {\n if (!(err instanceof UnsafePathError)) throw err;\n return { state: \"unreadable\", what: `the pinned path is unsafe: ${err.message}` };\n }\n try {\n const info = await stat(absolute);\n if (!info.isFile()) return { state: \"unreadable\", what: \"the cited path is not a regular file\" };\n const text = await readFile(absolute, \"utf8\");\n return { state: \"ok\", digest: pinnedDigest(text), lines: sourceLines(text), text };\n } catch (err) {\n // ENOENT is the common drift — the cited file is gone from the tree; any\n // other errno (EACCES, EISDIR racing the stat) is named as itself, because\n // \"could not read\" and \"does not exist\" call for different repairs.\n if ((err as NodeJS.ErrnoException).code === \"ENOENT\") {\n return { state: \"unreadable\", what: \"the cited file does not exist in this working tree\" };\n }\n return { state: \"unreadable\", what: err instanceof Error ? err.message : String(err) };\n }\n}\n"]}
|
|
@@ -62,7 +62,7 @@ export function pinnedDigest(source) {
|
|
|
62
62
|
return createHash("sha256").update(source.replace(/\r\n|\r/g, "\n")).digest("hex");
|
|
63
63
|
}
|
|
64
64
|
/**
|
|
65
|
-
* The read-shape guard `asVerification` (../
|
|
65
|
+
* The read-shape guard `asVerification` (../read/shape.ts) asks per claim, on the
|
|
66
66
|
* `isConsumedContractReport` model to the letter: absent is fine — every
|
|
67
67
|
* record written before pins existed, and every runner-answered claim, has
|
|
68
68
|
* none — but present must be whole for the four fields the lint dereferences.
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"pin.js","sourceRoot":"","sources":["../../../../src/core/verify/pins/pin.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;GAYG;AACH,OAAO,EAAE,UAAU,EAAE,MAAM,aAAa,CAAC;AACzC,OAAO,EAAE,QAAQ,EAAE,MAAM,yBAAyB,CAAC;AA4BnD;;;;;GAKG;AACH,MAAM,CAAC,MAAM,YAAY,GAAG,GAAG,CAAC;AAEhC,kGAAkG;AAClG,MAAM,UAAU,UAAU,CAAC,IAAY;IACrC,MAAM,OAAO,GAAG,IAAI,CAAC,IAAI,EAAE,CAAC;IAC5B,OAAO,OAAO,CAAC,MAAM,GAAG,YAAY,CAAC,CAAC,CAAC,OAAO,CAAC,KAAK,CAAC,CAAC,EAAE,YAAY,CAAC,CAAC,CAAC,CAAC,OAAO,CAAC;AAClF,CAAC;AAED;;;;;;GAMG;AACH,MAAM,UAAU,WAAW,CAAC,MAAc;IACxC,OAAO,MAAM,CAAC,KAAK,CAAC,YAAY,CAAC,CAAC;AACpC,CAAC;AAED;;;;GAIG;AACH,MAAM,UAAU,SAAS,CAAC,MAAc,EAAE,IAAY;IACpD,MAAM,GAAG,GAAG,WAAW,CAAC,MAAM,CAAC,CAAC,IAAI,GAAG,CAAC,CAAC,CAAC;IAC1C,OAAO,GAAG,KAAK,SAAS,CAAC,CAAC,CAAC,SAAS,CAAC,CAAC,CAAC,UAAU,CAAC,GAAG,CAAC,CAAC;AACzD,CAAC;AAED;;;;;;;;;;;;;GAaG;AACH,MAAM,UAAU,YAAY,CAAC,MAAc;IACzC,OAAO,UAAU,CAAC,QAAQ,CAAC,CAAC,MAAM,CAAC,MAAM,CAAC,OAAO,CAAC,UAAU,EAAE,IAAI,CAAC,CAAC,CAAC,MAAM,CAAC,KAAK,CAAC,CAAC;AACrF,CAAC;AAED;;;;;;;;;;;GAWG;AACH,MAAM,UAAU,iBAAiB,CAAC,CAAU;IAC1C,IAAI,CAAC,KAAK,SAAS;QAAE,OAAO,IAAI,CAAC;IACjC,IAAI,CAAC,KAAK,CAAC,OAAO,CAAC,CAAC,CAAC;QAAE,OAAO,KAAK,CAAC;IACpC,KAAK,MAAM,GAAG,IAAI,CAAC,EAAE,CAAC;QACpB,IAAI,CAAC,QAAQ,CAAC,GAAG,CAAC;YAAE,OAAO,KAAK,CAAC;QACjC,IAAI,OAAO,GAAG,CAAC,MAAM,CAAC,KAAK,QAAQ;YAAE,OAAO,KAAK,CAAC;QAClD,IAAI,OAAO,GAAG,CAAC,MAAM,CAAC,KAAK,QAAQ,IAAI,CAAC,MAAM,CAAC,SAAS,CAAC,GAAG,CAAC,MAAM,CAAC,CAAC,IAAI,GAAG,CAAC,MAAM,CAAC,GAAG,CAAC;YAAE,OAAO,KAAK,CAAC;QACvG,IAAI,OAAO,GAAG,CAAC,aAAa,CAAC,KAAK,QAAQ,IAAI,OAAO,GAAG,CAAC,MAAM,CAAC,KAAK,QAAQ;YAAE,OAAO,KAAK,CAAC;QAC5F,IAAI,GAAG,CAAC,OAAO,CAAC,KAAK,SAAS,IAAI,OAAO,GAAG,CAAC,OAAO,CAAC,KAAK,QAAQ;YAAE,OAAO,KAAK,CAAC;IACnF,CAAC;IACD,OAAO,IAAI,CAAC;AACd,CAAC","sourcesContent":["/**\n * The evidence pin's identity: what `verify --service --record` stamps beside a\n * confirmed agent answer so a later `loam validate`, standing in the same\n * service repository, can re-check the citation against the working tree.\n *\n * Be precise about what a pin proves, because the record travels without loam:\n * a pin identifies WHAT was cited at the attested commit — the cited file's\n * normalized sha256, the cited line's text, the literal token the claim\n * asserts. It does not prove the claim true; an agent that cited the wrong\n * line gets a faithful pin of the wrong line. That is why the read side\n * (`./lint.ts`) only ever demotes reviewer confidence: no pin state moves a\n * verdict in either direction.\n */\nimport { createHash } from \"node:crypto\";\nimport { isRecord } from \"../../kernel/records.js\";\n\nexport interface EvidencePin {\n /** The cited file, repo-relative in the portable spelling evidence entries use. */\n path: string;\n /** The cited line, 1-based — the `:N` of the evidence entry this pins. */\n line: number;\n /** sha256 hex of the whole file at the attested commit, normalized — see {@link pinnedDigest}. */\n file_sha256: string;\n /** The cited line at the attested commit, trimmed and capped — see {@link pinnedText}. */\n text: string;\n /**\n * The literal string the claim asserts of the cited artifact — the\n * operationId for `api.exposes`, the message name for `event.declares`, the\n * edge's op for `c4.calls` (`Claim.token`, stamped at record time). Carried\n * on the pin so the read side scans the file for it and never has to\n * re-derive a checklist or parse claim prose. Absent for the kinds that\n * assert no token (`service.exists`, `scenario.tested`) — and absent when\n * the attested blob did not CONTAIN the token: that fact is warned once, at\n * record time, where the answerer can still act on it; a pin carrying it\n * would make `loam validate` repeat \"no longer contains\" forever about a\n * file that never did, with a re-record that re-derives the same state as\n * the only offered repair. `token-missing` therefore always means \"was\n * there at the attested commit and is gone\".\n */\n token?: string;\n}\n\n/**\n * How much of the cited line a pin carries. Enough to recognize a line of\n * source by eye and by equality; a cap because evidence may cite a line of a\n * generated client or a minified bundle, and the record is a YAML file people\n * read and diff — one pathological line must not turn it into one.\n */\nexport const PIN_TEXT_CAP = 200;\n\n/** The pin's line recipe: trimmed (indentation is formatting), capped at {@link PIN_TEXT_CAP}. */\nexport function pinnedText(line: string): string {\n const trimmed = line.trim();\n return trimmed.length > PIN_TEXT_CAP ? trimmed.slice(0, PIN_TEXT_CAP) : trimmed;\n}\n\n/**\n * The one spelling of where lines fall. Both sides of the pin — the write\n * side's bounds checks and the read side's re-grade — must number lines\n * identically, or a pin recorded at line 12 gets graded against a different\n * line 12; four call sites each spelling the regex themselves is how that\n * divergence would arrive, so the recipe has exactly one home.\n */\nexport function sourceLines(source: string): string[] {\n return source.split(/\\r\\n|\\n|\\r/);\n}\n\n/**\n * The cited line of a source text, through the pin recipe — or undefined when\n * the line is past the end, which the caller grades (a refusal on the write\n * side, `unresolved` on the read side).\n */\nexport function citedLine(source: string, line: number): string | undefined {\n const raw = sourceLines(source)[line - 1];\n return raw === undefined ? undefined : pinnedText(raw);\n}\n\n/**\n * The pin's file digest: sha256 hex over the text with CRLF/CR normalized to\n * LF.\n *\n * Normalized because git's eol config makes blob bytes and working-tree bytes\n * legally differ: on a Windows checkout with `core.autocrlf=true`, the blob\n * the attested commit holds is LF while every file on disk is CRLF, so an\n * unnormalized digest would grade every pin `evidence.moved` forever on the\n * checkout config alone. The pin must convict content change, never checkout\n * config. The recipe is over TEXT by construction — the write side digests\n * `git show` output already decoded as utf8 — and the symmetric cost is\n * accepted and documented in SCHEMA.md: a pure line-ending change is invisible\n * to the lint.\n */\nexport function pinnedDigest(source: string): string {\n return createHash(\"sha256\").update(source.replace(/\\r\\n|\\r/g, \"\\n\")).digest(\"hex\");\n}\n\n/**\n * The read-shape guard `asVerification` (../
|
|
1
|
+
{"version":3,"file":"pin.js","sourceRoot":"","sources":["../../../../src/core/verify/pins/pin.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;GAYG;AACH,OAAO,EAAE,UAAU,EAAE,MAAM,aAAa,CAAC;AACzC,OAAO,EAAE,QAAQ,EAAE,MAAM,yBAAyB,CAAC;AA4BnD;;;;;GAKG;AACH,MAAM,CAAC,MAAM,YAAY,GAAG,GAAG,CAAC;AAEhC,kGAAkG;AAClG,MAAM,UAAU,UAAU,CAAC,IAAY;IACrC,MAAM,OAAO,GAAG,IAAI,CAAC,IAAI,EAAE,CAAC;IAC5B,OAAO,OAAO,CAAC,MAAM,GAAG,YAAY,CAAC,CAAC,CAAC,OAAO,CAAC,KAAK,CAAC,CAAC,EAAE,YAAY,CAAC,CAAC,CAAC,CAAC,OAAO,CAAC;AAClF,CAAC;AAED;;;;;;GAMG;AACH,MAAM,UAAU,WAAW,CAAC,MAAc;IACxC,OAAO,MAAM,CAAC,KAAK,CAAC,YAAY,CAAC,CAAC;AACpC,CAAC;AAED;;;;GAIG;AACH,MAAM,UAAU,SAAS,CAAC,MAAc,EAAE,IAAY;IACpD,MAAM,GAAG,GAAG,WAAW,CAAC,MAAM,CAAC,CAAC,IAAI,GAAG,CAAC,CAAC,CAAC;IAC1C,OAAO,GAAG,KAAK,SAAS,CAAC,CAAC,CAAC,SAAS,CAAC,CAAC,CAAC,UAAU,CAAC,GAAG,CAAC,CAAC;AACzD,CAAC;AAED;;;;;;;;;;;;;GAaG;AACH,MAAM,UAAU,YAAY,CAAC,MAAc;IACzC,OAAO,UAAU,CAAC,QAAQ,CAAC,CAAC,MAAM,CAAC,MAAM,CAAC,OAAO,CAAC,UAAU,EAAE,IAAI,CAAC,CAAC,CAAC,MAAM,CAAC,KAAK,CAAC,CAAC;AACrF,CAAC;AAED;;;;;;;;;;;GAWG;AACH,MAAM,UAAU,iBAAiB,CAAC,CAAU;IAC1C,IAAI,CAAC,KAAK,SAAS;QAAE,OAAO,IAAI,CAAC;IACjC,IAAI,CAAC,KAAK,CAAC,OAAO,CAAC,CAAC,CAAC;QAAE,OAAO,KAAK,CAAC;IACpC,KAAK,MAAM,GAAG,IAAI,CAAC,EAAE,CAAC;QACpB,IAAI,CAAC,QAAQ,CAAC,GAAG,CAAC;YAAE,OAAO,KAAK,CAAC;QACjC,IAAI,OAAO,GAAG,CAAC,MAAM,CAAC,KAAK,QAAQ;YAAE,OAAO,KAAK,CAAC;QAClD,IAAI,OAAO,GAAG,CAAC,MAAM,CAAC,KAAK,QAAQ,IAAI,CAAC,MAAM,CAAC,SAAS,CAAC,GAAG,CAAC,MAAM,CAAC,CAAC,IAAI,GAAG,CAAC,MAAM,CAAC,GAAG,CAAC;YAAE,OAAO,KAAK,CAAC;QACvG,IAAI,OAAO,GAAG,CAAC,aAAa,CAAC,KAAK,QAAQ,IAAI,OAAO,GAAG,CAAC,MAAM,CAAC,KAAK,QAAQ;YAAE,OAAO,KAAK,CAAC;QAC5F,IAAI,GAAG,CAAC,OAAO,CAAC,KAAK,SAAS,IAAI,OAAO,GAAG,CAAC,OAAO,CAAC,KAAK,QAAQ;YAAE,OAAO,KAAK,CAAC;IACnF,CAAC;IACD,OAAO,IAAI,CAAC;AACd,CAAC","sourcesContent":["/**\n * The evidence pin's identity: what `verify --service --record` stamps beside a\n * confirmed agent answer so a later `loam validate`, standing in the same\n * service repository, can re-check the citation against the working tree.\n *\n * Be precise about what a pin proves, because the record travels without loam:\n * a pin identifies WHAT was cited at the attested commit — the cited file's\n * normalized sha256, the cited line's text, the literal token the claim\n * asserts. It does not prove the claim true; an agent that cited the wrong\n * line gets a faithful pin of the wrong line. That is why the read side\n * (`./lint.ts`) only ever demotes reviewer confidence: no pin state moves a\n * verdict in either direction.\n */\nimport { createHash } from \"node:crypto\";\nimport { isRecord } from \"../../kernel/records.js\";\n\nexport interface EvidencePin {\n /** The cited file, repo-relative in the portable spelling evidence entries use. */\n path: string;\n /** The cited line, 1-based — the `:N` of the evidence entry this pins. */\n line: number;\n /** sha256 hex of the whole file at the attested commit, normalized — see {@link pinnedDigest}. */\n file_sha256: string;\n /** The cited line at the attested commit, trimmed and capped — see {@link pinnedText}. */\n text: string;\n /**\n * The literal string the claim asserts of the cited artifact — the\n * operationId for `api.exposes`, the message name for `event.declares`, the\n * edge's op for `c4.calls` (`Claim.token`, stamped at record time). Carried\n * on the pin so the read side scans the file for it and never has to\n * re-derive a checklist or parse claim prose. Absent for the kinds that\n * assert no token (`service.exists`, `scenario.tested`) — and absent when\n * the attested blob did not CONTAIN the token: that fact is warned once, at\n * record time, where the answerer can still act on it; a pin carrying it\n * would make `loam validate` repeat \"no longer contains\" forever about a\n * file that never did, with a re-record that re-derives the same state as\n * the only offered repair. `token-missing` therefore always means \"was\n * there at the attested commit and is gone\".\n */\n token?: string;\n}\n\n/**\n * How much of the cited line a pin carries. Enough to recognize a line of\n * source by eye and by equality; a cap because evidence may cite a line of a\n * generated client or a minified bundle, and the record is a YAML file people\n * read and diff — one pathological line must not turn it into one.\n */\nexport const PIN_TEXT_CAP = 200;\n\n/** The pin's line recipe: trimmed (indentation is formatting), capped at {@link PIN_TEXT_CAP}. */\nexport function pinnedText(line: string): string {\n const trimmed = line.trim();\n return trimmed.length > PIN_TEXT_CAP ? trimmed.slice(0, PIN_TEXT_CAP) : trimmed;\n}\n\n/**\n * The one spelling of where lines fall. Both sides of the pin — the write\n * side's bounds checks and the read side's re-grade — must number lines\n * identically, or a pin recorded at line 12 gets graded against a different\n * line 12; four call sites each spelling the regex themselves is how that\n * divergence would arrive, so the recipe has exactly one home.\n */\nexport function sourceLines(source: string): string[] {\n return source.split(/\\r\\n|\\n|\\r/);\n}\n\n/**\n * The cited line of a source text, through the pin recipe — or undefined when\n * the line is past the end, which the caller grades (a refusal on the write\n * side, `unresolved` on the read side).\n */\nexport function citedLine(source: string, line: number): string | undefined {\n const raw = sourceLines(source)[line - 1];\n return raw === undefined ? undefined : pinnedText(raw);\n}\n\n/**\n * The pin's file digest: sha256 hex over the text with CRLF/CR normalized to\n * LF.\n *\n * Normalized because git's eol config makes blob bytes and working-tree bytes\n * legally differ: on a Windows checkout with `core.autocrlf=true`, the blob\n * the attested commit holds is LF while every file on disk is CRLF, so an\n * unnormalized digest would grade every pin `evidence.moved` forever on the\n * checkout config alone. The pin must convict content change, never checkout\n * config. The recipe is over TEXT by construction — the write side digests\n * `git show` output already decoded as utf8 — and the symmetric cost is\n * accepted and documented in SCHEMA.md: a pure line-ending change is invisible\n * to the lint.\n */\nexport function pinnedDigest(source: string): string {\n return createHash(\"sha256\").update(source.replace(/\\r\\n|\\r/g, \"\\n\")).digest(\"hex\");\n}\n\n/**\n * The read-shape guard `asVerification` (../read/shape.ts) asks per claim, on the\n * `isConsumedContractReport` model to the letter: absent is fine — every\n * record written before pins existed, and every runner-answered claim, has\n * none — but present must be whole for the four fields the lint dereferences.\n * A broken pins block makes the WHOLE record unreadable and therefore never\n * overwritten, exactly the contract-report rule: it is somebody's record, and\n * a lint that silently skipped a malformed pin would read a hand-damaged\n * record as drift-free. `token` stays optional-and-any-string for `format`'s\n * reason — a field a newer loam learned to stamp must not brick the record\n * here.\n */\nexport function isEvidencePinList(v: unknown): boolean {\n if (v === undefined) return true;\n if (!Array.isArray(v)) return false;\n for (const pin of v) {\n if (!isRecord(pin)) return false;\n if (typeof pin[\"path\"] !== \"string\") return false;\n if (typeof pin[\"line\"] !== \"number\" || !Number.isInteger(pin[\"line\"]) || pin[\"line\"] < 1) return false;\n if (typeof pin[\"file_sha256\"] !== \"string\" || typeof pin[\"text\"] !== \"string\") return false;\n if (pin[\"token\"] !== undefined && typeof pin[\"token\"] !== \"string\") return false;\n }\n return true;\n}\n"]}
|
|
@@ -0,0 +1,224 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The verification record read back, and refused when it cannot be trusted.
|
|
3
|
+
*
|
|
4
|
+
* The record is several files now: the one-file `verification.yaml` (the
|
|
5
|
+
* all-at-once form, and every federated record written before the directory
|
|
6
|
+
* existed) and one `verification/<svc>.yaml` per attesting service. Each file
|
|
7
|
+
* is graded on its own here, and `./union.ts` adds the graded files up
|
|
8
|
+
* into the one `Verification` every reader takes — so no reader learns the
|
|
9
|
+
* layout, and a record that is half one form and half the other reads as one
|
|
10
|
+
* record.
|
|
11
|
+
*
|
|
12
|
+
* Reading is deliberately not the inverse of rendering. A file loam wrote is
|
|
13
|
+
* re-graded on the way in — git conflict markers, unreadable YAML, a shape that
|
|
14
|
+
* is not a verification, a `summary` that disagrees with the claims below it —
|
|
15
|
+
* because the record is data meant to survive without loam, and anything may
|
|
16
|
+
* have edited it since. The render lives with the writer it feeds
|
|
17
|
+
* (`../store/render.ts` produces exactly the bytes `../store/commit.ts` swaps into
|
|
18
|
+
* place; races and kills mid-write live there too), and the lock that
|
|
19
|
+
* serialises writers is taken by `commands/verify/verify.ts` before this
|
|
20
|
+
* module's authoritative read. Nothing here touches the filesystem except to
|
|
21
|
+
* read.
|
|
22
|
+
*
|
|
23
|
+
* A package of its own (`core/verify/read/`, moved from `core/verify/file.ts`)
|
|
24
|
+
* because the reader now leans on two modules that lean back on
|
|
25
|
+
* `../record.ts`: kept in `core/verify` beside them it would have been a
|
|
26
|
+
* package cycle that `import/no-cycle` cannot see.
|
|
27
|
+
*/
|
|
28
|
+
import { lstat, readdir, readFile } from "node:fs/promises";
|
|
29
|
+
import { join } from "node:path";
|
|
30
|
+
import { parse } from "yaml";
|
|
31
|
+
import { conflictMarkerLines } from "../../conflict-markers.js";
|
|
32
|
+
import { inOrder } from "../../kernel/concurrency.js";
|
|
33
|
+
import { verificationDir, verificationPath } from "../location.js";
|
|
34
|
+
import {} from "../record.js";
|
|
35
|
+
import { asVerification, summaryDisagreement } from "./shape.js";
|
|
36
|
+
import { assembleRecord } from "./union.js";
|
|
37
|
+
/**
|
|
38
|
+
* `live` is the digest of the checklist the caller knows is being asked — see
|
|
39
|
+
* `assembleRecord` for why the union needs it. A caller that only learns it
|
|
40
|
+
* after the read (it derives the checklist only when there IS a record) adds
|
|
41
|
+
* the same files up again with {@link answering} instead of reading twice.
|
|
42
|
+
*/
|
|
43
|
+
export async function readVerificationState(featureDir, live) {
|
|
44
|
+
const legacy = await readRecordFile(verificationPath(featureDir), null);
|
|
45
|
+
if (legacy.state === "unreadable")
|
|
46
|
+
return legacy;
|
|
47
|
+
const listed = await serviceFiles(featureDir);
|
|
48
|
+
if (!Array.isArray(listed))
|
|
49
|
+
return listed;
|
|
50
|
+
// In parallel — the files are independent, and a fleet-sized feature has
|
|
51
|
+
// one per service — but graded in the listing's sorted order, so the file a
|
|
52
|
+
// refusal names is the first unreadable one by name on every machine, never
|
|
53
|
+
// whichever read happened to fail first.
|
|
54
|
+
const reads = await inOrder(listed, ({ service, path }) => readRecordFile(path, service));
|
|
55
|
+
const services = new Map();
|
|
56
|
+
for (const [i, read] of reads.entries()) {
|
|
57
|
+
if (read.state === "unreadable")
|
|
58
|
+
return read;
|
|
59
|
+
// Listed and then gone is a file a concurrent writer removed: absent.
|
|
60
|
+
if (read.state === "ok")
|
|
61
|
+
services.set(listed[i].service, read.file);
|
|
62
|
+
}
|
|
63
|
+
const files = { legacy: legacy.state === "ok" ? legacy.file : null, services };
|
|
64
|
+
const verification = answering(files, live);
|
|
65
|
+
if (verification === null)
|
|
66
|
+
return { state: "absent" };
|
|
67
|
+
return { state: "ok", verification, files };
|
|
68
|
+
}
|
|
69
|
+
/**
|
|
70
|
+
* The record's files added up for the checklist `live` asks — what
|
|
71
|
+
* {@link readVerificationState} answers, and what it would have answered for a
|
|
72
|
+
* caller that only learned `live` after the read. Pure: the files are the ones
|
|
73
|
+
* that read already graded. Null only when there are no files at all.
|
|
74
|
+
*/
|
|
75
|
+
export function answering(files, live) {
|
|
76
|
+
return assembleRecord(files.legacy?.verification ?? null, [...files.services.values()].map((file) => file.verification), live);
|
|
77
|
+
}
|
|
78
|
+
/**
|
|
79
|
+
* The per-service files, sorted by service id so the union is the same on
|
|
80
|
+
* every machine. Dotfiles are skipped — staging parks its temp files beside
|
|
81
|
+
* the target as `.<name>.loam-….tmp`, and a reader racing a writer must not
|
|
82
|
+
* grade one — and so is anything that is not `*.yaml`: the directory is the
|
|
83
|
+
* list of attestations, and a README beside them is not one.
|
|
84
|
+
*/
|
|
85
|
+
async function serviceFiles(featureDir) {
|
|
86
|
+
const dir = verificationDir(featureDir);
|
|
87
|
+
let entries;
|
|
88
|
+
try {
|
|
89
|
+
entries = await readdir(dir, { withFileTypes: true });
|
|
90
|
+
}
|
|
91
|
+
catch (err) {
|
|
92
|
+
if (err.code === "ENOENT")
|
|
93
|
+
return [];
|
|
94
|
+
return {
|
|
95
|
+
state: "unreadable",
|
|
96
|
+
path: dir,
|
|
97
|
+
reason: err instanceof Error ? err.message : String(err),
|
|
98
|
+
advice: "It has to be a directory holding one <svc>.yaml per attesting service; move whatever is there aside, then record again.",
|
|
99
|
+
};
|
|
100
|
+
}
|
|
101
|
+
return entries
|
|
102
|
+
.filter((e) => !e.name.startsWith(".") && e.name.endsWith(".yaml") && !e.isDirectory())
|
|
103
|
+
.map((e) => ({ service: e.name.slice(0, -".yaml".length), path: join(dir, e.name) }))
|
|
104
|
+
.sort((a, b) => (a.service < b.service ? -1 : a.service > b.service ? 1 : 0));
|
|
105
|
+
}
|
|
106
|
+
/**
|
|
107
|
+
* The recipe for a file loam could not use. See {@link VerificationRead} for
|
|
108
|
+
* why the two kinds of file get different ones. Neither tells anybody to edit
|
|
109
|
+
* the counts — a summary made to agree by hand still says nothing about what
|
|
110
|
+
* was checked — and both end in a record that can actually run: the old
|
|
111
|
+
* one-size "record the answers again" sent a miscounted record straight back
|
|
112
|
+
* into this refusal, because `--record` will not write over a file it could
|
|
113
|
+
* not read.
|
|
114
|
+
*/
|
|
115
|
+
function adviceFor(service) {
|
|
116
|
+
if (service !== null) {
|
|
117
|
+
return (`It is ${service}'s attestation file and holds no other service's word: repair it by hand, or delete it and ` +
|
|
118
|
+
`record ${service} again from that service's own repository (\`--service ${service} --record\`). ` +
|
|
119
|
+
"After a merge, both sides of a conflict in it are that one service's attestations.");
|
|
120
|
+
}
|
|
121
|
+
return ("It may hold other services' attestations, so do not delete it: repair it by hand, or restore it from version " +
|
|
122
|
+
"control — after a merge, take the copy on the branch you merged into — and then record again. A `--service` " +
|
|
123
|
+
"record goes to verification/<svc>.yaml beside it and leaves this file as it is.");
|
|
124
|
+
}
|
|
125
|
+
/**
|
|
126
|
+
* One file of the record, graded. `service` is the file's own service for a
|
|
127
|
+
* `verification/<svc>.yaml` — the file NAME is the claim about whose word it
|
|
128
|
+
* holds, so the content has to agree with it — and null for the one-file
|
|
129
|
+
* record.
|
|
130
|
+
*/
|
|
131
|
+
async function readRecordFile(path, service) {
|
|
132
|
+
// The code rides in an object, as a `code:` property, rather than as a
|
|
133
|
+
// parameter: a parameter named `code` makes this helper an emitter to the
|
|
134
|
+
// stable-code collector, which then cannot read the slot at the call sites
|
|
135
|
+
// that leave it out.
|
|
136
|
+
const unreadable = (reason, coded = {}) => ({
|
|
137
|
+
state: "unreadable",
|
|
138
|
+
path,
|
|
139
|
+
reason,
|
|
140
|
+
advice: adviceFor(service),
|
|
141
|
+
...coded,
|
|
142
|
+
});
|
|
143
|
+
let raw;
|
|
144
|
+
try {
|
|
145
|
+
raw = await readFile(path);
|
|
146
|
+
}
|
|
147
|
+
catch (err) {
|
|
148
|
+
if (err.code === "ENOENT") {
|
|
149
|
+
// ENOENT is "absent" — except from a symlink whose target is gone, which
|
|
150
|
+
// `existsSync` used to fold into the same answer. That mislabel was a
|
|
151
|
+
// trap: the read said "no record" while the commit's exclusive link(2)
|
|
152
|
+
// saw the link itself and refused EEXIST, so every `--record` answered
|
|
153
|
+
// `record-raced` with advice (re-run) that could never work. The lstat
|
|
154
|
+
// must CONFIRM the link before the message asserts one: a real file can
|
|
155
|
+
// also appear between the two calls, and telling its owner to "remove
|
|
156
|
+
// the link" would delete a valid record.
|
|
157
|
+
let entry;
|
|
158
|
+
try {
|
|
159
|
+
entry = await lstat(path);
|
|
160
|
+
}
|
|
161
|
+
catch {
|
|
162
|
+
return { state: "absent" };
|
|
163
|
+
}
|
|
164
|
+
if (entry.isSymbolicLink()) {
|
|
165
|
+
return unreadable("it is a symlink whose target does not exist — replace the link with a real record, or remove it");
|
|
166
|
+
}
|
|
167
|
+
return unreadable("it appeared while being read — another writer is landing; re-run");
|
|
168
|
+
}
|
|
169
|
+
return unreadable(err instanceof Error ? err.message : String(err));
|
|
170
|
+
}
|
|
171
|
+
const text = raw.toString("utf8");
|
|
172
|
+
// Before the parser, because the parser's answer to a merge is the wrong
|
|
173
|
+
// one: YAML either fails on a line that is not the problem or — where the
|
|
174
|
+
// markers land inside a block scalar — reads both sides as one plausible
|
|
175
|
+
// document. Neither says "a merge left this", which is the one sentence that
|
|
176
|
+
// leads to the right fix.
|
|
177
|
+
const markers = conflictMarkerLines(text);
|
|
178
|
+
if (markers.length > 0) {
|
|
179
|
+
return unreadable(`it still contains git conflict markers (line${markers.length === 1 ? "" : "s"} ${markers.join(", ")}) — ` +
|
|
180
|
+
"a merge left both sides in it, so nothing it says is anyone's record");
|
|
181
|
+
}
|
|
182
|
+
let doc;
|
|
183
|
+
try {
|
|
184
|
+
doc = parse(text);
|
|
185
|
+
}
|
|
186
|
+
catch (err) {
|
|
187
|
+
return unreadable(yamlReason(err));
|
|
188
|
+
}
|
|
189
|
+
const verification = asVerification(doc);
|
|
190
|
+
if (verification === null) {
|
|
191
|
+
return unreadable("it parses as YAML but does not have a verification record's shape (feature, recorded, checklist, summary and claims)");
|
|
192
|
+
}
|
|
193
|
+
const foreign = service === null ? null : notOnlyFrom(verification, service);
|
|
194
|
+
if (foreign !== null)
|
|
195
|
+
return unreadable(foreign);
|
|
196
|
+
const miscount = summaryDisagreement(verification);
|
|
197
|
+
// Spelled twice on purpose: the field is what a caller branches on, the
|
|
198
|
+
// prose is what a terminal prints and what somebody greps for.
|
|
199
|
+
if (miscount !== null)
|
|
200
|
+
return unreadable(`verify.record-miscounted: ${miscount}`, { code: "verify.record-miscounted" });
|
|
201
|
+
return { state: "ok", file: { path, verification, raw } };
|
|
202
|
+
}
|
|
203
|
+
/**
|
|
204
|
+
* A per-service file must be exactly what its name says: a federated record
|
|
205
|
+
* carrying that one service's attestation and that service's answers. loam
|
|
206
|
+
* never writes anything else there, and the advice for a damaged file — delete
|
|
207
|
+
* it and record that service again — is only safe while it is true.
|
|
208
|
+
*/
|
|
209
|
+
function notOnlyFrom(v, service) {
|
|
210
|
+
const attested = (v.attestations ?? []).map((a) => a.service);
|
|
211
|
+
if (v.schema !== 2 || attested.length !== 1 || attested[0] !== service) {
|
|
212
|
+
return `it is named for ${service} but does not hold exactly ${service}'s attestation (it holds ${attested.length === 0 ? "none" : attested.join(", ")})`;
|
|
213
|
+
}
|
|
214
|
+
const strays = v.claims.filter((c) => c.subject !== service).map((c) => c.id);
|
|
215
|
+
return strays.length === 0 ? null : `it is named for ${service} but answers claims filed under another service: ${strays.join(", ")}`;
|
|
216
|
+
}
|
|
217
|
+
/** A YAML failure with its line, when the parser located one: that line is the fix. */
|
|
218
|
+
function yamlReason(err) {
|
|
219
|
+
const message = err instanceof Error ? err.message.split("\n")[0].trim() : String(err);
|
|
220
|
+
const pos = err?.linePos;
|
|
221
|
+
const line = Array.isArray(pos) ? pos[0]?.line : undefined;
|
|
222
|
+
return line === undefined ? `YAML error: ${message}` : `YAML error at line ${line}: ${message}`;
|
|
223
|
+
}
|
|
224
|
+
//# sourceMappingURL=file.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"file.js","sourceRoot":"","sources":["../../../../src/core/verify/read/file.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * The verification record read back, and refused when it cannot be trusted.\n *\n * The record is several files now: the one-file `verification.yaml` (the\n * all-at-once form, and every federated record written before the directory\n * existed) and one `verification/<svc>.yaml` per attesting service. Each file\n * is graded on its own here, and `./union.ts` adds the graded files up\n * into the one `Verification` every reader takes — so no reader learns the\n * layout, and a record that is half one form and half the other reads as one\n * record.\n *\n * Reading is deliberately not the inverse of rendering. A file loam wrote is\n * re-graded on the way in — git conflict markers, unreadable YAML, a shape that\n * is not a verification, a `summary` that disagrees with the claims below it —\n * because the record is data meant to survive without loam, and anything may\n * have edited it since. The render lives with the writer it feeds\n * (`../store/render.ts` produces exactly the bytes `../store/commit.ts` swaps into\n * place; races and kills mid-write live there too), and the lock that\n * serialises writers is taken by `commands/verify/verify.ts` before this\n * module's authoritative read. Nothing here touches the filesystem except to\n * read.\n *\n * A package of its own (`core/verify/read/`, moved from `core/verify/file.ts`)\n * because the reader now leans on two modules that lean back on\n * `../record.ts`: kept in `core/verify` beside them it would have been a\n * package cycle that `import/no-cycle` cannot see.\n */\nimport { lstat, readdir, readFile } from \"node:fs/promises\";\nimport { join } from \"node:path\";\nimport { parse } from \"yaml\";\nimport { conflictMarkerLines } from \"../../conflict-markers.js\";\nimport { inOrder } from \"../../kernel/concurrency.js\";\nimport { verificationDir, verificationPath } from \"../location.js\";\nimport { type Verification } from \"../record.js\";\nimport { asVerification, summaryDisagreement } from \"./shape.js\";\nimport { assembleRecord } from \"./union.js\";\n\n/**\n * What is on disk beside a feature: nothing, something unreadable, or a record.\n *\n * The three are kept apart because they call for opposite handling. \"Absent\" is\n * a feature nobody has verified — verify starts a fresh record over it without\n * a second thought. \"Unreadable\" is a file that IS somebody's record: it parses\n * as garbage, or its shape is not a record's, and every answer and attestation\n * it holds is unaccounted for. Collapsing the two (which this function used to\n * do, returning null for both) meant a hand-edited or half-written\n * verification.yaml read as \"not verified\" and the next `--record` overwrote a\n * whole fleet's attestations without a word.\n */\nexport type VerificationRead =\n | { state: \"absent\" }\n /**\n * `path` is the ONE file that failed — with several files making up the\n * record, \"the record is unreadable\" names nothing a person can open.\n * `reason` names the YAML line when the parser gave one, or the conflict\n * markers' lines. `advice` is the recipe, and it differs by file on purpose:\n * a `verification/<svc>.yaml` holds one service's word, so deleting it and\n * recording that service again loses nobody else's evidence, while the\n * one-file record may hold a whole fleet's attestations and must never be\n * the file a recipe tells somebody to delete. `code` is set for the one\n * unreadable that parses perfectly: a file whose `summary` contradicts its\n * own `claims[]`. That lives here rather than in a state of its own so every\n * reader which already refuses an unreadable record refuses this one too,\n * without a line of new code.\n */\n | { state: \"unreadable\"; path: string; reason: string; advice: string; code?: \"verify.record-miscounted\" }\n /**\n * `verification` is the union the readers use; `files` is what it was added\n * up from, each with the exact bytes it was parsed from. A writer merges over\n * the PARSE, but commits over the BYTES: `commitVerification` compares each\n * file it replaces or deletes against these immediately before its swap, so\n * an edit that landed between this read and that write — an editor, a writer\n * that ignored the docs lock — is refused and preserved instead of silently\n * buried.\n */\n | { state: \"ok\"; verification: Verification; files: RecordFiles };\n\n/** One graded file of the record, with the bytes it was graded from. */\nexport interface RecordFile {\n path: string;\n verification: Verification;\n raw: Buffer;\n}\n\n/** The files a record was added up from: the one-file form, and the per-service files by service id. */\nexport interface RecordFiles {\n legacy: RecordFile | null;\n services: ReadonlyMap<string, RecordFile>;\n}\n\ntype Unreadable = Extract<VerificationRead, { state: \"unreadable\" }>;\ntype FileRead = { state: \"absent\" } | Unreadable | { state: \"ok\"; file: RecordFile };\n\n/**\n * `live` is the digest of the checklist the caller knows is being asked — see\n * `assembleRecord` for why the union needs it. A caller that only learns it\n * after the read (it derives the checklist only when there IS a record) adds\n * the same files up again with {@link answering} instead of reading twice.\n */\nexport async function readVerificationState(featureDir: string, live?: string): Promise<VerificationRead> {\n const legacy = await readRecordFile(verificationPath(featureDir), null);\n if (legacy.state === \"unreadable\") return legacy;\n const listed = await serviceFiles(featureDir);\n if (!Array.isArray(listed)) return listed;\n // In parallel — the files are independent, and a fleet-sized feature has\n // one per service — but graded in the listing's sorted order, so the file a\n // refusal names is the first unreadable one by name on every machine, never\n // whichever read happened to fail first.\n const reads = await inOrder(listed, ({ service, path }) => readRecordFile(path, service));\n const services = new Map<string, RecordFile>();\n for (const [i, read] of reads.entries()) {\n if (read.state === \"unreadable\") return read;\n // Listed and then gone is a file a concurrent writer removed: absent.\n if (read.state === \"ok\") services.set(listed[i]!.service, read.file);\n }\n const files: RecordFiles = { legacy: legacy.state === \"ok\" ? legacy.file : null, services };\n const verification = answering(files, live);\n if (verification === null) return { state: \"absent\" };\n return { state: \"ok\", verification, files };\n}\n\n/**\n * The record's files added up for the checklist `live` asks — what\n * {@link readVerificationState} answers, and what it would have answered for a\n * caller that only learned `live` after the read. Pure: the files are the ones\n * that read already graded. Null only when there are no files at all.\n */\nexport function answering(files: RecordFiles, live: string | undefined): Verification | null {\n return assembleRecord(\n files.legacy?.verification ?? null,\n [...files.services.values()].map((file) => file.verification),\n live,\n );\n}\n\n/**\n * The per-service files, sorted by service id so the union is the same on\n * every machine. Dotfiles are skipped — staging parks its temp files beside\n * the target as `.<name>.loam-….tmp`, and a reader racing a writer must not\n * grade one — and so is anything that is not `*.yaml`: the directory is the\n * list of attestations, and a README beside them is not one.\n */\nasync function serviceFiles(featureDir: string): Promise<Array<{ service: string; path: string }> | Unreadable> {\n const dir = verificationDir(featureDir);\n let entries;\n try {\n entries = await readdir(dir, { withFileTypes: true });\n } catch (err) {\n if ((err as NodeJS.ErrnoException).code === \"ENOENT\") return [];\n return {\n state: \"unreadable\",\n path: dir,\n reason: err instanceof Error ? err.message : String(err),\n advice: \"It has to be a directory holding one <svc>.yaml per attesting service; move whatever is there aside, then record again.\",\n };\n }\n return entries\n .filter((e) => !e.name.startsWith(\".\") && e.name.endsWith(\".yaml\") && !e.isDirectory())\n .map((e) => ({ service: e.name.slice(0, -\".yaml\".length), path: join(dir, e.name) }))\n .sort((a, b) => (a.service < b.service ? -1 : a.service > b.service ? 1 : 0));\n}\n\n/**\n * The recipe for a file loam could not use. See {@link VerificationRead} for\n * why the two kinds of file get different ones. Neither tells anybody to edit\n * the counts — a summary made to agree by hand still says nothing about what\n * was checked — and both end in a record that can actually run: the old\n * one-size \"record the answers again\" sent a miscounted record straight back\n * into this refusal, because `--record` will not write over a file it could\n * not read.\n */\nfunction adviceFor(service: string | null): string {\n if (service !== null) {\n return (\n `It is ${service}'s attestation file and holds no other service's word: repair it by hand, or delete it and ` +\n `record ${service} again from that service's own repository (\\`--service ${service} --record\\`). ` +\n \"After a merge, both sides of a conflict in it are that one service's attestations.\"\n );\n }\n return (\n \"It may hold other services' attestations, so do not delete it: repair it by hand, or restore it from version \" +\n \"control — after a merge, take the copy on the branch you merged into — and then record again. A `--service` \" +\n \"record goes to verification/<svc>.yaml beside it and leaves this file as it is.\"\n );\n}\n\n/**\n * One file of the record, graded. `service` is the file's own service for a\n * `verification/<svc>.yaml` — the file NAME is the claim about whose word it\n * holds, so the content has to agree with it — and null for the one-file\n * record.\n */\nasync function readRecordFile(path: string, service: string | null): Promise<FileRead> {\n // The code rides in an object, as a `code:` property, rather than as a\n // parameter: a parameter named `code` makes this helper an emitter to the\n // stable-code collector, which then cannot read the slot at the call sites\n // that leave it out.\n const unreadable = (reason: string, coded: { code?: \"verify.record-miscounted\" } = {}): Unreadable => ({\n state: \"unreadable\",\n path,\n reason,\n advice: adviceFor(service),\n ...coded,\n });\n let raw: Buffer;\n try {\n raw = await readFile(path);\n } catch (err) {\n if ((err as NodeJS.ErrnoException).code === \"ENOENT\") {\n // ENOENT is \"absent\" — except from a symlink whose target is gone, which\n // `existsSync` used to fold into the same answer. That mislabel was a\n // trap: the read said \"no record\" while the commit's exclusive link(2)\n // saw the link itself and refused EEXIST, so every `--record` answered\n // `record-raced` with advice (re-run) that could never work. The lstat\n // must CONFIRM the link before the message asserts one: a real file can\n // also appear between the two calls, and telling its owner to \"remove\n // the link\" would delete a valid record.\n let entry;\n try {\n entry = await lstat(path);\n } catch {\n return { state: \"absent\" };\n }\n if (entry.isSymbolicLink()) {\n return unreadable(\"it is a symlink whose target does not exist — replace the link with a real record, or remove it\");\n }\n return unreadable(\"it appeared while being read — another writer is landing; re-run\");\n }\n return unreadable(err instanceof Error ? err.message : String(err));\n }\n const text = raw.toString(\"utf8\");\n // Before the parser, because the parser's answer to a merge is the wrong\n // one: YAML either fails on a line that is not the problem or — where the\n // markers land inside a block scalar — reads both sides as one plausible\n // document. Neither says \"a merge left this\", which is the one sentence that\n // leads to the right fix.\n const markers = conflictMarkerLines(text);\n if (markers.length > 0) {\n return unreadable(\n `it still contains git conflict markers (line${markers.length === 1 ? \"\" : \"s\"} ${markers.join(\", \")}) — ` +\n \"a merge left both sides in it, so nothing it says is anyone's record\",\n );\n }\n let doc: unknown;\n try {\n doc = parse(text);\n } catch (err) {\n return unreadable(yamlReason(err));\n }\n const verification = asVerification(doc);\n if (verification === null) {\n return unreadable(\n \"it parses as YAML but does not have a verification record's shape (feature, recorded, checklist, summary and claims)\",\n );\n }\n const foreign = service === null ? null : notOnlyFrom(verification, service);\n if (foreign !== null) return unreadable(foreign);\n const miscount = summaryDisagreement(verification);\n // Spelled twice on purpose: the field is what a caller branches on, the\n // prose is what a terminal prints and what somebody greps for.\n if (miscount !== null) return unreadable(`verify.record-miscounted: ${miscount}`, { code: \"verify.record-miscounted\" });\n return { state: \"ok\", file: { path, verification, raw } };\n}\n\n/**\n * A per-service file must be exactly what its name says: a federated record\n * carrying that one service's attestation and that service's answers. loam\n * never writes anything else there, and the advice for a damaged file — delete\n * it and record that service again — is only safe while it is true.\n */\nfunction notOnlyFrom(v: Verification, service: string): string | null {\n const attested = (v.attestations ?? []).map((a) => a.service);\n if (v.schema !== 2 || attested.length !== 1 || attested[0] !== service) {\n return `it is named for ${service} but does not hold exactly ${service}'s attestation (it holds ${attested.length === 0 ? \"none\" : attested.join(\", \")})`;\n }\n const strays = v.claims.filter((c) => c.subject !== service).map((c) => c.id);\n return strays.length === 0 ? null : `it is named for ${service} but answers claims filed under another service: ${strays.join(\", \")}`;\n}\n\n/** A YAML failure with its line, when the parser located one: that line is the fix. */\nfunction yamlReason(err: unknown): string {\n const message = err instanceof Error ? err.message.split(\"\\n\")[0]!.trim() : String(err);\n const pos = (err as { linePos?: Array<{ line: number; col: number }> } | null)?.linePos;\n const line = Array.isArray(pos) ? pos[0]?.line : undefined;\n return line === undefined ? `YAML error: ${message}` : `YAML error at line ${line}: ${message}`;\n}\n"]}
|