@ybotok/loam 0.2.0-alpha.1 → 0.2.0-alpha.13
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +3448 -0
- package/COMPARISON.md +91 -3
- package/CONTRIBUTING.md +37 -0
- package/MIGRATING-from-OpenSpec.md +47 -23
- package/QUICKSTART.md +189 -12
- package/README.md +274 -65
- package/ROADMAP.md +572 -31
- package/SCHEMA.md +1709 -155
- package/WORKFLOW.md +298 -72
- package/dist/cli.js +26 -8
- package/dist/cli.js.map +1 -1
- package/dist/commands/adopt/adopt.js +5 -10
- package/dist/commands/adopt/adopt.js.map +1 -1
- package/dist/commands/adopt/render.js +50 -9
- package/dist/commands/adopt/render.js.map +1 -1
- package/dist/commands/archive/archive.js +16 -19
- package/dist/commands/archive/archive.js.map +1 -1
- package/dist/commands/archive/closing.js +42 -0
- package/dist/commands/archive/closing.js.map +1 -0
- package/dist/commands/archive/plan/contracts/asyncapi.js +36 -42
- package/dist/commands/archive/plan/contracts/asyncapi.js.map +1 -1
- package/dist/commands/archive/plan/contracts/consumers.js +56 -0
- package/dist/commands/archive/plan/contracts/consumers.js.map +1 -0
- package/dist/commands/archive/plan/contracts/openapi.js +77 -43
- package/dist/commands/archive/plan/contracts/openapi.js.map +1 -1
- package/dist/commands/archive/plan/contracts/verdict-lines.js +103 -0
- package/dist/commands/archive/plan/contracts/verdict-lines.js.map +1 -0
- package/dist/commands/archive/plan/document/head.js +151 -0
- package/dist/commands/archive/plan/document/head.js.map +1 -0
- package/dist/commands/archive/plan/document/links.js +284 -0
- package/dist/commands/archive/plan/document/links.js.map +1 -0
- package/dist/commands/archive/plan/document/run.js +136 -0
- package/dist/commands/archive/plan/document/run.js.map +1 -0
- package/dist/commands/archive/plan/gate.js +21 -19
- package/dist/commands/archive/plan/gate.js.map +1 -1
- package/dist/commands/archive/plan/landscape.js +167 -30
- package/dist/commands/archive/plan/landscape.js.map +1 -1
- package/dist/commands/archive/plan/model/extending.js +85 -0
- package/dist/commands/archive/plan/model/extending.js.map +1 -0
- package/dist/commands/archive/plan/refusal.js +14 -19
- package/dist/commands/archive/plan/refusal.js.map +1 -1
- package/dist/commands/archive/plan/specs.js +137 -56
- package/dist/commands/archive/plan/specs.js.map +1 -1
- package/dist/commands/archive/plan/state.js +3 -0
- package/dist/commands/archive/plan/state.js.map +1 -1
- package/dist/commands/archive/run.js +134 -30
- package/dist/commands/archive/run.js.map +1 -1
- package/dist/commands/archive/shipped.js +49 -0
- package/dist/commands/archive/shipped.js.map +1 -0
- package/dist/commands/context/context.js +85 -18
- package/dist/commands/context/context.js.map +1 -1
- package/dist/commands/context/print.js +29 -7
- package/dist/commands/context/print.js.map +1 -1
- package/dist/commands/delta/delta.js +102 -14
- package/dist/commands/delta/delta.js.map +1 -1
- package/dist/commands/delta/print.js +17 -1
- package/dist/commands/delta/print.js.map +1 -1
- package/dist/commands/dependencies.js +3 -7
- package/dist/commands/dependencies.js.map +1 -1
- package/dist/commands/diff/diff.js +20 -27
- package/dist/commands/diff/diff.js.map +1 -1
- package/dist/commands/doctor.js +60 -1
- package/dist/commands/doctor.js.map +1 -1
- package/dist/commands/explain/explain.js +89 -13
- package/dist/commands/explain/explain.js.map +1 -1
- package/dist/commands/explore.js +16 -10
- package/dist/commands/explore.js.map +1 -1
- package/dist/commands/gate/gate.js +45 -8
- package/dist/commands/gate/gate.js.map +1 -1
- package/dist/commands/gate/print.js +15 -3
- package/dist/commands/gate/print.js.map +1 -1
- package/dist/commands/gherkin/commit.js +13 -54
- package/dist/commands/gherkin/commit.js.map +1 -1
- package/dist/commands/gherkin/gherkin.js +36 -20
- package/dist/commands/gherkin/gherkin.js.map +1 -1
- package/dist/commands/gherkin/reconcile.js +10 -13
- package/dist/commands/gherkin/reconcile.js.map +1 -1
- package/dist/commands/init/first-hour.js +44 -0
- package/dist/commands/init/first-hour.js.map +1 -1
- package/dist/commands/init/init.js +18 -22
- package/dist/commands/init/init.js.map +1 -1
- package/dist/commands/list/campaign/campaign.js +4 -6
- package/dist/commands/list/campaign/campaign.js.map +1 -1
- package/dist/commands/list/json.js +5 -7
- package/dist/commands/list/json.js.map +1 -1
- package/dist/commands/list/list.js +44 -29
- package/dist/commands/list/list.js.map +1 -1
- package/dist/commands/list/print.js +52 -9
- package/dist/commands/list/print.js.map +1 -1
- package/dist/commands/list/suites/suites.js +140 -0
- package/dist/commands/list/suites/suites.js.map +1 -0
- package/dist/commands/list/views.js +37 -34
- package/dist/commands/list/views.js.map +1 -1
- package/dist/commands/mcp/dispatch.js +91 -36
- package/dist/commands/mcp/dispatch.js.map +1 -1
- package/dist/commands/mcp/mcp.js +56 -15
- package/dist/commands/mcp/mcp.js.map +1 -1
- package/dist/commands/mcp/serve.js.map +1 -1
- package/dist/commands/migrate-openspec/materialize/active.js +27 -22
- package/dist/commands/migrate-openspec/materialize/active.js.map +1 -1
- package/dist/commands/migrate-openspec/materialize/stage.js +4 -3
- package/dist/commands/migrate-openspec/materialize/stage.js.map +1 -1
- package/dist/commands/migrate-openspec/materialize/target.js +2 -2
- package/dist/commands/migrate-openspec/materialize/target.js.map +1 -1
- package/dist/commands/migrate-openspec/openspec/decisions.js +9 -14
- package/dist/commands/migrate-openspec/openspec/decisions.js.map +1 -1
- package/dist/commands/migrate-openspec/openspec/error.js +7 -8
- package/dist/commands/migrate-openspec/openspec/error.js.map +1 -1
- package/dist/commands/migrate-openspec/openspec/mapping.js +1 -9
- package/dist/commands/migrate-openspec/openspec/mapping.js.map +1 -1
- package/dist/commands/migrate-openspec/openspec/paths.js +3 -6
- package/dist/commands/migrate-openspec/openspec/paths.js.map +1 -1
- package/dist/commands/new/capability-template.js +118 -0
- package/dist/commands/new/capability-template.js.map +1 -0
- package/dist/commands/new/commit.js +8 -22
- package/dist/commands/new/commit.js.map +1 -1
- package/dist/commands/new/modifies.js +206 -0
- package/dist/commands/new/modifies.js.map +1 -0
- package/dist/commands/new/new.js +59 -14
- package/dist/commands/new/new.js.map +1 -1
- package/dist/commands/new/templates.js +48 -124
- package/dist/commands/new/templates.js.map +1 -1
- package/dist/commands/open.js +3 -7
- package/dist/commands/open.js.map +1 -1
- package/dist/commands/policy/gate.js +27 -0
- package/dist/commands/policy/gate.js.map +1 -1
- package/dist/commands/rebase/plan.js +2 -2
- package/dist/commands/rebase/plan.js.map +1 -1
- package/dist/commands/rebase/rebase.js +60 -35
- package/dist/commands/rebase/rebase.js.map +1 -1
- package/dist/commands/seed/commit.js +55 -26
- package/dist/commands/seed/commit.js.map +1 -1
- package/dist/commands/seed/plan.js +158 -13
- package/dist/commands/seed/plan.js.map +1 -1
- package/dist/commands/seed/seed.js +13 -11
- package/dist/commands/seed/seed.js.map +1 -1
- package/dist/commands/show/feature.js +9 -5
- package/dist/commands/show/feature.js.map +1 -1
- package/dist/commands/show/introduced.js +157 -0
- package/dist/commands/show/introduced.js.map +1 -0
- package/dist/commands/show/marks.js +35 -6
- package/dist/commands/show/marks.js.map +1 -1
- package/dist/commands/show/service.js +41 -33
- package/dist/commands/show/service.js.map +1 -1
- package/dist/commands/show/show.js +17 -7
- package/dist/commands/show/show.js.map +1 -1
- package/dist/commands/status/fleet-next.js +90 -0
- package/dist/commands/status/fleet-next.js.map +1 -0
- package/dist/commands/status/json.js +2 -0
- package/dist/commands/status/json.js.map +1 -1
- package/dist/commands/status/print.js +89 -37
- package/dist/commands/status/print.js.map +1 -1
- package/dist/commands/status/status.js +3 -7
- package/dist/commands/status/status.js.map +1 -1
- package/dist/commands/steps/steps.js +103 -23
- package/dist/commands/steps/steps.js.map +1 -1
- package/dist/commands/subsystem/edit.js +7 -7
- package/dist/commands/subsystem/edit.js.map +1 -1
- package/dist/commands/subsystem/move.js +106 -46
- package/dist/commands/subsystem/move.js.map +1 -1
- package/dist/commands/subsystem/query.js +2 -4
- package/dist/commands/subsystem/query.js.map +1 -1
- package/dist/commands/subsystem/subsystem.js +4 -7
- package/dist/commands/subsystem/subsystem.js.map +1 -1
- package/dist/commands/subsystem/sync.js +289 -74
- package/dist/commands/subsystem/sync.js.map +1 -1
- package/dist/commands/subsystem/txn/exclude.js +132 -0
- package/dist/commands/subsystem/txn/exclude.js.map +1 -0
- package/dist/commands/subsystem/txn/links.js +128 -0
- package/dist/commands/subsystem/txn/links.js.map +1 -0
- package/dist/commands/subsystem/txn/report.js +174 -0
- package/dist/commands/subsystem/txn/report.js.map +1 -0
- package/dist/commands/subsystem/txn/txn.js +147 -45
- package/dist/commands/subsystem/txn/txn.js.map +1 -1
- package/dist/commands/subsystem/txn/views.js +200 -21
- package/dist/commands/subsystem/txn/views.js.map +1 -1
- package/dist/commands/tasks/actions.js +107 -0
- package/dist/commands/tasks/actions.js.map +1 -0
- package/dist/commands/tasks/render.js +253 -0
- package/dist/commands/tasks/render.js.map +1 -0
- package/dist/commands/tasks/tasks.js +119 -0
- package/dist/commands/tasks/tasks.js.map +1 -0
- package/dist/commands/unarchive/manifest.js +42 -26
- package/dist/commands/unarchive/manifest.js.map +1 -1
- package/dist/commands/unarchive/restore.js +83 -33
- package/dist/commands/unarchive/restore.js.map +1 -1
- package/dist/commands/unarchive/unarchive.js +22 -30
- package/dist/commands/unarchive/unarchive.js.map +1 -1
- package/dist/commands/validate/arch-coverage.js +78 -27
- package/dist/commands/validate/arch-coverage.js.map +1 -1
- package/dist/commands/validate/checks/fleet-shape.js +34 -126
- package/dist/commands/validate/checks/fleet-shape.js.map +1 -1
- package/dist/commands/validate/checks/health-deps.js +9 -6
- package/dist/commands/validate/checks/health-deps.js.map +1 -1
- package/dist/commands/validate/checks/introduced.js +102 -0
- package/dist/commands/validate/checks/introduced.js.map +1 -0
- package/dist/commands/validate/checks/requirements.js +18 -8
- package/dist/commands/validate/checks/requirements.js.map +1 -1
- package/dist/commands/validate/checks/sources.js +11 -2
- package/dist/commands/validate/checks/sources.js.map +1 -1
- package/dist/commands/validate/coverage/covers-after.js +105 -0
- package/dist/commands/validate/coverage/covers-after.js.map +1 -0
- package/dist/commands/validate/feature.js +26 -20
- package/dist/commands/validate/feature.js.map +1 -1
- package/dist/commands/validate/fleet/census.js +37 -13
- package/dist/commands/validate/fleet/census.js.map +1 -1
- package/dist/commands/validate/fleet/landscape.js +144 -126
- package/dist/commands/validate/fleet/landscape.js.map +1 -1
- package/dist/commands/validate/fleet/load.js +66 -0
- package/dist/commands/validate/fleet/load.js.map +1 -1
- package/dist/commands/validate/fleet/map/attest.js +98 -0
- package/dist/commands/validate/fleet/map/attest.js.map +1 -0
- package/dist/commands/validate/fleet/map/bindings.js +107 -0
- package/dist/commands/validate/fleet/map/bindings.js.map +1 -0
- package/dist/commands/validate/fleet/map/consumers.js +244 -0
- package/dist/commands/validate/fleet/map/consumers.js.map +1 -0
- package/dist/commands/validate/fleet/map/isolation.js +116 -0
- package/dist/commands/validate/fleet/map/isolation.js.map +1 -0
- package/dist/commands/validate/fleet/map/usecases.js +54 -0
- package/dist/commands/validate/fleet/map/usecases.js.map +1 -0
- package/dist/commands/validate/fleet/obligations.js +44 -14
- package/dist/commands/validate/fleet/obligations.js.map +1 -1
- package/dist/commands/validate/fleet/scorecard/print.js +10 -2
- package/dist/commands/validate/fleet/scorecard/print.js.map +1 -1
- package/dist/commands/validate/fleet/scorecard/scorecard.js +17 -11
- package/dist/commands/validate/fleet/scorecard/scorecard.js.map +1 -1
- package/dist/commands/validate/fleet/usecases/place.js.map +1 -1
- package/dist/commands/validate/fleet/views/fleet-project.js +217 -0
- package/dist/commands/validate/fleet/views/fleet-project.js.map +1 -0
- package/dist/commands/validate/fleet/views/ids.js +50 -15
- package/dist/commands/validate/fleet/views/ids.js.map +1 -1
- package/dist/commands/validate/fleet/views/projects.js +278 -0
- package/dist/commands/validate/fleet/views/projects.js.map +1 -0
- package/dist/commands/validate/fleet/views/root-documents.js +83 -0
- package/dist/commands/validate/fleet/views/root-documents.js.map +1 -0
- package/dist/commands/validate/fleet/views/stale.js +40 -47
- package/dist/commands/validate/fleet/views/stale.js.map +1 -1
- package/dist/commands/validate/links/corpus.js +1 -4
- package/dist/commands/validate/links/corpus.js.map +1 -1
- package/dist/commands/validate/links/glossary.js.map +1 -1
- package/dist/commands/validate/render/repeats.js +63 -0
- package/dist/commands/validate/render/repeats.js.map +1 -0
- package/dist/commands/validate/render/rollups.js +115 -0
- package/dist/commands/validate/render/rollups.js.map +1 -0
- package/dist/commands/validate/render/text.js +199 -0
- package/dist/commands/validate/render/text.js.map +1 -0
- package/dist/commands/validate/report.js +36 -160
- package/dist/commands/validate/report.js.map +1 -1
- package/dist/commands/validate/scope.js +113 -0
- package/dist/commands/validate/scope.js.map +1 -0
- package/dist/commands/validate/service/api.js +21 -107
- package/dist/commands/validate/service/api.js.map +1 -1
- package/dist/commands/validate/service/contracts/generated.js +134 -0
- package/dist/commands/validate/service/contracts/generated.js.map +1 -0
- package/dist/commands/validate/service/events/events.js +36 -14
- package/dist/commands/validate/service/events/events.js.map +1 -1
- package/dist/commands/validate/service/events/message-producers.js +1 -1
- package/dist/commands/validate/service/events/message-producers.js.map +1 -1
- package/dist/commands/validate/service/evidence-pins.js +56 -10
- package/dist/commands/validate/service/evidence-pins.js.map +1 -1
- package/dist/commands/validate/service/model/diverged.js +226 -0
- package/dist/commands/validate/service/model/diverged.js.map +1 -0
- package/dist/commands/validate/service/model/grade.js +163 -0
- package/dist/commands/validate/service/model/grade.js.map +1 -0
- package/dist/commands/validate/service/service.js +121 -61
- package/dist/commands/validate/service/service.js.map +1 -1
- package/dist/commands/validate/service/specs.js +88 -28
- package/dist/commands/validate/service/specs.js.map +1 -1
- package/dist/commands/validate/service/spine.js +38 -3
- package/dist/commands/validate/service/spine.js.map +1 -1
- package/dist/commands/validate/service/usecases/tags.js +113 -0
- package/dist/commands/validate/service/usecases/tags.js.map +1 -0
- package/dist/commands/validate/service/usecases/usecases.js +209 -0
- package/dist/commands/validate/service/usecases/usecases.js.map +1 -0
- package/dist/commands/validate/validate.js +85 -60
- package/dist/commands/validate/validate.js.map +1 -1
- package/dist/commands/verify/cross/diff.js +2 -6
- package/dist/commands/verify/cross/diff.js.map +1 -1
- package/dist/commands/verify/evidence/contract.js +1 -1
- package/dist/commands/verify/evidence/contract.js.map +1 -1
- package/dist/commands/verify/evidence/read.js +12 -3
- package/dist/commands/verify/evidence/read.js.map +1 -1
- package/dist/commands/verify/frozen.js +45 -23
- package/dist/commands/verify/frozen.js.map +1 -1
- package/dist/commands/verify/record.js +69 -17
- package/dist/commands/verify/record.js.map +1 -1
- package/dist/commands/verify/report.js +27 -44
- package/dist/commands/verify/report.js.map +1 -1
- package/dist/commands/verify/results.js +112 -30
- package/dist/commands/verify/results.js.map +1 -1
- package/dist/commands/verify/verify.js +55 -52
- package/dist/commands/verify/verify.js.map +1 -1
- package/dist/commands/vouch/pack/pack.js +2 -2
- package/dist/commands/vouch/pack/pack.js.map +1 -1
- package/dist/commands/vouch/pack/print.js +1 -6
- package/dist/commands/vouch/pack/print.js.map +1 -1
- package/dist/commands/vouch/pack/sections.js +1 -2
- package/dist/commands/vouch/pack/sections.js.map +1 -1
- package/dist/commands/vouch/run.js +26 -81
- package/dist/commands/vouch/run.js.map +1 -1
- package/dist/commands/vouch/sample/plan.js +20 -34
- package/dist/commands/vouch/sample/plan.js.map +1 -1
- package/dist/commands/vouch/vet/verify.js +39 -7
- package/dist/commands/vouch/vet/verify.js.map +1 -1
- package/dist/commands/vouch/vouch.js +10 -17
- package/dist/commands/vouch/vouch.js.map +1 -1
- package/dist/core/agent/agents-md/command-map.js +81 -106
- package/dist/core/agent/agents-md/command-map.js.map +1 -1
- package/dist/core/agent/agents-md/map/explain.js +21 -9
- package/dist/core/agent/agents-md/map/explain.js.map +1 -1
- package/dist/core/agent/agents-md/map/lenses/context.js +17 -8
- package/dist/core/agent/agents-md/map/lenses/context.js.map +1 -1
- package/dist/core/agent/agents-md/map/lenses/diff.js +33 -20
- package/dist/core/agent/agents-md/map/lenses/diff.js.map +1 -1
- package/dist/core/agent/agents-md/map/lenses/gate.js +13 -5
- package/dist/core/agent/agents-md/map/lenses/gate.js.map +1 -1
- package/dist/core/agent/agents-md/map/mcp.js +10 -7
- package/dist/core/agent/agents-md/map/mcp.js.map +1 -1
- package/dist/core/agent/agents-md/map/status.js +24 -8
- package/dist/core/agent/agents-md/map/status.js.map +1 -1
- package/dist/core/agent/agents-md/map/subsystem.js +80 -8
- package/dist/core/agent/agents-md/map/subsystem.js.map +1 -1
- package/dist/core/agent/agents-md/map/usecases.js +88 -10
- package/dist/core/agent/agents-md/map/usecases.js.map +1 -1
- package/dist/core/agent/agents-md/refusals.js +49 -22
- package/dist/core/agent/agents-md/refusals.js.map +1 -1
- package/dist/core/agent/agents-md.js +39 -6
- package/dist/core/agent/agents-md.js.map +1 -1
- package/dist/core/agent/agents-stamp.js +10 -0
- package/dist/core/agent/agents-stamp.js.map +1 -1
- package/dist/core/agent/protocol.js +1 -1
- package/dist/core/agent/protocol.js.map +1 -1
- package/dist/core/agent/scaffold.js +10 -15
- package/dist/core/agent/scaffold.js.map +1 -1
- package/dist/core/agent/support/report.js +46 -5
- package/dist/core/agent/support/report.js.map +1 -1
- package/dist/core/agent/tools/dialects.js +1 -0
- package/dist/core/agent/tools/dialects.js.map +1 -1
- package/dist/core/agent/tools/registry.js +41 -76
- package/dist/core/agent/tools/registry.js.map +1 -1
- package/dist/core/agent/workflows/adopt.js +158 -11
- package/dist/core/agent/workflows/adopt.js.map +1 -1
- package/dist/core/agent/workflows/check.js +78 -39
- package/dist/core/agent/workflows/check.js.map +1 -1
- package/dist/core/agent/workflows/closing.js +88 -27
- package/dist/core/agent/workflows/closing.js.map +1 -1
- package/dist/core/agent/workflows/feature.js +128 -13
- package/dist/core/agent/workflows/feature.js.map +1 -1
- package/dist/core/agent/workflows/implement.js +96 -13
- package/dist/core/agent/workflows/implement.js.map +1 -1
- package/dist/core/agent/workflows/reference/authoring.js +106 -4
- package/dist/core/agent/workflows/reference/authoring.js.map +1 -1
- package/dist/core/agent/workflows/reference/codes.js +8 -6
- package/dist/core/agent/workflows/reference/codes.js.map +1 -1
- package/dist/core/agent/workflows/reference/done-check.js +37 -15
- package/dist/core/agent/workflows/reference/done-check.js.map +1 -1
- package/dist/core/agent/workflows/reference/drawing/drawing.js +253 -0
- package/dist/core/agent/workflows/reference/drawing/drawing.js.map +1 -0
- package/dist/core/agent/workflows/reference/reference.js +6 -3
- package/dist/core/agent/workflows/reference/reference.js.map +1 -1
- package/dist/core/agent/workflows/reference/spine.js +68 -27
- package/dist/core/agent/workflows/reference/spine.js.map +1 -1
- package/dist/core/asyncapi/baseline/plan.js +14 -81
- package/dist/core/asyncapi/baseline/plan.js.map +1 -1
- package/dist/core/asyncapi/digest.js +30 -25
- package/dist/core/asyncapi/digest.js.map +1 -1
- package/dist/core/asyncapi/merge/components.js +5 -4
- package/dist/core/asyncapi/merge/components.js.map +1 -1
- package/dist/core/asyncapi/merge/error.js +7 -2
- package/dist/core/asyncapi/merge/error.js.map +1 -1
- package/dist/core/asyncapi/merge/markers.js +32 -50
- package/dist/core/asyncapi/merge/markers.js.map +1 -1
- package/dist/core/asyncapi/merge/merge.js +33 -53
- package/dist/core/asyncapi/merge/merge.js.map +1 -1
- package/dist/core/asyncapi/merge/pin.js +12 -36
- package/dist/core/asyncapi/merge/pin.js.map +1 -1
- package/dist/core/asyncapi/model.js.map +1 -1
- package/dist/core/asyncapi/read.js +36 -12
- package/dist/core/asyncapi/read.js.map +1 -1
- package/dist/core/brief/brief.js +96 -36
- package/dist/core/brief/brief.js.map +1 -1
- package/dist/core/brief/checks.js +138 -3
- package/dist/core/brief/checks.js.map +1 -1
- package/dist/core/brief/landscape.js +209 -130
- package/dist/core/brief/landscape.js.map +1 -1
- package/dist/core/brief/map/extend.js +84 -0
- package/dist/core/brief/map/extend.js.map +1 -0
- package/dist/core/brief/map/owed.js +266 -0
- package/dist/core/brief/map/owed.js.map +1 -0
- package/dist/core/brief/targets.js +109 -31
- package/dist/core/brief/targets.js.map +1 -1
- package/dist/core/brief/unchecked.js +14 -3
- package/dist/core/brief/unchecked.js.map +1 -1
- package/dist/core/c4/arch.js +90 -182
- package/dist/core/c4/arch.js.map +1 -1
- package/dist/core/c4/direction/direction.js +114 -0
- package/dist/core/c4/direction/direction.js.map +1 -0
- package/dist/core/c4/likec4.js +169 -14
- package/dist/core/c4/likec4.js.map +1 -1
- package/dist/core/c4/locate/unlocated.js +150 -0
- package/dist/core/c4/locate/unlocated.js.map +1 -0
- package/dist/core/c4/parsed/deployment.js +160 -0
- package/dist/core/c4/parsed/deployment.js.map +1 -0
- package/dist/core/c4/parsed/dynamic-views.js.map +1 -1
- package/dist/core/c4/parsed/values.js +54 -1
- package/dist/core/c4/parsed/values.js.map +1 -1
- package/dist/core/c4/parsed/view-ids.js +39 -1
- package/dist/core/c4/parsed/view-ids.js.map +1 -1
- package/dist/core/c4/prefetch/fleet.js +134 -0
- package/dist/core/c4/prefetch/fleet.js.map +1 -0
- package/dist/core/c4/project/architecture.js +123 -7
- package/dist/core/c4/project/architecture.js.map +1 -1
- package/dist/core/c4/project/batch.js +140 -0
- package/dist/core/c4/project/batch.js.map +1 -0
- package/dist/core/c4/project/documents.js +43 -12
- package/dist/core/c4/project/documents.js.map +1 -1
- package/dist/core/c4/project/load.js +79 -43
- package/dist/core/c4/project/load.js.map +1 -1
- package/dist/core/c4/project/staged.js +277 -0
- package/dist/core/c4/project/staged.js.map +1 -0
- package/dist/core/c4/resolve/attested.js +0 -0
- package/dist/core/c4/resolve/attested.js.map +1 -0
- package/dist/core/c4/resolve/external.js +51 -0
- package/dist/core/c4/resolve/external.js.map +1 -0
- package/dist/core/c4/resolve/service.js +78 -17
- package/dist/core/c4/resolve/service.js.map +1 -1
- package/dist/core/c4/resolve/steps.js +290 -0
- package/dist/core/c4/resolve/steps.js.map +1 -0
- package/dist/core/c4/resolve/systems.js +154 -0
- package/dist/core/c4/resolve/systems.js.map +1 -0
- package/dist/core/c4/root-project/exclude.js +352 -0
- package/dist/core/c4/root-project/exclude.js.map +1 -0
- package/dist/core/c4/seed/template.js +65 -1
- package/dist/core/c4/seed/template.js.map +1 -1
- package/dist/core/c4/service-model/created/models.js +96 -0
- package/dist/core/c4/service-model/created/models.js.map +1 -0
- package/dist/core/c4/service-model/documents.js +47 -0
- package/dist/core/c4/service-model/documents.js.map +1 -0
- package/dist/core/c4/service-model/fleet/extending.js +82 -0
- package/dist/core/c4/service-model/fleet/extending.js.map +1 -0
- package/dist/core/c4/service-model/load.js +165 -0
- package/dist/core/c4/service-model/load.js.map +1 -0
- package/dist/core/c4/service-model/renderer.js +128 -0
- package/dist/core/c4/service-model/renderer.js.map +1 -0
- package/dist/core/c4/service-model/shape.js +106 -0
- package/dist/core/c4/service-model/shape.js.map +1 -0
- package/dist/core/c4/service-model/slice.js +188 -0
- package/dist/core/c4/service-model/slice.js.map +1 -0
- package/dist/core/c4/source-mask.js +297 -10
- package/dist/core/c4/source-mask.js.map +1 -1
- package/dist/core/c4/source-scan.js +222 -141
- package/dist/core/c4/source-scan.js.map +1 -1
- package/dist/core/c4/splice/authored-source.js +79 -13
- package/dist/core/c4/splice/authored-source.js.map +1 -1
- package/dist/core/c4/splice/contract.js +20 -0
- package/dist/core/c4/splice/contract.js.map +1 -1
- package/dist/core/c4/splice/delta-blocks.js +20 -20
- package/dist/core/c4/splice/delta-blocks.js.map +1 -1
- package/dist/core/c4/splice/identity/declared.js +87 -0
- package/dist/core/c4/splice/identity/declared.js.map +1 -0
- package/dist/core/c4/splice/identity/edges.js +37 -0
- package/dist/core/c4/splice/identity/edges.js.map +1 -1
- package/dist/core/c4/splice/identity/existing.js +80 -0
- package/dist/core/c4/splice/identity/existing.js.map +1 -0
- package/dist/core/c4/splice/identity/interior.js +80 -0
- package/dist/core/c4/splice/identity/interior.js.map +1 -0
- package/dist/core/c4/splice/identity/owner.js +50 -0
- package/dist/core/c4/splice/identity/owner.js.map +1 -0
- package/dist/core/c4/splice/landscape-merge.js +182 -114
- package/dist/core/c4/splice/landscape-merge.js.map +1 -1
- package/dist/core/c4/splice/model/merge.js +151 -0
- package/dist/core/c4/splice/model/merge.js.map +1 -0
- package/dist/core/c4/splice/placement.js +160 -56
- package/dist/core/c4/splice/placement.js.map +1 -1
- package/dist/core/c4/splice/text/spot.js +131 -0
- package/dist/core/c4/splice/text/spot.js.map +1 -0
- package/dist/core/c4/workspace.js +13 -19
- package/dist/core/c4/workspace.js.map +1 -1
- package/dist/core/capabilities/capabilities.js +6 -6
- package/dist/core/capabilities/capabilities.js.map +1 -1
- package/dist/core/capabilities/delta/doc.js +7 -10
- package/dist/core/capabilities/delta/doc.js.map +1 -1
- package/dist/core/capabilities/delta/uncovered.js +2 -2
- package/dist/core/capabilities/delta/uncovered.js.map +1 -1
- package/dist/core/capabilities/findings.js +18 -22
- package/dist/core/capabilities/findings.js.map +1 -1
- package/dist/core/capabilities/realizes/findings.js +12 -19
- package/dist/core/capabilities/realizes/findings.js.map +1 -1
- package/dist/core/capabilities/realizes/join.js +0 -20
- package/dist/core/capabilities/realizes/join.js.map +1 -1
- package/dist/core/capabilities/rollup.js +6 -0
- package/dist/core/capabilities/rollup.js.map +1 -1
- package/dist/core/capabilities/tree.js +6 -28
- package/dist/core/capabilities/tree.js.map +1 -1
- package/dist/core/capabilities/usecase-join.js +3 -13
- package/dist/core/capabilities/usecase-join.js.map +1 -1
- package/dist/core/coherence/authoring/scaffold.js +11 -3
- package/dist/core/coherence/authoring/scaffold.js.map +1 -1
- package/dist/core/coherence/authoring/sentinels.js +2 -1
- package/dist/core/coherence/authoring/sentinels.js.map +1 -1
- package/dist/core/coherence/calls/called.js +52 -0
- package/dist/core/coherence/calls/called.js.map +1 -0
- package/dist/core/coherence/coherence.js +103 -22
- package/dist/core/coherence/coherence.js.map +1 -1
- package/dist/core/coherence/consumed/operations.js +70 -0
- package/dist/core/coherence/consumed/operations.js.map +1 -0
- package/dist/core/coherence/consumed/refusal.js +38 -0
- package/dist/core/coherence/consumed/refusal.js.map +1 -0
- package/dist/core/coherence/covers/health.js +85 -0
- package/dist/core/coherence/covers/health.js.map +1 -0
- package/dist/core/coherence/declared.js +60 -34
- package/dist/core/coherence/declared.js.map +1 -1
- package/dist/core/coherence/events/declared.js +44 -59
- package/dist/core/coherence/events/declared.js.map +1 -1
- package/dist/core/coherence/events/events.js +147 -16
- package/dist/core/coherence/events/events.js.map +1 -1
- package/dist/core/coherence/events/grades.js +31 -8
- package/dist/core/coherence/events/grades.js.map +1 -1
- package/dist/core/coherence/events/lookups.js +28 -87
- package/dist/core/coherence/events/lookups.js.map +1 -1
- package/dist/core/coherence/events/produced.js +100 -0
- package/dist/core/coherence/events/produced.js.map +1 -0
- package/dist/core/coherence/lookups.js +18 -78
- package/dist/core/coherence/lookups.js.map +1 -1
- package/dist/core/coherence/retired/archived.js +285 -0
- package/dist/core/coherence/retired/archived.js.map +1 -0
- package/dist/core/coherence/retired/event-pair.js +62 -0
- package/dist/core/coherence/retired/event-pair.js.map +1 -0
- package/dist/core/coherence/retired/merged.js +79 -0
- package/dist/core/coherence/retired/merged.js.map +1 -0
- package/dist/core/coherence/retired/retirement.js +49 -0
- package/dist/core/coherence/retired/retirement.js.map +1 -0
- package/dist/core/coherence/retired/revival/arrivals.js +131 -0
- package/dist/core/coherence/retired/revival/arrivals.js.map +1 -0
- package/dist/core/coherence/retired/revival/revived.js +297 -0
- package/dist/core/coherence/retired/revival/revived.js.map +1 -0
- package/dist/core/consumers/after.js +113 -0
- package/dist/core/consumers/after.js.map +1 -0
- package/dist/core/consumers/consumers.js +296 -0
- package/dist/core/consumers/consumers.js.map +1 -0
- package/dist/core/consumers/current.js +211 -0
- package/dist/core/consumers/current.js.map +1 -0
- package/dist/core/consumers/scan.js +46 -0
- package/dist/core/consumers/scan.js.map +1 -0
- package/dist/core/consumers/state.js +27 -0
- package/dist/core/consumers/state.js.map +1 -0
- package/dist/core/delta/delta.js +51 -6
- package/dist/core/delta/delta.js.map +1 -1
- package/dist/core/dependencies/fanin.js +2 -2
- package/dist/core/dependencies/fanin.js.map +1 -1
- package/dist/core/deployment/delta.js +191 -0
- package/dist/core/deployment/delta.js.map +1 -0
- package/dist/core/deployment/objects.js +145 -0
- package/dist/core/deployment/objects.js.map +1 -0
- package/dist/core/diff/base-git.js +94 -12
- package/dist/core/diff/base-git.js.map +1 -1
- package/dist/core/diff/base-state.js +49 -21
- package/dist/core/diff/base-state.js.map +1 -1
- package/dist/core/diff/joins.js +136 -0
- package/dist/core/diff/joins.js.map +1 -0
- package/dist/core/diff/scope/changed-paths.js +22 -6
- package/dist/core/diff/scope/changed-paths.js.map +1 -1
- package/dist/core/diff/scope/edges.js +82 -0
- package/dist/core/diff/scope/edges.js.map +1 -0
- package/dist/core/diff/semantic.js +100 -31
- package/dist/core/diff/semantic.js.map +1 -1
- package/dist/core/docs.js +31 -22
- package/dist/core/docs.js.map +1 -1
- package/dist/core/doctor/agents.js +34 -18
- package/dist/core/doctor/agents.js.map +1 -1
- package/dist/core/doctor/config.js +7 -3
- package/dist/core/doctor/config.js.map +1 -1
- package/dist/core/doctor/doctor.js +91 -11
- package/dist/core/doctor/doctor.js.map +1 -1
- package/dist/core/doctor/report.js.map +1 -1
- package/dist/core/doctor/reports/scan.js +242 -0
- package/dist/core/doctor/reports/scan.js.map +1 -0
- package/dist/core/document/apply.js +158 -11
- package/dist/core/document/apply.js.map +1 -1
- package/dist/core/document/frontmatter.js +55 -0
- package/dist/core/document/frontmatter.js.map +1 -1
- package/dist/core/document/parse.js +29 -6
- package/dist/core/document/parse.js.map +1 -1
- package/dist/core/document/scenarios.js +17 -0
- package/dist/core/document/scenarios.js.map +1 -1
- package/dist/core/document/spec.js +4 -11
- package/dist/core/document/spec.js.map +1 -1
- package/dist/core/envelope/config.js +10 -0
- package/dist/core/envelope/config.js.map +1 -1
- package/dist/core/envelope/json.js +34 -0
- package/dist/core/envelope/json.js.map +1 -1
- package/dist/core/explain/families.js +26 -21
- package/dist/core/explain/families.js.map +1 -1
- package/dist/core/explain/lookup.js +14 -1
- package/dist/core/explain/lookup.js.map +1 -1
- package/dist/core/explain/refusals.js +5 -5
- package/dist/core/explain/refusals.js.map +1 -1
- package/dist/core/explain/terms/documents.js +227 -0
- package/dist/core/explain/terms/documents.js.map +1 -0
- package/dist/core/explain/terms/done-check.js +144 -0
- package/dist/core/explain/terms/done-check.js.map +1 -0
- package/dist/core/explain/terms/entry.js +14 -0
- package/dist/core/explain/terms/entry.js.map +1 -0
- package/dist/core/explain/terms/stages.js +71 -0
- package/dist/core/explain/terms/stages.js.map +1 -0
- package/dist/core/explain/terms/tasks.js +97 -0
- package/dist/core/explain/terms/tasks.js.map +1 -0
- package/dist/core/explain/terms.js +68 -109
- package/dist/core/explain/terms.js.map +1 -1
- package/dist/core/explore/explore.js +40 -10
- package/dist/core/explore/explore.js.map +1 -1
- package/dist/core/fleet-context.js +81 -95
- package/dist/core/fleet-context.js.map +1 -1
- package/dist/core/gate/checks.js +11 -4
- package/dist/core/gate/checks.js.map +1 -1
- package/dist/core/gate/partners.js +69 -28
- package/dist/core/gate/partners.js.map +1 -1
- package/dist/core/gate/report.js.map +1 -1
- package/dist/core/gate/verification.js +75 -12
- package/dist/core/gate/verification.js.map +1 -1
- package/dist/core/gherkin/stale.js +13 -37
- package/dist/core/gherkin/stale.js.map +1 -1
- package/dist/core/glossary/delta.js +10 -7
- package/dist/core/glossary/delta.js.map +1 -1
- package/dist/core/glossary/tree.js +6 -19
- package/dist/core/glossary/tree.js.map +1 -1
- package/dist/core/kernel/concurrency.js +19 -0
- package/dist/core/kernel/concurrency.js.map +1 -1
- package/dist/core/kernel/ids/fqn/ancestors.js +65 -0
- package/dist/core/kernel/ids/fqn/ancestors.js.map +1 -0
- package/dist/core/kernel/path-safety.js +38 -0
- package/dist/core/kernel/path-safety.js.map +1 -1
- package/dist/core/kernel/records.js +11 -0
- package/dist/core/kernel/records.js.map +1 -1
- package/dist/core/links/backlinks.js +1 -3
- package/dist/core/links/backlinks.js.map +1 -1
- package/dist/core/links/corpus.js +51 -37
- package/dist/core/links/corpus.js.map +1 -1
- package/dist/core/links/findings.js.map +1 -1
- package/dist/core/links/parse.js +38 -9
- package/dist/core/links/parse.js.map +1 -1
- package/dist/core/mcp/argv.js +101 -0
- package/dist/core/mcp/argv.js.map +1 -1
- package/dist/core/mcp/author-tools.js +2 -1
- package/dist/core/mcp/author-tools.js.map +1 -1
- package/dist/core/mcp/protocol.js +2 -2
- package/dist/core/mcp/protocol.js.map +1 -1
- package/dist/core/mcp/tools.js +43 -33
- package/dist/core/mcp/tools.js.map +1 -1
- package/dist/core/openapi/baseline/gate.js +53 -12
- package/dist/core/openapi/baseline/gate.js.map +1 -1
- package/dist/core/openapi/baseline/plan.js +40 -10
- package/dist/core/openapi/baseline/plan.js.map +1 -1
- package/dist/core/openapi/baseline/record.js +0 -1
- package/dist/core/openapi/baseline/record.js.map +1 -1
- package/dist/core/openapi/contract-text.js +33 -0
- package/dist/core/openapi/contract-text.js.map +1 -0
- package/dist/core/openapi/contract-tree.js +301 -0
- package/dist/core/openapi/contract-tree.js.map +1 -0
- package/dist/core/openapi/depth.js +38 -24
- package/dist/core/openapi/depth.js.map +1 -1
- package/dist/core/openapi/digest.js +52 -2
- package/dist/core/openapi/digest.js.map +1 -1
- package/dist/core/openapi/doc.js +12 -4
- package/dist/core/openapi/doc.js.map +1 -1
- package/dist/core/openapi/merge/components.js +25 -60
- package/dist/core/openapi/merge/components.js.map +1 -1
- package/dist/core/openapi/merge/error.js +3 -13
- package/dist/core/openapi/merge/error.js.map +1 -1
- package/dist/core/openapi/merge/markers.js +26 -30
- package/dist/core/openapi/merge/markers.js.map +1 -1
- package/dist/core/openapi/merge/merge.js +28 -52
- package/dist/core/openapi/merge/merge.js.map +1 -1
- package/dist/core/openapi/merge/pin.js +17 -35
- package/dist/core/openapi/merge/pin.js.map +1 -1
- package/dist/core/openspec/decide/verdict.js.map +1 -1
- package/dist/core/openspec/inventory.js +4 -5
- package/dist/core/openspec/inventory.js.map +1 -1
- package/dist/core/openspec/model/mapping.js +30 -28
- package/dist/core/openspec/model/mapping.js.map +1 -1
- package/dist/core/openspec/model/model.js +12 -7
- package/dist/core/openspec/model/model.js.map +1 -1
- package/dist/core/openspec/scan/changes.js +10 -4
- package/dist/core/openspec/scan/changes.js.map +1 -1
- package/dist/core/openspec/scan/config.js +7 -16
- package/dist/core/openspec/scan/config.js.map +1 -1
- package/dist/core/openspec/scan/shape.js +63 -19
- package/dist/core/openspec/scan/shape.js.map +1 -1
- package/dist/core/openspec/scan/walk.js +21 -38
- package/dist/core/openspec/scan/walk.js.map +1 -1
- package/dist/core/pack/living.js +43 -4
- package/dist/core/pack/living.js.map +1 -1
- package/dist/core/pack/pack.js +50 -6
- package/dist/core/pack/pack.js.map +1 -1
- package/dist/core/projection/arch-slice.js +2 -2
- package/dist/core/projection/arch-slice.js.map +1 -1
- package/dist/core/projection/introduced.js +14 -0
- package/dist/core/projection/introduced.js.map +1 -0
- package/dist/core/provenance/findings.js +48 -12
- package/dist/core/provenance/findings.js.map +1 -1
- package/dist/core/provenance/git.js +33 -57
- package/dist/core/provenance/git.js.map +1 -1
- package/dist/core/provenance/gitq/moves.js +71 -114
- package/dist/core/provenance/gitq/moves.js.map +1 -1
- package/dist/core/provenance/gitq/vouched-ancestor.js +2 -2
- package/dist/core/provenance/gitq/vouched-ancestor.js.map +1 -1
- package/dist/core/provenance/sources.js +23 -6
- package/dist/core/provenance/sources.js.map +1 -1
- package/dist/core/provenance/stamp.js +5 -17
- package/dist/core/provenance/stamp.js.map +1 -1
- package/dist/core/repo/authored/paths.js +18 -0
- package/dist/core/repo/authored/paths.js.map +1 -1
- package/dist/core/repo/entries.js.map +1 -1
- package/dist/core/repo/paths.js +92 -6
- package/dist/core/repo/paths.js.map +1 -1
- package/dist/core/repo/repo.js +74 -14
- package/dist/core/repo/repo.js.map +1 -1
- package/dist/core/repo/tree/find.js +12 -3
- package/dist/core/repo/tree/find.js.map +1 -1
- package/dist/core/repo/tree/fs.js +27 -1
- package/dist/core/repo/tree/fs.js.map +1 -1
- package/dist/core/repo/tree/render/body.js +258 -0
- package/dist/core/repo/tree/render/body.js.map +1 -0
- package/dist/core/repo/tree/render/projects.js +182 -0
- package/dist/core/repo/tree/render/projects.js.map +1 -0
- package/dist/core/repo/tree/render/service/header.js +125 -0
- package/dist/core/repo/tree/render/service/header.js.map +1 -0
- package/dist/core/repo/tree/render/service/peers.js +28 -0
- package/dist/core/repo/tree/render/service/peers.js.map +1 -0
- package/dist/core/repo/tree/render/service/view-id.js +67 -0
- package/dist/core/repo/tree/render/service/view-id.js.map +1 -0
- package/dist/core/repo/tree/render/service/views.js +78 -0
- package/dist/core/repo/tree/render/service/views.js.map +1 -0
- package/dist/core/repo/tree/render/stale.js +42 -0
- package/dist/core/repo/tree/render/stale.js.map +1 -0
- package/dist/core/repo/tree/render/text.js +84 -0
- package/dist/core/repo/tree/render/text.js.map +1 -0
- package/dist/core/repo/tree/render/views.js +293 -0
- package/dist/core/repo/tree/render/views.js.map +1 -0
- package/dist/core/repo/tree/walk.js +53 -32
- package/dist/core/repo/tree/walk.js.map +1 -1
- package/dist/core/results.js +47 -12
- package/dist/core/results.js.map +1 -1
- package/dist/core/scaffold/landscape.js +121 -13
- package/dist/core/scaffold/landscape.js.map +1 -1
- package/dist/core/scaffold/legacy.js +271 -0
- package/dist/core/scaffold/legacy.js.map +1 -0
- package/dist/core/staging/commit.js +26 -15
- package/dist/core/staging/commit.js.map +1 -1
- package/dist/core/staging/lock.js +74 -12
- package/dist/core/staging/lock.js.map +1 -1
- package/dist/core/staging/recovery/intent.js +2 -5
- package/dist/core/staging/recovery/intent.js.map +1 -1
- package/dist/core/staging/recovery/recover.js +84 -6
- package/dist/core/staging/recovery/recover.js.map +1 -1
- package/dist/core/staging/snapshot/manifest.js +188 -0
- package/dist/core/staging/snapshot/manifest.js.map +1 -0
- package/dist/core/staging/{snapshot.js → snapshot/snapshot.js} +39 -45
- package/dist/core/staging/snapshot/snapshot.js.map +1 -0
- package/dist/core/staging/txn/forward.js +24 -14
- package/dist/core/staging/txn/forward.js.map +1 -1
- package/dist/core/staging/writes.js.map +1 -1
- package/dist/core/status/actions/execution.js +11 -0
- package/dist/core/status/actions/execution.js.map +1 -1
- package/dist/core/status/contracts.js +39 -15
- package/dist/core/status/contracts.js.map +1 -1
- package/dist/core/status/feature/artifacts.js +33 -8
- package/dist/core/status/feature/artifacts.js.map +1 -1
- package/dist/core/status/feature/feature.js +24 -17
- package/dist/core/status/feature/feature.js.map +1 -1
- package/dist/core/status/feature/next.js +20 -7
- package/dist/core/status/feature/next.js.map +1 -1
- package/dist/core/status/feature/words.js +96 -0
- package/dist/core/status/feature/words.js.map +1 -0
- package/dist/core/status/fleet/fleet.js +43 -14
- package/dist/core/status/fleet/fleet.js.map +1 -1
- package/dist/core/status/fleet/next.js +153 -58
- package/dist/core/status/fleet/next.js.map +1 -1
- package/dist/core/status/report.js.map +1 -1
- package/dist/core/status/scan.js.map +1 -1
- package/dist/core/status/verification.js +30 -15
- package/dist/core/status/verification.js.map +1 -1
- package/dist/core/tasks/packet.js +199 -0
- package/dist/core/tasks/packet.js.map +1 -0
- package/dist/core/tasks/record.js +43 -0
- package/dist/core/tasks/record.js.map +1 -0
- package/dist/core/tasks/sources.js +181 -0
- package/dist/core/tasks/sources.js.map +1 -0
- package/dist/core/tasks/stamps.js +76 -0
- package/dist/core/tasks/stamps.js.map +1 -0
- package/dist/core/tasks/unit.js +0 -0
- package/dist/core/tasks/unit.js.map +1 -0
- package/dist/core/usecases/capability.js +11 -14
- package/dist/core/usecases/capability.js.map +1 -1
- package/dist/core/usecases/delta/claims.js +31 -7
- package/dist/core/usecases/delta/claims.js.map +1 -1
- package/dist/core/usecases/delta/flows.js +5 -7
- package/dist/core/usecases/delta/flows.js.map +1 -1
- package/dist/core/usecases/delta/modified.js +315 -0
- package/dist/core/usecases/delta/modified.js.map +1 -0
- package/dist/core/usecases/delta/overlay.js +32 -191
- package/dist/core/usecases/delta/overlay.js.map +1 -1
- package/dist/core/usecases/fleet.js +195 -36
- package/dist/core/usecases/fleet.js.map +1 -1
- package/dist/{commands/validate/fleet/usecases → core/usecases/grade}/capability-tag.js +8 -9
- package/dist/core/usecases/grade/capability-tag.js.map +1 -0
- package/dist/core/usecases/grade/feature.js +88 -0
- package/dist/core/usecases/grade/feature.js.map +1 -0
- package/dist/{commands/validate/fleet/usecases → core/usecases/grade}/requirement-tag.js +3 -4
- package/dist/core/usecases/grade/requirement-tag.js.map +1 -0
- package/dist/{commands/validate/fleet/usecases → core/usecases/grade}/steps.js +102 -33
- package/dist/core/usecases/grade/steps.js.map +1 -0
- package/dist/{commands/validate/fleet/usecases → core/usecases/grade}/usecases.js +23 -10
- package/dist/core/usecases/grade/usecases.js.map +1 -0
- package/dist/core/usecases/operations.js +26 -14
- package/dist/core/usecases/operations.js.map +1 -1
- package/dist/core/usecases/place.js.map +1 -1
- package/dist/core/usecases/service/exclude.js +119 -0
- package/dist/core/usecases/service/exclude.js.map +1 -0
- package/dist/core/usecases/service/flows.js +164 -0
- package/dist/core/usecases/service/flows.js.map +1 -0
- package/dist/core/usecases/suites/participants.js +83 -0
- package/dist/core/usecases/suites/participants.js.map +1 -0
- package/dist/core/usecases/suites/suites.js +45 -0
- package/dist/core/usecases/suites/suites.js.map +1 -0
- package/dist/core/verify/answers.js +12 -5
- package/dist/core/verify/answers.js.map +1 -1
- package/dist/core/verify/build.js +102 -82
- package/dist/core/verify/build.js.map +1 -1
- package/dist/core/verify/checklist.js +70 -12
- package/dist/core/verify/checklist.js.map +1 -1
- package/dist/core/verify/location.js +67 -0
- package/dist/core/verify/location.js.map +1 -0
- package/dist/core/verify/pins/lint.js +3 -2
- package/dist/core/verify/pins/lint.js.map +1 -1
- package/dist/core/verify/pins/pin.js +1 -1
- package/dist/core/verify/pins/pin.js.map +1 -1
- package/dist/core/verify/read/file.js +224 -0
- package/dist/core/verify/read/file.js.map +1 -0
- package/dist/core/verify/{file.js → read/shape.js} +16 -104
- package/dist/core/verify/read/shape.js.map +1 -0
- package/dist/core/verify/read/union.js +105 -0
- package/dist/core/verify/read/union.js.map +1 -0
- package/dist/core/verify/record.js +35 -12
- package/dist/core/verify/record.js.map +1 -1
- package/dist/core/verify/store/commit.js +40 -30
- package/dist/core/verify/store/commit.js.map +1 -1
- package/dist/core/verify/store/render.js +8 -4
- package/dist/core/verify/store/render.js.map +1 -1
- package/dist/core/vocabulary/issue.js +4 -0
- package/dist/core/vocabulary/issue.js.map +1 -1
- package/dist/core/vocabulary/maturity.js +75 -4
- package/dist/core/vocabulary/maturity.js.map +1 -1
- package/dist/core/vocabulary/report.js +16 -1
- package/dist/core/vocabulary/report.js.map +1 -1
- package/examples/README.md +54 -11
- package/examples/docs/architecture/landscape.likec4 +62 -6
- package/examples/docs/architecture/services.likec4 +70 -0
- package/examples/docs/architecture/subsystems.likec4 +7 -0
- package/examples/docs/architecture/usecases/order-notification.likec4 +16 -7
- package/examples/docs/features/FEAT-101-payment-splitting/specs/payment-service/asyncapi.yaml +11 -8
- package/examples/docs/features/FEAT-101-payment-splitting/specs/payment-split-service/openapi.yaml +6 -6
- package/examples/docs/features/FEAT-112-retire-order-v1/specs/order-service/openapi.yaml +4 -1
- package/examples/docs/features/archive/FEAT-088-refunds/specs/payment-service/openapi.yaml +13 -10
- package/examples/docs/likec4.config.json +0 -1
- package/examples/docs/services/checkout-web/model.likec4 +39 -56
- package/examples/docs/services/order-service/arch.spec.md +13 -6
- package/examples/docs/services/order-service/asyncapi.yaml +3 -2
- package/examples/docs/services/order-service/model.likec4 +58 -70
- package/examples/docs/services/payment-service/arch.spec.md +7 -4
- package/examples/docs/services/payment-service/model.likec4 +38 -37
- package/examples/docs/services/platform/identity-service/arch.spec.md +7 -4
- package/examples/docs/services/platform/identity-service/model.likec4 +35 -37
- package/examples/docs/services/platform/notification-service/asyncapi.yaml +2 -1
- package/examples/docs/services/platform/notification-service/health.yaml +8 -3
- package/examples/docs/services/platform/notification-service/model.likec4 +43 -64
- package/package.json +1 -1
- package/dist/commands/validate/checks/vocabulary.js +0 -31
- package/dist/commands/validate/checks/vocabulary.js.map +0 -1
- package/dist/commands/validate/fleet/usecases/capability-tag.js.map +0 -1
- package/dist/commands/validate/fleet/usecases/requirement-tag.js.map +0 -1
- package/dist/commands/validate/fleet/usecases/steps.js.map +0 -1
- package/dist/commands/validate/fleet/usecases/usecases.js.map +0 -1
- package/dist/core/agent/agents-md/artifacts.js +0 -220
- package/dist/core/agent/agents-md/artifacts.js.map +0 -1
- package/dist/core/agent/agents-md/cycle.js +0 -165
- package/dist/core/agent/agents-md/cycle.js.map +0 -1
- package/dist/core/agent/agents-md/shipped/archive-gate.js +0 -98
- package/dist/core/agent/agents-md/shipped/archive-gate.js.map +0 -1
- package/dist/core/agent/agents-md/spine.js +0 -40
- package/dist/core/agent/agents-md/spine.js.map +0 -1
- package/dist/core/coherence/events/merged.js +0 -40
- package/dist/core/coherence/events/merged.js.map +0 -1
- package/dist/core/diff/current-state.js +0 -96
- package/dist/core/diff/current-state.js.map +0 -1
- package/dist/core/diff/victims.js +0 -247
- package/dist/core/diff/victims.js.map +0 -1
- package/dist/core/repo/tree/views.js +0 -172
- package/dist/core/repo/tree/views.js.map +0 -1
- package/dist/core/staging/snapshot.js.map +0 -1
- package/dist/core/verify/file.js.map +0 -1
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"uncovered.js","sourceRoot":"","sources":["../../../../src/core/capabilities/delta/uncovered.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * The `Realizes:` join taken INSIDE ONE FEATURE, in both directions: a promise\n * this feature makes that nothing in it keeps, and a promise it retires that\n * something outside it still keeps.\n *\n * `../realizes/findings.ts` grades the same join at the two places it can be\n * WRONG — an entry that resolves to nothing, a living requirement nothing\n * realizes. This module grades it at the one place it can silently COME APART:\n * the merge. Both directions are about a change, both are answered from\n * documents the archive gate already has parsed, and neither has any meaning\n * outside a feature — which is why they live together here and not beside their\n * fleet-scope siblings.\n *\n * TWO CODES, NOT ONE, and the decision is worth the paragraph because the shape\n * invites the opposite:\n *\n * - The FIXES are different. `capability.uncovered` is fixed by writing the\n * `Realizes:` line the promise is missing; `capability.remove-requirement-realized`\n * is fixed by retiring the requirement that carries the line, or by dropping\n * the line from it. A code is what a machine branches on and the branch is\n * the fix, so a single code covering both would be a code neither caller can\n * act on.\n * - The SEVERITIES are different, and they are different for the reason\n * `../../vocabulary/issue.ts` draws the distinction at all. `capability.uncovered`\n * is a warning that GATES: the document is legal — writing a business promise\n * ahead of the fleet is the recorded intended use, which is exactly why\n * `capability.requirement-unrealized` warns and never gates — while the MERGE\n * is what is unsafe, because once it lands the only thing that will ever\n * mention the promise again is a fleet-scope warning nobody reads. The\n * removal direction has no such legal reading: after that merge a LIVING\n * service document nobody in the feature touched carries a pointer at\n * nothing, and `capability.realizes-unknown` is already an ERROR there. So it\n * is an error here, exactly as `openapi.remove-op-consumed` is one axis over\n * for the identical shape — this feature retires a thing the living fleet\n * still uses. One code carrying two severities is a code that says nothing.\n * - The family already spells every direction of every join as its own code:\n * `capability.unknown` / `capability.realizes-unknown` name the entry that\n * resolves to nothing, `capability.unrealized` /\n * `capability.requirement-unrealized` name the promise nobody keeps. A pair\n * here is the shape a reader of that list already expects.\n *\n * A `#req-` TAGGED FLOW COUNTS AS COVER, and the reason it did not is worth\n * recording because it was a consequence rather than a policy. A `dynamic view`\n * used to be a fleet document with no feature-delta path, so a `#req-` tag\n * naming a capability requirement that was not living yet was ALREADY\n * `usecase.requirement-unresolved` — an error on `validate --all` — for the\n * whole window before the archive. At the moment this ran, a legal flow claim on\n * a newly-ADDED requirement could not exist, and the message said so: archive\n * with `--approve` first, tag the flow afterwards. That advice lands a promise\n * kept by nothing and relies on somebody coming back.\n *\n * `features/<FEAT>/usecases/` closed that (`core/usecases/delta/flows.ts`), so\n * the claim can now be legal at the moment it is graded, and the third arm of\n * `flows` below is it. What has NOT changed is which claims count: both tags\n * must resolve, against the vocabulary and requirement index this feature's own\n * merge would leave behind. A broken tag of either kind is an error in its own\n * right and must never also mark a promise kept — a typo that silenced this gate\n * would turn a mistake into a green archive.\n *\n * NO FILESYSTEM, deliberately, exactly as `../realizes/join.ts` has none — and\n * that is why the flows arrive as resolved pairs rather than as a docsDir. The\n * living side of the removal direction arrives as a thunk the caller supplies,\n * so the fleet-wide read happens only when this feature retires something, and\n * this module stays testable without a tree.\n */\nimport type { Requirement } from \"../../document/spec.js\";\nimport type { Issue } from \"../../vocabulary/issue.js\";\nimport { splitRealizesEntry } from \"../realizes/join.js\";\nimport { tagSlug } from \"../usecase-join.js\";\n\n/**\n * One requirements document on the SERVICE side of the join, and the two facts\n * that address it: whose it is, and which axis. Both halves of the key are\n * needed because a feature's delta of `payment-service`'s `arch.spec.md`\n * supersedes the living `arch.spec.md` and says nothing at all about the living\n * `spec.md` beside it.\n */\nexport interface RealizingDoc {\n /** The service id. */\n service: string;\n /** The axis's filename — `spec.md` or `arch.spec.md`, as `SPEC_AXES` spells it. */\n file: string;\n reqs: readonly Requirement[];\n}\n\n/**\n * One capability document a feature's delta changes, with the living text that\n * delta applies to.\n *\n * `living` is here rather than derived because a REMOVED requirement is exempt\n * from `capability.requirement-unidentified` and so may be spelled by heading\n * alone — the id it retires is then a fact about the LIVING document, and\n * without it the removal direction would silently grade nothing for the\n * commonest way a removal is written. Requirements rather than the delta\n * package's `LivingIndex`: `core/delta` imports this package, so importing it\n * back would be a package cycle the file-level cycle check cannot see.\n */\nexport interface CapabilityDeltaDoc {\n /** The capability id — the directory chain under `features/<FEAT>/capabilities/`. */\n id: string;\n reqs: readonly Requirement[];\n /** The LIVING `capabilities/<id>/spec.md` requirements, or `[]` when this feature creates it. */\n living: readonly Requirement[];\n}\n\n/**\n * The `(capability, requirement)` pair, keyed for a Set.\n *\n * NUL — the separator `core/delta/scope.ts`'s claim key already uses — rather\n * than `#`, because a key's separator must be one its halves cannot contain and\n * a capability id is a YAML key and a directory name, constrained nowhere.\n * Re-joining with `#` would make `a#b` + `C` and `a` + `b#C` one key, and the\n * second is unreachable only because the requirement grammar happens to exclude\n * `#` today.\n */\nfunction pairKey(capability: string, requirement: string): string {\n return `${capability}\\0${requirement}`;\n}\n\n/**\n * The pairs one requirement's `Realizes:` lines name.\n *\n * Through `splitRealizesEntry`, never a local split, and the LAST `#` is what\n * makes it right: the requirement half has a grammar that excludes `#` and the\n * capability half has none, so a first-`#` split mis-parses every id containing\n * one — silently, which is the failure this axis exists to refuse.\n */\nfunction realizedPairs(r: Requirement): string[] {\n return r.realizes.flatMap((entry) => {\n const target = splitRealizesEntry(entry);\n return target === null ? [] : [pairKey(target.capability, target.requirement)];\n });\n}\n\n/** How a service document is named in a message — the axis only where it is not the default one. */\nfunction docLabel(doc: RealizingDoc): string {\n return doc.file === \"spec.md\" ? doc.service : `${doc.service} (${doc.file})`;\n}\n\n/**\n * One promise a `#req-` tagged flow in the SAME feature keeps.\n *\n * Structurally typed rather than imported from `core/usecases/delta/claims.ts`,\n * where the caller builds it, and the reason is the package graph rather than\n * taste: `core/usecases/` already imports this package, so importing it back\n * would be a cycle that `import/no-cycle` cannot see — the acyclicity obligation\n * `scripts/package-graph.mjs` exists to hold. The shape is three fields; the\n * RULE that produced them (both tags resolved, or the claim does not count) is\n * that module's, and this one grades what it is handed.\n */\nexport interface KeptByFlow {\n capability: string;\n requirement: string;\n /** The view ids keeping it. Carried for a caller's report; nothing here reads it. */\n flows: readonly string[];\n}\n\n/**\n * The flow corpus, or the honest statement that there was not one to read.\n *\n * A TRI-STATE and not a possibly-empty list, because the message says what was\n * checked and an empty list would make it say something loam never looked at.\n * \"No flow keeps this\" and \"the flows were not graded\" are different sentences\n * and only one of them is an accusation — the standing rule this axis states\n * everywhere else, applied to the message rather than to a severity.\n *\n * `why` is printed, so it names the cause an author can act on: an\n * `architecture/` that did not parse is one thing, a `capabilities.yaml` that\n * does not read as a vocabulary is another, and inside a feature gate NOTHING\n * ELSE reports the second — `core/coherence/declared.ts` keeps\n * `capability.invalid` for `validate --all`. Without the sentence the run\n * refuses, names the wrong cause, and points at nothing.\n */\nexport type FlowCorpus =\n | { graded: true; kept: readonly KeptByFlow[] }\n | { graded: false; why: string };\n\n/**\n * `capability.uncovered` — a capability requirement this feature ADDS that no\n * `Realizes:` line in the same feature's service deltas names.\n *\n * ADDED ONLY. A MODIFIED changes a promise the living fleet may already\n * realize, so demanding a re-declaration would force an edit with nothing to\n * change — the same exemption `c4.uncovered` takes one axis over. A requirement\n * with no `Requirement-ID:` is SKIPPED rather than reported: it is already\n * `capability.requirement-unidentified`, and there is nothing for a `Realizes:`\n * line to address, so two findings for one mistake is the failure to avoid.\n *\n * The covering side is ADDED or MODIFIED, never BASE and never REMOVED. A BASE\n * requirement in a delta quotes living context and the merge never writes it,\n * so a `Realizes:` line there lands nowhere; a REMOVED one is on its way out and\n * cannot keep a promise after it is gone.\n *\n * Feature scope, deliberately. The fleet-scope version already ships as\n * `capability.requirement-unrealized`, and grading a feature-ADDED requirement\n * against the LIVING fleet is vacuous — no living `Realizes:` can name it\n * without being `capability.realizes-unknown` today.\n */\nexport function uncoveredIssues(\n capabilities: readonly CapabilityDeltaDoc[],\n services: readonly RealizingDoc[],\n // REQUIRED, with no default. A default would have to be one of the two arms,\n // and either choice is wrong for a caller that forgot: `graded: true` with an\n // empty list makes the message assert a negative, and `graded: false` makes it\n // quietly stop checking a corpus the caller does have. The compiler asking is\n // cheaper than either.\n flows: FlowCorpus,\n): Issue[] {\n if (capabilities.length === 0) return [];\n const kept = new Set<string>();\n for (const doc of services) {\n for (const r of doc.reqs) {\n if (r.kind !== \"ADDED\" && r.kind !== \"MODIFIED\") continue;\n for (const pair of realizedPairs(r)) kept.add(pair);\n }\n }\n // The other carrier, through the SAME `pairKey` as the `Realizes:` side: two\n // corpora answering one question must not be able to disagree about which\n // promise a claim addresses. The caller resolved these; a claim whose tags did\n // not both resolve never reaches here, which is what stops a typo from\n // silencing the gate.\n if (flows.graded) for (const flow of flows.kept) kept.add(pairKey(flow.capability, flow.requirement));\n // What the message may CLAIM about the flow half. Ungraded, it claims nothing\n // and says so — the `Realizes:` half was still checked and is still worth\n // gating on, so the finding stands; what must not stand is a sentence\n // convicting a flow nobody resolved.\n const flowClause = flows.graded\n ? \"and no `dynamic view` under this feature's usecases/ resolves a `#req-` tag to it. \"\n : `and the flow half of the join was NOT checked — ${flows.why}, so a \\`#req-\\` claim could not be resolved either way. `;\n const issues: Issue[] = [];\n for (const doc of capabilities) {\n for (const r of doc.reqs) {\n if (r.kind !== \"ADDED\" || r.id === undefined) continue;\n if (kept.has(pairKey(doc.id, r.id))) continue;\n const entry = `${doc.id}#${r.id}`;\n issues.push({\n severity: \"warn\",\n gates: true,\n code: \"capability.uncovered\",\n subject: doc.id,\n message:\n `capability ${doc.id}: ADDED requirement '${r.name}' (${r.id}) is realized by nothing this feature changes — ` +\n `no ADDED or MODIFIED requirement in its specs/ deltas carries \\`Realizes: ${entry}\\`, ` +\n flowClause +\n \"The document is legal; the MERGE is what is unsafe: once it lands, the only thing that will ever mention \" +\n \"this promise again is a fleet-scope warning (capability.requirement-unrealized) nobody reads. \" +\n `Three ways on: add \\`Realizes: ${entry}\\` to the service requirement that carries part of it; ` +\n `if the promise CROSSES services, write the flow that keeps it as features/<FEAT>/usecases/<name>.likec4 and ` +\n `tag its \\`dynamic view\\` \\`#cap-${tagSlug(doc.id)}\\` and \\`#req-${tagSlug(r.id)}\\` — the archive merges it into ` +\n \"architecture/usecases/ with everything else this feature lands; or archive with --approve to land the promise \" +\n \"ahead of the fleet and let capability.requirement-unrealized carry it.\",\n });\n }\n }\n return issues;\n}\n\n/** Everything the removal direction needs, and nothing it would have to read twice. */\nexport interface RemovalJoin {\n /** This feature's capability deltas, each with the living document it applies to. */\n capabilities: readonly CapabilityDeltaDoc[];\n /** This feature's own service delta documents, already parsed by the same walk. */\n deltas: readonly RealizingDoc[];\n /**\n * Every LIVING service requirements document in the fleet. A thunk, called at\n * most once and only when this feature actually retires a capability\n * requirement — the overwhelming majority of features retire none, and this\n * read is the whole fleet's `spec.md` and `arch.spec.md`.\n */\n living: () => Promise<readonly RealizingDoc[]>;\n}\n\n/** The living requirements a delta document supersedes — MODIFIED and REMOVED both restate or delete. */\nfunction supersedes(delta: RealizingDoc | undefined, living: Requirement): boolean {\n if (delta === undefined) return false;\n return delta.reqs.some((r) => {\n if (r.kind !== \"MODIFIED\" && r.kind !== \"REMOVED\") return false;\n return r.id === undefined ? r.name === living.name : r.id === living.id;\n });\n}\n\n/**\n * `capability.remove-requirement-realized` — this feature retires a capability\n * requirement that something the merge LEAVES BEHIND still realizes.\n *\n * The hole the forward direction does not close, and the one this whole module\n * exists to make impossible: a feature that REMOVES a capability requirement a\n * living service requirement realizes used to archive at exit 0, and the next\n * `loam validate --all` then failed with `capability.realizes-unknown` against a\n * service document nobody had touched. An archive that leaves the fleet red\n * against an untouched document is the exact class of silent damage the product\n * exists to refuse.\n *\n * GRADED AGAINST THE POST-MERGE FLEET, which is the only reading under which\n * the two escape hatches fall out instead of being special-cased. A living\n * requirement that this feature's own delta MODIFIES or REMOVES is gone as\n * written — a MODIFIED carries its FULL new text, so restating the requirement\n * without its `Realizes:` line IS how an author drops the join — and the delta's\n * own ADDED/MODIFIED requirements are what stands in its place. So: retire the\n * realizer in the same change and there is no breach; keep the line and there\n * is.\n *\n * A `#req-` tagged flow realizing the promise is NOT consulted, and that is the\n * other subject rather than a gap: the tag names the requirement directly, so a\n * removal turns it into `usecase.requirement-unresolved` on the living view —\n * loud, in the document that has to change, and nothing here would make it\n * louder.\n *\n * A REMOVED spelled by heading resolves through the living document, so it\n * grades exactly what the merge would delete. One that selects nothing is\n * already `delta.removed-unknown` and retires nothing, so it is skipped here —\n * two findings for one mistake, again.\n */\nexport async function removedRealizedIssues(join: RemovalJoin): Promise<Issue[]> {\n const retired = new Map<string, { capability: string; id: string; name: string }>();\n for (const doc of join.capabilities) {\n for (const r of doc.reqs) {\n if (r.kind !== \"REMOVED\") continue;\n const selected = doc.living.filter((l) => (r.id === undefined ? l.name === r.name : l.id === r.id));\n for (const l of selected) {\n if (l.id === undefined) continue;\n retired.set(pairKey(doc.id, l.id), { capability: doc.id, id: l.id, name: l.name });\n }\n }\n }\n if (retired.size === 0) return [];\n\n // Who still realizes each retired pair after the merge: the living\n // requirements this feature does not supersede, plus the requirements its own\n // deltas add or restate.\n const realizers = new Map<string, string[]>();\n const record = (pair: string, who: string): void => {\n if (!retired.has(pair)) return;\n const found = realizers.get(pair) ?? [];\n found.push(who);\n realizers.set(pair, found);\n };\n for (const doc of await join.living()) {\n const delta = join.deltas.find((d) => d.service === doc.service && d.file === doc.file);\n for (const r of doc.reqs) {\n if (supersedes(delta, r)) continue;\n for (const pair of realizedPairs(r)) record(pair, `${docLabel(doc)}'s living requirement '${r.name}'`);\n }\n }\n for (const doc of join.deltas) {\n for (const r of doc.reqs) {\n if (r.kind !== \"ADDED\" && r.kind !== \"MODIFIED\") continue;\n for (const pair of realizedPairs(r)) record(pair, `this feature's ${docLabel(doc)} delta requirement '${r.name}'`);\n }\n }\n\n const issues: Issue[] = [];\n for (const [pair, promise] of retired) {\n const who = realizers.get(pair);\n if (who === undefined) continue;\n const entry = `${promise.capability}#${promise.id}`;\n issues.push({\n severity: \"error\",\n code: \"capability.remove-requirement-realized\",\n subject: promise.capability,\n message:\n `capability ${promise.capability}: this feature REMOVES '${promise.name}' (${promise.id}), but ${who.join(\"; \")} ` +\n `still carries \\`Realizes: ${entry}\\`. After the merge that line points at a promise no document declares, and ` +\n \"the very next `loam validate --all` reports capability.realizes-unknown against whatever carries it — for a \" +\n \"LIVING realizer, a service document this feature never touched. Retire the realizing requirement in the same \" +\n \"feature, or drop its `Realizes:` line (a MODIFIED does that by restating the requirement without it), or \" +\n \"archive with --approve to break the join deliberately.\",\n });\n }\n return issues;\n}\n"]}
|
|
1
|
+
{"version":3,"file":"uncovered.js","sourceRoot":"","sources":["../../../../src/core/capabilities/delta/uncovered.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * The `Realizes:` join taken INSIDE ONE FEATURE, in both directions: a promise\n * this feature makes that nothing in it keeps, and a promise it retires that\n * something outside it still keeps.\n *\n * `../realizes/findings.ts` grades the same join at the two places it can be\n * WRONG — an entry that resolves to nothing, a living requirement nothing\n * realizes. This module grades it at the one place it can silently COME APART:\n * the merge. Both directions are about a change, both are answered from\n * documents the archive gate already has parsed, and neither has any meaning\n * outside a feature — which is why they live together here and not beside their\n * fleet-scope siblings.\n *\n * TWO CODES, NOT ONE, and the decision is worth the paragraph because the shape\n * invites the opposite:\n *\n * - The FIXES are different. `capability.uncovered` is fixed by writing the\n * `Realizes:` line the promise is missing; `capability.remove-requirement-realized`\n * is fixed by retiring the requirement that carries the line, or by dropping\n * the line from it. A code is what a machine branches on and the branch is\n * the fix, so a single code covering both would be a code neither caller can\n * act on.\n * - The SEVERITIES are different, and they are different for the reason\n * `../../vocabulary/issue.ts` draws the distinction at all. `capability.uncovered`\n * is a warning that GATES: the document is legal — writing a business promise\n * ahead of the fleet is the recorded intended use, which is exactly why\n * `capability.requirement-unrealized` warns and never gates — while the MERGE\n * is what is unsafe, because once it lands the only thing that will ever\n * mention the promise again is a fleet-scope warning nobody reads. The\n * removal direction has no such legal reading: after that merge a LIVING\n * service document nobody in the feature touched carries a pointer at\n * nothing, and `capability.realizes-unknown` is already an ERROR there. So it\n * is an error here, exactly as `openapi.remove-op-consumed` is one axis over\n * for the identical shape — this feature retires a thing the living fleet\n * still uses. One code carrying two severities is a code that says nothing.\n * - The family already spells every direction of every join as its own code:\n * `capability.unknown` / `capability.realizes-unknown` name the entry that\n * resolves to nothing, `capability.unrealized` /\n * `capability.requirement-unrealized` name the promise nobody keeps. A pair\n * here is the shape a reader of that list already expects.\n *\n * A `#req-` TAGGED FLOW COUNTS AS COVER, and the reason it did not is worth\n * recording because it was a consequence rather than a policy. A `dynamic view`\n * used to be a fleet document with no feature-delta path, so a `#req-` tag\n * naming a capability requirement that was not living yet was ALREADY\n * `usecase.requirement-unresolved` — an error on `validate --all` — for the\n * whole window before the archive. At the moment this ran, a legal flow claim on\n * a newly-ADDED requirement could not exist, and the message said so: archive\n * with `--approve` first, tag the flow afterwards. That advice lands a promise\n * kept by nothing and relies on somebody coming back.\n *\n * `features/<FEAT>/usecases/` closed that (`core/usecases/delta/flows.ts`), so\n * the claim can now be legal at the moment it is graded, and the third arm of\n * `flows` below is it. What has NOT changed is which claims count: both tags\n * must resolve, against the vocabulary and requirement index this feature's own\n * merge would leave behind. A broken tag of either kind is an error in its own\n * right and must never also mark a promise kept — a typo that silenced this gate\n * would turn a mistake into a green archive.\n *\n * NO FILESYSTEM, deliberately, exactly as `../realizes/join.ts` has none — and\n * that is why the flows arrive as resolved pairs rather than as a docsDir. The\n * living side of the removal direction arrives as a thunk the caller supplies,\n * so the fleet-wide read happens only when this feature retires something, and\n * this module stays testable without a tree.\n */\nimport type { Requirement } from \"../../document/spec.js\";\nimport type { Issue } from \"../../vocabulary/issue.js\";\nimport { splitRealizesEntry } from \"../realizes/join.js\";\nimport { tagSlug } from \"../usecase-join.js\";\n\n/**\n * One requirements document on the SERVICE side of the join, and the two facts\n * that address it: whose it is, and which axis. Both halves of the key are\n * needed because a feature's delta of `payment-service`'s `arch.spec.md`\n * supersedes the living `arch.spec.md` and says nothing at all about the living\n * `spec.md` beside it.\n */\nexport interface RealizingDoc {\n /** The service id. */\n service: string;\n /** The axis's filename — `spec.md` or `arch.spec.md`, as `SPEC_AXES` spells it. */\n file: string;\n reqs: readonly Requirement[];\n}\n\n/**\n * One capability document a feature's delta changes, with the living text that\n * delta applies to.\n *\n * `living` is here rather than derived because a REMOVED requirement is exempt\n * from `capability.requirement-unidentified` and so may be spelled by heading\n * alone — the id it retires is then a fact about the LIVING document, and\n * without it the removal direction would silently grade nothing for the\n * commonest way a removal is written. Requirements rather than the delta\n * package's `LivingIndex`: `core/delta` imports this package, so importing it\n * back would be a package cycle the file-level cycle check cannot see.\n */\nexport interface CapabilityDeltaDoc {\n /** The capability id — the directory chain under `features/<FEAT>/capabilities/`. */\n id: string;\n reqs: readonly Requirement[];\n /** The LIVING `capabilities/<id>/spec.md` requirements, or `[]` when this feature creates it. */\n living: readonly Requirement[];\n}\n\n/**\n * The `(capability, requirement)` pair, keyed for a Set.\n *\n * NUL — the separator `core/delta/scope.ts`'s claim key already uses — rather\n * than `#`, because a key's separator must be one its halves cannot contain and\n * a capability id is a YAML key and a directory name, constrained nowhere.\n * Re-joining with `#` would make `a#b` + `C` and `a` + `b#C` one key, and the\n * second is unreachable only because the requirement grammar happens to exclude\n * `#` today.\n */\nfunction pairKey(capability: string, requirement: string): string {\n return `${capability}\\0${requirement}`;\n}\n\n/**\n * The pairs one requirement's `Realizes:` lines name.\n *\n * Through `splitRealizesEntry`, never a local split, and the LAST `#` is what\n * makes it right: the requirement half has a grammar that excludes `#` and the\n * capability half has none, so a first-`#` split mis-parses every id containing\n * one — silently, which is the failure this axis exists to refuse.\n */\nfunction realizedPairs(r: Requirement): string[] {\n return r.realizes.flatMap((entry) => {\n const target = splitRealizesEntry(entry);\n return target === null ? [] : [pairKey(target.capability, target.requirement)];\n });\n}\n\n/** How a service document is named in a message — the axis only where it is not the default one. */\nfunction docLabel(doc: RealizingDoc): string {\n return doc.file === \"spec.md\" ? doc.service : `${doc.service} (${doc.file})`;\n}\n\n/**\n * One promise a `#req-` tagged flow in the SAME feature keeps.\n *\n * Structurally typed rather than imported from `core/usecases/delta/claims.ts`,\n * where the caller builds it, and the reason is the package graph rather than\n * taste: `core/usecases/` already imports this package, so importing it back\n * would be a cycle that `import/no-cycle` cannot see — the acyclicity obligation\n * `scripts/package-graph.mjs` exists to hold. The shape is three fields; the\n * RULE that produced them (both tags resolved, or the claim does not count) is\n * that module's, and this one grades what it is handed.\n */\nexport interface KeptByFlow {\n capability: string;\n requirement: string;\n /** The view ids keeping it. Carried for a caller's report; nothing here reads it. */\n flows: readonly string[];\n}\n\n/**\n * The flow corpus, or the honest statement that there was not one to read.\n *\n * A TRI-STATE and not a possibly-empty list, because the message says what was\n * checked and an empty list would make it say something loam never looked at.\n * \"No flow keeps this\" and \"the flows were not graded\" are different sentences\n * and only one of them is an accusation — the standing rule this axis states\n * everywhere else, applied to the message rather than to a severity.\n *\n * `why` is printed, so it names the cause an author can act on: an\n * `architecture/` that did not parse is one thing, a `capabilities.yaml` that\n * does not read as a vocabulary is another, and inside a feature gate NOTHING\n * ELSE reports the second — `core/coherence/declared.ts` keeps\n * `capability.invalid` for `validate --all`. Without the sentence the run\n * refuses, names the wrong cause, and points at nothing.\n */\nexport type FlowCorpus =\n | { graded: true; kept: readonly KeptByFlow[] }\n | { graded: false; why: string };\n\n/**\n * `capability.uncovered` — a capability requirement this feature ADDS that no\n * `Realizes:` line in the same feature's service deltas names.\n *\n * ADDED ONLY. A MODIFIED changes a promise the living fleet may already\n * realize, so demanding a re-declaration would force an edit with nothing to\n * change — the same exemption `c4.uncovered` takes one axis over. A requirement\n * with no `Requirement-ID:` is SKIPPED rather than reported: it is already\n * `capability.requirement-unidentified`, and there is nothing for a `Realizes:`\n * line to address, so two findings for one mistake is the failure to avoid.\n *\n * The covering side is ADDED or MODIFIED, never BASE and never REMOVED. A BASE\n * requirement in a delta quotes living context and the merge never writes it,\n * so a `Realizes:` line there lands nowhere; a REMOVED one is on its way out and\n * cannot keep a promise after it is gone.\n *\n * Feature scope, deliberately. The fleet-scope version already ships as\n * `capability.requirement-unrealized`, and grading a feature-ADDED requirement\n * against the LIVING fleet is vacuous — no living `Realizes:` can name it\n * without being `capability.realizes-unknown` today.\n */\nexport function uncoveredIssues(\n capabilities: readonly CapabilityDeltaDoc[],\n services: readonly RealizingDoc[],\n // REQUIRED, with no default. A default would have to be one of the two arms,\n // and either choice is wrong for a caller that forgot: `graded: true` with an\n // empty list makes the message assert a negative, and `graded: false` makes it\n // quietly stop checking a corpus the caller does have. The compiler asking is\n // cheaper than either.\n flows: FlowCorpus,\n): Issue[] {\n if (capabilities.length === 0) return [];\n const kept = new Set<string>();\n for (const doc of services) {\n for (const r of doc.reqs) {\n if (r.kind !== \"ADDED\" && r.kind !== \"MODIFIED\") continue;\n for (const pair of realizedPairs(r)) kept.add(pair);\n }\n }\n // The other carrier, through the SAME `pairKey` as the `Realizes:` side: two\n // corpora answering one question must not be able to disagree about which\n // promise a claim addresses. The caller resolved these; a claim whose tags did\n // not both resolve never reaches here, which is what stops a typo from\n // silencing the gate.\n if (flows.graded) for (const flow of flows.kept) kept.add(pairKey(flow.capability, flow.requirement));\n // What the message may CLAIM about the flow half. Ungraded, it claims nothing\n // and says so — the `Realizes:` half was still checked and is still worth\n // gating on, so the finding stands; what must not stand is a sentence\n // convicting a flow nobody resolved.\n const flowClause = flows.graded\n ? \"and no `dynamic view` under this feature's usecases/ resolves a `#req-` tag to it. \"\n : `and the flow half of the join was NOT checked — ${flows.why}, so a \\`#req-\\` claim could not be resolved either way. `;\n const issues: Issue[] = [];\n for (const doc of capabilities) {\n for (const r of doc.reqs) {\n if (r.kind !== \"ADDED\" || r.id === undefined) continue;\n if (kept.has(pairKey(doc.id, r.id))) continue;\n const entry = `${doc.id}#${r.id}`;\n issues.push({\n severity: \"warn\",\n gates: true,\n code: \"capability.uncovered\",\n subject: doc.id,\n message:\n `capability ${doc.id}: ADDED requirement '${r.name}' (${r.id}) is realized by nothing this feature changes — ` +\n `no ADDED or MODIFIED requirement in its specs/ deltas carries \\`Realizes: ${entry}\\`, ` +\n flowClause +\n \"The document is legal; the MERGE is what is unsafe: once it lands, the only thing that will ever mention \" +\n \"this promise again is a fleet-scope warning (capability.requirement-unrealized) nobody reads. \" +\n `Three ways on: add \\`Realizes: ${entry}\\` to the service requirement that carries part of it; ` +\n `if the promise CROSSES services, write the flow that keeps it as features/<FEAT>/usecases/<name>.likec4 and ` +\n `tag its \\`dynamic view\\` \\`#cap-${tagSlug(doc.id)}\\` and \\`#req-${tagSlug(r.id)}\\` — the archive merges it into ` +\n \"architecture/usecases/ with everything else this feature lands; or archive with --approve to land the promise \" +\n \"ahead of the fleet and let capability.requirement-unrealized carry it.\",\n });\n }\n }\n return issues;\n}\n\n/** Everything the removal direction needs, and nothing it would have to read twice. */\nexport interface RemovalJoin {\n /** This feature's capability deltas, each with the living document it applies to. */\n capabilities: readonly CapabilityDeltaDoc[];\n /** This feature's own service delta documents, already parsed by the same walk. */\n deltas: readonly RealizingDoc[];\n /**\n * Every LIVING service requirements document in the fleet. A thunk, called at\n * most once and only when this feature actually retires a capability\n * requirement — the overwhelming majority of features retire none, and this\n * read is the whole fleet's `spec.md` and `arch.spec.md`.\n */\n living: () => Promise<readonly RealizingDoc[]>;\n}\n\n/** The living requirements a delta document supersedes — MODIFIED and REMOVED both restate or delete. */\nfunction supersedes(delta: RealizingDoc | undefined, living: Requirement): boolean {\n if (delta === undefined) return false;\n return delta.reqs.some((r) => {\n if (r.kind !== \"MODIFIED\" && r.kind !== \"REMOVED\") return false;\n return r.id === undefined ? r.name === living.name : r.id === living.id;\n });\n}\n\n/**\n * `capability.remove-requirement-realized` — this feature retires a capability\n * requirement that something the merge LEAVES BEHIND still realizes.\n *\n * The hole the forward direction does not close, and the one this whole module\n * exists to make impossible: a feature that REMOVES a capability requirement a\n * living service requirement realizes used to archive at exit 0, and the next\n * `loam validate --all` then failed with `capability.realizes-unknown` against a\n * service document nobody had touched. An archive that leaves the fleet red\n * against an untouched document is the exact class of silent damage the product\n * exists to refuse.\n *\n * GRADED AGAINST THE POST-MERGE FLEET, which is the only reading under which\n * the two escape hatches fall out instead of being special-cased. A living\n * requirement that this feature's own delta MODIFIES or REMOVES is gone as\n * written — a MODIFIED carries its FULL new text, so restating the requirement\n * without its `Realizes:` line IS how an author drops the join — and the delta's\n * own ADDED/MODIFIED requirements are what stands in its place. So: retire the\n * realizer in the same change and there is no breach; keep the line and there\n * is.\n *\n * A `#req-` tagged flow realizing the promise is NOT consulted, and that is the\n * other subject rather than a gap: the tag names the requirement directly, so a\n * removal turns it into `usecase.requirement-unresolved` on the living view —\n * an error the delta walk raises before the archive, naming that view\n * (`core/usecases/delta/modified.ts`), and nothing here would make it louder.\n *\n * A REMOVED spelled by heading resolves through the living document, so it\n * grades exactly what the merge would delete. One that selects nothing is\n * already `delta.removed-unknown` and retires nothing, so it is skipped here —\n * two findings for one mistake, again.\n */\nexport async function removedRealizedIssues(join: RemovalJoin): Promise<Issue[]> {\n const retired = new Map<string, { capability: string; id: string; name: string }>();\n for (const doc of join.capabilities) {\n for (const r of doc.reqs) {\n if (r.kind !== \"REMOVED\") continue;\n const selected = doc.living.filter((l) => (r.id === undefined ? l.name === r.name : l.id === r.id));\n for (const l of selected) {\n if (l.id === undefined) continue;\n retired.set(pairKey(doc.id, l.id), { capability: doc.id, id: l.id, name: l.name });\n }\n }\n }\n if (retired.size === 0) return [];\n\n // Who still realizes each retired pair after the merge: the living\n // requirements this feature does not supersede, plus the requirements its own\n // deltas add or restate.\n const realizers = new Map<string, string[]>();\n const record = (pair: string, who: string): void => {\n if (!retired.has(pair)) return;\n const found = realizers.get(pair) ?? [];\n found.push(who);\n realizers.set(pair, found);\n };\n for (const doc of await join.living()) {\n const delta = join.deltas.find((d) => d.service === doc.service && d.file === doc.file);\n for (const r of doc.reqs) {\n if (supersedes(delta, r)) continue;\n for (const pair of realizedPairs(r)) record(pair, `${docLabel(doc)}'s living requirement '${r.name}'`);\n }\n }\n for (const doc of join.deltas) {\n for (const r of doc.reqs) {\n if (r.kind !== \"ADDED\" && r.kind !== \"MODIFIED\") continue;\n for (const pair of realizedPairs(r)) record(pair, `this feature's ${docLabel(doc)} delta requirement '${r.name}'`);\n }\n }\n\n const issues: Issue[] = [];\n for (const [pair, promise] of retired) {\n const who = realizers.get(pair);\n if (who === undefined) continue;\n const entry = `${promise.capability}#${promise.id}`;\n issues.push({\n severity: \"error\",\n code: \"capability.remove-requirement-realized\",\n subject: promise.capability,\n message:\n `capability ${promise.capability}: this feature REMOVES '${promise.name}' (${promise.id}), but ${who.join(\"; \")} ` +\n `still carries \\`Realizes: ${entry}\\`. After the merge that line points at a promise no document declares, and ` +\n \"the very next `loam validate --all` reports capability.realizes-unknown against whatever carries it — for a \" +\n \"LIVING realizer, a service document this feature never touched. Retire the realizing requirement in the same \" +\n \"feature, or drop its `Realizes:` line (a MODIFIED does that by restating the requirement without it), or \" +\n \"archive with --approve to break the join deliberately.\",\n });\n }\n return issues;\n}\n"]}
|
|
@@ -3,9 +3,13 @@
|
|
|
3
3
|
* entry the vocabulary does not declare, a vocabulary nobody can read, a
|
|
4
4
|
* declared capability no living requirement realizes, a directory that declares
|
|
5
5
|
* nothing, and the two rules an authored capability document itself must keep.
|
|
6
|
-
* Values only — core never prints — and the join rule
|
|
7
|
-
*
|
|
8
|
-
*
|
|
6
|
+
* Values only — core never prints — and the join rule written ONCE, as the
|
|
7
|
+
* Issue coherence gates on, which validate files as a Finding unchanged (an
|
|
8
|
+
* Issue is assignable to a Finding), with a literal code so
|
|
9
|
+
* test/codes-drift.test.ts can collect it. It used to be shaped twice, a
|
|
10
|
+
* Finding twin and an Issue twin with byte-identical bodies; one function over
|
|
11
|
+
* one message builder is what keeps the finding an author reads at the service
|
|
12
|
+
* target and the refusal they hit at `loam archive` from drifting apart.
|
|
9
13
|
*
|
|
10
14
|
* The opt-in rule lives here, and it deliberately DIVERGES from `Requires:`.
|
|
11
15
|
* There the LINE is the opt-in: a `Requires:` entry against a missing
|
|
@@ -31,8 +35,9 @@ import { requirementDigest } from "../document/spec.js";
|
|
|
31
35
|
*
|
|
32
36
|
* Exported because the suppression is not this module's private business — the
|
|
33
37
|
* use-case axis asks the same question about the same file
|
|
34
|
-
* (`commands/validate/fleet/
|
|
35
|
-
* `usecases/capability-tag.ts
|
|
38
|
+
* (`commands/validate/fleet/map/usecases.ts` hands the answer to
|
|
39
|
+
* `core/usecases/grade/capability-tag.ts`, and `core/usecases/delta/claims.ts`
|
|
40
|
+
* to the same grader for a feature's own flows), and a hand-rolled
|
|
36
41
|
* `present && invalid === undefined ? [...byId.keys()] : null` at each site is
|
|
37
42
|
* three copies of one rule. The failure that costs is not hypothetical: the next
|
|
38
43
|
* un-gradable state added to `CapabilityVocabulary` — "present but partially
|
|
@@ -77,23 +82,14 @@ function unknownMessage(where, u) {
|
|
|
77
82
|
: ". Declare it in either — a name alone is a `capabilities: {<id>: {description, owner}}` entry, a name with prose behind it is the document — or fix the spelling"));
|
|
78
83
|
}
|
|
79
84
|
/**
|
|
80
|
-
* `capability.unknown`
|
|
81
|
-
*
|
|
82
|
-
*
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
subject: target.subject,
|
|
89
|
-
message: unknownMessage(target.where, u),
|
|
90
|
-
}));
|
|
91
|
-
}
|
|
92
|
-
/**
|
|
93
|
-
* The same rule shaped as coherence Issues, for a feature's delta documents: a
|
|
94
|
-
* delta requirement naming an undeclared capability would merge a join that
|
|
95
|
-
* resolves to nothing, so it gates `loam archive` like other errors and stays
|
|
96
|
-
* `--approve`-overridable. The one new archive gate of this axis.
|
|
85
|
+
* `capability.unknown` — an ERROR, the same grade as `Requires:` and for its
|
|
86
|
+
* reason: an invented capability reads exactly like a real one in the
|
|
87
|
+
* requirement and in every rollup built over it. Validate files it as a
|
|
88
|
+
* Finding at the service target. For a feature's delta documents it is a
|
|
89
|
+
* coherence Issue: a delta requirement naming an undeclared capability would
|
|
90
|
+
* merge a join that resolves to nothing, so it gates `loam archive` like other
|
|
91
|
+
* errors and stays `--approve`-overridable. The one new archive gate of this
|
|
92
|
+
* axis.
|
|
97
93
|
*/
|
|
98
94
|
export function capabilityUnknownIssues(reqs, target, vocab) {
|
|
99
95
|
return unknownEntries(reqs, vocab).map((u) => ({
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"findings.js","sourceRoot":"","sources":["../../../src/core/capabilities/findings.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;GAuBG;AACH,OAAO,EAAE,QAAQ,EAAE,MAAM,eAAe,CAAC;AACzC,OAAO,EAAE,UAAU,EAAE,MAAM,oBAAoB,CAAC;AAChD,OAAO,EAAE,iBAAiB,EAAoB,MAAM,qBAAqB,CAAC;AAa1E;;;;;;;;;;;;;;;;;;;;;;GAsBG;AACH,MAAM,UAAU,qBAAqB,CAAC,KAA2B;IAC/D,IAAI,CAAC,KAAK,CAAC,OAAO,IAAI,KAAK,CAAC,OAAO,KAAK,SAAS;QAAE,OAAO,IAAI,CAAC;IAC/D,OAAO,CAAC,GAAG,KAAK,CAAC,IAAI,CAAC,IAAI,EAAE,CAAC,CAAC;AAChC,CAAC;AASD,iHAAiH;AACjH,SAAS,cAAc,CAAC,IAAmB,EAAE,KAA2B;IACtE,MAAM,KAAK,GAAG,qBAAqB,CAAC,KAAK,CAAC,CAAC;IAC3C,IAAI,KAAK,KAAK,IAAI;QAAE,OAAO,EAAE,CAAC;IAC9B,MAAM,GAAG,GAAmB,EAAE,CAAC;IAC/B,KAAK,MAAM,CAAC,IAAI,IAAI,EAAE,CAAC;QACrB,IAAI,CAAC,CAAC,IAAI,KAAK,SAAS;YAAE,SAAS;QACnC,KAAK,MAAM,KAAK,IAAI,CAAC,CAAC,YAAY,EAAE,CAAC;YACnC,IAAI,KAAK,CAAC,IAAI,CAAC,GAAG,CAAC,KAAK,CAAC;gBAAE,SAAS;YACpC,GAAG,CAAC,IAAI,CAAC,EAAE,WAAW,EAAE,CAAC,CAAC,IAAI,EAAE,KAAK,EAAE,KAAK,EAAE,QAAQ,CAAC,KAAK,EAAE,KAAK,CAAC,EAAE,CAAC,CAAC;QAC1E,CAAC;IACH,CAAC;IACD,OAAO,GAAG,CAAC;AACb,CAAC;AAED,SAAS,cAAc,CAAC,KAAa,EAAE,CAAe;IACpD,OAAO,CACL,GAAG,KAAK,kBAAkB,CAAC,CAAC,WAAW,oBAAoB,CAAC,CAAC,KAAK,+EAA+E,CAAC,CAAC,KAAK,UAAU;QAClK,CAAC,CAAC,CAAC,KAAK,CAAC,MAAM,GAAG,CAAC;YACjB,CAAC,CAAC,mBAAmB,CAAC,CAAC,KAAK,CAAC,IAAI,CAAC,IAAI,CAAC,GAAG;YAC1C,CAAC,CAAC,kKAAkK,CAAC,CACxK,CAAC;AACJ,CAAC;AAED;;;;GAIG;AACH,MAAM,UAAU,yBAAyB,CACvC,IAAmB,EACnB,MAAwB,EACxB,KAA2B;IAE3B,OAAO,cAAc,CAAC,IAAI,EAAE,KAAK,CAAC,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC;QAC7C,QAAQ,EAAE,OAAgB;QAC1B,IAAI,EAAE,oBAAoB;QAC1B,OAAO,EAAE,MAAM,CAAC,OAAO;QACvB,OAAO,EAAE,cAAc,CAAC,MAAM,CAAC,KAAK,EAAE,CAAC,CAAC;KACzC,CAAC,CAAC,CAAC;AACN,CAAC;AAED;;;;;GAKG;AACH,MAAM,UAAU,uBAAuB,CACrC,IAAmB,EACnB,MAAwB,EACxB,KAA2B;IAE3B,OAAO,cAAc,CAAC,IAAI,EAAE,KAAK,CAAC,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC;QAC7C,QAAQ,EAAE,OAAgB;QAC1B,IAAI,EAAE,oBAA6B;QACnC,OAAO,EAAE,MAAM,CAAC,OAAO;QACvB,OAAO,EAAE,cAAc,CAAC,MAAM,CAAC,KAAK,EAAE,CAAC,CAAC;KACzC,CAAC,CAAC,CAAC;AACN,CAAC;AAED;;;;;;;;;GASG;AACH,MAAM,UAAU,wBAAwB,CAAC,KAA2B;IAClE,IAAI,KAAK,CAAC,OAAO,KAAK,SAAS;QAAE,OAAO,IAAI,CAAC;IAC7C,OAAO;QACL,QAAQ,EAAE,OAAO;QACjB,IAAI,EAAE,oBAAoB;QAC1B,OAAO,EACL,6EAA6E,KAAK,CAAC,OAAO,IAAI;YAC9F,sJAAsJ;YACtJ,wGAAwG;KAC3G,CAAC;AACJ,CAAC;AAED;;;;;;;GAOG;AACH,MAAM,UAAU,kBAAkB,CAAC,KAA2B,EAAE,IAAyB;IACvF,MAAM,QAAQ,GAAG,qBAAqB,CAAC,KAAK,CAAC,CAAC;IAC9C,IAAI,QAAQ,KAAK,IAAI;QAAE,OAAO,EAAE,CAAC;IACjC,OAAO,QAAQ;SACZ,MAAM,CAAC,CAAC,EAAE,EAAE,EAAE,CAAC,CAAC,IAAI,CAAC,GAAG,CAAC,EAAE,CAAC,CAAC;SAC7B,IAAI,CAAC,UAAU,CAAC;SAChB,GAAG,CAAC,CAAC,EAAE,EAAE,EAAE,CAAC,CAAC;QACZ,QAAQ,EAAE,MAAe;QACzB,IAAI,EAAE,uBAAuB;QAC7B,OAAO,EAAE,EAAE;QACX,OAAO,EACL,0BAA0B,EAAE,oBAAoB,UAAU,CAAC,KAAK,EAAE,EAAE,CAAC,0CAA0C;YAC/G,kIAAkI;YAClI,4JAA4J;KAC/J,CAAC,CAAC,CAAC;AACR,CAAC;AAED;;;;;GAKG;AACH,SAAS,UAAU,CAAC,KAA2B,EAAE,EAAU;IACzD,MAAM,MAAM,GAAG,KAAK,CAAC,IAAI,CAAC,GAAG,CAAC,EAAE,CAAC,EAAE,MAAM,CAAC;IAC1C,IAAI,MAAM,KAAK,MAAM;QAAE,OAAO,gBAAgB,EAAE,UAAU,CAAC;IAC3D,IAAI,MAAM,KAAK,MAAM;QAAE,OAAO,mDAAmD,EAAE,UAAU,CAAC;IAC9F,OAAO,gCAAgC,CAAC;AAC1C,CAAC;AAED,wEAAwE;AACxE,0EAA0E;AAC1E,0EAA0E;AAC1E,wEAAwE;AAExE;;;;;;;;;;;;;;;;;;;;;GAqBG;AACH,MAAM,UAAU,kBAAkB,CAAC,IAAoB,EAAE,cAAuB;IAC9E,OAAO,IAAI,CAAC,YAAY,CAAC,GAAG,CAAC,CAAC,MAAM,EAAE,EAAE;QACtC,MAAM,IAAI,GAAG,cAAc,KAAK,SAAS,CAAC,CAAC,CAAC,MAAM,CAAC,CAAC,CAAC,YAAY,cAAc,IAAI,MAAM,EAAE,CAAC;QAC5F,MAAM,IAAI,GACR,cAAc,KAAK,SAAS;YAC1B,CAAC,CAAC,6FAA6F;gBAC7F,SAAS,IAAI,8EAA8E;YAC7F,CAAC,CAAC,4HAA4H;gBAC5H,SAAS,IAAI,0EAA0E,CAAC;QAC9F,OAAO;YACL,QAAQ,EAAE,MAAe;YACzB,IAAI,EAAE,wBAAwB;YAC9B,OAAO,EAAE,IAAI;YACb,OAAO,EAAE,GAAG,IAAI,sIAAsI,IAAI,EAAE;SAC7J,CAAC;IACJ,CAAC,CAAC,CAAC;AACL,CAAC;AAED;;;;;;;;;;;;;;;;GAgBG;AACH,MAAM,aAAa,GAAG;IACpB,EAAE,KAAK,EAAE,YAAY,EAAE,IAAI,EAAE,aAAa,EAAE,KAAK,EAAE,4CAA4C,EAAE;IACjG,EAAE,KAAK,EAAE,QAAQ,EAAE,IAAI,EAAE,SAAS,EAAE,KAAK,EAAE,qCAAqC,EAAE;IAClF,EAAE,KAAK,EAAE,WAAW,EAAE,IAAI,EAAE,YAAY,EAAE,KAAK,EAAE,wCAAwC,EAAE;IAC3F,EAAE,KAAK,EAAE,UAAU,EAAE,IAAI,EAAE,WAAW,EAAE,KAAK,EAAE,wCAAwC,EAAE;CACE,CAAC;AAE9F;;;;;;;;;;;;;;;;;;GAkBG;AACH,MAAM,cAAc,GAAG;IACrB;QACE,KAAK,EAAE,cAAc;QACrB,IAAI,EAAE,aAAa;QACnB,GAAG,EAAE,gHAAgH;KACtH;IACD;QACE,KAAK,EAAE,UAAU;QACjB,IAAI,EAAE,WAAW;QACjB,GAAG,EAAE,6GAA6G;KACnH;CACwF,CAAC;AAoB5F,MAAM,UAAU,qBAAqB,CAAC,IAAmB,EAAE,EAAU,EAAE,KAAK,GAAG,gBAAgB,EAAE,UAAU;IACzG,MAAM,QAAQ,GAAe,EAAE,CAAC;IAChC,KAAK,MAAM,CAAC,IAAI,IAAI,EAAE,CAAC;QACrB,IAAI,CAAC,CAAC,EAAE,KAAK,SAAS,EAAE,CAAC;YACvB,QAAQ,CAAC,IAAI,CAAC;gBACZ,QAAQ,EAAE,OAAO;gBACjB,IAAI,EAAE,qCAAqC;gBAC3C,OAAO,EAAE,EAAE;gBACX,OAAO,EACL,GAAG,KAAK,kBAAkB,CAAC,CAAC,IAAI,qCAAqC;oBACrE,kJAAkJ;oBAClJ,8GAA8G;oBAC9G,oDAAoD;aACvD,CAAC,CAAC;QACL,CAAC;QACD,KAAK,MAAM,IAAI,IAAI,aAAa,EAAE,CAAC;YACjC,IAAI,CAAC,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC,MAAM,KAAK,CAAC;gBAAE,SAAS;YACzC,QAAQ,CAAC,IAAI,CAAC;gBACZ,QAAQ,EAAE,OAAO;gBACjB,IAAI,EAAE,uCAAuC;gBAC7C,OAAO,EAAE,EAAE;gBACX,OAAO,EACL,GAAG,KAAK,kBAAkB,CAAC,CAAC,IAAI,eAAe,IAAI,CAAC,IAAI,mBAAmB,IAAI,CAAC,KAAK,IAAI;oBACzF,iKAAiK;oBACjK,sFAAsF;oBACtF,oHAAoH;aACvH,CAAC,CAAC;QACL,CAAC;QACD,KAAK,MAAM,IAAI,IAAI,cAAc,EAAE,CAAC;YAClC,IAAI,CAAC,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC,MAAM,KAAK,CAAC;gBAAE,SAAS;YACzC,QAAQ,CAAC,IAAI,CAAC;gBACZ,QAAQ,EAAE,OAAO;gBACjB,IAAI,EAAE,mCAAmC;gBACzC,OAAO,EAAE,EAAE;gBACX,OAAO,EACL,GAAG,KAAK,kBAAkB,CAAC,CAAC,IAAI,eAAe,IAAI,CAAC,IAAI,oDAAoD;oBAC5G,GAAG,IAAI,CAAC,GAAG,qGAAqG;oBAChH,wGAAwG;oBACxG,eAAe,EAAE,IAAI,CAAC,CAAC,EAAE,IAAI,kBAAkB,KAAK;aACvD,CAAC,CAAC;QACL,CAAC;IACH,CAAC;IACD,OAAO,QAAQ,CAAC;AAClB,CAAC;AAED;;;;;;;;;;;;;;;;;;;;GAoBG;AACH,MAAM,CAAC,KAAK,UAAU,0BAA0B,CAC9C,KAA2B,EAC3B,IAA8C;IAE9C,MAAM,QAAQ,GAAG,qBAAqB,CAAC,KAAK,CAAC,CAAC;IAC9C,MAAM,YAAY,GAAG,IAAI,GAAG,EAA+B,CAAC;IAC5D,MAAM,OAAO,GAAG,IAAI,GAAG,EAAuC,CAAC;IAC/D,IAAI,QAAQ,KAAK,IAAI;QAAE,OAAO,EAAE,QAAQ,EAAE,YAAY,EAAE,OAAO,EAAE,CAAC;IAClE,KAAK,MAAM,GAAG,IAAI,KAAK,CAAC,IAAI,CAAC,IAAI,EAAE,CAAC;QAClC,MAAM,IAAI,GAAG,MAAM,IAAI,CAAC,GAAG,CAAC,IAAI,CAAC,CAAC;QAClC,wEAAwE;QACxE,sEAAsE;QACtE,wEAAwE;QACxE,MAAM,IAAI,GAAG,IAAI,CAAC,MAAM,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,IAAI,KAAK,SAAS,IAAI,CAAC,CAAC,EAAE,KAAK,SAAS,CAAC,CAAC;QAC5E,YAAY,CAAC,GAAG,CAAC,GAAG,CAAC,EAAE,EAAE,IAAI,GAAG,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,EAAG,CAAC,CAAC,CAAC,CAAC;QAC1D,OAAO,CAAC,GAAG,CAAC,GAAG,CAAC,EAAE,EAAE,IAAI,GAAG,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,CAAC,EAAG,EAAE,iBAAiB,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC;IAC/E,CAAC;IACD,OAAO,EAAE,QAAQ,EAAE,YAAY,EAAE,OAAO,EAAE,CAAC;AAC7C,CAAC","sourcesContent":["/**\n * The capability grades, in every direction the axis is graded: a `Capability:`\n * entry the vocabulary does not declare, a vocabulary nobody can read, a\n * declared capability no living requirement realizes, a directory that declares\n * nothing, and the two rules an authored capability document itself must keep.\n * Values only — core never prints — and the join rule shaped twice (Finding for\n * validate, Issue for coherence), both spelled with literal codes so\n * test/codes-drift.test.ts can collect them.\n *\n * The opt-in rule lives here, and it deliberately DIVERGES from `Requires:`.\n * There the LINE is the opt-in: a `Requires:` entry against a missing\n * permissions.yaml errors, because an authorization join that resolves against\n * nothing is the failure that axis exists to prevent. Here the FLEET'S OWN\n * FILES are the opt-in — `architecture/capabilities.yaml`, the `capabilities/`\n * tree, or both — so a fleet holding neither stays finding-free however many\n * `Capability:` lines its requirements already carry. (The roadmap's exit\n * criterion was written when the YAML was the only side and says 'a fleet with\n * no architecture/capabilities.yaml produces no capability findings at all';\n * the authored tree is the second way to opt in, and `CapabilityVocabulary.present`\n * is where the two are combined.) An INVALID vocabulary is also silence for the\n * per-entry grades — `capability.invalid` is the honest finding there, and\n * grading a hundred entries against a file nobody can read is the cascade that\n * suppression exists to stop.\n */\nimport { closeIds } from \"../c4/arch.js\";\nimport { compareIds } from \"../repo/entries.js\";\nimport { requirementDigest, type Requirement } from \"../document/spec.js\";\nimport type { Finding } from \"../vocabulary/report.js\";\nimport type { CapabilityDocCode, Issue } from \"../vocabulary/issue.js\";\nimport type { CapabilityVocabulary } from \"./capabilities.js\";\nimport type { CapabilityTree } from \"./tree.js\";\nimport type { CapabilityRequirementIndex } from \"./realizes/join.js\";\n\n/** Which document is being graded, and under whose name the finding is filed. */\nexport interface CapabilityTarget {\n where: string;\n subject: string;\n}\n\n/**\n * THE LADDER, and the only place it is applied: the declared capability ids when\n * the vocabulary can be graded against at all, `null` when it cannot.\n *\n * Exported because the suppression is not this module's private business — the\n * use-case axis asks the same question about the same file\n * (`commands/validate/fleet/landscape.ts` hands the answer to\n * `usecases/capability-tag.ts`), and a hand-rolled\n * `present && invalid === undefined ? [...byId.keys()] : null` at each site is\n * three copies of one rule. The failure that costs is not hypothetical: the next\n * un-gradable state added to `CapabilityVocabulary` — \"present but partially\n * readable\" is the obvious one, since `readCapabilities` already treats a\n * non-mapping declaration body as `{}` — gets fixed wherever the greps land, and\n * any copy left behind answers with the whole key set. A half-read vocabulary\n * then reads as a complete one, and every `#cap-` tag whose capability lived in\n * the unreadable half becomes one `usecase.capability-unresolved` ERROR per\n * view: the cascade this ladder exists to prevent, arriving through the ladder.\n *\n * `null` rather than `[]`, and the two are never interchangeable: an empty list\n * is the real verdict \"this fleet declares no capabilities\" and every entry\n * fails against it, while `null` says there is nothing to grade against and\n * suspends the family. Callers that only need the boolean read `=== null`.\n */\nexport function gradableCapabilityIds(vocab: CapabilityVocabulary): readonly string[] | null {\n if (!vocab.present || vocab.invalid !== undefined) return null;\n return [...vocab.byId.keys()];\n}\n\n/** One unresolved entry: the requirement that wrote it, the entry, the close names. */\ninterface UnknownEntry {\n requirement: string;\n entry: string;\n close: string[];\n}\n\n/** Non-REMOVED requirements' entries the vocabulary does not declare — or nothing, per the opt-in rule above. */\nfunction unknownEntries(reqs: Requirement[], vocab: CapabilityVocabulary): UnknownEntry[] {\n const known = gradableCapabilityIds(vocab);\n if (known === null) return [];\n const out: UnknownEntry[] = [];\n for (const r of reqs) {\n if (r.kind === \"REMOVED\") continue;\n for (const entry of r.capabilities) {\n if (vocab.byId.has(entry)) continue;\n out.push({ requirement: r.name, entry, close: closeIds(entry, known) });\n }\n }\n return out;\n}\n\nfunction unknownMessage(where: string, u: UnknownEntry): string {\n return (\n `${where}: requirement '${u.requirement}' — Capability: '${u.entry}' is declared neither in architecture/capabilities.yaml nor as capabilities/${u.entry}/spec.md` +\n (u.close.length > 0\n ? `. Did you mean: ${u.close.join(\", \")}?`\n : \". Declare it in either — a name alone is a `capabilities: {<id>: {description, owner}}` entry, a name with prose behind it is the document — or fix the spelling\")\n );\n}\n\n/**\n * `capability.unknown` as validate sees it — an ERROR, the same grade as\n * `Requires:` and for its reason: an invented capability reads exactly like a\n * real one in the requirement and in every rollup built over it.\n */\nexport function capabilityUnknownFindings(\n reqs: Requirement[],\n target: CapabilityTarget,\n vocab: CapabilityVocabulary,\n): Finding[] {\n return unknownEntries(reqs, vocab).map((u) => ({\n severity: \"error\" as const,\n code: \"capability.unknown\",\n subject: target.subject,\n message: unknownMessage(target.where, u),\n }));\n}\n\n/**\n * The same rule shaped as coherence Issues, for a feature's delta documents: a\n * delta requirement naming an undeclared capability would merge a join that\n * resolves to nothing, so it gates `loam archive` like other errors and stays\n * `--approve`-overridable. The one new archive gate of this axis.\n */\nexport function capabilityUnknownIssues(\n reqs: Requirement[],\n target: CapabilityTarget,\n vocab: CapabilityVocabulary,\n): Issue[] {\n return unknownEntries(reqs, vocab).map((u) => ({\n severity: \"error\" as const,\n code: \"capability.unknown\" as const,\n subject: target.subject,\n message: unknownMessage(target.where, u),\n }));\n}\n\n/**\n * `capability.invalid` — the whole run's ONE finding about an unreadable\n * vocabulary, spelling the expected shape. Every grade that RESOLVES AGAINST\n * the vocabulary stays suspended behind it (see the module comment); the\n * authored tree's own grades do not, because none of them consults it — a\n * directory holding no document, a requirement without a stable id and a\n * requirement carrying a service-level join are facts about files that a broken\n * YAML does not make untrue, and hiding them until an unrelated file parses is\n * suppression without the cascade that justifies it.\n */\nexport function invalidVocabularyFinding(vocab: CapabilityVocabulary): Finding | null {\n if (vocab.invalid === undefined) return null;\n return {\n severity: \"error\",\n code: \"capability.invalid\",\n message:\n `landscape: architecture/capabilities.yaml does not read as a vocabulary — ${vocab.invalid}. ` +\n \"Every `Capability:` line in the fleet resolves against this file, so the whole capability family is suspended until it parses — fix the YAML first. \" +\n \"The shape is `capabilities: {<id>: {description, owner}}`, ids flat keys with `/` allowed for nesting.\",\n };\n}\n\n/**\n * `capability.unrealized` — ONE warn per declared capability no living\n * non-REMOVED requirement names, subject = the id, sorted with compareIds.\n * Per capability rather than one rollup finding with details[] (the\n * permissions.unenforced shape) because the roadmap's own exit criterion is\n * 'one warning per capability, not one per service' — each is a distinct\n * promise nobody implemented, or a distinct word nobody adopted.\n */\nexport function unrealizedFindings(vocab: CapabilityVocabulary, used: ReadonlySet<string>): Finding[] {\n const declared = gradableCapabilityIds(vocab);\n if (declared === null) return [];\n return declared\n .filter((id) => !used.has(id))\n .sort(compareIds)\n .map((id) => ({\n severity: \"warn\" as const,\n code: \"capability.unrealized\",\n subject: id,\n message:\n `landscape: capability '${id}' is declared in ${declaredIn(vocab, id)} and nothing in the fleet realizes it — ` +\n \"no living requirement names it with `Capability:` or `Realizes:`, and no `#cap-`/`#req-` tagged flow keeps one of its promises. \" +\n \"Either a promise nobody implemented or a word nobody adopted; write the requirement that realizes it, draw the flow that keeps it, or drop the declaration\",\n }));\n}\n\n/**\n * Where an id was declared, spelled for a message. The fix differs by side —\n * a YAML line is deleted, a document is deleted with its directory — and a\n * message naming the wrong file sends its reader to edit something that is not\n * there. `both` names both because either alone would leave the id declared.\n */\nfunction declaredIn(vocab: CapabilityVocabulary, id: string): string {\n const source = vocab.byId.get(id)?.source;\n if (source === \"tree\") return `capabilities/${id}/spec.md`;\n if (source === \"both\") return `architecture/capabilities.yaml and capabilities/${id}/spec.md`;\n return \"architecture/capabilities.yaml\";\n}\n\n/* ------------------------------------------------------------------ */\n/* The authored tree — the documents themselves, graded on their own */\n/* terms rather than on the join any requirement makes to them. */\n/* ------------------------------------------------------------------ */\n\n/**\n * `capability.doc-missing` — a directory in a `capabilities/` tree that holds no\n * `spec.md` and has no capability beneath it.\n *\n * WARN, not error, and the severity is the whole judgement: the state it names\n * is what `mkdir` leaves behind halfway through creating a capability, and a\n * half-finished authoring step must not fail somebody else's `loam validate`\n * on the same tree. What makes it worth a finding at all is that the directory\n * LOOKS declared — a reader browsing `capabilities/` counts it, and a\n * `Capability:` line naming it still reports `capability.unknown`, which is the\n * confusing pair this warning exists to join up.\n *\n * BOTH TREES EARN IT, and `featureDirName` is which one. The same walk reads\n * `features/<dir>/capabilities/`, so the same `mkdir` leaves the same shape\n * there — quieter, not louder: an empty directory is not a delta, so the delta\n * algebra, the merge and `loam show` alike do not see it and the business change\n * ships as nothing at exit 0. One code, one mistake; two messages, because the\n * path and the fix differ. It is the DIRECTORY name and not the feature id\n * (`FEAT-1-split` is where the directory is), and `undocumented` is spelled from\n * the WALK'S root — literally `capabilities/…` on both sides — so a feature's\n * entry is re-rooted here or the message names a path nobody can open.\n */\nexport function docMissingFindings(tree: CapabilityTree, featureDirName?: string): Finding[] {\n return tree.undocumented.map((walked) => {\n const path = featureDirName === undefined ? walked : `features/${featureDirName}/${walked}`;\n const rest =\n featureDirName === undefined\n ? \"so this one declares nothing and a requirement naming it still reports capability.unknown. \" +\n `Write ${path}/spec.md (the narrative, then \\`## Requirements\\`), or remove the directory.`\n : \"so this one changes nothing — the archive merges the capability deltas the walk FINDS, and an empty directory is not one. \" +\n `Write ${path}/spec.md (a \\`## ADDED Requirements\\` section), or remove the directory.`;\n return {\n severity: \"warn\" as const,\n code: \"capability.doc-missing\",\n subject: path,\n message: `${path}/ holds no spec.md and no capability beneath it — a directory under capabilities/ is a capability only when it holds the document, ${rest}`,\n };\n });\n}\n\n/**\n * The service-level joins, and the whole of what \"names no service\" is checked\n * as. Each entry is a `Requirement` field that is non-empty exactly when the\n * requirement wrote the line and named something in it.\n *\n * THE PROSE RULE IS NOT CHECKED AND MUST NOT BE FAKED. \"A capability\n * requirement names no service\" is an authoring rule about wording, and the\n * only mechanical reading of it — scanning the text for a declared service id —\n * is a heuristic that convicts a payments capability for the word \"payments\".\n * loam refuses that class of check everywhere else and refuses it here; PR\n * review holds the prose. What IS exact is the structural half: these four\n * lines resolve against a SERVICE's contract and model, so a requirement\n * carrying one has already left the business altitude whatever its wording\n * says. `Requires:` is deliberately absent — a permission is a domain fact\n * (\"who may see a profile\"), observable outside the fleet, and gating a\n * capability requirement on one is legitimate.\n */\nconst SERVICE_JOINS = [\n { field: \"operations\", line: \"Operations:\", names: \"an operationId in a service's openapi.yaml\" },\n { field: \"covers\", line: \"Covers:\", names: \"a C4 element, edge or health signal\" },\n { field: \"publishes\", line: \"Publishes:\", names: \"an AsyncAPI message a service produces\" },\n { field: \"consumes\", line: \"Consumes:\", names: \"an AsyncAPI message a service consumes\" },\n] as const satisfies ReadonlyArray<{ field: keyof Requirement; line: string; names: string }>;\n\n/**\n * The two joins that point INTO the capability tree, and are therefore inert\n * written inside it.\n *\n * Not \"wrong at this altitude\" like the four above — these are the axis's own\n * lines. They are refused here because nothing reads them: `capabilityRollup`\n * joins SERVICE requirements to capabilities, so a `Capability:` line in a\n * capability document is a requirement claiming the document it is already in,\n * and a `Realizes:` line there points at a promise loam will never see anybody\n * keep. Both parse, both look exactly like the working joins one directory\n * over, and both would sit in a reviewed document meaning nothing.\n *\n * A capability realizing ANOTHER capability's requirement — a nested\n * `payments/refunds` serving a promise made by `payments` — is a real idea and\n * is deliberately not being smuggled in as a line that happens to parse. It\n * would need a design: a second corpus of joins, its own cycle rule, and an\n * answer for what `capability.requirement-unrealized` means when the realizer\n * is itself unrealized. Refusing the line now is what keeps that decision open.\n */\nconst INTERNAL_JOINS = [\n {\n field: \"capabilities\",\n line: \"Capability:\",\n why: \"the rollup joins SERVICE requirements to capabilities, so written here it claims the document it is already in\",\n },\n {\n field: \"realizes\",\n line: \"Realizes:\",\n why: \"a capability requirement is what gets realized, not what realizes — nothing reads this line inside the tree\",\n },\n] as const satisfies ReadonlyArray<{ field: keyof Requirement; line: string; why: string }>;\n\n/**\n * Both grades a capability document earns on its own: every requirement needs\n * a stable id, and none may carry a service-level join.\n *\n * `where` is the document's repo-relative path and `subject` the capability id,\n * so a `--json` consumer can group by capability while a reader gets the file.\n * The two grades ride one walk because they ask about the same requirements and\n * a second pass is a second chance to disagree about which ones are graded.\n *\n * `where` DEFAULTS to the living spelling; `capabilities/delta/doc.ts` overrides\n * it to grade the same requirements inside a feature's own capability delta and\n * file them as archive Issues. A parameter rather than a second function because\n * these messages are what a reader acts on, and two copies drift. The return type\n * states severity and code exactly for that caller: the Finding→Issue conversion\n * needs no cast, and a fourth grade added here cannot silently stop gating.\n */\ntype DocGrade = Finding & { severity: \"error\"; code: CapabilityDocCode };\n\nexport function capabilityDocFindings(reqs: Requirement[], id: string, where = `capabilities/${id}/spec.md`): DocGrade[] {\n const findings: DocGrade[] = [];\n for (const r of reqs) {\n if (r.id === undefined) {\n findings.push({\n severity: \"error\",\n code: \"capability.requirement-unidentified\",\n subject: id,\n message:\n `${where}: requirement '${r.name}' has no \\`Requirement-ID:\\` line. ` +\n \"A capability document outlives every service that realizes it, so its requirements are identified by a stable id rather than by their heading — \" +\n \"without one, rewording the heading is a removal and an addition, and every join made to it breaks silently. \" +\n \"Add `Requirement-ID: <id>` as the first body line.\",\n });\n }\n for (const join of SERVICE_JOINS) {\n if (r[join.field].length === 0) continue;\n findings.push({\n severity: \"error\",\n code: \"capability.requirement-service-scoped\",\n subject: id,\n message:\n `${where}: requirement '${r.name}' carries \\`${join.line}\\`, which names ${join.names}. ` +\n \"A capability requirement must be observable outside the fleet — a promise a customer could check — and these lines resolve against one service's own contract, \" +\n \"so a requirement carrying one is a service requirement filed at the wrong altitude. \" +\n \"Move it into that service's spec.md, where the line resolves; what belongs here is the promise, not the mechanism.\",\n });\n }\n for (const join of INTERNAL_JOINS) {\n if (r[join.field].length === 0) continue;\n findings.push({\n severity: \"error\",\n code: \"capability.requirement-inert-join\",\n subject: id,\n message:\n `${where}: requirement '${r.name}' carries \\`${join.line}\\`, which does nothing in a capability document — ` +\n `${join.why}. It parses and reads exactly like the working join one directory over, which is why it is refused ` +\n \"rather than ignored. Delete the line; the join that matters is written on the SERVICE requirement, as \" +\n `\\`Realizes: ${id}#${r.id ?? \"<Requirement-ID>\"}\\`.`,\n });\n }\n }\n return findings;\n}\n\n/**\n * The requirement ids each capability document declares, with THE LADDER\n * already applied — the one shape `Realizes:` is ever resolved against.\n *\n * Built here rather than at the three call sites for the reason\n * `gradableCapabilityIds` above is exported: the suppression rule and the index\n * it guards belong together. A caller that composed them itself would be a\n * fourth place where \"can this be graded at all\" is decided, and the first one\n * to be forgotten when a fifth un-gradable vocabulary state arrives.\n *\n * A requirement with no `Requirement-ID:` is absent from the index, so a\n * `Realizes:` entry naming its heading does not resolve. That is deliberate and\n * not a gap: an unidentified capability requirement is already an ERROR\n * (`capability.requirement-unidentified`), and letting a heading work as an\n * address would quietly reintroduce the identity-by-heading the tree exists to\n * refuse. REMOVED is skipped for the reason every other reader skips it.\n *\n * The reads are NOT wrapped: a `capabilities/<id>/spec.md` that cannot be\n * decoded takes the run down exactly as a service spec does — the rule\n * `capabilityFleetFindings` already states one directory over.\n */\nexport async function capabilityRequirementIndex(\n vocab: CapabilityVocabulary,\n read: (path: string) => Promise<Requirement[]>,\n): Promise<CapabilityRequirementIndex> {\n const declared = gradableCapabilityIds(vocab);\n const byCapability = new Map<string, ReadonlySet<string>>();\n const digests = new Map<string, ReadonlyMap<string, string>>();\n if (declared === null) return { declared, byCapability, digests };\n for (const doc of vocab.tree.docs) {\n const reqs = await read(doc.spec);\n // ONE pass fills both, which is what keeps them from disagreeing: an id\n // reaches the set and the digest map in the same iteration or reaches\n // neither. The skip rule is stated once, above the loop body it guards.\n const kept = reqs.filter((r) => r.kind !== \"REMOVED\" && r.id !== undefined);\n byCapability.set(doc.id, new Set(kept.map((r) => r.id!)));\n digests.set(doc.id, new Map(kept.map((r) => [r.id!, requirementDigest(r)])));\n }\n return { declared, byCapability, digests };\n}\n"]}
|
|
1
|
+
{"version":3,"file":"findings.js","sourceRoot":"","sources":["../../../src/core/capabilities/findings.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * The capability grades, in every direction the axis is graded: a `Capability:`\n * entry the vocabulary does not declare, a vocabulary nobody can read, a\n * declared capability no living requirement realizes, a directory that declares\n * nothing, and the two rules an authored capability document itself must keep.\n * Values only — core never prints — and the join rule written ONCE, as the\n * Issue coherence gates on, which validate files as a Finding unchanged (an\n * Issue is assignable to a Finding), with a literal code so\n * test/codes-drift.test.ts can collect it. It used to be shaped twice, a\n * Finding twin and an Issue twin with byte-identical bodies; one function over\n * one message builder is what keeps the finding an author reads at the service\n * target and the refusal they hit at `loam archive` from drifting apart.\n *\n * The opt-in rule lives here, and it deliberately DIVERGES from `Requires:`.\n * There the LINE is the opt-in: a `Requires:` entry against a missing\n * permissions.yaml errors, because an authorization join that resolves against\n * nothing is the failure that axis exists to prevent. Here the FLEET'S OWN\n * FILES are the opt-in — `architecture/capabilities.yaml`, the `capabilities/`\n * tree, or both — so a fleet holding neither stays finding-free however many\n * `Capability:` lines its requirements already carry. (The roadmap's exit\n * criterion was written when the YAML was the only side and says 'a fleet with\n * no architecture/capabilities.yaml produces no capability findings at all';\n * the authored tree is the second way to opt in, and `CapabilityVocabulary.present`\n * is where the two are combined.) An INVALID vocabulary is also silence for the\n * per-entry grades — `capability.invalid` is the honest finding there, and\n * grading a hundred entries against a file nobody can read is the cascade that\n * suppression exists to stop.\n */\nimport { closeIds } from \"../c4/arch.js\";\nimport { compareIds } from \"../repo/entries.js\";\nimport { requirementDigest, type Requirement } from \"../document/spec.js\";\nimport type { Finding } from \"../vocabulary/report.js\";\nimport type { CapabilityDocCode, Issue } from \"../vocabulary/issue.js\";\nimport type { CapabilityVocabulary } from \"./capabilities.js\";\nimport type { CapabilityTree } from \"./tree.js\";\nimport type { CapabilityRequirementIndex } from \"./realizes/join.js\";\n\n/** Which document is being graded, and under whose name the finding is filed. */\nexport interface CapabilityTarget {\n where: string;\n subject: string;\n}\n\n/**\n * THE LADDER, and the only place it is applied: the declared capability ids when\n * the vocabulary can be graded against at all, `null` when it cannot.\n *\n * Exported because the suppression is not this module's private business — the\n * use-case axis asks the same question about the same file\n * (`commands/validate/fleet/map/usecases.ts` hands the answer to\n * `core/usecases/grade/capability-tag.ts`, and `core/usecases/delta/claims.ts`\n * to the same grader for a feature's own flows), and a hand-rolled\n * `present && invalid === undefined ? [...byId.keys()] : null` at each site is\n * three copies of one rule. The failure that costs is not hypothetical: the next\n * un-gradable state added to `CapabilityVocabulary` — \"present but partially\n * readable\" is the obvious one, since `readCapabilities` already treats a\n * non-mapping declaration body as `{}` — gets fixed wherever the greps land, and\n * any copy left behind answers with the whole key set. A half-read vocabulary\n * then reads as a complete one, and every `#cap-` tag whose capability lived in\n * the unreadable half becomes one `usecase.capability-unresolved` ERROR per\n * view: the cascade this ladder exists to prevent, arriving through the ladder.\n *\n * `null` rather than `[]`, and the two are never interchangeable: an empty list\n * is the real verdict \"this fleet declares no capabilities\" and every entry\n * fails against it, while `null` says there is nothing to grade against and\n * suspends the family. Callers that only need the boolean read `=== null`.\n */\nexport function gradableCapabilityIds(vocab: CapabilityVocabulary): readonly string[] | null {\n if (!vocab.present || vocab.invalid !== undefined) return null;\n return [...vocab.byId.keys()];\n}\n\n/** One unresolved entry: the requirement that wrote it, the entry, the close names. */\ninterface UnknownEntry {\n requirement: string;\n entry: string;\n close: string[];\n}\n\n/** Non-REMOVED requirements' entries the vocabulary does not declare — or nothing, per the opt-in rule above. */\nfunction unknownEntries(reqs: Requirement[], vocab: CapabilityVocabulary): UnknownEntry[] {\n const known = gradableCapabilityIds(vocab);\n if (known === null) return [];\n const out: UnknownEntry[] = [];\n for (const r of reqs) {\n if (r.kind === \"REMOVED\") continue;\n for (const entry of r.capabilities) {\n if (vocab.byId.has(entry)) continue;\n out.push({ requirement: r.name, entry, close: closeIds(entry, known) });\n }\n }\n return out;\n}\n\nfunction unknownMessage(where: string, u: UnknownEntry): string {\n return (\n `${where}: requirement '${u.requirement}' — Capability: '${u.entry}' is declared neither in architecture/capabilities.yaml nor as capabilities/${u.entry}/spec.md` +\n (u.close.length > 0\n ? `. Did you mean: ${u.close.join(\", \")}?`\n : \". Declare it in either — a name alone is a `capabilities: {<id>: {description, owner}}` entry, a name with prose behind it is the document — or fix the spelling\")\n );\n}\n\n/**\n * `capability.unknown` — an ERROR, the same grade as `Requires:` and for its\n * reason: an invented capability reads exactly like a real one in the\n * requirement and in every rollup built over it. Validate files it as a\n * Finding at the service target. For a feature's delta documents it is a\n * coherence Issue: a delta requirement naming an undeclared capability would\n * merge a join that resolves to nothing, so it gates `loam archive` like other\n * errors and stays `--approve`-overridable. The one new archive gate of this\n * axis.\n */\nexport function capabilityUnknownIssues(\n reqs: Requirement[],\n target: CapabilityTarget,\n vocab: CapabilityVocabulary,\n): Issue[] {\n return unknownEntries(reqs, vocab).map((u) => ({\n severity: \"error\" as const,\n code: \"capability.unknown\" as const,\n subject: target.subject,\n message: unknownMessage(target.where, u),\n }));\n}\n\n/**\n * `capability.invalid` — the whole run's ONE finding about an unreadable\n * vocabulary, spelling the expected shape. Every grade that RESOLVES AGAINST\n * the vocabulary stays suspended behind it (see the module comment); the\n * authored tree's own grades do not, because none of them consults it — a\n * directory holding no document, a requirement without a stable id and a\n * requirement carrying a service-level join are facts about files that a broken\n * YAML does not make untrue, and hiding them until an unrelated file parses is\n * suppression without the cascade that justifies it.\n */\nexport function invalidVocabularyFinding(vocab: CapabilityVocabulary): Finding | null {\n if (vocab.invalid === undefined) return null;\n return {\n severity: \"error\",\n code: \"capability.invalid\",\n message:\n `landscape: architecture/capabilities.yaml does not read as a vocabulary — ${vocab.invalid}. ` +\n \"Every `Capability:` line in the fleet resolves against this file, so the whole capability family is suspended until it parses — fix the YAML first. \" +\n \"The shape is `capabilities: {<id>: {description, owner}}`, ids flat keys with `/` allowed for nesting.\",\n };\n}\n\n/**\n * `capability.unrealized` — ONE warn per declared capability no living\n * non-REMOVED requirement names, subject = the id, sorted with compareIds.\n * Per capability rather than one rollup finding with details[] (the\n * permissions.unenforced shape) because the roadmap's own exit criterion is\n * 'one warning per capability, not one per service' — each is a distinct\n * promise nobody implemented, or a distinct word nobody adopted.\n */\nexport function unrealizedFindings(vocab: CapabilityVocabulary, used: ReadonlySet<string>): Finding[] {\n const declared = gradableCapabilityIds(vocab);\n if (declared === null) return [];\n return declared\n .filter((id) => !used.has(id))\n .sort(compareIds)\n .map((id) => ({\n severity: \"warn\" as const,\n code: \"capability.unrealized\",\n subject: id,\n message:\n `landscape: capability '${id}' is declared in ${declaredIn(vocab, id)} and nothing in the fleet realizes it — ` +\n \"no living requirement names it with `Capability:` or `Realizes:`, and no `#cap-`/`#req-` tagged flow keeps one of its promises. \" +\n \"Either a promise nobody implemented or a word nobody adopted; write the requirement that realizes it, draw the flow that keeps it, or drop the declaration\",\n }));\n}\n\n/**\n * Where an id was declared, spelled for a message. The fix differs by side —\n * a YAML line is deleted, a document is deleted with its directory — and a\n * message naming the wrong file sends its reader to edit something that is not\n * there. `both` names both because either alone would leave the id declared.\n */\nfunction declaredIn(vocab: CapabilityVocabulary, id: string): string {\n const source = vocab.byId.get(id)?.source;\n if (source === \"tree\") return `capabilities/${id}/spec.md`;\n if (source === \"both\") return `architecture/capabilities.yaml and capabilities/${id}/spec.md`;\n return \"architecture/capabilities.yaml\";\n}\n\n/* ------------------------------------------------------------------ */\n/* The authored tree — the documents themselves, graded on their own */\n/* terms rather than on the join any requirement makes to them. */\n/* ------------------------------------------------------------------ */\n\n/**\n * `capability.doc-missing` — a directory in a `capabilities/` tree that holds no\n * `spec.md` and has no capability beneath it.\n *\n * WARN, not error, and the severity is the whole judgement: the state it names\n * is what `mkdir` leaves behind halfway through creating a capability, and a\n * half-finished authoring step must not fail somebody else's `loam validate`\n * on the same tree. What makes it worth a finding at all is that the directory\n * LOOKS declared — a reader browsing `capabilities/` counts it, and a\n * `Capability:` line naming it still reports `capability.unknown`, which is the\n * confusing pair this warning exists to join up.\n *\n * BOTH TREES EARN IT, and `featureDirName` is which one. The same walk reads\n * `features/<dir>/capabilities/`, so the same `mkdir` leaves the same shape\n * there — quieter, not louder: an empty directory is not a delta, so the delta\n * algebra, the merge and `loam show` alike do not see it and the business change\n * ships as nothing at exit 0. One code, one mistake; two messages, because the\n * path and the fix differ. It is the DIRECTORY name and not the feature id\n * (`FEAT-1-split` is where the directory is), and `undocumented` is spelled from\n * the WALK'S root — literally `capabilities/…` on both sides — so a feature's\n * entry is re-rooted here or the message names a path nobody can open.\n */\nexport function docMissingFindings(tree: CapabilityTree, featureDirName?: string): Finding[] {\n return tree.undocumented.map((walked) => {\n const path = featureDirName === undefined ? walked : `features/${featureDirName}/${walked}`;\n const rest =\n featureDirName === undefined\n ? \"so this one declares nothing and a requirement naming it still reports capability.unknown. \" +\n `Write ${path}/spec.md (the narrative, then \\`## Requirements\\`), or remove the directory.`\n : \"so this one changes nothing — the archive merges the capability deltas the walk FINDS, and an empty directory is not one. \" +\n `Write ${path}/spec.md (a \\`## ADDED Requirements\\` section), or remove the directory.`;\n return {\n severity: \"warn\" as const,\n code: \"capability.doc-missing\",\n subject: path,\n message: `${path}/ holds no spec.md and no capability beneath it — a directory under capabilities/ is a capability only when it holds the document, ${rest}`,\n };\n });\n}\n\n/**\n * The service-level joins, and the whole of what \"names no service\" is checked\n * as. Each entry is a `Requirement` field that is non-empty exactly when the\n * requirement wrote the line and named something in it.\n *\n * THE PROSE RULE IS NOT CHECKED AND MUST NOT BE FAKED. \"A capability\n * requirement names no service\" is an authoring rule about wording, and the\n * only mechanical reading of it — scanning the text for a declared service id —\n * is a heuristic that convicts a payments capability for the word \"payments\".\n * loam refuses that class of check everywhere else and refuses it here; PR\n * review holds the prose. What IS exact is the structural half: these four\n * lines resolve against a SERVICE's contract and model, so a requirement\n * carrying one has already left the business altitude whatever its wording\n * says. `Requires:` is deliberately absent — a permission is a domain fact\n * (\"who may see a profile\"), observable outside the fleet, and gating a\n * capability requirement on one is legitimate.\n */\nconst SERVICE_JOINS = [\n { field: \"operations\", line: \"Operations:\", names: \"an operationId in a service's openapi.yaml\" },\n { field: \"covers\", line: \"Covers:\", names: \"a C4 element, edge or health signal\" },\n { field: \"publishes\", line: \"Publishes:\", names: \"an AsyncAPI message a service produces\" },\n { field: \"consumes\", line: \"Consumes:\", names: \"an AsyncAPI message a service consumes\" },\n] as const satisfies ReadonlyArray<{ field: keyof Requirement; line: string; names: string }>;\n\n/**\n * The two joins that point INTO the capability tree, and are therefore inert\n * written inside it.\n *\n * Not \"wrong at this altitude\" like the four above — these are the axis's own\n * lines. They are refused here because nothing reads them: `capabilityRollup`\n * joins SERVICE requirements to capabilities, so a `Capability:` line in a\n * capability document is a requirement claiming the document it is already in,\n * and a `Realizes:` line there points at a promise loam will never see anybody\n * keep. Both parse, both look exactly like the working joins one directory\n * over, and both would sit in a reviewed document meaning nothing.\n *\n * A capability realizing ANOTHER capability's requirement — a nested\n * `payments/refunds` serving a promise made by `payments` — is a real idea and\n * is deliberately not being smuggled in as a line that happens to parse. It\n * would need a design: a second corpus of joins, its own cycle rule, and an\n * answer for what `capability.requirement-unrealized` means when the realizer\n * is itself unrealized. Refusing the line now is what keeps that decision open.\n */\nconst INTERNAL_JOINS = [\n {\n field: \"capabilities\",\n line: \"Capability:\",\n why: \"the rollup joins SERVICE requirements to capabilities, so written here it claims the document it is already in\",\n },\n {\n field: \"realizes\",\n line: \"Realizes:\",\n why: \"a capability requirement is what gets realized, not what realizes — nothing reads this line inside the tree\",\n },\n] as const satisfies ReadonlyArray<{ field: keyof Requirement; line: string; why: string }>;\n\n/**\n * Both grades a capability document earns on its own: every requirement needs\n * a stable id, and none may carry a service-level join.\n *\n * `where` is the document's repo-relative path and `subject` the capability id,\n * so a `--json` consumer can group by capability while a reader gets the file.\n * The two grades ride one walk because they ask about the same requirements and\n * a second pass is a second chance to disagree about which ones are graded.\n *\n * `where` DEFAULTS to the living spelling; `capabilities/delta/doc.ts` overrides\n * it to grade the same requirements inside a feature's own capability delta and\n * file them as archive Issues. A parameter rather than a second function because\n * these messages are what a reader acts on, and two copies drift. The return type\n * states severity and code exactly for that caller: the Finding→Issue hand-off is\n * a plain assignment — no cast, no copy — and a fourth grade added here cannot\n * silently stop gating.\n */\ntype DocGrade = Finding & { severity: \"error\"; code: CapabilityDocCode };\n\nexport function capabilityDocFindings(reqs: Requirement[], id: string, where = `capabilities/${id}/spec.md`): DocGrade[] {\n const findings: DocGrade[] = [];\n for (const r of reqs) {\n if (r.id === undefined) {\n findings.push({\n severity: \"error\",\n code: \"capability.requirement-unidentified\",\n subject: id,\n message:\n `${where}: requirement '${r.name}' has no \\`Requirement-ID:\\` line. ` +\n \"A capability document outlives every service that realizes it, so its requirements are identified by a stable id rather than by their heading — \" +\n \"without one, rewording the heading is a removal and an addition, and every join made to it breaks silently. \" +\n \"Add `Requirement-ID: <id>` as the first body line.\",\n });\n }\n for (const join of SERVICE_JOINS) {\n if (r[join.field].length === 0) continue;\n findings.push({\n severity: \"error\",\n code: \"capability.requirement-service-scoped\",\n subject: id,\n message:\n `${where}: requirement '${r.name}' carries \\`${join.line}\\`, which names ${join.names}. ` +\n \"A capability requirement must be observable outside the fleet — a promise a customer could check — and these lines resolve against one service's own contract, \" +\n \"so a requirement carrying one is a service requirement filed at the wrong altitude. \" +\n \"Move it into that service's spec.md, where the line resolves; what belongs here is the promise, not the mechanism.\",\n });\n }\n for (const join of INTERNAL_JOINS) {\n if (r[join.field].length === 0) continue;\n findings.push({\n severity: \"error\",\n code: \"capability.requirement-inert-join\",\n subject: id,\n message:\n `${where}: requirement '${r.name}' carries \\`${join.line}\\`, which does nothing in a capability document — ` +\n `${join.why}. It parses and reads exactly like the working join one directory over, which is why it is refused ` +\n \"rather than ignored. Delete the line; the join that matters is written on the SERVICE requirement, as \" +\n `\\`Realizes: ${id}#${r.id ?? \"<Requirement-ID>\"}\\`.`,\n });\n }\n }\n return findings;\n}\n\n/**\n * The requirement ids each capability document declares, with THE LADDER\n * already applied — the one shape `Realizes:` is ever resolved against.\n *\n * Built here rather than at the three call sites for the reason\n * `gradableCapabilityIds` above is exported: the suppression rule and the index\n * it guards belong together. A caller that composed them itself would be a\n * fourth place where \"can this be graded at all\" is decided, and the first one\n * to be forgotten when a fifth un-gradable vocabulary state arrives.\n *\n * A requirement with no `Requirement-ID:` is absent from the index, so a\n * `Realizes:` entry naming its heading does not resolve. That is deliberate and\n * not a gap: an unidentified capability requirement is already an ERROR\n * (`capability.requirement-unidentified`), and letting a heading work as an\n * address would quietly reintroduce the identity-by-heading the tree exists to\n * refuse. REMOVED is skipped for the reason every other reader skips it.\n *\n * The reads are NOT wrapped: a `capabilities/<id>/spec.md` that cannot be\n * decoded takes the run down exactly as a service spec does — the rule\n * `capabilityFleetFindings` already states one directory over.\n */\nexport async function capabilityRequirementIndex(\n vocab: CapabilityVocabulary,\n read: (path: string) => Promise<Requirement[]>,\n): Promise<CapabilityRequirementIndex> {\n const declared = gradableCapabilityIds(vocab);\n const byCapability = new Map<string, ReadonlySet<string>>();\n const digests = new Map<string, ReadonlyMap<string, string>>();\n if (declared === null) return { declared, byCapability, digests };\n for (const doc of vocab.tree.docs) {\n const reqs = await read(doc.spec);\n // ONE pass fills both, which is what keeps them from disagreeing: an id\n // reaches the set and the digest map in the same iteration or reaches\n // neither. The skip rule is stated once, above the loop body it guards.\n const kept = reqs.filter((r) => r.kind !== \"REMOVED\" && r.id !== undefined);\n byCapability.set(doc.id, new Set(kept.map((r) => r.id!)));\n digests.set(doc.id, new Map(kept.map((r) => [r.id!, requirementDigest(r)])));\n }\n return { declared, byCapability, digests };\n}\n"]}
|
|
@@ -2,12 +2,13 @@
|
|
|
2
2
|
* The `Realizes:` grades, both directions: an entry that resolves to nothing,
|
|
3
3
|
* and a capability requirement nothing realizes.
|
|
4
4
|
*
|
|
5
|
-
* Values only — core never prints — and the join rule
|
|
6
|
-
*
|
|
7
|
-
* `../findings.ts` does for `capability.unknown`.
|
|
8
|
-
* is the only arrangement in which a finding an author
|
|
9
|
-
* target and the refusal they hit at `loam archive`
|
|
10
|
-
* breach differently
|
|
5
|
+
* Values only — core never prints — and the join rule written ONCE, as the
|
|
6
|
+
* Issue coherence gates on, which validate files as a Finding unchanged,
|
|
7
|
+
* exactly as `../findings.ts` does for `capability.unknown`. One function over
|
|
8
|
+
* ONE message builder is the only arrangement in which a finding an author
|
|
9
|
+
* reads at the service target and the refusal they hit at `loam archive`
|
|
10
|
+
* cannot describe the same breach differently — the Finding and Issue twins
|
|
11
|
+
* it replaced had byte-identical bodies.
|
|
11
12
|
*
|
|
12
13
|
* WHY THE DELTA SIDE GATES ARCHIVE. A delta requirement whose `Realizes:` entry
|
|
13
14
|
* resolves to nothing would merge a join pointing at a promise that does not
|
|
@@ -72,20 +73,12 @@ function unresolvedEntries(reqs, index) {
|
|
|
72
73
|
return out;
|
|
73
74
|
}
|
|
74
75
|
/**
|
|
75
|
-
* `capability.realizes-unknown`
|
|
76
|
-
*
|
|
77
|
-
*
|
|
78
|
-
*
|
|
76
|
+
* `capability.realizes-unknown` — an ERROR, the grade its two siblings already
|
|
77
|
+
* carry: a join that resolves to nothing reads exactly like a working one in
|
|
78
|
+
* the requirement, in the rollup and in every answer built over either. One
|
|
79
|
+
* function for validate's service target and a feature's delta documents —
|
|
80
|
+
* see the module comment.
|
|
79
81
|
*/
|
|
80
|
-
export function realizesUnknownFindings(reqs, target, index) {
|
|
81
|
-
return unresolvedEntries(reqs, index).map((u) => ({
|
|
82
|
-
severity: "error",
|
|
83
|
-
code: "capability.realizes-unknown",
|
|
84
|
-
subject: target.subject,
|
|
85
|
-
message: unresolvedMessage(target.where, u.requirement, u.claim),
|
|
86
|
-
}));
|
|
87
|
-
}
|
|
88
|
-
/** The same rule shaped as coherence Issues, for a feature's delta documents — see the module comment. */
|
|
89
82
|
export function realizesUnknownIssues(reqs, target, index) {
|
|
90
83
|
return unresolvedEntries(reqs, index).map((u) => ({
|
|
91
84
|
severity: "error",
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"findings.js","sourceRoot":"","sources":["../../../../src/core/capabilities/realizes/findings.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;GAgBG;AACH,OAAO,EAAE,UAAU,EAAE,MAAM,uBAAuB,CAAC;AAInD,OAAO,EAAE,eAAe,EAAuD,MAAM,WAAW,CAAC;AAgBjG;;;GAGG;AACH,SAAS,MAAM,CAAC,KAAsB;IACpC,QAAQ,KAAK,CAAC,IAAI,EAAE,CAAC;QACnB,KAAK,WAAW;YACd,OAAO,CACL,8FAA8F;gBAC9F,sGAAsG;gBACtG,4EAA4E,CAC7E,CAAC;QACJ,KAAK,oBAAoB;YACvB,OAAO,KAAK,CAAC,KAAK,CAAC,MAAM,GAAG,CAAC;gBAC3B,CAAC,CAAC,iDAAiD,KAAK,CAAC,KAAK,CAAC,IAAI,CAAC,IAAI,CAAC,GAAG;gBAC5E,CAAC,CAAC,iFAAiF;oBAC/E,sBAAsB,KAAK,CAAC,UAAU,+BAA+B,CAAC;QAC9E,KAAK,yBAAyB;YAC5B,OAAO,CACL,eAAe,KAAK,CAAC,UAAU,yCAAyC,KAAK,CAAC,UAAU,YAAY;gBACpG,kGAAkG;gBAClG,oDAAoD,CACrD,CAAC;QACJ,KAAK,kBAAkB;YACrB,OAAO,CACL,gBAAgB,KAAK,CAAC,UAAU,qDAAqD;gBACrF,oFAAoF,CACrF,CAAC;QACJ,KAAK,qBAAqB;YACxB,OAAO,KAAK,CAAC,KAAK,CAAC,MAAM,GAAG,CAAC;gBAC3B,CAAC,CAAC,gBAAgB,KAAK,CAAC,UAAU,gEAAgE,KAAK,CAAC,KAAK,CAAC,IAAI,CAAC,IAAI,CAAC,GAAG;gBAC3H,CAAC,CAAC,gBAAgB,KAAK,CAAC,UAAU,kDAAkD;oBAChF,oDAAoD,CAAC;IAC/D,CAAC;AACH,CAAC;AAED,SAAS,iBAAiB,CAAC,KAAa,EAAE,WAAmB,EAAE,KAAsB;IACnF,OAAO,GAAG,KAAK,kBAAkB,WAAW,kBAAkB,KAAK,CAAC,KAAK,uBAAuB,MAAM,CAAC,KAAK,CAAC,EAAE,CAAC;AAClH,CAAC;AAQD;;;;;;;;;GASG;AACH,SAAS,iBAAiB,CAAC,IAAmB,EAAE,KAAiC;IAC/E,MAAM,GAAG,GAAiB,EAAE,CAAC;IAC7B,KAAK,MAAM,CAAC,IAAI,IAAI,EAAE,CAAC;QACrB,IAAI,CAAC,CAAC,IAAI,KAAK,SAAS;YAAE,SAAS;QACnC,KAAK,MAAM,KAAK,IAAI,eAAe,CAAC,CAAC,CAAC,QAAQ,EAAE,KAAK,CAAC,EAAE,CAAC;YACvD,IAAI,KAAK,CAAC,IAAI,KAAK,UAAU;gBAAE,GAAG,CAAC,IAAI,CAAC,EAAE,WAAW,EAAE,CAAC,CAAC,IAAI,EAAE,KAAK,EAAE,CAAC,CAAC;QAC1E,CAAC;IACH,CAAC;IACD,OAAO,GAAG,CAAC;AACb,CAAC;AAED;;;;;GAKG;AACH,MAAM,UAAU,uBAAuB,CACrC,IAAmB,EACnB,MAAyB,EACzB,KAAiC;IAEjC,OAAO,iBAAiB,CAAC,IAAI,EAAE,KAAK,CAAC,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC;QAChD,QAAQ,EAAE,OAAgB;QAC1B,IAAI,EAAE,6BAA6B;QACnC,OAAO,EAAE,MAAM,CAAC,OAAO;QACvB,OAAO,EAAE,iBAAiB,CAAC,MAAM,CAAC,KAAK,EAAE,CAAC,CAAC,WAAW,EAAE,CAAC,CAAC,KAAK,CAAC;KACjE,CAAC,CAAC,CAAC;AACN,CAAC;AAED,0GAA0G;AAC1G,MAAM,UAAU,qBAAqB,CACnC,IAAmB,EACnB,MAAyB,EACzB,KAAiC;IAEjC,OAAO,iBAAiB,CAAC,IAAI,EAAE,KAAK,CAAC,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC;QAChD,QAAQ,EAAE,OAAgB;QAC1B,IAAI,EAAE,6BAAsC;QAC5C,OAAO,EAAE,MAAM,CAAC,OAAO;QACvB,OAAO,EAAE,iBAAiB,CAAC,MAAM,CAAC,KAAK,EAAE,CAAC,CAAC,WAAW,EAAE,CAAC,CAAC,KAAK,CAAC;KACjE,CAAC,CAAC,CAAC;AACN,CAAC;AAYD;;;;;;;;;;;;;;;;;;;;;;;GAuBG;AACH,MAAM,UAAU,qBAAqB,CACnC,IAAmB,EACnB,MAAyB,EACzB,KAAiC;IAEjC,MAAM,KAAK,GAAiB,EAAE,CAAC;IAC/B,KAAK,MAAM,CAAC,IAAI,IAAI,EAAE,CAAC;QACrB,IAAI,CAAC,CAAC,IAAI,KAAK,SAAS;YAAE,SAAS;QACnC,KAAK,MAAM,KAAK,IAAI,eAAe,CAAC,CAAC,CAAC,QAAQ,EAAE,KAAK,CAAC,EAAE,CAAC;YACvD,IAAI,KAAK,CAAC,IAAI,KAAK,UAAU,IAAI,KAAK,CAAC,KAAK,KAAK,IAAI;gBAAE,SAAS;YAChE,KAAK,CAAC,IAAI,CAAC;gBACT,WAAW,EAAE,CAAC,CAAC,IAAI;gBACnB,KAAK,EAAE,KAAK,CAAC,KAAK;gBAClB,UAAU,EAAE,KAAK,CAAC,UAAU;gBAC5B,MAAM,EAAE,KAAK,CAAC,WAAW;gBACzB,GAAG,EAAE,KAAK,CAAC,GAAI;gBACf,OAAO,EAAE,KAAK,CAAC,OAAQ;aACxB,CAAC,CAAC;QACL,CAAC;IACH,CAAC;IACD,OAAO,KAAK,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC;QACvB,QAAQ,EAAE,MAAe;QACzB,IAAI,EAAE,2BAA2B;QACjC,OAAO,EAAE,MAAM,CAAC,OAAO;QACvB,OAAO,EACL,GAAG,MAAM,CAAC,KAAK,kBAAkB,CAAC,CAAC,WAAW,cAAc,CAAC,CAAC,UAAU,IAAI,CAAC,CAAC,MAAM,IAAI;YACxF,gEAAgE,CAAC,CAAC,GAAG,SAAS,CAAC,CAAC,OAAO,KAAK;YAC5F,0EAA0E;YAC1E,wBAAwB,CAAC,CAAC,UAAU,+CAA+C;YACnF,gFAAgF;KACnF,CAAC,CAAC,CAAC;AACN,CAAC;AAWD;;;;;;;;;;;;;;;;;GAiBG;AACH,MAAM,UAAU,6BAA6B,CAAC,UAAmC;IAC/E,OAAO,CAAC,GAAG,UAAU,CAAC;SACnB,IAAI,CAAC,CAAC,CAAC,EAAE,CAAC,EAAE,EAAE,CAAC,UAAU,CAAC,CAAC,CAAC,UAAU,EAAE,CAAC,CAAC,UAAU,CAAC,IAAI,UAAU,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,EAAE,CAAC,CAAC;SAChF,GAAG,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC;QACX,QAAQ,EAAE,MAAe;QACzB,IAAI,EAAE,mCAAmC;QACzC,OAAO,EAAE,GAAG,CAAC,CAAC,UAAU,IAAI,CAAC,CAAC,EAAE,EAAE;QAClC,OAAO,EACL,2BAA2B,CAAC,CAAC,UAAU,eAAe,CAAC,CAAC,IAAI,MAAM,CAAC,CAAC,EAAE,0CAA0C;YAChH,yCAAyC,CAAC,CAAC,UAAU,IAAI,CAAC,CAAC,EAAE,IAAI;YACjE,sGAAsG;YACtG,kEAAkE;KACrE,CAAC,CAAC,CAAC;AACR,CAAC","sourcesContent":["/**\n * The `Realizes:` grades, both directions: an entry that resolves to nothing,\n * and a capability requirement nothing realizes.\n *\n * Values only — core never prints — and the join rule shaped twice (Finding for\n * validate, Issue for coherence) from ONE message builder, exactly as\n * `../findings.ts` does for `capability.unknown`. Two shapes over one sentence\n * is the only arrangement in which a finding an author reads at the service\n * target and the refusal they hit at `loam archive` cannot describe the same\n * breach differently.\n *\n * WHY THE DELTA SIDE GATES ARCHIVE. A delta requirement whose `Realizes:` entry\n * resolves to nothing would merge a join pointing at a promise that does not\n * exist, and the living document would then carry it looking exactly like a\n * working join — the same argument that made `capability.unknown` a gate.\n * `--approve` overrides it like any other error.\n */\nimport { compareIds } from \"../../repo/entries.js\";\nimport type { Requirement } from \"../../document/spec.js\";\nimport type { Finding } from \"../../vocabulary/report.js\";\nimport type { Issue } from \"../../vocabulary/issue.js\";\nimport { resolveRealizes, type CapabilityRequirementIndex, type RealizesClaim } from \"./join.js\";\n\n/** Which document is being graded, and under whose name the finding is filed. */\nexport interface RealizesTargetDoc {\n where: string;\n subject: string;\n}\n\n/**\n * An entry that did NOT resolve. Spelled as an exclusion rather than as a\n * second union so the arms cannot drift: adding a failure kind to\n * `RealizesClaim` makes `advice` below fail to compile until it says what to do\n * about it, which is exactly the reminder a new failure mode deserves.\n */\ntype UnresolvedClaim = Exclude<RealizesClaim, { kind: \"resolved\" }>;\n\n/**\n * The fix for one unresolved entry, as a sentence. Five arms, five different\n * repairs — the whole reason `RealizesClaim` is not collapsed to a boolean.\n */\nfunction advice(claim: UnresolvedClaim): string {\n switch (claim.kind) {\n case \"malformed\":\n return (\n \"an entry names a capability requirement and is spelled `<capability-id>#<Requirement-ID>` — \" +\n \"the capability half addresses the document, because a Requirement-ID is unique only inside its own. \" +\n \"To claim a theme rather than a promise, use the `Capability:` line instead\"\n );\n case \"unknown-capability\":\n return claim.close.length > 0\n ? `no such capability is declared. Did you mean: ${claim.close.join(\", \")}?`\n : \"no such capability is declared — declare it in architecture/capabilities.yaml, \" +\n `write capabilities/${claim.capability}/spec.md, or fix the spelling`;\n case \"undocumented-capability\":\n return (\n `capability '${claim.capability}' is declared but has no capabilities/${claim.capability}/spec.md, ` +\n \"so it carries no requirements to realize. Write the document — a name alone can be claimed with \" +\n \"`Capability:`, but only a document can be realized\"\n );\n case \"empty-capability\":\n return (\n `capabilities/${claim.capability}/spec.md exists and declares no requirements yet — ` +\n \"add the `## Requirements` section, each requirement carrying its `Requirement-ID:`\"\n );\n case \"unknown-requirement\":\n return claim.close.length > 0\n ? `capabilities/${claim.capability}/spec.md declares no requirement with that id. Did you mean: ${claim.close.join(\", \")}?`\n : `capabilities/${claim.capability}/spec.md declares no requirement with that id — ` +\n \"check the `Requirement-ID:` lines in that document\";\n }\n}\n\nfunction unresolvedMessage(where: string, requirement: string, claim: UnresolvedClaim): string {\n return `${where}: requirement '${requirement}' — Realizes: '${claim.entry}' does not resolve: ${advice(claim)}`;\n}\n\n/** One unresolved entry, with the requirement that wrote it. */\ninterface Unresolved {\n requirement: string;\n claim: UnresolvedClaim;\n}\n\n/**\n * Non-REMOVED requirements' entries that do not resolve — or nothing at all\n * when the vocabulary cannot be graded against, which `resolveRealizes` answers\n * by returning no claims.\n *\n * REMOVED is skipped for the reason its `Capability:` sibling skips it: a\n * requirement on its way out of the living document is not making a claim about\n * the future, and grading its joins would refuse the very change that removes\n * the last consumer of a retired capability requirement.\n */\nfunction unresolvedEntries(reqs: Requirement[], index: CapabilityRequirementIndex): Unresolved[] {\n const out: Unresolved[] = [];\n for (const r of reqs) {\n if (r.kind === \"REMOVED\") continue;\n for (const claim of resolveRealizes(r.realizes, index)) {\n if (claim.kind !== \"resolved\") out.push({ requirement: r.name, claim });\n }\n }\n return out;\n}\n\n/**\n * `capability.realizes-unknown` as validate sees it — an ERROR, the grade its\n * two siblings already carry: a join that resolves to nothing reads exactly\n * like a working one in the requirement, in the rollup and in every answer\n * built over either.\n */\nexport function realizesUnknownFindings(\n reqs: Requirement[],\n target: RealizesTargetDoc,\n index: CapabilityRequirementIndex,\n): Finding[] {\n return unresolvedEntries(reqs, index).map((u) => ({\n severity: \"error\" as const,\n code: \"capability.realizes-unknown\",\n subject: target.subject,\n message: unresolvedMessage(target.where, u.requirement, u.claim),\n }));\n}\n\n/** The same rule shaped as coherence Issues, for a feature's delta documents — see the module comment. */\nexport function realizesUnknownIssues(\n reqs: Requirement[],\n target: RealizesTargetDoc,\n index: CapabilityRequirementIndex,\n): Issue[] {\n return unresolvedEntries(reqs, index).map((u) => ({\n severity: \"error\" as const,\n code: \"capability.realizes-unknown\" as const,\n subject: target.subject,\n message: unresolvedMessage(target.where, u.requirement, u.claim),\n }));\n}\n\n/** One resolved entry whose pin no longer matches the promise it points at. */\ninterface StaleEntry {\n requirement: string;\n entry: string;\n capability: string;\n target: string;\n pin: string;\n current: string;\n}\n\n/**\n * `capability.realizes-stale` — ONE warn per pinned entry whose target has been\n * rewritten since the pin was stamped.\n *\n * THE ONLY CHECK IN THE PRODUCT THAT NOTICES A LIVING DOCUMENT DECAYING. Every\n * other join here is an EXISTENCE constraint: `capability.realizes-unknown`\n * fires when the target is gone, and stays silent when the target is merely\n * different. That silence is the failure this axis was missing — an analyst\n * narrows a promise, and every service requirement claiming to keep it goes on\n * claiming so, in a corpus loam rewrote itself, with nothing anywhere to say\n * the claim was made about different words.\n *\n * WARN, AND IT MUST STAY A WARN. A pin going stale is not a defect: it is the\n * fleet being told that a promise moved and somebody should re-read the\n * requirements standing on it. The re-read is a human act with three legitimate\n * outcomes — change the requirement, change the promise, or decide neither\n * needed to change — and only the third is `loam rebase --living` on its own.\n * An error would make the middle outcome unavailable without `--approve`, and a\n * gate would make an analyst's ordinary edit break other teams' builds.\n *\n * Unpinned entries produce nothing, forever. A corpus that has never been\n * pinned grades exactly as it did before pins existed, which is what lets this\n * land without a migration.\n */\nexport function realizesStaleFindings(\n reqs: Requirement[],\n target: RealizesTargetDoc,\n index: CapabilityRequirementIndex,\n): Finding[] {\n const stale: StaleEntry[] = [];\n for (const r of reqs) {\n if (r.kind === \"REMOVED\") continue;\n for (const claim of resolveRealizes(r.realizes, index)) {\n if (claim.kind !== \"resolved\" || claim.stale !== true) continue;\n stale.push({\n requirement: r.name,\n entry: claim.entry,\n capability: claim.capability,\n target: claim.requirement,\n pin: claim.pin!,\n current: claim.current!,\n });\n }\n }\n return stale.map((s) => ({\n severity: \"warn\" as const,\n code: \"capability.realizes-stale\",\n subject: target.subject,\n message:\n `${target.where}: requirement '${s.requirement}' realizes ${s.capability}#${s.target}, ` +\n `which has been rewritten since this claim was pinned (pinned ${s.pin}, now ${s.current}). ` +\n \"The join still resolves — what changed is the promise, not its address. \" +\n `Re-read capabilities/${s.capability}/spec.md and decide which side is now wrong; ` +\n \"when this requirement is right as it stands, `loam rebase --living` re-pins it\",\n }));\n}\n\n/** One capability requirement nothing realizes, located exactly. */\nexport interface UnrealizedRequirement {\n capability: string;\n /** Its `Requirement-ID:`. */\n id: string;\n /** Its `### Requirement:` heading, for a reader who has the document open. */\n name: string;\n}\n\n/**\n * `capability.requirement-unrealized` — ONE warn per capability requirement no\n * living service requirement's `Realizes:` line names.\n *\n * The sharper half of the axis, and the reason the join was worth building.\n * `capability.unrealized` already says a whole capability is claimed by nobody,\n * which is the loud case somebody notices; this says the fleet claims the\n * capability and has left one named promise inside it unimplemented — the case\n * that survives every rollup, because the capability's OTHER requirements make\n * its row look healthy.\n *\n * Warn rather than error, and per requirement rather than one rollup finding\n * with details[]: an analyst writing next quarter's promises before anybody\n * implements them is the normal, intended use of the document, and a corpus\n * that fails the gate the moment it is written ahead of the fleet is a corpus\n * nobody writes ahead. Subject is `<capability>#<id>` — the entry an author\n * would type to fix it, which is the string they should be able to copy.\n */\nexport function requirementUnrealizedFindings(unrealized: UnrealizedRequirement[]): Finding[] {\n return [...unrealized]\n .sort((a, b) => compareIds(a.capability, b.capability) || compareIds(a.id, b.id))\n .map((u) => ({\n severity: \"warn\" as const,\n code: \"capability.requirement-unrealized\",\n subject: `${u.capability}#${u.id}`,\n message:\n `landscape: capabilities/${u.capability}/spec.md — '${u.name}' (${u.id}) is realized by no living requirement: ` +\n `no service's \\`Realizes:\\` line names ${u.capability}#${u.id}. ` +\n \"Either the promise is not implemented yet, or the requirements that implement it have not said so — \" +\n \"add the line to each service requirement that carries part of it\",\n }));\n}\n"]}
|
|
1
|
+
{"version":3,"file":"findings.js","sourceRoot":"","sources":["../../../../src/core/capabilities/realizes/findings.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * The `Realizes:` grades, both directions: an entry that resolves to nothing,\n * and a capability requirement nothing realizes.\n *\n * Values only — core never prints — and the join rule written ONCE, as the\n * Issue coherence gates on, which validate files as a Finding unchanged,\n * exactly as `../findings.ts` does for `capability.unknown`. One function over\n * ONE message builder is the only arrangement in which a finding an author\n * reads at the service target and the refusal they hit at `loam archive`\n * cannot describe the same breach differently — the Finding and Issue twins\n * it replaced had byte-identical bodies.\n *\n * WHY THE DELTA SIDE GATES ARCHIVE. A delta requirement whose `Realizes:` entry\n * resolves to nothing would merge a join pointing at a promise that does not\n * exist, and the living document would then carry it looking exactly like a\n * working join — the same argument that made `capability.unknown` a gate.\n * `--approve` overrides it like any other error.\n */\nimport { compareIds } from \"../../repo/entries.js\";\nimport type { Requirement } from \"../../document/spec.js\";\nimport type { Finding } from \"../../vocabulary/report.js\";\nimport type { Issue } from \"../../vocabulary/issue.js\";\nimport { resolveRealizes, type CapabilityRequirementIndex, type RealizesClaim } from \"./join.js\";\n\n/** Which document is being graded, and under whose name the finding is filed. */\nexport interface RealizesTargetDoc {\n where: string;\n subject: string;\n}\n\n/**\n * An entry that did NOT resolve. Spelled as an exclusion rather than as a\n * second union so the arms cannot drift: adding a failure kind to\n * `RealizesClaim` makes `advice` below fail to compile until it says what to do\n * about it, which is exactly the reminder a new failure mode deserves.\n */\ntype UnresolvedClaim = Exclude<RealizesClaim, { kind: \"resolved\" }>;\n\n/**\n * The fix for one unresolved entry, as a sentence. Five arms, five different\n * repairs — the whole reason `RealizesClaim` is not collapsed to a boolean.\n */\nfunction advice(claim: UnresolvedClaim): string {\n switch (claim.kind) {\n case \"malformed\":\n return (\n \"an entry names a capability requirement and is spelled `<capability-id>#<Requirement-ID>` — \" +\n \"the capability half addresses the document, because a Requirement-ID is unique only inside its own. \" +\n \"To claim a theme rather than a promise, use the `Capability:` line instead\"\n );\n case \"unknown-capability\":\n return claim.close.length > 0\n ? `no such capability is declared. Did you mean: ${claim.close.join(\", \")}?`\n : \"no such capability is declared — declare it in architecture/capabilities.yaml, \" +\n `write capabilities/${claim.capability}/spec.md, or fix the spelling`;\n case \"undocumented-capability\":\n return (\n `capability '${claim.capability}' is declared but has no capabilities/${claim.capability}/spec.md, ` +\n \"so it carries no requirements to realize. Write the document — a name alone can be claimed with \" +\n \"`Capability:`, but only a document can be realized\"\n );\n case \"empty-capability\":\n return (\n `capabilities/${claim.capability}/spec.md exists and declares no requirements yet — ` +\n \"add the `## Requirements` section, each requirement carrying its `Requirement-ID:`\"\n );\n case \"unknown-requirement\":\n return claim.close.length > 0\n ? `capabilities/${claim.capability}/spec.md declares no requirement with that id. Did you mean: ${claim.close.join(\", \")}?`\n : `capabilities/${claim.capability}/spec.md declares no requirement with that id — ` +\n \"check the `Requirement-ID:` lines in that document\";\n }\n}\n\nfunction unresolvedMessage(where: string, requirement: string, claim: UnresolvedClaim): string {\n return `${where}: requirement '${requirement}' — Realizes: '${claim.entry}' does not resolve: ${advice(claim)}`;\n}\n\n/** One unresolved entry, with the requirement that wrote it. */\ninterface Unresolved {\n requirement: string;\n claim: UnresolvedClaim;\n}\n\n/**\n * Non-REMOVED requirements' entries that do not resolve — or nothing at all\n * when the vocabulary cannot be graded against, which `resolveRealizes` answers\n * by returning no claims.\n *\n * REMOVED is skipped for the reason its `Capability:` sibling skips it: a\n * requirement on its way out of the living document is not making a claim about\n * the future, and grading its joins would refuse the very change that removes\n * the last consumer of a retired capability requirement.\n */\nfunction unresolvedEntries(reqs: Requirement[], index: CapabilityRequirementIndex): Unresolved[] {\n const out: Unresolved[] = [];\n for (const r of reqs) {\n if (r.kind === \"REMOVED\") continue;\n for (const claim of resolveRealizes(r.realizes, index)) {\n if (claim.kind !== \"resolved\") out.push({ requirement: r.name, claim });\n }\n }\n return out;\n}\n\n/**\n * `capability.realizes-unknown` — an ERROR, the grade its two siblings already\n * carry: a join that resolves to nothing reads exactly like a working one in\n * the requirement, in the rollup and in every answer built over either. One\n * function for validate's service target and a feature's delta documents —\n * see the module comment.\n */\nexport function realizesUnknownIssues(\n reqs: Requirement[],\n target: RealizesTargetDoc,\n index: CapabilityRequirementIndex,\n): Issue[] {\n return unresolvedEntries(reqs, index).map((u) => ({\n severity: \"error\" as const,\n code: \"capability.realizes-unknown\" as const,\n subject: target.subject,\n message: unresolvedMessage(target.where, u.requirement, u.claim),\n }));\n}\n\n/** One resolved entry whose pin no longer matches the promise it points at. */\ninterface StaleEntry {\n requirement: string;\n entry: string;\n capability: string;\n target: string;\n pin: string;\n current: string;\n}\n\n/**\n * `capability.realizes-stale` — ONE warn per pinned entry whose target has been\n * rewritten since the pin was stamped.\n *\n * THE ONLY CHECK IN THE PRODUCT THAT NOTICES A LIVING DOCUMENT DECAYING. Every\n * other join here is an EXISTENCE constraint: `capability.realizes-unknown`\n * fires when the target is gone, and stays silent when the target is merely\n * different. That silence is the failure this axis was missing — an analyst\n * narrows a promise, and every service requirement claiming to keep it goes on\n * claiming so, in a corpus loam rewrote itself, with nothing anywhere to say\n * the claim was made about different words.\n *\n * WARN, AND IT MUST STAY A WARN. A pin going stale is not a defect: it is the\n * fleet being told that a promise moved and somebody should re-read the\n * requirements standing on it. The re-read is a human act with three legitimate\n * outcomes — change the requirement, change the promise, or decide neither\n * needed to change — and only the third is `loam rebase --living` on its own.\n * An error would make the middle outcome unavailable without `--approve`, and a\n * gate would make an analyst's ordinary edit break other teams' builds.\n *\n * Unpinned entries produce nothing, forever. A corpus that has never been\n * pinned grades exactly as it did before pins existed, which is what lets this\n * land without a migration.\n */\nexport function realizesStaleFindings(\n reqs: Requirement[],\n target: RealizesTargetDoc,\n index: CapabilityRequirementIndex,\n): Finding[] {\n const stale: StaleEntry[] = [];\n for (const r of reqs) {\n if (r.kind === \"REMOVED\") continue;\n for (const claim of resolveRealizes(r.realizes, index)) {\n if (claim.kind !== \"resolved\" || claim.stale !== true) continue;\n stale.push({\n requirement: r.name,\n entry: claim.entry,\n capability: claim.capability,\n target: claim.requirement,\n pin: claim.pin!,\n current: claim.current!,\n });\n }\n }\n return stale.map((s) => ({\n severity: \"warn\" as const,\n code: \"capability.realizes-stale\",\n subject: target.subject,\n message:\n `${target.where}: requirement '${s.requirement}' realizes ${s.capability}#${s.target}, ` +\n `which has been rewritten since this claim was pinned (pinned ${s.pin}, now ${s.current}). ` +\n \"The join still resolves — what changed is the promise, not its address. \" +\n `Re-read capabilities/${s.capability}/spec.md and decide which side is now wrong; ` +\n \"when this requirement is right as it stands, `loam rebase --living` re-pins it\",\n }));\n}\n\n/** One capability requirement nothing realizes, located exactly. */\nexport interface UnrealizedRequirement {\n capability: string;\n /** Its `Requirement-ID:`. */\n id: string;\n /** Its `### Requirement:` heading, for a reader who has the document open. */\n name: string;\n}\n\n/**\n * `capability.requirement-unrealized` — ONE warn per capability requirement no\n * living service requirement's `Realizes:` line names.\n *\n * The sharper half of the axis, and the reason the join was worth building.\n * `capability.unrealized` already says a whole capability is claimed by nobody,\n * which is the loud case somebody notices; this says the fleet claims the\n * capability and has left one named promise inside it unimplemented — the case\n * that survives every rollup, because the capability's OTHER requirements make\n * its row look healthy.\n *\n * Warn rather than error, and per requirement rather than one rollup finding\n * with details[]: an analyst writing next quarter's promises before anybody\n * implements them is the normal, intended use of the document, and a corpus\n * that fails the gate the moment it is written ahead of the fleet is a corpus\n * nobody writes ahead. Subject is `<capability>#<id>` — the entry an author\n * would type to fix it, which is the string they should be able to copy.\n */\nexport function requirementUnrealizedFindings(unrealized: UnrealizedRequirement[]): Finding[] {\n return [...unrealized]\n .sort((a, b) => compareIds(a.capability, b.capability) || compareIds(a.id, b.id))\n .map((u) => ({\n severity: \"warn\" as const,\n code: \"capability.requirement-unrealized\",\n subject: `${u.capability}#${u.id}`,\n message:\n `landscape: capabilities/${u.capability}/spec.md — '${u.name}' (${u.id}) is realized by no living requirement: ` +\n `no service's \\`Realizes:\\` line names ${u.capability}#${u.id}. ` +\n \"Either the promise is not implemented yet, or the requirements that implement it have not said so — \" +\n \"add the line to each service requirement that carries part of it\",\n }));\n}\n"]}
|
|
@@ -120,24 +120,4 @@ export function resolveRealizes(entries, index) {
|
|
|
120
120
|
};
|
|
121
121
|
});
|
|
122
122
|
}
|
|
123
|
-
/**
|
|
124
|
-
* The capabilities an entry list CLAIMS, whether or not each entry resolves.
|
|
125
|
-
*
|
|
126
|
-
* Used to widen the set `capability.unrealized` is taken against, and the
|
|
127
|
-
* widening is a correctness fix rather than a convenience: realizing a
|
|
128
|
-
* capability's requirement IS realizing part of that capability, so an author
|
|
129
|
-
* who wrote only `Realizes:` lines would otherwise be warned that a capability
|
|
130
|
-
* demonstrably realized by their fleet is realized by nobody.
|
|
131
|
-
*
|
|
132
|
-
* Claims from UNRESOLVED entries count too, and that is deliberate. The entry
|
|
133
|
-
* already earns `capability.realizes-unknown`; letting it also produce a
|
|
134
|
-
* spurious `capability.unrealized` would be two findings for one mistake, and
|
|
135
|
-
* the second would point at the analyst's document rather than at the typo.
|
|
136
|
-
*/
|
|
137
|
-
export function capabilitiesClaimedByRealizes(entries) {
|
|
138
|
-
return entries.flatMap((entry) => {
|
|
139
|
-
const target = splitRealizesEntry(entry);
|
|
140
|
-
return target === null ? [] : [target.capability];
|
|
141
|
-
});
|
|
142
|
-
}
|
|
143
123
|
//# sourceMappingURL=join.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"join.js","sourceRoot":"","sources":["../../../../src/core/capabilities/realizes/join.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * The join from a service requirement INTO the authored business tree:\n * `Realizes: <capability-id>#<Requirement-ID>`.\n *\n * This is the axis's load-bearing edge. `Capability:` beside it answers a\n * different question and neither replaces the other — `Capability: checkout`\n * claims a theme, `Realizes: checkout#CHECKOUT-CHARGE-ONCE` claims that this\n * requirement is part of what makes one named promise true. Only the second can\n * be graded in both directions, and both directions are the point: an entry\n * that resolves to nothing, and a capability requirement nothing realizes.\n *\n * WHO WRITES IT. Not the analyst. The analyst writes the capability and its\n * requirements and names no service; whoever implements a service requirement\n * writes the line that says which promise it serves. That is why the line lives\n * on the SERVICE requirement and not as a back-reference in the capability\n * document: a business document must not have to be edited every time a fleet\n * rearranges which service carries which part.\n *\n * WHY NO SEPARATE LINE FOR THE API HOP. A capability requirement reaches its\n * operations by composing this join with the `Operations:` lines that already\n * exist on the requirements realizing it. A third place to keep in sync would\n * buy nothing and could disagree with the two.\n *\n * DELIBERATELY THE NARROWEST LAYER THAT CAN MEAN IT, exactly as\n * `../usecase-join.ts` is: no file is read, no finding is emitted, nothing is\n * thrown, and — the load-bearing part — this package does NOT import\n * `../capabilities.js`. It takes ids somebody else supplies. That keeps the\n * package edge one-way (`capabilities` → `capabilities/realizes`), and it keeps\n * the resolution testable without a filesystem.\n *\n * WHAT THE CALLER STILL OWES. The vocabulary ladder in `../findings.ts` — an\n * absent or unreadable `architecture/capabilities.yaml` means silence, not a\n * fleet full of unresolved joins — is applied BEFORE calling here, and travels\n * as `declared: null`.\n */\nimport { closeIds } from \"../../c4/arch.js\";\nimport { splitRealizesPin } from \"../../document/spec.js\";\nimport { compareIds } from \"../../repo/entries.js\";\n\n/**\n * The separator between the two halves of an entry.\n *\n * A composite entry rather than two lines because a `Requirement-ID` is unique\n * only inside its own document: the capability half is not decoration, it is\n * what makes the target addressable. Two lines would let one requirement name\n * three capabilities and four ids with nothing saying which belongs to which.\n */\nexport const REALIZES_SEPARATOR = \"#\";\n\n/** The two halves of one entry, plus the version it was written against. */\nexport interface RealizesTarget {\n capability: string;\n requirement: string;\n /**\n * The digest of the capability requirement this claim was last pinned to, or\n * `null` for an unpinned entry. Written by `loam rebase --living`, never by\n * hand; `null` is the normal state of a corpus that has not been pinned and\n * grades exactly as it did before pins existed.\n */\n pin: string | null;\n}\n\n/**\n * Split one entry, or `null` when it has no two halves.\n *\n * THE LAST SEPARATOR, NOT THE FIRST, and the asymmetry is the reason: the\n * requirement half has a strict grammar (`REQUIREMENT_ID_RE` in\n * `core/document/spec.ts`) that excludes `#`, while the capability half is a\n * YAML key and a directory name and is constrained nowhere. Splitting at the\n * last `#` is therefore unambiguous for every capability id there is;\n * splitting at the first would mis-parse any id containing one, and would do it\n * silently — the failure mode this whole axis exists to refuse.\n *\n * Both halves must be non-empty. `checkout#`, `#CHK-1` and a bare `checkout`\n * are all one answer — `null` — because the fix for all three is the same\n * sentence, and inventing three refusals for one mistake helps nobody.\n */\nexport function splitRealizesEntry(entry: string): RealizesTarget | null {\n // The pin comes off FIRST. It is a suffix of the whole entry, and leaving it\n // on would put `@<digest>` inside the requirement half, where it would fail\n // to resolve against an id that never contained one.\n const { target, pin } = splitRealizesPin(entry);\n const at = target.lastIndexOf(REALIZES_SEPARATOR);\n if (at <= 0 || at === target.length - 1) return null;\n return { capability: target.slice(0, at), requirement: target.slice(at + 1), pin };\n}\n\n/**\n * What the tree declares, as the resolution needs it.\n *\n * The two fields answer different questions and both are needed to tell the\n * failures apart: `declared` is every capability id the fleet knows — the union\n * of the YAML and the tree — while `byCapability` holds only those with a\n * document, since only a document can carry requirements. A capability in\n * `declared` and absent from `byCapability` is declared-but-undocumented, which\n * is a real and common mid-adoption state with a fix of its own.\n */\nexport interface CapabilityRequirementIndex {\n /**\n * Every declared capability id, or `null` when the vocabulary cannot be\n * graded against at all. `null` and `[]` are never interchangeable — the same\n * distinction `gradableCapabilityIds` draws, for the same reason.\n */\n declared: readonly string[] | null;\n /** Requirement ids per capability that HAS a document. Present-and-empty is a real answer. */\n byCapability: ReadonlyMap<string, ReadonlySet<string>>;\n /**\n * The current `requirementDigest` of each id in `byCapability`, for the pin\n * comparison and nothing else.\n *\n * A second collection over the same keys, which is a shape this codebase\n * otherwise refuses — the justification is that the alternative is worse.\n * Widening `byCapability`'s values to a Map would change what `[...ids]`\n * iterates at four call sites that legitimately want the ids alone, and a\n * silent change of meaning in an existing iteration is a far more expensive\n * mistake than a redundant map built in the same loop. The invariant — same\n * capabilities, same ids, both sides — is asserted by\n * `test/capability-realizes-pin.test.ts` rather than left to a comment.\n */\n digests: ReadonlyMap<string, ReadonlyMap<string, string>>;\n}\n\n/**\n * What one `Realizes:` entry joins to. Branch on `kind`; every arm carries the\n * entry it is about, so a message can name it without re-deriving it.\n *\n * Five arms because there are five different fixes. Collapsing them would be\n * cheaper to write and worse to read: \"does not resolve\" tells an author\n * nothing about whether to declare a capability, write a document, add a\n * requirements section, or correct four characters.\n */\nexport type RealizesClaim =\n /**\n * Both halves resolve. `stale` is the pin verdict, and it is a THIRD state\n * rather than a boolean: `null` means the entry carries no pin, which is not\n * the same answer as a pin that still matches. Only `true` is a finding.\n */\n | {\n entry: string;\n kind: \"resolved\";\n capability: string;\n requirement: string;\n pin: string | null;\n /** The target's digest now, or `null` when the index could not supply one. */\n current: string | null;\n stale: boolean | null;\n }\n /** No usable separator — the fix is the spelling of the entry itself. */\n | { entry: string; kind: \"malformed\" }\n /** Nothing declares the capability half. `close` holds real declared ids. */\n | { entry: string; kind: \"unknown-capability\"; capability: string; close: string[] }\n /** Declared, but with no `capabilities/<id>/spec.md` — so it has no requirements at all. */\n | { entry: string; kind: \"undocumented-capability\"; capability: string }\n /** The document exists and declares no requirements yet. */\n | { entry: string; kind: \"empty-capability\"; capability: string }\n /** The document exists and declares requirements, but not this one. */\n | { entry: string; kind: \"unknown-requirement\"; capability: string; requirement: string; close: string[] };\n\n/**\n * Resolve every entry, in the order the author wrote them.\n *\n * Order preserved and repeats NOT collapsed, matching the `Capability:` line's\n * behaviour in `../findings.ts` rather than the `#cap-` tag's in\n * `../usecase-join.ts`. The two siblings differ because a tag set is a set —\n * LikeC4 hands it over with no authored order to preserve — while a body line\n * is text somebody typed, and a reader chasing a finding is looking at that\n * text. Consistency with the line beside it is worth more here than a\n * deduplication nobody asked for.\n *\n * An empty result when `declared` is `null` is the ladder, not an answer: there\n * is nothing to grade against, so nothing is graded.\n */\nexport function resolveRealizes(\n entries: readonly string[],\n index: CapabilityRequirementIndex,\n): RealizesClaim[] {\n const { declared, byCapability, digests } = index;\n if (declared === null) return [];\n const known = new Set(declared);\n\n return entries.map((entry): RealizesClaim => {\n const target = splitRealizesEntry(entry);\n if (target === null) return { entry, kind: \"malformed\" };\n const { capability, requirement, pin } = target;\n if (!known.has(capability)) {\n return { entry, kind: \"unknown-capability\", capability, close: closeIds(capability, declared) };\n }\n const ids = byCapability.get(capability);\n if (ids === undefined) return { entry, kind: \"undocumented-capability\", capability };\n if (ids.size === 0) return { entry, kind: \"empty-capability\", capability };\n if (ids.has(requirement)) {\n // An index with no digest for a resolved id leaves the verdict UNKNOWN\n // rather than STALE. The two are not the same claim, and a reader who is\n // told a pin went stale because the grader could not compute the other\n // side would be told something false.\n const current = digests.get(capability)?.get(requirement) ?? null;\n const stale = pin === null || current === null ? null : pin !== current;\n return { entry, kind: \"resolved\", capability, requirement, pin, current, stale };\n }\n return {\n entry,\n kind: \"unknown-requirement\",\n capability,\n requirement,\n close: closeIds(requirement, [...ids].sort(compareIds)),\n };\n });\n}\n\n/**\n * The capabilities an entry list CLAIMS, whether or not each entry resolves.\n *\n * Used to widen the set `capability.unrealized` is taken against, and the\n * widening is a correctness fix rather than a convenience: realizing a\n * capability's requirement IS realizing part of that capability, so an author\n * who wrote only `Realizes:` lines would otherwise be warned that a capability\n * demonstrably realized by their fleet is realized by nobody.\n *\n * Claims from UNRESOLVED entries count too, and that is deliberate. The entry\n * already earns `capability.realizes-unknown`; letting it also produce a\n * spurious `capability.unrealized` would be two findings for one mistake, and\n * the second would point at the analyst's document rather than at the typo.\n */\nexport function capabilitiesClaimedByRealizes(entries: readonly string[]): string[] {\n return entries.flatMap((entry) => {\n const target = splitRealizesEntry(entry);\n return target === null ? [] : [target.capability];\n });\n}\n"]}
|
|
1
|
+
{"version":3,"file":"join.js","sourceRoot":"","sources":["../../../../src/core/capabilities/realizes/join.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * The join from a service requirement INTO the authored business tree:\n * `Realizes: <capability-id>#<Requirement-ID>`.\n *\n * This is the axis's load-bearing edge. `Capability:` beside it answers a\n * different question and neither replaces the other — `Capability: checkout`\n * claims a theme, `Realizes: checkout#CHECKOUT-CHARGE-ONCE` claims that this\n * requirement is part of what makes one named promise true. Only the second can\n * be graded in both directions, and both directions are the point: an entry\n * that resolves to nothing, and a capability requirement nothing realizes.\n *\n * WHO WRITES IT. Not the analyst. The analyst writes the capability and its\n * requirements and names no service; whoever implements a service requirement\n * writes the line that says which promise it serves. That is why the line lives\n * on the SERVICE requirement and not as a back-reference in the capability\n * document: a business document must not have to be edited every time a fleet\n * rearranges which service carries which part.\n *\n * WHY NO SEPARATE LINE FOR THE API HOP. A capability requirement reaches its\n * operations by composing this join with the `Operations:` lines that already\n * exist on the requirements realizing it. A third place to keep in sync would\n * buy nothing and could disagree with the two.\n *\n * DELIBERATELY THE NARROWEST LAYER THAT CAN MEAN IT, exactly as\n * `../usecase-join.ts` is: no file is read, no finding is emitted, nothing is\n * thrown, and — the load-bearing part — this package does NOT import\n * `../capabilities.js`. It takes ids somebody else supplies. That keeps the\n * package edge one-way (`capabilities` → `capabilities/realizes`), and it keeps\n * the resolution testable without a filesystem.\n *\n * WHAT THE CALLER STILL OWES. The vocabulary ladder in `../findings.ts` — an\n * absent or unreadable `architecture/capabilities.yaml` means silence, not a\n * fleet full of unresolved joins — is applied BEFORE calling here, and travels\n * as `declared: null`.\n */\nimport { closeIds } from \"../../c4/arch.js\";\nimport { splitRealizesPin } from \"../../document/spec.js\";\nimport { compareIds } from \"../../repo/entries.js\";\n\n/**\n * The separator between the two halves of an entry.\n *\n * A composite entry rather than two lines because a `Requirement-ID` is unique\n * only inside its own document: the capability half is not decoration, it is\n * what makes the target addressable. Two lines would let one requirement name\n * three capabilities and four ids with nothing saying which belongs to which.\n */\nexport const REALIZES_SEPARATOR = \"#\";\n\n/** The two halves of one entry, plus the version it was written against. */\nexport interface RealizesTarget {\n capability: string;\n requirement: string;\n /**\n * The digest of the capability requirement this claim was last pinned to, or\n * `null` for an unpinned entry. Written by `loam rebase --living`, never by\n * hand; `null` is the normal state of a corpus that has not been pinned and\n * grades exactly as it did before pins existed.\n */\n pin: string | null;\n}\n\n/**\n * Split one entry, or `null` when it has no two halves.\n *\n * THE LAST SEPARATOR, NOT THE FIRST, and the asymmetry is the reason: the\n * requirement half has a strict grammar (`REQUIREMENT_ID_RE` in\n * `core/document/spec.ts`) that excludes `#`, while the capability half is a\n * YAML key and a directory name and is constrained nowhere. Splitting at the\n * last `#` is therefore unambiguous for every capability id there is;\n * splitting at the first would mis-parse any id containing one, and would do it\n * silently — the failure mode this whole axis exists to refuse.\n *\n * Both halves must be non-empty. `checkout#`, `#CHK-1` and a bare `checkout`\n * are all one answer — `null` — because the fix for all three is the same\n * sentence, and inventing three refusals for one mistake helps nobody.\n */\nexport function splitRealizesEntry(entry: string): RealizesTarget | null {\n // The pin comes off FIRST. It is a suffix of the whole entry, and leaving it\n // on would put `@<digest>` inside the requirement half, where it would fail\n // to resolve against an id that never contained one.\n const { target, pin } = splitRealizesPin(entry);\n const at = target.lastIndexOf(REALIZES_SEPARATOR);\n if (at <= 0 || at === target.length - 1) return null;\n return { capability: target.slice(0, at), requirement: target.slice(at + 1), pin };\n}\n\n/**\n * What the tree declares, as the resolution needs it.\n *\n * The two fields answer different questions and both are needed to tell the\n * failures apart: `declared` is every capability id the fleet knows — the union\n * of the YAML and the tree — while `byCapability` holds only those with a\n * document, since only a document can carry requirements. A capability in\n * `declared` and absent from `byCapability` is declared-but-undocumented, which\n * is a real and common mid-adoption state with a fix of its own.\n */\nexport interface CapabilityRequirementIndex {\n /**\n * Every declared capability id, or `null` when the vocabulary cannot be\n * graded against at all. `null` and `[]` are never interchangeable — the same\n * distinction `gradableCapabilityIds` draws, for the same reason.\n */\n declared: readonly string[] | null;\n /** Requirement ids per capability that HAS a document. Present-and-empty is a real answer. */\n byCapability: ReadonlyMap<string, ReadonlySet<string>>;\n /**\n * The current `requirementDigest` of each id in `byCapability`, for the pin\n * comparison and nothing else.\n *\n * A second collection over the same keys, which is a shape this codebase\n * otherwise refuses — the justification is that the alternative is worse.\n * Widening `byCapability`'s values to a Map would change what `[...ids]`\n * iterates at four call sites that legitimately want the ids alone, and a\n * silent change of meaning in an existing iteration is a far more expensive\n * mistake than a redundant map built in the same loop. The invariant — same\n * capabilities, same ids, both sides — is asserted by\n * `test/capability-realizes-pin.test.ts` rather than left to a comment.\n */\n digests: ReadonlyMap<string, ReadonlyMap<string, string>>;\n}\n\n/**\n * What one `Realizes:` entry joins to. Branch on `kind`; every arm carries the\n * entry it is about, so a message can name it without re-deriving it.\n *\n * Five arms because there are five different fixes. Collapsing them would be\n * cheaper to write and worse to read: \"does not resolve\" tells an author\n * nothing about whether to declare a capability, write a document, add a\n * requirements section, or correct four characters.\n */\nexport type RealizesClaim =\n /**\n * Both halves resolve. `stale` is the pin verdict, and it is a THIRD state\n * rather than a boolean: `null` means the entry carries no pin, which is not\n * the same answer as a pin that still matches. Only `true` is a finding.\n */\n | {\n entry: string;\n kind: \"resolved\";\n capability: string;\n requirement: string;\n pin: string | null;\n /** The target's digest now, or `null` when the index could not supply one. */\n current: string | null;\n stale: boolean | null;\n }\n /** No usable separator — the fix is the spelling of the entry itself. */\n | { entry: string; kind: \"malformed\" }\n /** Nothing declares the capability half. `close` holds real declared ids. */\n | { entry: string; kind: \"unknown-capability\"; capability: string; close: string[] }\n /** Declared, but with no `capabilities/<id>/spec.md` — so it has no requirements at all. */\n | { entry: string; kind: \"undocumented-capability\"; capability: string }\n /** The document exists and declares no requirements yet. */\n | { entry: string; kind: \"empty-capability\"; capability: string }\n /** The document exists and declares requirements, but not this one. */\n | { entry: string; kind: \"unknown-requirement\"; capability: string; requirement: string; close: string[] };\n\n/**\n * Resolve every entry, in the order the author wrote them.\n *\n * Order preserved and repeats NOT collapsed, matching the `Capability:` line's\n * behaviour in `../findings.ts` rather than the `#cap-` tag's in\n * `../usecase-join.ts`. The two siblings differ because a tag set is a set —\n * LikeC4 hands it over with no authored order to preserve — while a body line\n * is text somebody typed, and a reader chasing a finding is looking at that\n * text. Consistency with the line beside it is worth more here than a\n * deduplication nobody asked for.\n *\n * An empty result when `declared` is `null` is the ladder, not an answer: there\n * is nothing to grade against, so nothing is graded.\n */\nexport function resolveRealizes(\n entries: readonly string[],\n index: CapabilityRequirementIndex,\n): RealizesClaim[] {\n const { declared, byCapability, digests } = index;\n if (declared === null) return [];\n const known = new Set(declared);\n\n return entries.map((entry): RealizesClaim => {\n const target = splitRealizesEntry(entry);\n if (target === null) return { entry, kind: \"malformed\" };\n const { capability, requirement, pin } = target;\n if (!known.has(capability)) {\n return { entry, kind: \"unknown-capability\", capability, close: closeIds(capability, declared) };\n }\n const ids = byCapability.get(capability);\n if (ids === undefined) return { entry, kind: \"undocumented-capability\", capability };\n if (ids.size === 0) return { entry, kind: \"empty-capability\", capability };\n if (ids.has(requirement)) {\n // An index with no digest for a resolved id leaves the verdict UNKNOWN\n // rather than STALE. The two are not the same claim, and a reader who is\n // told a pin went stale because the grader could not compute the other\n // side would be told something false.\n const current = digests.get(capability)?.get(requirement) ?? null;\n const stale = pin === null || current === null ? null : pin !== current;\n return { entry, kind: \"resolved\", capability, requirement, pin, current, stale };\n }\n return {\n entry,\n kind: \"unknown-requirement\",\n capability,\n requirement,\n close: closeIds(requirement, [...ids].sort(compareIds)),\n };\n });\n}\n"]}
|
|
@@ -80,6 +80,12 @@ export async function capabilityRollup(input) {
|
|
|
80
80
|
for (const target of r.realizes.map(splitRealizesEntry)) {
|
|
81
81
|
if (target === null)
|
|
82
82
|
continue; // malformed — capability.realizes-unknown's business
|
|
83
|
+
// The capability is claimed BEFORE asking whether the requirement id
|
|
84
|
+
// resolves, and that is deliberate: an entry naming an undeclared id
|
|
85
|
+
// already earns `capability.realizes-unknown`, and letting it also
|
|
86
|
+
// leave its capability `capability.unrealized` would be two findings
|
|
87
|
+
// for one mistake — the second pointing at the analyst's document
|
|
88
|
+
// rather than at the typo.
|
|
83
89
|
claim(rows, claimed, target.capability, where);
|
|
84
90
|
// The requirement-level half. An entry naming an id the document does
|
|
85
91
|
// not declare lands nowhere, which is exactly right: it is already
|