@ybotok/loam 0.2.0-alpha.6 → 0.2.0-alpha.9
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +1477 -1
- package/COMPARISON.md +91 -3
- package/CONTRIBUTING.md +37 -0
- package/MIGRATING-from-OpenSpec.md +40 -22
- package/QUICKSTART.md +181 -9
- package/README.md +256 -57
- package/ROADMAP.md +108 -11
- package/SCHEMA.md +721 -130
- package/WORKFLOW.md +272 -62
- package/dist/cli.js +26 -8
- package/dist/cli.js.map +1 -1
- package/dist/commands/adopt/adopt.js +5 -10
- package/dist/commands/adopt/adopt.js.map +1 -1
- package/dist/commands/archive/archive.js +15 -18
- package/dist/commands/archive/archive.js.map +1 -1
- package/dist/commands/archive/plan/contracts/asyncapi.js +27 -41
- package/dist/commands/archive/plan/contracts/asyncapi.js.map +1 -1
- package/dist/commands/archive/plan/contracts/consumers.js +56 -0
- package/dist/commands/archive/plan/contracts/consumers.js.map +1 -0
- package/dist/commands/archive/plan/contracts/openapi.js +25 -42
- package/dist/commands/archive/plan/contracts/openapi.js.map +1 -1
- package/dist/commands/archive/plan/contracts/verdict-lines.js +41 -0
- package/dist/commands/archive/plan/contracts/verdict-lines.js.map +1 -0
- package/dist/commands/archive/plan/gate.js +3 -7
- package/dist/commands/archive/plan/gate.js.map +1 -1
- package/dist/commands/archive/plan/landscape.js +4 -4
- package/dist/commands/archive/plan/landscape.js.map +1 -1
- package/dist/commands/archive/plan/refusal.js +8 -18
- package/dist/commands/archive/plan/refusal.js.map +1 -1
- package/dist/commands/archive/plan/specs.js +134 -25
- package/dist/commands/archive/plan/specs.js.map +1 -1
- package/dist/commands/archive/plan/state.js +1 -0
- package/dist/commands/archive/plan/state.js.map +1 -1
- package/dist/commands/archive/run.js +82 -33
- package/dist/commands/archive/run.js.map +1 -1
- package/dist/commands/archive/shipped.js +49 -0
- package/dist/commands/archive/shipped.js.map +1 -0
- package/dist/commands/context/context.js +4 -7
- package/dist/commands/context/context.js.map +1 -1
- package/dist/commands/context/print.js +2 -2
- package/dist/commands/context/print.js.map +1 -1
- package/dist/commands/delta/delta.js +44 -12
- package/dist/commands/delta/delta.js.map +1 -1
- package/dist/commands/delta/print.js +1 -1
- package/dist/commands/delta/print.js.map +1 -1
- package/dist/commands/dependencies.js +3 -7
- package/dist/commands/dependencies.js.map +1 -1
- package/dist/commands/diff/diff.js +20 -27
- package/dist/commands/diff/diff.js.map +1 -1
- package/dist/commands/explore.js +4 -8
- package/dist/commands/explore.js.map +1 -1
- package/dist/commands/gate/gate.js +45 -8
- package/dist/commands/gate/gate.js.map +1 -1
- package/dist/commands/gate/print.js +15 -3
- package/dist/commands/gate/print.js.map +1 -1
- package/dist/commands/gherkin/commit.js +13 -54
- package/dist/commands/gherkin/commit.js.map +1 -1
- package/dist/commands/gherkin/gherkin.js +36 -20
- package/dist/commands/gherkin/gherkin.js.map +1 -1
- package/dist/commands/gherkin/reconcile.js +10 -13
- package/dist/commands/gherkin/reconcile.js.map +1 -1
- package/dist/commands/init/first-hour.js +44 -0
- package/dist/commands/init/first-hour.js.map +1 -1
- package/dist/commands/init/init.js +18 -22
- package/dist/commands/init/init.js.map +1 -1
- package/dist/commands/list/campaign/campaign.js +4 -6
- package/dist/commands/list/campaign/campaign.js.map +1 -1
- package/dist/commands/list/json.js +0 -4
- package/dist/commands/list/json.js.map +1 -1
- package/dist/commands/list/list.js +38 -23
- package/dist/commands/list/list.js.map +1 -1
- package/dist/commands/list/print.js +38 -6
- package/dist/commands/list/print.js.map +1 -1
- package/dist/commands/list/suites/suites.js +140 -0
- package/dist/commands/list/suites/suites.js.map +1 -0
- package/dist/commands/list/views.js +28 -33
- package/dist/commands/list/views.js.map +1 -1
- package/dist/commands/mcp/dispatch.js +91 -36
- package/dist/commands/mcp/dispatch.js.map +1 -1
- package/dist/commands/mcp/mcp.js +56 -15
- package/dist/commands/mcp/mcp.js.map +1 -1
- package/dist/commands/mcp/serve.js.map +1 -1
- package/dist/commands/migrate-openspec/materialize/active.js +27 -22
- package/dist/commands/migrate-openspec/materialize/active.js.map +1 -1
- package/dist/commands/migrate-openspec/materialize/stage.js +4 -3
- package/dist/commands/migrate-openspec/materialize/stage.js.map +1 -1
- package/dist/commands/migrate-openspec/materialize/target.js +2 -2
- package/dist/commands/migrate-openspec/materialize/target.js.map +1 -1
- package/dist/commands/migrate-openspec/openspec/decisions.js +9 -14
- package/dist/commands/migrate-openspec/openspec/decisions.js.map +1 -1
- package/dist/commands/migrate-openspec/openspec/error.js +7 -8
- package/dist/commands/migrate-openspec/openspec/error.js.map +1 -1
- package/dist/commands/migrate-openspec/openspec/mapping.js +1 -9
- package/dist/commands/migrate-openspec/openspec/mapping.js.map +1 -1
- package/dist/commands/migrate-openspec/openspec/paths.js +3 -6
- package/dist/commands/migrate-openspec/openspec/paths.js.map +1 -1
- package/dist/commands/new/capability-template.js +118 -0
- package/dist/commands/new/capability-template.js.map +1 -0
- package/dist/commands/new/commit.js +8 -22
- package/dist/commands/new/commit.js.map +1 -1
- package/dist/commands/new/modifies.js +206 -0
- package/dist/commands/new/modifies.js.map +1 -0
- package/dist/commands/new/new.js +59 -14
- package/dist/commands/new/new.js.map +1 -1
- package/dist/commands/new/templates.js +48 -124
- package/dist/commands/new/templates.js.map +1 -1
- package/dist/commands/open.js +3 -7
- package/dist/commands/open.js.map +1 -1
- package/dist/commands/policy/gate.js +27 -0
- package/dist/commands/policy/gate.js.map +1 -1
- package/dist/commands/rebase/plan.js +2 -2
- package/dist/commands/rebase/plan.js.map +1 -1
- package/dist/commands/rebase/rebase.js +60 -35
- package/dist/commands/rebase/rebase.js.map +1 -1
- package/dist/commands/seed/commit.js +55 -26
- package/dist/commands/seed/commit.js.map +1 -1
- package/dist/commands/seed/plan.js +64 -13
- package/dist/commands/seed/plan.js.map +1 -1
- package/dist/commands/seed/seed.js +6 -9
- package/dist/commands/seed/seed.js.map +1 -1
- package/dist/commands/show/show.js +3 -7
- package/dist/commands/show/show.js.map +1 -1
- package/dist/commands/status/print.js +13 -4
- package/dist/commands/status/print.js.map +1 -1
- package/dist/commands/status/status.js +3 -7
- package/dist/commands/status/status.js.map +1 -1
- package/dist/commands/steps/steps.js +3 -7
- package/dist/commands/steps/steps.js.map +1 -1
- package/dist/commands/subsystem/edit.js +7 -7
- package/dist/commands/subsystem/edit.js.map +1 -1
- package/dist/commands/subsystem/move.js +60 -25
- package/dist/commands/subsystem/move.js.map +1 -1
- package/dist/commands/subsystem/query.js +2 -4
- package/dist/commands/subsystem/query.js.map +1 -1
- package/dist/commands/subsystem/subsystem.js +4 -7
- package/dist/commands/subsystem/subsystem.js.map +1 -1
- package/dist/commands/subsystem/sync.js +141 -71
- package/dist/commands/subsystem/sync.js.map +1 -1
- package/dist/commands/subsystem/txn/report.js +55 -5
- package/dist/commands/subsystem/txn/report.js.map +1 -1
- package/dist/commands/subsystem/txn/txn.js +121 -45
- package/dist/commands/subsystem/txn/txn.js.map +1 -1
- package/dist/commands/subsystem/txn/views.js +82 -12
- package/dist/commands/subsystem/txn/views.js.map +1 -1
- package/dist/commands/tasks/render.js +116 -0
- package/dist/commands/tasks/render.js.map +1 -0
- package/dist/commands/tasks/tasks.js +113 -0
- package/dist/commands/tasks/tasks.js.map +1 -0
- package/dist/commands/unarchive/manifest.js +6 -9
- package/dist/commands/unarchive/manifest.js.map +1 -1
- package/dist/commands/unarchive/restore.js +17 -28
- package/dist/commands/unarchive/restore.js.map +1 -1
- package/dist/commands/unarchive/unarchive.js +22 -30
- package/dist/commands/unarchive/unarchive.js.map +1 -1
- package/dist/commands/validate/arch-coverage.js +6 -15
- package/dist/commands/validate/arch-coverage.js.map +1 -1
- package/dist/commands/validate/checks/fleet-shape.js +4 -49
- package/dist/commands/validate/checks/fleet-shape.js.map +1 -1
- package/dist/commands/validate/checks/health-deps.js +9 -6
- package/dist/commands/validate/checks/health-deps.js.map +1 -1
- package/dist/commands/validate/checks/requirements.js +18 -8
- package/dist/commands/validate/checks/requirements.js.map +1 -1
- package/dist/commands/validate/checks/sources.js +6 -2
- package/dist/commands/validate/checks/sources.js.map +1 -1
- package/dist/commands/validate/feature.js +23 -19
- package/dist/commands/validate/feature.js.map +1 -1
- package/dist/commands/validate/fleet/census.js +26 -3
- package/dist/commands/validate/fleet/census.js.map +1 -1
- package/dist/commands/validate/fleet/landscape.js +35 -32
- package/dist/commands/validate/fleet/landscape.js.map +1 -1
- package/dist/commands/validate/fleet/map/attest.js +7 -12
- package/dist/commands/validate/fleet/map/attest.js.map +1 -1
- package/dist/commands/validate/fleet/map/bindings.js +16 -5
- package/dist/commands/validate/fleet/map/bindings.js.map +1 -1
- package/dist/commands/validate/fleet/map/consumers.js +43 -122
- package/dist/commands/validate/fleet/map/consumers.js.map +1 -1
- package/dist/commands/validate/fleet/map/usecases.js +5 -11
- package/dist/commands/validate/fleet/map/usecases.js.map +1 -1
- package/dist/commands/validate/fleet/obligations.js +1 -5
- package/dist/commands/validate/fleet/obligations.js.map +1 -1
- package/dist/commands/validate/fleet/scorecard/print.js +5 -1
- package/dist/commands/validate/fleet/scorecard/print.js.map +1 -1
- package/dist/commands/validate/fleet/scorecard/scorecard.js +17 -11
- package/dist/commands/validate/fleet/scorecard/scorecard.js.map +1 -1
- package/dist/commands/validate/fleet/usecases/usecases.js +6 -4
- package/dist/commands/validate/fleet/usecases/usecases.js.map +1 -1
- package/dist/commands/validate/fleet/views/fleet-project.js +1 -1
- package/dist/commands/validate/fleet/views/fleet-project.js.map +1 -1
- package/dist/commands/validate/fleet/views/ids.js +33 -7
- package/dist/commands/validate/fleet/views/ids.js.map +1 -1
- package/dist/commands/validate/fleet/views/root-documents.js +17 -35
- package/dist/commands/validate/fleet/views/root-documents.js.map +1 -1
- package/dist/commands/validate/fleet/views/stale.js +31 -14
- package/dist/commands/validate/fleet/views/stale.js.map +1 -1
- package/dist/commands/validate/links/corpus.js +1 -4
- package/dist/commands/validate/links/corpus.js.map +1 -1
- package/dist/commands/validate/links/glossary.js.map +1 -1
- package/dist/commands/validate/report.js +5 -4
- package/dist/commands/validate/report.js.map +1 -1
- package/dist/commands/validate/scope.js +113 -0
- package/dist/commands/validate/scope.js.map +1 -0
- package/dist/commands/validate/service/api.js +16 -104
- package/dist/commands/validate/service/api.js.map +1 -1
- package/dist/commands/validate/service/contracts/generated.js +134 -0
- package/dist/commands/validate/service/contracts/generated.js.map +1 -0
- package/dist/commands/validate/service/events/events.js +1 -1
- package/dist/commands/validate/service/events/events.js.map +1 -1
- package/dist/commands/validate/service/events/message-producers.js +1 -1
- package/dist/commands/validate/service/events/message-producers.js.map +1 -1
- package/dist/commands/validate/service/evidence-pins.js +56 -10
- package/dist/commands/validate/service/evidence-pins.js.map +1 -1
- package/dist/commands/validate/service/service.js +16 -26
- package/dist/commands/validate/service/service.js.map +1 -1
- package/dist/commands/validate/service/specs.js +31 -38
- package/dist/commands/validate/service/specs.js.map +1 -1
- package/dist/commands/validate/service/spine.js +8 -11
- package/dist/commands/validate/service/spine.js.map +1 -1
- package/dist/commands/validate/service/usecases/usecases.js +3 -3
- package/dist/commands/validate/service/usecases/usecases.js.map +1 -1
- package/dist/commands/validate/validate.js +44 -26
- package/dist/commands/validate/validate.js.map +1 -1
- package/dist/commands/verify/cross/diff.js +2 -6
- package/dist/commands/verify/cross/diff.js.map +1 -1
- package/dist/commands/verify/evidence/contract.js +1 -1
- package/dist/commands/verify/evidence/contract.js.map +1 -1
- package/dist/commands/verify/evidence/read.js +12 -3
- package/dist/commands/verify/evidence/read.js.map +1 -1
- package/dist/commands/verify/frozen.js +45 -23
- package/dist/commands/verify/frozen.js.map +1 -1
- package/dist/commands/verify/record.js +69 -17
- package/dist/commands/verify/record.js.map +1 -1
- package/dist/commands/verify/report.js +27 -44
- package/dist/commands/verify/report.js.map +1 -1
- package/dist/commands/verify/results.js +112 -30
- package/dist/commands/verify/results.js.map +1 -1
- package/dist/commands/verify/verify.js +54 -51
- package/dist/commands/verify/verify.js.map +1 -1
- package/dist/commands/vouch/pack/pack.js +2 -2
- package/dist/commands/vouch/pack/pack.js.map +1 -1
- package/dist/commands/vouch/pack/print.js +1 -6
- package/dist/commands/vouch/pack/print.js.map +1 -1
- package/dist/commands/vouch/pack/sections.js +1 -2
- package/dist/commands/vouch/pack/sections.js.map +1 -1
- package/dist/commands/vouch/run.js +26 -81
- package/dist/commands/vouch/run.js.map +1 -1
- package/dist/commands/vouch/sample/plan.js +20 -34
- package/dist/commands/vouch/sample/plan.js.map +1 -1
- package/dist/commands/vouch/vet/verify.js +39 -7
- package/dist/commands/vouch/vet/verify.js.map +1 -1
- package/dist/commands/vouch/vouch.js +10 -17
- package/dist/commands/vouch/vouch.js.map +1 -1
- package/dist/core/agent/agents-md/command-map.js +57 -102
- package/dist/core/agent/agents-md/command-map.js.map +1 -1
- package/dist/core/agent/agents-md/map/explain.js +4 -3
- package/dist/core/agent/agents-md/map/explain.js.map +1 -1
- package/dist/core/agent/agents-md/map/lenses/context.js +4 -3
- package/dist/core/agent/agents-md/map/lenses/context.js.map +1 -1
- package/dist/core/agent/agents-md/map/lenses/diff.js +33 -20
- package/dist/core/agent/agents-md/map/lenses/diff.js.map +1 -1
- package/dist/core/agent/agents-md/map/lenses/gate.js +13 -5
- package/dist/core/agent/agents-md/map/lenses/gate.js.map +1 -1
- package/dist/core/agent/agents-md/map/mcp.js +10 -7
- package/dist/core/agent/agents-md/map/mcp.js.map +1 -1
- package/dist/core/agent/agents-md/map/status.js +9 -3
- package/dist/core/agent/agents-md/map/status.js.map +1 -1
- package/dist/core/agent/agents-md/map/subsystem.js +31 -17
- package/dist/core/agent/agents-md/map/subsystem.js.map +1 -1
- package/dist/core/agent/agents-md/map/usecases.js +24 -6
- package/dist/core/agent/agents-md/map/usecases.js.map +1 -1
- package/dist/core/agent/agents-md/refusals.js +37 -21
- package/dist/core/agent/agents-md/refusals.js.map +1 -1
- package/dist/core/agent/agents-md.js +25 -10
- package/dist/core/agent/agents-md.js.map +1 -1
- package/dist/core/agent/agents-stamp.js +10 -0
- package/dist/core/agent/agents-stamp.js.map +1 -1
- package/dist/core/agent/scaffold.js +10 -15
- package/dist/core/agent/scaffold.js.map +1 -1
- package/dist/core/agent/tools/dialects.js +1 -0
- package/dist/core/agent/tools/dialects.js.map +1 -1
- package/dist/core/agent/tools/registry.js +41 -76
- package/dist/core/agent/tools/registry.js.map +1 -1
- package/dist/core/agent/workflows/adopt.js +5 -3
- package/dist/core/agent/workflows/adopt.js.map +1 -1
- package/dist/core/agent/workflows/check.js +25 -22
- package/dist/core/agent/workflows/check.js.map +1 -1
- package/dist/core/agent/workflows/closing.js +54 -21
- package/dist/core/agent/workflows/closing.js.map +1 -1
- package/dist/core/agent/workflows/feature.js +79 -11
- package/dist/core/agent/workflows/feature.js.map +1 -1
- package/dist/core/agent/workflows/implement.js +91 -13
- package/dist/core/agent/workflows/implement.js.map +1 -1
- package/dist/core/agent/workflows/reference/authoring.js +3 -1
- package/dist/core/agent/workflows/reference/authoring.js.map +1 -1
- package/dist/core/agent/workflows/reference/codes.js +8 -6
- package/dist/core/agent/workflows/reference/codes.js.map +1 -1
- package/dist/core/agent/workflows/reference/done-check.js +37 -15
- package/dist/core/agent/workflows/reference/done-check.js.map +1 -1
- package/dist/core/agent/workflows/reference/reference.js.map +1 -1
- package/dist/core/agent/workflows/reference/spine.js +62 -24
- package/dist/core/agent/workflows/reference/spine.js.map +1 -1
- package/dist/core/asyncapi/baseline/plan.js +14 -81
- package/dist/core/asyncapi/baseline/plan.js.map +1 -1
- package/dist/core/asyncapi/digest.js +30 -25
- package/dist/core/asyncapi/digest.js.map +1 -1
- package/dist/core/asyncapi/merge/components.js +5 -4
- package/dist/core/asyncapi/merge/components.js.map +1 -1
- package/dist/core/asyncapi/merge/error.js +7 -2
- package/dist/core/asyncapi/merge/error.js.map +1 -1
- package/dist/core/asyncapi/merge/markers.js +30 -50
- package/dist/core/asyncapi/merge/markers.js.map +1 -1
- package/dist/core/asyncapi/merge/merge.js +31 -53
- package/dist/core/asyncapi/merge/merge.js.map +1 -1
- package/dist/core/asyncapi/merge/pin.js +10 -36
- package/dist/core/asyncapi/merge/pin.js.map +1 -1
- package/dist/core/asyncapi/model.js.map +1 -1
- package/dist/core/asyncapi/read.js +36 -12
- package/dist/core/asyncapi/read.js.map +1 -1
- package/dist/core/brief/brief.js +30 -25
- package/dist/core/brief/brief.js.map +1 -1
- package/dist/core/brief/landscape.js +8 -12
- package/dist/core/brief/landscape.js.map +1 -1
- package/dist/core/brief/map/extend.js +8 -5
- package/dist/core/brief/map/extend.js.map +1 -1
- package/dist/core/brief/map/owed.js +8 -26
- package/dist/core/brief/map/owed.js.map +1 -1
- package/dist/core/brief/targets.js +79 -5
- package/dist/core/brief/targets.js.map +1 -1
- package/dist/core/brief/unchecked.js +1 -1
- package/dist/core/brief/unchecked.js.map +1 -1
- package/dist/core/c4/likec4.js +147 -16
- package/dist/core/c4/likec4.js.map +1 -1
- package/dist/core/c4/locate/unlocated.js +150 -0
- package/dist/core/c4/locate/unlocated.js.map +1 -0
- package/dist/core/c4/parsed/values.js +54 -1
- package/dist/core/c4/parsed/values.js.map +1 -1
- package/dist/core/c4/prefetch/fleet.js +1 -9
- package/dist/core/c4/prefetch/fleet.js.map +1 -1
- package/dist/core/c4/project/architecture.js +31 -9
- package/dist/core/c4/project/architecture.js.map +1 -1
- package/dist/core/c4/project/documents.js +36 -10
- package/dist/core/c4/project/documents.js.map +1 -1
- package/dist/core/c4/project/load.js +14 -21
- package/dist/core/c4/project/load.js.map +1 -1
- package/dist/core/c4/project/staged.js +7 -3
- package/dist/core/c4/project/staged.js.map +1 -1
- package/dist/core/c4/resolve/external.js +51 -0
- package/dist/core/c4/resolve/external.js.map +1 -0
- package/dist/core/c4/resolve/service.js +25 -1
- package/dist/core/c4/resolve/service.js.map +1 -1
- package/dist/core/c4/seed/template.js +15 -1
- package/dist/core/c4/seed/template.js.map +1 -1
- package/dist/core/c4/service-model/shape.js +2 -22
- package/dist/core/c4/service-model/shape.js.map +1 -1
- package/dist/core/c4/source-mask.js +288 -4
- package/dist/core/c4/source-mask.js.map +1 -1
- package/dist/core/c4/source-scan.js +203 -147
- package/dist/core/c4/source-scan.js.map +1 -1
- package/dist/core/c4/splice/authored-source.js +19 -0
- package/dist/core/c4/splice/authored-source.js.map +1 -1
- package/dist/core/c4/splice/delta-blocks.js +5 -18
- package/dist/core/c4/splice/delta-blocks.js.map +1 -1
- package/dist/core/c4/splice/landscape-merge.js +13 -8
- package/dist/core/c4/splice/landscape-merge.js.map +1 -1
- package/dist/core/c4/splice/model/merge.js +10 -6
- package/dist/core/c4/splice/model/merge.js.map +1 -1
- package/dist/core/c4/splice/placement.js +108 -18
- package/dist/core/c4/splice/placement.js.map +1 -1
- package/dist/core/c4/workspace.js +13 -22
- package/dist/core/c4/workspace.js.map +1 -1
- package/dist/core/capabilities/capabilities.js +6 -6
- package/dist/core/capabilities/capabilities.js.map +1 -1
- package/dist/core/capabilities/delta/doc.js +7 -10
- package/dist/core/capabilities/delta/doc.js.map +1 -1
- package/dist/core/capabilities/findings.js +15 -20
- package/dist/core/capabilities/findings.js.map +1 -1
- package/dist/core/capabilities/realizes/findings.js +12 -19
- package/dist/core/capabilities/realizes/findings.js.map +1 -1
- package/dist/core/capabilities/realizes/join.js +0 -20
- package/dist/core/capabilities/realizes/join.js.map +1 -1
- package/dist/core/capabilities/rollup.js +6 -0
- package/dist/core/capabilities/rollup.js.map +1 -1
- package/dist/core/capabilities/tree.js +6 -28
- package/dist/core/capabilities/tree.js.map +1 -1
- package/dist/core/capabilities/usecase-join.js +3 -13
- package/dist/core/capabilities/usecase-join.js.map +1 -1
- package/dist/core/coherence/authoring/scaffold.js +11 -3
- package/dist/core/coherence/authoring/scaffold.js.map +1 -1
- package/dist/core/coherence/authoring/sentinels.js +2 -1
- package/dist/core/coherence/authoring/sentinels.js.map +1 -1
- package/dist/core/coherence/coherence.js +30 -16
- package/dist/core/coherence/coherence.js.map +1 -1
- package/dist/core/coherence/consumed/operations.js +70 -0
- package/dist/core/coherence/consumed/operations.js.map +1 -0
- package/dist/core/coherence/consumed/refusal.js +38 -0
- package/dist/core/coherence/consumed/refusal.js.map +1 -0
- package/dist/core/coherence/declared.js +60 -34
- package/dist/core/coherence/declared.js.map +1 -1
- package/dist/core/coherence/events/declared.js +44 -59
- package/dist/core/coherence/events/declared.js.map +1 -1
- package/dist/core/coherence/events/events.js +119 -8
- package/dist/core/coherence/events/events.js.map +1 -1
- package/dist/core/coherence/events/grades.js +31 -8
- package/dist/core/coherence/events/grades.js.map +1 -1
- package/dist/core/coherence/events/lookups.js +14 -82
- package/dist/core/coherence/events/lookups.js.map +1 -1
- package/dist/core/coherence/lookups.js +18 -78
- package/dist/core/coherence/lookups.js.map +1 -1
- package/dist/core/coherence/retired/archived.js +285 -0
- package/dist/core/coherence/retired/archived.js.map +1 -0
- package/dist/core/coherence/retired/event-pair.js +62 -0
- package/dist/core/coherence/retired/event-pair.js.map +1 -0
- package/dist/core/coherence/retired/merged.js +79 -0
- package/dist/core/coherence/retired/merged.js.map +1 -0
- package/dist/core/coherence/retired/retirement.js +49 -0
- package/dist/core/coherence/retired/retirement.js.map +1 -0
- package/dist/core/coherence/retired/revival/arrivals.js +131 -0
- package/dist/core/coherence/retired/revival/arrivals.js.map +1 -0
- package/dist/core/coherence/retired/revival/revived.js +297 -0
- package/dist/core/coherence/retired/revival/revived.js.map +1 -0
- package/dist/core/consumers/after.js +113 -0
- package/dist/core/consumers/after.js.map +1 -0
- package/dist/core/consumers/consumers.js +296 -0
- package/dist/core/consumers/consumers.js.map +1 -0
- package/dist/core/consumers/current.js +211 -0
- package/dist/core/consumers/current.js.map +1 -0
- package/dist/core/consumers/scan.js +46 -0
- package/dist/core/consumers/scan.js.map +1 -0
- package/dist/core/consumers/state.js +27 -0
- package/dist/core/consumers/state.js.map +1 -0
- package/dist/core/delta/delta.js +1 -1
- package/dist/core/delta/delta.js.map +1 -1
- package/dist/core/deployment/delta.js +15 -16
- package/dist/core/deployment/delta.js.map +1 -1
- package/dist/core/diff/base-git.js +94 -12
- package/dist/core/diff/base-git.js.map +1 -1
- package/dist/core/diff/base-state.js +28 -16
- package/dist/core/diff/base-state.js.map +1 -1
- package/dist/core/diff/joins.js +136 -0
- package/dist/core/diff/joins.js.map +1 -0
- package/dist/core/diff/scope/changed-paths.js +22 -6
- package/dist/core/diff/scope/changed-paths.js.map +1 -1
- package/dist/core/diff/scope/edges.js +82 -0
- package/dist/core/diff/scope/edges.js.map +1 -0
- package/dist/core/diff/semantic.js +100 -31
- package/dist/core/diff/semantic.js.map +1 -1
- package/dist/core/doctor/agents.js +34 -18
- package/dist/core/doctor/agents.js.map +1 -1
- package/dist/core/doctor/report.js.map +1 -1
- package/dist/core/document/apply.js +10 -2
- package/dist/core/document/apply.js.map +1 -1
- package/dist/core/document/frontmatter.js +55 -0
- package/dist/core/document/frontmatter.js.map +1 -1
- package/dist/core/document/parse.js +29 -6
- package/dist/core/document/parse.js.map +1 -1
- package/dist/core/document/scenarios.js +17 -0
- package/dist/core/document/scenarios.js.map +1 -1
- package/dist/core/document/spec.js +4 -11
- package/dist/core/document/spec.js.map +1 -1
- package/dist/core/envelope/config.js +10 -0
- package/dist/core/envelope/config.js.map +1 -1
- package/dist/core/envelope/json.js +34 -0
- package/dist/core/envelope/json.js.map +1 -1
- package/dist/core/explain/families.js +17 -12
- package/dist/core/explain/families.js.map +1 -1
- package/dist/core/explain/refusals.js +3 -3
- package/dist/core/explain/refusals.js.map +1 -1
- package/dist/core/explain/terms.js +18 -9
- package/dist/core/explain/terms.js.map +1 -1
- package/dist/core/explore/explore.js +2 -7
- package/dist/core/explore/explore.js.map +1 -1
- package/dist/core/fleet-context.js +34 -95
- package/dist/core/fleet-context.js.map +1 -1
- package/dist/core/gate/checks.js +10 -3
- package/dist/core/gate/checks.js.map +1 -1
- package/dist/core/gate/partners.js +64 -19
- package/dist/core/gate/partners.js.map +1 -1
- package/dist/core/gate/report.js.map +1 -1
- package/dist/core/gate/verification.js +75 -12
- package/dist/core/gate/verification.js.map +1 -1
- package/dist/core/gherkin/stale.js +13 -37
- package/dist/core/gherkin/stale.js.map +1 -1
- package/dist/core/glossary/tree.js +6 -19
- package/dist/core/glossary/tree.js.map +1 -1
- package/dist/core/kernel/concurrency.js +19 -0
- package/dist/core/kernel/concurrency.js.map +1 -1
- package/dist/core/kernel/records.js +11 -0
- package/dist/core/kernel/records.js.map +1 -1
- package/dist/core/links/backlinks.js +1 -3
- package/dist/core/links/backlinks.js.map +1 -1
- package/dist/core/links/corpus.js +5 -36
- package/dist/core/links/corpus.js.map +1 -1
- package/dist/core/mcp/argv.js +101 -0
- package/dist/core/mcp/argv.js.map +1 -1
- package/dist/core/mcp/author-tools.js +2 -1
- package/dist/core/mcp/author-tools.js.map +1 -1
- package/dist/core/mcp/protocol.js +2 -2
- package/dist/core/mcp/protocol.js.map +1 -1
- package/dist/core/mcp/tools.js +43 -33
- package/dist/core/mcp/tools.js.map +1 -1
- package/dist/core/openapi/baseline/gate.js +53 -12
- package/dist/core/openapi/baseline/gate.js.map +1 -1
- package/dist/core/openapi/baseline/plan.js +22 -8
- package/dist/core/openapi/baseline/plan.js.map +1 -1
- package/dist/core/openapi/baseline/record.js +0 -1
- package/dist/core/openapi/baseline/record.js.map +1 -1
- package/dist/core/openapi/contract-text.js +33 -0
- package/dist/core/openapi/contract-text.js.map +1 -0
- package/dist/core/openapi/contract-tree.js +56 -0
- package/dist/core/openapi/contract-tree.js.map +1 -0
- package/dist/core/openapi/depth.js +38 -24
- package/dist/core/openapi/depth.js.map +1 -1
- package/dist/core/openapi/digest.js +52 -2
- package/dist/core/openapi/digest.js.map +1 -1
- package/dist/core/openapi/doc.js +12 -4
- package/dist/core/openapi/doc.js.map +1 -1
- package/dist/core/openapi/merge/components.js +25 -60
- package/dist/core/openapi/merge/components.js.map +1 -1
- package/dist/core/openapi/merge/error.js +3 -13
- package/dist/core/openapi/merge/error.js.map +1 -1
- package/dist/core/openapi/merge/markers.js +26 -30
- package/dist/core/openapi/merge/markers.js.map +1 -1
- package/dist/core/openapi/merge/merge.js +26 -52
- package/dist/core/openapi/merge/merge.js.map +1 -1
- package/dist/core/openapi/merge/pin.js +15 -35
- package/dist/core/openapi/merge/pin.js.map +1 -1
- package/dist/core/openspec/decide/verdict.js.map +1 -1
- package/dist/core/openspec/inventory.js +4 -5
- package/dist/core/openspec/inventory.js.map +1 -1
- package/dist/core/openspec/model/mapping.js +30 -28
- package/dist/core/openspec/model/mapping.js.map +1 -1
- package/dist/core/openspec/model/model.js +12 -7
- package/dist/core/openspec/model/model.js.map +1 -1
- package/dist/core/openspec/scan/changes.js +10 -4
- package/dist/core/openspec/scan/changes.js.map +1 -1
- package/dist/core/openspec/scan/config.js +7 -16
- package/dist/core/openspec/scan/config.js.map +1 -1
- package/dist/core/openspec/scan/shape.js +63 -19
- package/dist/core/openspec/scan/shape.js.map +1 -1
- package/dist/core/openspec/scan/walk.js +21 -38
- package/dist/core/openspec/scan/walk.js.map +1 -1
- package/dist/core/pack/living.js +2 -16
- package/dist/core/pack/living.js.map +1 -1
- package/dist/core/provenance/findings.js +48 -12
- package/dist/core/provenance/findings.js.map +1 -1
- package/dist/core/provenance/git.js +33 -57
- package/dist/core/provenance/git.js.map +1 -1
- package/dist/core/provenance/gitq/moves.js +71 -114
- package/dist/core/provenance/gitq/moves.js.map +1 -1
- package/dist/core/provenance/gitq/vouched-ancestor.js +2 -2
- package/dist/core/provenance/gitq/vouched-ancestor.js.map +1 -1
- package/dist/core/provenance/sources.js +23 -6
- package/dist/core/provenance/sources.js.map +1 -1
- package/dist/core/provenance/stamp.js +5 -17
- package/dist/core/provenance/stamp.js.map +1 -1
- package/dist/core/repo/entries.js.map +1 -1
- package/dist/core/repo/paths.js +34 -29
- package/dist/core/repo/paths.js.map +1 -1
- package/dist/core/repo/repo.js +24 -14
- package/dist/core/repo/repo.js.map +1 -1
- package/dist/core/repo/tree/find.js +12 -3
- package/dist/core/repo/tree/find.js.map +1 -1
- package/dist/core/repo/tree/fs.js +27 -1
- package/dist/core/repo/tree/fs.js.map +1 -1
- package/dist/core/repo/tree/render/body.js +45 -6
- package/dist/core/repo/tree/render/body.js.map +1 -1
- package/dist/core/repo/tree/render/service/header.js +125 -0
- package/dist/core/repo/tree/render/service/header.js.map +1 -0
- package/dist/core/repo/tree/render/service/peers.js +28 -0
- package/dist/core/repo/tree/render/service/peers.js.map +1 -0
- package/dist/core/repo/tree/render/service/view-id.js +67 -0
- package/dist/core/repo/tree/render/service/view-id.js.map +1 -0
- package/dist/core/repo/tree/render/service/views.js +78 -0
- package/dist/core/repo/tree/render/service/views.js.map +1 -0
- package/dist/core/repo/tree/render/stale.js +19 -2
- package/dist/core/repo/tree/render/stale.js.map +1 -1
- package/dist/core/repo/tree/render/views.js +102 -33
- package/dist/core/repo/tree/render/views.js.map +1 -1
- package/dist/core/repo/tree/walk.js +8 -30
- package/dist/core/repo/tree/walk.js.map +1 -1
- package/dist/core/results.js +47 -12
- package/dist/core/results.js.map +1 -1
- package/dist/core/staging/commit.js +10 -12
- package/dist/core/staging/commit.js.map +1 -1
- package/dist/core/staging/lock.js +74 -12
- package/dist/core/staging/lock.js.map +1 -1
- package/dist/core/staging/recovery/intent.js +2 -5
- package/dist/core/staging/recovery/intent.js.map +1 -1
- package/dist/core/staging/recovery/recover.js +60 -1
- package/dist/core/staging/recovery/recover.js.map +1 -1
- package/dist/core/staging/snapshot/manifest.js +152 -0
- package/dist/core/staging/snapshot/manifest.js.map +1 -0
- package/dist/core/staging/{snapshot.js → snapshot/snapshot.js} +10 -34
- package/dist/core/staging/snapshot/snapshot.js.map +1 -0
- package/dist/core/status/actions/execution.js +11 -0
- package/dist/core/status/actions/execution.js.map +1 -1
- package/dist/core/status/feature/artifacts.js +2 -2
- package/dist/core/status/feature/artifacts.js.map +1 -1
- package/dist/core/status/feature/feature.js +5 -11
- package/dist/core/status/feature/feature.js.map +1 -1
- package/dist/core/status/fleet/fleet.js +6 -2
- package/dist/core/status/fleet/fleet.js.map +1 -1
- package/dist/core/status/fleet/next.js +26 -3
- package/dist/core/status/fleet/next.js.map +1 -1
- package/dist/core/status/report.js.map +1 -1
- package/dist/core/status/verification.js +30 -15
- package/dist/core/status/verification.js.map +1 -1
- package/dist/core/tasks/packet.js +174 -0
- package/dist/core/tasks/packet.js.map +1 -0
- package/dist/core/tasks/record.js +43 -0
- package/dist/core/tasks/record.js.map +1 -0
- package/dist/core/tasks/sources.js +181 -0
- package/dist/core/tasks/sources.js.map +1 -0
- package/dist/core/tasks/stamps.js +76 -0
- package/dist/core/tasks/stamps.js.map +1 -0
- package/dist/core/tasks/unit.js +0 -0
- package/dist/core/tasks/unit.js.map +1 -0
- package/dist/core/usecases/capability.js +11 -14
- package/dist/core/usecases/capability.js.map +1 -1
- package/dist/core/usecases/delta/flows.js +5 -7
- package/dist/core/usecases/delta/flows.js.map +1 -1
- package/dist/core/usecases/fleet.js +178 -31
- package/dist/core/usecases/fleet.js.map +1 -1
- package/dist/core/usecases/operations.js +5 -4
- package/dist/core/usecases/operations.js.map +1 -1
- package/dist/core/usecases/service/flows.js +9 -6
- package/dist/core/usecases/service/flows.js.map +1 -1
- package/dist/core/usecases/suites/participants.js +83 -0
- package/dist/core/usecases/suites/participants.js.map +1 -0
- package/dist/core/usecases/suites/suites.js +45 -0
- package/dist/core/usecases/suites/suites.js.map +1 -0
- package/dist/core/verify/answers.js +12 -5
- package/dist/core/verify/answers.js.map +1 -1
- package/dist/core/verify/build.js +102 -82
- package/dist/core/verify/build.js.map +1 -1
- package/dist/core/verify/checklist.js +45 -10
- package/dist/core/verify/checklist.js.map +1 -1
- package/dist/core/verify/location.js +67 -0
- package/dist/core/verify/location.js.map +1 -0
- package/dist/core/verify/pins/lint.js +3 -2
- package/dist/core/verify/pins/lint.js.map +1 -1
- package/dist/core/verify/pins/pin.js +1 -1
- package/dist/core/verify/pins/pin.js.map +1 -1
- package/dist/core/verify/read/file.js +224 -0
- package/dist/core/verify/read/file.js.map +1 -0
- package/dist/core/verify/{file.js → read/shape.js} +16 -104
- package/dist/core/verify/read/shape.js.map +1 -0
- package/dist/core/verify/read/union.js +105 -0
- package/dist/core/verify/read/union.js.map +1 -0
- package/dist/core/verify/record.js +35 -12
- package/dist/core/verify/record.js.map +1 -1
- package/dist/core/verify/store/commit.js +40 -30
- package/dist/core/verify/store/commit.js.map +1 -1
- package/dist/core/verify/store/render.js +8 -4
- package/dist/core/verify/store/render.js.map +1 -1
- package/dist/core/vocabulary/issue.js +1 -0
- package/dist/core/vocabulary/issue.js.map +1 -1
- package/dist/core/vocabulary/maturity.js +53 -3
- package/dist/core/vocabulary/maturity.js.map +1 -1
- package/examples/README.md +28 -9
- package/examples/docs/architecture/landscape.likec4 +21 -7
- package/examples/docs/architecture/services.likec4 +70 -0
- package/examples/docs/architecture/subsystems.likec4 +3 -0
- package/examples/docs/architecture/usecases/order-notification.likec4 +6 -2
- package/examples/docs/features/FEAT-101-payment-splitting/specs/payment-service/asyncapi.yaml +11 -8
- package/examples/docs/features/FEAT-101-payment-splitting/specs/payment-split-service/openapi.yaml +6 -6
- package/examples/docs/features/archive/FEAT-088-refunds/specs/payment-service/openapi.yaml +13 -10
- package/examples/docs/services/order-service/asyncapi.yaml +3 -2
- package/examples/docs/services/order-service/model.likec4 +10 -8
- package/examples/docs/services/payment-service/model.likec4 +6 -4
- package/examples/docs/services/platform/identity-service/model.likec4 +9 -7
- package/examples/docs/services/platform/notification-service/asyncapi.yaml +2 -1
- package/examples/docs/services/platform/notification-service/model.likec4 +4 -5
- package/package.json +1 -1
- package/dist/commands/validate/checks/vocabulary.js +0 -16
- package/dist/commands/validate/checks/vocabulary.js.map +0 -1
- package/dist/core/agent/agents-md/artifacts.js +0 -221
- package/dist/core/agent/agents-md/artifacts.js.map +0 -1
- package/dist/core/agent/agents-md/cycle.js +0 -180
- package/dist/core/agent/agents-md/cycle.js.map +0 -1
- package/dist/core/agent/agents-md/shipped/archive-gate.js +0 -98
- package/dist/core/agent/agents-md/shipped/archive-gate.js.map +0 -1
- package/dist/core/agent/agents-md/spine.js +0 -40
- package/dist/core/agent/agents-md/spine.js.map +0 -1
- package/dist/core/coherence/events/merged.js +0 -40
- package/dist/core/coherence/events/merged.js.map +0 -1
- package/dist/core/diff/current-state.js +0 -96
- package/dist/core/diff/current-state.js.map +0 -1
- package/dist/core/diff/victims.js +0 -256
- package/dist/core/diff/victims.js.map +0 -1
- package/dist/core/staging/snapshot.js.map +0 -1
- package/dist/core/verify/file.js.map +0 -1
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
import { ancestorIds } from "../../kernel/ids/fqn/ancestors.js";
|
|
2
|
+
import { compareIds } from "../../repo/entries.js";
|
|
3
|
+
import { EXTERNAL_TAG } from "../../vocabulary/maturity.js";
|
|
4
|
+
/**
|
|
5
|
+
* Every endpoint the views' steps draw, resolved through the scan's ONE
|
|
6
|
+
* element→service resolver — the resolver `attributeStep` grades the same hops
|
|
7
|
+
* with, built over the same enumerated fleet (`readUseCases`). A second
|
|
8
|
+
* resolver here could not disagree today and would be a second place to change.
|
|
9
|
+
*
|
|
10
|
+
* AN ENDPOINT THAT RESOLVES TO NO ENUMERATED SERVICE IS REPORTED, NEVER DROPPED.
|
|
11
|
+
* This answer is what a test environment is provisioned from, and a silently
|
|
12
|
+
* shorter list is the one failure that cannot be seen from the list itself: the
|
|
13
|
+
* stand comes up, the missing participant is simply absent, and the journey
|
|
14
|
+
* fails against something nobody was told to start. The actor who begins a
|
|
15
|
+
* flow, a topic in the middle, a partner's API — each is named here, so the
|
|
16
|
+
* reader decides whether it needs a double, a fixture or a directory. The
|
|
17
|
+
* caller that only wants seeds (`servicesInFlowsClaiming`) reads `services` and
|
|
18
|
+
* ignores the rest, which is a choice it can see itself making.
|
|
19
|
+
*
|
|
20
|
+
* Sorted with compareIds rather than in step order: a union has no document
|
|
21
|
+
* order to preserve, and a payload ordered by LikeC4's record order would
|
|
22
|
+
* reorder under a dependency bump.
|
|
23
|
+
*/
|
|
24
|
+
export function participantsOf(scan, views) {
|
|
25
|
+
const known = scan.model.known;
|
|
26
|
+
const services = new Set();
|
|
27
|
+
const unresolved = new Set();
|
|
28
|
+
for (const view of views) {
|
|
29
|
+
for (const step of view.steps) {
|
|
30
|
+
for (const id of [step.source, step.target]) {
|
|
31
|
+
const service = scan.resolve(id);
|
|
32
|
+
if (known.has(service))
|
|
33
|
+
services.add(service);
|
|
34
|
+
else
|
|
35
|
+
unresolved.add(id);
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
const byId = new Map(scan.model.elements.map((e) => [e.id, e]));
|
|
40
|
+
return {
|
|
41
|
+
services: [...services].sort(compareIds),
|
|
42
|
+
unresolved: [...unresolved].sort(compareIds).map((element) => ({ element, external: isExternal(element, byId, known) })),
|
|
43
|
+
};
|
|
44
|
+
}
|
|
45
|
+
/**
|
|
46
|
+
* `#external` on the element or on anything it is drawn inside — up to, and
|
|
47
|
+
* including, the first element that stands for a service.
|
|
48
|
+
*
|
|
49
|
+
* The walk is `core/gate/partners.ts`' reading of the same tag (`endFacts`),
|
|
50
|
+
* stop included, so `list suites` and the gate cannot disagree about whether
|
|
51
|
+
* an endpoint is ours. Walking up is what is true for a stand: `stripe.api`
|
|
52
|
+
* drawn inside an `#external` `stripe` is somebody else's endpoint whether or
|
|
53
|
+
* not the container repeats the tag, and LikeC4 does not inherit tags
|
|
54
|
+
* downward, so reading the element's own tags alone would print "ours, with no
|
|
55
|
+
* directory" over a partner's API. Stopping is just as true: an element bound
|
|
56
|
+
* with `metadata { service '<id>' }`, or titled for a real `services/<id>/`,
|
|
57
|
+
* IS a service, and what sits above it is a grouping whose `#external` is the
|
|
58
|
+
* grouping's claim, not the service's. Without the stop a service bound inside
|
|
59
|
+
* an `#external` zone and not yet given a directory was printed as "bring a
|
|
60
|
+
* double" while the gate graded it as ours. The tag on that element itself is
|
|
61
|
+
* read before the stop, as the gate reads it.
|
|
62
|
+
*
|
|
63
|
+
* "Stands for a service" is `commands/validate/fleet/census.ts`'
|
|
64
|
+
* `standsForService`, spelled here rather than imported because core may not
|
|
65
|
+
* reach into commands — as `core/gate/partners.ts` and `core/verify/checklist.ts`
|
|
66
|
+
* spell it.
|
|
67
|
+
*
|
|
68
|
+
* A tag a KIND declares (`element topic { #external }`) arrives on every element
|
|
69
|
+
* of that kind in the parsed model, so a topic counts without repeating it.
|
|
70
|
+
*/
|
|
71
|
+
function isExternal(element, byId, known) {
|
|
72
|
+
for (const id of ancestorIds(element)) {
|
|
73
|
+
const e = byId.get(id);
|
|
74
|
+
if (e === undefined)
|
|
75
|
+
continue;
|
|
76
|
+
if (e.tags.includes(EXTERNAL_TAG))
|
|
77
|
+
return true;
|
|
78
|
+
if (e.service !== undefined || known.has(e.title))
|
|
79
|
+
return false;
|
|
80
|
+
}
|
|
81
|
+
return false;
|
|
82
|
+
}
|
|
83
|
+
//# sourceMappingURL=participants.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"participants.js","sourceRoot":"","sources":["../../../../src/core/usecases/suites/participants.ts"],"names":[],"mappings":"AAoBA,OAAO,EAAE,WAAW,EAAE,MAAM,mCAAmC,CAAC;AAChE,OAAO,EAAE,UAAU,EAAE,MAAM,uBAAuB,CAAC;AACnD,OAAO,EAAE,YAAY,EAAE,MAAM,8BAA8B,CAAC;AA2B5D;;;;;;;;;;;;;;;;;;;GAmBG;AACH,MAAM,UAAU,cAAc,CAAC,IAAc,EAAE,KAA4B;IACzE,MAAM,KAAK,GAAG,IAAI,CAAC,KAAK,CAAC,KAAK,CAAC;IAC/B,MAAM,QAAQ,GAAG,IAAI,GAAG,EAAU,CAAC;IACnC,MAAM,UAAU,GAAG,IAAI,GAAG,EAAU,CAAC;IACrC,KAAK,MAAM,IAAI,IAAI,KAAK,EAAE,CAAC;QACzB,KAAK,MAAM,IAAI,IAAI,IAAI,CAAC,KAAK,EAAE,CAAC;YAC9B,KAAK,MAAM,EAAE,IAAI,CAAC,IAAI,CAAC,MAAM,EAAE,IAAI,CAAC,MAAM,CAAC,EAAE,CAAC;gBAC5C,MAAM,OAAO,GAAG,IAAI,CAAC,OAAO,CAAC,EAAE,CAAC,CAAC;gBACjC,IAAI,KAAK,CAAC,GAAG,CAAC,OAAO,CAAC;oBAAE,QAAQ,CAAC,GAAG,CAAC,OAAO,CAAC,CAAC;;oBACzC,UAAU,CAAC,GAAG,CAAC,EAAE,CAAC,CAAC;YAC1B,CAAC;QACH,CAAC;IACH,CAAC;IACD,MAAM,IAAI,GAAG,IAAI,GAAG,CAAC,IAAI,CAAC,KAAK,CAAC,QAAQ,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,CAAC,CAAC;IAChE,OAAO;QACL,QAAQ,EAAE,CAAC,GAAG,QAAQ,CAAC,CAAC,IAAI,CAAC,UAAU,CAAC;QACxC,UAAU,EAAE,CAAC,GAAG,UAAU,CAAC,CAAC,IAAI,CAAC,UAAU,CAAC,CAAC,GAAG,CAAC,CAAC,OAAO,EAAE,EAAE,CAAC,CAAC,EAAE,OAAO,EAAE,QAAQ,EAAE,UAAU,CAAC,OAAO,EAAE,IAAI,EAAE,KAAK,CAAC,EAAE,CAAC,CAAC;KACzH,CAAC;AACJ,CAAC;AAED;;;;;;;;;;;;;;;;;;;;;;;;;GAyBG;AACH,SAAS,UAAU,CAAC,OAAe,EAAE,IAA+B,EAAE,KAA0B;IAC9F,KAAK,MAAM,EAAE,IAAI,WAAW,CAAC,OAAO,CAAC,EAAE,CAAC;QACtC,MAAM,CAAC,GAAG,IAAI,CAAC,GAAG,CAAC,EAAE,CAAC,CAAC;QACvB,IAAI,CAAC,KAAK,SAAS;YAAE,SAAS;QAC9B,IAAI,CAAC,CAAC,IAAI,CAAC,QAAQ,CAAC,YAAY,CAAC;YAAE,OAAO,IAAI,CAAC;QAC/C,IAAI,CAAC,CAAC,OAAO,KAAK,SAAS,IAAI,KAAK,CAAC,GAAG,CAAC,CAAC,CAAC,KAAK,CAAC;YAAE,OAAO,KAAK,CAAC;IAClE,CAAC;IACD,OAAO,KAAK,CAAC;AACf,CAAC","sourcesContent":["/**\n * Who takes part in a set of use cases — the union a test environment is built\n * from, and the participants that union cannot place.\n *\n * It began as `servicesInFlowsClaiming` in `../capability.ts`, which answered\n * the same question for the flows claiming one capability and kept only the\n * half `loam explore` needed: the enumerated services. A suite asks it of a\n * different set of views — the ones carrying one `#suite-` tag — and needs the\n * other half too, so the walk moved here and grew a second output rather than\n * being written twice. Two walks over \"which services does this flow touch\"\n * would be two answers to the question a test stand is provisioned from.\n *\n * THE DEPENDENCY RUNS PARENT → CHILD, never back. `../capability.ts` and\n * `../fleet.ts` (which takes the suite prefix from `./suites.ts`) import this\n * package, so nothing here may value-import `core/usecases/` — the package\n * graph would close a cycle `import/no-cycle` cannot see (docs/DESIGN.md rule\n * 21). `UseCaseScan` crosses as a TYPE only, which erases.\n */\nimport type { Elem } from \"../../c4/likec4.js\";\nimport type { ParsedView } from \"../../c4/parsed/dynamic-views.js\";\nimport { ancestorIds } from \"../../kernel/ids/fqn/ancestors.js\";\nimport { compareIds } from \"../../repo/entries.js\";\nimport { EXTERNAL_TAG } from \"../../vocabulary/maturity.js\";\nimport type { UseCaseScan } from \"../fleet.js\";\n\n/** A scan that read — the only kind with elements to resolve against. */\nexport type ReadScan = Extract<UseCaseScan, { kind: \"read\" }>;\n\n/** A participant the element→service join could not land on an enumerated service. */\nexport interface UnresolvedParticipant {\n /** The element id a step drew, exactly as the view spells it. */\n element: string;\n /**\n * Does the element, or anything it is drawn inside short of a service, carry\n * `#external`? Then it is deliberately not the fleet's, and a stand needs a\n * double for it rather than a directory. See `isExternal` for why an\n * ancestor's tag counts and why the walk stops at a service.\n */\n external: boolean;\n}\n\n/** The union of a set of views' participants, split by whether the fleet can stand each one up. */\nexport interface Participants {\n /** Enumerated service ids, sorted and deduplicated. */\n services: string[];\n /** Everything else the steps drew, one entry per element id, sorted by it. */\n unresolved: UnresolvedParticipant[];\n}\n\n/**\n * Every endpoint the views' steps draw, resolved through the scan's ONE\n * element→service resolver — the resolver `attributeStep` grades the same hops\n * with, built over the same enumerated fleet (`readUseCases`). A second\n * resolver here could not disagree today and would be a second place to change.\n *\n * AN ENDPOINT THAT RESOLVES TO NO ENUMERATED SERVICE IS REPORTED, NEVER DROPPED.\n * This answer is what a test environment is provisioned from, and a silently\n * shorter list is the one failure that cannot be seen from the list itself: the\n * stand comes up, the missing participant is simply absent, and the journey\n * fails against something nobody was told to start. The actor who begins a\n * flow, a topic in the middle, a partner's API — each is named here, so the\n * reader decides whether it needs a double, a fixture or a directory. The\n * caller that only wants seeds (`servicesInFlowsClaiming`) reads `services` and\n * ignores the rest, which is a choice it can see itself making.\n *\n * Sorted with compareIds rather than in step order: a union has no document\n * order to preserve, and a payload ordered by LikeC4's record order would\n * reorder under a dependency bump.\n */\nexport function participantsOf(scan: ReadScan, views: readonly ParsedView[]): Participants {\n const known = scan.model.known;\n const services = new Set<string>();\n const unresolved = new Set<string>();\n for (const view of views) {\n for (const step of view.steps) {\n for (const id of [step.source, step.target]) {\n const service = scan.resolve(id);\n if (known.has(service)) services.add(service);\n else unresolved.add(id);\n }\n }\n }\n const byId = new Map(scan.model.elements.map((e) => [e.id, e]));\n return {\n services: [...services].sort(compareIds),\n unresolved: [...unresolved].sort(compareIds).map((element) => ({ element, external: isExternal(element, byId, known) })),\n };\n}\n\n/**\n * `#external` on the element or on anything it is drawn inside — up to, and\n * including, the first element that stands for a service.\n *\n * The walk is `core/gate/partners.ts`' reading of the same tag (`endFacts`),\n * stop included, so `list suites` and the gate cannot disagree about whether\n * an endpoint is ours. Walking up is what is true for a stand: `stripe.api`\n * drawn inside an `#external` `stripe` is somebody else's endpoint whether or\n * not the container repeats the tag, and LikeC4 does not inherit tags\n * downward, so reading the element's own tags alone would print \"ours, with no\n * directory\" over a partner's API. Stopping is just as true: an element bound\n * with `metadata { service '<id>' }`, or titled for a real `services/<id>/`,\n * IS a service, and what sits above it is a grouping whose `#external` is the\n * grouping's claim, not the service's. Without the stop a service bound inside\n * an `#external` zone and not yet given a directory was printed as \"bring a\n * double\" while the gate graded it as ours. The tag on that element itself is\n * read before the stop, as the gate reads it.\n *\n * \"Stands for a service\" is `commands/validate/fleet/census.ts`'\n * `standsForService`, spelled here rather than imported because core may not\n * reach into commands — as `core/gate/partners.ts` and `core/verify/checklist.ts`\n * spell it.\n *\n * A tag a KIND declares (`element topic { #external }`) arrives on every element\n * of that kind in the parsed model, so a topic counts without repeating it.\n */\nfunction isExternal(element: string, byId: ReadonlyMap<string, Elem>, known: ReadonlySet<string>): boolean {\n for (const id of ancestorIds(element)) {\n const e = byId.get(id);\n if (e === undefined) continue;\n if (e.tags.includes(EXTERNAL_TAG)) return true;\n if (e.service !== undefined || known.has(e.title)) return false;\n }\n return false;\n}\n"]}
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
import { compareIds } from "../../repo/entries.js";
|
|
2
|
+
import { participantsOf } from "./participants.js";
|
|
3
|
+
/**
|
|
4
|
+
* The reserved tag prefix, matched EXACTLY — case included — as `cap-` and
|
|
5
|
+
* `req-` are. Exported for `../fleet.ts`, where it joins the other two in the
|
|
6
|
+
* one opt-in predicate and the one byte gate in front of it.
|
|
7
|
+
*/
|
|
8
|
+
export const SUITE_TAG_PREFIX = "suite-";
|
|
9
|
+
/**
|
|
10
|
+
* The suite names one view declares: the tags with the prefix stripped,
|
|
11
|
+
* deduplicated and sorted.
|
|
12
|
+
*
|
|
13
|
+
* A bare `#suite-` is a suite named `''` and is KEPT, for the reason a bare
|
|
14
|
+
* `#cap-` stays a claim: the prefix is the opt-in however little follows it,
|
|
15
|
+
* and a view that asked to be in a suite must not silently be in none.
|
|
16
|
+
*/
|
|
17
|
+
function suiteNames(view) {
|
|
18
|
+
const names = view.tags.filter((tag) => tag.startsWith(SUITE_TAG_PREFIX)).map((tag) => tag.slice(SUITE_TAG_PREFIX.length));
|
|
19
|
+
return [...new Set(names)].sort(compareIds);
|
|
20
|
+
}
|
|
21
|
+
/**
|
|
22
|
+
* Group the scan's use cases by suite.
|
|
23
|
+
*
|
|
24
|
+
* The order is by (document, view id). `../place.ts`' `viewFile` is how a
|
|
25
|
+
* document is SPELLED, and it only prefixes `sourcePath` with a constant, so
|
|
26
|
+
* ordering on `sourcePath` itself orders identically without pointing this
|
|
27
|
+
* package back at its parent.
|
|
28
|
+
*/
|
|
29
|
+
export function suiteListing(scan) {
|
|
30
|
+
const ordered = [...scan.views].sort((a, b) => compareIds(a.sourcePath ?? "", b.sourcePath ?? "") || compareIds(a.id, b.id));
|
|
31
|
+
const byName = new Map();
|
|
32
|
+
const ungrouped = [];
|
|
33
|
+
for (const view of ordered) {
|
|
34
|
+
const names = suiteNames(view);
|
|
35
|
+
if (names.length === 0)
|
|
36
|
+
ungrouped.push(view);
|
|
37
|
+
for (const name of names)
|
|
38
|
+
byName.set(name, [...(byName.get(name) ?? []), view]);
|
|
39
|
+
}
|
|
40
|
+
const suites = [...byName]
|
|
41
|
+
.sort(([a], [b]) => compareIds(a, b))
|
|
42
|
+
.map(([name, views]) => ({ name, views, ...participantsOf(scan, views) }));
|
|
43
|
+
return { suites, ungrouped };
|
|
44
|
+
}
|
|
45
|
+
//# sourceMappingURL=suites.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"suites.js","sourceRoot":"","sources":["../../../../src/core/usecases/suites/suites.ts"],"names":[],"mappings":"AA+BA,OAAO,EAAE,UAAU,EAAE,MAAM,uBAAuB,CAAC;AACnD,OAAO,EAAE,cAAc,EAA6C,MAAM,mBAAmB,CAAC;AAE9F;;;;GAIG;AACH,MAAM,CAAC,MAAM,gBAAgB,GAAG,QAAQ,CAAC;AAEzC;;;;;;;GAOG;AACH,SAAS,UAAU,CAAC,IAAiC;IACnD,MAAM,KAAK,GAAG,IAAI,CAAC,IAAI,CAAC,MAAM,CAAC,CAAC,GAAG,EAAE,EAAE,CAAC,GAAG,CAAC,UAAU,CAAC,gBAAgB,CAAC,CAAC,CAAC,GAAG,CAAC,CAAC,GAAG,EAAE,EAAE,CAAC,GAAG,CAAC,KAAK,CAAC,gBAAgB,CAAC,MAAM,CAAC,CAAC,CAAC;IAC3H,OAAO,CAAC,GAAG,IAAI,GAAG,CAAC,KAAK,CAAC,CAAC,CAAC,IAAI,CAAC,UAAU,CAAC,CAAC;AAC9C,CAAC;AA6BD;;;;;;;GAOG;AACH,MAAM,UAAU,YAAY,CAAC,IAAc;IACzC,MAAM,OAAO,GAAG,CAAC,GAAG,IAAI,CAAC,KAAK,CAAC,CAAC,IAAI,CAClC,CAAC,CAAC,EAAE,CAAC,EAAE,EAAE,CAAC,UAAU,CAAC,CAAC,CAAC,UAAU,IAAI,EAAE,EAAE,CAAC,CAAC,UAAU,IAAI,EAAE,CAAC,IAAI,UAAU,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,EAAE,CAAC,CACvF,CAAC;IACF,MAAM,MAAM,GAAG,IAAI,GAAG,EAAwB,CAAC;IAC/C,MAAM,SAAS,GAAiB,EAAE,CAAC;IACnC,KAAK,MAAM,IAAI,IAAI,OAAO,EAAE,CAAC;QAC3B,MAAM,KAAK,GAAG,UAAU,CAAC,IAAI,CAAC,CAAC;QAC/B,IAAI,KAAK,CAAC,MAAM,KAAK,CAAC;YAAE,SAAS,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;QAC7C,KAAK,MAAM,IAAI,IAAI,KAAK;YAAE,MAAM,CAAC,GAAG,CAAC,IAAI,EAAE,CAAC,GAAG,CAAC,MAAM,CAAC,GAAG,CAAC,IAAI,CAAC,IAAI,EAAE,CAAC,EAAE,IAAI,CAAC,CAAC,CAAC;IAClF,CAAC;IACD,MAAM,MAAM,GAAG,CAAC,GAAG,MAAM,CAAC;SACvB,IAAI,CAAC,CAAC,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,UAAU,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC;SACpC,GAAG,CAAC,CAAC,CAAC,IAAI,EAAE,KAAK,CAAC,EAAE,EAAE,CAAC,CAAC,EAAE,IAAI,EAAE,KAAK,EAAE,GAAG,cAAc,CAAC,IAAI,EAAE,KAAK,CAAC,EAAE,CAAC,CAAC,CAAC;IAC7E,OAAO,EAAE,MAAM,EAAE,SAAS,EAAE,CAAC;AAC/B,CAAC","sourcesContent":["/**\n * Suites: which use cases run together, and what a run of one needs standing up.\n *\n * A use case joins a suite by carrying a `#suite-<name>` tag on its `dynamic\n * view`. A TAG rather than a directory, because suites are many-to-many — one\n * journey belongs to the smoke suite and the payments suite at once — and a\n * directory gives exactly one group. The tag also buys the typo check for free:\n * LikeC4 refuses an undeclared tag, so a suite declared once in the landscape's\n * `specification` block makes a mistyped suite a parse error, where a mistyped\n * directory would silently open a second, empty suite.\n *\n * A SEPARATE PREFIX, not `#cap-`. A capability says what a flow is FOR and may\n * be claimed once per meaning; a suite says what a flow is RUN WITH, and a\n * flow is routinely in several. Reusing the capability tag would make every\n * suite a business promise and every smoke run a capability, and the `#cap-`\n * resolution ladder (`../../capabilities/usecase-join.ts`) would convict every\n * suite name the vocabulary does not declare. The prefix also keeps the tag\n * clear of the feature-id grammar: a view tag is never read as a `#FEAT-`\n * marker (feature tags are read off elements and relationships only), and no\n * other reserved prefix — `cap-`, `req-`, `obl-` — is a prefix of this one or\n * has it as a prefix, so no tag can be claimed by two readers.\n *\n * TAGGING IS THE OPT-IN, and that is the owner's call recorded in SCHEMA.md: a\n * view carrying only `#suite-` tags IS a use case — `../fleet.ts`' `isUseCase`\n * admits it — and is graded hop by hop like any other. Nothing here grades; this\n * module groups the views the scan already admitted.\n *\n * Depends on its parent by TYPE only — `./participants.ts` says why the value\n * direction is parent → child.\n */\nimport type { ParsedView } from \"../../c4/parsed/dynamic-views.js\";\nimport { compareIds } from \"../../repo/entries.js\";\nimport { participantsOf, type ReadScan, type UnresolvedParticipant } from \"./participants.js\";\n\n/**\n * The reserved tag prefix, matched EXACTLY — case included — as `cap-` and\n * `req-` are. Exported for `../fleet.ts`, where it joins the other two in the\n * one opt-in predicate and the one byte gate in front of it.\n */\nexport const SUITE_TAG_PREFIX = \"suite-\";\n\n/**\n * The suite names one view declares: the tags with the prefix stripped,\n * deduplicated and sorted.\n *\n * A bare `#suite-` is a suite named `''` and is KEPT, for the reason a bare\n * `#cap-` stays a claim: the prefix is the opt-in however little follows it,\n * and a view that asked to be in a suite must not silently be in none.\n */\nfunction suiteNames(view: { tags: readonly string[] }): string[] {\n const names = view.tags.filter((tag) => tag.startsWith(SUITE_TAG_PREFIX)).map((tag) => tag.slice(SUITE_TAG_PREFIX.length));\n return [...new Set(names)].sort(compareIds);\n}\n\n/** One suite: its name, its use cases, and the environment their union needs. */\nexport interface Suite {\n /** The tag without `#suite-` — what `loam list suites <name>` takes. */\n name: string;\n /** The use cases carrying the tag, ordered by (document, view id). */\n views: ParsedView[];\n /** The enumerated services those use cases draw — what must be up. */\n services: string[];\n /** Participants that resolve to no enumerated service — named, never dropped. */\n unresolved: UnresolvedParticipant[];\n}\n\n/** Every suite the fleet's use cases declare, and the use cases in none. */\nexport interface SuiteListing {\n /** Sorted by name. */\n suites: Suite[];\n /**\n * The use cases carrying no `#suite-` tag, ordered by (document, view id).\n *\n * Answered because everything else here is per-suite: a journey in no suite\n * is absent from every suite's list, which is the one absence a per-suite\n * output cannot show. Legal and normal — an unfiled service is the same shape\n * — so it is named and never graded.\n */\n ungrouped: ParsedView[];\n}\n\n/**\n * Group the scan's use cases by suite.\n *\n * The order is by (document, view id). `../place.ts`' `viewFile` is how a\n * document is SPELLED, and it only prefixes `sourcePath` with a constant, so\n * ordering on `sourcePath` itself orders identically without pointing this\n * package back at its parent.\n */\nexport function suiteListing(scan: ReadScan): SuiteListing {\n const ordered = [...scan.views].sort(\n (a, b) => compareIds(a.sourcePath ?? \"\", b.sourcePath ?? \"\") || compareIds(a.id, b.id),\n );\n const byName = new Map<string, ParsedView[]>();\n const ungrouped: ParsedView[] = [];\n for (const view of ordered) {\n const names = suiteNames(view);\n if (names.length === 0) ungrouped.push(view);\n for (const name of names) byName.set(name, [...(byName.get(name) ?? []), view]);\n }\n const suites = [...byName]\n .sort(([a], [b]) => compareIds(a, b))\n .map(([name, views]) => ({ name, views, ...participantsOf(scan, views) }));\n return { suites, ungrouped };\n}\n"]}
|
|
@@ -36,6 +36,17 @@ export const VERDICTS = ["confirmed", "unconfirmed"];
|
|
|
36
36
|
* `attestedClaims` in `./record.ts` — exactly where it was.
|
|
37
37
|
*/
|
|
38
38
|
export const ANSWERED_BY = ["runner", "external-runner", "agent"];
|
|
39
|
+
/**
|
|
40
|
+
* The entries an answers file holds — a bare array, or an object with an
|
|
41
|
+
* `answers` array — or null for any other shape. One reading of the file's
|
|
42
|
+
* shape for `checkAnswers` and `--diff-answers`' off-lens note, so the two
|
|
43
|
+
* cannot disagree about which entries a file has.
|
|
44
|
+
*/
|
|
45
|
+
export function answerEntries(raw) {
|
|
46
|
+
if (Array.isArray(raw))
|
|
47
|
+
return raw;
|
|
48
|
+
return isRecord(raw) && Array.isArray(raw["answers"]) ? raw["answers"] : null;
|
|
49
|
+
}
|
|
39
50
|
/**
|
|
40
51
|
* Accept an answer set only if it answers THIS checklist: every claim once, no
|
|
41
52
|
* claim twice, nothing extra, and no "confirmed" that shows nothing.
|
|
@@ -66,11 +77,7 @@ export const ANSWERED_BY = ["runner", "external-runner", "agent"];
|
|
|
66
77
|
*/
|
|
67
78
|
export function checkAnswers(claims, raw, runnerOwned, context) {
|
|
68
79
|
const refuse = (code, message) => ({ ok: false, code, message });
|
|
69
|
-
const list =
|
|
70
|
-
? raw
|
|
71
|
-
: isRecord(raw) && Array.isArray(raw["answers"])
|
|
72
|
-
? raw["answers"]
|
|
73
|
-
: null;
|
|
80
|
+
const list = answerEntries(raw);
|
|
74
81
|
if (list === null) {
|
|
75
82
|
return refuse("answers-unreadable", "Expected a JSON array of answers, or an object with an `answers` array: " +
|
|
76
83
|
`[{ "id": "...", "verdict": "confirmed", "evidence": ["file:line"] }]`);
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"answers.js","sourceRoot":"","sources":["../../../src/core/verify/answers.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * The answers, and the one thing loam checks about them.\n *\n * loam does not judge an answer. It cannot: it never read the code. What it\n * guarantees is that every question was asked and that every answer sits next\n * to the question it answers — so an answer set that does not correspond to the\n * current checklist is refused rather than merged. An unchecked claim must\n * never be able to masquerade as checked, and that refusal is `checkAnswers`.\n *\n * A scenario claim is answered mechanically wherever it can be: `--results`\n * reads a cucumber JSON report and matches it by the digest tag `loam gherkin`\n * stamped, and only a green run confirms one (`core/results.ts`). Where it\n * cannot — a legacy service whose suite is months away, which is the fleet loam\n * exists for — `--record` may still take an agent's word for it, and\n * `./record.ts` marks that answer attested rather than run.\n */\nimport { isRecord } from \"../kernel/records.js\";\nimport { type Claim } from \"./checklist.js\";\nimport { type EvidencePin } from \"./pins/pin.js\";\n\n/**\n * Two verdicts, deliberately. Anything an agent cannot show evidence for is\n * unconfirmed — a third value for \"not applicable\" would absorb every claim\n * nobody wanted to answer.\n */\nexport const VERDICTS = [\"confirmed\", \"unconfirmed\"] as const;\nexport type Verdict = (typeof VERDICTS)[number];\n\n/**\n * Who answered a claim: the `runner` (a cucumber report's digest-tagged\n * scenarios, matched mechanically by `--results`), the `external-runner` (an\n * API contract-test report's operationId-matched entries, matched mechanically\n * by `--contract-results` — external because loam did not generate the suite\n * the way it generates the gherkin, so the join key is the operationId the\n * claim asserts rather than a digest loam stamped), or an `agent` (somebody's\n * word about the code, taken back by `--record`). On the record so a reviewer\n * can tell a green run from an assertion. `external-runner` never answers a\n * `scenario.tested` claim, which is what keeps the attested boundary —\n * `attestedClaims` in `./record.ts` — exactly where it was.\n */\nexport const ANSWERED_BY = [\"runner\", \"external-runner\", \"agent\"] as const;\nexport type AnsweredBy = (typeof ANSWERED_BY)[number];\n\nexport interface Answer {\n id: string;\n verdict: Verdict;\n /** Where it can be seen — `file:line`, or a report scenario for the runner. Required for `confirmed`. */\n evidence: string[];\n note?: string;\n answered_by: AnsweredBy;\n /**\n * loam's own stamp, never caller input: `validateServiceEvidence` computes\n * these from the attested commit's blobs and the record path attaches them.\n * `checkAnswers` below deliberately never reads the field, so an\n * `evidence_pins` key in an answers FILE is ignored, not honored — an agent\n * must not be able to hand loam a pin loam did not derive.\n */\n evidence_pins?: EvidencePin[];\n}\n\n/** Why an answer set was refused. Each names a different way it fails to answer. */\nexport type AnswerRefusal = \"answers-unreadable\" | \"answers-mismatch\" | \"answers-unevidenced\";\n\nexport type AnswerCheck =\n /** The answers, re-ordered to the checklist, one per claim. */\n | { ok: true; answers: Answer[] }\n | { ok: false; code: AnswerRefusal; message: string };\n\n/**\n * Accept an answer set only if it answers THIS checklist: every claim once, no\n * claim twice, nothing extra, and no \"confirmed\" that shows nothing.\n *\n * The refusals are the point of the command. If a stale or partial answer file\n * could be recorded, `verification.yaml` would say a claim was checked when\n * nobody ever asked it — and a record that can lie is worse than no record,\n * because it looks like evidence.\n *\n * `runnerOwned` is the composition rule under `--results` and\n * `--contract-results`: ids a mechanical report answers — scenario claims under\n * the first flag, api.exposes claims under the second — which an answers file\n * must therefore not touch. An entry naming one refuses with its own diagnosis\n * — the id IS on the feature's checklist, so calling it unknown would send the\n * caller hunting a staleness that is not there. `--diff-answers`\n * (commands/verify/cross/diff.ts) calls this with it UNSET on purpose: under a\n * diff both agents answer everything in scope, scenario claims included,\n * because the diff consults no report — so making scenario claims runner-only\n * even without `--results` would break that lens.\n *\n * `context` is what turns the last line from a circle into an instruction. In a\n * fleet the commonest way to \"miss\" a claim is that it is not yours: the\n * checklist spans ten repositories and this one can only answer its own. Told\n * only to \"answer the claims it lists\", an agent in the payment-service repo\n * re-runs verify, sees the same checkout-web claims it cannot speak for, and\n * either loops or invents evidence. So when every unanswered claim belongs to\n * another service, say THAT, and name the command that records them.\n */\nexport function checkAnswers(\n claims: Claim[],\n raw: unknown,\n runnerOwned?: ReadonlySet<string>,\n context?: { feature: string; service?: string },\n): AnswerCheck {\n const refuse = (code: AnswerRefusal, message: string): AnswerCheck => ({ ok: false, code, message });\n\n const list = Array.isArray(raw)\n ? raw\n : isRecord(raw) && Array.isArray(raw[\"answers\"])\n ? (raw[\"answers\"] as unknown[])\n : null;\n if (list === null) {\n return refuse(\n \"answers-unreadable\",\n \"Expected a JSON array of answers, or an object with an `answers` array: \" +\n `[{ \"id\": \"...\", \"verdict\": \"confirmed\", \"evidence\": [\"file:line\"] }]`,\n );\n }\n\n const answers: Answer[] = [];\n for (const [i, entry] of list.entries()) {\n const where = `answer ${i + 1}`;\n if (!isRecord(entry)) return refuse(\"answers-unreadable\", `${where} is not an object`);\n const id = entry[\"id\"];\n if (typeof id !== \"string\" || id.length === 0) {\n return refuse(\"answers-unreadable\", `${where} has no claim id`);\n }\n const verdict = entry[\"verdict\"];\n if (typeof verdict !== \"string\" || !(VERDICTS as readonly string[]).includes(verdict)) {\n return refuse(\n \"answers-unreadable\",\n `${where} ('${id}') has verdict ${JSON.stringify(verdict)} — expected one of ${VERDICTS.join(\", \")}`,\n );\n }\n const note = typeof entry[\"note\"] === \"string\" ? entry[\"note\"].trim() : \"\";\n answers.push({\n id,\n verdict: verdict as Verdict,\n evidence: stringList(entry[\"evidence\"]),\n ...(note.length > 0 ? { note } : {}),\n answered_by: \"agent\",\n });\n }\n\n const byId = new Map<string, Answer>();\n const twice: string[] = [];\n for (const a of answers) {\n if (byId.has(a.id)) twice.push(a.id);\n else byId.set(a.id, a);\n }\n const known = new Set(claims.map((c) => c.id));\n const strayIds = [...new Set(answers.map((a) => a.id).filter((id) => !known.has(id)))];\n const runnerHit = strayIds.filter((id) => runnerOwned?.has(id) === true);\n const unknown = strayIds.filter((id) => runnerOwned?.has(id) !== true);\n const missingClaims = claims.filter((c) => !byId.has(c.id));\n const missing = missingClaims.map((c) => c.id);\n if (runnerHit.length + unknown.length + missing.length + twice.length > 0) {\n const parts = [\n runnerHit.length > 0\n ? `${runnerHit.length} answer(s) name claim(s) a report owns under --results or --contract-results: ${runnerHit.join(\", \")} — the report answers those; take them out of the answers file`\n : \"\",\n unknown.length > 0 ? `${unknown.length} answer(s) name a claim that is not on the checklist: ${unknown.join(\", \")}` : \"\",\n missing.length > 0 ? `${missing.length} claim(s) have no answer: ${missing.join(\", \")}` : \"\",\n twice.length > 0 ? `${twice.length} claim(s) answered more than once: ${[...new Set(twice)].join(\", \")}` : \"\",\n ].filter((s) => s.length > 0);\n return refuse(\n \"answers-mismatch\",\n `These answers do not match the checklist — ${parts.join(\"; \")}. ` +\n missingAdvice(missingClaims, claims.filter((c) => byId.has(c.id)), context),\n );\n }\n\n const unevidenced = claims\n .map((c) => byId.get(c.id)!)\n .filter((a) => a.verdict === \"confirmed\" && a.evidence.length === 0)\n .map((a) => a.id);\n if (unevidenced.length > 0) {\n return refuse(\n \"answers-unevidenced\",\n `${unevidenced.length} claim(s) are confirmed with no evidence: ${unevidenced.join(\", \")}. A confirmation with nothing behind it is an assertion — give a file:line, or answer 'unconfirmed'.`,\n );\n }\n\n // Checklist order, not the order they were written in: the record reads in the\n // order the questions were asked, whatever the agent did.\n return { ok: true, answers: claims.map((c) => byId.get(c.id)!) };\n}\n\n/**\n * The last sentence of an `answers-mismatch`. \"Re-run and answer the claims it\n * lists\" is sound advice only when the caller CAN answer them. In a fleet the\n * checklist is federated — a feature's claims are filed under the service whose\n * code answers them — so the honest diagnosis for a repo that answered\n * everything it owns is \"the rest is not yours\", together with the form that\n * records the rest. Anything else sends the caller round the same loop.\n */\nfunction missingAdvice(\n missing: Claim[],\n answered: Claim[],\n context?: { feature: string; service?: string },\n): string {\n const generic = \"Re-run `loam verify` and answer the claims it lists.\";\n if (context === undefined || missing.length === 0 || answered.length === 0) return generic;\n // The signature of \"I answered mine and the rest is not mine\": the services\n // that own the unanswered claims and the services that own the answered ones\n // do not overlap. A single-service checklist with one claim forgotten fails\n // this test and keeps the generic advice, which is the right advice there —\n // that claim really is the caller's to answer.\n const owners = [...new Set(missing.map((c) => c.subject))].sort();\n const covered = new Set(answered.map((c) => c.subject));\n if (owners.some((s) => covered.has(s))) return generic;\n const form = `\\`loam verify ${context.feature} --record answers.json --service <svc>\\``;\n const mine = context.service;\n return mine === undefined\n ? `Those claims are owned by ${owners.join(\", \")} — each service attests its own from its own repository (${form}); ` +\n \"this all-at-once form has to answer the whole checklist in one file.\"\n : `Every unanswered claim belongs to a different service (${owners.join(\", \")}) and this repository can only attest '${mine}' — ` +\n `run ${form} in each of those repositories instead.`;\n}\n\nfunction stringList(v: unknown): string[] {\n if (typeof v === \"string\") return v.trim().length > 0 ? [v.trim()] : [];\n if (!Array.isArray(v)) return [];\n return v.map((x) => String(x).trim()).filter((s) => s.length > 0);\n}\n\n"]}
|
|
1
|
+
{"version":3,"file":"answers.js","sourceRoot":"","sources":["../../../src/core/verify/answers.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * The answers, and the one thing loam checks about them.\n *\n * loam does not judge an answer. It cannot: it never read the code. What it\n * guarantees is that every question was asked and that every answer sits next\n * to the question it answers — so an answer set that does not correspond to the\n * current checklist is refused rather than merged. An unchecked claim must\n * never be able to masquerade as checked, and that refusal is `checkAnswers`.\n *\n * A scenario claim is answered mechanically wherever it can be: `--results`\n * reads a cucumber JSON report and matches it by the digest tag `loam gherkin`\n * stamped, and only a green run confirms one (`core/results.ts`). Where it\n * cannot — a legacy service whose suite is months away, which is the fleet loam\n * exists for — `--record` may still take an agent's word for it, and\n * `./record.ts` marks that answer attested rather than run.\n */\nimport { isRecord } from \"../kernel/records.js\";\nimport { type Claim } from \"./checklist.js\";\nimport { type EvidencePin } from \"./pins/pin.js\";\n\n/**\n * Two verdicts, deliberately. Anything an agent cannot show evidence for is\n * unconfirmed — a third value for \"not applicable\" would absorb every claim\n * nobody wanted to answer.\n */\nexport const VERDICTS = [\"confirmed\", \"unconfirmed\"] as const;\nexport type Verdict = (typeof VERDICTS)[number];\n\n/**\n * Who answered a claim: the `runner` (a cucumber report's digest-tagged\n * scenarios, matched mechanically by `--results`), the `external-runner` (an\n * API contract-test report's operationId-matched entries, matched mechanically\n * by `--contract-results` — external because loam did not generate the suite\n * the way it generates the gherkin, so the join key is the operationId the\n * claim asserts rather than a digest loam stamped), or an `agent` (somebody's\n * word about the code, taken back by `--record`). On the record so a reviewer\n * can tell a green run from an assertion. `external-runner` never answers a\n * `scenario.tested` claim, which is what keeps the attested boundary —\n * `attestedClaims` in `./record.ts` — exactly where it was.\n */\nexport const ANSWERED_BY = [\"runner\", \"external-runner\", \"agent\"] as const;\nexport type AnsweredBy = (typeof ANSWERED_BY)[number];\n\nexport interface Answer {\n id: string;\n verdict: Verdict;\n /** Where it can be seen — `file:line`, or a report scenario for the runner. Required for `confirmed`. */\n evidence: string[];\n note?: string;\n answered_by: AnsweredBy;\n /**\n * loam's own stamp, never caller input: `validateServiceEvidence` computes\n * these from the attested commit's blobs and the record path attaches them.\n * `checkAnswers` below deliberately never reads the field, so an\n * `evidence_pins` key in an answers FILE is ignored, not honored — an agent\n * must not be able to hand loam a pin loam did not derive.\n */\n evidence_pins?: EvidencePin[];\n}\n\n/**\n * The entries an answers file holds — a bare array, or an object with an\n * `answers` array — or null for any other shape. One reading of the file's\n * shape for `checkAnswers` and `--diff-answers`' off-lens note, so the two\n * cannot disagree about which entries a file has.\n */\nexport function answerEntries(raw: unknown): unknown[] | null {\n if (Array.isArray(raw)) return raw;\n return isRecord(raw) && Array.isArray(raw[\"answers\"]) ? (raw[\"answers\"] as unknown[]) : null;\n}\n\n/** Why an answer set was refused. Each names a different way it fails to answer. */\nexport type AnswerRefusal = \"answers-unreadable\" | \"answers-mismatch\" | \"answers-unevidenced\";\n\nexport type AnswerCheck =\n /** The answers, re-ordered to the checklist, one per claim. */\n | { ok: true; answers: Answer[] }\n | { ok: false; code: AnswerRefusal; message: string };\n\n/**\n * Accept an answer set only if it answers THIS checklist: every claim once, no\n * claim twice, nothing extra, and no \"confirmed\" that shows nothing.\n *\n * The refusals are the point of the command. If a stale or partial answer file\n * could be recorded, `verification.yaml` would say a claim was checked when\n * nobody ever asked it — and a record that can lie is worse than no record,\n * because it looks like evidence.\n *\n * `runnerOwned` is the composition rule under `--results` and\n * `--contract-results`: ids a mechanical report answers — scenario claims under\n * the first flag, api.exposes claims under the second — which an answers file\n * must therefore not touch. An entry naming one refuses with its own diagnosis\n * — the id IS on the feature's checklist, so calling it unknown would send the\n * caller hunting a staleness that is not there. `--diff-answers`\n * (commands/verify/cross/diff.ts) calls this with it UNSET on purpose: under a\n * diff both agents answer everything in scope, scenario claims included,\n * because the diff consults no report — so making scenario claims runner-only\n * even without `--results` would break that lens.\n *\n * `context` is what turns the last line from a circle into an instruction. In a\n * fleet the commonest way to \"miss\" a claim is that it is not yours: the\n * checklist spans ten repositories and this one can only answer its own. Told\n * only to \"answer the claims it lists\", an agent in the payment-service repo\n * re-runs verify, sees the same checkout-web claims it cannot speak for, and\n * either loops or invents evidence. So when every unanswered claim belongs to\n * another service, say THAT, and name the command that records them.\n */\nexport function checkAnswers(\n claims: Claim[],\n raw: unknown,\n runnerOwned?: ReadonlySet<string>,\n context?: { feature: string; service?: string },\n): AnswerCheck {\n const refuse = (code: AnswerRefusal, message: string): AnswerCheck => ({ ok: false, code, message });\n\n const list = answerEntries(raw);\n if (list === null) {\n return refuse(\n \"answers-unreadable\",\n \"Expected a JSON array of answers, or an object with an `answers` array: \" +\n `[{ \"id\": \"...\", \"verdict\": \"confirmed\", \"evidence\": [\"file:line\"] }]`,\n );\n }\n\n const answers: Answer[] = [];\n for (const [i, entry] of list.entries()) {\n const where = `answer ${i + 1}`;\n if (!isRecord(entry)) return refuse(\"answers-unreadable\", `${where} is not an object`);\n const id = entry[\"id\"];\n if (typeof id !== \"string\" || id.length === 0) {\n return refuse(\"answers-unreadable\", `${where} has no claim id`);\n }\n const verdict = entry[\"verdict\"];\n if (typeof verdict !== \"string\" || !(VERDICTS as readonly string[]).includes(verdict)) {\n return refuse(\n \"answers-unreadable\",\n `${where} ('${id}') has verdict ${JSON.stringify(verdict)} — expected one of ${VERDICTS.join(\", \")}`,\n );\n }\n const note = typeof entry[\"note\"] === \"string\" ? entry[\"note\"].trim() : \"\";\n answers.push({\n id,\n verdict: verdict as Verdict,\n evidence: stringList(entry[\"evidence\"]),\n ...(note.length > 0 ? { note } : {}),\n answered_by: \"agent\",\n });\n }\n\n const byId = new Map<string, Answer>();\n const twice: string[] = [];\n for (const a of answers) {\n if (byId.has(a.id)) twice.push(a.id);\n else byId.set(a.id, a);\n }\n const known = new Set(claims.map((c) => c.id));\n const strayIds = [...new Set(answers.map((a) => a.id).filter((id) => !known.has(id)))];\n const runnerHit = strayIds.filter((id) => runnerOwned?.has(id) === true);\n const unknown = strayIds.filter((id) => runnerOwned?.has(id) !== true);\n const missingClaims = claims.filter((c) => !byId.has(c.id));\n const missing = missingClaims.map((c) => c.id);\n if (runnerHit.length + unknown.length + missing.length + twice.length > 0) {\n const parts = [\n runnerHit.length > 0\n ? `${runnerHit.length} answer(s) name claim(s) a report owns under --results or --contract-results: ${runnerHit.join(\", \")} — the report answers those; take them out of the answers file`\n : \"\",\n unknown.length > 0 ? `${unknown.length} answer(s) name a claim that is not on the checklist: ${unknown.join(\", \")}` : \"\",\n missing.length > 0 ? `${missing.length} claim(s) have no answer: ${missing.join(\", \")}` : \"\",\n twice.length > 0 ? `${twice.length} claim(s) answered more than once: ${[...new Set(twice)].join(\", \")}` : \"\",\n ].filter((s) => s.length > 0);\n return refuse(\n \"answers-mismatch\",\n `These answers do not match the checklist — ${parts.join(\"; \")}. ` +\n missingAdvice(missingClaims, claims.filter((c) => byId.has(c.id)), context),\n );\n }\n\n const unevidenced = claims\n .map((c) => byId.get(c.id)!)\n .filter((a) => a.verdict === \"confirmed\" && a.evidence.length === 0)\n .map((a) => a.id);\n if (unevidenced.length > 0) {\n return refuse(\n \"answers-unevidenced\",\n `${unevidenced.length} claim(s) are confirmed with no evidence: ${unevidenced.join(\", \")}. A confirmation with nothing behind it is an assertion — give a file:line, or answer 'unconfirmed'.`,\n );\n }\n\n // Checklist order, not the order they were written in: the record reads in the\n // order the questions were asked, whatever the agent did.\n return { ok: true, answers: claims.map((c) => byId.get(c.id)!) };\n}\n\n/**\n * The last sentence of an `answers-mismatch`. \"Re-run and answer the claims it\n * lists\" is sound advice only when the caller CAN answer them. In a fleet the\n * checklist is federated — a feature's claims are filed under the service whose\n * code answers them — so the honest diagnosis for a repo that answered\n * everything it owns is \"the rest is not yours\", together with the form that\n * records the rest. Anything else sends the caller round the same loop.\n */\nfunction missingAdvice(\n missing: Claim[],\n answered: Claim[],\n context?: { feature: string; service?: string },\n): string {\n const generic = \"Re-run `loam verify` and answer the claims it lists.\";\n if (context === undefined || missing.length === 0 || answered.length === 0) return generic;\n // The signature of \"I answered mine and the rest is not mine\": the services\n // that own the unanswered claims and the services that own the answered ones\n // do not overlap. A single-service checklist with one claim forgotten fails\n // this test and keeps the generic advice, which is the right advice there —\n // that claim really is the caller's to answer.\n const owners = [...new Set(missing.map((c) => c.subject))].sort();\n const covered = new Set(answered.map((c) => c.subject));\n if (owners.some((s) => covered.has(s))) return generic;\n const form = `\\`loam verify ${context.feature} --record answers.json --service <svc>\\``;\n const mine = context.service;\n return mine === undefined\n ? `Those claims are owned by ${owners.join(\", \")} — each service attests its own from its own repository (${form}); ` +\n \"this all-at-once form has to answer the whole checklist in one file.\"\n : `Every unanswered claim belongs to a different service (${owners.join(\", \")}) and this repository can only attest '${mine}' — ` +\n `run ${form} in each of those repositories instead.`;\n}\n\nfunction stringList(v: unknown): string[] {\n if (typeof v === \"string\") return v.trim().length > 0 ? [v.trim()] : [];\n if (!Array.isArray(v)) return [];\n return v.map((x) => String(x).trim()).filter((s) => s.length > 0);\n}\n\n"]}
|
|
@@ -6,7 +6,10 @@
|
|
|
6
6
|
* because the two disagree about what an unanswered claim means: answering
|
|
7
7
|
* everything at once, a missing answer is a gap in this run, while a federated
|
|
8
8
|
* record is assembled from attestations that each covered one service and were
|
|
9
|
-
* never expected to cover the rest.
|
|
9
|
+
* never expected to cover the rest. They also land in different places: the
|
|
10
|
+
* all-at-once record is the one file `verification.yaml`, while each service's
|
|
11
|
+
* attestation is a file of its own under `verification/` (`./location.ts` says
|
|
12
|
+
* why, on `verificationDir`).
|
|
10
13
|
*/
|
|
11
14
|
import {} from "./checklist.js";
|
|
12
15
|
import {} from "./answers.js";
|
|
@@ -47,104 +50,121 @@ export function buildVerification(checklist, answers, today, reports = {}) {
|
|
|
47
50
|
...(reports.contract === undefined ? {} : { contractReport: reports.contract }),
|
|
48
51
|
};
|
|
49
52
|
}
|
|
50
|
-
|
|
53
|
+
/**
|
|
54
|
+
* One service's attestation, as the file only that service ever writes.
|
|
55
|
+
*
|
|
56
|
+
* This used to be a merge: the new attestation spliced into the one shared
|
|
57
|
+
* record, every other service's answers retained, pruned to the current
|
|
58
|
+
* checklist and re-sorted — and that rewrite of everybody's bytes is exactly
|
|
59
|
+
* what made two teams' docs PRs conflict. Now nothing else is touched. The
|
|
60
|
+
* other services' files stand as they are, the union is added up at read time
|
|
61
|
+
* (`read/union.ts`), and an answer of theirs the feature no longer asks is
|
|
62
|
+
* simply not counted there rather than pruned here.
|
|
63
|
+
*
|
|
64
|
+
* The file is a whole schema-2 record with one attestation, so it is graded
|
|
65
|
+
* with the same shape check as every record and reads without loam: its
|
|
66
|
+
* `summary` counts the whole checklist, with the claims other services answer
|
|
67
|
+
* honestly `unanswered` in it.
|
|
68
|
+
*/
|
|
69
|
+
export function buildServiceRecord(checklist, attestation, answers, previous) {
|
|
51
70
|
const { service, recorded, commit, reports, docsCommit } = attestation;
|
|
52
|
-
const
|
|
53
|
-
const
|
|
54
|
-
|
|
55
|
-
// attestation and therefore cannot silently become somebody else's
|
|
56
|
-
// federated contribution. The first schema-2 write starts a clean federation.
|
|
57
|
-
const previouslyAttested = new Set((previous?.attestations ?? []).flatMap((a) => a.claims));
|
|
58
|
-
const retained = (previous?.schema === 2 ? previous.claims : []).filter((claim) => {
|
|
59
|
-
const current = currentById.get(claim.id);
|
|
60
|
-
return current !== undefined && current.subject !== service && previouslyAttested.has(claim.id);
|
|
61
|
-
});
|
|
62
|
-
const local = answers.map((answer) => {
|
|
71
|
+
const byId = new Map(answers.map((answer) => [answer.id, answer]));
|
|
72
|
+
const currentIds = new Set(checklist.claims.map((claim) => claim.id));
|
|
73
|
+
for (const answer of answers) {
|
|
63
74
|
// Same contract as `buildVerification`: an answer is built from the live
|
|
64
75
|
// checklist, so it always names a current claim. Off-checklist answers do
|
|
65
76
|
// NOT belong here — `discarded` means "an answer the PREVIOUS record held",
|
|
66
77
|
// and routing an incoming stray into it would change what the printed
|
|
67
78
|
// notice tells an operator. A caller that reaches this gets told which
|
|
68
|
-
// answer it was rather than
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
throw new Error(`buildFederatedVerification: answer ${answer.id} names no current claim`);
|
|
79
|
+
// answer it was rather than an answer silently missing from the file.
|
|
80
|
+
if (!currentIds.has(answer.id)) {
|
|
81
|
+
throw new Error(`buildServiceRecord: answer ${answer.id} names no current claim`);
|
|
72
82
|
}
|
|
73
|
-
|
|
74
|
-
id: claim.id,
|
|
75
|
-
kind: claim.kind,
|
|
76
|
-
subject: claim.subject,
|
|
77
|
-
claim: claim.claim,
|
|
78
|
-
verdict: answer.verdict,
|
|
79
|
-
answered_by: answer.answered_by,
|
|
80
|
-
evidence: answer.evidence,
|
|
81
|
-
...(answer.note === undefined ? {} : { note: answer.note }),
|
|
82
|
-
// The pins the evidence validation stamped onto this repository's
|
|
83
|
-
// answers ride into the record here; retained other-service claims are
|
|
84
|
-
// spread whole below, so THEIR pins survive a re-record untouched.
|
|
85
|
-
...(answer.evidence_pins === undefined ? {} : { evidence_pins: answer.evidence_pins }),
|
|
86
|
-
};
|
|
87
|
-
});
|
|
88
|
-
const byId = new Map([...retained, ...local].map((claim) => [claim.id, claim]));
|
|
83
|
+
}
|
|
89
84
|
const claims = checklist.claims.flatMap((claim) => {
|
|
90
85
|
const answer = byId.get(claim.id);
|
|
91
86
|
if (answer === undefined)
|
|
92
87
|
return [];
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
88
|
+
return [
|
|
89
|
+
{
|
|
90
|
+
id: claim.id,
|
|
91
|
+
kind: claim.kind,
|
|
92
|
+
subject: claim.subject,
|
|
93
|
+
claim: claim.claim,
|
|
94
|
+
verdict: answer.verdict,
|
|
95
|
+
answered_by: answer.answered_by,
|
|
96
|
+
evidence: answer.evidence,
|
|
97
|
+
...(answer.note === undefined ? {} : { note: answer.note }),
|
|
98
|
+
// The pins the evidence validation stamped onto this repository's
|
|
99
|
+
// answers ride into the file here; no other service's file is
|
|
100
|
+
// rewritten, so THEIR pins are never touched at all.
|
|
101
|
+
...(answer.evidence_pins === undefined ? {} : { evidence_pins: answer.evidence_pins }),
|
|
102
|
+
},
|
|
103
|
+
];
|
|
102
104
|
});
|
|
103
|
-
const attestations = [
|
|
104
|
-
...retainedAttestations,
|
|
105
|
-
{
|
|
106
|
-
service,
|
|
107
|
-
commit,
|
|
108
|
-
recorded,
|
|
109
|
-
claims: checklist.claims.filter((c) => localIds.has(c.id)).map((c) => c.id),
|
|
110
|
-
// What THIS repository answered, and what it answered it against. Both
|
|
111
|
-
// ride with the attestation rather than the record for the reason the
|
|
112
|
-
// reports do: they are one repository's facts, pruned and retained with
|
|
113
|
-
// that repository's answers.
|
|
114
|
-
checklist: checklist.digest,
|
|
115
|
-
...(docsCommit === undefined ? {} : { docsCommit }),
|
|
116
|
-
// Filed with the attestation, not the record: each report is one
|
|
117
|
-
// repository's run, and it is pruned or retained with that repository's
|
|
118
|
-
// answers rather than outliving them — the contract report under the
|
|
119
|
-
// same rule as the cucumber one.
|
|
120
|
-
...(reports.results === undefined ? {} : { report: reports.results }),
|
|
121
|
-
...(reports.contract === undefined ? {} : { contractReport: reports.contract }),
|
|
122
|
-
},
|
|
123
|
-
].sort((a, b) => a.service.localeCompare(b.service));
|
|
124
105
|
const confirmed = claims.filter((claim) => claim.verdict === "confirmed").length;
|
|
125
|
-
const
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
106
|
+
const record = {
|
|
107
|
+
schema: 2,
|
|
108
|
+
feature: checklist.feature,
|
|
109
|
+
recorded,
|
|
110
|
+
checklist: checklist.digest,
|
|
111
|
+
summary: {
|
|
112
|
+
claims: checklist.claims.length,
|
|
113
|
+
confirmed,
|
|
114
|
+
unconfirmed: claims.length - confirmed,
|
|
115
|
+
unanswered: checklist.claims.length - claims.length,
|
|
116
|
+
},
|
|
117
|
+
claims,
|
|
118
|
+
attestations: [
|
|
119
|
+
{
|
|
120
|
+
service,
|
|
121
|
+
commit,
|
|
122
|
+
recorded,
|
|
123
|
+
claims: checklist.claims.filter((c) => c.subject === service).map((c) => c.id),
|
|
124
|
+
// What THIS repository answered, and what it answered it against.
|
|
125
|
+
// `checklist` is what `verify.checklist-forked` and the union's
|
|
126
|
+
// one-version rule read; `docsCommit` pins the side the question came
|
|
127
|
+
// from.
|
|
128
|
+
checklist: checklist.digest,
|
|
129
|
+
...(docsCommit === undefined ? {} : { docsCommit }),
|
|
130
|
+
// Each report is one repository's run, filed with that repository's
|
|
131
|
+
// attestation — the contract report under the same rule as the
|
|
132
|
+
// cucumber one.
|
|
133
|
+
...(reports.results === undefined ? {} : { report: reports.results }),
|
|
134
|
+
...(reports.contract === undefined ? {} : { contractReport: reports.contract }),
|
|
135
|
+
},
|
|
136
|
+
],
|
|
137
|
+
};
|
|
138
|
+
const { legacy, own, filed } = previous;
|
|
139
|
+
const federatedLegacy = legacy?.schema === 2;
|
|
140
|
+
const dropLegacy = legacy !== null &&
|
|
141
|
+
(!federatedLegacy || (legacy.attestations ?? []).every((a) => a.service === service || filed.has(a.service)));
|
|
142
|
+
// Everything this write takes off the disk that the new file does not carry:
|
|
143
|
+
// this service's own previous answers (from its file, or from its share of a
|
|
144
|
+
// one-file record no file of its own had superseded yet) and, when the
|
|
145
|
+
// one-file record is dropped, an all-at-once record's answers. Another
|
|
146
|
+
// service's answers are never in this list — their files are not touched.
|
|
147
|
+
const replaced = [
|
|
148
|
+
...(own?.claims ?? []),
|
|
149
|
+
...(legacy === null
|
|
150
|
+
? []
|
|
151
|
+
: !federatedLegacy
|
|
152
|
+
? legacy.claims
|
|
153
|
+
: own === null
|
|
154
|
+
? legacy.claims.filter((claim) => claim.subject === service)
|
|
155
|
+
: []),
|
|
156
|
+
];
|
|
157
|
+
const carried = new Set(claims.map((claim) => claim.id));
|
|
158
|
+
// Keyed by id: a service's own file and its share of the one-file record
|
|
159
|
+
// can both hold the same stale answer, and it is one answer.
|
|
160
|
+
const dropped = new Map(replaced.filter((claim) => !carried.has(claim.id)).map((claim) => [claim.id, claim]));
|
|
161
|
+
const discarded = [...dropped.values()]
|
|
131
162
|
.map((claim) => ({
|
|
132
163
|
id: claim.id,
|
|
133
164
|
claim: claim.claim,
|
|
134
165
|
...(claim.subject === undefined ? {} : { subject: claim.subject }),
|
|
135
|
-
reason:
|
|
166
|
+
reason: currentIds.has(claim.id) ? "unattested" : "off-checklist",
|
|
136
167
|
}));
|
|
137
|
-
return {
|
|
138
|
-
verification: {
|
|
139
|
-
schema: 2,
|
|
140
|
-
feature: checklist.feature,
|
|
141
|
-
recorded,
|
|
142
|
-
checklist: checklist.digest,
|
|
143
|
-
summary: { claims: checklist.claims.length, confirmed, unconfirmed, unanswered },
|
|
144
|
-
claims,
|
|
145
|
-
attestations,
|
|
146
|
-
},
|
|
147
|
-
discarded,
|
|
148
|
-
};
|
|
168
|
+
return { record, dropLegacy, discarded };
|
|
149
169
|
}
|
|
150
170
|
//# sourceMappingURL=build.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"build.js","sourceRoot":"","sources":["../../../src/core/verify/build.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * Checklist plus answers becomes a record — once for a feature answered all at\n * once, once for a fleet answering service by service.\n *\n * The federated form is separate from `buildVerification` and not a flag on it\n * because the two disagree about what an unanswered claim means: answering\n * everything at once, a missing answer is a gap in this run, while a federated\n * record is assembled from attestations that each covered one service and were\n * never expected to cover the rest.\n */\nimport { type Checklist } from \"./checklist.js\";\nimport { type Answer } from \"./answers.js\";\nimport {\n type ConsumedReports,\n type RecordedClaim,\n type ServiceAttestation,\n type Verification,\n} from \"./record.js\";\n\nexport function buildVerification(\n checklist: Checklist,\n answers: Answer[],\n today: string,\n reports: ConsumedReports = {},\n): Verification {\n const byId = new Map(answers.map((a) => [a.id, a]));\n const claims: RecordedClaim[] = checklist.claims.map((c) => {\n // Callers hand in an answer per claim — `runnerAnswers` is total over the\n // checklist, and both other callers map the checklist to build theirs. The\n // branch is here because the alternative to a diagnosis is a `TypeError` on\n // `a.verdict`, which cli.ts reports as `internal` with a stack about a\n // property of `undefined` and no word about which claim went missing.\n const a = byId.get(c.id);\n if (a === undefined) throw new Error(`buildVerification: no answer for claim ${c.id}`);\n // Deliberately no `evidence_pins` here: the legacy all-at-once form binds\n // to no commit, so there is no attested blob to pin — and none is ever\n // handed in, because pins are computed inside the federated evidence\n // validation this form never runs.\n return {\n id: c.id,\n kind: c.kind,\n claim: c.claim,\n verdict: a.verdict,\n answered_by: a.answered_by,\n evidence: a.evidence,\n ...(a.note === undefined ? {} : { note: a.note }),\n };\n });\n const confirmed = claims.filter((c) => c.verdict === \"confirmed\").length;\n return {\n feature: checklist.feature,\n recorded: today,\n checklist: checklist.digest,\n summary: { claims: claims.length, confirmed, unconfirmed: claims.length - confirmed },\n claims,\n ...(reports.results === undefined ? {} : { report: reports.results }),\n ...(reports.contract === undefined ? {} : { contractReport: reports.contract }),\n };\n}\n\n/**\n * An answer the previous record held that the new one does not carry.\n *\n * Dropping these is correct — see `buildFederatedVerification` — but doing it\n * silently is not: a schema-1 record answers the WHOLE checklist, so the first\n * federated write turns a green record into a partial one, and an operator who\n * is not told which answers went missing reads the drop as loam losing data.\n */\nexport interface DiscardedAnswer {\n id: string;\n claim: string;\n /** The service the record filed it under, when it said. Absent in schema-1 records. */\n subject?: string;\n /**\n * `off-checklist` — the feature changed and nobody asks this question any\n * more. `unattested` — the question still stands, but no service attestation\n * binds the answer to a commit, so it cannot be carried into a federated\n * record; the owning repository has to record it again.\n */\n reason: \"off-checklist\" | \"unattested\";\n}\n\nexport interface FederatedBuild {\n verification: Verification;\n /** In checklist-independent record order — see {@link DiscardedAnswer}. */\n discarded: DiscardedAnswer[];\n}\n\n/**\n * Replace one service's contribution while retaining current contributions\n * from other repositories. Claim ids removed from the live checklist are\n * pruned from both the answers and their attestations, so a changed feature can\n * never keep a green answer to a question it no longer asks.\n */\n/** One service's attestation: whose word it is, when, and against which commit. */\nexport interface Attestation {\n service: string;\n recorded: string;\n commit: string;\n reports: ConsumedReports;\n /** The docs-repo HEAD the checklist was derived from, when git could answer. */\n docsCommit?: string;\n}\n\nexport function buildFederatedVerification(\n checklist: Checklist,\n attestation: Attestation,\n answers: Answer[],\n previous: Verification | null,\n): FederatedBuild {\n const { service, recorded, commit, reports, docsCommit } = attestation;\n const currentById = new Map(checklist.claims.map((claim) => [claim.id, claim]));\n const localIds = new Set(checklist.claims.filter((claim) => claim.subject === service).map((claim) => claim.id));\n\n // An old all-at-once record remains readable, but its answers have no commit\n // attestation and therefore cannot silently become somebody else's\n // federated contribution. The first schema-2 write starts a clean federation.\n const previouslyAttested = new Set((previous?.attestations ?? []).flatMap((a) => a.claims));\n const retained = (previous?.schema === 2 ? previous.claims : []).filter((claim) => {\n const current = currentById.get(claim.id);\n return current !== undefined && current.subject !== service && previouslyAttested.has(claim.id);\n });\n const local = answers.map((answer): RecordedClaim => {\n // Same contract as `buildVerification`: an answer is built from the live\n // checklist, so it always names a current claim. Off-checklist answers do\n // NOT belong here — `discarded` means \"an answer the PREVIOUS record held\",\n // and routing an incoming stray into it would change what the printed\n // notice tells an operator. A caller that reaches this gets told which\n // answer it was rather than a `TypeError` on `claim.kind`.\n const claim = currentById.get(answer.id);\n if (claim === undefined) {\n throw new Error(`buildFederatedVerification: answer ${answer.id} names no current claim`);\n }\n return {\n id: claim.id,\n kind: claim.kind,\n subject: claim.subject,\n claim: claim.claim,\n verdict: answer.verdict,\n answered_by: answer.answered_by,\n evidence: answer.evidence,\n ...(answer.note === undefined ? {} : { note: answer.note }),\n // The pins the evidence validation stamped onto this repository's\n // answers ride into the record here; retained other-service claims are\n // spread whole below, so THEIR pins survive a re-record untouched.\n ...(answer.evidence_pins === undefined ? {} : { evidence_pins: answer.evidence_pins }),\n };\n });\n const byId = new Map([...retained, ...local].map((claim) => [claim.id, claim]));\n const claims = checklist.claims.flatMap((claim) => {\n const answer = byId.get(claim.id);\n if (answer === undefined) return [];\n // Normalize retained legacy entries to the current question text and add\n // the subject schema 2 needs for future independent replacement.\n return [{ ...answer, kind: claim.kind, subject: claim.subject, claim: claim.claim }];\n });\n\n const retainedAttestations = (previous?.attestations ?? []).flatMap((attestation) => {\n if (attestation.service === service) return [];\n const ids = attestation.claims.filter(\n (id) => currentById.get(id)?.subject === attestation.service && byId.has(id),\n );\n return ids.length === 0 ? [] : [{ ...attestation, claims: ids }];\n });\n const attestations: ServiceAttestation[] = [\n ...retainedAttestations,\n {\n service,\n commit,\n recorded,\n claims: checklist.claims.filter((c) => localIds.has(c.id)).map((c) => c.id),\n // What THIS repository answered, and what it answered it against. Both\n // ride with the attestation rather than the record for the reason the\n // reports do: they are one repository's facts, pruned and retained with\n // that repository's answers.\n checklist: checklist.digest,\n ...(docsCommit === undefined ? {} : { docsCommit }),\n // Filed with the attestation, not the record: each report is one\n // repository's run, and it is pruned or retained with that repository's\n // answers rather than outliving them — the contract report under the\n // same rule as the cucumber one.\n ...(reports.results === undefined ? {} : { report: reports.results }),\n ...(reports.contract === undefined ? {} : { contractReport: reports.contract }),\n },\n ].sort((a, b) => a.service.localeCompare(b.service));\n\n const confirmed = claims.filter((claim) => claim.verdict === \"confirmed\").length;\n const unconfirmed = claims.length - confirmed;\n const unanswered = checklist.claims.length - claims.length;\n\n // Everything the old record answered that the new one does not. The caller\n // prints it; nothing here decides it is acceptable.\n const discarded = (previous?.claims ?? [])\n .filter((claim) => !byId.has(claim.id))\n .map((claim): DiscardedAnswer => ({\n id: claim.id,\n claim: claim.claim,\n ...(claim.subject === undefined ? {} : { subject: claim.subject }),\n reason: currentById.has(claim.id) ? \"unattested\" : \"off-checklist\",\n }));\n\n return {\n verification: {\n schema: 2,\n feature: checklist.feature,\n recorded,\n checklist: checklist.digest,\n summary: { claims: checklist.claims.length, confirmed, unconfirmed, unanswered },\n claims,\n attestations,\n },\n discarded,\n };\n}\n"]}
|
|
1
|
+
{"version":3,"file":"build.js","sourceRoot":"","sources":["../../../src/core/verify/build.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * Checklist plus answers becomes a record — once for a feature answered all at\n * once, once for a fleet answering service by service.\n *\n * The federated form is separate from `buildVerification` and not a flag on it\n * because the two disagree about what an unanswered claim means: answering\n * everything at once, a missing answer is a gap in this run, while a federated\n * record is assembled from attestations that each covered one service and were\n * never expected to cover the rest. They also land in different places: the\n * all-at-once record is the one file `verification.yaml`, while each service's\n * attestation is a file of its own under `verification/` (`./location.ts` says\n * why, on `verificationDir`).\n */\nimport { type Checklist } from \"./checklist.js\";\nimport { type Answer } from \"./answers.js\";\nimport { type ConsumedReports, type RecordedClaim, type Verification } from \"./record.js\";\n\nexport function buildVerification(\n checklist: Checklist,\n answers: Answer[],\n today: string,\n reports: ConsumedReports = {},\n): Verification {\n const byId = new Map(answers.map((a) => [a.id, a]));\n const claims: RecordedClaim[] = checklist.claims.map((c) => {\n // Callers hand in an answer per claim — `runnerAnswers` is total over the\n // checklist, and both other callers map the checklist to build theirs. The\n // branch is here because the alternative to a diagnosis is a `TypeError` on\n // `a.verdict`, which cli.ts reports as `internal` with a stack about a\n // property of `undefined` and no word about which claim went missing.\n const a = byId.get(c.id);\n if (a === undefined) throw new Error(`buildVerification: no answer for claim ${c.id}`);\n // Deliberately no `evidence_pins` here: the legacy all-at-once form binds\n // to no commit, so there is no attested blob to pin — and none is ever\n // handed in, because pins are computed inside the federated evidence\n // validation this form never runs.\n return {\n id: c.id,\n kind: c.kind,\n claim: c.claim,\n verdict: a.verdict,\n answered_by: a.answered_by,\n evidence: a.evidence,\n ...(a.note === undefined ? {} : { note: a.note }),\n };\n });\n const confirmed = claims.filter((c) => c.verdict === \"confirmed\").length;\n return {\n feature: checklist.feature,\n recorded: today,\n checklist: checklist.digest,\n summary: { claims: claims.length, confirmed, unconfirmed: claims.length - confirmed },\n claims,\n ...(reports.results === undefined ? {} : { report: reports.results }),\n ...(reports.contract === undefined ? {} : { contractReport: reports.contract }),\n };\n}\n\n/**\n * An answer the previous record held that the new one does not carry.\n *\n * Dropping these is correct — see `buildServiceRecord` — but doing it silently\n * is not: a schema-1 record answers the WHOLE checklist, so the first\n * federated write turns a green record into a partial one, and an operator who\n * is not told which answers went missing reads the drop as loam losing data.\n */\nexport interface DiscardedAnswer {\n id: string;\n claim: string;\n /** The service the record filed it under, when it said. Absent in schema-1 records. */\n subject?: string;\n /**\n * `off-checklist` — the feature changed and nobody asks this question any\n * more. `unattested` — the question still stands, but no service attestation\n * binds the answer to a commit, so it cannot be carried into a federated\n * record; the owning repository has to record it again.\n */\n reason: \"off-checklist\" | \"unattested\";\n}\n\n/** One service's attestation: whose word it is, when, and against which commit. */\nexport interface Attestation {\n service: string;\n recorded: string;\n commit: string;\n reports: ConsumedReports;\n /** The docs-repo HEAD the checklist was derived from, when git could answer. */\n docsCommit?: string;\n}\n\n/**\n * The record files a service's write replaces, as the authoritative read found\n * them: the one-file record, and this service's own `verification/<svc>.yaml`.\n * `filed` names every service that already has a file of its own — what\n * decides whether anything in the one-file record still counts.\n */\nexport interface PreviousFiles {\n legacy: Verification | null;\n own: Verification | null;\n filed: ReadonlySet<string>;\n}\n\nexport interface ServiceBuild {\n /** `verification/<svc>.yaml` as it will be written: this service's attestation and answers, nobody else's. */\n record: Verification;\n /**\n * Whether the one-file record goes in the same commit, because nothing in it\n * counts any more: an all-at-once record never counts once a service has a\n * file (its answers carry no attestation — today's write used to overwrite it\n * for the same reason), and a federated one stops counting once every\n * service it names has a file of its own.\n */\n dropLegacy: boolean;\n /** In record order — see {@link DiscardedAnswer}. */\n discarded: DiscardedAnswer[];\n}\n\n/**\n * One service's attestation, as the file only that service ever writes.\n *\n * This used to be a merge: the new attestation spliced into the one shared\n * record, every other service's answers retained, pruned to the current\n * checklist and re-sorted — and that rewrite of everybody's bytes is exactly\n * what made two teams' docs PRs conflict. Now nothing else is touched. The\n * other services' files stand as they are, the union is added up at read time\n * (`read/union.ts`), and an answer of theirs the feature no longer asks is\n * simply not counted there rather than pruned here.\n *\n * The file is a whole schema-2 record with one attestation, so it is graded\n * with the same shape check as every record and reads without loam: its\n * `summary` counts the whole checklist, with the claims other services answer\n * honestly `unanswered` in it.\n */\nexport function buildServiceRecord(\n checklist: Checklist,\n attestation: Attestation,\n answers: Answer[],\n previous: PreviousFiles,\n): ServiceBuild {\n const { service, recorded, commit, reports, docsCommit } = attestation;\n const byId = new Map(answers.map((answer) => [answer.id, answer]));\n const currentIds = new Set(checklist.claims.map((claim) => claim.id));\n for (const answer of answers) {\n // Same contract as `buildVerification`: an answer is built from the live\n // checklist, so it always names a current claim. Off-checklist answers do\n // NOT belong here — `discarded` means \"an answer the PREVIOUS record held\",\n // and routing an incoming stray into it would change what the printed\n // notice tells an operator. A caller that reaches this gets told which\n // answer it was rather than an answer silently missing from the file.\n if (!currentIds.has(answer.id)) {\n throw new Error(`buildServiceRecord: answer ${answer.id} names no current claim`);\n }\n }\n const claims = checklist.claims.flatMap((claim): RecordedClaim[] => {\n const answer = byId.get(claim.id);\n if (answer === undefined) return [];\n return [\n {\n id: claim.id,\n kind: claim.kind,\n subject: claim.subject,\n claim: claim.claim,\n verdict: answer.verdict,\n answered_by: answer.answered_by,\n evidence: answer.evidence,\n ...(answer.note === undefined ? {} : { note: answer.note }),\n // The pins the evidence validation stamped onto this repository's\n // answers ride into the file here; no other service's file is\n // rewritten, so THEIR pins are never touched at all.\n ...(answer.evidence_pins === undefined ? {} : { evidence_pins: answer.evidence_pins }),\n },\n ];\n });\n const confirmed = claims.filter((claim) => claim.verdict === \"confirmed\").length;\n const record: Verification = {\n schema: 2,\n feature: checklist.feature,\n recorded,\n checklist: checklist.digest,\n summary: {\n claims: checklist.claims.length,\n confirmed,\n unconfirmed: claims.length - confirmed,\n unanswered: checklist.claims.length - claims.length,\n },\n claims,\n attestations: [\n {\n service,\n commit,\n recorded,\n claims: checklist.claims.filter((c) => c.subject === service).map((c) => c.id),\n // What THIS repository answered, and what it answered it against.\n // `checklist` is what `verify.checklist-forked` and the union's\n // one-version rule read; `docsCommit` pins the side the question came\n // from.\n checklist: checklist.digest,\n ...(docsCommit === undefined ? {} : { docsCommit }),\n // Each report is one repository's run, filed with that repository's\n // attestation — the contract report under the same rule as the\n // cucumber one.\n ...(reports.results === undefined ? {} : { report: reports.results }),\n ...(reports.contract === undefined ? {} : { contractReport: reports.contract }),\n },\n ],\n };\n\n const { legacy, own, filed } = previous;\n const federatedLegacy = legacy?.schema === 2;\n const dropLegacy =\n legacy !== null &&\n (!federatedLegacy || (legacy.attestations ?? []).every((a) => a.service === service || filed.has(a.service)));\n // Everything this write takes off the disk that the new file does not carry:\n // this service's own previous answers (from its file, or from its share of a\n // one-file record no file of its own had superseded yet) and, when the\n // one-file record is dropped, an all-at-once record's answers. Another\n // service's answers are never in this list — their files are not touched.\n const replaced = [\n ...(own?.claims ?? []),\n ...(legacy === null\n ? []\n : !federatedLegacy\n ? legacy.claims\n : own === null\n ? legacy.claims.filter((claim) => claim.subject === service)\n : []),\n ];\n const carried = new Set(claims.map((claim) => claim.id));\n // Keyed by id: a service's own file and its share of the one-file record\n // can both hold the same stale answer, and it is one answer.\n const dropped = new Map(replaced.filter((claim) => !carried.has(claim.id)).map((claim) => [claim.id, claim]));\n const discarded = [...dropped.values()]\n .map((claim): DiscardedAnswer => ({\n id: claim.id,\n claim: claim.claim,\n ...(claim.subject === undefined ? {} : { subject: claim.subject }),\n reason: currentIds.has(claim.id) ? \"unattested\" : \"off-checklist\",\n }));\n return { record, dropLegacy, discarded };\n}\n"]}
|