@ybotok/loam 0.1.0-beta.2 → 0.2.0-alpha.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +2570 -61
- package/COMPARISON.md +80 -63
- package/CONTRIBUTING.md +84 -0
- package/MIGRATING-from-OpenSpec.md +213 -54
- package/QUICKSTART.md +125 -0
- package/README.md +450 -150
- package/ROADMAP.md +494 -0
- package/SCHEMA.md +2362 -245
- package/SECURITY.md +21 -3
- package/WORKFLOW.md +701 -0
- package/dist/cli.js +113 -19
- package/dist/cli.js.map +1 -1
- package/dist/commands/adopt/adopt.js +209 -0
- package/dist/commands/adopt/adopt.js.map +1 -0
- package/dist/commands/adopt/render.js +268 -0
- package/dist/commands/adopt/render.js.map +1 -0
- package/dist/commands/archive/archive.js +77 -0
- package/dist/commands/archive/archive.js.map +1 -0
- package/dist/commands/archive/plan/contracts/asyncapi.js +178 -0
- package/dist/commands/archive/plan/contracts/asyncapi.js.map +1 -0
- package/dist/commands/archive/plan/contracts/openapi.js +159 -0
- package/dist/commands/archive/plan/contracts/openapi.js.map +1 -0
- package/dist/commands/archive/plan/gate.js +317 -0
- package/dist/commands/archive/plan/gate.js.map +1 -0
- package/dist/commands/archive/plan/landscape.js +161 -0
- package/dist/commands/archive/plan/landscape.js.map +1 -0
- package/dist/commands/archive/plan/refusal.js +111 -0
- package/dist/commands/archive/plan/refusal.js.map +1 -0
- package/dist/commands/archive/plan/specs.js +237 -0
- package/dist/commands/archive/plan/specs.js.map +1 -0
- package/dist/commands/archive/plan/state.js +27 -0
- package/dist/commands/archive/plan/state.js.map +1 -0
- package/dist/commands/archive/run.js +251 -0
- package/dist/commands/archive/run.js.map +1 -0
- package/dist/commands/context/context.js +99 -0
- package/dist/commands/context/context.js.map +1 -0
- package/dist/commands/context/print.js +283 -0
- package/dist/commands/context/print.js.map +1 -0
- package/dist/commands/delta/delta.js +279 -0
- package/dist/commands/delta/delta.js.map +1 -0
- package/dist/commands/delta/print.js +205 -0
- package/dist/commands/delta/print.js.map +1 -0
- package/dist/commands/dependencies.js +9 -7
- package/dist/commands/dependencies.js.map +1 -1
- package/dist/commands/diff/diff.js +93 -0
- package/dist/commands/diff/diff.js.map +1 -0
- package/dist/commands/diff/print.js +38 -0
- package/dist/commands/diff/print.js.map +1 -0
- package/dist/commands/doctor.js +5 -4
- package/dist/commands/doctor.js.map +1 -1
- package/dist/commands/explain/explain.js +151 -0
- package/dist/commands/explain/explain.js.map +1 -0
- package/dist/commands/explore.js +29 -24
- package/dist/commands/explore.js.map +1 -1
- package/dist/commands/gate/gate.js +115 -0
- package/dist/commands/gate/gate.js.map +1 -0
- package/dist/commands/gate/print.js +54 -0
- package/dist/commands/gate/print.js.map +1 -0
- package/dist/commands/gherkin/commit.js +146 -0
- package/dist/commands/gherkin/commit.js.map +1 -0
- package/dist/commands/gherkin/gherkin.js +246 -0
- package/dist/commands/gherkin/gherkin.js.map +1 -0
- package/dist/commands/gherkin/reconcile.js +149 -0
- package/dist/commands/gherkin/reconcile.js.map +1 -0
- package/dist/commands/gherkin/render.js +77 -0
- package/dist/commands/gherkin/render.js.map +1 -0
- package/dist/commands/init/example.js +204 -0
- package/dist/commands/init/example.js.map +1 -0
- package/dist/commands/init/first-hour.js +28 -0
- package/dist/commands/init/first-hour.js.map +1 -0
- package/dist/commands/{init.js → init/init.js} +146 -71
- package/dist/commands/init/init.js.map +1 -0
- package/dist/commands/init/mcp.js +131 -0
- package/dist/commands/init/mcp.js.map +1 -0
- package/dist/commands/init/options.js +78 -0
- package/dist/commands/init/options.js.map +1 -0
- package/dist/commands/instructions.js +82 -14
- package/dist/commands/instructions.js.map +1 -1
- package/dist/commands/list/campaign/campaign.js +65 -0
- package/dist/commands/list/campaign/campaign.js.map +1 -0
- package/dist/commands/list/campaign/owners.js +65 -0
- package/dist/commands/list/campaign/owners.js.map +1 -0
- package/dist/commands/list/json.js +157 -0
- package/dist/commands/list/json.js.map +1 -0
- package/dist/commands/list/list.js +343 -0
- package/dist/commands/list/list.js.map +1 -0
- package/dist/commands/list/print.js +301 -0
- package/dist/commands/list/print.js.map +1 -0
- package/dist/commands/list/review.js +120 -0
- package/dist/commands/list/review.js.map +1 -0
- package/dist/commands/list/views.js +105 -0
- package/dist/commands/list/views.js.map +1 -0
- package/dist/commands/mcp/dispatch.js +183 -0
- package/dist/commands/mcp/dispatch.js.map +1 -0
- package/dist/commands/mcp/mcp.js +34 -0
- package/dist/commands/mcp/mcp.js.map +1 -0
- package/dist/commands/mcp/serve.js +55 -0
- package/dist/commands/mcp/serve.js.map +1 -0
- package/dist/commands/migrate-openspec/materialize/active.js +170 -0
- package/dist/commands/migrate-openspec/materialize/active.js.map +1 -0
- package/dist/commands/migrate-openspec/materialize/feature.js +41 -0
- package/dist/commands/migrate-openspec/materialize/feature.js.map +1 -0
- package/dist/commands/migrate-openspec/materialize/living.js +62 -0
- package/dist/commands/migrate-openspec/materialize/living.js.map +1 -0
- package/dist/commands/migrate-openspec/materialize/stage.js +142 -0
- package/dist/commands/migrate-openspec/materialize/stage.js.map +1 -0
- package/dist/commands/migrate-openspec/materialize/target.js +219 -0
- package/dist/commands/migrate-openspec/materialize/target.js.map +1 -0
- package/dist/commands/migrate-openspec/migrate-openspec.js +142 -0
- package/dist/commands/migrate-openspec/migrate-openspec.js.map +1 -0
- package/dist/commands/migrate-openspec/openspec/decisions.js +91 -0
- package/dist/commands/migrate-openspec/openspec/decisions.js.map +1 -0
- package/dist/commands/migrate-openspec/openspec/error.js +19 -0
- package/dist/commands/migrate-openspec/openspec/error.js.map +1 -0
- package/dist/commands/migrate-openspec/openspec/mapping.js +170 -0
- package/dist/commands/migrate-openspec/openspec/mapping.js.map +1 -0
- package/dist/commands/migrate-openspec/openspec/paths.js +43 -0
- package/dist/commands/migrate-openspec/openspec/paths.js.map +1 -0
- package/dist/commands/migrate-openspec/openspec/read.js +49 -0
- package/dist/commands/migrate-openspec/openspec/read.js.map +1 -0
- package/dist/commands/migrate-openspec/print.js +89 -0
- package/dist/commands/migrate-openspec/print.js.map +1 -0
- package/dist/commands/new/commit.js +76 -0
- package/dist/commands/new/commit.js.map +1 -0
- package/dist/commands/new/new.js +283 -0
- package/dist/commands/new/new.js.map +1 -0
- package/dist/commands/new/templates.js +355 -0
- package/dist/commands/new/templates.js.map +1 -0
- package/dist/commands/open.js +203 -0
- package/dist/commands/open.js.map +1 -0
- package/dist/commands/policy/format.js +152 -0
- package/dist/commands/policy/format.js.map +1 -0
- package/dist/commands/{docs-repo-gate.js → policy/gate.js} +11 -11
- package/dist/commands/policy/gate.js.map +1 -0
- package/dist/commands/rebase/edit.js +106 -0
- package/dist/commands/rebase/edit.js.map +1 -0
- package/dist/commands/rebase/living.js +185 -0
- package/dist/commands/rebase/living.js.map +1 -0
- package/dist/commands/rebase/plan.js +245 -0
- package/dist/commands/rebase/plan.js.map +1 -0
- package/dist/commands/rebase/rebase.js +275 -0
- package/dist/commands/rebase/rebase.js.map +1 -0
- package/dist/commands/seed/commit.js +192 -0
- package/dist/commands/seed/commit.js.map +1 -0
- package/dist/commands/seed/plan.js +133 -0
- package/dist/commands/seed/plan.js.map +1 -0
- package/dist/commands/seed/seed.js +172 -0
- package/dist/commands/seed/seed.js.map +1 -0
- package/dist/commands/show/feature.js +178 -0
- package/dist/commands/show/feature.js.map +1 -0
- package/dist/commands/show/marks.js +24 -0
- package/dist/commands/show/marks.js.map +1 -0
- package/dist/commands/{show.js → show/service.js} +63 -152
- package/dist/commands/show/service.js.map +1 -0
- package/dist/commands/show/show.js +77 -0
- package/dist/commands/show/show.js.map +1 -0
- package/dist/commands/status/json.js +45 -0
- package/dist/commands/status/json.js.map +1 -0
- package/dist/commands/status/print.js +157 -0
- package/dist/commands/status/print.js.map +1 -0
- package/dist/commands/status/status.js +119 -0
- package/dist/commands/status/status.js.map +1 -0
- package/dist/commands/steps/steps.js +189 -0
- package/dist/commands/steps/steps.js.map +1 -0
- package/dist/commands/subsystem/edit.js +192 -0
- package/dist/commands/subsystem/edit.js.map +1 -0
- package/dist/commands/subsystem/move.js +265 -0
- package/dist/commands/subsystem/move.js.map +1 -0
- package/dist/commands/subsystem/query.js +115 -0
- package/dist/commands/subsystem/query.js.map +1 -0
- package/dist/commands/subsystem/subsystem.js +101 -0
- package/dist/commands/subsystem/subsystem.js.map +1 -0
- package/dist/commands/subsystem/sync.js +103 -0
- package/dist/commands/subsystem/sync.js.map +1 -0
- package/dist/commands/subsystem/txn/txn.js +94 -0
- package/dist/commands/subsystem/txn/txn.js.map +1 -0
- package/dist/commands/subsystem/txn/views.js +70 -0
- package/dist/commands/subsystem/txn/views.js.map +1 -0
- package/dist/commands/unarchive/manifest.js +160 -0
- package/dist/commands/unarchive/manifest.js.map +1 -0
- package/dist/commands/{unarchive.js → unarchive/restore.js} +47 -160
- package/dist/commands/unarchive/restore.js.map +1 -0
- package/dist/commands/unarchive/unarchive.js +67 -0
- package/dist/commands/unarchive/unarchive.js.map +1 -0
- package/dist/commands/validate/arch-coverage.js +194 -0
- package/dist/commands/validate/arch-coverage.js.map +1 -0
- package/dist/commands/validate/checks/fleet-shape.js +296 -0
- package/dist/commands/validate/checks/fleet-shape.js.map +1 -0
- package/dist/commands/validate/checks/health-deps.js +55 -0
- package/dist/commands/validate/checks/health-deps.js.map +1 -0
- package/dist/commands/validate/checks/requirements.js +254 -0
- package/dist/commands/validate/checks/requirements.js.map +1 -0
- package/dist/commands/validate/checks/sources.js +76 -0
- package/dist/commands/validate/checks/sources.js.map +1 -0
- package/dist/commands/validate/checks/vocabulary.js +31 -0
- package/dist/commands/validate/checks/vocabulary.js.map +1 -0
- package/dist/commands/validate/feature.js +216 -0
- package/dist/commands/validate/feature.js.map +1 -0
- package/dist/commands/validate/fleet/census.js +69 -0
- package/dist/commands/validate/fleet/census.js.map +1 -0
- package/dist/commands/validate/fleet/kind-tags.js +39 -0
- package/dist/commands/validate/fleet/kind-tags.js.map +1 -0
- package/dist/commands/validate/fleet/landscape.js +353 -0
- package/dist/commands/validate/fleet/landscape.js.map +1 -0
- package/dist/commands/validate/fleet/load.js +52 -0
- package/dist/commands/validate/fleet/load.js.map +1 -0
- package/dist/commands/validate/fleet/obligations.js +236 -0
- package/dist/commands/validate/fleet/obligations.js.map +1 -0
- package/dist/commands/validate/fleet/scorecard/adoption.js +102 -0
- package/dist/commands/validate/fleet/scorecard/adoption.js.map +1 -0
- package/dist/commands/validate/fleet/scorecard/contracts.js +98 -0
- package/dist/commands/validate/fleet/scorecard/contracts.js.map +1 -0
- package/dist/commands/validate/fleet/scorecard/print.js +63 -0
- package/dist/commands/validate/fleet/scorecard/print.js.map +1 -0
- package/dist/commands/validate/fleet/scorecard/scorecard.js +156 -0
- package/dist/commands/validate/fleet/scorecard/scorecard.js.map +1 -0
- package/dist/commands/validate/fleet/usecases/capability-tag.js +94 -0
- package/dist/commands/validate/fleet/usecases/capability-tag.js.map +1 -0
- package/dist/commands/validate/fleet/usecases/place.js +13 -0
- package/dist/commands/validate/fleet/usecases/place.js.map +1 -0
- package/dist/commands/validate/fleet/usecases/requirement-tag.js +95 -0
- package/dist/commands/validate/fleet/usecases/requirement-tag.js.map +1 -0
- package/dist/commands/validate/fleet/usecases/steps.js +197 -0
- package/dist/commands/validate/fleet/usecases/steps.js.map +1 -0
- package/dist/commands/validate/fleet/usecases/usecases.js +99 -0
- package/dist/commands/validate/fleet/usecases/usecases.js.map +1 -0
- package/dist/commands/validate/fleet/views/ids.js +40 -0
- package/dist/commands/validate/fleet/views/ids.js.map +1 -0
- package/dist/commands/validate/fleet/views/stale.js +59 -0
- package/dist/commands/validate/fleet/views/stale.js.map +1 -0
- package/dist/commands/validate/links/corpus.js +92 -0
- package/dist/commands/validate/links/corpus.js.map +1 -0
- package/dist/commands/validate/links/glossary.js +75 -0
- package/dist/commands/validate/links/glossary.js.map +1 -0
- package/dist/commands/validate/report.js +269 -0
- package/dist/commands/validate/report.js.map +1 -0
- package/dist/commands/validate/service/api.js +338 -0
- package/dist/commands/validate/service/api.js.map +1 -0
- package/dist/commands/validate/service/events/events.js +219 -0
- package/dist/commands/validate/service/events/events.js.map +1 -0
- package/dist/commands/validate/service/events/message-producers.js +77 -0
- package/dist/commands/validate/service/events/message-producers.js.map +1 -0
- package/dist/commands/validate/service/evidence-pins.js +131 -0
- package/dist/commands/validate/service/evidence-pins.js.map +1 -0
- package/dist/commands/validate/service/service.js +250 -0
- package/dist/commands/validate/service/service.js.map +1 -0
- package/dist/commands/validate/service/specs.js +219 -0
- package/dist/commands/validate/service/specs.js.map +1 -0
- package/dist/commands/validate/service/spine.js +113 -0
- package/dist/commands/validate/service/spine.js.map +1 -0
- package/dist/commands/validate/validate.js +338 -0
- package/dist/commands/validate/validate.js.map +1 -0
- package/dist/commands/verify/cross/diff.js +182 -0
- package/dist/commands/verify/cross/diff.js.map +1 -0
- package/dist/commands/verify/evidence/contract.js +32 -0
- package/dist/commands/verify/evidence/contract.js.map +1 -0
- package/dist/commands/verify/evidence/pins.js +52 -0
- package/dist/commands/verify/evidence/pins.js.map +1 -0
- package/dist/commands/verify/evidence/read.js +72 -0
- package/dist/commands/verify/evidence/read.js.map +1 -0
- package/dist/commands/verify/frozen.js +241 -0
- package/dist/commands/verify/frozen.js.map +1 -0
- package/dist/commands/verify/record.js +226 -0
- package/dist/commands/verify/record.js.map +1 -0
- package/dist/commands/verify/report.js +223 -0
- package/dist/commands/verify/report.js.map +1 -0
- package/dist/commands/verify/results.js +246 -0
- package/dist/commands/verify/results.js.map +1 -0
- package/dist/commands/verify/verify.js +217 -0
- package/dist/commands/verify/verify.js.map +1 -0
- package/dist/commands/vouch/contract.js +11 -0
- package/dist/commands/vouch/contract.js.map +1 -0
- package/dist/commands/vouch/pack/contract.js +2 -0
- package/dist/commands/vouch/pack/contract.js.map +1 -0
- package/dist/commands/vouch/pack/pack.js +266 -0
- package/dist/commands/vouch/pack/pack.js.map +1 -0
- package/dist/commands/vouch/pack/print.js +271 -0
- package/dist/commands/vouch/pack/print.js.map +1 -0
- package/dist/commands/vouch/pack/sections.js +142 -0
- package/dist/commands/vouch/pack/sections.js.map +1 -0
- package/dist/commands/vouch/prompt/confirm.js +46 -0
- package/dist/commands/vouch/prompt/confirm.js.map +1 -0
- package/dist/commands/vouch/report.js +101 -0
- package/dist/commands/vouch/report.js.map +1 -0
- package/dist/commands/vouch/run.js +285 -0
- package/dist/commands/vouch/run.js.map +1 -0
- package/dist/commands/vouch/sample/plan.js +125 -0
- package/dist/commands/vouch/sample/plan.js.map +1 -0
- package/dist/commands/vouch/sample/print.js +109 -0
- package/dist/commands/vouch/sample/print.js.map +1 -0
- package/dist/commands/vouch/vet/verify.js +123 -0
- package/dist/commands/vouch/vet/verify.js.map +1 -0
- package/dist/commands/vouch/vouch.js +176 -0
- package/dist/commands/vouch/vouch.js.map +1 -0
- package/dist/core/agent/agents-md/artifacts.js +220 -0
- package/dist/core/agent/agents-md/artifacts.js.map +1 -0
- package/dist/core/agent/agents-md/command-map.js +324 -0
- package/dist/core/agent/agents-md/command-map.js.map +1 -0
- package/dist/core/agent/agents-md/cycle.js +165 -0
- package/dist/core/agent/agents-md/cycle.js.map +1 -0
- package/dist/core/agent/agents-md/map/explain.js +34 -0
- package/dist/core/agent/agents-md/map/explain.js.map +1 -0
- package/dist/core/agent/agents-md/map/lenses/context.js +41 -0
- package/dist/core/agent/agents-md/map/lenses/context.js.map +1 -0
- package/dist/core/agent/agents-md/map/lenses/diff.js +46 -0
- package/dist/core/agent/agents-md/map/lenses/diff.js.map +1 -0
- package/dist/core/agent/agents-md/map/lenses/gate.js +57 -0
- package/dist/core/agent/agents-md/map/lenses/gate.js.map +1 -0
- package/dist/core/agent/agents-md/map/mcp.js +41 -0
- package/dist/core/agent/agents-md/map/mcp.js.map +1 -0
- package/dist/core/agent/agents-md/map/status.js +62 -0
- package/dist/core/agent/agents-md/map/status.js.map +1 -0
- package/dist/core/agent/agents-md/map/subsystem.js +32 -0
- package/dist/core/agent/agents-md/map/subsystem.js.map +1 -0
- package/dist/core/agent/agents-md/map/usecases.js +60 -0
- package/dist/core/agent/agents-md/map/usecases.js.map +1 -0
- package/dist/core/agent/agents-md/refusals.js +246 -0
- package/dist/core/agent/agents-md/refusals.js.map +1 -0
- package/dist/core/agent/agents-md/shipped/archive-gate.js +98 -0
- package/dist/core/agent/agents-md/shipped/archive-gate.js.map +1 -0
- package/dist/core/agent/agents-md/spine.js +40 -0
- package/dist/core/agent/agents-md/spine.js.map +1 -0
- package/dist/core/agent/agents-md.js +170 -0
- package/dist/core/agent/agents-md.js.map +1 -0
- package/dist/core/agent/agents-stamp.js +155 -0
- package/dist/core/agent/agents-stamp.js.map +1 -0
- package/dist/core/agent/contract.js +2 -0
- package/dist/core/agent/contract.js.map +1 -0
- package/dist/core/agent/protocol.js +193 -0
- package/dist/core/agent/protocol.js.map +1 -0
- package/dist/core/agent/scaffold.js +259 -0
- package/dist/core/agent/scaffold.js.map +1 -0
- package/dist/core/agent/support/report.js +103 -0
- package/dist/core/agent/support/report.js.map +1 -0
- package/dist/core/agent/tools/dialects.js +150 -0
- package/dist/core/agent/tools/dialects.js.map +1 -0
- package/dist/core/agent/tools/registry.js +228 -0
- package/dist/core/agent/tools/registry.js.map +1 -0
- package/dist/core/agent/workflows/adopt.js +133 -0
- package/dist/core/agent/workflows/adopt.js.map +1 -0
- package/dist/core/agent/workflows/check.js +334 -0
- package/dist/core/agent/workflows/check.js.map +1 -0
- package/dist/core/agent/workflows/closing.js +262 -0
- package/dist/core/agent/workflows/closing.js.map +1 -0
- package/dist/core/agent/workflows/feature.js +105 -0
- package/dist/core/agent/workflows/feature.js.map +1 -0
- package/dist/core/agent/workflows/implement.js +105 -0
- package/dist/core/agent/workflows/implement.js.map +1 -0
- package/dist/core/agent/workflows/reference/authoring.js +209 -0
- package/dist/core/agent/workflows/reference/authoring.js.map +1 -0
- package/dist/core/agent/workflows/reference/codes.js +65 -0
- package/dist/core/agent/workflows/reference/codes.js.map +1 -0
- package/dist/core/agent/workflows/reference/done-check.js +188 -0
- package/dist/core/agent/workflows/reference/done-check.js.map +1 -0
- package/dist/core/agent/workflows/reference/reference.js +16 -0
- package/dist/core/agent/workflows/reference/reference.js.map +1 -0
- package/dist/core/agent/workflows/reference/spine.js +297 -0
- package/dist/core/agent/workflows/reference/spine.js.map +1 -0
- package/dist/core/asyncapi/baseline/plan.js +110 -0
- package/dist/core/asyncapi/baseline/plan.js.map +1 -0
- package/dist/core/asyncapi/baseline/surfaces.js +54 -0
- package/dist/core/asyncapi/baseline/surfaces.js.map +1 -0
- package/dist/core/asyncapi/depth.js +164 -0
- package/dist/core/asyncapi/depth.js.map +1 -0
- package/dist/core/asyncapi/digest.js +141 -0
- package/dist/core/asyncapi/digest.js.map +1 -0
- package/dist/core/asyncapi/merge/components.js +91 -0
- package/dist/core/asyncapi/merge/components.js.map +1 -0
- package/dist/core/asyncapi/merge/error.js +33 -0
- package/dist/core/asyncapi/merge/error.js.map +1 -0
- package/dist/core/asyncapi/merge/markers.js +308 -0
- package/dist/core/asyncapi/merge/markers.js.map +1 -0
- package/dist/core/asyncapi/merge/merge.js +215 -0
- package/dist/core/asyncapi/merge/merge.js.map +1 -0
- package/dist/core/asyncapi/merge/pin.js +107 -0
- package/dist/core/asyncapi/merge/pin.js.map +1 -0
- package/dist/core/asyncapi/model.js +11 -0
- package/dist/core/asyncapi/model.js.map +1 -0
- package/dist/core/asyncapi/producers.js +50 -0
- package/dist/core/asyncapi/producers.js.map +1 -0
- package/dist/core/asyncapi/read.js +239 -0
- package/dist/core/asyncapi/read.js.map +1 -0
- package/dist/core/brief/brief.js +225 -0
- package/dist/core/brief/brief.js.map +1 -0
- package/dist/core/brief/checks.js +250 -0
- package/dist/core/brief/checks.js.map +1 -0
- package/dist/core/brief/landscape.js +194 -0
- package/dist/core/brief/landscape.js.map +1 -0
- package/dist/core/brief/targets.js +226 -0
- package/dist/core/brief/targets.js.map +1 -0
- package/dist/core/brief/unchecked.js +47 -0
- package/dist/core/brief/unchecked.js.map +1 -0
- package/dist/core/c4/arch.js +282 -0
- package/dist/core/c4/arch.js.map +1 -0
- package/dist/core/c4/likec4.js +88 -0
- package/dist/core/c4/likec4.js.map +1 -0
- package/dist/core/c4/parsed/dynamic-views.js +132 -0
- package/dist/core/c4/parsed/dynamic-views.js.map +1 -0
- package/dist/core/c4/parsed/specification.js +78 -0
- package/dist/core/c4/parsed/specification.js.map +1 -0
- package/dist/core/c4/parsed/values.js +57 -0
- package/dist/core/c4/parsed/values.js.map +1 -0
- package/dist/core/c4/parsed/view-ids.js +75 -0
- package/dist/core/c4/parsed/view-ids.js.map +1 -0
- package/dist/core/c4/project/architecture.js +45 -0
- package/dist/core/c4/project/architecture.js.map +1 -0
- package/dist/core/c4/project/documents.js +58 -0
- package/dist/core/c4/project/documents.js.map +1 -0
- package/dist/core/c4/project/load.js +183 -0
- package/dist/core/c4/project/load.js.map +1 -0
- package/dist/core/c4/resolve/service.js +113 -0
- package/dist/core/c4/resolve/service.js.map +1 -0
- package/dist/core/c4/seed/fleet-file.js +201 -0
- package/dist/core/c4/seed/fleet-file.js.map +1 -0
- package/dist/core/c4/seed/items.js +91 -0
- package/dist/core/c4/seed/items.js.map +1 -0
- package/dist/core/c4/seed/stamp.js +48 -0
- package/dist/core/c4/seed/stamp.js.map +1 -0
- package/dist/core/c4/seed/template.js +109 -0
- package/dist/core/c4/seed/template.js.map +1 -0
- package/dist/core/c4/source-mask.js +82 -0
- package/dist/core/c4/source-mask.js.map +1 -0
- package/dist/core/c4/source-scan.js +227 -0
- package/dist/core/c4/source-scan.js.map +1 -0
- package/dist/core/c4/splice/authored-source.js +101 -0
- package/dist/core/c4/splice/authored-source.js.map +1 -0
- package/dist/core/c4/splice/contract.js +10 -0
- package/dist/core/c4/splice/contract.js.map +1 -0
- package/dist/core/c4/splice/delta-blocks.js +126 -0
- package/dist/core/c4/splice/delta-blocks.js.map +1 -0
- package/dist/core/c4/splice/identity/edges.js +36 -0
- package/dist/core/c4/splice/identity/edges.js.map +1 -0
- package/dist/core/c4/splice/landscape-merge.js +269 -0
- package/dist/core/c4/splice/landscape-merge.js.map +1 -0
- package/dist/core/c4/splice/placement.js +194 -0
- package/dist/core/c4/splice/placement.js.map +1 -0
- package/dist/core/c4/workspace.js +172 -0
- package/dist/core/c4/workspace.js.map +1 -0
- package/dist/core/capabilities/authoring/notes.js +141 -0
- package/dist/core/capabilities/authoring/notes.js.map +1 -0
- package/dist/core/capabilities/capabilities.js +124 -0
- package/dist/core/capabilities/capabilities.js.map +1 -0
- package/dist/core/capabilities/delta/doc.js +50 -0
- package/dist/core/capabilities/delta/doc.js.map +1 -0
- package/dist/core/capabilities/delta/overlay.js +129 -0
- package/dist/core/capabilities/delta/overlay.js.map +1 -0
- package/dist/core/capabilities/delta/summary.js +57 -0
- package/dist/core/capabilities/delta/summary.js.map +1 -0
- package/dist/core/capabilities/delta/tree.js +43 -0
- package/dist/core/capabilities/delta/tree.js.map +1 -0
- package/dist/core/capabilities/delta/uncovered.js +222 -0
- package/dist/core/capabilities/delta/uncovered.js.map +1 -0
- package/dist/core/capabilities/findings.js +342 -0
- package/dist/core/capabilities/findings.js.map +1 -0
- package/dist/core/capabilities/realizes/findings.js +181 -0
- package/dist/core/capabilities/realizes/findings.js.map +1 -0
- package/dist/core/capabilities/realizes/join.js +143 -0
- package/dist/core/capabilities/realizes/join.js.map +1 -0
- package/dist/core/capabilities/rollup.js +199 -0
- package/dist/core/capabilities/rollup.js.map +1 -0
- package/dist/core/capabilities/tree.js +126 -0
- package/dist/core/capabilities/tree.js.map +1 -0
- package/dist/core/capabilities/usecase-join.js +277 -0
- package/dist/core/capabilities/usecase-join.js.map +1 -0
- package/dist/core/coherence/authoring/scaffold.js +177 -0
- package/dist/core/coherence/authoring/scaffold.js.map +1 -0
- package/dist/core/coherence/authoring/sentinels.js +74 -0
- package/dist/core/coherence/authoring/sentinels.js.map +1 -0
- package/dist/core/coherence/coherence.js +251 -0
- package/dist/core/coherence/coherence.js.map +1 -0
- package/dist/core/coherence/declared.js +269 -0
- package/dist/core/coherence/declared.js.map +1 -0
- package/dist/core/coherence/events/declared.js +206 -0
- package/dist/core/coherence/events/declared.js.map +1 -0
- package/dist/core/coherence/events/events.js +204 -0
- package/dist/core/coherence/events/events.js.map +1 -0
- package/dist/core/coherence/events/grades.js +166 -0
- package/dist/core/coherence/events/grades.js.map +1 -0
- package/dist/core/coherence/events/lookups.js +122 -0
- package/dist/core/coherence/events/lookups.js.map +1 -0
- package/dist/core/coherence/events/merged.js +40 -0
- package/dist/core/coherence/events/merged.js.map +1 -0
- package/dist/core/coherence/living.js +198 -0
- package/dist/core/coherence/living.js.map +1 -0
- package/dist/core/coherence/lookups.js +155 -0
- package/dist/core/coherence/lookups.js.map +1 -0
- package/dist/core/coherence/pending.js +39 -0
- package/dist/core/coherence/pending.js.map +1 -0
- package/dist/core/conflict-markers.js +60 -0
- package/dist/core/conflict-markers.js.map +1 -0
- package/dist/core/delta/claims.js +73 -0
- package/dist/core/delta/claims.js.map +1 -0
- package/dist/core/delta/delta.js +296 -0
- package/dist/core/delta/delta.js.map +1 -0
- package/dist/core/delta/document.js +177 -0
- package/dist/core/delta/document.js.map +1 -0
- package/dist/core/delta/scope.js +48 -0
- package/dist/core/delta/scope.js.map +1 -0
- package/dist/core/delta/select.js +237 -0
- package/dist/core/delta/select.js.map +1 -0
- package/dist/core/dependencies/dependencies.js +141 -0
- package/dist/core/dependencies/dependencies.js.map +1 -0
- package/dist/core/dependencies/facts.js +165 -0
- package/dist/core/dependencies/facts.js.map +1 -0
- package/dist/core/dependencies/fanin.js +88 -0
- package/dist/core/dependencies/fanin.js.map +1 -0
- package/dist/core/dependencies/graph.js +106 -0
- package/dist/core/dependencies/graph.js.map +1 -0
- package/dist/core/diff/base-git.js +148 -0
- package/dist/core/diff/base-git.js.map +1 -0
- package/dist/core/diff/base-state.js +209 -0
- package/dist/core/diff/base-state.js.map +1 -0
- package/dist/core/diff/current-state.js +96 -0
- package/dist/core/diff/current-state.js.map +1 -0
- package/dist/core/diff/scope/changed-paths.js +188 -0
- package/dist/core/diff/scope/changed-paths.js.map +1 -0
- package/dist/core/diff/semantic.js +243 -0
- package/dist/core/diff/semantic.js.map +1 -0
- package/dist/core/diff/victims.js +247 -0
- package/dist/core/diff/victims.js.map +1 -0
- package/dist/core/docs.js +59 -55
- package/dist/core/docs.js.map +1 -1
- package/dist/core/doctor/agents.js +222 -0
- package/dist/core/doctor/agents.js.map +1 -0
- package/dist/core/doctor/config.js +135 -0
- package/dist/core/doctor/config.js.map +1 -0
- package/dist/core/doctor/doctor.js +275 -0
- package/dist/core/doctor/doctor.js.map +1 -0
- package/dist/core/doctor/report.js +13 -0
- package/dist/core/doctor/report.js.map +1 -0
- package/dist/core/doctor/residue.js +110 -0
- package/dist/core/doctor/residue.js.map +1 -0
- package/dist/core/document/apply.js +77 -0
- package/dist/core/document/apply.js.map +1 -0
- package/dist/core/{frontmatter.js → document/frontmatter.js} +48 -2
- package/dist/core/document/frontmatter.js.map +1 -0
- package/dist/core/document/grammar/directives.js +143 -0
- package/dist/core/document/grammar/directives.js.map +1 -0
- package/dist/core/document/parse.js +263 -0
- package/dist/core/document/parse.js.map +1 -0
- package/dist/core/document/scenarios.js +221 -0
- package/dist/core/document/scenarios.js.map +1 -0
- package/dist/core/document/spec.js +241 -0
- package/dist/core/document/spec.js.map +1 -0
- package/dist/core/{config.js → envelope/config.js} +79 -18
- package/dist/core/envelope/config.js.map +1 -0
- package/dist/core/envelope/json.js +175 -0
- package/dist/core/envelope/json.js.map +1 -0
- package/dist/core/{version.js → envelope/version.js} +3 -3
- package/dist/core/envelope/version.js.map +1 -0
- package/dist/core/explain/families.js +175 -0
- package/dist/core/explain/families.js.map +1 -0
- package/dist/core/explain/fix-tables.js +178 -0
- package/dist/core/explain/fix-tables.js.map +1 -0
- package/dist/core/explain/lookup.js +181 -0
- package/dist/core/explain/lookup.js.map +1 -0
- package/dist/core/explain/refusals.js +49 -0
- package/dist/core/explain/refusals.js.map +1 -0
- package/dist/core/explain/terms.js +144 -0
- package/dist/core/explain/terms.js.map +1 -0
- package/dist/core/explore/describe.js +105 -0
- package/dist/core/explore/describe.js.map +1 -0
- package/dist/core/{explore.js → explore/explore.js} +50 -101
- package/dist/core/explore/explore.js.map +1 -0
- package/dist/core/fleet-context.js +132 -77
- package/dist/core/fleet-context.js.map +1 -1
- package/dist/core/gate/checks.js +201 -0
- package/dist/core/gate/checks.js.map +1 -0
- package/dist/core/gate/partners.js +205 -0
- package/dist/core/gate/partners.js.map +1 -0
- package/dist/core/gate/report.js +47 -0
- package/dist/core/gate/report.js.map +1 -0
- package/dist/core/gate/verification.js +113 -0
- package/dist/core/gate/verification.js.map +1 -0
- package/dist/core/gherkin/digest.js +60 -0
- package/dist/core/gherkin/digest.js.map +1 -0
- package/dist/core/gherkin/emit.js +177 -0
- package/dist/core/gherkin/emit.js.map +1 -0
- package/dist/core/gherkin/read.js +76 -0
- package/dist/core/gherkin/read.js.map +1 -0
- package/dist/core/gherkin/stale.js +234 -0
- package/dist/core/gherkin/stale.js.map +1 -0
- package/dist/core/gherkin/stamp.js +77 -0
- package/dist/core/gherkin/stamp.js.map +1 -0
- package/dist/core/gherkin/steps/catalogue.js +154 -0
- package/dist/core/gherkin/steps/catalogue.js.map +1 -0
- package/dist/core/gherkin/steps/inventory.js +98 -0
- package/dist/core/gherkin/steps/inventory.js.map +1 -0
- package/dist/core/gherkin/steps/phrase.js +72 -0
- package/dist/core/gherkin/steps/phrase.js.map +1 -0
- package/dist/core/glossary/delta.js +106 -0
- package/dist/core/glossary/delta.js.map +1 -0
- package/dist/core/glossary/tree.js +86 -0
- package/dist/core/glossary/tree.js.map +1 -0
- package/dist/core/kernel/concurrency.js.map +1 -0
- package/dist/core/{document-bytes.js → kernel/document-bytes.js} +1 -1
- package/dist/core/kernel/document-bytes.js.map +1 -0
- package/dist/core/kernel/fences.js +42 -0
- package/dist/core/kernel/fences.js.map +1 -0
- package/dist/core/kernel/ids/capability.js +72 -0
- package/dist/core/kernel/ids/capability.js.map +1 -0
- package/dist/core/kernel/ids/dirs.js +33 -0
- package/dist/core/kernel/ids/dirs.js.map +1 -0
- package/dist/core/kernel/ids/feature.js +75 -0
- package/dist/core/kernel/ids/feature.js.map +1 -0
- package/dist/core/kernel/ids/service.js +156 -0
- package/dist/core/kernel/ids/service.js.map +1 -0
- package/dist/core/kernel/ids/subsystem.js +63 -0
- package/dist/core/kernel/ids/subsystem.js.map +1 -0
- package/dist/core/{path-safety.js → kernel/path-safety.js} +18 -4
- package/dist/core/kernel/path-safety.js.map +1 -0
- package/dist/core/kernel/records.js.map +1 -0
- package/dist/core/links/backlinks.js +68 -0
- package/dist/core/links/backlinks.js.map +1 -0
- package/dist/core/links/case.js +74 -0
- package/dist/core/links/case.js.map +1 -0
- package/dist/core/links/corpus.js +104 -0
- package/dist/core/links/corpus.js.map +1 -0
- package/dist/core/links/findings.js +110 -0
- package/dist/core/links/findings.js.map +1 -0
- package/dist/core/links/parse.js +236 -0
- package/dist/core/links/parse.js.map +1 -0
- package/dist/core/mcp/argv.js +111 -0
- package/dist/core/mcp/argv.js.map +1 -0
- package/dist/core/mcp/author-tools.js +72 -0
- package/dist/core/mcp/author-tools.js.map +1 -0
- package/dist/core/mcp/framing.js +46 -0
- package/dist/core/mcp/framing.js.map +1 -0
- package/dist/core/mcp/protocol.js +280 -0
- package/dist/core/mcp/protocol.js.map +1 -0
- package/dist/core/mcp/tools.js +314 -0
- package/dist/core/mcp/tools.js.map +1 -0
- package/dist/core/obligations/obligations.js +106 -0
- package/dist/core/obligations/obligations.js.map +1 -0
- package/dist/core/openapi/baseline/gate.js +166 -0
- package/dist/core/openapi/baseline/gate.js.map +1 -0
- package/dist/core/openapi/baseline/plan.js +112 -0
- package/dist/core/openapi/baseline/plan.js.map +1 -0
- package/dist/core/openapi/baseline/record.js +175 -0
- package/dist/core/openapi/baseline/record.js.map +1 -0
- package/dist/core/openapi/depth.js +122 -0
- package/dist/core/openapi/depth.js.map +1 -0
- package/dist/core/openapi/digest.js +189 -0
- package/dist/core/openapi/digest.js.map +1 -0
- package/dist/core/{openapi.js → openapi/doc.js} +44 -106
- package/dist/core/openapi/doc.js.map +1 -0
- package/dist/core/openapi/merge/components.js +234 -0
- package/dist/core/openapi/merge/components.js.map +1 -0
- package/dist/core/openapi/merge/error.js +26 -0
- package/dist/core/openapi/merge/error.js.map +1 -0
- package/dist/core/openapi/merge/markers.js +208 -0
- package/dist/core/openapi/merge/markers.js.map +1 -0
- package/dist/core/openapi/merge/merge.js +256 -0
- package/dist/core/openapi/merge/merge.js.map +1 -0
- package/dist/core/openapi/merge/pin.js +128 -0
- package/dist/core/openapi/merge/pin.js.map +1 -0
- package/dist/core/openspec/decide/capabilities.js +244 -0
- package/dist/core/openspec/decide/capabilities.js.map +1 -0
- package/dist/core/openspec/decide/dispositions.js +63 -0
- package/dist/core/openspec/decide/dispositions.js.map +1 -0
- package/dist/core/openspec/decide/renames.js +179 -0
- package/dist/core/openspec/decide/renames.js.map +1 -0
- package/dist/core/openspec/decide/verdict.js +84 -0
- package/dist/core/openspec/decide/verdict.js.map +1 -0
- package/dist/core/openspec/inventory.js +221 -0
- package/dist/core/openspec/inventory.js.map +1 -0
- package/dist/core/openspec/model/mapping.js +76 -0
- package/dist/core/openspec/model/mapping.js.map +1 -0
- package/dist/core/openspec/model/model.js +65 -0
- package/dist/core/openspec/model/model.js.map +1 -0
- package/dist/core/openspec/scan/changes.js +235 -0
- package/dist/core/openspec/scan/changes.js.map +1 -0
- package/dist/core/openspec/scan/config.js +200 -0
- package/dist/core/openspec/scan/config.js.map +1 -0
- package/dist/core/openspec/scan/shape.js +260 -0
- package/dist/core/openspec/scan/shape.js.map +1 -0
- package/dist/core/openspec/scan/walk.js +133 -0
- package/dist/core/openspec/scan/walk.js.map +1 -0
- package/dist/core/openspec/scan/workspace.js +71 -0
- package/dist/core/openspec/scan/workspace.js.map +1 -0
- package/dist/core/owners/codeowners.js +141 -0
- package/dist/core/owners/codeowners.js.map +1 -0
- package/dist/core/pack/features.js +120 -0
- package/dist/core/pack/features.js.map +1 -0
- package/dist/core/pack/joins.js +112 -0
- package/dist/core/pack/joins.js.map +1 -0
- package/dist/core/pack/living.js +118 -0
- package/dist/core/pack/living.js.map +1 -0
- package/dist/core/pack/pack.js +76 -0
- package/dist/core/pack/pack.js.map +1 -0
- package/dist/core/permissions/permissions.js +87 -0
- package/dist/core/permissions/permissions.js.map +1 -0
- package/dist/core/projection/api.js +84 -0
- package/dist/core/projection/api.js.map +1 -0
- package/dist/core/projection/arch-slice.js +63 -0
- package/dist/core/projection/arch-slice.js.map +1 -0
- package/dist/core/projection/events.js +34 -0
- package/dist/core/projection/events.js.map +1 -0
- package/dist/core/provenance/findings.js +208 -0
- package/dist/core/provenance/findings.js.map +1 -0
- package/dist/core/provenance/git.js +225 -0
- package/dist/core/provenance/git.js.map +1 -0
- package/dist/core/provenance/gitq/moves.js +182 -0
- package/dist/core/provenance/gitq/moves.js.map +1 -0
- package/dist/core/provenance/gitq/vouched-ancestor.js +179 -0
- package/dist/core/provenance/gitq/vouched-ancestor.js.map +1 -0
- package/dist/core/provenance/sample/scope.js +238 -0
- package/dist/core/provenance/sample/scope.js.map +1 -0
- package/dist/core/provenance/sample/sections.js +61 -0
- package/dist/core/provenance/sample/sections.js.map +1 -0
- package/dist/core/provenance/sources.js +263 -0
- package/dist/core/provenance/sources.js.map +1 -0
- package/dist/core/provenance/stamp.js +232 -0
- package/dist/core/provenance/stamp.js.map +1 -0
- package/dist/core/provenance/walk.js +157 -0
- package/dist/core/provenance/walk.js.map +1 -0
- package/dist/core/repo/authored/paths.js +192 -0
- package/dist/core/repo/authored/paths.js.map +1 -0
- package/dist/core/repo/entries.js +126 -0
- package/dist/core/repo/entries.js.map +1 -0
- package/dist/core/repo/paths.js +236 -0
- package/dist/core/repo/paths.js.map +1 -0
- package/dist/core/repo/repo.js +248 -0
- package/dist/core/repo/repo.js.map +1 -0
- package/dist/core/repo/service-target.js +153 -0
- package/dist/core/repo/service-target.js.map +1 -0
- package/dist/core/repo/state.js +58 -0
- package/dist/core/repo/state.js.map +1 -0
- package/dist/core/repo/tree/find.js +52 -0
- package/dist/core/repo/tree/find.js.map +1 -0
- package/dist/core/repo/tree/fs.js +64 -0
- package/dist/core/repo/tree/fs.js.map +1 -0
- package/dist/core/repo/tree/marker.js +61 -0
- package/dist/core/repo/tree/marker.js.map +1 -0
- package/dist/core/repo/tree/views.js +172 -0
- package/dist/core/repo/tree/views.js.map +1 -0
- package/dist/core/repo/tree/walk.js +234 -0
- package/dist/core/repo/tree/walk.js.map +1 -0
- package/dist/core/results.js +99 -2
- package/dist/core/results.js.map +1 -1
- package/dist/core/scaffold/landscape.js +131 -0
- package/dist/core/scaffold/landscape.js.map +1 -0
- package/dist/core/scaffold/readme.js +84 -0
- package/dist/core/scaffold/readme.js.map +1 -0
- package/dist/core/staging/commit.js +241 -0
- package/dist/core/staging/commit.js.map +1 -0
- package/dist/core/staging/interrupted.js +96 -0
- package/dist/core/staging/interrupted.js.map +1 -0
- package/dist/core/staging/lock.js +206 -0
- package/dist/core/staging/lock.js.map +1 -0
- package/dist/core/staging/recovery/finding.js +35 -0
- package/dist/core/staging/recovery/finding.js.map +1 -0
- package/dist/core/staging/recovery/intent.js +98 -0
- package/dist/core/staging/recovery/intent.js.map +1 -0
- package/dist/core/staging/recovery/recover.js +126 -0
- package/dist/core/staging/recovery/recover.js.map +1 -0
- package/dist/core/staging/recovery/residue.js +62 -0
- package/dist/core/staging/recovery/residue.js.map +1 -0
- package/dist/core/staging/snapshot.js +202 -0
- package/dist/core/staging/snapshot.js.map +1 -0
- package/dist/core/staging/txn/forward.js +171 -0
- package/dist/core/staging/txn/forward.js.map +1 -0
- package/dist/core/staging/txn/journal.js +139 -0
- package/dist/core/staging/txn/journal.js.map +1 -0
- package/dist/core/staging/txn/transaction.js +75 -0
- package/dist/core/staging/txn/transaction.js.map +1 -0
- package/dist/core/staging/writes.js +113 -0
- package/dist/core/staging/writes.js.map +1 -0
- package/dist/core/status/actions/execution.js +49 -0
- package/dist/core/status/actions/execution.js.map +1 -0
- package/dist/core/status/contracts.js +102 -0
- package/dist/core/status/contracts.js.map +1 -0
- package/dist/core/status/feature/artifacts.js +171 -0
- package/dist/core/status/feature/artifacts.js.map +1 -0
- package/dist/core/status/feature/feature.js +207 -0
- package/dist/core/status/feature/feature.js.map +1 -0
- package/dist/core/status/feature/next.js +150 -0
- package/dist/core/status/feature/next.js.map +1 -0
- package/dist/core/status/feature/state.js +21 -0
- package/dist/core/status/feature/state.js.map +1 -0
- package/dist/core/status/fleet/fleet.js +187 -0
- package/dist/core/status/fleet/fleet.js.map +1 -0
- package/dist/core/status/fleet/next.js +207 -0
- package/dist/core/status/fleet/next.js.map +1 -0
- package/dist/core/status/interrupted.js +97 -0
- package/dist/core/status/interrupted.js.map +1 -0
- package/dist/core/status/report.js +31 -0
- package/dist/core/status/report.js.map +1 -0
- package/dist/core/status/scan.js +41 -0
- package/dist/core/status/scan.js.map +1 -0
- package/dist/core/status/verification.js +201 -0
- package/dist/core/status/verification.js.map +1 -0
- package/dist/core/usecases/capability.js +169 -0
- package/dist/core/usecases/capability.js.map +1 -0
- package/dist/core/usecases/delta/claims.js +116 -0
- package/dist/core/usecases/delta/claims.js.map +1 -0
- package/dist/core/usecases/delta/flows.js +103 -0
- package/dist/core/usecases/delta/flows.js.map +1 -0
- package/dist/core/usecases/delta/overlay.js +235 -0
- package/dist/core/usecases/delta/overlay.js.map +1 -0
- package/dist/core/usecases/fleet.js +137 -0
- package/dist/core/usecases/fleet.js.map +1 -0
- package/dist/core/usecases/operations.js +111 -0
- package/dist/core/usecases/operations.js.map +1 -0
- package/dist/core/usecases/place.js +44 -0
- package/dist/core/usecases/place.js.map +1 -0
- package/dist/core/usecases/touch.js +101 -0
- package/dist/core/usecases/touch.js.map +1 -0
- package/dist/core/verify/answers.js +173 -0
- package/dist/core/verify/answers.js.map +1 -0
- package/dist/core/verify/build.js +150 -0
- package/dist/core/verify/build.js.map +1 -0
- package/dist/core/verify/checklist.js +201 -0
- package/dist/core/verify/checklist.js.map +1 -0
- package/dist/core/verify/claims/identity.js +61 -0
- package/dist/core/verify/claims/identity.js.map +1 -0
- package/dist/core/verify/cross/diff.js +97 -0
- package/dist/core/verify/cross/diff.js.map +1 -0
- package/dist/core/verify/evidence/contract.js +169 -0
- package/dist/core/verify/evidence/contract.js.map +1 -0
- package/dist/core/verify/file.js +257 -0
- package/dist/core/verify/file.js.map +1 -0
- package/dist/core/verify/pins/lint.js +157 -0
- package/dist/core/verify/pins/lint.js.map +1 -0
- package/dist/core/verify/pins/pin.js +95 -0
- package/dist/core/verify/pins/pin.js.map +1 -0
- package/dist/core/verify/record.js +142 -0
- package/dist/core/verify/record.js.map +1 -0
- package/dist/core/verify/store/commit.js +89 -0
- package/dist/core/verify/store/commit.js.map +1 -0
- package/dist/core/verify/store/render.js +57 -0
- package/dist/core/verify/store/render.js.map +1 -0
- package/dist/core/vocabulary/health.js +91 -0
- package/dist/core/vocabulary/health.js.map +1 -0
- package/dist/core/vocabulary/issue.js +55 -0
- package/dist/core/vocabulary/issue.js.map +1 -0
- package/dist/core/vocabulary/maturity.js +145 -0
- package/dist/core/vocabulary/maturity.js.map +1 -0
- package/dist/core/{report.js → vocabulary/report.js} +30 -2
- package/dist/core/vocabulary/report.js.map +1 -0
- package/dist/core/vocabulary/steps.js +266 -0
- package/dist/core/vocabulary/steps.js.map +1 -0
- package/dist/core/workspace/discover.js +239 -0
- package/dist/core/workspace/discover.js.map +1 -0
- package/dist/core/workspace/render.js +52 -0
- package/dist/core/workspace/render.js.map +1 -0
- package/examples/README.md +189 -0
- package/examples/docs/architecture/adrs/0001-transactional-outbox.md +33 -0
- package/examples/docs/architecture/capabilities.yaml +31 -0
- package/examples/docs/architecture/landscape.likec4 +280 -0
- package/examples/docs/architecture/obligations.yaml +23 -0
- package/examples/docs/architecture/permissions.yaml +50 -0
- package/examples/docs/architecture/subsystems.likec4 +11 -0
- package/examples/docs/architecture/usecases/order-notification.likec4 +65 -0
- package/examples/docs/capabilities/checkout/spec.md +55 -0
- package/examples/docs/capabilities/identity/tokens/spec.md +42 -0
- package/examples/docs/capabilities/order-notifications/spec.md +43 -0
- package/examples/docs/features/FEAT-101-payment-splitting/adrs/0001-dedicated-split-service.md +18 -0
- package/examples/docs/features/FEAT-101-payment-splitting/delta.likec4 +94 -0
- package/examples/docs/features/FEAT-101-payment-splitting/intent.md +44 -0
- package/examples/docs/features/FEAT-101-payment-splitting/specs/checkout-web/spec.md +21 -0
- package/examples/docs/features/FEAT-101-payment-splitting/specs/payment-service/asyncapi.yaml +129 -0
- package/examples/docs/features/FEAT-101-payment-splitting/specs/payment-service/spec.md +36 -0
- package/examples/docs/features/FEAT-101-payment-splitting/specs/payment-split-service/arch.spec.md +40 -0
- package/examples/docs/features/FEAT-101-payment-splitting/specs/payment-split-service/openapi.yaml +116 -0
- package/examples/docs/features/FEAT-101-payment-splitting/specs/payment-split-service/spec.md +55 -0
- package/examples/docs/features/FEAT-112-retire-order-v1/intent.md +35 -0
- package/examples/docs/features/FEAT-112-retire-order-v1/specs/order-service/openapi.yaml +28 -0
- package/examples/docs/features/FEAT-112-retire-order-v1/specs/order-service/spec.md +17 -0
- package/examples/docs/features/archive/FEAT-088-refunds/.loam-before/files/architecture/landscape.likec4 +253 -0
- package/examples/docs/features/archive/FEAT-088-refunds/.loam-before/files/services/order-service/spec.md +121 -0
- package/examples/docs/features/archive/FEAT-088-refunds/.loam-before/files/services/payment-service/openapi.yaml +111 -0
- package/examples/docs/features/archive/FEAT-088-refunds/.loam-before/files/services/payment-service/spec.md +58 -0
- package/examples/docs/features/archive/FEAT-088-refunds/.loam-before/manifest.json +32 -0
- package/examples/docs/features/archive/FEAT-088-refunds/adrs/0001-refunds-stay-in-payment-service.md +26 -0
- package/examples/docs/features/archive/FEAT-088-refunds/delta.likec4 +28 -0
- package/examples/docs/features/archive/FEAT-088-refunds/intent.md +26 -0
- package/examples/docs/features/archive/FEAT-088-refunds/specs/order-service/spec.md +37 -0
- package/examples/docs/features/archive/FEAT-088-refunds/specs/payment-service/openapi.yaml +70 -0
- package/examples/docs/features/archive/FEAT-088-refunds/specs/payment-service/spec.md +23 -0
- package/examples/docs/features/archive/FEAT-088-refunds/verification.yaml +76 -0
- package/examples/docs/features/archive/FEAT-120-refund-notification/.loam-before/files/architecture/landscape.likec4 +275 -0
- package/examples/docs/features/archive/FEAT-120-refund-notification/.loam-before/files/services/payment-service/asyncapi.yaml +96 -0
- package/examples/docs/features/archive/FEAT-120-refund-notification/.loam-before/files/services/payment-service/spec.md +82 -0
- package/examples/docs/features/archive/FEAT-120-refund-notification/.loam-before/files/services/platform/notification-service/asyncapi.yaml +164 -0
- package/examples/docs/features/archive/FEAT-120-refund-notification/.loam-before/files/services/platform/notification-service/spec.md +91 -0
- package/examples/docs/features/archive/FEAT-120-refund-notification/.loam-before/manifest.json +46 -0
- package/examples/docs/features/archive/FEAT-120-refund-notification/delta.likec4 +36 -0
- package/examples/docs/features/archive/FEAT-120-refund-notification/intent.md +22 -0
- package/examples/docs/features/archive/FEAT-120-refund-notification/scenario-report.json +10 -0
- package/examples/docs/features/archive/FEAT-120-refund-notification/specs/notification-service/asyncapi.yaml +208 -0
- package/examples/docs/features/archive/FEAT-120-refund-notification/specs/notification-service/spec.md +24 -0
- package/examples/docs/features/archive/FEAT-120-refund-notification/specs/payment-service/asyncapi.yaml +137 -0
- package/examples/docs/features/archive/FEAT-120-refund-notification/specs/payment-service/spec.md +31 -0
- package/examples/docs/features/archive/FEAT-120-refund-notification/verification.yaml +84 -0
- package/examples/docs/glossary/order.md +15 -0
- package/examples/docs/glossary/payments/authorization.md +14 -0
- package/examples/docs/likec4.config.json +9 -0
- package/examples/docs/loam.json +3 -0
- package/examples/docs/services/checkout-web/model.likec4 +109 -0
- package/examples/docs/services/checkout-web/spec.md +91 -0
- package/examples/docs/services/checkout-web/ui/pages/checkout.page.yaml +40 -0
- package/examples/docs/services/checkout-web/ui/pages/order-status.page.yaml +24 -0
- package/examples/docs/services/checkout-web/ui/pages/split-payment.page.yaml +26 -0
- package/examples/docs/services/order-service/adrs/0001-orchestrated-order-lifecycle.md +52 -0
- package/examples/docs/services/order-service/arch.spec.md +127 -0
- package/examples/docs/services/order-service/asyncapi.yaml +108 -0
- package/examples/docs/services/order-service/health.yaml +57 -0
- package/examples/docs/services/order-service/model.likec4 +138 -0
- package/examples/docs/services/order-service/openapi.yaml +257 -0
- package/examples/docs/services/order-service/runbook.md +62 -0
- package/examples/docs/services/order-service/spec.md +134 -0
- package/examples/docs/services/payment-service/adrs/0001-payments-outbox.md +18 -0
- package/examples/docs/services/payment-service/adrs/0002-idempotency-keys-on-authorization.md +27 -0
- package/examples/docs/services/payment-service/arch.spec.md +73 -0
- package/examples/docs/services/payment-service/asyncapi.yaml +130 -0
- package/examples/docs/services/payment-service/health.yaml +39 -0
- package/examples/docs/services/payment-service/model.likec4 +67 -0
- package/examples/docs/services/payment-service/openapi.yaml +151 -0
- package/examples/docs/services/payment-service/runbook.md +37 -0
- package/examples/docs/services/payment-service/spec.md +88 -0
- package/examples/docs/services/platform/identity-service/adrs/0001-token-introspection-over-local-validation.md +60 -0
- package/examples/docs/services/platform/identity-service/arch.spec.md +117 -0
- package/examples/docs/services/platform/identity-service/health.yaml +70 -0
- package/examples/docs/services/platform/identity-service/model.likec4 +83 -0
- package/examples/docs/services/platform/identity-service/openapi.yaml +197 -0
- package/examples/docs/services/platform/identity-service/runbook.md +95 -0
- package/examples/docs/services/platform/identity-service/spec.md +119 -0
- package/examples/docs/services/platform/notification-service/asyncapi.yaml +199 -0
- package/examples/docs/services/platform/notification-service/health.yaml +35 -0
- package/examples/docs/services/platform/notification-service/model.likec4 +118 -0
- package/examples/docs/services/platform/notification-service/runbook.md +48 -0
- package/examples/docs/services/platform/notification-service/spec.md +112 -0
- package/examples/docs/services/platform/subsystem.yaml +2 -0
- package/examples/fleet.yaml +57 -0
- package/package.json +19 -6
- package/dist/commands/adopt.js +0 -240
- package/dist/commands/adopt.js.map +0 -1
- package/dist/commands/archive.js +0 -1313
- package/dist/commands/archive.js.map +0 -1
- package/dist/commands/delta.js +0 -388
- package/dist/commands/delta.js.map +0 -1
- package/dist/commands/docs-repo-gate.js.map +0 -1
- package/dist/commands/format.js +0 -17
- package/dist/commands/format.js.map +0 -1
- package/dist/commands/gherkin.js +0 -309
- package/dist/commands/gherkin.js.map +0 -1
- package/dist/commands/init.js.map +0 -1
- package/dist/commands/list.js +0 -291
- package/dist/commands/list.js.map +0 -1
- package/dist/commands/migrate-openspec.js +0 -950
- package/dist/commands/migrate-openspec.js.map +0 -1
- package/dist/commands/new.js +0 -413
- package/dist/commands/new.js.map +0 -1
- package/dist/commands/rebase.js +0 -356
- package/dist/commands/rebase.js.map +0 -1
- package/dist/commands/show.js.map +0 -1
- package/dist/commands/status.js +0 -254
- package/dist/commands/status.js.map +0 -1
- package/dist/commands/unarchive.js.map +0 -1
- package/dist/commands/validate.js +0 -1591
- package/dist/commands/validate.js.map +0 -1
- package/dist/commands/verify.js +0 -775
- package/dist/commands/verify.js.map +0 -1
- package/dist/commands/vouch.js +0 -343
- package/dist/commands/vouch.js.map +0 -1
- package/dist/core/agent.js +0 -2065
- package/dist/core/agent.js.map +0 -1
- package/dist/core/agents-stamp.js +0 -68
- package/dist/core/agents-stamp.js.map +0 -1
- package/dist/core/arch.js +0 -130
- package/dist/core/arch.js.map +0 -1
- package/dist/core/brief.js +0 -733
- package/dist/core/brief.js.map +0 -1
- package/dist/core/coherence.js +0 -577
- package/dist/core/coherence.js.map +0 -1
- package/dist/core/concurrency.js.map +0 -1
- package/dist/core/config.js.map +0 -1
- package/dist/core/delta.js +0 -476
- package/dist/core/delta.js.map +0 -1
- package/dist/core/dependencies.js +0 -356
- package/dist/core/dependencies.js.map +0 -1
- package/dist/core/doctor.js +0 -559
- package/dist/core/doctor.js.map +0 -1
- package/dist/core/document-bytes.js.map +0 -1
- package/dist/core/explore.js.map +0 -1
- package/dist/core/frontmatter.js.map +0 -1
- package/dist/core/gherkin.js +0 -463
- package/dist/core/gherkin.js.map +0 -1
- package/dist/core/health.js +0 -67
- package/dist/core/health.js.map +0 -1
- package/dist/core/ids.js +0 -122
- package/dist/core/ids.js.map +0 -1
- package/dist/core/issue.js +0 -9
- package/dist/core/issue.js.map +0 -1
- package/dist/core/json.js +0 -74
- package/dist/core/json.js.map +0 -1
- package/dist/core/likec4.js +0 -479
- package/dist/core/likec4.js.map +0 -1
- package/dist/core/maturity.js +0 -66
- package/dist/core/maturity.js.map +0 -1
- package/dist/core/openapi-merge.js +0 -552
- package/dist/core/openapi-merge.js.map +0 -1
- package/dist/core/openapi.js.map +0 -1
- package/dist/core/openspec-inventory.js +0 -1440
- package/dist/core/openspec-inventory.js.map +0 -1
- package/dist/core/path-safety.js.map +0 -1
- package/dist/core/provenance.js +0 -720
- package/dist/core/provenance.js.map +0 -1
- package/dist/core/records.js.map +0 -1
- package/dist/core/repo.js +0 -436
- package/dist/core/repo.js.map +0 -1
- package/dist/core/report.js.map +0 -1
- package/dist/core/spec.js +0 -481
- package/dist/core/spec.js.map +0 -1
- package/dist/core/staging.js +0 -890
- package/dist/core/staging.js.map +0 -1
- package/dist/core/status.js +0 -1104
- package/dist/core/status.js.map +0 -1
- package/dist/core/steps.js +0 -72
- package/dist/core/steps.js.map +0 -1
- package/dist/core/verify.js +0 -807
- package/dist/core/verify.js.map +0 -1
- package/dist/core/version.js.map +0 -1
- /package/dist/core/{concurrency.js → kernel/concurrency.js} +0 -0
- /package/dist/core/{records.js → kernel/records.js} +0 -0
|
@@ -0,0 +1,257 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `verification.yaml` read back, and refused when it cannot be trusted.
|
|
3
|
+
*
|
|
4
|
+
* Reading is deliberately not the inverse of rendering. A record loam wrote is
|
|
5
|
+
* re-graded on the way in — unreadable YAML, a shape that is not a
|
|
6
|
+
* verification, a `summary` that disagrees with the claims below it — because
|
|
7
|
+
* the file is data meant to survive without loam, and anything may have edited
|
|
8
|
+
* it since. The render lives with the writer it feeds (`store/render.ts`
|
|
9
|
+
* produces exactly the bytes `store/commit.ts` swaps into place; races and
|
|
10
|
+
* kills mid-write live there too), and the lock that serialises writers is
|
|
11
|
+
* taken by `commands/verify/verify.ts` before this module's authoritative
|
|
12
|
+
* read. Nothing here touches the filesystem except to read.
|
|
13
|
+
*/
|
|
14
|
+
import { lstat, readFile } from "node:fs/promises";
|
|
15
|
+
import { parse } from "yaml";
|
|
16
|
+
import { isRecord } from "../kernel/records.js";
|
|
17
|
+
import { VERDICTS } from "./answers.js";
|
|
18
|
+
import { isEvidencePinList } from "./pins/pin.js";
|
|
19
|
+
import { tallyAnswers, verificationPath } from "./record.js";
|
|
20
|
+
export async function readVerificationState(featureDir) {
|
|
21
|
+
const path = verificationPath(featureDir);
|
|
22
|
+
let raw;
|
|
23
|
+
try {
|
|
24
|
+
raw = await readFile(path);
|
|
25
|
+
}
|
|
26
|
+
catch (err) {
|
|
27
|
+
if (err.code === "ENOENT") {
|
|
28
|
+
// ENOENT is "absent" — except from a symlink whose target is gone, which
|
|
29
|
+
// `existsSync` used to fold into the same answer. That mislabel was a
|
|
30
|
+
// trap: the read said "no record" while the commit's exclusive link(2)
|
|
31
|
+
// saw the link itself and refused EEXIST, so every `--record` answered
|
|
32
|
+
// `record-raced` with advice (re-run) that could never work. The lstat
|
|
33
|
+
// must CONFIRM the link before the message asserts one: a real file can
|
|
34
|
+
// also appear between the two calls, and telling its owner to "remove
|
|
35
|
+
// the link" would delete a valid record.
|
|
36
|
+
let entry;
|
|
37
|
+
try {
|
|
38
|
+
entry = await lstat(path);
|
|
39
|
+
}
|
|
40
|
+
catch {
|
|
41
|
+
return { state: "absent" };
|
|
42
|
+
}
|
|
43
|
+
if (entry.isSymbolicLink()) {
|
|
44
|
+
return {
|
|
45
|
+
state: "unreadable",
|
|
46
|
+
reason: "it is a symlink whose target does not exist — replace the link with a real record, or remove it",
|
|
47
|
+
};
|
|
48
|
+
}
|
|
49
|
+
return {
|
|
50
|
+
state: "unreadable",
|
|
51
|
+
reason: "it appeared while being read — another writer is landing; re-run",
|
|
52
|
+
};
|
|
53
|
+
}
|
|
54
|
+
return { state: "unreadable", reason: err instanceof Error ? err.message : String(err) };
|
|
55
|
+
}
|
|
56
|
+
let doc;
|
|
57
|
+
try {
|
|
58
|
+
doc = parse(raw.toString("utf8"));
|
|
59
|
+
}
|
|
60
|
+
catch (err) {
|
|
61
|
+
return { state: "unreadable", reason: yamlReason(err) };
|
|
62
|
+
}
|
|
63
|
+
const verification = asVerification(doc);
|
|
64
|
+
if (verification === null) {
|
|
65
|
+
return {
|
|
66
|
+
state: "unreadable",
|
|
67
|
+
reason: "it parses as YAML but does not have a verification record's shape (feature, recorded, checklist, summary and claims)",
|
|
68
|
+
};
|
|
69
|
+
}
|
|
70
|
+
const miscount = summaryDisagreement(verification);
|
|
71
|
+
if (miscount === null)
|
|
72
|
+
return { state: "ok", verification, raw };
|
|
73
|
+
return {
|
|
74
|
+
state: "unreadable",
|
|
75
|
+
// Spelled twice on purpose: the field is what a caller branches on, the
|
|
76
|
+
// prose is what a terminal prints and what somebody greps for.
|
|
77
|
+
code: "verify.record-miscounted",
|
|
78
|
+
reason: `verify.record-miscounted: ${miscount}`,
|
|
79
|
+
};
|
|
80
|
+
}
|
|
81
|
+
/**
|
|
82
|
+
* Does the record's `summary` add up from its own `claims[]`?
|
|
83
|
+
*
|
|
84
|
+
* The summary is what wrote the file; the claims are what it answered. Nothing
|
|
85
|
+
* ever compared them, so every reader that took the shortcut — verify's frozen
|
|
86
|
+
* verdict for a shipped feature, list's column, status's block — reported a
|
|
87
|
+
* record full of unconfirmed claims as fully confirmed. Believing either side
|
|
88
|
+
* over the other would be a guess about which half was tampered with, so a
|
|
89
|
+
* record that contradicts itself is refused like any other file loam cannot
|
|
90
|
+
* use. The code is in the reason: it travels wherever the reason is printed.
|
|
91
|
+
*/
|
|
92
|
+
function summaryDisagreement(v) {
|
|
93
|
+
const t = tallyAnswers(v.claims);
|
|
94
|
+
const unanswered = v.summary.unanswered ?? 0;
|
|
95
|
+
const says = [];
|
|
96
|
+
// Before the arithmetic, because a negative count defeats it: `unanswered:
|
|
97
|
+
// -2` is never something loam wrote, and it is the one value that lets a
|
|
98
|
+
// hand-edited summary balance its own books — every comparison below then
|
|
99
|
+
// agrees, and a record contradicting its claims reported `verified`.
|
|
100
|
+
for (const [field, value] of Object.entries(v.summary)) {
|
|
101
|
+
if (typeof value === "number" && value < 0)
|
|
102
|
+
says.push(`carries a negative count (${field}: ${value})`);
|
|
103
|
+
}
|
|
104
|
+
if (v.summary.confirmed !== t.confirmed) {
|
|
105
|
+
says.push(`says ${v.summary.confirmed} confirmed where claims[] holds ${t.confirmed}`);
|
|
106
|
+
}
|
|
107
|
+
if (v.summary.unconfirmed !== t.unconfirmed) {
|
|
108
|
+
says.push(`says ${v.summary.unconfirmed} unconfirmed where claims[] holds ${t.unconfirmed}`);
|
|
109
|
+
}
|
|
110
|
+
if (v.summary.claims !== v.claims.length + unanswered) {
|
|
111
|
+
says.push(`says ${v.summary.claims} claim(s)${unanswered === 0 ? "" : ` including ${unanswered} unanswered`} where claims[] holds ${v.claims.length}`);
|
|
112
|
+
}
|
|
113
|
+
return says.length === 0
|
|
114
|
+
? null
|
|
115
|
+
: `its summary ${says.join(", ")} — the record contradicts itself, so neither half can be believed`;
|
|
116
|
+
}
|
|
117
|
+
/** A YAML failure with its line, when the parser located one: that line is the fix. */
|
|
118
|
+
function yamlReason(err) {
|
|
119
|
+
const message = err instanceof Error ? err.message.split("\n")[0].trim() : String(err);
|
|
120
|
+
const pos = err?.linePos;
|
|
121
|
+
const line = Array.isArray(pos) ? pos[0]?.line : undefined;
|
|
122
|
+
return line === undefined ? `YAML error: ${message}` : `YAML error at line ${line}: ${message}`;
|
|
123
|
+
}
|
|
124
|
+
/**
|
|
125
|
+
* The record beside a feature, or null when there is none loam can use. Kept
|
|
126
|
+
* for readers that have nothing to say about the difference (`loam list`'s
|
|
127
|
+
* verification column shows one glyph either way); anything that WRITES must
|
|
128
|
+
* use {@link readVerificationState} instead, or it overwrites what it could not
|
|
129
|
+
* read.
|
|
130
|
+
*
|
|
131
|
+
* A record whose summary contradicts its own claims is one of the nulls now, so
|
|
132
|
+
* no reader can print its counts as fact. Reading it as ABSENT is the safe half
|
|
133
|
+
* of the truth and not the whole of it: a reader that can say why should take
|
|
134
|
+
* {@link readVerificationState} and its `code`.
|
|
135
|
+
*/
|
|
136
|
+
export async function readVerification(featureDir) {
|
|
137
|
+
const read = await readVerificationState(featureDir);
|
|
138
|
+
return read.state === "ok" ? read.verification : null;
|
|
139
|
+
}
|
|
140
|
+
/**
|
|
141
|
+
* The shape check `readVerification` stands behind: everything its readers
|
|
142
|
+
* dereference (verify's report/frozen views, list's verification column) must
|
|
143
|
+
* be present and typed, or the cast would manufacture a Verification that
|
|
144
|
+
* crashes them. Deliberately no tighter than the readers need: extra keys pass
|
|
145
|
+
* (a human may annotate), `answered_by` stays optional (absent in records that
|
|
146
|
+
* predate `--results`) and is any string, like `kind` — the readers only ever
|
|
147
|
+
* compare them, and a record written by a newer loam must not read as absent.
|
|
148
|
+
*/
|
|
149
|
+
function asVerification(doc) {
|
|
150
|
+
if (!isRecord(doc))
|
|
151
|
+
return null;
|
|
152
|
+
const summary = doc["summary"];
|
|
153
|
+
if (typeof doc["feature"] !== "string" ||
|
|
154
|
+
typeof doc["recorded"] !== "string" ||
|
|
155
|
+
typeof doc["checklist"] !== "string" ||
|
|
156
|
+
!isRecord(summary) ||
|
|
157
|
+
!isCount(summary["claims"]) ||
|
|
158
|
+
!isCount(summary["confirmed"]) ||
|
|
159
|
+
!isCount(summary["unconfirmed"]) ||
|
|
160
|
+
(summary["unanswered"] !== undefined && !isCount(summary["unanswered"])) ||
|
|
161
|
+
!Array.isArray(doc["claims"])) {
|
|
162
|
+
return null;
|
|
163
|
+
}
|
|
164
|
+
for (const c of doc["claims"]) {
|
|
165
|
+
if (!isRecord(c))
|
|
166
|
+
return null;
|
|
167
|
+
if (typeof c["id"] !== "string" || typeof c["kind"] !== "string" || typeof c["claim"] !== "string")
|
|
168
|
+
return null;
|
|
169
|
+
if (c["subject"] !== undefined && typeof c["subject"] !== "string")
|
|
170
|
+
return null;
|
|
171
|
+
if (typeof c["verdict"] !== "string" || !VERDICTS.includes(c["verdict"]))
|
|
172
|
+
return null;
|
|
173
|
+
// The frozen view iterates evidence per claim — a scalar here would crash it.
|
|
174
|
+
if (!Array.isArray(c["evidence"]) || c["evidence"].some((e) => typeof e !== "string"))
|
|
175
|
+
return null;
|
|
176
|
+
if (c["note"] !== undefined && typeof c["note"] !== "string")
|
|
177
|
+
return null;
|
|
178
|
+
if (c["answered_by"] !== undefined && typeof c["answered_by"] !== "string")
|
|
179
|
+
return null;
|
|
180
|
+
// Absent fine, present whole — the isConsumedContractReport precedent, and
|
|
181
|
+
// the guard body lives with the pin shape it vouches for (pins/pin.ts). A
|
|
182
|
+
// broken pins block makes the whole record unreadable and therefore never
|
|
183
|
+
// overwritten: skipping it instead would read a hand-damaged record as
|
|
184
|
+
// drift-free.
|
|
185
|
+
if (!isEvidencePinList(c["evidence_pins"]))
|
|
186
|
+
return null;
|
|
187
|
+
}
|
|
188
|
+
if (doc["schema"] !== undefined && doc["schema"] !== 2)
|
|
189
|
+
return null;
|
|
190
|
+
if (!isConsumedReport(doc["report"]))
|
|
191
|
+
return null;
|
|
192
|
+
if (!isConsumedContractReport(doc["contractReport"]))
|
|
193
|
+
return null;
|
|
194
|
+
if (doc["attestations"] !== undefined) {
|
|
195
|
+
if (!Array.isArray(doc["attestations"]))
|
|
196
|
+
return null;
|
|
197
|
+
for (const a of doc["attestations"]) {
|
|
198
|
+
if (!isRecord(a))
|
|
199
|
+
return null;
|
|
200
|
+
if (typeof a["service"] !== "string" || typeof a["commit"] !== "string" || typeof a["recorded"] !== "string")
|
|
201
|
+
return null;
|
|
202
|
+
if (!/^[0-9a-f]{40,64}$/i.test(a["commit"]))
|
|
203
|
+
return null;
|
|
204
|
+
if (!Array.isArray(a["claims"]) || a["claims"].some((id) => typeof id !== "string"))
|
|
205
|
+
return null;
|
|
206
|
+
if (!isConsumedReport(a["report"]))
|
|
207
|
+
return null;
|
|
208
|
+
if (!isConsumedContractReport(a["contractReport"]))
|
|
209
|
+
return null;
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
return doc;
|
|
213
|
+
}
|
|
214
|
+
/** Absent is fine — most records answer nothing from a report. Present must be whole: it is printed. */
|
|
215
|
+
function isConsumedReport(v) {
|
|
216
|
+
if (v === undefined)
|
|
217
|
+
return true;
|
|
218
|
+
return (isRecord(v) &&
|
|
219
|
+
typeof v["path"] === "string" &&
|
|
220
|
+
typeof v["digest"] === "string" &&
|
|
221
|
+
typeof v["mtime"] === "string" &&
|
|
222
|
+
isCount(v["scenarios"]));
|
|
223
|
+
}
|
|
224
|
+
/**
|
|
225
|
+
* The contract report's pin, exactly as loose as {@link isConsumedReport}:
|
|
226
|
+
* absent fine, present whole — for the fields the readers dereference.
|
|
227
|
+
* `format` is OPTIONAL here even though loam always writes it: nothing in the
|
|
228
|
+
* readers dereferences it (it is provenance for a person, echoed only inside
|
|
229
|
+
* whole-object payloads), and the generated AGENTS.md teaches the pin as
|
|
230
|
+
* path/sha256/mtime/operation-count — so a hand-written block faithful to that
|
|
231
|
+
* teaching must not make the WHOLE record unreadable over a field no reader
|
|
232
|
+
* uses. When present it must be a string, and any string: a record written by
|
|
233
|
+
* a newer loam that learned a second report format must not read as
|
|
234
|
+
* unreadable here.
|
|
235
|
+
*/
|
|
236
|
+
function isConsumedContractReport(v) {
|
|
237
|
+
if (v === undefined)
|
|
238
|
+
return true;
|
|
239
|
+
return (isRecord(v) &&
|
|
240
|
+
typeof v["path"] === "string" &&
|
|
241
|
+
typeof v["digest"] === "string" &&
|
|
242
|
+
typeof v["mtime"] === "string" &&
|
|
243
|
+
isCount(v["operations"]) &&
|
|
244
|
+
(v["format"] === undefined || typeof v["format"] === "string"));
|
|
245
|
+
}
|
|
246
|
+
/**
|
|
247
|
+
* A summary count: an integer. YAML hands back strings for anything quoted.
|
|
248
|
+
* Negativity is deliberately NOT checked here: a negative count is a summary
|
|
249
|
+
* that disagrees with the claims below it, and `summaryDisagreement` owns that
|
|
250
|
+
* refusal — failing the SHAPE check instead made the message list five keys
|
|
251
|
+
* that were all present, while the miscount branch one call away holds the
|
|
252
|
+
* advice the case actually needs.
|
|
253
|
+
*/
|
|
254
|
+
function isCount(v) {
|
|
255
|
+
return typeof v === "number" && Number.isInteger(v);
|
|
256
|
+
}
|
|
257
|
+
//# sourceMappingURL=file.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"file.js","sourceRoot":"","sources":["../../../src/core/verify/file.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * `verification.yaml` read back, and refused when it cannot be trusted.\n *\n * Reading is deliberately not the inverse of rendering. A record loam wrote is\n * re-graded on the way in — unreadable YAML, a shape that is not a\n * verification, a `summary` that disagrees with the claims below it — because\n * the file is data meant to survive without loam, and anything may have edited\n * it since. The render lives with the writer it feeds (`store/render.ts`\n * produces exactly the bytes `store/commit.ts` swaps into place; races and\n * kills mid-write live there too), and the lock that serialises writers is\n * taken by `commands/verify/verify.ts` before this module's authoritative\n * read. Nothing here touches the filesystem except to read.\n */\nimport { lstat, readFile } from \"node:fs/promises\";\nimport { parse } from \"yaml\";\nimport { isRecord } from \"../kernel/records.js\";\nimport { VERDICTS } from \"./answers.js\";\nimport { isEvidencePinList } from \"./pins/pin.js\";\nimport { tallyAnswers, verificationPath, type Verification } from \"./record.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 * `reason` names the YAML line when the parser gave one — the file is the\n * thing to fix. `code` is set for the one unreadable that parses perfectly: a\n * record whose `summary` contradicts its own `claims[]`. That lives here\n * rather than in a state of its own so every reader which already refuses an\n * unreadable record refuses this one too, without a line of new code.\n */\n | { state: \"unreadable\"; reason: string; code?: \"verify.record-miscounted\" }\n /**\n * `raw` is the exact bytes `verification` was parsed from. A writer merges\n * over the PARSE, but commits over the BYTES: `commitVerification` compares\n * these against the file immediately before its swap, so an edit that landed\n * between this read and that write — an editor, a writer that ignored the\n * docs lock — is refused and preserved instead of silently buried.\n */\n | { state: \"ok\"; verification: Verification; raw: Buffer };\n\nexport async function readVerificationState(featureDir: string): Promise<VerificationRead> {\n const path = verificationPath(featureDir);\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 {\n state: \"unreadable\",\n reason: \"it is a symlink whose target does not exist — replace the link with a real record, or remove it\",\n };\n }\n return {\n state: \"unreadable\",\n reason: \"it appeared while being read — another writer is landing; re-run\",\n };\n }\n return { state: \"unreadable\", reason: err instanceof Error ? err.message : String(err) };\n }\n let doc: unknown;\n try {\n doc = parse(raw.toString(\"utf8\"));\n } catch (err) {\n return { state: \"unreadable\", reason: yamlReason(err) };\n }\n const verification = asVerification(doc);\n if (verification === null) {\n return {\n state: \"unreadable\",\n reason:\n \"it parses as YAML but does not have a verification record's shape (feature, recorded, checklist, summary and claims)\",\n };\n }\n const miscount = summaryDisagreement(verification);\n if (miscount === null) return { state: \"ok\", verification, raw };\n return {\n state: \"unreadable\",\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 code: \"verify.record-miscounted\",\n reason: `verify.record-miscounted: ${miscount}`,\n };\n}\n\n/**\n * Does the record's `summary` add up from its own `claims[]`?\n *\n * The summary is what wrote the file; the claims are what it answered. Nothing\n * ever compared them, so every reader that took the shortcut — verify's frozen\n * verdict for a shipped feature, list's column, status's block — reported a\n * record full of unconfirmed claims as fully confirmed. Believing either side\n * over the other would be a guess about which half was tampered with, so a\n * record that contradicts itself is refused like any other file loam cannot\n * use. The code is in the reason: it travels wherever the reason is printed.\n */\nfunction summaryDisagreement(v: Verification): string | null {\n const t = tallyAnswers(v.claims);\n const unanswered = v.summary.unanswered ?? 0;\n const says: string[] = [];\n // Before the arithmetic, because a negative count defeats it: `unanswered:\n // -2` is never something loam wrote, and it is the one value that lets a\n // hand-edited summary balance its own books — every comparison below then\n // agrees, and a record contradicting its claims reported `verified`.\n for (const [field, value] of Object.entries(v.summary)) {\n if (typeof value === \"number\" && value < 0) says.push(`carries a negative count (${field}: ${value})`);\n }\n if (v.summary.confirmed !== t.confirmed) {\n says.push(`says ${v.summary.confirmed} confirmed where claims[] holds ${t.confirmed}`);\n }\n if (v.summary.unconfirmed !== t.unconfirmed) {\n says.push(`says ${v.summary.unconfirmed} unconfirmed where claims[] holds ${t.unconfirmed}`);\n }\n if (v.summary.claims !== v.claims.length + unanswered) {\n says.push(\n `says ${v.summary.claims} claim(s)${unanswered === 0 ? \"\" : ` including ${unanswered} unanswered`} where claims[] holds ${v.claims.length}`,\n );\n }\n return says.length === 0\n ? null\n : `its summary ${says.join(\", \")} — the record contradicts itself, so neither half can be believed`;\n}\n\n/** 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\n/**\n * The record beside a feature, or null when there is none loam can use. Kept\n * for readers that have nothing to say about the difference (`loam list`'s\n * verification column shows one glyph either way); anything that WRITES must\n * use {@link readVerificationState} instead, or it overwrites what it could not\n * read.\n *\n * A record whose summary contradicts its own claims is one of the nulls now, so\n * no reader can print its counts as fact. Reading it as ABSENT is the safe half\n * of the truth and not the whole of it: a reader that can say why should take\n * {@link readVerificationState} and its `code`.\n */\nexport async function readVerification(featureDir: string): Promise<Verification | null> {\n const read = await readVerificationState(featureDir);\n return read.state === \"ok\" ? read.verification : null;\n}\n\n/**\n * The shape check `readVerification` stands behind: everything its readers\n * dereference (verify's report/frozen views, list's verification column) must\n * be present and typed, or the cast would manufacture a Verification that\n * crashes them. Deliberately no tighter than the readers need: extra keys pass\n * (a human may annotate), `answered_by` stays optional (absent in records that\n * predate `--results`) and is any string, like `kind` — the readers only ever\n * compare them, and a record written by a newer loam must not read as absent.\n */\nfunction asVerification(doc: unknown): Verification | null {\n if (!isRecord(doc)) return null;\n const summary = doc[\"summary\"];\n if (\n typeof doc[\"feature\"] !== \"string\" ||\n typeof doc[\"recorded\"] !== \"string\" ||\n typeof doc[\"checklist\"] !== \"string\" ||\n !isRecord(summary) ||\n !isCount(summary[\"claims\"]) ||\n !isCount(summary[\"confirmed\"]) ||\n !isCount(summary[\"unconfirmed\"]) ||\n (summary[\"unanswered\"] !== undefined && !isCount(summary[\"unanswered\"])) ||\n !Array.isArray(doc[\"claims\"])\n ) {\n return null;\n }\n for (const c of doc[\"claims\"]) {\n if (!isRecord(c)) return null;\n if (typeof c[\"id\"] !== \"string\" || typeof c[\"kind\"] !== \"string\" || typeof c[\"claim\"] !== \"string\") return null;\n if (c[\"subject\"] !== undefined && typeof c[\"subject\"] !== \"string\") return null;\n if (typeof c[\"verdict\"] !== \"string\" || !(VERDICTS as readonly string[]).includes(c[\"verdict\"])) return null;\n // The frozen view iterates evidence per claim — a scalar here would crash it.\n if (!Array.isArray(c[\"evidence\"]) || c[\"evidence\"].some((e) => typeof e !== \"string\")) return null;\n if (c[\"note\"] !== undefined && typeof c[\"note\"] !== \"string\") return null;\n if (c[\"answered_by\"] !== undefined && typeof c[\"answered_by\"] !== \"string\") return null;\n // Absent fine, present whole — the isConsumedContractReport precedent, and\n // the guard body lives with the pin shape it vouches for (pins/pin.ts). A\n // broken pins block makes the whole record unreadable and therefore never\n // overwritten: skipping it instead would read a hand-damaged record as\n // drift-free.\n if (!isEvidencePinList(c[\"evidence_pins\"])) return null;\n }\n if (doc[\"schema\"] !== undefined && doc[\"schema\"] !== 2) return null;\n if (!isConsumedReport(doc[\"report\"])) return null;\n if (!isConsumedContractReport(doc[\"contractReport\"])) return null;\n if (doc[\"attestations\"] !== undefined) {\n if (!Array.isArray(doc[\"attestations\"])) return null;\n for (const a of doc[\"attestations\"]) {\n if (!isRecord(a)) return null;\n if (typeof a[\"service\"] !== \"string\" || typeof a[\"commit\"] !== \"string\" || typeof a[\"recorded\"] !== \"string\") return null;\n if (!/^[0-9a-f]{40,64}$/i.test(a[\"commit\"])) return null;\n if (!Array.isArray(a[\"claims\"]) || a[\"claims\"].some((id) => typeof id !== \"string\")) return null;\n if (!isConsumedReport(a[\"report\"])) return null;\n if (!isConsumedContractReport(a[\"contractReport\"])) return null;\n }\n }\n return doc as unknown as Verification;\n}\n\n/** Absent is fine — most records answer nothing from a report. Present must be whole: it is printed. */\nfunction isConsumedReport(v: unknown): boolean {\n if (v === undefined) return true;\n return (\n isRecord(v) &&\n typeof v[\"path\"] === \"string\" &&\n typeof v[\"digest\"] === \"string\" &&\n typeof v[\"mtime\"] === \"string\" &&\n isCount(v[\"scenarios\"])\n );\n}\n\n/**\n * The contract report's pin, exactly as loose as {@link isConsumedReport}:\n * absent fine, present whole — for the fields the readers dereference.\n * `format` is OPTIONAL here even though loam always writes it: nothing in the\n * readers dereferences it (it is provenance for a person, echoed only inside\n * whole-object payloads), and the generated AGENTS.md teaches the pin as\n * path/sha256/mtime/operation-count — so a hand-written block faithful to that\n * teaching must not make the WHOLE record unreadable over a field no reader\n * uses. When present it must be a string, and any string: a record written by\n * a newer loam that learned a second report format must not read as\n * unreadable here.\n */\nfunction isConsumedContractReport(v: unknown): boolean {\n if (v === undefined) return true;\n return (\n isRecord(v) &&\n typeof v[\"path\"] === \"string\" &&\n typeof v[\"digest\"] === \"string\" &&\n typeof v[\"mtime\"] === \"string\" &&\n isCount(v[\"operations\"]) &&\n (v[\"format\"] === undefined || typeof v[\"format\"] === \"string\")\n );\n}\n\n/**\n * A summary count: an integer. YAML hands back strings for anything quoted.\n * Negativity is deliberately NOT checked here: a negative count is a summary\n * that disagrees with the claims below it, and `summaryDisagreement` owns that\n * refusal — failing the SHAPE check instead made the message list five keys\n * that were all present, while the miscount branch one call away holds the\n * advice the case actually needs.\n */\nfunction isCount(v: unknown): v is number {\n return typeof v === \"number\" && Number.isInteger(v);\n}\n\n"]}
|
|
@@ -0,0 +1,157 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Re-checking evidence pins against a working tree: pure data out, one grade
|
|
3
|
+
* per pin, and no verdict anywhere in the result on purpose — the lint demotes
|
|
4
|
+
* reviewer confidence, it never promotes or demotes a claim. Prints nothing;
|
|
5
|
+
* the validate command turns the grades into findings.
|
|
6
|
+
*
|
|
7
|
+
* The claim shape is structural rather than an import of `../record.js`'s
|
|
8
|
+
* `RecordedClaim`, and not only for the smallest-parameter rule: this package
|
|
9
|
+
* is imported BY `core/verify` (`../file.ts` asks `./pin.ts` for the read
|
|
10
|
+
* guard, `../answers.ts` for the shape), so importing the parent back would
|
|
11
|
+
* put a cycle in the package graph that `import/no-cycle` cannot see.
|
|
12
|
+
*/
|
|
13
|
+
import { readFile, stat } from "node:fs/promises";
|
|
14
|
+
import { inOrder } from "../../kernel/concurrency.js";
|
|
15
|
+
import { resolvePortableFileInside, UnsafePathError } from "../../kernel/path-safety.js";
|
|
16
|
+
import { pinnedDigest, pinnedText, sourceLines } from "./pin.js";
|
|
17
|
+
/**
|
|
18
|
+
* Grade one record's pins for one service against that service's working tree.
|
|
19
|
+
*
|
|
20
|
+
* Only agent-confirmed claims of `service` are graded: a runner's or contract
|
|
21
|
+
* runner's evidence names a report entry, not a file, so there is nothing to
|
|
22
|
+
* re-check, and an unconfirmed claim pinned nothing. Absent `answered_by`
|
|
23
|
+
* counts as agent — the `attestedClaims` rule.
|
|
24
|
+
*
|
|
25
|
+
* Containment is per citation: one unreadable cited file degrades exactly the
|
|
26
|
+
* pins that cite it (graded `unresolved`), never the sibling pins and never
|
|
27
|
+
* the whole record. And "could not look" is never "looks fine": every failure
|
|
28
|
+
* to read is a drift entry, not a skip — fail closed at the validator.
|
|
29
|
+
*/
|
|
30
|
+
export async function lintEvidencePins(claims, service, repoDir) {
|
|
31
|
+
// The closed side of the answerer test, on purpose: `answered_by` is an open
|
|
32
|
+
// string here (a record is hand-editable YAML, and a newer loam may add
|
|
33
|
+
// answerers), so excluding the known mechanical values would sweep every
|
|
34
|
+
// FUTURE mechanical answerer's report-entry citations into the `unpinned`
|
|
35
|
+
// tally with false "recorded before pins existed" advice. Only what is
|
|
36
|
+
// positively an agent's word is graded; absent counts as agent — the
|
|
37
|
+
// `attestedClaims` rule for records that predate `--results`.
|
|
38
|
+
const mine = claims.filter((c) => c.subject === service &&
|
|
39
|
+
c.verdict === "confirmed" &&
|
|
40
|
+
(c.answered_by === undefined || c.answered_by === "agent"));
|
|
41
|
+
// Join pins to evidence entries by the `path:line` key, never by position:
|
|
42
|
+
// a record is hand-editable YAML, and a positional join would let a deleted
|
|
43
|
+
// evidence line silently shift every later pin onto the wrong citation.
|
|
44
|
+
const joined = [];
|
|
45
|
+
let unpinned = 0;
|
|
46
|
+
for (const claim of mine) {
|
|
47
|
+
const pins = new Map((claim.evidence_pins ?? []).map((pin) => [`${pin.path}:${pin.line}`, pin]));
|
|
48
|
+
// A pin no evidence entry names pins nothing this claim cites (a hand
|
|
49
|
+
// edit); grading it would report drift about a citation the claim does not
|
|
50
|
+
// make, so only the joined pairs are graded. The reverse direction — a
|
|
51
|
+
// citation without a pin — is the `unpinned` tally.
|
|
52
|
+
for (const evidence of claim.evidence) {
|
|
53
|
+
const pin = pins.get(evidence);
|
|
54
|
+
if (pin === undefined)
|
|
55
|
+
unpinned += 1;
|
|
56
|
+
else
|
|
57
|
+
joined.push({ claim: claim.id, evidence, pin });
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
// Each distinct cited file is read and hashed once per call — a local map,
|
|
61
|
+
// per invocation, because module-level state leaks across tests and hosts.
|
|
62
|
+
// The reads are independent, so they go through the bounded pool; grading
|
|
63
|
+
// stays sequential below so `drifts` keeps record order.
|
|
64
|
+
const paths = [...new Set(joined.map((j) => j.pin.path))];
|
|
65
|
+
const states = new Map(await inOrder(paths, async (path) => [path, await readCited(repoDir, path)]));
|
|
66
|
+
let checked = 0;
|
|
67
|
+
const drifts = [];
|
|
68
|
+
for (const { claim, evidence, pin } of joined) {
|
|
69
|
+
const file = states.get(pin.path);
|
|
70
|
+
if (file === undefined || file.state === "unreadable") {
|
|
71
|
+
drifts.push({ claim, evidence, grade: "unresolved", what: file?.what ?? "the cited file was not read" });
|
|
72
|
+
continue;
|
|
73
|
+
}
|
|
74
|
+
const cited = file.lines[pin.line - 1];
|
|
75
|
+
if (cited === undefined) {
|
|
76
|
+
drifts.push({
|
|
77
|
+
claim,
|
|
78
|
+
evidence,
|
|
79
|
+
grade: "unresolved",
|
|
80
|
+
// `continue`, like the unreadable-file arm: an unresolvable CITATION
|
|
81
|
+
// is one finding, and a token scan on top would report the same
|
|
82
|
+
// damaged citation twice under two codes.
|
|
83
|
+
what: `the cited line is past the end — the file now has ${file.lines.length} line(s)`,
|
|
84
|
+
});
|
|
85
|
+
continue;
|
|
86
|
+
}
|
|
87
|
+
if (file.digest === pin.file_sha256) {
|
|
88
|
+
checked += 1;
|
|
89
|
+
}
|
|
90
|
+
else if (pinnedText(cited) === pin.text) {
|
|
91
|
+
// The mildest drift: the file changed around the cited line, which
|
|
92
|
+
// still reads as pinned — "reads as", not "is": both sides of the
|
|
93
|
+
// comparison are capped at PIN_TEXT_CAP, so a change past the cap on a
|
|
94
|
+
// pathological line is invisible to this grade (and visible to the
|
|
95
|
+
// digest, which is why this is a drift and not a clean pin).
|
|
96
|
+
drifts.push({
|
|
97
|
+
claim,
|
|
98
|
+
evidence,
|
|
99
|
+
grade: "moved",
|
|
100
|
+
what: "the file changed around the cited line, which still reads as pinned",
|
|
101
|
+
});
|
|
102
|
+
}
|
|
103
|
+
else {
|
|
104
|
+
drifts.push({
|
|
105
|
+
claim,
|
|
106
|
+
evidence,
|
|
107
|
+
grade: "line-changed",
|
|
108
|
+
what: `the cited line now reads '${pinnedText(cited)}' where the record pinned '${pin.text}'`,
|
|
109
|
+
});
|
|
110
|
+
}
|
|
111
|
+
// Independent of the grades above, clean pins included — a digest match
|
|
112
|
+
// must not read as "the token is still there", because content identical
|
|
113
|
+
// to record time says nothing once the file drifts back. "No longer" is
|
|
114
|
+
// the honest word here: the write side stamps `token` only when the
|
|
115
|
+
// attested blob contained it (EvidencePin.token), so this grade always
|
|
116
|
+
// convicts a disappearance, never re-litigates an absence the record-time
|
|
117
|
+
// notice already reported.
|
|
118
|
+
if (pin.token !== undefined && !file.text.includes(pin.token)) {
|
|
119
|
+
drifts.push({
|
|
120
|
+
claim,
|
|
121
|
+
evidence,
|
|
122
|
+
grade: "token-missing",
|
|
123
|
+
what: `the cited file no longer contains '${pin.token}' — the literal string the claim asserts`,
|
|
124
|
+
});
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
return { checked, drifts, unpinned };
|
|
128
|
+
}
|
|
129
|
+
/** One cited file against the working tree, every failure named rather than thrown. */
|
|
130
|
+
async function readCited(repoDir, path) {
|
|
131
|
+
let absolute;
|
|
132
|
+
try {
|
|
133
|
+
absolute = resolvePortableFileInside(repoDir, path, "pinned evidence");
|
|
134
|
+
}
|
|
135
|
+
catch (err) {
|
|
136
|
+
if (!(err instanceof UnsafePathError))
|
|
137
|
+
throw err;
|
|
138
|
+
return { state: "unreadable", what: `the pinned path is unsafe: ${err.message}` };
|
|
139
|
+
}
|
|
140
|
+
try {
|
|
141
|
+
const info = await stat(absolute);
|
|
142
|
+
if (!info.isFile())
|
|
143
|
+
return { state: "unreadable", what: "the cited path is not a regular file" };
|
|
144
|
+
const text = await readFile(absolute, "utf8");
|
|
145
|
+
return { state: "ok", digest: pinnedDigest(text), lines: sourceLines(text), text };
|
|
146
|
+
}
|
|
147
|
+
catch (err) {
|
|
148
|
+
// ENOENT is the common drift — the cited file is gone from the tree; any
|
|
149
|
+
// other errno (EACCES, EISDIR racing the stat) is named as itself, because
|
|
150
|
+
// "could not read" and "does not exist" call for different repairs.
|
|
151
|
+
if (err.code === "ENOENT") {
|
|
152
|
+
return { state: "unreadable", what: "the cited file does not exist in this working tree" };
|
|
153
|
+
}
|
|
154
|
+
return { state: "unreadable", what: err instanceof Error ? err.message : String(err) };
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
//# sourceMappingURL=lint.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"lint.js","sourceRoot":"","sources":["../../../../src/core/verify/pins/lint.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * Re-checking evidence pins against a working tree: pure data out, one grade\n * per pin, and no verdict anywhere in the result on purpose — the lint demotes\n * reviewer confidence, it never promotes or demotes a claim. Prints nothing;\n * the validate command turns the grades into findings.\n *\n * The claim shape is structural rather than an import of `../record.js`'s\n * `RecordedClaim`, and not only for the smallest-parameter rule: this package\n * is imported BY `core/verify` (`../file.ts` asks `./pin.ts` for the read\n * guard, `../answers.ts` for the shape), so importing the parent back would\n * put a cycle in the package graph that `import/no-cycle` cannot see.\n */\nimport { readFile, stat } from \"node:fs/promises\";\nimport { inOrder } from \"../../kernel/concurrency.js\";\nimport { resolvePortableFileInside, UnsafePathError } from \"../../kernel/path-safety.js\";\nimport { pinnedDigest, pinnedText, sourceLines, type EvidencePin } from \"./pin.js\";\n\n/** What the lint reads off a recorded claim — structurally, see the header. */\nexport interface PinnedClaim {\n id: string;\n /** Present in federated (schema 2) records — the service whose code answers it. */\n subject?: string;\n verdict: string;\n /** Absent counts as agent — the `attestedClaims` rule for records that predate `--results`. */\n answered_by?: string;\n evidence: string[];\n evidence_pins?: EvidencePin[];\n}\n\n/** One pin's disagreement with the working tree. `grade` is what a finding code is made from. */\nexport interface PinDrift {\n claim: string;\n /** The `path:line` citation the pin belongs to. */\n evidence: string;\n grade: \"unresolved\" | \"moved\" | \"line-changed\" | \"token-missing\";\n /** What changed, in one clause — rendered into a finding's detail line. */\n what: string;\n}\n\nexport interface PinLint {\n /** Pins that resolved clean: file digest unchanged against the working tree. */\n checked: number;\n /** In record order — claim by claim, citation by citation — so two runs diff. */\n drifts: PinDrift[];\n /** Agent-confirmed citations carrying no pin, plus claims with no pins at all. */\n unpinned: number;\n}\n\n/** What one cited file turned out to be, read and hashed once per lint call. */\ntype FileState =\n | { state: \"unreadable\"; what: string }\n | { state: \"ok\"; digest: string; lines: string[]; text: string };\n\n/**\n * Grade one record's pins for one service against that service's working tree.\n *\n * Only agent-confirmed claims of `service` are graded: a runner's or contract\n * runner's evidence names a report entry, not a file, so there is nothing to\n * re-check, and an unconfirmed claim pinned nothing. Absent `answered_by`\n * counts as agent — the `attestedClaims` rule.\n *\n * Containment is per citation: one unreadable cited file degrades exactly the\n * pins that cite it (graded `unresolved`), never the sibling pins and never\n * the whole record. And \"could not look\" is never \"looks fine\": every failure\n * to read is a drift entry, not a skip — fail closed at the validator.\n */\nexport async function lintEvidencePins(\n claims: readonly PinnedClaim[],\n service: string,\n repoDir: string,\n): Promise<PinLint> {\n // The closed side of the answerer test, on purpose: `answered_by` is an open\n // string here (a record is hand-editable YAML, and a newer loam may add\n // answerers), so excluding the known mechanical values would sweep every\n // FUTURE mechanical answerer's report-entry citations into the `unpinned`\n // tally with false \"recorded before pins existed\" advice. Only what is\n // positively an agent's word is graded; absent counts as agent — the\n // `attestedClaims` rule for records that predate `--results`.\n const mine = claims.filter(\n (c) =>\n c.subject === service &&\n c.verdict === \"confirmed\" &&\n (c.answered_by === undefined || c.answered_by === \"agent\"),\n );\n\n // Join pins to evidence entries by the `path:line` key, never by position:\n // a record is hand-editable YAML, and a positional join would let a deleted\n // evidence line silently shift every later pin onto the wrong citation.\n const joined: Array<{ claim: string; evidence: string; pin: EvidencePin }> = [];\n let unpinned = 0;\n for (const claim of mine) {\n const pins = new Map((claim.evidence_pins ?? []).map((pin) => [`${pin.path}:${pin.line}`, pin]));\n // A pin no evidence entry names pins nothing this claim cites (a hand\n // edit); grading it would report drift about a citation the claim does not\n // make, so only the joined pairs are graded. The reverse direction — a\n // citation without a pin — is the `unpinned` tally.\n for (const evidence of claim.evidence) {\n const pin = pins.get(evidence);\n if (pin === undefined) unpinned += 1;\n else joined.push({ claim: claim.id, evidence, pin });\n }\n }\n\n // Each distinct cited file is read and hashed once per call — a local map,\n // per invocation, because module-level state leaks across tests and hosts.\n // The reads are independent, so they go through the bounded pool; grading\n // stays sequential below so `drifts` keeps record order.\n const paths = [...new Set(joined.map((j) => j.pin.path))];\n const states = new Map(\n await inOrder(paths, async (path): Promise<[string, FileState]> => [path, await readCited(repoDir, path)]),\n );\n\n let checked = 0;\n const drifts: PinDrift[] = [];\n for (const { claim, evidence, pin } of joined) {\n const file = states.get(pin.path);\n if (file === undefined || file.state === \"unreadable\") {\n drifts.push({ claim, evidence, grade: \"unresolved\", what: file?.what ?? \"the cited file was not read\" });\n continue;\n }\n const cited = file.lines[pin.line - 1];\n if (cited === undefined) {\n drifts.push({\n claim,\n evidence,\n grade: \"unresolved\",\n // `continue`, like the unreadable-file arm: an unresolvable CITATION\n // is one finding, and a token scan on top would report the same\n // damaged citation twice under two codes.\n what: `the cited line is past the end — the file now has ${file.lines.length} line(s)`,\n });\n continue;\n }\n if (file.digest === pin.file_sha256) {\n checked += 1;\n } else if (pinnedText(cited) === pin.text) {\n // The mildest drift: the file changed around the cited line, which\n // still reads as pinned — \"reads as\", not \"is\": both sides of the\n // comparison are capped at PIN_TEXT_CAP, so a change past the cap on a\n // pathological line is invisible to this grade (and visible to the\n // digest, which is why this is a drift and not a clean pin).\n drifts.push({\n claim,\n evidence,\n grade: \"moved\",\n what: \"the file changed around the cited line, which still reads as pinned\",\n });\n } else {\n drifts.push({\n claim,\n evidence,\n grade: \"line-changed\",\n what: `the cited line now reads '${pinnedText(cited)}' where the record pinned '${pin.text}'`,\n });\n }\n // Independent of the grades above, clean pins included — a digest match\n // must not read as \"the token is still there\", because content identical\n // to record time says nothing once the file drifts back. \"No longer\" is\n // the honest word here: the write side stamps `token` only when the\n // attested blob contained it (EvidencePin.token), so this grade always\n // convicts a disappearance, never re-litigates an absence the record-time\n // notice already reported.\n if (pin.token !== undefined && !file.text.includes(pin.token)) {\n drifts.push({\n claim,\n evidence,\n grade: \"token-missing\",\n what: `the cited file no longer contains '${pin.token}' — the literal string the claim asserts`,\n });\n }\n }\n return { checked, drifts, unpinned };\n}\n\n/** One cited file against the working tree, every failure named rather than thrown. */\nasync function readCited(repoDir: string, path: string): Promise<FileState> {\n let absolute: string;\n try {\n absolute = resolvePortableFileInside(repoDir, path, \"pinned evidence\");\n } catch (err) {\n if (!(err instanceof UnsafePathError)) throw err;\n return { state: \"unreadable\", what: `the pinned path is unsafe: ${err.message}` };\n }\n try {\n const info = await stat(absolute);\n if (!info.isFile()) return { state: \"unreadable\", what: \"the cited path is not a regular file\" };\n const text = await readFile(absolute, \"utf8\");\n return { state: \"ok\", digest: pinnedDigest(text), lines: sourceLines(text), text };\n } catch (err) {\n // ENOENT is the common drift — the cited file is gone from the tree; any\n // other errno (EACCES, EISDIR racing the stat) is named as itself, because\n // \"could not read\" and \"does not exist\" call for different repairs.\n if ((err as NodeJS.ErrnoException).code === \"ENOENT\") {\n return { state: \"unreadable\", what: \"the cited file does not exist in this working tree\" };\n }\n return { state: \"unreadable\", what: err instanceof Error ? err.message : String(err) };\n }\n}\n"]}
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The evidence pin's identity: what `verify --service --record` stamps beside a
|
|
3
|
+
* confirmed agent answer so a later `loam validate`, standing in the same
|
|
4
|
+
* service repository, can re-check the citation against the working tree.
|
|
5
|
+
*
|
|
6
|
+
* Be precise about what a pin proves, because the record travels without loam:
|
|
7
|
+
* a pin identifies WHAT was cited at the attested commit — the cited file's
|
|
8
|
+
* normalized sha256, the cited line's text, the literal token the claim
|
|
9
|
+
* asserts. It does not prove the claim true; an agent that cited the wrong
|
|
10
|
+
* line gets a faithful pin of the wrong line. That is why the read side
|
|
11
|
+
* (`./lint.ts`) only ever demotes reviewer confidence: no pin state moves a
|
|
12
|
+
* verdict in either direction.
|
|
13
|
+
*/
|
|
14
|
+
import { createHash } from "node:crypto";
|
|
15
|
+
import { isRecord } from "../../kernel/records.js";
|
|
16
|
+
/**
|
|
17
|
+
* How much of the cited line a pin carries. Enough to recognize a line of
|
|
18
|
+
* source by eye and by equality; a cap because evidence may cite a line of a
|
|
19
|
+
* generated client or a minified bundle, and the record is a YAML file people
|
|
20
|
+
* read and diff — one pathological line must not turn it into one.
|
|
21
|
+
*/
|
|
22
|
+
export const PIN_TEXT_CAP = 200;
|
|
23
|
+
/** The pin's line recipe: trimmed (indentation is formatting), capped at {@link PIN_TEXT_CAP}. */
|
|
24
|
+
export function pinnedText(line) {
|
|
25
|
+
const trimmed = line.trim();
|
|
26
|
+
return trimmed.length > PIN_TEXT_CAP ? trimmed.slice(0, PIN_TEXT_CAP) : trimmed;
|
|
27
|
+
}
|
|
28
|
+
/**
|
|
29
|
+
* The one spelling of where lines fall. Both sides of the pin — the write
|
|
30
|
+
* side's bounds checks and the read side's re-grade — must number lines
|
|
31
|
+
* identically, or a pin recorded at line 12 gets graded against a different
|
|
32
|
+
* line 12; four call sites each spelling the regex themselves is how that
|
|
33
|
+
* divergence would arrive, so the recipe has exactly one home.
|
|
34
|
+
*/
|
|
35
|
+
export function sourceLines(source) {
|
|
36
|
+
return source.split(/\r\n|\n|\r/);
|
|
37
|
+
}
|
|
38
|
+
/**
|
|
39
|
+
* The cited line of a source text, through the pin recipe — or undefined when
|
|
40
|
+
* the line is past the end, which the caller grades (a refusal on the write
|
|
41
|
+
* side, `unresolved` on the read side).
|
|
42
|
+
*/
|
|
43
|
+
export function citedLine(source, line) {
|
|
44
|
+
const raw = sourceLines(source)[line - 1];
|
|
45
|
+
return raw === undefined ? undefined : pinnedText(raw);
|
|
46
|
+
}
|
|
47
|
+
/**
|
|
48
|
+
* The pin's file digest: sha256 hex over the text with CRLF/CR normalized to
|
|
49
|
+
* LF.
|
|
50
|
+
*
|
|
51
|
+
* Normalized because git's eol config makes blob bytes and working-tree bytes
|
|
52
|
+
* legally differ: on a Windows checkout with `core.autocrlf=true`, the blob
|
|
53
|
+
* the attested commit holds is LF while every file on disk is CRLF, so an
|
|
54
|
+
* unnormalized digest would grade every pin `evidence.moved` forever on the
|
|
55
|
+
* checkout config alone. The pin must convict content change, never checkout
|
|
56
|
+
* config. The recipe is over TEXT by construction — the write side digests
|
|
57
|
+
* `git show` output already decoded as utf8 — and the symmetric cost is
|
|
58
|
+
* accepted and documented in SCHEMA.md: a pure line-ending change is invisible
|
|
59
|
+
* to the lint.
|
|
60
|
+
*/
|
|
61
|
+
export function pinnedDigest(source) {
|
|
62
|
+
return createHash("sha256").update(source.replace(/\r\n|\r/g, "\n")).digest("hex");
|
|
63
|
+
}
|
|
64
|
+
/**
|
|
65
|
+
* The read-shape guard `asVerification` (../file.ts) asks per claim, on the
|
|
66
|
+
* `isConsumedContractReport` model to the letter: absent is fine — every
|
|
67
|
+
* record written before pins existed, and every runner-answered claim, has
|
|
68
|
+
* none — but present must be whole for the four fields the lint dereferences.
|
|
69
|
+
* A broken pins block makes the WHOLE record unreadable and therefore never
|
|
70
|
+
* overwritten, exactly the contract-report rule: it is somebody's record, and
|
|
71
|
+
* a lint that silently skipped a malformed pin would read a hand-damaged
|
|
72
|
+
* record as drift-free. `token` stays optional-and-any-string for `format`'s
|
|
73
|
+
* reason — a field a newer loam learned to stamp must not brick the record
|
|
74
|
+
* here.
|
|
75
|
+
*/
|
|
76
|
+
export function isEvidencePinList(v) {
|
|
77
|
+
if (v === undefined)
|
|
78
|
+
return true;
|
|
79
|
+
if (!Array.isArray(v))
|
|
80
|
+
return false;
|
|
81
|
+
for (const pin of v) {
|
|
82
|
+
if (!isRecord(pin))
|
|
83
|
+
return false;
|
|
84
|
+
if (typeof pin["path"] !== "string")
|
|
85
|
+
return false;
|
|
86
|
+
if (typeof pin["line"] !== "number" || !Number.isInteger(pin["line"]) || pin["line"] < 1)
|
|
87
|
+
return false;
|
|
88
|
+
if (typeof pin["file_sha256"] !== "string" || typeof pin["text"] !== "string")
|
|
89
|
+
return false;
|
|
90
|
+
if (pin["token"] !== undefined && typeof pin["token"] !== "string")
|
|
91
|
+
return false;
|
|
92
|
+
}
|
|
93
|
+
return true;
|
|
94
|
+
}
|
|
95
|
+
//# sourceMappingURL=pin.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"pin.js","sourceRoot":"","sources":["../../../../src/core/verify/pins/pin.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;GAYG;AACH,OAAO,EAAE,UAAU,EAAE,MAAM,aAAa,CAAC;AACzC,OAAO,EAAE,QAAQ,EAAE,MAAM,yBAAyB,CAAC;AA4BnD;;;;;GAKG;AACH,MAAM,CAAC,MAAM,YAAY,GAAG,GAAG,CAAC;AAEhC,kGAAkG;AAClG,MAAM,UAAU,UAAU,CAAC,IAAY;IACrC,MAAM,OAAO,GAAG,IAAI,CAAC,IAAI,EAAE,CAAC;IAC5B,OAAO,OAAO,CAAC,MAAM,GAAG,YAAY,CAAC,CAAC,CAAC,OAAO,CAAC,KAAK,CAAC,CAAC,EAAE,YAAY,CAAC,CAAC,CAAC,CAAC,OAAO,CAAC;AAClF,CAAC;AAED;;;;;;GAMG;AACH,MAAM,UAAU,WAAW,CAAC,MAAc;IACxC,OAAO,MAAM,CAAC,KAAK,CAAC,YAAY,CAAC,CAAC;AACpC,CAAC;AAED;;;;GAIG;AACH,MAAM,UAAU,SAAS,CAAC,MAAc,EAAE,IAAY;IACpD,MAAM,GAAG,GAAG,WAAW,CAAC,MAAM,CAAC,CAAC,IAAI,GAAG,CAAC,CAAC,CAAC;IAC1C,OAAO,GAAG,KAAK,SAAS,CAAC,CAAC,CAAC,SAAS,CAAC,CAAC,CAAC,UAAU,CAAC,GAAG,CAAC,CAAC;AACzD,CAAC;AAED;;;;;;;;;;;;;GAaG;AACH,MAAM,UAAU,YAAY,CAAC,MAAc;IACzC,OAAO,UAAU,CAAC,QAAQ,CAAC,CAAC,MAAM,CAAC,MAAM,CAAC,OAAO,CAAC,UAAU,EAAE,IAAI,CAAC,CAAC,CAAC,MAAM,CAAC,KAAK,CAAC,CAAC;AACrF,CAAC;AAED;;;;;;;;;;;GAWG;AACH,MAAM,UAAU,iBAAiB,CAAC,CAAU;IAC1C,IAAI,CAAC,KAAK,SAAS;QAAE,OAAO,IAAI,CAAC;IACjC,IAAI,CAAC,KAAK,CAAC,OAAO,CAAC,CAAC,CAAC;QAAE,OAAO,KAAK,CAAC;IACpC,KAAK,MAAM,GAAG,IAAI,CAAC,EAAE,CAAC;QACpB,IAAI,CAAC,QAAQ,CAAC,GAAG,CAAC;YAAE,OAAO,KAAK,CAAC;QACjC,IAAI,OAAO,GAAG,CAAC,MAAM,CAAC,KAAK,QAAQ;YAAE,OAAO,KAAK,CAAC;QAClD,IAAI,OAAO,GAAG,CAAC,MAAM,CAAC,KAAK,QAAQ,IAAI,CAAC,MAAM,CAAC,SAAS,CAAC,GAAG,CAAC,MAAM,CAAC,CAAC,IAAI,GAAG,CAAC,MAAM,CAAC,GAAG,CAAC;YAAE,OAAO,KAAK,CAAC;QACvG,IAAI,OAAO,GAAG,CAAC,aAAa,CAAC,KAAK,QAAQ,IAAI,OAAO,GAAG,CAAC,MAAM,CAAC,KAAK,QAAQ;YAAE,OAAO,KAAK,CAAC;QAC5F,IAAI,GAAG,CAAC,OAAO,CAAC,KAAK,SAAS,IAAI,OAAO,GAAG,CAAC,OAAO,CAAC,KAAK,QAAQ;YAAE,OAAO,KAAK,CAAC;IACnF,CAAC;IACD,OAAO,IAAI,CAAC;AACd,CAAC","sourcesContent":["/**\n * The evidence pin's identity: what `verify --service --record` stamps beside a\n * confirmed agent answer so a later `loam validate`, standing in the same\n * service repository, can re-check the citation against the working tree.\n *\n * Be precise about what a pin proves, because the record travels without loam:\n * a pin identifies WHAT was cited at the attested commit — the cited file's\n * normalized sha256, the cited line's text, the literal token the claim\n * asserts. It does not prove the claim true; an agent that cited the wrong\n * line gets a faithful pin of the wrong line. That is why the read side\n * (`./lint.ts`) only ever demotes reviewer confidence: no pin state moves a\n * verdict in either direction.\n */\nimport { createHash } from \"node:crypto\";\nimport { isRecord } from \"../../kernel/records.js\";\n\nexport interface EvidencePin {\n /** The cited file, repo-relative in the portable spelling evidence entries use. */\n path: string;\n /** The cited line, 1-based — the `:N` of the evidence entry this pins. */\n line: number;\n /** sha256 hex of the whole file at the attested commit, normalized — see {@link pinnedDigest}. */\n file_sha256: string;\n /** The cited line at the attested commit, trimmed and capped — see {@link pinnedText}. */\n text: string;\n /**\n * The literal string the claim asserts of the cited artifact — the\n * operationId for `api.exposes`, the message name for `event.declares`, the\n * edge's op for `c4.calls` (`Claim.token`, stamped at record time). Carried\n * on the pin so the read side scans the file for it and never has to\n * re-derive a checklist or parse claim prose. Absent for the kinds that\n * assert no token (`service.exists`, `scenario.tested`) — and absent when\n * the attested blob did not CONTAIN the token: that fact is warned once, at\n * record time, where the answerer can still act on it; a pin carrying it\n * would make `loam validate` repeat \"no longer contains\" forever about a\n * file that never did, with a re-record that re-derives the same state as\n * the only offered repair. `token-missing` therefore always means \"was\n * there at the attested commit and is gone\".\n */\n token?: string;\n}\n\n/**\n * How much of the cited line a pin carries. Enough to recognize a line of\n * source by eye and by equality; a cap because evidence may cite a line of a\n * generated client or a minified bundle, and the record is a YAML file people\n * read and diff — one pathological line must not turn it into one.\n */\nexport const PIN_TEXT_CAP = 200;\n\n/** The pin's line recipe: trimmed (indentation is formatting), capped at {@link PIN_TEXT_CAP}. */\nexport function pinnedText(line: string): string {\n const trimmed = line.trim();\n return trimmed.length > PIN_TEXT_CAP ? trimmed.slice(0, PIN_TEXT_CAP) : trimmed;\n}\n\n/**\n * The one spelling of where lines fall. Both sides of the pin — the write\n * side's bounds checks and the read side's re-grade — must number lines\n * identically, or a pin recorded at line 12 gets graded against a different\n * line 12; four call sites each spelling the regex themselves is how that\n * divergence would arrive, so the recipe has exactly one home.\n */\nexport function sourceLines(source: string): string[] {\n return source.split(/\\r\\n|\\n|\\r/);\n}\n\n/**\n * The cited line of a source text, through the pin recipe — or undefined when\n * the line is past the end, which the caller grades (a refusal on the write\n * side, `unresolved` on the read side).\n */\nexport function citedLine(source: string, line: number): string | undefined {\n const raw = sourceLines(source)[line - 1];\n return raw === undefined ? undefined : pinnedText(raw);\n}\n\n/**\n * The pin's file digest: sha256 hex over the text with CRLF/CR normalized to\n * LF.\n *\n * Normalized because git's eol config makes blob bytes and working-tree bytes\n * legally differ: on a Windows checkout with `core.autocrlf=true`, the blob\n * the attested commit holds is LF while every file on disk is CRLF, so an\n * unnormalized digest would grade every pin `evidence.moved` forever on the\n * checkout config alone. The pin must convict content change, never checkout\n * config. The recipe is over TEXT by construction — the write side digests\n * `git show` output already decoded as utf8 — and the symmetric cost is\n * accepted and documented in SCHEMA.md: a pure line-ending change is invisible\n * to the lint.\n */\nexport function pinnedDigest(source: string): string {\n return createHash(\"sha256\").update(source.replace(/\\r\\n|\\r/g, \"\\n\")).digest(\"hex\");\n}\n\n/**\n * The read-shape guard `asVerification` (../file.ts) asks per claim, on the\n * `isConsumedContractReport` model to the letter: absent is fine — every\n * record written before pins existed, and every runner-answered claim, has\n * none — but present must be whole for the four fields the lint dereferences.\n * A broken pins block makes the WHOLE record unreadable and therefore never\n * overwritten, exactly the contract-report rule: it is somebody's record, and\n * a lint that silently skipped a malformed pin would read a hand-damaged\n * record as drift-free. `token` stays optional-and-any-string for `format`'s\n * reason — a field a newer loam learned to stamp must not brick the record\n * here.\n */\nexport function isEvidencePinList(v: unknown): boolean {\n if (v === undefined) return true;\n if (!Array.isArray(v)) return false;\n for (const pin of v) {\n if (!isRecord(pin)) return false;\n if (typeof pin[\"path\"] !== \"string\") return false;\n if (typeof pin[\"line\"] !== \"number\" || !Number.isInteger(pin[\"line\"]) || pin[\"line\"] < 1) return false;\n if (typeof pin[\"file_sha256\"] !== \"string\" || typeof pin[\"text\"] !== \"string\") return false;\n if (pin[\"token\"] !== undefined && typeof pin[\"token\"] !== \"string\") return false;\n }\n return true;\n}\n"]}
|