@ybotok/loam 0.2.0-alpha.7 → 0.2.0-alpha.9
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +1315 -1
- package/COMPARISON.md +91 -3
- package/CONTRIBUTING.md +37 -0
- package/MIGRATING-from-OpenSpec.md +40 -22
- package/QUICKSTART.md +181 -9
- package/README.md +256 -57
- package/ROADMAP.md +83 -7
- package/SCHEMA.md +569 -98
- package/WORKFLOW.md +272 -62
- package/dist/cli.js +26 -8
- package/dist/cli.js.map +1 -1
- package/dist/commands/adopt/adopt.js +5 -10
- package/dist/commands/adopt/adopt.js.map +1 -1
- package/dist/commands/archive/archive.js +15 -18
- package/dist/commands/archive/archive.js.map +1 -1
- package/dist/commands/archive/plan/contracts/asyncapi.js +27 -41
- package/dist/commands/archive/plan/contracts/asyncapi.js.map +1 -1
- package/dist/commands/archive/plan/contracts/consumers.js +56 -0
- package/dist/commands/archive/plan/contracts/consumers.js.map +1 -0
- package/dist/commands/archive/plan/contracts/openapi.js +25 -42
- package/dist/commands/archive/plan/contracts/openapi.js.map +1 -1
- package/dist/commands/archive/plan/contracts/verdict-lines.js +41 -0
- package/dist/commands/archive/plan/contracts/verdict-lines.js.map +1 -0
- package/dist/commands/archive/plan/gate.js +3 -7
- package/dist/commands/archive/plan/gate.js.map +1 -1
- package/dist/commands/archive/plan/landscape.js +4 -4
- package/dist/commands/archive/plan/landscape.js.map +1 -1
- package/dist/commands/archive/plan/refusal.js +8 -18
- package/dist/commands/archive/plan/refusal.js.map +1 -1
- package/dist/commands/archive/plan/specs.js +134 -25
- package/dist/commands/archive/plan/specs.js.map +1 -1
- package/dist/commands/archive/plan/state.js +1 -0
- package/dist/commands/archive/plan/state.js.map +1 -1
- package/dist/commands/archive/run.js +33 -11
- package/dist/commands/archive/run.js.map +1 -1
- package/dist/commands/archive/shipped.js +49 -0
- package/dist/commands/archive/shipped.js.map +1 -0
- package/dist/commands/context/context.js +4 -7
- package/dist/commands/context/context.js.map +1 -1
- package/dist/commands/context/print.js +2 -2
- package/dist/commands/context/print.js.map +1 -1
- package/dist/commands/delta/delta.js +44 -12
- package/dist/commands/delta/delta.js.map +1 -1
- package/dist/commands/delta/print.js +1 -1
- package/dist/commands/delta/print.js.map +1 -1
- package/dist/commands/dependencies.js +3 -7
- package/dist/commands/dependencies.js.map +1 -1
- package/dist/commands/diff/diff.js +20 -27
- package/dist/commands/diff/diff.js.map +1 -1
- package/dist/commands/explore.js +4 -8
- package/dist/commands/explore.js.map +1 -1
- package/dist/commands/gate/gate.js +45 -8
- package/dist/commands/gate/gate.js.map +1 -1
- package/dist/commands/gate/print.js +15 -3
- package/dist/commands/gate/print.js.map +1 -1
- package/dist/commands/gherkin/commit.js +13 -54
- package/dist/commands/gherkin/commit.js.map +1 -1
- package/dist/commands/gherkin/gherkin.js +36 -20
- package/dist/commands/gherkin/gherkin.js.map +1 -1
- package/dist/commands/gherkin/reconcile.js +10 -13
- package/dist/commands/gherkin/reconcile.js.map +1 -1
- package/dist/commands/init/first-hour.js +44 -0
- package/dist/commands/init/first-hour.js.map +1 -1
- package/dist/commands/init/init.js +18 -22
- package/dist/commands/init/init.js.map +1 -1
- package/dist/commands/list/campaign/campaign.js +4 -6
- package/dist/commands/list/campaign/campaign.js.map +1 -1
- package/dist/commands/list/json.js +0 -4
- package/dist/commands/list/json.js.map +1 -1
- package/dist/commands/list/list.js +38 -23
- package/dist/commands/list/list.js.map +1 -1
- package/dist/commands/list/print.js +38 -6
- package/dist/commands/list/print.js.map +1 -1
- package/dist/commands/list/suites/suites.js +140 -0
- package/dist/commands/list/suites/suites.js.map +1 -0
- package/dist/commands/list/views.js +28 -33
- package/dist/commands/list/views.js.map +1 -1
- package/dist/commands/mcp/dispatch.js +91 -36
- package/dist/commands/mcp/dispatch.js.map +1 -1
- package/dist/commands/mcp/mcp.js +56 -15
- package/dist/commands/mcp/mcp.js.map +1 -1
- package/dist/commands/mcp/serve.js.map +1 -1
- package/dist/commands/migrate-openspec/materialize/active.js +27 -22
- package/dist/commands/migrate-openspec/materialize/active.js.map +1 -1
- package/dist/commands/migrate-openspec/materialize/stage.js +4 -3
- package/dist/commands/migrate-openspec/materialize/stage.js.map +1 -1
- package/dist/commands/migrate-openspec/materialize/target.js +2 -2
- package/dist/commands/migrate-openspec/materialize/target.js.map +1 -1
- package/dist/commands/migrate-openspec/openspec/decisions.js +9 -14
- package/dist/commands/migrate-openspec/openspec/decisions.js.map +1 -1
- package/dist/commands/migrate-openspec/openspec/error.js +7 -8
- package/dist/commands/migrate-openspec/openspec/error.js.map +1 -1
- package/dist/commands/migrate-openspec/openspec/mapping.js +1 -9
- package/dist/commands/migrate-openspec/openspec/mapping.js.map +1 -1
- package/dist/commands/migrate-openspec/openspec/paths.js +3 -6
- package/dist/commands/migrate-openspec/openspec/paths.js.map +1 -1
- package/dist/commands/new/capability-template.js +118 -0
- package/dist/commands/new/capability-template.js.map +1 -0
- package/dist/commands/new/commit.js +8 -22
- package/dist/commands/new/commit.js.map +1 -1
- package/dist/commands/new/modifies.js +206 -0
- package/dist/commands/new/modifies.js.map +1 -0
- package/dist/commands/new/new.js +59 -14
- package/dist/commands/new/new.js.map +1 -1
- package/dist/commands/new/templates.js +48 -124
- package/dist/commands/new/templates.js.map +1 -1
- package/dist/commands/open.js +3 -7
- package/dist/commands/open.js.map +1 -1
- package/dist/commands/policy/gate.js +27 -0
- package/dist/commands/policy/gate.js.map +1 -1
- package/dist/commands/rebase/plan.js +2 -2
- package/dist/commands/rebase/plan.js.map +1 -1
- package/dist/commands/rebase/rebase.js +60 -35
- package/dist/commands/rebase/rebase.js.map +1 -1
- package/dist/commands/seed/commit.js +21 -25
- package/dist/commands/seed/commit.js.map +1 -1
- package/dist/commands/seed/seed.js +6 -9
- package/dist/commands/seed/seed.js.map +1 -1
- package/dist/commands/show/show.js +3 -7
- package/dist/commands/show/show.js.map +1 -1
- package/dist/commands/status/print.js +13 -4
- package/dist/commands/status/print.js.map +1 -1
- package/dist/commands/status/status.js +3 -7
- package/dist/commands/status/status.js.map +1 -1
- package/dist/commands/steps/steps.js +3 -7
- package/dist/commands/steps/steps.js.map +1 -1
- package/dist/commands/subsystem/edit.js +3 -5
- package/dist/commands/subsystem/edit.js.map +1 -1
- package/dist/commands/subsystem/move.js +5 -8
- 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 +14 -41
- package/dist/commands/subsystem/sync.js.map +1 -1
- package/dist/commands/subsystem/txn/txn.js +17 -38
- package/dist/commands/subsystem/txn/txn.js.map +1 -1
- package/dist/commands/subsystem/txn/views.js +32 -5
- package/dist/commands/subsystem/txn/views.js.map +1 -1
- package/dist/commands/tasks/render.js +116 -0
- package/dist/commands/tasks/render.js.map +1 -0
- package/dist/commands/tasks/tasks.js +113 -0
- package/dist/commands/tasks/tasks.js.map +1 -0
- package/dist/commands/unarchive/manifest.js +6 -9
- package/dist/commands/unarchive/manifest.js.map +1 -1
- package/dist/commands/unarchive/restore.js +17 -28
- package/dist/commands/unarchive/restore.js.map +1 -1
- package/dist/commands/unarchive/unarchive.js +22 -30
- package/dist/commands/unarchive/unarchive.js.map +1 -1
- package/dist/commands/validate/arch-coverage.js +6 -15
- package/dist/commands/validate/arch-coverage.js.map +1 -1
- package/dist/commands/validate/checks/fleet-shape.js +4 -49
- package/dist/commands/validate/checks/fleet-shape.js.map +1 -1
- package/dist/commands/validate/checks/requirements.js +18 -8
- package/dist/commands/validate/checks/requirements.js.map +1 -1
- package/dist/commands/validate/checks/sources.js +6 -2
- package/dist/commands/validate/checks/sources.js.map +1 -1
- package/dist/commands/validate/feature.js +23 -19
- package/dist/commands/validate/feature.js.map +1 -1
- package/dist/commands/validate/fleet/landscape.js +23 -23
- package/dist/commands/validate/fleet/landscape.js.map +1 -1
- package/dist/commands/validate/fleet/map/attest.js +6 -10
- package/dist/commands/validate/fleet/map/attest.js.map +1 -1
- package/dist/commands/validate/fleet/map/bindings.js +1 -3
- package/dist/commands/validate/fleet/map/bindings.js.map +1 -1
- package/dist/commands/validate/fleet/map/usecases.js +5 -11
- package/dist/commands/validate/fleet/map/usecases.js.map +1 -1
- package/dist/commands/validate/fleet/obligations.js +1 -5
- package/dist/commands/validate/fleet/obligations.js.map +1 -1
- package/dist/commands/validate/fleet/scorecard/print.js +5 -1
- package/dist/commands/validate/fleet/scorecard/print.js.map +1 -1
- package/dist/commands/validate/fleet/scorecard/scorecard.js +17 -11
- package/dist/commands/validate/fleet/scorecard/scorecard.js.map +1 -1
- package/dist/commands/validate/fleet/usecases/usecases.js +6 -4
- package/dist/commands/validate/fleet/usecases/usecases.js.map +1 -1
- package/dist/commands/validate/fleet/views/fleet-project.js.map +1 -1
- package/dist/commands/validate/fleet/views/root-documents.js +8 -28
- package/dist/commands/validate/fleet/views/root-documents.js.map +1 -1
- package/dist/commands/validate/links/corpus.js +1 -4
- package/dist/commands/validate/links/corpus.js.map +1 -1
- package/dist/commands/validate/links/glossary.js.map +1 -1
- package/dist/commands/validate/report.js +5 -4
- package/dist/commands/validate/report.js.map +1 -1
- package/dist/commands/validate/scope.js +113 -0
- package/dist/commands/validate/scope.js.map +1 -0
- package/dist/commands/validate/service/api.js +16 -104
- package/dist/commands/validate/service/api.js.map +1 -1
- package/dist/commands/validate/service/contracts/generated.js +134 -0
- package/dist/commands/validate/service/contracts/generated.js.map +1 -0
- package/dist/commands/validate/service/events/events.js +1 -1
- package/dist/commands/validate/service/events/events.js.map +1 -1
- package/dist/commands/validate/service/events/message-producers.js +1 -1
- package/dist/commands/validate/service/events/message-producers.js.map +1 -1
- package/dist/commands/validate/service/evidence-pins.js +56 -10
- package/dist/commands/validate/service/evidence-pins.js.map +1 -1
- package/dist/commands/validate/service/service.js +16 -26
- package/dist/commands/validate/service/service.js.map +1 -1
- package/dist/commands/validate/service/specs.js +31 -38
- package/dist/commands/validate/service/specs.js.map +1 -1
- package/dist/commands/validate/service/spine.js +8 -11
- package/dist/commands/validate/service/spine.js.map +1 -1
- package/dist/commands/validate/service/usecases/usecases.js +3 -3
- package/dist/commands/validate/service/usecases/usecases.js.map +1 -1
- package/dist/commands/validate/validate.js +44 -26
- package/dist/commands/validate/validate.js.map +1 -1
- package/dist/commands/verify/cross/diff.js +2 -6
- package/dist/commands/verify/cross/diff.js.map +1 -1
- package/dist/commands/verify/evidence/contract.js +1 -1
- package/dist/commands/verify/evidence/contract.js.map +1 -1
- package/dist/commands/verify/evidence/read.js +12 -3
- package/dist/commands/verify/evidence/read.js.map +1 -1
- package/dist/commands/verify/frozen.js +45 -23
- package/dist/commands/verify/frozen.js.map +1 -1
- package/dist/commands/verify/record.js +69 -17
- package/dist/commands/verify/record.js.map +1 -1
- package/dist/commands/verify/report.js +27 -44
- package/dist/commands/verify/report.js.map +1 -1
- package/dist/commands/verify/results.js +112 -30
- package/dist/commands/verify/results.js.map +1 -1
- package/dist/commands/verify/verify.js +54 -51
- package/dist/commands/verify/verify.js.map +1 -1
- package/dist/commands/vouch/pack/pack.js +2 -2
- package/dist/commands/vouch/pack/pack.js.map +1 -1
- package/dist/commands/vouch/pack/print.js +1 -6
- package/dist/commands/vouch/pack/print.js.map +1 -1
- package/dist/commands/vouch/pack/sections.js +1 -2
- package/dist/commands/vouch/pack/sections.js.map +1 -1
- package/dist/commands/vouch/run.js +26 -81
- package/dist/commands/vouch/run.js.map +1 -1
- package/dist/commands/vouch/sample/plan.js +20 -34
- package/dist/commands/vouch/sample/plan.js.map +1 -1
- package/dist/commands/vouch/vet/verify.js +39 -7
- package/dist/commands/vouch/vet/verify.js.map +1 -1
- package/dist/commands/vouch/vouch.js +10 -17
- package/dist/commands/vouch/vouch.js.map +1 -1
- package/dist/core/agent/agents-md/command-map.js +56 -101
- package/dist/core/agent/agents-md/command-map.js.map +1 -1
- package/dist/core/agent/agents-md/map/explain.js +4 -3
- package/dist/core/agent/agents-md/map/explain.js.map +1 -1
- package/dist/core/agent/agents-md/map/lenses/context.js +4 -3
- package/dist/core/agent/agents-md/map/lenses/context.js.map +1 -1
- package/dist/core/agent/agents-md/map/lenses/diff.js +33 -20
- package/dist/core/agent/agents-md/map/lenses/diff.js.map +1 -1
- package/dist/core/agent/agents-md/map/lenses/gate.js +13 -5
- package/dist/core/agent/agents-md/map/lenses/gate.js.map +1 -1
- package/dist/core/agent/agents-md/map/mcp.js +10 -7
- package/dist/core/agent/agents-md/map/mcp.js.map +1 -1
- package/dist/core/agent/agents-md/map/status.js +9 -3
- package/dist/core/agent/agents-md/map/status.js.map +1 -1
- package/dist/core/agent/agents-md/map/subsystem.js +4 -3
- package/dist/core/agent/agents-md/map/subsystem.js.map +1 -1
- package/dist/core/agent/agents-md/map/usecases.js +24 -6
- package/dist/core/agent/agents-md/map/usecases.js.map +1 -1
- package/dist/core/agent/agents-md/refusals.js +32 -18
- package/dist/core/agent/agents-md/refusals.js.map +1 -1
- package/dist/core/agent/agents-md.js +9 -3
- package/dist/core/agent/agents-md.js.map +1 -1
- package/dist/core/agent/agents-stamp.js +10 -0
- package/dist/core/agent/agents-stamp.js.map +1 -1
- package/dist/core/agent/scaffold.js +10 -15
- package/dist/core/agent/scaffold.js.map +1 -1
- package/dist/core/agent/tools/dialects.js +1 -0
- package/dist/core/agent/tools/dialects.js.map +1 -1
- package/dist/core/agent/tools/registry.js +41 -76
- package/dist/core/agent/tools/registry.js.map +1 -1
- package/dist/core/agent/workflows/adopt.js +5 -3
- package/dist/core/agent/workflows/adopt.js.map +1 -1
- package/dist/core/agent/workflows/check.js +20 -16
- package/dist/core/agent/workflows/check.js.map +1 -1
- package/dist/core/agent/workflows/closing.js +54 -21
- package/dist/core/agent/workflows/closing.js.map +1 -1
- package/dist/core/agent/workflows/feature.js +79 -11
- package/dist/core/agent/workflows/feature.js.map +1 -1
- package/dist/core/agent/workflows/implement.js +91 -13
- package/dist/core/agent/workflows/implement.js.map +1 -1
- package/dist/core/agent/workflows/reference/authoring.js +3 -1
- package/dist/core/agent/workflows/reference/authoring.js.map +1 -1
- package/dist/core/agent/workflows/reference/codes.js +8 -6
- package/dist/core/agent/workflows/reference/codes.js.map +1 -1
- package/dist/core/agent/workflows/reference/done-check.js +37 -15
- package/dist/core/agent/workflows/reference/done-check.js.map +1 -1
- package/dist/core/agent/workflows/reference/reference.js.map +1 -1
- package/dist/core/agent/workflows/reference/spine.js +50 -18
- package/dist/core/agent/workflows/reference/spine.js.map +1 -1
- package/dist/core/asyncapi/baseline/plan.js +14 -81
- package/dist/core/asyncapi/baseline/plan.js.map +1 -1
- package/dist/core/asyncapi/digest.js +30 -25
- package/dist/core/asyncapi/digest.js.map +1 -1
- package/dist/core/asyncapi/merge/components.js +5 -4
- package/dist/core/asyncapi/merge/components.js.map +1 -1
- package/dist/core/asyncapi/merge/error.js +7 -2
- package/dist/core/asyncapi/merge/error.js.map +1 -1
- package/dist/core/asyncapi/merge/markers.js +30 -50
- package/dist/core/asyncapi/merge/markers.js.map +1 -1
- package/dist/core/asyncapi/merge/merge.js +31 -53
- package/dist/core/asyncapi/merge/merge.js.map +1 -1
- package/dist/core/asyncapi/merge/pin.js +10 -36
- package/dist/core/asyncapi/merge/pin.js.map +1 -1
- package/dist/core/asyncapi/model.js.map +1 -1
- package/dist/core/asyncapi/read.js +36 -12
- package/dist/core/asyncapi/read.js.map +1 -1
- package/dist/core/brief/brief.js +6 -18
- package/dist/core/brief/brief.js.map +1 -1
- package/dist/core/brief/landscape.js +8 -12
- package/dist/core/brief/landscape.js.map +1 -1
- package/dist/core/brief/map/extend.js +8 -5
- package/dist/core/brief/map/extend.js.map +1 -1
- package/dist/core/brief/map/owed.js +8 -26
- package/dist/core/brief/map/owed.js.map +1 -1
- package/dist/core/brief/targets.js +15 -5
- package/dist/core/brief/targets.js.map +1 -1
- package/dist/core/c4/likec4.js +147 -16
- package/dist/core/c4/likec4.js.map +1 -1
- package/dist/core/c4/locate/unlocated.js +150 -0
- package/dist/core/c4/locate/unlocated.js.map +1 -0
- package/dist/core/c4/parsed/values.js +54 -1
- package/dist/core/c4/parsed/values.js.map +1 -1
- package/dist/core/c4/prefetch/fleet.js +1 -9
- package/dist/core/c4/prefetch/fleet.js.map +1 -1
- package/dist/core/c4/project/documents.js +31 -6
- package/dist/core/c4/project/documents.js.map +1 -1
- package/dist/core/c4/project/load.js +6 -17
- package/dist/core/c4/project/load.js.map +1 -1
- package/dist/core/c4/resolve/external.js +51 -0
- package/dist/core/c4/resolve/external.js.map +1 -0
- package/dist/core/c4/resolve/service.js +25 -1
- package/dist/core/c4/resolve/service.js.map +1 -1
- package/dist/core/c4/seed/template.js +15 -1
- package/dist/core/c4/seed/template.js.map +1 -1
- package/dist/core/c4/service-model/shape.js +2 -22
- package/dist/core/c4/service-model/shape.js.map +1 -1
- package/dist/core/c4/source-mask.js +288 -4
- package/dist/core/c4/source-mask.js.map +1 -1
- package/dist/core/c4/source-scan.js +203 -147
- package/dist/core/c4/source-scan.js.map +1 -1
- package/dist/core/c4/splice/authored-source.js +19 -0
- package/dist/core/c4/splice/authored-source.js.map +1 -1
- package/dist/core/c4/splice/delta-blocks.js +5 -18
- package/dist/core/c4/splice/delta-blocks.js.map +1 -1
- package/dist/core/c4/splice/landscape-merge.js +13 -8
- package/dist/core/c4/splice/landscape-merge.js.map +1 -1
- package/dist/core/c4/splice/model/merge.js +10 -6
- package/dist/core/c4/splice/model/merge.js.map +1 -1
- package/dist/core/c4/splice/placement.js +108 -18
- package/dist/core/c4/splice/placement.js.map +1 -1
- package/dist/core/c4/workspace.js +13 -22
- package/dist/core/c4/workspace.js.map +1 -1
- package/dist/core/capabilities/capabilities.js +6 -6
- package/dist/core/capabilities/capabilities.js.map +1 -1
- package/dist/core/capabilities/delta/doc.js +7 -10
- package/dist/core/capabilities/delta/doc.js.map +1 -1
- package/dist/core/capabilities/findings.js +15 -20
- package/dist/core/capabilities/findings.js.map +1 -1
- package/dist/core/capabilities/realizes/findings.js +12 -19
- package/dist/core/capabilities/realizes/findings.js.map +1 -1
- package/dist/core/capabilities/realizes/join.js +0 -20
- package/dist/core/capabilities/realizes/join.js.map +1 -1
- package/dist/core/capabilities/rollup.js +6 -0
- package/dist/core/capabilities/rollup.js.map +1 -1
- package/dist/core/capabilities/tree.js +6 -28
- package/dist/core/capabilities/tree.js.map +1 -1
- package/dist/core/capabilities/usecase-join.js +3 -13
- package/dist/core/capabilities/usecase-join.js.map +1 -1
- package/dist/core/coherence/authoring/scaffold.js +11 -3
- package/dist/core/coherence/authoring/scaffold.js.map +1 -1
- package/dist/core/coherence/authoring/sentinels.js +2 -1
- package/dist/core/coherence/authoring/sentinels.js.map +1 -1
- package/dist/core/coherence/coherence.js +30 -16
- package/dist/core/coherence/coherence.js.map +1 -1
- package/dist/core/coherence/consumed/operations.js +70 -0
- package/dist/core/coherence/consumed/operations.js.map +1 -0
- package/dist/core/coherence/consumed/refusal.js +38 -0
- package/dist/core/coherence/consumed/refusal.js.map +1 -0
- package/dist/core/coherence/declared.js +60 -34
- package/dist/core/coherence/declared.js.map +1 -1
- package/dist/core/coherence/events/declared.js +44 -59
- package/dist/core/coherence/events/declared.js.map +1 -1
- package/dist/core/coherence/events/events.js +119 -8
- package/dist/core/coherence/events/events.js.map +1 -1
- package/dist/core/coherence/events/grades.js +31 -8
- package/dist/core/coherence/events/grades.js.map +1 -1
- package/dist/core/coherence/events/lookups.js +14 -82
- package/dist/core/coherence/events/lookups.js.map +1 -1
- package/dist/core/coherence/lookups.js +18 -78
- package/dist/core/coherence/lookups.js.map +1 -1
- package/dist/core/coherence/retired/archived.js +285 -0
- package/dist/core/coherence/retired/archived.js.map +1 -0
- package/dist/core/coherence/retired/event-pair.js +62 -0
- package/dist/core/coherence/retired/event-pair.js.map +1 -0
- package/dist/core/coherence/retired/merged.js +79 -0
- package/dist/core/coherence/retired/merged.js.map +1 -0
- package/dist/core/coherence/retired/retirement.js +49 -0
- package/dist/core/coherence/retired/retirement.js.map +1 -0
- package/dist/core/coherence/retired/revival/arrivals.js +131 -0
- package/dist/core/coherence/retired/revival/arrivals.js.map +1 -0
- package/dist/core/coherence/retired/revival/revived.js +297 -0
- package/dist/core/coherence/retired/revival/revived.js.map +1 -0
- package/dist/core/consumers/after.js +113 -0
- package/dist/core/consumers/after.js.map +1 -0
- package/dist/core/consumers/consumers.js +296 -0
- package/dist/core/consumers/consumers.js.map +1 -0
- package/dist/core/consumers/current.js +211 -0
- package/dist/core/consumers/current.js.map +1 -0
- package/dist/core/consumers/scan.js +46 -0
- package/dist/core/consumers/scan.js.map +1 -0
- package/dist/core/consumers/state.js +27 -0
- package/dist/core/consumers/state.js.map +1 -0
- package/dist/core/delta/delta.js +1 -1
- package/dist/core/delta/delta.js.map +1 -1
- package/dist/core/deployment/delta.js +3 -6
- package/dist/core/deployment/delta.js.map +1 -1
- package/dist/core/diff/base-git.js +94 -12
- package/dist/core/diff/base-git.js.map +1 -1
- package/dist/core/diff/base-state.js +28 -16
- package/dist/core/diff/base-state.js.map +1 -1
- package/dist/core/diff/joins.js +136 -0
- package/dist/core/diff/joins.js.map +1 -0
- package/dist/core/diff/scope/changed-paths.js +22 -6
- package/dist/core/diff/scope/changed-paths.js.map +1 -1
- package/dist/core/diff/scope/edges.js +82 -0
- package/dist/core/diff/scope/edges.js.map +1 -0
- package/dist/core/diff/semantic.js +100 -31
- package/dist/core/diff/semantic.js.map +1 -1
- package/dist/core/doctor/agents.js +34 -18
- package/dist/core/doctor/agents.js.map +1 -1
- package/dist/core/doctor/report.js.map +1 -1
- package/dist/core/document/apply.js +10 -2
- package/dist/core/document/apply.js.map +1 -1
- package/dist/core/document/frontmatter.js +55 -0
- package/dist/core/document/frontmatter.js.map +1 -1
- package/dist/core/document/parse.js +29 -6
- package/dist/core/document/parse.js.map +1 -1
- package/dist/core/document/scenarios.js +17 -0
- package/dist/core/document/scenarios.js.map +1 -1
- package/dist/core/document/spec.js +4 -11
- package/dist/core/document/spec.js.map +1 -1
- package/dist/core/envelope/config.js +10 -0
- package/dist/core/envelope/config.js.map +1 -1
- package/dist/core/envelope/json.js +34 -0
- package/dist/core/envelope/json.js.map +1 -1
- package/dist/core/explain/families.js +17 -12
- package/dist/core/explain/families.js.map +1 -1
- package/dist/core/explain/refusals.js +3 -3
- package/dist/core/explain/refusals.js.map +1 -1
- package/dist/core/explain/terms.js +18 -9
- package/dist/core/explain/terms.js.map +1 -1
- package/dist/core/explore/explore.js +2 -7
- package/dist/core/explore/explore.js.map +1 -1
- package/dist/core/fleet-context.js +34 -95
- package/dist/core/fleet-context.js.map +1 -1
- package/dist/core/gate/checks.js +5 -2
- package/dist/core/gate/checks.js.map +1 -1
- package/dist/core/gate/partners.js +4 -2
- package/dist/core/gate/partners.js.map +1 -1
- package/dist/core/gate/report.js.map +1 -1
- package/dist/core/gate/verification.js +75 -12
- package/dist/core/gate/verification.js.map +1 -1
- package/dist/core/gherkin/stale.js +13 -37
- package/dist/core/gherkin/stale.js.map +1 -1
- package/dist/core/glossary/tree.js +6 -19
- package/dist/core/glossary/tree.js.map +1 -1
- package/dist/core/kernel/concurrency.js +19 -0
- package/dist/core/kernel/concurrency.js.map +1 -1
- package/dist/core/kernel/records.js +11 -0
- package/dist/core/kernel/records.js.map +1 -1
- package/dist/core/links/backlinks.js +1 -3
- package/dist/core/links/backlinks.js.map +1 -1
- package/dist/core/links/corpus.js +5 -36
- package/dist/core/links/corpus.js.map +1 -1
- package/dist/core/mcp/argv.js +101 -0
- package/dist/core/mcp/argv.js.map +1 -1
- package/dist/core/mcp/author-tools.js +2 -1
- package/dist/core/mcp/author-tools.js.map +1 -1
- package/dist/core/mcp/protocol.js +2 -2
- package/dist/core/mcp/protocol.js.map +1 -1
- package/dist/core/mcp/tools.js +43 -33
- package/dist/core/mcp/tools.js.map +1 -1
- package/dist/core/openapi/baseline/gate.js +53 -12
- package/dist/core/openapi/baseline/gate.js.map +1 -1
- package/dist/core/openapi/baseline/plan.js +22 -8
- package/dist/core/openapi/baseline/plan.js.map +1 -1
- package/dist/core/openapi/baseline/record.js +0 -1
- package/dist/core/openapi/baseline/record.js.map +1 -1
- package/dist/core/openapi/contract-text.js +33 -0
- package/dist/core/openapi/contract-text.js.map +1 -0
- package/dist/core/openapi/contract-tree.js +56 -0
- package/dist/core/openapi/contract-tree.js.map +1 -0
- package/dist/core/openapi/depth.js +38 -24
- package/dist/core/openapi/depth.js.map +1 -1
- package/dist/core/openapi/digest.js +52 -2
- package/dist/core/openapi/digest.js.map +1 -1
- package/dist/core/openapi/doc.js +12 -4
- package/dist/core/openapi/doc.js.map +1 -1
- package/dist/core/openapi/merge/components.js +25 -60
- package/dist/core/openapi/merge/components.js.map +1 -1
- package/dist/core/openapi/merge/error.js +3 -13
- package/dist/core/openapi/merge/error.js.map +1 -1
- package/dist/core/openapi/merge/markers.js +26 -30
- package/dist/core/openapi/merge/markers.js.map +1 -1
- package/dist/core/openapi/merge/merge.js +26 -52
- package/dist/core/openapi/merge/merge.js.map +1 -1
- package/dist/core/openapi/merge/pin.js +15 -35
- package/dist/core/openapi/merge/pin.js.map +1 -1
- package/dist/core/openspec/decide/verdict.js.map +1 -1
- package/dist/core/openspec/inventory.js +4 -5
- package/dist/core/openspec/inventory.js.map +1 -1
- package/dist/core/openspec/model/mapping.js +30 -28
- package/dist/core/openspec/model/mapping.js.map +1 -1
- package/dist/core/openspec/model/model.js +12 -7
- package/dist/core/openspec/model/model.js.map +1 -1
- package/dist/core/openspec/scan/changes.js +10 -4
- package/dist/core/openspec/scan/changes.js.map +1 -1
- package/dist/core/openspec/scan/config.js +7 -16
- package/dist/core/openspec/scan/config.js.map +1 -1
- package/dist/core/openspec/scan/shape.js +63 -19
- package/dist/core/openspec/scan/shape.js.map +1 -1
- package/dist/core/openspec/scan/walk.js +21 -38
- package/dist/core/openspec/scan/walk.js.map +1 -1
- package/dist/core/pack/living.js +2 -16
- package/dist/core/pack/living.js.map +1 -1
- package/dist/core/provenance/findings.js +48 -12
- package/dist/core/provenance/findings.js.map +1 -1
- package/dist/core/provenance/git.js +33 -57
- package/dist/core/provenance/git.js.map +1 -1
- package/dist/core/provenance/gitq/moves.js +71 -114
- package/dist/core/provenance/gitq/moves.js.map +1 -1
- package/dist/core/provenance/gitq/vouched-ancestor.js +2 -2
- package/dist/core/provenance/gitq/vouched-ancestor.js.map +1 -1
- package/dist/core/provenance/sources.js +23 -6
- package/dist/core/provenance/sources.js.map +1 -1
- package/dist/core/provenance/stamp.js +5 -17
- package/dist/core/provenance/stamp.js.map +1 -1
- package/dist/core/repo/entries.js.map +1 -1
- package/dist/core/repo/repo.js +24 -14
- package/dist/core/repo/repo.js.map +1 -1
- package/dist/core/repo/tree/find.js +12 -3
- package/dist/core/repo/tree/find.js.map +1 -1
- package/dist/core/repo/tree/fs.js +27 -1
- package/dist/core/repo/tree/fs.js.map +1 -1
- package/dist/core/repo/tree/walk.js +8 -30
- package/dist/core/repo/tree/walk.js.map +1 -1
- package/dist/core/results.js +47 -12
- package/dist/core/results.js.map +1 -1
- package/dist/core/staging/commit.js +10 -12
- package/dist/core/staging/commit.js.map +1 -1
- package/dist/core/staging/lock.js +74 -12
- package/dist/core/staging/lock.js.map +1 -1
- package/dist/core/staging/recovery/intent.js +2 -5
- package/dist/core/staging/recovery/intent.js.map +1 -1
- package/dist/core/staging/recovery/recover.js +60 -1
- package/dist/core/staging/recovery/recover.js.map +1 -1
- package/dist/core/staging/snapshot/manifest.js +152 -0
- package/dist/core/staging/snapshot/manifest.js.map +1 -0
- package/dist/core/staging/{snapshot.js → snapshot/snapshot.js} +10 -34
- package/dist/core/staging/snapshot/snapshot.js.map +1 -0
- package/dist/core/status/actions/execution.js +11 -0
- package/dist/core/status/actions/execution.js.map +1 -1
- package/dist/core/status/feature/artifacts.js +2 -2
- package/dist/core/status/feature/artifacts.js.map +1 -1
- package/dist/core/status/feature/feature.js +5 -11
- package/dist/core/status/feature/feature.js.map +1 -1
- package/dist/core/status/fleet/fleet.js +6 -2
- package/dist/core/status/fleet/fleet.js.map +1 -1
- package/dist/core/status/fleet/next.js +26 -3
- package/dist/core/status/fleet/next.js.map +1 -1
- package/dist/core/status/report.js.map +1 -1
- package/dist/core/status/verification.js +30 -15
- package/dist/core/status/verification.js.map +1 -1
- package/dist/core/tasks/packet.js +174 -0
- package/dist/core/tasks/packet.js.map +1 -0
- package/dist/core/tasks/record.js +43 -0
- package/dist/core/tasks/record.js.map +1 -0
- package/dist/core/tasks/sources.js +181 -0
- package/dist/core/tasks/sources.js.map +1 -0
- package/dist/core/tasks/stamps.js +76 -0
- package/dist/core/tasks/stamps.js.map +1 -0
- package/dist/core/tasks/unit.js +0 -0
- package/dist/core/tasks/unit.js.map +1 -0
- package/dist/core/usecases/capability.js +11 -14
- package/dist/core/usecases/capability.js.map +1 -1
- package/dist/core/usecases/delta/flows.js +5 -7
- package/dist/core/usecases/delta/flows.js.map +1 -1
- package/dist/core/usecases/fleet.js +178 -31
- package/dist/core/usecases/fleet.js.map +1 -1
- package/dist/core/usecases/operations.js +5 -4
- package/dist/core/usecases/operations.js.map +1 -1
- package/dist/core/usecases/service/flows.js +9 -6
- package/dist/core/usecases/service/flows.js.map +1 -1
- package/dist/core/usecases/suites/participants.js +83 -0
- package/dist/core/usecases/suites/participants.js.map +1 -0
- package/dist/core/usecases/suites/suites.js +45 -0
- package/dist/core/usecases/suites/suites.js.map +1 -0
- package/dist/core/verify/answers.js +12 -5
- package/dist/core/verify/answers.js.map +1 -1
- package/dist/core/verify/build.js +102 -82
- package/dist/core/verify/build.js.map +1 -1
- package/dist/core/verify/checklist.js +3 -0
- package/dist/core/verify/checklist.js.map +1 -1
- package/dist/core/verify/location.js +67 -0
- package/dist/core/verify/location.js.map +1 -0
- package/dist/core/verify/pins/lint.js +3 -2
- package/dist/core/verify/pins/lint.js.map +1 -1
- package/dist/core/verify/pins/pin.js +1 -1
- package/dist/core/verify/pins/pin.js.map +1 -1
- package/dist/core/verify/read/file.js +224 -0
- package/dist/core/verify/read/file.js.map +1 -0
- package/dist/core/verify/{file.js → read/shape.js} +16 -104
- package/dist/core/verify/read/shape.js.map +1 -0
- package/dist/core/verify/read/union.js +105 -0
- package/dist/core/verify/read/union.js.map +1 -0
- package/dist/core/verify/record.js +35 -12
- package/dist/core/verify/record.js.map +1 -1
- package/dist/core/verify/store/commit.js +40 -30
- package/dist/core/verify/store/commit.js.map +1 -1
- package/dist/core/verify/store/render.js +8 -4
- package/dist/core/verify/store/render.js.map +1 -1
- package/dist/core/vocabulary/issue.js +1 -0
- package/dist/core/vocabulary/issue.js.map +1 -1
- package/dist/core/vocabulary/maturity.js +12 -2
- package/dist/core/vocabulary/maturity.js.map +1 -1
- package/examples/README.md +21 -6
- package/examples/docs/architecture/landscape.likec4 +8 -0
- package/examples/docs/architecture/usecases/order-notification.likec4 +6 -2
- package/examples/docs/features/FEAT-101-payment-splitting/specs/payment-service/asyncapi.yaml +11 -8
- package/examples/docs/features/FEAT-101-payment-splitting/specs/payment-split-service/openapi.yaml +6 -6
- package/examples/docs/features/archive/FEAT-088-refunds/specs/payment-service/openapi.yaml +13 -10
- package/examples/docs/services/order-service/asyncapi.yaml +3 -2
- package/examples/docs/services/platform/notification-service/asyncapi.yaml +2 -1
- package/package.json +1 -1
- package/dist/commands/validate/checks/vocabulary.js +0 -16
- package/dist/commands/validate/checks/vocabulary.js.map +0 -1
- package/dist/core/agent/agents-md/artifacts.js +0 -221
- package/dist/core/agent/agents-md/artifacts.js.map +0 -1
- package/dist/core/agent/agents-md/cycle.js +0 -180
- package/dist/core/agent/agents-md/cycle.js.map +0 -1
- package/dist/core/agent/agents-md/shipped/archive-gate.js +0 -98
- package/dist/core/agent/agents-md/shipped/archive-gate.js.map +0 -1
- package/dist/core/agent/agents-md/spine.js +0 -40
- package/dist/core/agent/agents-md/spine.js.map +0 -1
- package/dist/core/coherence/events/merged.js +0 -40
- package/dist/core/coherence/events/merged.js.map +0 -1
- package/dist/core/diff/current-state.js +0 -96
- package/dist/core/diff/current-state.js.map +0 -1
- package/dist/core/diff/victims.js +0 -256
- package/dist/core/diff/victims.js.map +0 -1
- package/dist/core/staging/snapshot.js.map +0 -1
- package/dist/core/verify/file.js.map +0 -1
|
@@ -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
|
|
@@ -77,23 +81,14 @@ function unknownMessage(where, u) {
|
|
|
77
81
|
: ". 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
82
|
}
|
|
79
83
|
/**
|
|
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.
|
|
84
|
+
* `capability.unknown` — an ERROR, the same grade as `Requires:` and for its
|
|
85
|
+
* reason: an invented capability reads exactly like a real one in the
|
|
86
|
+
* requirement and in every rollup built over it. Validate files it as a
|
|
87
|
+
* Finding at the service target. For a feature's delta documents it is a
|
|
88
|
+
* coherence Issue: a delta requirement naming an undeclared capability would
|
|
89
|
+
* merge a join that resolves to nothing, so it gates `loam archive` like other
|
|
90
|
+
* errors and stays `--approve`-overridable. The one new archive gate of this
|
|
91
|
+
* axis.
|
|
97
92
|
*/
|
|
98
93
|
export function capabilityUnknownIssues(reqs, target, vocab) {
|
|
99
94
|
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/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` — 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":"AAAA;;;;;;;;;;;;;;;;;GAiBG;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;;;;;;GAMG;AACH,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 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":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAkCG;AACH,OAAO,EAAE,QAAQ,EAAE,MAAM,kBAAkB,CAAC;AAC5C,OAAO,EAAE,gBAAgB,EAAE,MAAM,wBAAwB,CAAC;AAC1D,OAAO,EAAE,UAAU,EAAE,MAAM,uBAAuB,CAAC;AAEnD;;;;;;;GAOG;AACH,MAAM,CAAC,MAAM,kBAAkB,GAAG,GAAG,CAAC;AAetC;;;;;;;;;;;;;;GAcG;AACH,MAAM,UAAU,kBAAkB,CAAC,KAAa;IAC9C,6EAA6E;IAC7E,4EAA4E;IAC5E,qDAAqD;IACrD,MAAM,EAAE,MAAM,EAAE,GAAG,EAAE,GAAG,gBAAgB,CAAC,KAAK,CAAC,CAAC;IAChD,MAAM,EAAE,GAAG,MAAM,CAAC,WAAW,CAAC,kBAAkB,CAAC,CAAC;IAClD,IAAI,EAAE,IAAI,CAAC,IAAI,EAAE,KAAK,MAAM,CAAC,MAAM,GAAG,CAAC;QAAE,OAAO,IAAI,CAAC;IACrD,OAAO,EAAE,UAAU,EAAE,MAAM,CAAC,KAAK,CAAC,CAAC,EAAE,EAAE,CAAC,EAAE,WAAW,EAAE,MAAM,CAAC,KAAK,CAAC,EAAE,GAAG,CAAC,CAAC,EAAE,GAAG,EAAE,CAAC;AACrF,CAAC;AAyED;;;;;;;;;;;;;GAaG;AACH,MAAM,UAAU,eAAe,CAC7B,OAA0B,EAC1B,KAAiC;IAEjC,MAAM,EAAE,QAAQ,EAAE,YAAY,EAAE,OAAO,EAAE,GAAG,KAAK,CAAC;IAClD,IAAI,QAAQ,KAAK,IAAI;QAAE,OAAO,EAAE,CAAC;IACjC,MAAM,KAAK,GAAG,IAAI,GAAG,CAAC,QAAQ,CAAC,CAAC;IAEhC,OAAO,OAAO,CAAC,GAAG,CAAC,CAAC,KAAK,EAAiB,EAAE;QAC1C,MAAM,MAAM,GAAG,kBAAkB,CAAC,KAAK,CAAC,CAAC;QACzC,IAAI,MAAM,KAAK,IAAI;YAAE,OAAO,EAAE,KAAK,EAAE,IAAI,EAAE,WAAW,EAAE,CAAC;QACzD,MAAM,EAAE,UAAU,EAAE,WAAW,EAAE,GAAG,EAAE,GAAG,MAAM,CAAC;QAChD,IAAI,CAAC,KAAK,CAAC,GAAG,CAAC,UAAU,CAAC,EAAE,CAAC;YAC3B,OAAO,EAAE,KAAK,EAAE,IAAI,EAAE,oBAAoB,EAAE,UAAU,EAAE,KAAK,EAAE,QAAQ,CAAC,UAAU,EAAE,QAAQ,CAAC,EAAE,CAAC;QAClG,CAAC;QACD,MAAM,GAAG,GAAG,YAAY,CAAC,GAAG,CAAC,UAAU,CAAC,CAAC;QACzC,IAAI,GAAG,KAAK,SAAS;YAAE,OAAO,EAAE,KAAK,EAAE,IAAI,EAAE,yBAAyB,EAAE,UAAU,EAAE,CAAC;QACrF,IAAI,GAAG,CAAC,IAAI,KAAK,CAAC;YAAE,OAAO,EAAE,KAAK,EAAE,IAAI,EAAE,kBAAkB,EAAE,UAAU,EAAE,CAAC;QAC3E,IAAI,GAAG,CAAC,GAAG,CAAC,WAAW,CAAC,EAAE,CAAC;YACzB,uEAAuE;YACvE,yEAAyE;YACzE,uEAAuE;YACvE,sCAAsC;YACtC,MAAM,OAAO,GAAG,OAAO,CAAC,GAAG,CAAC,UAAU,CAAC,EAAE,GAAG,CAAC,WAAW,CAAC,IAAI,IAAI,CAAC;YAClE,MAAM,KAAK,GAAG,GAAG,KAAK,IAAI,IAAI,OAAO,KAAK,IAAI,CAAC,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC,GAAG,KAAK,OAAO,CAAC;YACxE,OAAO,EAAE,KAAK,EAAE,IAAI,EAAE,UAAU,EAAE,UAAU,EAAE,WAAW,EAAE,GAAG,EAAE,OAAO,EAAE,KAAK,EAAE,CAAC;QACnF,CAAC;QACD,OAAO;YACL,KAAK;YACL,IAAI,EAAE,qBAAqB;YAC3B,UAAU;YACV,WAAW;YACX,KAAK,EAAE,QAAQ,CAAC,WAAW,EAAE,CAAC,GAAG,GAAG,CAAC,CAAC,IAAI,CAAC,UAAU,CAAC,CAAC;SACxD,CAAC;IACJ,CAAC,CAAC,CAAC;AACL,CAAC;AAED;;;;;;;;;;;;;GAaG;AACH,MAAM,UAAU,6BAA6B,CAAC,OAA0B;IACtE,OAAO,OAAO,CAAC,OAAO,CAAC,CAAC,KAAK,EAAE,EAAE;QAC/B,MAAM,MAAM,GAAG,kBAAkB,CAAC,KAAK,CAAC,CAAC;QACzC,OAAO,MAAM,KAAK,IAAI,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,CAAC,MAAM,CAAC,UAAU,CAAC,CAAC;IACpD,CAAC,CAAC,CAAC;AACL,CAAC","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
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"rollup.js","sourceRoot":"","sources":["../../../src/core/capabilities/rollup.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * The fleet total over the capability join, built once and reused by the fleet\n * grade, `loam list capabilities` and `loam explore --capability` — one walk,\n * so the unrealized warning and the rollup a reader diffs can never disagree\n * about what \"realized\" means.\n *\n * Deterministic and diff-stable by construction: rows sorted with compareIds,\n * realizedBy sorted by (service, file, requirement), statuses' keys sorted —\n * no timestamps, no absolute paths, no readdir-order dependence. The reader is\n * INJECTED (`read`) rather than a FleetContext, because fleet-context.ts\n * imports this package for its memo and an import back would be a package\n * cycle (see the memo's comment); callers pass `fleet.readRequirements` bound,\n * so under `validate --all` every parse is already cached from the service\n * targets and the rollup costs no additional per-service read.\n *\n * TWO CORPORA MEET HERE, and neither is derived from the other. A promise is\n * kept by a service requirement's `Realizes:` line (walked below) or by a\n * `#cap-`/`#req-` tagged flow (injected as `keptByFlows`, because the module\n * that computes it imports this one). The rollup carries both on one row for the\n * reason it carries both `Capability:` joins on one row: `loam list capabilities`\n * and `capability.requirement-unrealized` must not be able to give different\n * answers to \"is this business promise kept\", and they can only give one answer\n * if they read one row.\n */\nimport { existsSync } from \"node:fs\";\nimport type { Requirement } from \"../document/spec.js\";\nimport { compareIds, type ServiceEntry } from \"../repo/entries.js\";\nimport { SPEC_AXES, servicePathsAt } from \"../repo/paths.js\";\nimport type { CapabilityVocabulary } from \"./capabilities.js\";\nimport { splitRealizesEntry } from \"./realizes/join.js\";\n\n/** One requirement realizing one capability, located exactly. */\nexport interface CapabilityRealization {\n service: string;\n file: \"spec.md\" | \"arch.spec.md\";\n /** The requirement's `### Requirement:` name. */\n requirement: string;\n}\n\n/** One requirement OF a capability document, and what realizes it. */\nexport interface CapabilityRequirementRow {\n /** Its `Requirement-ID:`. A requirement without one is absent here — see `capabilityRequirementRows`. */\n id: string;\n /** Its `### Requirement:` heading. */\n name: string;\n /** Service requirements whose `Realizes:` line names `<capability>#<id>`. */\n realizedBy: CapabilityRealization[];\n /**\n * The ids of the declared flows that keep this promise — a `#cap-`/`#req-`\n * tagged `dynamic view` whose BOTH tags resolve. THE SECOND CORPUS, and the\n * only carrier a cross-service promise has: \"I enter a login and a password\n * and I am in\" belongs to no single service's spec, so a fleet can keep it\n * perfectly through a flow and realize it by no `Realizes:` line at all.\n *\n * Three states, all of them different answers, exactly as `requirements`\n * itself has three:\n * present and non-empty — these flows keep it;\n * present and empty — loam read the flows and none of them keeps it;\n * ABSENT — nobody looked. Either the caller did not ask\n * (`CapabilityRollupInput.keptByFlows` omitted) or\n * it asked and `architecture/` could not be read.\n * The third must never collapse into the second. \"Kept by no flow\" over a\n * project that did not parse is the vacuously-green claim the whole use-case\n * axis exists to refuse, and it is the claim a `[]` default would make.\n */\n keptBy?: string[];\n}\n\n/** One declared capability with everything the fleet says about it. */\nexport interface CapabilityRow {\n id: string;\n description?: string;\n owner?: string;\n /**\n * Service requirements realizing this capability BY EITHER JOIN, deduplicated\n * on (service, file, requirement).\n *\n * Both joins land here on purpose. `Realizes: checkout#CHK-1` realizes part of\n * `checkout` as surely as `Capability: checkout` does, and a rollup that\n * counted only the second would report a capability as realized by nobody\n * while the fleet pointed at its individual promises — which would then be\n * one spurious `capability.unrealized` per capability an author had documented\n * carefully enough to join at requirement level. The deduplication is what\n * lets a requirement carry both lines, which is the normal shape: the theme\n * and the promise are different claims.\n */\n realizedBy: CapabilityRealization[];\n /** Distinct realizing services, sorted. */\n services: string[];\n /**\n * Realizing requirements counted by their service's frontmatter status\n * (`unset` for a service nobody has marked) — the draft/verified split the\n * roadmap asks the total to answer. Counted over the DEDUPLICATED set above,\n * so a requirement writing both joins is one requirement.\n */\n statuses: Record<string, number>;\n /**\n * The capability document's own requirements, each with what realizes it.\n * Present only for a capability that HAS a document — absent and empty are\n * different answers, and only the second means \"the document declares none\".\n */\n requirements?: CapabilityRequirementRow[];\n}\n\nexport interface CapabilityRollupInput {\n services: ServiceEntry[];\n vocab: CapabilityVocabulary;\n /** The requirement reader — pass a FleetContext's readRequirements, bound. */\n read: (path: string) => Promise<Requirement[]>;\n /**\n * `<capability>#<Requirement-ID>` → the ids of the flows that keep it, from\n * `core/usecases`' `useCaseRequirementClaims`. Injected as PLAIN DATA, exactly\n * as `read` is, and for a harder reason than the memo: `core/usecases` imports\n * this package, so an import back would be a package cycle and\n * `npm run arch:check` refuses it. What travels is a Map of strings, which\n * carries no dependency in either direction.\n *\n * OMIT IT to answer \"nobody looked\": every `keptBy` above is then absent\n * rather than empty, which is what keeps a caller that never asked about\n * flows — `explore`, the context pack, `validate`'s own rollup — from\n * publishing a `[]` that reads as \"no flow keeps this\". A caller that DID look\n * and found nothing passes an empty Map; a caller that looked and could not\n * see must omit it and say so in its own words (`loam list` prints the note\n * and carries `useCases.unreadable`).\n */\n keptByFlows?: ReadonlyMap<string, readonly string[]>;\n}\n\n/**\n * Walk every service's spec.md and arch.spec.md (existsSync-guarded, exactly\n * like permissionFindings: an absent OPTIONAL artifact must never take a whole\n * `--all` run down) and join non-REMOVED requirements' `Capability:` entries to\n * the declared vocabulary. Rows exist for every DECLARED id — an unrealized\n * declaration is a row with empty realizedBy, which is what the unrealized\n * grade and the list's `0 — unrealized` marker both read.\n */\nexport async function capabilityRollup(input: CapabilityRollupInput): Promise<CapabilityRow[]> {\n const { services, vocab, read, keptByFlows } = input;\n // Rows exist only for DECLARED ids, so an empty vocabulary (absent file, or\n // present with nothing declared) can build nothing — walking every service's\n // two spec files to produce zero rows is what a vocabless 120-service fleet\n // would otherwise pay on every `list`/`explore` that asks.\n if (vocab.byId.size === 0) return [];\n const rows = new Map<string, CapabilityRow>();\n for (const id of [...vocab.byId.keys()].sort(compareIds)) {\n const decl = vocab.byId.get(id)!;\n rows.set(id, {\n id,\n ...(decl.description !== undefined ? { description: decl.description } : {}),\n ...(decl.owner !== undefined ? { owner: decl.owner } : {}),\n realizedBy: [],\n services: [],\n statuses: {},\n });\n }\n // The documents, before the services, because a `Realizes:` entry can only\n // land on a requirement row that already exists.\n for (const doc of vocab.tree.docs) {\n const row = rows.get(doc.id);\n if (row === undefined) continue; // unreachable: the tree is one half of byId\n row.requirements = capabilityRequirementRows(await read(doc.spec));\n }\n\n // Realizations are collected as a SET per row before they become a list, so\n // the two joins cannot count one requirement twice — see `CapabilityRow.realizedBy`.\n const claimed = new Map<CapabilityRow, Map<string, CapabilityRealization>>();\n for (const entry of services) {\n const paths = servicePathsAt(entry.dir);\n for (const axis of SPEC_AXES) {\n const path = paths[axis.key];\n if (!existsSync(path)) continue;\n for (const r of await read(path)) {\n if (r.kind === \"REMOVED\") continue;\n const where: CapabilityRealization = { service: entry.id, file: axis.file, requirement: r.name };\n for (const capability of r.capabilities) claim(rows, claimed, capability, where);\n for (const target of r.realizes.map(splitRealizesEntry)) {\n if (target === null) continue; // malformed — capability.realizes-unknown's business\n claim(rows, claimed, target.capability, where);\n // The requirement-level half. An entry naming an id the document does\n // not declare lands nowhere, which is exactly right: it is already\n // `capability.realizes-unknown`, and inventing a row for it here would\n // make a typo look like a promise somebody wrote.\n rows\n .get(target.capability)\n ?.requirements?.find((req) => req.id === target.requirement)\n ?.realizedBy.push(where);\n }\n }\n }\n }\n\n for (const row of rows.values()) {\n row.realizedBy = [...(claimed.get(row)?.values() ?? [])].sort(compareRealizations);\n row.services = [...new Set(row.realizedBy.map((r) => r.service))].sort(compareIds);\n const byStatus: Record<string, number> = {};\n for (const r of row.realizedBy) {\n const status = statusOf(services, r.service);\n byStatus[status] = (byStatus[status] ?? 0) + 1;\n }\n row.statuses = Object.fromEntries(Object.entries(byStatus).sort(([a], [b]) => compareIds(a, b)));\n for (const req of row.requirements ?? []) {\n req.realizedBy.sort(compareRealizations);\n // Copied out of the injected map rather than aliased into the row: the row\n // is a mutable value a caller may sort or splice, and handing it the\n // caller's own array would let one consumer's edit rewrite the answer the\n // next one reads. Already sorted upstream — copied, never re-sorted, so\n // there is exactly one statement of the order.\n if (keptByFlows !== undefined) req.keptBy = [...(keptByFlows.get(`${row.id}#${req.id}`) ?? [])];\n }\n }\n return [...rows.values()];\n}\n\n/** Deterministic realization order — the diff-stable rule this module states for its rows. */\nfunction compareRealizations(a: CapabilityRealization, b: CapabilityRealization): number {\n return (\n compareIds(a.service, b.service) ||\n a.file.localeCompare(b.file) ||\n compareIds(a.requirement, b.requirement)\n );\n}\n\n/** The service's declared frontmatter status, or `unset` for one nobody has marked. */\nfunction statusOf(services: ServiceEntry[], id: string): string {\n return services.find((s) => s.id === id)?.status ?? \"unset\";\n}\n\n/** Record one realization of one capability, deduplicated on where it came from. */\nfunction claim(\n rows: Map<string, CapabilityRow>,\n claimed: Map<CapabilityRow, Map<string, CapabilityRealization>>,\n capability: string,\n where: CapabilityRealization,\n): void {\n const row = rows.get(capability);\n if (row === undefined) return; // undeclared — capability.unknown's business, not the rollup's\n const seen = claimed.get(row) ?? new Map<string, CapabilityRealization>();\n seen.set(`${where.service}\\0${where.file}\\0${where.requirement}`, where);\n claimed.set(row, seen);\n}\n\n/**\n * A capability document's requirements as rows, keyed by their stable id.\n *\n * A requirement WITHOUT a `Requirement-ID:` is dropped rather than keyed by its\n * heading, and the drop is the honest answer rather than a shortcut: nothing\n * can address it — `Realizes:` names an id — so a row for it would be a promise\n * reported as unrealized that no author could ever mark realized.\n * `capability.requirement-unidentified` is the finding that names it, and it is\n * an ERROR, so the state does not persist quietly.\n *\n * A duplicate id keeps the FIRST, matching `requirementIdProblems`' own\n * `duplicate` grade: the document already earns an error for it, and picking a\n * winner here is only about not producing two identical rows.\n */\nexport function capabilityRequirementRows(reqs: Requirement[]): CapabilityRequirementRow[] {\n const rows = new Map<string, CapabilityRequirementRow>();\n for (const r of reqs) {\n if (r.kind === \"REMOVED\" || r.id === undefined || rows.has(r.id)) continue;\n rows.set(r.id, { id: r.id, name: r.name, realizedBy: [] });\n }\n return [...rows.values()].sort((a, b) => compareIds(a.id, b.id));\n}\n\n/** The declared ids something realizes — the set the unrealized grade is taken against. */\nexport function usedCapabilities(rows: CapabilityRow[]): Set<string> {\n return new Set(rows.filter(isRealized).map((row) => row.id));\n}\n\n/**\n * Does anything in the fleet realize this capability — by EITHER corpus?\n *\n * A flow keeping one of a capability's promises realizes part of that\n * capability as surely as a `Capability:` line does, and the case is not\n * hypothetical: a capability whose promises are all cross-service can be kept\n * perfectly by `#cap-`/`#req-` tagged flows and named by no service requirement\n * at all. Counting only the service corpus told such a fleet its capability was\n * \"a promise nobody implemented or a word nobody adopted\" while the fleet's own\n * use cases said otherwise — one warning, contradicted by loam's own answer two\n * lines further down the same report.\n *\n * ASYMMETRIC WITH `capability.requirement-unrealized`, on purpose. That grade\n * suspends entirely when the flows cannot be read, because a promise crossing\n * services often has NO possible realizer but a flow — unreadable flows remove\n * the only evidence there was. A capability has independent primary evidence in\n * the `Capability:`/`Realizes:` lines, so unreadable flows leave it answering\n * from less rather than from nothing, and going silent would lose the warning\n * for a fleet that has a vocabulary and no fleet map at all — a real and legal\n * shape, and one where the whole point of the code is to be heard.\n */\nfunction isRealized(row: CapabilityRow): boolean {\n if (row.realizedBy.length > 0) return true;\n return (row.requirements ?? []).some((req) => (req.keptBy?.length ?? 0) > 0);\n}\n\n/**\n * The requested ids the rollup cannot seed from: undeclared, or declared but\n * realized by nothing. One list on purpose — explore's contract is 'every miss\n * is a field', and splitting the two miss kinds waits for a consumer to ask.\n */\nexport function unresolvedCapabilities(rows: CapabilityRow[], requested: string[]): string[] {\n const realized = usedCapabilities(rows);\n return requested.filter((id) => !realized.has(id));\n}\n"]}
|
|
1
|
+
{"version":3,"file":"rollup.js","sourceRoot":"","sources":["../../../src/core/capabilities/rollup.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * The fleet total over the capability join, built once and reused by the fleet\n * grade, `loam list capabilities` and `loam explore --capability` — one walk,\n * so the unrealized warning and the rollup a reader diffs can never disagree\n * about what \"realized\" means.\n *\n * Deterministic and diff-stable by construction: rows sorted with compareIds,\n * realizedBy sorted by (service, file, requirement), statuses' keys sorted —\n * no timestamps, no absolute paths, no readdir-order dependence. The reader is\n * INJECTED (`read`) rather than a FleetContext, because fleet-context.ts\n * imports this package for its memo and an import back would be a package\n * cycle (see the memo's comment); callers pass `fleet.readRequirements` bound,\n * so under `validate --all` every parse is already cached from the service\n * targets and the rollup costs no additional per-service read.\n *\n * TWO CORPORA MEET HERE, and neither is derived from the other. A promise is\n * kept by a service requirement's `Realizes:` line (walked below) or by a\n * `#cap-`/`#req-` tagged flow (injected as `keptByFlows`, because the module\n * that computes it imports this one). The rollup carries both on one row for the\n * reason it carries both `Capability:` joins on one row: `loam list capabilities`\n * and `capability.requirement-unrealized` must not be able to give different\n * answers to \"is this business promise kept\", and they can only give one answer\n * if they read one row.\n */\nimport { existsSync } from \"node:fs\";\nimport type { Requirement } from \"../document/spec.js\";\nimport { compareIds, type ServiceEntry } from \"../repo/entries.js\";\nimport { SPEC_AXES, servicePathsAt } from \"../repo/paths.js\";\nimport type { CapabilityVocabulary } from \"./capabilities.js\";\nimport { splitRealizesEntry } from \"./realizes/join.js\";\n\n/** One requirement realizing one capability, located exactly. */\nexport interface CapabilityRealization {\n service: string;\n file: \"spec.md\" | \"arch.spec.md\";\n /** The requirement's `### Requirement:` name. */\n requirement: string;\n}\n\n/** One requirement OF a capability document, and what realizes it. */\nexport interface CapabilityRequirementRow {\n /** Its `Requirement-ID:`. A requirement without one is absent here — see `capabilityRequirementRows`. */\n id: string;\n /** Its `### Requirement:` heading. */\n name: string;\n /** Service requirements whose `Realizes:` line names `<capability>#<id>`. */\n realizedBy: CapabilityRealization[];\n /**\n * The ids of the declared flows that keep this promise — a `#cap-`/`#req-`\n * tagged `dynamic view` whose BOTH tags resolve. THE SECOND CORPUS, and the\n * only carrier a cross-service promise has: \"I enter a login and a password\n * and I am in\" belongs to no single service's spec, so a fleet can keep it\n * perfectly through a flow and realize it by no `Realizes:` line at all.\n *\n * Three states, all of them different answers, exactly as `requirements`\n * itself has three:\n * present and non-empty — these flows keep it;\n * present and empty — loam read the flows and none of them keeps it;\n * ABSENT — nobody looked. Either the caller did not ask\n * (`CapabilityRollupInput.keptByFlows` omitted) or\n * it asked and `architecture/` could not be read.\n * The third must never collapse into the second. \"Kept by no flow\" over a\n * project that did not parse is the vacuously-green claim the whole use-case\n * axis exists to refuse, and it is the claim a `[]` default would make.\n */\n keptBy?: string[];\n}\n\n/** One declared capability with everything the fleet says about it. */\nexport interface CapabilityRow {\n id: string;\n description?: string;\n owner?: string;\n /**\n * Service requirements realizing this capability BY EITHER JOIN, deduplicated\n * on (service, file, requirement).\n *\n * Both joins land here on purpose. `Realizes: checkout#CHK-1` realizes part of\n * `checkout` as surely as `Capability: checkout` does, and a rollup that\n * counted only the second would report a capability as realized by nobody\n * while the fleet pointed at its individual promises — which would then be\n * one spurious `capability.unrealized` per capability an author had documented\n * carefully enough to join at requirement level. The deduplication is what\n * lets a requirement carry both lines, which is the normal shape: the theme\n * and the promise are different claims.\n */\n realizedBy: CapabilityRealization[];\n /** Distinct realizing services, sorted. */\n services: string[];\n /**\n * Realizing requirements counted by their service's frontmatter status\n * (`unset` for a service nobody has marked) — the draft/verified split the\n * roadmap asks the total to answer. Counted over the DEDUPLICATED set above,\n * so a requirement writing both joins is one requirement.\n */\n statuses: Record<string, number>;\n /**\n * The capability document's own requirements, each with what realizes it.\n * Present only for a capability that HAS a document — absent and empty are\n * different answers, and only the second means \"the document declares none\".\n */\n requirements?: CapabilityRequirementRow[];\n}\n\nexport interface CapabilityRollupInput {\n services: ServiceEntry[];\n vocab: CapabilityVocabulary;\n /** The requirement reader — pass a FleetContext's readRequirements, bound. */\n read: (path: string) => Promise<Requirement[]>;\n /**\n * `<capability>#<Requirement-ID>` → the ids of the flows that keep it, from\n * `core/usecases`' `useCaseRequirementClaims`. Injected as PLAIN DATA, exactly\n * as `read` is, and for a harder reason than the memo: `core/usecases` imports\n * this package, so an import back would be a package cycle and\n * `npm run arch:check` refuses it. What travels is a Map of strings, which\n * carries no dependency in either direction.\n *\n * OMIT IT to answer \"nobody looked\": every `keptBy` above is then absent\n * rather than empty, which is what keeps a caller that never asked about\n * flows — `explore`, the context pack, `validate`'s own rollup — from\n * publishing a `[]` that reads as \"no flow keeps this\". A caller that DID look\n * and found nothing passes an empty Map; a caller that looked and could not\n * see must omit it and say so in its own words (`loam list` prints the note\n * and carries `useCases.unreadable`).\n */\n keptByFlows?: ReadonlyMap<string, readonly string[]>;\n}\n\n/**\n * Walk every service's spec.md and arch.spec.md (existsSync-guarded, exactly\n * like permissionFindings: an absent OPTIONAL artifact must never take a whole\n * `--all` run down) and join non-REMOVED requirements' `Capability:` entries to\n * the declared vocabulary. Rows exist for every DECLARED id — an unrealized\n * declaration is a row with empty realizedBy, which is what the unrealized\n * grade and the list's `0 — unrealized` marker both read.\n */\nexport async function capabilityRollup(input: CapabilityRollupInput): Promise<CapabilityRow[]> {\n const { services, vocab, read, keptByFlows } = input;\n // Rows exist only for DECLARED ids, so an empty vocabulary (absent file, or\n // present with nothing declared) can build nothing — walking every service's\n // two spec files to produce zero rows is what a vocabless 120-service fleet\n // would otherwise pay on every `list`/`explore` that asks.\n if (vocab.byId.size === 0) return [];\n const rows = new Map<string, CapabilityRow>();\n for (const id of [...vocab.byId.keys()].sort(compareIds)) {\n const decl = vocab.byId.get(id)!;\n rows.set(id, {\n id,\n ...(decl.description !== undefined ? { description: decl.description } : {}),\n ...(decl.owner !== undefined ? { owner: decl.owner } : {}),\n realizedBy: [],\n services: [],\n statuses: {},\n });\n }\n // The documents, before the services, because a `Realizes:` entry can only\n // land on a requirement row that already exists.\n for (const doc of vocab.tree.docs) {\n const row = rows.get(doc.id);\n if (row === undefined) continue; // unreachable: the tree is one half of byId\n row.requirements = capabilityRequirementRows(await read(doc.spec));\n }\n\n // Realizations are collected as a SET per row before they become a list, so\n // the two joins cannot count one requirement twice — see `CapabilityRow.realizedBy`.\n const claimed = new Map<CapabilityRow, Map<string, CapabilityRealization>>();\n for (const entry of services) {\n const paths = servicePathsAt(entry.dir);\n for (const axis of SPEC_AXES) {\n const path = paths[axis.key];\n if (!existsSync(path)) continue;\n for (const r of await read(path)) {\n if (r.kind === \"REMOVED\") continue;\n const where: CapabilityRealization = { service: entry.id, file: axis.file, requirement: r.name };\n for (const capability of r.capabilities) claim(rows, claimed, capability, where);\n for (const target of r.realizes.map(splitRealizesEntry)) {\n if (target === null) continue; // malformed — capability.realizes-unknown's business\n // The capability is claimed BEFORE asking whether the requirement id\n // resolves, and that is deliberate: an entry naming an undeclared id\n // already earns `capability.realizes-unknown`, and letting it also\n // leave its capability `capability.unrealized` would be two findings\n // for one mistake — the second pointing at the analyst's document\n // rather than at the typo.\n claim(rows, claimed, target.capability, where);\n // The requirement-level half. An entry naming an id the document does\n // not declare lands nowhere, which is exactly right: it is already\n // `capability.realizes-unknown`, and inventing a row for it here would\n // make a typo look like a promise somebody wrote.\n rows\n .get(target.capability)\n ?.requirements?.find((req) => req.id === target.requirement)\n ?.realizedBy.push(where);\n }\n }\n }\n }\n\n for (const row of rows.values()) {\n row.realizedBy = [...(claimed.get(row)?.values() ?? [])].sort(compareRealizations);\n row.services = [...new Set(row.realizedBy.map((r) => r.service))].sort(compareIds);\n const byStatus: Record<string, number> = {};\n for (const r of row.realizedBy) {\n const status = statusOf(services, r.service);\n byStatus[status] = (byStatus[status] ?? 0) + 1;\n }\n row.statuses = Object.fromEntries(Object.entries(byStatus).sort(([a], [b]) => compareIds(a, b)));\n for (const req of row.requirements ?? []) {\n req.realizedBy.sort(compareRealizations);\n // Copied out of the injected map rather than aliased into the row: the row\n // is a mutable value a caller may sort or splice, and handing it the\n // caller's own array would let one consumer's edit rewrite the answer the\n // next one reads. Already sorted upstream — copied, never re-sorted, so\n // there is exactly one statement of the order.\n if (keptByFlows !== undefined) req.keptBy = [...(keptByFlows.get(`${row.id}#${req.id}`) ?? [])];\n }\n }\n return [...rows.values()];\n}\n\n/** Deterministic realization order — the diff-stable rule this module states for its rows. */\nfunction compareRealizations(a: CapabilityRealization, b: CapabilityRealization): number {\n return (\n compareIds(a.service, b.service) ||\n a.file.localeCompare(b.file) ||\n compareIds(a.requirement, b.requirement)\n );\n}\n\n/** The service's declared frontmatter status, or `unset` for one nobody has marked. */\nfunction statusOf(services: ServiceEntry[], id: string): string {\n return services.find((s) => s.id === id)?.status ?? \"unset\";\n}\n\n/** Record one realization of one capability, deduplicated on where it came from. */\nfunction claim(\n rows: Map<string, CapabilityRow>,\n claimed: Map<CapabilityRow, Map<string, CapabilityRealization>>,\n capability: string,\n where: CapabilityRealization,\n): void {\n const row = rows.get(capability);\n if (row === undefined) return; // undeclared — capability.unknown's business, not the rollup's\n const seen = claimed.get(row) ?? new Map<string, CapabilityRealization>();\n seen.set(`${where.service}\\0${where.file}\\0${where.requirement}`, where);\n claimed.set(row, seen);\n}\n\n/**\n * A capability document's requirements as rows, keyed by their stable id.\n *\n * A requirement WITHOUT a `Requirement-ID:` is dropped rather than keyed by its\n * heading, and the drop is the honest answer rather than a shortcut: nothing\n * can address it — `Realizes:` names an id — so a row for it would be a promise\n * reported as unrealized that no author could ever mark realized.\n * `capability.requirement-unidentified` is the finding that names it, and it is\n * an ERROR, so the state does not persist quietly.\n *\n * A duplicate id keeps the FIRST, matching `requirementIdProblems`' own\n * `duplicate` grade: the document already earns an error for it, and picking a\n * winner here is only about not producing two identical rows.\n */\nexport function capabilityRequirementRows(reqs: Requirement[]): CapabilityRequirementRow[] {\n const rows = new Map<string, CapabilityRequirementRow>();\n for (const r of reqs) {\n if (r.kind === \"REMOVED\" || r.id === undefined || rows.has(r.id)) continue;\n rows.set(r.id, { id: r.id, name: r.name, realizedBy: [] });\n }\n return [...rows.values()].sort((a, b) => compareIds(a.id, b.id));\n}\n\n/** The declared ids something realizes — the set the unrealized grade is taken against. */\nexport function usedCapabilities(rows: CapabilityRow[]): Set<string> {\n return new Set(rows.filter(isRealized).map((row) => row.id));\n}\n\n/**\n * Does anything in the fleet realize this capability — by EITHER corpus?\n *\n * A flow keeping one of a capability's promises realizes part of that\n * capability as surely as a `Capability:` line does, and the case is not\n * hypothetical: a capability whose promises are all cross-service can be kept\n * perfectly by `#cap-`/`#req-` tagged flows and named by no service requirement\n * at all. Counting only the service corpus told such a fleet its capability was\n * \"a promise nobody implemented or a word nobody adopted\" while the fleet's own\n * use cases said otherwise — one warning, contradicted by loam's own answer two\n * lines further down the same report.\n *\n * ASYMMETRIC WITH `capability.requirement-unrealized`, on purpose. That grade\n * suspends entirely when the flows cannot be read, because a promise crossing\n * services often has NO possible realizer but a flow — unreadable flows remove\n * the only evidence there was. A capability has independent primary evidence in\n * the `Capability:`/`Realizes:` lines, so unreadable flows leave it answering\n * from less rather than from nothing, and going silent would lose the warning\n * for a fleet that has a vocabulary and no fleet map at all — a real and legal\n * shape, and one where the whole point of the code is to be heard.\n */\nfunction isRealized(row: CapabilityRow): boolean {\n if (row.realizedBy.length > 0) return true;\n return (row.requirements ?? []).some((req) => (req.keptBy?.length ?? 0) > 0);\n}\n\n/**\n * The requested ids the rollup cannot seed from: undeclared, or declared but\n * realized by nothing. One list on purpose — explore's contract is 'every miss\n * is a field', and splitting the two miss kinds waits for a consumer to ask.\n */\nexport function unresolvedCapabilities(rows: CapabilityRow[], requested: string[]): string[] {\n const realized = usedCapabilities(rows);\n return requested.filter((id) => !realized.has(id));\n}\n"]}
|
|
@@ -33,12 +33,12 @@
|
|
|
33
33
|
* the reason `./rollup.ts` states: this package is imported by
|
|
34
34
|
* `core/fleet-context.ts` and may never import it back.
|
|
35
35
|
*/
|
|
36
|
-
import { existsSync
|
|
36
|
+
import { existsSync } from "node:fs";
|
|
37
37
|
import { readdir } from "node:fs/promises";
|
|
38
38
|
import { join } from "node:path";
|
|
39
39
|
import { compareIds } from "../repo/entries.js";
|
|
40
40
|
import { capabilityDocPathsAt } from "../repo/authored/paths.js";
|
|
41
|
-
import { entryIs } from "../repo/tree/fs.js";
|
|
41
|
+
import { entryIs, markDescended, subdirNames } from "../repo/tree/fs.js";
|
|
42
42
|
/**
|
|
43
43
|
* Read the tree under `root` (`capabilityDocsDir(docsDir)`).
|
|
44
44
|
*
|
|
@@ -54,7 +54,9 @@ export async function readCapabilityTree(root) {
|
|
|
54
54
|
if (!tree.present)
|
|
55
55
|
return tree;
|
|
56
56
|
const state = { tree, visited: new Set() };
|
|
57
|
-
|
|
57
|
+
// The same guard `core/repo/tree/walk.ts` carries (`markDescended`), for
|
|
58
|
+
// the same reason: `entryIs` follows symlinks on purpose.
|
|
59
|
+
if (markDescended(root, state.visited)) {
|
|
58
60
|
for (const name of await subdirNames(root)) {
|
|
59
61
|
await visit({ dir: join(root, name), chain: [name] }, state);
|
|
60
62
|
}
|
|
@@ -63,30 +65,6 @@ export async function readCapabilityTree(root) {
|
|
|
63
65
|
tree.undocumented.sort(compareIds);
|
|
64
66
|
return tree;
|
|
65
67
|
}
|
|
66
|
-
/**
|
|
67
|
-
* Mark a directory as descended-into; false when it (or another name for it)
|
|
68
|
-
* already was. The same guard `core/repo/tree/walk.ts` carries and for the same
|
|
69
|
-
* reason: `entryIs` follows symlinks on purpose, and a followed cycle without a
|
|
70
|
-
* visited set recurses until the stack ends.
|
|
71
|
-
*/
|
|
72
|
-
function descended(dir, state) {
|
|
73
|
-
let real;
|
|
74
|
-
try {
|
|
75
|
-
real = realpathSync(dir);
|
|
76
|
-
}
|
|
77
|
-
catch {
|
|
78
|
-
return false;
|
|
79
|
-
}
|
|
80
|
-
if (state.visited.has(real))
|
|
81
|
-
return false;
|
|
82
|
-
state.visited.add(real);
|
|
83
|
-
return true;
|
|
84
|
-
}
|
|
85
|
-
/** Sorted non-dot subdirectory names — the walk's deterministic child order. */
|
|
86
|
-
async function subdirNames(dir) {
|
|
87
|
-
const entries = await readdir(dir, { withFileTypes: true });
|
|
88
|
-
return entries.filter((e) => !e.name.startsWith(".") && entryIs(dir, e, "dir")).map((e) => e.name).sort();
|
|
89
|
-
}
|
|
90
68
|
/**
|
|
91
69
|
* Classify one directory and walk what is inside it. Returns whether a
|
|
92
70
|
* capability was found at or beneath it — the only thing a caller needs, since
|
|
@@ -105,7 +83,7 @@ async function visit(spot, state) {
|
|
|
105
83
|
if (documented)
|
|
106
84
|
state.tree.docs.push({ id: spot.chain.join("/"), dir: spot.dir, spec: paths.spec });
|
|
107
85
|
let beneath = false;
|
|
108
|
-
if (
|
|
86
|
+
if (markDescended(spot.dir, state.visited)) {
|
|
109
87
|
const dirs = entries.filter((e) => entryIs(spot.dir, e, "dir")).map((e) => e.name).sort();
|
|
110
88
|
for (const child of dirs) {
|
|
111
89
|
// Sequential, not Promise.all: the visited set is shared mutable state and
|