@ybotok/loam 0.2.0-alpha.1 → 0.2.0-alpha.13
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +3448 -0
- package/COMPARISON.md +91 -3
- package/CONTRIBUTING.md +37 -0
- package/MIGRATING-from-OpenSpec.md +47 -23
- package/QUICKSTART.md +189 -12
- package/README.md +274 -65
- package/ROADMAP.md +572 -31
- package/SCHEMA.md +1709 -155
- package/WORKFLOW.md +298 -72
- package/dist/cli.js +26 -8
- package/dist/cli.js.map +1 -1
- package/dist/commands/adopt/adopt.js +5 -10
- package/dist/commands/adopt/adopt.js.map +1 -1
- package/dist/commands/adopt/render.js +50 -9
- package/dist/commands/adopt/render.js.map +1 -1
- package/dist/commands/archive/archive.js +16 -19
- package/dist/commands/archive/archive.js.map +1 -1
- package/dist/commands/archive/closing.js +42 -0
- package/dist/commands/archive/closing.js.map +1 -0
- package/dist/commands/archive/plan/contracts/asyncapi.js +36 -42
- package/dist/commands/archive/plan/contracts/asyncapi.js.map +1 -1
- package/dist/commands/archive/plan/contracts/consumers.js +56 -0
- package/dist/commands/archive/plan/contracts/consumers.js.map +1 -0
- package/dist/commands/archive/plan/contracts/openapi.js +77 -43
- package/dist/commands/archive/plan/contracts/openapi.js.map +1 -1
- package/dist/commands/archive/plan/contracts/verdict-lines.js +103 -0
- package/dist/commands/archive/plan/contracts/verdict-lines.js.map +1 -0
- package/dist/commands/archive/plan/document/head.js +151 -0
- package/dist/commands/archive/plan/document/head.js.map +1 -0
- package/dist/commands/archive/plan/document/links.js +284 -0
- package/dist/commands/archive/plan/document/links.js.map +1 -0
- package/dist/commands/archive/plan/document/run.js +136 -0
- package/dist/commands/archive/plan/document/run.js.map +1 -0
- package/dist/commands/archive/plan/gate.js +21 -19
- package/dist/commands/archive/plan/gate.js.map +1 -1
- package/dist/commands/archive/plan/landscape.js +167 -30
- package/dist/commands/archive/plan/landscape.js.map +1 -1
- package/dist/commands/archive/plan/model/extending.js +85 -0
- package/dist/commands/archive/plan/model/extending.js.map +1 -0
- package/dist/commands/archive/plan/refusal.js +14 -19
- package/dist/commands/archive/plan/refusal.js.map +1 -1
- package/dist/commands/archive/plan/specs.js +137 -56
- package/dist/commands/archive/plan/specs.js.map +1 -1
- package/dist/commands/archive/plan/state.js +3 -0
- package/dist/commands/archive/plan/state.js.map +1 -1
- package/dist/commands/archive/run.js +134 -30
- package/dist/commands/archive/run.js.map +1 -1
- package/dist/commands/archive/shipped.js +49 -0
- package/dist/commands/archive/shipped.js.map +1 -0
- package/dist/commands/context/context.js +85 -18
- package/dist/commands/context/context.js.map +1 -1
- package/dist/commands/context/print.js +29 -7
- package/dist/commands/context/print.js.map +1 -1
- package/dist/commands/delta/delta.js +102 -14
- package/dist/commands/delta/delta.js.map +1 -1
- package/dist/commands/delta/print.js +17 -1
- package/dist/commands/delta/print.js.map +1 -1
- package/dist/commands/dependencies.js +3 -7
- package/dist/commands/dependencies.js.map +1 -1
- package/dist/commands/diff/diff.js +20 -27
- package/dist/commands/diff/diff.js.map +1 -1
- package/dist/commands/doctor.js +60 -1
- package/dist/commands/doctor.js.map +1 -1
- package/dist/commands/explain/explain.js +89 -13
- package/dist/commands/explain/explain.js.map +1 -1
- package/dist/commands/explore.js +16 -10
- package/dist/commands/explore.js.map +1 -1
- package/dist/commands/gate/gate.js +45 -8
- package/dist/commands/gate/gate.js.map +1 -1
- package/dist/commands/gate/print.js +15 -3
- package/dist/commands/gate/print.js.map +1 -1
- package/dist/commands/gherkin/commit.js +13 -54
- package/dist/commands/gherkin/commit.js.map +1 -1
- package/dist/commands/gherkin/gherkin.js +36 -20
- package/dist/commands/gherkin/gherkin.js.map +1 -1
- package/dist/commands/gherkin/reconcile.js +10 -13
- package/dist/commands/gherkin/reconcile.js.map +1 -1
- package/dist/commands/init/first-hour.js +44 -0
- package/dist/commands/init/first-hour.js.map +1 -1
- package/dist/commands/init/init.js +18 -22
- package/dist/commands/init/init.js.map +1 -1
- package/dist/commands/list/campaign/campaign.js +4 -6
- package/dist/commands/list/campaign/campaign.js.map +1 -1
- package/dist/commands/list/json.js +5 -7
- package/dist/commands/list/json.js.map +1 -1
- package/dist/commands/list/list.js +44 -29
- package/dist/commands/list/list.js.map +1 -1
- package/dist/commands/list/print.js +52 -9
- package/dist/commands/list/print.js.map +1 -1
- package/dist/commands/list/suites/suites.js +140 -0
- package/dist/commands/list/suites/suites.js.map +1 -0
- package/dist/commands/list/views.js +37 -34
- package/dist/commands/list/views.js.map +1 -1
- package/dist/commands/mcp/dispatch.js +91 -36
- package/dist/commands/mcp/dispatch.js.map +1 -1
- package/dist/commands/mcp/mcp.js +56 -15
- package/dist/commands/mcp/mcp.js.map +1 -1
- package/dist/commands/mcp/serve.js.map +1 -1
- package/dist/commands/migrate-openspec/materialize/active.js +27 -22
- package/dist/commands/migrate-openspec/materialize/active.js.map +1 -1
- package/dist/commands/migrate-openspec/materialize/stage.js +4 -3
- package/dist/commands/migrate-openspec/materialize/stage.js.map +1 -1
- package/dist/commands/migrate-openspec/materialize/target.js +2 -2
- package/dist/commands/migrate-openspec/materialize/target.js.map +1 -1
- package/dist/commands/migrate-openspec/openspec/decisions.js +9 -14
- package/dist/commands/migrate-openspec/openspec/decisions.js.map +1 -1
- package/dist/commands/migrate-openspec/openspec/error.js +7 -8
- package/dist/commands/migrate-openspec/openspec/error.js.map +1 -1
- package/dist/commands/migrate-openspec/openspec/mapping.js +1 -9
- package/dist/commands/migrate-openspec/openspec/mapping.js.map +1 -1
- package/dist/commands/migrate-openspec/openspec/paths.js +3 -6
- package/dist/commands/migrate-openspec/openspec/paths.js.map +1 -1
- package/dist/commands/new/capability-template.js +118 -0
- package/dist/commands/new/capability-template.js.map +1 -0
- package/dist/commands/new/commit.js +8 -22
- package/dist/commands/new/commit.js.map +1 -1
- package/dist/commands/new/modifies.js +206 -0
- package/dist/commands/new/modifies.js.map +1 -0
- package/dist/commands/new/new.js +59 -14
- package/dist/commands/new/new.js.map +1 -1
- package/dist/commands/new/templates.js +48 -124
- package/dist/commands/new/templates.js.map +1 -1
- package/dist/commands/open.js +3 -7
- package/dist/commands/open.js.map +1 -1
- package/dist/commands/policy/gate.js +27 -0
- package/dist/commands/policy/gate.js.map +1 -1
- package/dist/commands/rebase/plan.js +2 -2
- package/dist/commands/rebase/plan.js.map +1 -1
- package/dist/commands/rebase/rebase.js +60 -35
- package/dist/commands/rebase/rebase.js.map +1 -1
- package/dist/commands/seed/commit.js +55 -26
- package/dist/commands/seed/commit.js.map +1 -1
- package/dist/commands/seed/plan.js +158 -13
- package/dist/commands/seed/plan.js.map +1 -1
- package/dist/commands/seed/seed.js +13 -11
- package/dist/commands/seed/seed.js.map +1 -1
- package/dist/commands/show/feature.js +9 -5
- package/dist/commands/show/feature.js.map +1 -1
- package/dist/commands/show/introduced.js +157 -0
- package/dist/commands/show/introduced.js.map +1 -0
- package/dist/commands/show/marks.js +35 -6
- package/dist/commands/show/marks.js.map +1 -1
- package/dist/commands/show/service.js +41 -33
- package/dist/commands/show/service.js.map +1 -1
- package/dist/commands/show/show.js +17 -7
- package/dist/commands/show/show.js.map +1 -1
- package/dist/commands/status/fleet-next.js +90 -0
- package/dist/commands/status/fleet-next.js.map +1 -0
- package/dist/commands/status/json.js +2 -0
- package/dist/commands/status/json.js.map +1 -1
- package/dist/commands/status/print.js +89 -37
- package/dist/commands/status/print.js.map +1 -1
- package/dist/commands/status/status.js +3 -7
- package/dist/commands/status/status.js.map +1 -1
- package/dist/commands/steps/steps.js +103 -23
- package/dist/commands/steps/steps.js.map +1 -1
- package/dist/commands/subsystem/edit.js +7 -7
- package/dist/commands/subsystem/edit.js.map +1 -1
- package/dist/commands/subsystem/move.js +106 -46
- package/dist/commands/subsystem/move.js.map +1 -1
- package/dist/commands/subsystem/query.js +2 -4
- package/dist/commands/subsystem/query.js.map +1 -1
- package/dist/commands/subsystem/subsystem.js +4 -7
- package/dist/commands/subsystem/subsystem.js.map +1 -1
- package/dist/commands/subsystem/sync.js +289 -74
- package/dist/commands/subsystem/sync.js.map +1 -1
- package/dist/commands/subsystem/txn/exclude.js +132 -0
- package/dist/commands/subsystem/txn/exclude.js.map +1 -0
- package/dist/commands/subsystem/txn/links.js +128 -0
- package/dist/commands/subsystem/txn/links.js.map +1 -0
- package/dist/commands/subsystem/txn/report.js +174 -0
- package/dist/commands/subsystem/txn/report.js.map +1 -0
- package/dist/commands/subsystem/txn/txn.js +147 -45
- package/dist/commands/subsystem/txn/txn.js.map +1 -1
- package/dist/commands/subsystem/txn/views.js +200 -21
- package/dist/commands/subsystem/txn/views.js.map +1 -1
- package/dist/commands/tasks/actions.js +107 -0
- package/dist/commands/tasks/actions.js.map +1 -0
- package/dist/commands/tasks/render.js +253 -0
- package/dist/commands/tasks/render.js.map +1 -0
- package/dist/commands/tasks/tasks.js +119 -0
- package/dist/commands/tasks/tasks.js.map +1 -0
- package/dist/commands/unarchive/manifest.js +42 -26
- package/dist/commands/unarchive/manifest.js.map +1 -1
- package/dist/commands/unarchive/restore.js +83 -33
- package/dist/commands/unarchive/restore.js.map +1 -1
- package/dist/commands/unarchive/unarchive.js +22 -30
- package/dist/commands/unarchive/unarchive.js.map +1 -1
- package/dist/commands/validate/arch-coverage.js +78 -27
- package/dist/commands/validate/arch-coverage.js.map +1 -1
- package/dist/commands/validate/checks/fleet-shape.js +34 -126
- package/dist/commands/validate/checks/fleet-shape.js.map +1 -1
- package/dist/commands/validate/checks/health-deps.js +9 -6
- package/dist/commands/validate/checks/health-deps.js.map +1 -1
- package/dist/commands/validate/checks/introduced.js +102 -0
- package/dist/commands/validate/checks/introduced.js.map +1 -0
- package/dist/commands/validate/checks/requirements.js +18 -8
- package/dist/commands/validate/checks/requirements.js.map +1 -1
- package/dist/commands/validate/checks/sources.js +11 -2
- package/dist/commands/validate/checks/sources.js.map +1 -1
- package/dist/commands/validate/coverage/covers-after.js +105 -0
- package/dist/commands/validate/coverage/covers-after.js.map +1 -0
- package/dist/commands/validate/feature.js +26 -20
- package/dist/commands/validate/feature.js.map +1 -1
- package/dist/commands/validate/fleet/census.js +37 -13
- package/dist/commands/validate/fleet/census.js.map +1 -1
- package/dist/commands/validate/fleet/landscape.js +144 -126
- package/dist/commands/validate/fleet/landscape.js.map +1 -1
- package/dist/commands/validate/fleet/load.js +66 -0
- package/dist/commands/validate/fleet/load.js.map +1 -1
- package/dist/commands/validate/fleet/map/attest.js +98 -0
- package/dist/commands/validate/fleet/map/attest.js.map +1 -0
- package/dist/commands/validate/fleet/map/bindings.js +107 -0
- package/dist/commands/validate/fleet/map/bindings.js.map +1 -0
- package/dist/commands/validate/fleet/map/consumers.js +244 -0
- package/dist/commands/validate/fleet/map/consumers.js.map +1 -0
- package/dist/commands/validate/fleet/map/isolation.js +116 -0
- package/dist/commands/validate/fleet/map/isolation.js.map +1 -0
- package/dist/commands/validate/fleet/map/usecases.js +54 -0
- package/dist/commands/validate/fleet/map/usecases.js.map +1 -0
- package/dist/commands/validate/fleet/obligations.js +44 -14
- package/dist/commands/validate/fleet/obligations.js.map +1 -1
- package/dist/commands/validate/fleet/scorecard/print.js +10 -2
- package/dist/commands/validate/fleet/scorecard/print.js.map +1 -1
- package/dist/commands/validate/fleet/scorecard/scorecard.js +17 -11
- package/dist/commands/validate/fleet/scorecard/scorecard.js.map +1 -1
- package/dist/commands/validate/fleet/usecases/place.js.map +1 -1
- package/dist/commands/validate/fleet/views/fleet-project.js +217 -0
- package/dist/commands/validate/fleet/views/fleet-project.js.map +1 -0
- package/dist/commands/validate/fleet/views/ids.js +50 -15
- package/dist/commands/validate/fleet/views/ids.js.map +1 -1
- package/dist/commands/validate/fleet/views/projects.js +278 -0
- package/dist/commands/validate/fleet/views/projects.js.map +1 -0
- package/dist/commands/validate/fleet/views/root-documents.js +83 -0
- package/dist/commands/validate/fleet/views/root-documents.js.map +1 -0
- package/dist/commands/validate/fleet/views/stale.js +40 -47
- package/dist/commands/validate/fleet/views/stale.js.map +1 -1
- package/dist/commands/validate/links/corpus.js +1 -4
- package/dist/commands/validate/links/corpus.js.map +1 -1
- package/dist/commands/validate/links/glossary.js.map +1 -1
- package/dist/commands/validate/render/repeats.js +63 -0
- package/dist/commands/validate/render/repeats.js.map +1 -0
- package/dist/commands/validate/render/rollups.js +115 -0
- package/dist/commands/validate/render/rollups.js.map +1 -0
- package/dist/commands/validate/render/text.js +199 -0
- package/dist/commands/validate/render/text.js.map +1 -0
- package/dist/commands/validate/report.js +36 -160
- package/dist/commands/validate/report.js.map +1 -1
- package/dist/commands/validate/scope.js +113 -0
- package/dist/commands/validate/scope.js.map +1 -0
- package/dist/commands/validate/service/api.js +21 -107
- package/dist/commands/validate/service/api.js.map +1 -1
- package/dist/commands/validate/service/contracts/generated.js +134 -0
- package/dist/commands/validate/service/contracts/generated.js.map +1 -0
- package/dist/commands/validate/service/events/events.js +36 -14
- package/dist/commands/validate/service/events/events.js.map +1 -1
- package/dist/commands/validate/service/events/message-producers.js +1 -1
- package/dist/commands/validate/service/events/message-producers.js.map +1 -1
- package/dist/commands/validate/service/evidence-pins.js +56 -10
- package/dist/commands/validate/service/evidence-pins.js.map +1 -1
- package/dist/commands/validate/service/model/diverged.js +226 -0
- package/dist/commands/validate/service/model/diverged.js.map +1 -0
- package/dist/commands/validate/service/model/grade.js +163 -0
- package/dist/commands/validate/service/model/grade.js.map +1 -0
- package/dist/commands/validate/service/service.js +121 -61
- package/dist/commands/validate/service/service.js.map +1 -1
- package/dist/commands/validate/service/specs.js +88 -28
- package/dist/commands/validate/service/specs.js.map +1 -1
- package/dist/commands/validate/service/spine.js +38 -3
- package/dist/commands/validate/service/spine.js.map +1 -1
- package/dist/commands/validate/service/usecases/tags.js +113 -0
- package/dist/commands/validate/service/usecases/tags.js.map +1 -0
- package/dist/commands/validate/service/usecases/usecases.js +209 -0
- package/dist/commands/validate/service/usecases/usecases.js.map +1 -0
- package/dist/commands/validate/validate.js +85 -60
- package/dist/commands/validate/validate.js.map +1 -1
- package/dist/commands/verify/cross/diff.js +2 -6
- package/dist/commands/verify/cross/diff.js.map +1 -1
- package/dist/commands/verify/evidence/contract.js +1 -1
- package/dist/commands/verify/evidence/contract.js.map +1 -1
- package/dist/commands/verify/evidence/read.js +12 -3
- package/dist/commands/verify/evidence/read.js.map +1 -1
- package/dist/commands/verify/frozen.js +45 -23
- package/dist/commands/verify/frozen.js.map +1 -1
- package/dist/commands/verify/record.js +69 -17
- package/dist/commands/verify/record.js.map +1 -1
- package/dist/commands/verify/report.js +27 -44
- package/dist/commands/verify/report.js.map +1 -1
- package/dist/commands/verify/results.js +112 -30
- package/dist/commands/verify/results.js.map +1 -1
- package/dist/commands/verify/verify.js +55 -52
- package/dist/commands/verify/verify.js.map +1 -1
- package/dist/commands/vouch/pack/pack.js +2 -2
- package/dist/commands/vouch/pack/pack.js.map +1 -1
- package/dist/commands/vouch/pack/print.js +1 -6
- package/dist/commands/vouch/pack/print.js.map +1 -1
- package/dist/commands/vouch/pack/sections.js +1 -2
- package/dist/commands/vouch/pack/sections.js.map +1 -1
- package/dist/commands/vouch/run.js +26 -81
- package/dist/commands/vouch/run.js.map +1 -1
- package/dist/commands/vouch/sample/plan.js +20 -34
- package/dist/commands/vouch/sample/plan.js.map +1 -1
- package/dist/commands/vouch/vet/verify.js +39 -7
- package/dist/commands/vouch/vet/verify.js.map +1 -1
- package/dist/commands/vouch/vouch.js +10 -17
- package/dist/commands/vouch/vouch.js.map +1 -1
- package/dist/core/agent/agents-md/command-map.js +81 -106
- package/dist/core/agent/agents-md/command-map.js.map +1 -1
- package/dist/core/agent/agents-md/map/explain.js +21 -9
- package/dist/core/agent/agents-md/map/explain.js.map +1 -1
- package/dist/core/agent/agents-md/map/lenses/context.js +17 -8
- package/dist/core/agent/agents-md/map/lenses/context.js.map +1 -1
- package/dist/core/agent/agents-md/map/lenses/diff.js +33 -20
- package/dist/core/agent/agents-md/map/lenses/diff.js.map +1 -1
- package/dist/core/agent/agents-md/map/lenses/gate.js +13 -5
- package/dist/core/agent/agents-md/map/lenses/gate.js.map +1 -1
- package/dist/core/agent/agents-md/map/mcp.js +10 -7
- package/dist/core/agent/agents-md/map/mcp.js.map +1 -1
- package/dist/core/agent/agents-md/map/status.js +24 -8
- package/dist/core/agent/agents-md/map/status.js.map +1 -1
- package/dist/core/agent/agents-md/map/subsystem.js +80 -8
- package/dist/core/agent/agents-md/map/subsystem.js.map +1 -1
- package/dist/core/agent/agents-md/map/usecases.js +88 -10
- package/dist/core/agent/agents-md/map/usecases.js.map +1 -1
- package/dist/core/agent/agents-md/refusals.js +49 -22
- package/dist/core/agent/agents-md/refusals.js.map +1 -1
- package/dist/core/agent/agents-md.js +39 -6
- package/dist/core/agent/agents-md.js.map +1 -1
- package/dist/core/agent/agents-stamp.js +10 -0
- package/dist/core/agent/agents-stamp.js.map +1 -1
- package/dist/core/agent/protocol.js +1 -1
- package/dist/core/agent/protocol.js.map +1 -1
- package/dist/core/agent/scaffold.js +10 -15
- package/dist/core/agent/scaffold.js.map +1 -1
- package/dist/core/agent/support/report.js +46 -5
- package/dist/core/agent/support/report.js.map +1 -1
- package/dist/core/agent/tools/dialects.js +1 -0
- package/dist/core/agent/tools/dialects.js.map +1 -1
- package/dist/core/agent/tools/registry.js +41 -76
- package/dist/core/agent/tools/registry.js.map +1 -1
- package/dist/core/agent/workflows/adopt.js +158 -11
- package/dist/core/agent/workflows/adopt.js.map +1 -1
- package/dist/core/agent/workflows/check.js +78 -39
- package/dist/core/agent/workflows/check.js.map +1 -1
- package/dist/core/agent/workflows/closing.js +88 -27
- package/dist/core/agent/workflows/closing.js.map +1 -1
- package/dist/core/agent/workflows/feature.js +128 -13
- package/dist/core/agent/workflows/feature.js.map +1 -1
- package/dist/core/agent/workflows/implement.js +96 -13
- package/dist/core/agent/workflows/implement.js.map +1 -1
- package/dist/core/agent/workflows/reference/authoring.js +106 -4
- package/dist/core/agent/workflows/reference/authoring.js.map +1 -1
- package/dist/core/agent/workflows/reference/codes.js +8 -6
- package/dist/core/agent/workflows/reference/codes.js.map +1 -1
- package/dist/core/agent/workflows/reference/done-check.js +37 -15
- package/dist/core/agent/workflows/reference/done-check.js.map +1 -1
- package/dist/core/agent/workflows/reference/drawing/drawing.js +253 -0
- package/dist/core/agent/workflows/reference/drawing/drawing.js.map +1 -0
- package/dist/core/agent/workflows/reference/reference.js +6 -3
- package/dist/core/agent/workflows/reference/reference.js.map +1 -1
- package/dist/core/agent/workflows/reference/spine.js +68 -27
- package/dist/core/agent/workflows/reference/spine.js.map +1 -1
- package/dist/core/asyncapi/baseline/plan.js +14 -81
- package/dist/core/asyncapi/baseline/plan.js.map +1 -1
- package/dist/core/asyncapi/digest.js +30 -25
- package/dist/core/asyncapi/digest.js.map +1 -1
- package/dist/core/asyncapi/merge/components.js +5 -4
- package/dist/core/asyncapi/merge/components.js.map +1 -1
- package/dist/core/asyncapi/merge/error.js +7 -2
- package/dist/core/asyncapi/merge/error.js.map +1 -1
- package/dist/core/asyncapi/merge/markers.js +32 -50
- package/dist/core/asyncapi/merge/markers.js.map +1 -1
- package/dist/core/asyncapi/merge/merge.js +33 -53
- package/dist/core/asyncapi/merge/merge.js.map +1 -1
- package/dist/core/asyncapi/merge/pin.js +12 -36
- package/dist/core/asyncapi/merge/pin.js.map +1 -1
- package/dist/core/asyncapi/model.js.map +1 -1
- package/dist/core/asyncapi/read.js +36 -12
- package/dist/core/asyncapi/read.js.map +1 -1
- package/dist/core/brief/brief.js +96 -36
- package/dist/core/brief/brief.js.map +1 -1
- package/dist/core/brief/checks.js +138 -3
- package/dist/core/brief/checks.js.map +1 -1
- package/dist/core/brief/landscape.js +209 -130
- package/dist/core/brief/landscape.js.map +1 -1
- package/dist/core/brief/map/extend.js +84 -0
- package/dist/core/brief/map/extend.js.map +1 -0
- package/dist/core/brief/map/owed.js +266 -0
- package/dist/core/brief/map/owed.js.map +1 -0
- package/dist/core/brief/targets.js +109 -31
- package/dist/core/brief/targets.js.map +1 -1
- package/dist/core/brief/unchecked.js +14 -3
- package/dist/core/brief/unchecked.js.map +1 -1
- package/dist/core/c4/arch.js +90 -182
- package/dist/core/c4/arch.js.map +1 -1
- package/dist/core/c4/direction/direction.js +114 -0
- package/dist/core/c4/direction/direction.js.map +1 -0
- package/dist/core/c4/likec4.js +169 -14
- package/dist/core/c4/likec4.js.map +1 -1
- package/dist/core/c4/locate/unlocated.js +150 -0
- package/dist/core/c4/locate/unlocated.js.map +1 -0
- package/dist/core/c4/parsed/deployment.js +160 -0
- package/dist/core/c4/parsed/deployment.js.map +1 -0
- package/dist/core/c4/parsed/dynamic-views.js.map +1 -1
- package/dist/core/c4/parsed/values.js +54 -1
- package/dist/core/c4/parsed/values.js.map +1 -1
- package/dist/core/c4/parsed/view-ids.js +39 -1
- package/dist/core/c4/parsed/view-ids.js.map +1 -1
- package/dist/core/c4/prefetch/fleet.js +134 -0
- package/dist/core/c4/prefetch/fleet.js.map +1 -0
- package/dist/core/c4/project/architecture.js +123 -7
- package/dist/core/c4/project/architecture.js.map +1 -1
- package/dist/core/c4/project/batch.js +140 -0
- package/dist/core/c4/project/batch.js.map +1 -0
- package/dist/core/c4/project/documents.js +43 -12
- package/dist/core/c4/project/documents.js.map +1 -1
- package/dist/core/c4/project/load.js +79 -43
- package/dist/core/c4/project/load.js.map +1 -1
- package/dist/core/c4/project/staged.js +277 -0
- package/dist/core/c4/project/staged.js.map +1 -0
- package/dist/core/c4/resolve/attested.js +0 -0
- package/dist/core/c4/resolve/attested.js.map +1 -0
- package/dist/core/c4/resolve/external.js +51 -0
- package/dist/core/c4/resolve/external.js.map +1 -0
- package/dist/core/c4/resolve/service.js +78 -17
- package/dist/core/c4/resolve/service.js.map +1 -1
- package/dist/core/c4/resolve/steps.js +290 -0
- package/dist/core/c4/resolve/steps.js.map +1 -0
- package/dist/core/c4/resolve/systems.js +154 -0
- package/dist/core/c4/resolve/systems.js.map +1 -0
- package/dist/core/c4/root-project/exclude.js +352 -0
- package/dist/core/c4/root-project/exclude.js.map +1 -0
- package/dist/core/c4/seed/template.js +65 -1
- package/dist/core/c4/seed/template.js.map +1 -1
- package/dist/core/c4/service-model/created/models.js +96 -0
- package/dist/core/c4/service-model/created/models.js.map +1 -0
- package/dist/core/c4/service-model/documents.js +47 -0
- package/dist/core/c4/service-model/documents.js.map +1 -0
- package/dist/core/c4/service-model/fleet/extending.js +82 -0
- package/dist/core/c4/service-model/fleet/extending.js.map +1 -0
- package/dist/core/c4/service-model/load.js +165 -0
- package/dist/core/c4/service-model/load.js.map +1 -0
- package/dist/core/c4/service-model/renderer.js +128 -0
- package/dist/core/c4/service-model/renderer.js.map +1 -0
- package/dist/core/c4/service-model/shape.js +106 -0
- package/dist/core/c4/service-model/shape.js.map +1 -0
- package/dist/core/c4/service-model/slice.js +188 -0
- package/dist/core/c4/service-model/slice.js.map +1 -0
- package/dist/core/c4/source-mask.js +297 -10
- package/dist/core/c4/source-mask.js.map +1 -1
- package/dist/core/c4/source-scan.js +222 -141
- package/dist/core/c4/source-scan.js.map +1 -1
- package/dist/core/c4/splice/authored-source.js +79 -13
- package/dist/core/c4/splice/authored-source.js.map +1 -1
- package/dist/core/c4/splice/contract.js +20 -0
- package/dist/core/c4/splice/contract.js.map +1 -1
- package/dist/core/c4/splice/delta-blocks.js +20 -20
- package/dist/core/c4/splice/delta-blocks.js.map +1 -1
- package/dist/core/c4/splice/identity/declared.js +87 -0
- package/dist/core/c4/splice/identity/declared.js.map +1 -0
- package/dist/core/c4/splice/identity/edges.js +37 -0
- package/dist/core/c4/splice/identity/edges.js.map +1 -1
- package/dist/core/c4/splice/identity/existing.js +80 -0
- package/dist/core/c4/splice/identity/existing.js.map +1 -0
- package/dist/core/c4/splice/identity/interior.js +80 -0
- package/dist/core/c4/splice/identity/interior.js.map +1 -0
- package/dist/core/c4/splice/identity/owner.js +50 -0
- package/dist/core/c4/splice/identity/owner.js.map +1 -0
- package/dist/core/c4/splice/landscape-merge.js +182 -114
- package/dist/core/c4/splice/landscape-merge.js.map +1 -1
- package/dist/core/c4/splice/model/merge.js +151 -0
- package/dist/core/c4/splice/model/merge.js.map +1 -0
- package/dist/core/c4/splice/placement.js +160 -56
- package/dist/core/c4/splice/placement.js.map +1 -1
- package/dist/core/c4/splice/text/spot.js +131 -0
- package/dist/core/c4/splice/text/spot.js.map +1 -0
- package/dist/core/c4/workspace.js +13 -19
- package/dist/core/c4/workspace.js.map +1 -1
- package/dist/core/capabilities/capabilities.js +6 -6
- package/dist/core/capabilities/capabilities.js.map +1 -1
- package/dist/core/capabilities/delta/doc.js +7 -10
- package/dist/core/capabilities/delta/doc.js.map +1 -1
- package/dist/core/capabilities/delta/uncovered.js +2 -2
- package/dist/core/capabilities/delta/uncovered.js.map +1 -1
- package/dist/core/capabilities/findings.js +18 -22
- package/dist/core/capabilities/findings.js.map +1 -1
- package/dist/core/capabilities/realizes/findings.js +12 -19
- package/dist/core/capabilities/realizes/findings.js.map +1 -1
- package/dist/core/capabilities/realizes/join.js +0 -20
- package/dist/core/capabilities/realizes/join.js.map +1 -1
- package/dist/core/capabilities/rollup.js +6 -0
- package/dist/core/capabilities/rollup.js.map +1 -1
- package/dist/core/capabilities/tree.js +6 -28
- package/dist/core/capabilities/tree.js.map +1 -1
- package/dist/core/capabilities/usecase-join.js +3 -13
- package/dist/core/capabilities/usecase-join.js.map +1 -1
- package/dist/core/coherence/authoring/scaffold.js +11 -3
- package/dist/core/coherence/authoring/scaffold.js.map +1 -1
- package/dist/core/coherence/authoring/sentinels.js +2 -1
- package/dist/core/coherence/authoring/sentinels.js.map +1 -1
- package/dist/core/coherence/calls/called.js +52 -0
- package/dist/core/coherence/calls/called.js.map +1 -0
- package/dist/core/coherence/coherence.js +103 -22
- package/dist/core/coherence/coherence.js.map +1 -1
- package/dist/core/coherence/consumed/operations.js +70 -0
- package/dist/core/coherence/consumed/operations.js.map +1 -0
- package/dist/core/coherence/consumed/refusal.js +38 -0
- package/dist/core/coherence/consumed/refusal.js.map +1 -0
- package/dist/core/coherence/covers/health.js +85 -0
- package/dist/core/coherence/covers/health.js.map +1 -0
- package/dist/core/coherence/declared.js +60 -34
- package/dist/core/coherence/declared.js.map +1 -1
- package/dist/core/coherence/events/declared.js +44 -59
- package/dist/core/coherence/events/declared.js.map +1 -1
- package/dist/core/coherence/events/events.js +147 -16
- package/dist/core/coherence/events/events.js.map +1 -1
- package/dist/core/coherence/events/grades.js +31 -8
- package/dist/core/coherence/events/grades.js.map +1 -1
- package/dist/core/coherence/events/lookups.js +28 -87
- package/dist/core/coherence/events/lookups.js.map +1 -1
- package/dist/core/coherence/events/produced.js +100 -0
- package/dist/core/coherence/events/produced.js.map +1 -0
- package/dist/core/coherence/lookups.js +18 -78
- package/dist/core/coherence/lookups.js.map +1 -1
- package/dist/core/coherence/retired/archived.js +285 -0
- package/dist/core/coherence/retired/archived.js.map +1 -0
- package/dist/core/coherence/retired/event-pair.js +62 -0
- package/dist/core/coherence/retired/event-pair.js.map +1 -0
- package/dist/core/coherence/retired/merged.js +79 -0
- package/dist/core/coherence/retired/merged.js.map +1 -0
- package/dist/core/coherence/retired/retirement.js +49 -0
- package/dist/core/coherence/retired/retirement.js.map +1 -0
- package/dist/core/coherence/retired/revival/arrivals.js +131 -0
- package/dist/core/coherence/retired/revival/arrivals.js.map +1 -0
- package/dist/core/coherence/retired/revival/revived.js +297 -0
- package/dist/core/coherence/retired/revival/revived.js.map +1 -0
- package/dist/core/consumers/after.js +113 -0
- package/dist/core/consumers/after.js.map +1 -0
- package/dist/core/consumers/consumers.js +296 -0
- package/dist/core/consumers/consumers.js.map +1 -0
- package/dist/core/consumers/current.js +211 -0
- package/dist/core/consumers/current.js.map +1 -0
- package/dist/core/consumers/scan.js +46 -0
- package/dist/core/consumers/scan.js.map +1 -0
- package/dist/core/consumers/state.js +27 -0
- package/dist/core/consumers/state.js.map +1 -0
- package/dist/core/delta/delta.js +51 -6
- package/dist/core/delta/delta.js.map +1 -1
- package/dist/core/dependencies/fanin.js +2 -2
- package/dist/core/dependencies/fanin.js.map +1 -1
- package/dist/core/deployment/delta.js +191 -0
- package/dist/core/deployment/delta.js.map +1 -0
- package/dist/core/deployment/objects.js +145 -0
- package/dist/core/deployment/objects.js.map +1 -0
- package/dist/core/diff/base-git.js +94 -12
- package/dist/core/diff/base-git.js.map +1 -1
- package/dist/core/diff/base-state.js +49 -21
- package/dist/core/diff/base-state.js.map +1 -1
- package/dist/core/diff/joins.js +136 -0
- package/dist/core/diff/joins.js.map +1 -0
- package/dist/core/diff/scope/changed-paths.js +22 -6
- package/dist/core/diff/scope/changed-paths.js.map +1 -1
- package/dist/core/diff/scope/edges.js +82 -0
- package/dist/core/diff/scope/edges.js.map +1 -0
- package/dist/core/diff/semantic.js +100 -31
- package/dist/core/diff/semantic.js.map +1 -1
- package/dist/core/docs.js +31 -22
- package/dist/core/docs.js.map +1 -1
- package/dist/core/doctor/agents.js +34 -18
- package/dist/core/doctor/agents.js.map +1 -1
- package/dist/core/doctor/config.js +7 -3
- package/dist/core/doctor/config.js.map +1 -1
- package/dist/core/doctor/doctor.js +91 -11
- package/dist/core/doctor/doctor.js.map +1 -1
- package/dist/core/doctor/report.js.map +1 -1
- package/dist/core/doctor/reports/scan.js +242 -0
- package/dist/core/doctor/reports/scan.js.map +1 -0
- package/dist/core/document/apply.js +158 -11
- package/dist/core/document/apply.js.map +1 -1
- package/dist/core/document/frontmatter.js +55 -0
- package/dist/core/document/frontmatter.js.map +1 -1
- package/dist/core/document/parse.js +29 -6
- package/dist/core/document/parse.js.map +1 -1
- package/dist/core/document/scenarios.js +17 -0
- package/dist/core/document/scenarios.js.map +1 -1
- package/dist/core/document/spec.js +4 -11
- package/dist/core/document/spec.js.map +1 -1
- package/dist/core/envelope/config.js +10 -0
- package/dist/core/envelope/config.js.map +1 -1
- package/dist/core/envelope/json.js +34 -0
- package/dist/core/envelope/json.js.map +1 -1
- package/dist/core/explain/families.js +26 -21
- package/dist/core/explain/families.js.map +1 -1
- package/dist/core/explain/lookup.js +14 -1
- package/dist/core/explain/lookup.js.map +1 -1
- package/dist/core/explain/refusals.js +5 -5
- package/dist/core/explain/refusals.js.map +1 -1
- package/dist/core/explain/terms/documents.js +227 -0
- package/dist/core/explain/terms/documents.js.map +1 -0
- package/dist/core/explain/terms/done-check.js +144 -0
- package/dist/core/explain/terms/done-check.js.map +1 -0
- package/dist/core/explain/terms/entry.js +14 -0
- package/dist/core/explain/terms/entry.js.map +1 -0
- package/dist/core/explain/terms/stages.js +71 -0
- package/dist/core/explain/terms/stages.js.map +1 -0
- package/dist/core/explain/terms/tasks.js +97 -0
- package/dist/core/explain/terms/tasks.js.map +1 -0
- package/dist/core/explain/terms.js +68 -109
- package/dist/core/explain/terms.js.map +1 -1
- package/dist/core/explore/explore.js +40 -10
- package/dist/core/explore/explore.js.map +1 -1
- package/dist/core/fleet-context.js +81 -95
- package/dist/core/fleet-context.js.map +1 -1
- package/dist/core/gate/checks.js +11 -4
- package/dist/core/gate/checks.js.map +1 -1
- package/dist/core/gate/partners.js +69 -28
- package/dist/core/gate/partners.js.map +1 -1
- package/dist/core/gate/report.js.map +1 -1
- package/dist/core/gate/verification.js +75 -12
- package/dist/core/gate/verification.js.map +1 -1
- package/dist/core/gherkin/stale.js +13 -37
- package/dist/core/gherkin/stale.js.map +1 -1
- package/dist/core/glossary/delta.js +10 -7
- package/dist/core/glossary/delta.js.map +1 -1
- package/dist/core/glossary/tree.js +6 -19
- package/dist/core/glossary/tree.js.map +1 -1
- package/dist/core/kernel/concurrency.js +19 -0
- package/dist/core/kernel/concurrency.js.map +1 -1
- package/dist/core/kernel/ids/fqn/ancestors.js +65 -0
- package/dist/core/kernel/ids/fqn/ancestors.js.map +1 -0
- package/dist/core/kernel/path-safety.js +38 -0
- package/dist/core/kernel/path-safety.js.map +1 -1
- package/dist/core/kernel/records.js +11 -0
- package/dist/core/kernel/records.js.map +1 -1
- package/dist/core/links/backlinks.js +1 -3
- package/dist/core/links/backlinks.js.map +1 -1
- package/dist/core/links/corpus.js +51 -37
- package/dist/core/links/corpus.js.map +1 -1
- package/dist/core/links/findings.js.map +1 -1
- package/dist/core/links/parse.js +38 -9
- package/dist/core/links/parse.js.map +1 -1
- package/dist/core/mcp/argv.js +101 -0
- package/dist/core/mcp/argv.js.map +1 -1
- package/dist/core/mcp/author-tools.js +2 -1
- package/dist/core/mcp/author-tools.js.map +1 -1
- package/dist/core/mcp/protocol.js +2 -2
- package/dist/core/mcp/protocol.js.map +1 -1
- package/dist/core/mcp/tools.js +43 -33
- package/dist/core/mcp/tools.js.map +1 -1
- package/dist/core/openapi/baseline/gate.js +53 -12
- package/dist/core/openapi/baseline/gate.js.map +1 -1
- package/dist/core/openapi/baseline/plan.js +40 -10
- package/dist/core/openapi/baseline/plan.js.map +1 -1
- package/dist/core/openapi/baseline/record.js +0 -1
- package/dist/core/openapi/baseline/record.js.map +1 -1
- package/dist/core/openapi/contract-text.js +33 -0
- package/dist/core/openapi/contract-text.js.map +1 -0
- package/dist/core/openapi/contract-tree.js +301 -0
- package/dist/core/openapi/contract-tree.js.map +1 -0
- package/dist/core/openapi/depth.js +38 -24
- package/dist/core/openapi/depth.js.map +1 -1
- package/dist/core/openapi/digest.js +52 -2
- package/dist/core/openapi/digest.js.map +1 -1
- package/dist/core/openapi/doc.js +12 -4
- package/dist/core/openapi/doc.js.map +1 -1
- package/dist/core/openapi/merge/components.js +25 -60
- package/dist/core/openapi/merge/components.js.map +1 -1
- package/dist/core/openapi/merge/error.js +3 -13
- package/dist/core/openapi/merge/error.js.map +1 -1
- package/dist/core/openapi/merge/markers.js +26 -30
- package/dist/core/openapi/merge/markers.js.map +1 -1
- package/dist/core/openapi/merge/merge.js +28 -52
- package/dist/core/openapi/merge/merge.js.map +1 -1
- package/dist/core/openapi/merge/pin.js +17 -35
- package/dist/core/openapi/merge/pin.js.map +1 -1
- package/dist/core/openspec/decide/verdict.js.map +1 -1
- package/dist/core/openspec/inventory.js +4 -5
- package/dist/core/openspec/inventory.js.map +1 -1
- package/dist/core/openspec/model/mapping.js +30 -28
- package/dist/core/openspec/model/mapping.js.map +1 -1
- package/dist/core/openspec/model/model.js +12 -7
- package/dist/core/openspec/model/model.js.map +1 -1
- package/dist/core/openspec/scan/changes.js +10 -4
- package/dist/core/openspec/scan/changes.js.map +1 -1
- package/dist/core/openspec/scan/config.js +7 -16
- package/dist/core/openspec/scan/config.js.map +1 -1
- package/dist/core/openspec/scan/shape.js +63 -19
- package/dist/core/openspec/scan/shape.js.map +1 -1
- package/dist/core/openspec/scan/walk.js +21 -38
- package/dist/core/openspec/scan/walk.js.map +1 -1
- package/dist/core/pack/living.js +43 -4
- package/dist/core/pack/living.js.map +1 -1
- package/dist/core/pack/pack.js +50 -6
- package/dist/core/pack/pack.js.map +1 -1
- package/dist/core/projection/arch-slice.js +2 -2
- package/dist/core/projection/arch-slice.js.map +1 -1
- package/dist/core/projection/introduced.js +14 -0
- package/dist/core/projection/introduced.js.map +1 -0
- package/dist/core/provenance/findings.js +48 -12
- package/dist/core/provenance/findings.js.map +1 -1
- package/dist/core/provenance/git.js +33 -57
- package/dist/core/provenance/git.js.map +1 -1
- package/dist/core/provenance/gitq/moves.js +71 -114
- package/dist/core/provenance/gitq/moves.js.map +1 -1
- package/dist/core/provenance/gitq/vouched-ancestor.js +2 -2
- package/dist/core/provenance/gitq/vouched-ancestor.js.map +1 -1
- package/dist/core/provenance/sources.js +23 -6
- package/dist/core/provenance/sources.js.map +1 -1
- package/dist/core/provenance/stamp.js +5 -17
- package/dist/core/provenance/stamp.js.map +1 -1
- package/dist/core/repo/authored/paths.js +18 -0
- package/dist/core/repo/authored/paths.js.map +1 -1
- package/dist/core/repo/entries.js.map +1 -1
- package/dist/core/repo/paths.js +92 -6
- package/dist/core/repo/paths.js.map +1 -1
- package/dist/core/repo/repo.js +74 -14
- package/dist/core/repo/repo.js.map +1 -1
- package/dist/core/repo/tree/find.js +12 -3
- package/dist/core/repo/tree/find.js.map +1 -1
- package/dist/core/repo/tree/fs.js +27 -1
- package/dist/core/repo/tree/fs.js.map +1 -1
- package/dist/core/repo/tree/render/body.js +258 -0
- package/dist/core/repo/tree/render/body.js.map +1 -0
- package/dist/core/repo/tree/render/projects.js +182 -0
- package/dist/core/repo/tree/render/projects.js.map +1 -0
- package/dist/core/repo/tree/render/service/header.js +125 -0
- package/dist/core/repo/tree/render/service/header.js.map +1 -0
- package/dist/core/repo/tree/render/service/peers.js +28 -0
- package/dist/core/repo/tree/render/service/peers.js.map +1 -0
- package/dist/core/repo/tree/render/service/view-id.js +67 -0
- package/dist/core/repo/tree/render/service/view-id.js.map +1 -0
- package/dist/core/repo/tree/render/service/views.js +78 -0
- package/dist/core/repo/tree/render/service/views.js.map +1 -0
- package/dist/core/repo/tree/render/stale.js +42 -0
- package/dist/core/repo/tree/render/stale.js.map +1 -0
- package/dist/core/repo/tree/render/text.js +84 -0
- package/dist/core/repo/tree/render/text.js.map +1 -0
- package/dist/core/repo/tree/render/views.js +293 -0
- package/dist/core/repo/tree/render/views.js.map +1 -0
- package/dist/core/repo/tree/walk.js +53 -32
- package/dist/core/repo/tree/walk.js.map +1 -1
- package/dist/core/results.js +47 -12
- package/dist/core/results.js.map +1 -1
- package/dist/core/scaffold/landscape.js +121 -13
- package/dist/core/scaffold/landscape.js.map +1 -1
- package/dist/core/scaffold/legacy.js +271 -0
- package/dist/core/scaffold/legacy.js.map +1 -0
- package/dist/core/staging/commit.js +26 -15
- package/dist/core/staging/commit.js.map +1 -1
- package/dist/core/staging/lock.js +74 -12
- package/dist/core/staging/lock.js.map +1 -1
- package/dist/core/staging/recovery/intent.js +2 -5
- package/dist/core/staging/recovery/intent.js.map +1 -1
- package/dist/core/staging/recovery/recover.js +84 -6
- package/dist/core/staging/recovery/recover.js.map +1 -1
- package/dist/core/staging/snapshot/manifest.js +188 -0
- package/dist/core/staging/snapshot/manifest.js.map +1 -0
- package/dist/core/staging/{snapshot.js → snapshot/snapshot.js} +39 -45
- package/dist/core/staging/snapshot/snapshot.js.map +1 -0
- package/dist/core/staging/txn/forward.js +24 -14
- package/dist/core/staging/txn/forward.js.map +1 -1
- package/dist/core/staging/writes.js.map +1 -1
- package/dist/core/status/actions/execution.js +11 -0
- package/dist/core/status/actions/execution.js.map +1 -1
- package/dist/core/status/contracts.js +39 -15
- package/dist/core/status/contracts.js.map +1 -1
- package/dist/core/status/feature/artifacts.js +33 -8
- package/dist/core/status/feature/artifacts.js.map +1 -1
- package/dist/core/status/feature/feature.js +24 -17
- package/dist/core/status/feature/feature.js.map +1 -1
- package/dist/core/status/feature/next.js +20 -7
- package/dist/core/status/feature/next.js.map +1 -1
- package/dist/core/status/feature/words.js +96 -0
- package/dist/core/status/feature/words.js.map +1 -0
- package/dist/core/status/fleet/fleet.js +43 -14
- package/dist/core/status/fleet/fleet.js.map +1 -1
- package/dist/core/status/fleet/next.js +153 -58
- package/dist/core/status/fleet/next.js.map +1 -1
- package/dist/core/status/report.js.map +1 -1
- package/dist/core/status/scan.js.map +1 -1
- package/dist/core/status/verification.js +30 -15
- package/dist/core/status/verification.js.map +1 -1
- package/dist/core/tasks/packet.js +199 -0
- package/dist/core/tasks/packet.js.map +1 -0
- package/dist/core/tasks/record.js +43 -0
- package/dist/core/tasks/record.js.map +1 -0
- package/dist/core/tasks/sources.js +181 -0
- package/dist/core/tasks/sources.js.map +1 -0
- package/dist/core/tasks/stamps.js +76 -0
- package/dist/core/tasks/stamps.js.map +1 -0
- package/dist/core/tasks/unit.js +0 -0
- package/dist/core/tasks/unit.js.map +1 -0
- package/dist/core/usecases/capability.js +11 -14
- package/dist/core/usecases/capability.js.map +1 -1
- package/dist/core/usecases/delta/claims.js +31 -7
- package/dist/core/usecases/delta/claims.js.map +1 -1
- package/dist/core/usecases/delta/flows.js +5 -7
- package/dist/core/usecases/delta/flows.js.map +1 -1
- package/dist/core/usecases/delta/modified.js +315 -0
- package/dist/core/usecases/delta/modified.js.map +1 -0
- package/dist/core/usecases/delta/overlay.js +32 -191
- package/dist/core/usecases/delta/overlay.js.map +1 -1
- package/dist/core/usecases/fleet.js +195 -36
- package/dist/core/usecases/fleet.js.map +1 -1
- package/dist/{commands/validate/fleet/usecases → core/usecases/grade}/capability-tag.js +8 -9
- package/dist/core/usecases/grade/capability-tag.js.map +1 -0
- package/dist/core/usecases/grade/feature.js +88 -0
- package/dist/core/usecases/grade/feature.js.map +1 -0
- package/dist/{commands/validate/fleet/usecases → core/usecases/grade}/requirement-tag.js +3 -4
- package/dist/core/usecases/grade/requirement-tag.js.map +1 -0
- package/dist/{commands/validate/fleet/usecases → core/usecases/grade}/steps.js +102 -33
- package/dist/core/usecases/grade/steps.js.map +1 -0
- package/dist/{commands/validate/fleet/usecases → core/usecases/grade}/usecases.js +23 -10
- package/dist/core/usecases/grade/usecases.js.map +1 -0
- package/dist/core/usecases/operations.js +26 -14
- package/dist/core/usecases/operations.js.map +1 -1
- package/dist/core/usecases/place.js.map +1 -1
- package/dist/core/usecases/service/exclude.js +119 -0
- package/dist/core/usecases/service/exclude.js.map +1 -0
- package/dist/core/usecases/service/flows.js +164 -0
- package/dist/core/usecases/service/flows.js.map +1 -0
- package/dist/core/usecases/suites/participants.js +83 -0
- package/dist/core/usecases/suites/participants.js.map +1 -0
- package/dist/core/usecases/suites/suites.js +45 -0
- package/dist/core/usecases/suites/suites.js.map +1 -0
- package/dist/core/verify/answers.js +12 -5
- package/dist/core/verify/answers.js.map +1 -1
- package/dist/core/verify/build.js +102 -82
- package/dist/core/verify/build.js.map +1 -1
- package/dist/core/verify/checklist.js +70 -12
- package/dist/core/verify/checklist.js.map +1 -1
- package/dist/core/verify/location.js +67 -0
- package/dist/core/verify/location.js.map +1 -0
- package/dist/core/verify/pins/lint.js +3 -2
- package/dist/core/verify/pins/lint.js.map +1 -1
- package/dist/core/verify/pins/pin.js +1 -1
- package/dist/core/verify/pins/pin.js.map +1 -1
- package/dist/core/verify/read/file.js +224 -0
- package/dist/core/verify/read/file.js.map +1 -0
- package/dist/core/verify/{file.js → read/shape.js} +16 -104
- package/dist/core/verify/read/shape.js.map +1 -0
- package/dist/core/verify/read/union.js +105 -0
- package/dist/core/verify/read/union.js.map +1 -0
- package/dist/core/verify/record.js +35 -12
- package/dist/core/verify/record.js.map +1 -1
- package/dist/core/verify/store/commit.js +40 -30
- package/dist/core/verify/store/commit.js.map +1 -1
- package/dist/core/verify/store/render.js +8 -4
- package/dist/core/verify/store/render.js.map +1 -1
- package/dist/core/vocabulary/issue.js +4 -0
- package/dist/core/vocabulary/issue.js.map +1 -1
- package/dist/core/vocabulary/maturity.js +75 -4
- package/dist/core/vocabulary/maturity.js.map +1 -1
- package/dist/core/vocabulary/report.js +16 -1
- package/dist/core/vocabulary/report.js.map +1 -1
- package/examples/README.md +54 -11
- package/examples/docs/architecture/landscape.likec4 +62 -6
- package/examples/docs/architecture/services.likec4 +70 -0
- package/examples/docs/architecture/subsystems.likec4 +7 -0
- package/examples/docs/architecture/usecases/order-notification.likec4 +16 -7
- package/examples/docs/features/FEAT-101-payment-splitting/specs/payment-service/asyncapi.yaml +11 -8
- package/examples/docs/features/FEAT-101-payment-splitting/specs/payment-split-service/openapi.yaml +6 -6
- package/examples/docs/features/FEAT-112-retire-order-v1/specs/order-service/openapi.yaml +4 -1
- package/examples/docs/features/archive/FEAT-088-refunds/specs/payment-service/openapi.yaml +13 -10
- package/examples/docs/likec4.config.json +0 -1
- package/examples/docs/services/checkout-web/model.likec4 +39 -56
- package/examples/docs/services/order-service/arch.spec.md +13 -6
- package/examples/docs/services/order-service/asyncapi.yaml +3 -2
- package/examples/docs/services/order-service/model.likec4 +58 -70
- package/examples/docs/services/payment-service/arch.spec.md +7 -4
- package/examples/docs/services/payment-service/model.likec4 +38 -37
- package/examples/docs/services/platform/identity-service/arch.spec.md +7 -4
- package/examples/docs/services/platform/identity-service/model.likec4 +35 -37
- package/examples/docs/services/platform/notification-service/asyncapi.yaml +2 -1
- package/examples/docs/services/platform/notification-service/health.yaml +8 -3
- package/examples/docs/services/platform/notification-service/model.likec4 +43 -64
- package/package.json +1 -1
- package/dist/commands/validate/checks/vocabulary.js +0 -31
- package/dist/commands/validate/checks/vocabulary.js.map +0 -1
- package/dist/commands/validate/fleet/usecases/capability-tag.js.map +0 -1
- package/dist/commands/validate/fleet/usecases/requirement-tag.js.map +0 -1
- package/dist/commands/validate/fleet/usecases/steps.js.map +0 -1
- package/dist/commands/validate/fleet/usecases/usecases.js.map +0 -1
- package/dist/core/agent/agents-md/artifacts.js +0 -220
- package/dist/core/agent/agents-md/artifacts.js.map +0 -1
- package/dist/core/agent/agents-md/cycle.js +0 -165
- package/dist/core/agent/agents-md/cycle.js.map +0 -1
- package/dist/core/agent/agents-md/shipped/archive-gate.js +0 -98
- package/dist/core/agent/agents-md/shipped/archive-gate.js.map +0 -1
- package/dist/core/agent/agents-md/spine.js +0 -40
- package/dist/core/agent/agents-md/spine.js.map +0 -1
- package/dist/core/coherence/events/merged.js +0 -40
- package/dist/core/coherence/events/merged.js.map +0 -1
- package/dist/core/diff/current-state.js +0 -96
- package/dist/core/diff/current-state.js.map +0 -1
- package/dist/core/diff/victims.js +0 -247
- package/dist/core/diff/victims.js.map +0 -1
- package/dist/core/repo/tree/views.js +0 -172
- package/dist/core/repo/tree/views.js.map +0 -1
- package/dist/core/staging/snapshot.js.map +0 -1
- package/dist/core/verify/file.js.map +0 -1
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"fleet-shape.js","sourceRoot":"","sources":["../../../../src/commands/validate/checks/fleet-shape.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * Fleet-shape advisories over the parsed landscape: the two ways a fleet map\n * stops answering questions as it grows, caught while the map is already in\n * memory.\n *\n * Both shapes become visible at service three rather than service two.\n * Ubiquitous infrastructure — logging Kafka, an identity provider, service\n * discovery — takes one inbound edge per modelled service until the map is a\n * hairball around it; deleting the element fixes the picture and loses \"who\n * depends on the Identity Provider\", which is the question the map exists to\n * answer during an incident. Tagging keeps both: the fleet view excludes\n * `#platform`, and a platform view keeps the dependents. And a datastore drawn as a fleet-level\n * peer makes a claim its consumer count either supports or refutes: one\n * consumer means the drawing is false — the store is that service's\n * internals, not a system in its own right — while two or more mean the\n * strongest coupling two services can have, which deserves to be stated\n * rather than inferred.\n *\n * Everything here is O(relationships) over data the fleet target already\n * holds. loam still reads no code and decides no policy: the team chooses\n * what counts as platform and which data is truly shared — these warnings\n * only name the shapes that are usually wrong.\n */\nimport { existsSync } from \"node:fs\";\nimport { readFile } from \"node:fs/promises\";\nimport { type Elem, type LoadedDoc, type Rel } from \"../../../core/c4/likec4.js\";\nimport { type DeclaredService, type RawServiceId } from \"../../../core/kernel/ids/service.js\";\nimport { type Finding } from \"../../../core/vocabulary/report.js\";\nimport { type ServiceEntry } from \"../../../core/repo/entries.js\";\nimport { permissionsPath, servicePathsAt } from \"../../../core/repo/paths.js\";\nimport { readVocabulary } from \"../../../core/permissions/permissions.js\";\nimport { readCapabilityVocabulary } from \"../../../core/capabilities/capabilities.js\";\nimport {\n capabilityDocFindings,\n capabilityRequirementIndex,\n docMissingFindings,\n gradableCapabilityIds,\n invalidVocabularyFinding,\n unrealizedFindings,\n} from \"../../../core/capabilities/findings.js\";\nimport { useCaseRequirementClaims } from \"../../../core/usecases/capability.js\";\nimport type { ParsedView } from \"../../../core/c4/parsed/dynamic-views.js\";\nimport { capabilityRollup, usedCapabilities } from \"../../../core/capabilities/rollup.js\";\nimport { requirementUnrealizedFindings } from \"../../../core/capabilities/realizes/findings.js\";\nimport { parseRequirements } from \"../../../core/document/parse.js\";\nimport { type Requirement } from \"../../../core/document/spec.js\";\nimport { FleetContext } from \"../../../core/fleet-context.js\";\nimport { EXTERNAL_TAG, PLATFORM_TAG } from \"../../../core/vocabulary/maturity.js\";\nimport type { DocsDir } from \"../../../core/kernel/ids/dirs.js\";\n\n/**\n * Consumers at which an untagged external hub starts to warn. Three, not two:\n * two services sharing a dependency is a fact about those two services, the\n * same edge from three is a pattern about the element — and three is where\n * the shared hub starts making the map unreadable.\n */\nexport const PLATFORM_CANDIDATE_MIN_CONSUMERS = 3;\n\n/** The element kind read as a datastore, compared case-insensitively. */\nconst DATASTORE_KIND = \"database\";\n\nexport interface FleetShape {\n /** Service-LEVEL elements — the fleet map's own boxes (landscape.ts `drawn`). */\n drawn: Elem[];\n relationships: Rel[];\n /** The services/<id>/ directories that exist. */\n services: ReadonlySet<string>;\n /** The shared element→service resolver every edge join uses. */\n resolve: (id: string) => string;\n /**\n * Where a service id's directory actually sits, repo-relative and without a\n * trailing slash. Injected rather than joined: only the enumeration knows\n * whether a service is filed under a subsystem, and a fix instruction naming\n * the wrong directory is worse than one naming none.\n */\n pathOf: (id: string) => string;\n}\n\nexport function fleetShapeFindings(shape: FleetShape): Finding[] {\n const { drawn, relationships, services, resolve } = shape;\n const findings: Finding[] = [];\n\n // Distinct consumer SERVICES of an element, its nested children included: an\n // edge into `kafka.paymentEvents` is consumption of `kafka`. Persons, other\n // external systems and unresolved sources never count — a consumer is a\n // source the shared resolver files under a real services/<id>/, so two edges\n // from one service are one consumer.\n const consumersOf = (e: Elem): string[] => {\n const out = new Set<string>();\n for (const r of relationships) {\n if (r.target !== e.id && !r.target.startsWith(`${e.id}.`)) continue;\n const svc = resolve(r.source);\n if (services.has(svc)) out.add(svc);\n }\n return [...out].sort();\n };\n\n for (const e of drawn) {\n // An element that stands for a real service is the fleet itself, not its\n // shape: many consumers is what a well-used service looks like, and a\n // datastore bound to a directory is graded by the binding checks instead.\n if (e.service !== undefined || services.has(e.title)) continue;\n const consumers = consumersOf(e);\n\n if (\n e.tags.includes(EXTERNAL_TAG) &&\n !e.tags.includes(PLATFORM_TAG) &&\n consumers.length >= PLATFORM_CANDIDATE_MIN_CONSUMERS\n ) {\n findings.push({\n severity: \"warn\",\n code: \"landscape.platform-candidate\",\n subject: e.title,\n message:\n `landscape: '${e.title}' is consumed by ${consumers.length} services (${consumers.join(\", \")}) ` +\n `and is not tagged #platform — a hub like this takes one more edge per modelled service until ` +\n `the fleet view is unreadable. Declare \\`tag platform\\` in the specification block and tag the ` +\n `element (LikeC4 refuses an undeclared tag, so both steps are needed): the fleet view then ` +\n `excludes it, and a platform view over \\`include * -> element.tag = #platform\\` keeps ` +\n `\"who depends on it\" answerable`,\n });\n }\n\n if (e.kind.toLowerCase() !== DATASTORE_KIND || consumers.length === 0) continue;\n if (consumers.length === 1) {\n findings.push({\n severity: \"warn\",\n code: \"landscape.datastore-private\",\n subject: e.title,\n message:\n `landscape: '${e.title}' is a datastore with a single consumer at fleet level ` +\n `('${consumers[0]!}') — drawn as a peer it reads as a system in its own right, available to ` +\n `be depended on. Move it into ${shape.pathOf(consumers[0]!)}/model.likec4 as a nested container ` +\n `and delete it here, or add the second consumer's edge if another service really reaches ` +\n `the same data`,\n });\n } else {\n findings.push({\n severity: \"warn\",\n code: \"landscape.datastore-shared\",\n subject: e.title,\n message:\n `landscape: '${e.title}' is a datastore shared by ${consumers.length} services ` +\n `(${consumers.join(\", \")}) — the strongest coupling two services can have, and the hardest ` +\n `to undo. Shared means the same DATA: if they read the same tables or keys, keep it drawn ` +\n `and let this warning state the coupling; if they only share a host or cluster (two schemas, ` +\n `two lock paths), that is operational blast radius — a runbook fact — and the honest model ` +\n `is one private store per service`,\n });\n }\n }\n return findings;\n}\n\n/**\n * The #external element that declares it PUBLISHES this message, or null —\n * the landscape-side half of the event spine's fleet question, beside the\n * other checks that read tags off the drawn map.\n *\n * Positive evidence only: an edge carrying `metadata { publishes '<msg>' }`\n * whose source element — itself or a declared ancestor, since the edge may\n * point out of a topic nested inside the broker — is tagged #external and\n * resolves to no known service directory. The resolution guard is what keeps\n * a mis-tagged internal service reading as unproduced: a tag is cheap to\n * write, and a services/<id>/ directory outranks it.\n */\n/**\n * The authorization vocabulary graded against the fleet that uses it — the half\n * of the axis no single service can answer.\n *\n * `permissions.invalid` is an error and it is the only one reported when it\n * fires: a vocabulary nobody can read resolves nothing, so every `Requires:`\n * line in the fleet would follow it as `permissions.unknown`, and a hundred\n * findings about one broken file is a cascade, not a diagnosis.\n *\n * `permissions.unenforced` is the mirror of `api.ungoverned`, and it earns its\n * place for the same reason: a vocabulary is only worth what cites it. A\n * permission nothing requires is either a rule that was removed and left its\n * word behind, or a word nobody adopted — both are drift, and neither is\n * visible from inside the file. Warn, because the honest answer is sometimes\n * \"not modelled yet\".\n */\nexport async function permissionFindings(\n docsDir: DocsDir,\n services: ServiceEntry[],\n fleet: FleetContext | undefined,\n): Promise<Finding[]> {\n const vocabulary = await readVocabulary(permissionsPath(docsDir));\n if (!vocabulary.present) return [];\n if (vocabulary.invalid !== undefined) {\n return [\n {\n severity: \"error\",\n code: \"permissions.invalid\",\n message:\n `landscape: architecture/permissions.yaml does not read as a vocabulary — ${vocabulary.invalid}. ` +\n \"Every `Requires:` line in the fleet resolves against this file, so none of them can be graded until it parses. \" +\n \"The shape is `subjects: {<kind>: {description}}` then `permissions: {<kind>: {<name>: {description, owned_by, enforced_by}}}`.\",\n },\n ];\n }\n const used = new Set<string>();\n for (const entry of services) {\n const paths = servicePathsAt(entry.dir);\n for (const path of [paths.spec, paths.archSpec]) {\n // Existence first, and for BOTH readers: arch.spec.md is optional (most\n // of a legacy fleet has none) and spec.md is missing on a half-adopted\n // service. `FleetContext.readRequirements` throws ENOENT, which surfaces\n // as `repository-unavailable` and takes the whole `--all` run down — an\n // absent optional artifact must never be able to do that.\n if (!existsSync(path)) continue;\n const reqs = fleet === undefined ? await readRequirementsAt(path) : await fleet.readRequirements(path);\n for (const r of reqs) {\n if (r.kind !== \"REMOVED\") for (const p of r.requires) used.add(p);\n }\n }\n }\n const unenforced = [...vocabulary.byId.keys()].filter((id) => !used.has(id));\n if (unenforced.length === 0) return [];\n return [\n {\n severity: \"warn\",\n code: \"permissions.unenforced\",\n message: `landscape: ${unenforced.length} declared permission(s) that no requirement's \\`Requires:\\` line names — write the requirement that gates on each, or drop the declaration`,\n details: unenforced,\n },\n ];\n}\n\n/** The uncached read, for the `--service` path where no fleet context exists. */\nasync function readRequirementsAt(path: string): Promise<Requirement[]> {\n if (!existsSync(path)) return [];\n return parseRequirements(await readFile(path, \"utf8\"));\n}\n\n/**\n * The capability vocabulary graded against the fleet that realizes it — the\n * axis's fleet half, beside permissionFindings and shaped by the same two\n * verdicts. `capability.invalid` is a `validate --all` run's ONE finding about\n * an unreadable file — fleet scope, so single-target runs stay silent about\n * it — and it suppresses the rest of the family (the unknown\n * grades at every service target return [] for the same vocabulary — one\n * breach, one finding, never a cascade). `capability.unrealized` is one warn\n * PER declared-but-unnamed capability, subject = the id, because the roadmap's\n * criterion is 'one warning per capability, not one per service'. Holding\n * NEITHER `architecture/capabilities.yaml` nor `capabilities/` is total\n * silence: the fleet's own files are this axis's opt-in, either of them.\n *\n * The used set comes through capabilityRollup with the fleet's own cached\n * reader, so under --all the walk re-parses nothing the service targets have\n * not already paid for — and the rollup, the warn and `loam list capabilities`\n * can never disagree about what \"realized\" means.\n *\n * The AUTHORED documents are graded here too, and they ride this function\n * rather than a target of their own for `viewsStaleFindings`' reason one tree\n * over: a capability belongs to no service, so only the fleet run has the whole\n * vocabulary and the enumerated tree in view at once. Their reads are NOT\n * wrapped, deliberately — a `capabilities/<id>/spec.md` that is not UTF-8 takes\n * the run down exactly as a `services/<id>/spec.md` does, because a document\n * loam cannot decode is a refusal everywhere else in the product and a quiet\n * \"zero requirements\" here would grade a whole capability green on bytes\n * nobody read.\n */\nexport async function capabilityFleetFindings(\n docsDir: DocsDir,\n services: ServiceEntry[],\n fleet: FleetContext | undefined,\n flows: readonly ParsedView[] | null,\n): Promise<Finding[]> {\n const vocab = fleet === undefined ? await readCapabilityVocabulary(docsDir) : await fleet.capabilities(docsDir);\n if (!vocab.present) return [];\n const read = fleet === undefined ? readRequirementsAt : (p: string) => fleet.readRequirements(p);\n const authored: Finding[] = docMissingFindings(vocab.tree);\n for (const doc of vocab.tree.docs) authored.push(...capabilityDocFindings(await read(doc.spec), doc.id));\n\n const invalid = invalidVocabularyFinding(vocab);\n if (invalid !== null) return [invalid, ...authored];\n // A USE CASE IS THE OTHER WAY TO KEEP A PROMISE, and it is the only carrier a\n // cross-service criterion has: \"I enter a login and a password and I am in\"\n // belongs to no single service's spec, so a fleet could satisfy it perfectly\n // through a `#cap-`/`#req-` tagged flow and still be told nobody realizes it.\n //\n // The claims go INTO the rollup rather than beside it, so both unrealized\n // grades and `loam list capabilities` read one set of rows. `flows === null`\n // means loam could not READ the flows (no preload, or an `architecture/` that\n // did not parse); the key is then OMITTED rather than empty, and every\n // `keptBy` below is `undefined` — the three-state contract `rollup.ts` states.\n const index = await capabilityRequirementIndex(vocab, read);\n const keptByFlows =\n flows === null\n ? undefined\n : useCaseRequirementClaims(flows, gradableCapabilityIds(vocab), (c) => index.byCapability.get(c));\n const rows = await capabilityRollup({ services, vocab, read, ...(keptByFlows === undefined ? {} : { keptByFlows }) });\n\n // The two unrealized grades read the SAME rollup, which is what keeps them\n // from disagreeing about the word: a capability is unrealized when nothing\n // names it by any join, and one of its requirements is unrealized when nothing\n // keeps that promise. Both are warnings, and the second is the one that\n // survives a healthy-looking row — a capability with four requirements and\n // three kept reports nothing at all through the first.\n //\n // The suspension falls out of the DATA rather than a branch: `keptBy` absent\n // means nobody looked, and a promise nobody looked for is not a promise\n // reported unkept.\n const unrealizedRequirements = rows.flatMap((row) =>\n (row.requirements ?? [])\n .filter((req) => req.realizedBy.length === 0 && req.keptBy?.length === 0)\n .map((req) => ({ capability: row.id, id: req.id, name: req.name })),\n );\n return [\n ...authored,\n ...unrealizedFindings(vocab, usedCapabilities(rows)),\n ...requirementUnrealizedFindings(unrealizedRequirements),\n ];\n}\n\nexport function externalProducerOf(\n message: string,\n land: LoadedDoc | null,\n landSvcOf: ((id: string) => DeclaredService) | null,\n known: ReadonlySet<RawServiceId>,\n): string | null {\n if (land === null || land.errors.length > 0 || landSvcOf === null) return null;\n const knownIds: ReadonlySet<string> = known;\n const byId = new Map(land.elements.map((e) => [e.id, e]));\n for (const r of land.relationships) {\n if (r.publishes !== message) continue;\n if (knownIds.has(landSvcOf(r.source))) continue;\n for (let id = r.source; ; ) {\n const e = byId.get(id);\n if (e !== undefined && e.tags.includes(EXTERNAL_TAG)) return e.title;\n const dot = id.lastIndexOf(\".\");\n if (dot === -1) break;\n id = id.slice(0, dot);\n }\n }\n return null;\n}\n"]}
|
|
1
|
+
{"version":3,"file":"fleet-shape.js","sourceRoot":"","sources":["../../../../src/commands/validate/checks/fleet-shape.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * Fleet-shape advisories over the parsed landscape: the way a fleet map stops\n * answering questions as it grows, caught while the map is already in memory.\n *\n * The shape becomes visible at service three rather than service two.\n * Ubiquitous infrastructure — logging Kafka, an identity provider, service\n * discovery — takes one inbound edge per modelled service until the map is a\n * hairball around it; deleting the element fixes the picture and loses \"who\n * depends on the Identity Provider\", which is the question the map exists to\n * answer during an incident. Tagging keeps both: the fleet view excludes\n * `#platform`, and a platform view keeps the dependents.\n *\n * The DATASTORE half of this pair moved to `../fleet/map/consumers.ts` when its\n * consumer count stopped being a fact about the map alone: a private store now\n * lives nested inside its owner's element, and the services reaching it are\n * attested in their own models (E3/R1). The census both checks read is built\n * there and handed in here as `consumers`, so a hub and a store can never\n * disagree about who consumes what.\n *\n * loam still reads no code and decides no policy: the team chooses what counts\n * as platform — this warning only names a shape that is usually wrong.\n */\nimport { existsSync } from \"node:fs\";\nimport { type Elem } from \"../../../core/c4/likec4.js\";\nimport { type Finding } from \"../../../core/vocabulary/report.js\";\nimport { type ServiceEntry } from \"../../../core/repo/entries.js\";\nimport { permissionsPath, servicePathsAt } from \"../../../core/repo/paths.js\";\nimport { readVocabulary } from \"../../../core/permissions/permissions.js\";\nimport {\n capabilityDocFindings,\n capabilityRequirementIndex,\n docMissingFindings,\n gradableCapabilityIds,\n invalidVocabularyFinding,\n unrealizedFindings,\n} from \"../../../core/capabilities/findings.js\";\nimport { useCaseRequirementClaims } from \"../../../core/usecases/capability.js\";\nimport type { ParsedView } from \"../../../core/c4/parsed/dynamic-views.js\";\nimport { capabilityRollup, usedCapabilities } from \"../../../core/capabilities/rollup.js\";\nimport { requirementUnrealizedFindings } from \"../../../core/capabilities/realizes/findings.js\";\nimport { FleetContext } from \"../../../core/fleet-context.js\";\nimport { EXTERNAL_TAG, PLATFORM_TAG } from \"../../../core/vocabulary/maturity.js\";\nimport type { DocsDir } from \"../../../core/kernel/ids/dirs.js\";\n\n/**\n * Consumers at which an untagged external hub starts to warn. Three, not two:\n * two services sharing a dependency is a fact about those two services, the\n * same edge from three is a pattern about the element — and three is where\n * the shared hub starts making the map unreadable.\n */\nexport const PLATFORM_CANDIDATE_MIN_CONSUMERS = 3;\n\nexport interface FleetShape {\n /** Service-LEVEL elements — the fleet map's own boxes (landscape.ts `drawn`). */\n drawn: Elem[];\n /** The services/<id>/ directories that exist. */\n services: ReadonlySet<string>;\n /**\n * Distinct consumer SERVICES of an element, its nested children included —\n * `../fleet/map/consumers.ts` owns the derivation and both checks that read a\n * consumer count share this one instance of it.\n */\n consumers: (elementId: string) => string[];\n}\n\nexport function fleetShapeFindings(shape: FleetShape): Finding[] {\n const { drawn, services } = shape;\n const findings: Finding[] = [];\n\n for (const e of drawn) {\n // An element that stands for a real service is the fleet itself, not its\n // shape: many consumers is what a well-used service looks like.\n if (e.service !== undefined || services.has(e.title)) continue;\n if (!e.tags.includes(EXTERNAL_TAG) || e.tags.includes(PLATFORM_TAG)) continue;\n const consumers = shape.consumers(e.id);\n if (consumers.length < PLATFORM_CANDIDATE_MIN_CONSUMERS) continue;\n findings.push({\n severity: \"warn\",\n code: \"landscape.platform-candidate\",\n subject: e.title,\n message:\n `landscape: '${e.title}' is consumed by ${consumers.length} services (${consumers.join(\", \")}) ` +\n `and is not tagged #platform — a hub like this takes one more edge per modelled service until ` +\n `the fleet view is unreadable. Declare \\`tag platform\\` in the specification block and tag the ` +\n `element (LikeC4 refuses an undeclared tag, so both steps are needed): the fleet view then ` +\n `excludes it, and a platform view over \\`include * -> element.tag = #platform\\` keeps ` +\n `\"who depends on it\" answerable`,\n });\n }\n return findings;\n}\n\n/**\n * The authorization vocabulary graded against the fleet that uses it — the half\n * of the axis no single service can answer.\n *\n * `permissions.invalid` is an error and it is the only one reported when it\n * fires: a vocabulary nobody can read resolves nothing, so every `Requires:`\n * line in the fleet would follow it as `permissions.unknown`, and a hundred\n * findings about one broken file is a cascade, not a diagnosis.\n *\n * `permissions.unenforced` is the mirror of `api.ungoverned`, and it earns its\n * place for the same reason: a vocabulary is only worth what cites it. A\n * permission nothing requires is either a rule that was removed and left its\n * word behind, or a word nobody adopted — both are drift, and neither is\n * visible from inside the file. Warn, because the honest answer is sometimes\n * \"not modelled yet\".\n */\nexport async function permissionFindings(\n docsDir: DocsDir,\n services: ServiceEntry[],\n fleet: FleetContext,\n): Promise<Finding[]> {\n const vocabulary = await readVocabulary(permissionsPath(docsDir));\n if (!vocabulary.present) return [];\n if (vocabulary.invalid !== undefined) {\n return [\n {\n severity: \"error\",\n code: \"permissions.invalid\",\n message:\n `landscape: architecture/permissions.yaml does not read as a vocabulary — ${vocabulary.invalid}. ` +\n \"Every `Requires:` line in the fleet resolves against this file, so none of them can be graded until it parses. \" +\n \"The shape is `subjects: {<kind>: {description}}` then `permissions: {<kind>: {<name>: {description, owned_by, enforced_by}}}`.\",\n },\n ];\n }\n const used = new Set<string>();\n for (const entry of services) {\n const paths = servicePathsAt(entry.dir);\n for (const path of [paths.spec, paths.archSpec]) {\n // Existence first, and for BOTH documents: arch.spec.md is optional (most\n // of a legacy fleet has none) and spec.md is missing on a half-adopted\n // service. `FleetContext.readRequirements` throws ENOENT, which surfaces\n // as `repository-unavailable` and takes the whole `--all` run down — an\n // absent optional artifact must never be able to do that.\n if (!existsSync(path)) continue;\n const reqs = await fleet.readRequirements(path);\n for (const r of reqs) {\n if (r.kind !== \"REMOVED\") for (const p of r.requires) used.add(p);\n }\n }\n }\n const unenforced = [...vocabulary.byId.keys()].filter((id) => !used.has(id));\n if (unenforced.length === 0) return [];\n return [\n {\n severity: \"warn\",\n code: \"permissions.unenforced\",\n message: `landscape: ${unenforced.length} declared permission(s) that no requirement's \\`Requires:\\` line names — write the requirement that gates on each, or drop the declaration`,\n details: unenforced,\n },\n ];\n}\n\n/**\n * The capability vocabulary graded against the fleet that realizes it — the\n * axis's fleet half, beside permissionFindings and shaped by the same two\n * verdicts. `capability.invalid` is a `validate --all` run's ONE finding about\n * an unreadable file — fleet scope, so single-target runs stay silent about\n * it — and it suppresses the rest of the family (the unknown\n * grades at every service target return [] for the same vocabulary — one\n * breach, one finding, never a cascade). `capability.unrealized` is one warn\n * PER declared-but-unnamed capability, subject = the id, because the roadmap's\n * criterion is 'one warning per capability, not one per service'. Holding\n * NEITHER `architecture/capabilities.yaml` nor `capabilities/` is total\n * silence: the fleet's own files are this axis's opt-in, either of them.\n *\n * The used set comes through capabilityRollup with the fleet's own cached\n * reader, so under --all the walk re-parses nothing the service targets have\n * not already paid for — and the rollup, the warn and `loam list capabilities`\n * can never disagree about what \"realized\" means.\n *\n * The AUTHORED documents are graded here too, and they ride this function\n * rather than a target of their own for `viewsStaleFindings`' reason one tree\n * over: a capability belongs to no service, so only the fleet run has the whole\n * vocabulary and the enumerated tree in view at once. Their reads are NOT\n * wrapped, deliberately — a `capabilities/<id>/spec.md` that is not UTF-8 takes\n * the run down exactly as a `services/<id>/spec.md` does, because a document\n * loam cannot decode is a refusal everywhere else in the product and a quiet\n * \"zero requirements\" here would grade a whole capability green on bytes\n * nobody read.\n */\nexport async function capabilityFleetFindings(\n docsDir: DocsDir,\n services: ServiceEntry[],\n fleet: FleetContext,\n flows: readonly ParsedView[] | null,\n): Promise<Finding[]> {\n const vocab = await fleet.capabilities(docsDir);\n if (!vocab.present) return [];\n const read = (p: string) => fleet.readRequirements(p);\n const authored: Finding[] = docMissingFindings(vocab.tree);\n for (const doc of vocab.tree.docs) authored.push(...capabilityDocFindings(await read(doc.spec), doc.id));\n\n const invalid = invalidVocabularyFinding(vocab);\n if (invalid !== null) return [invalid, ...authored];\n // A USE CASE IS THE OTHER WAY TO KEEP A PROMISE, and it is the only carrier a\n // cross-service criterion has: \"I enter a login and a password and I am in\"\n // belongs to no single service's spec, so a fleet could satisfy it perfectly\n // through a `#cap-`/`#req-` tagged flow and still be told nobody realizes it.\n //\n // The claims go INTO the rollup rather than beside it, so both unrealized\n // grades and `loam list capabilities` read one set of rows. `flows === null`\n // means loam could not READ the flows (no preload, or an `architecture/` that\n // did not parse); the key is then OMITTED rather than empty, and every\n // `keptBy` below is `undefined` — the three-state contract `rollup.ts` states.\n const index = await capabilityRequirementIndex(vocab, read);\n const keptByFlows =\n flows === null\n ? undefined\n : useCaseRequirementClaims(flows, gradableCapabilityIds(vocab), (c) => index.byCapability.get(c));\n const rows = await capabilityRollup({ services, vocab, read, ...(keptByFlows === undefined ? {} : { keptByFlows }) });\n\n // The two unrealized grades read the SAME rollup, which is what keeps them\n // from disagreeing about the word: a capability is unrealized when nothing\n // names it by any join, and one of its requirements is unrealized when nothing\n // keeps that promise. Both are warnings, and the second is the one that\n // survives a healthy-looking row — a capability with four requirements and\n // three kept reports nothing at all through the first.\n //\n // The suspension falls out of the DATA rather than a branch: `keptBy` absent\n // means nobody looked, and a promise nobody looked for is not a promise\n // reported unkept.\n const unrealizedRequirements = rows.flatMap((row) =>\n (row.requirements ?? [])\n .filter((req) => req.realizedBy.length === 0 && req.keptBy?.length === 0)\n .map((req) => ({ capability: row.id, id: req.id, name: req.name })),\n );\n return [\n ...authored,\n ...unrealizedFindings(vocab, usedCapabilities(rows)),\n ...requirementUnrealizedFindings(unrealizedRequirements),\n ];\n}\n"]}
|
|
@@ -10,11 +10,13 @@
|
|
|
10
10
|
* concerned.
|
|
11
11
|
*
|
|
12
12
|
* Resolution is against the SERVICE'S OWN model — deliberately never the
|
|
13
|
-
* landscape. A
|
|
14
|
-
*
|
|
15
|
-
* landscape
|
|
16
|
-
*
|
|
17
|
-
*
|
|
13
|
+
* landscape. A store one service owns may be nested inside that service's
|
|
14
|
+
* element or drawn as a fleet-level peer, whichever the team chose (loam grades
|
|
15
|
+
* neither: `landscape.datastore-private`, which used to push it into the first,
|
|
16
|
+
* was retired 2026-09-07). Only the nested placement puts it in the landscape's
|
|
17
|
+
* interior at all, so the landscape is not a set this check could rely on, while
|
|
18
|
+
* the model carries everything the service touches — and that is the set the
|
|
19
|
+
* on-call file has to agree with. An id resolves by the same names every other join
|
|
18
20
|
* uses: the element's likec4 id, its `metadata { service }` binding, or its
|
|
19
21
|
* title. Exact match, no case folding — a fold would be a permanent
|
|
20
22
|
* equivalence semantic no other loam join has, able to silently join two
|
|
@@ -45,7 +47,8 @@ export function healthDependencyFindings(deps) {
|
|
|
45
47
|
message: `${service}: health.yaml declares dependency '${dep}' but nothing in ` +
|
|
46
48
|
`${modelPath} answers to that name — either a dependency nobody ` +
|
|
47
49
|
`modelled, or one that no longer exists. Model it (an external system as its own element ` +
|
|
48
|
-
`with the edge this service actually has; a
|
|
50
|
+
`with the edge this service actually has; a store this service owns as a nested container ` +
|
|
51
|
+
`or as its own element, whichever the map already does), or ` +
|
|
49
52
|
`delete the entry` +
|
|
50
53
|
(close.length > 0 ? `. Did you mean: ${close.join(", ")}?` : ""),
|
|
51
54
|
});
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"health-deps.js","sourceRoot":"","sources":["../../../../src/commands/validate/checks/health-deps.ts"],"names":[],"mappings":"AAAA
|
|
1
|
+
{"version":3,"file":"health-deps.js","sourceRoot":"","sources":["../../../../src/commands/validate/checks/health-deps.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;GAwBG;AACH,OAAO,EAAE,QAAQ,EAAE,MAAM,0BAA0B,CAAC;AACpD,OAAO,EAAa,MAAM,4BAA4B,CAAC;AACvD,OAAO,EAAE,cAAc,EAAE,MAAM,qCAAqC,CAAC;AACrE,OAAO,EAAgB,MAAM,oCAAoC,CAAC;AAoBlE,MAAM,UAAU,wBAAwB,CAAC,IAAgB;IACvD,MAAM,EAAE,OAAO,EAAE,YAAY,EAAE,QAAQ,EAAE,SAAS,EAAE,GAAG,IAAI,CAAC;IAC5D,yEAAyE;IACzE,4EAA4E;IAC5E,gCAAgC;IAChC,IAAI,QAAQ,CAAC,MAAM,KAAK,CAAC;QAAE,OAAO,EAAE,CAAC;IACrC,MAAM,QAAQ,GAAc,EAAE,CAAC;IAC/B,KAAK,MAAM,GAAG,IAAI,YAAY,EAAE,CAAC;QAC/B,IAAI,QAAQ,CAAC,IAAI,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,EAAE,KAAK,GAAG,IAAI,cAAc,CAAC,CAAC,CAAC,KAAK,GAAG,CAAC;YAAE,SAAS;QAC9E,MAAM,IAAI,GAAG,CAAC,GAAG,IAAI,GAAG,CAAC,QAAQ,CAAC,OAAO,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,CAAC,EAAE,EAAE,cAAc,CAAC,CAAC,CAAW,CAAC,CAAC,CAAC,CAAC,CAAC;QACxF,MAAM,KAAK,GAAG,QAAQ,CAAC,GAAG,EAAE,IAAI,CAAC,CAAC;QAClC,QAAQ,CAAC,IAAI,CAAC;YACZ,QAAQ,EAAE,MAAM;YAChB,IAAI,EAAE,8BAA8B;YACpC,OAAO,EAAE,OAAO;YAChB,OAAO,EACL,GAAG,OAAO,sCAAsC,GAAG,mBAAmB;gBACtE,GAAG,SAAS,qDAAqD;gBACjE,0FAA0F;gBAC1F,2FAA2F;gBAC3F,6DAA6D;gBAC7D,kBAAkB;gBAClB,CAAC,KAAK,CAAC,MAAM,GAAG,CAAC,CAAC,CAAC,CAAC,mBAAmB,KAAK,CAAC,IAAI,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,CAAC;SACnE,CAAC,CAAC;IACL,CAAC;IACD,OAAO,QAAQ,CAAC;AAClB,CAAC","sourcesContent":["/**\n * The health.yaml → model reconciliation: every id the `dependencies:` block\n * declares must be something this service's OWN model answers to.\n *\n * Without this reconciliation, a disabled optional service registry can leave\n * the model, landscape, arch spec and runbook corrected while health.yaml still\n * declares `serviceRegistry` as a `critical: startup` dependency under a green\n * validator — in the file an on-call engineer reaches for first. Its\n * dependencies block was otherwise free text as far as validation was\n * concerned.\n *\n * Resolution is against the SERVICE'S OWN model — deliberately never the\n * landscape. A store one service owns may be nested inside that service's\n * element or drawn as a fleet-level peer, whichever the team chose (loam grades\n * neither: `landscape.datastore-private`, which used to push it into the first,\n * was retired 2026-09-07). Only the nested placement puts it in the landscape's\n * interior at all, so the landscape is not a set this check could rely on, while\n * the model carries everything the service touches — and that is the set the\n * on-call file has to agree with. An id resolves by the same names every other join\n * uses: the element's likec4 id, its `metadata { service }` binding, or its\n * title. Exact match, no case folding — a fold would be a permanent\n * equivalence semantic no other loam join has, able to silently join two\n * genuinely different names; the did-you-mean hint bridges the typo case\n * instead.\n */\nimport { closeIds } from \"../../../core/c4/arch.js\";\nimport { type Elem } from \"../../../core/c4/likec4.js\";\nimport { elementService } from \"../../../core/c4/resolve/service.js\";\nimport { type Finding } from \"../../../core/vocabulary/report.js\";\n\nexport interface HealthDeps {\n service: string;\n /**\n * The resolved path of the service's own `model.likec4`, repo-relative and\n * `/`-separated — the file this check grades and the one its message names.\n * Handed in rather than joined from the id: `services/<id>/model.likec4` is\n * right only for an unfiled service, and every sibling finding in this\n * command prints the resolved path of the file it means. Relative because a\n * message is read beside other people's runs: an absolute path names the\n * machine that ran the check, not the file in the repository.\n */\n modelPath: string;\n /** `dependencies:` ids, already muted by the caller when health.yaml is unreadable. */\n dependencies: string[];\n /** The service's own parsed model — empty when absent or invalid, which mutes the check. */\n elements: Elem[];\n}\n\nexport function healthDependencyFindings(deps: HealthDeps): Finding[] {\n const { service, dependencies, elements, modelPath } = deps;\n // An absent or unparsable model already has its own finding, and grading\n // health.yaml against a model nobody could read would manufacture warns out\n // of the wrong file's breakage.\n if (elements.length === 0) return [];\n const findings: Finding[] = [];\n for (const dep of dependencies) {\n if (elements.some((e) => e.id === dep || elementService(e) === dep)) continue;\n const pool = [...new Set(elements.flatMap((e) => [e.id, elementService(e) as string]))];\n const close = closeIds(dep, pool);\n findings.push({\n severity: \"warn\",\n code: \"health.dependency-unmodelled\",\n subject: service,\n message:\n `${service}: health.yaml declares dependency '${dep}' but nothing in ` +\n `${modelPath} answers to that name — either a dependency nobody ` +\n `modelled, or one that no longer exists. Model it (an external system as its own element ` +\n `with the edge this service actually has; a store this service owns as a nested container ` +\n `or as its own element, whichever the map already does), or ` +\n `delete the entry` +\n (close.length > 0 ? `. Did you mean: ${close.join(\", \")}?` : \"\"),\n });\n }\n return findings;\n}\n"]}
|
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* A service an active feature INTRODUCES, as `loam validate` meets it — named
|
|
3
|
+
* by the repository's own binding, or named on the command line.
|
|
4
|
+
*
|
|
5
|
+
* Until the feature archives, such a service's only documents are the
|
|
6
|
+
* feature's `specs/<service>/`: `services/` learns the name at the merge
|
|
7
|
+
* (`core/projection/introduced.ts`, the one answer to "introduces" every
|
|
8
|
+
* command reads). `validate` looked only under `services/`, so in that
|
|
9
|
+
* service's own repository a bare `loam validate` refused with
|
|
10
|
+
* `service.unknown` and "Did you mean: payment-service?" — it read the newest
|
|
11
|
+
* service in the fleet as a typo, while `loam status` and `loam doctor` in the
|
|
12
|
+
* same repository named the feature that introduces it.
|
|
13
|
+
*
|
|
14
|
+
* Two readings, two answers:
|
|
15
|
+
*
|
|
16
|
+
* the binding (a bare `loam validate`) → the introducing features are
|
|
17
|
+
* validated in the service's place. They hold every document the service
|
|
18
|
+
* has, so theirs is the grade there is to give, and it is `--feature`'s
|
|
19
|
+
* grade target for target — never a slice of it, which would be a green
|
|
20
|
+
* over whatever the slice left out. The run says first which features it
|
|
21
|
+
* validated and why, in `show`'s words for the same fact, so a reader never
|
|
22
|
+
* takes a feature's findings for the service's own.
|
|
23
|
+
* a name (`--service <id>`, the positional) → still a service target, which
|
|
24
|
+
* does not exist, so still `service.unknown` at error: `--service` forces
|
|
25
|
+
* the service reading by contract. Only the prose changes — it names the
|
|
26
|
+
* feature and the command that validates its documents, where it used to
|
|
27
|
+
* offer the nearest living id as the correction.
|
|
28
|
+
*
|
|
29
|
+
* A name nothing introduces keeps the near-miss refusal, byte for byte.
|
|
30
|
+
*/
|
|
31
|
+
import { closeIds } from "../../../core/c4/arch.js";
|
|
32
|
+
import { introducingFeatures } from "../../../core/projection/introduced.js";
|
|
33
|
+
import { listServices } from "../../../core/repo/repo.js";
|
|
34
|
+
/**
|
|
35
|
+
* The features a bare run validates in place of the bound service — or null
|
|
36
|
+
* when the binding names a living service, or a name nothing introduces, and
|
|
37
|
+
* the run grades it as a service (`service.unknown` and all) exactly as before.
|
|
38
|
+
*/
|
|
39
|
+
export async function introducedBinding(docsDir, resolved, fleet) {
|
|
40
|
+
// An enumerated directory is living. `introducingFeatures` would say so too,
|
|
41
|
+
// after reading every feature to learn it.
|
|
42
|
+
if (resolved.dir !== undefined)
|
|
43
|
+
return null;
|
|
44
|
+
const features = await introducingFeatures(docsDir, resolved.id, fleet);
|
|
45
|
+
return features.length === 0 ? null : { service: resolved.id, features };
|
|
46
|
+
}
|
|
47
|
+
/**
|
|
48
|
+
* The two lines a bare run prints above the report: the first is `loam show
|
|
49
|
+
* <service>`'s header for the same state, the second says what was validated
|
|
50
|
+
* instead and why those features.
|
|
51
|
+
*/
|
|
52
|
+
export function introducedLead({ service, features }) {
|
|
53
|
+
const ids = features.map((f) => f.id);
|
|
54
|
+
const several = ids.length > 1;
|
|
55
|
+
const listed = several ? `${ids.slice(0, -1).join(", ")} and ${ids.slice(-1).join("")}` : ids.join("");
|
|
56
|
+
return [
|
|
57
|
+
`${service} introduced by ${ids.join(", ")} (in flight) — not in the living docs until ${several ? "one of them" : "it"} archives`,
|
|
58
|
+
` validating ${listed} instead: ${several ? "their" : "its"} specs/${service}/ are this service's documents until then`,
|
|
59
|
+
];
|
|
60
|
+
}
|
|
61
|
+
/**
|
|
62
|
+
* The `--json` payload's `boundService`: why `targets` holds features and no
|
|
63
|
+
* service. The keys are `show`'s and `steps`' for the same fact — `living:
|
|
64
|
+
* false` and `introducedBy` — so a consumer that reads one reads them all.
|
|
65
|
+
*/
|
|
66
|
+
export function introducedJson({ service, features }) {
|
|
67
|
+
return { id: service, living: false, introducedBy: features.map((f) => f.id) };
|
|
68
|
+
}
|
|
69
|
+
/**
|
|
70
|
+
* `service.unknown`: no directory under `services/` answers to the name. The
|
|
71
|
+
* hint depends on why. A feature in flight introduces it → name the feature
|
|
72
|
+
* and the command that validates its documents, in `loam doctor`'s words for
|
|
73
|
+
* the same state. Something close is living → the near-miss, which is what
|
|
74
|
+
* makes a typo diagnosable. Neither → where to look. Never `loam adopt`, in any
|
|
75
|
+
* branch: adopting a typo would document the misspelling, and adopting an
|
|
76
|
+
* introduced service would write `services/<id>/` ahead of the merge.
|
|
77
|
+
*/
|
|
78
|
+
export async function unknownServiceFinding(docsDir, service, treePath, fleet) {
|
|
79
|
+
const ids = (await introducingFeatures(docsDir, service, fleet)).map((f) => f.id);
|
|
80
|
+
const [lead, ...others] = ids;
|
|
81
|
+
if (lead !== undefined) {
|
|
82
|
+
const several = others.length > 0;
|
|
83
|
+
return {
|
|
84
|
+
severity: "error",
|
|
85
|
+
code: "service.unknown",
|
|
86
|
+
message: `No service directory at ${treePath}: ${lead} introduces it${several ? ` (so does ${others.join(", ")})` : ""}, ` +
|
|
87
|
+
`and it is not in the living docs until ${several ? "one of them" : lead} archives. Until then its documents are ` +
|
|
88
|
+
`${several ? `the specs/${service}/ of ${ids.join(", ")}` : `${lead}'s specs/${service}/`} — run ` +
|
|
89
|
+
`\`loam validate --feature ${lead}\`${several ? `, and likewise for ${others.join(", ")},` : ""} to validate them.`,
|
|
90
|
+
};
|
|
91
|
+
}
|
|
92
|
+
const close = closeIds(service, (await listServices(docsDir, fleet)).map((s) => s.id));
|
|
93
|
+
return {
|
|
94
|
+
severity: "error",
|
|
95
|
+
code: "service.unknown",
|
|
96
|
+
message: `No service directory at ${treePath}.` +
|
|
97
|
+
(close.length > 0
|
|
98
|
+
? ` Did you mean: ${close.join(", ")}?`
|
|
99
|
+
: " `loam list services` shows what exists."),
|
|
100
|
+
};
|
|
101
|
+
}
|
|
102
|
+
//# sourceMappingURL=introduced.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"introduced.js","sourceRoot":"","sources":["../../../../src/commands/validate/checks/introduced.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA6BG;AACH,OAAO,EAAE,QAAQ,EAAE,MAAM,0BAA0B,CAAC;AAGpD,OAAO,EAAE,mBAAmB,EAAE,MAAM,wCAAwC,CAAC;AAE7E,OAAO,EAAE,YAAY,EAAE,MAAM,4BAA4B,CAAC;AAY1D;;;;GAIG;AACH,MAAM,CAAC,KAAK,UAAU,iBAAiB,CACrC,OAAgB,EAChB,QAA8C,EAC9C,KAAmB;IAEnB,6EAA6E;IAC7E,2CAA2C;IAC3C,IAAI,QAAQ,CAAC,GAAG,KAAK,SAAS;QAAE,OAAO,IAAI,CAAC;IAC5C,MAAM,QAAQ,GAAG,MAAM,mBAAmB,CAAC,OAAO,EAAE,QAAQ,CAAC,EAAE,EAAE,KAAK,CAAC,CAAC;IACxE,OAAO,QAAQ,CAAC,MAAM,KAAK,CAAC,CAAC,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC,EAAE,OAAO,EAAE,QAAQ,CAAC,EAAE,EAAE,QAAQ,EAAE,CAAC;AAC3E,CAAC;AAED;;;;GAIG;AACH,MAAM,UAAU,cAAc,CAAC,EAAE,OAAO,EAAE,QAAQ,EAAqB;IACrE,MAAM,GAAG,GAAG,QAAQ,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC;IACtC,MAAM,OAAO,GAAG,GAAG,CAAC,MAAM,GAAG,CAAC,CAAC;IAC/B,MAAM,MAAM,GAAG,OAAO,CAAC,CAAC,CAAC,GAAG,GAAG,CAAC,KAAK,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,CAAC,IAAI,CAAC,IAAI,CAAC,QAAQ,GAAG,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC,CAAC,IAAI,CAAC,EAAE,CAAC,EAAE,CAAC,CAAC,CAAC,GAAG,CAAC,IAAI,CAAC,EAAE,CAAC,CAAC;IACvG,OAAO;QACL,GAAG,OAAO,oBAAoB,GAAG,CAAC,IAAI,CAAC,IAAI,CAAC,+CAA+C,OAAO,CAAC,CAAC,CAAC,aAAa,CAAC,CAAC,CAAC,IAAI,WAAW;QACpI,gBAAgB,MAAM,aAAa,OAAO,CAAC,CAAC,CAAC,OAAO,CAAC,CAAC,CAAC,KAAK,UAAU,OAAO,2CAA2C;KACzH,CAAC;AACJ,CAAC;AAED;;;;GAIG;AACH,MAAM,UAAU,cAAc,CAAC,EAAE,OAAO,EAAE,QAAQ,EAAqB;IAKrE,OAAO,EAAE,EAAE,EAAE,OAAO,EAAE,MAAM,EAAE,KAAK,EAAE,YAAY,EAAE,QAAQ,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC,EAAE,CAAC;AACjF,CAAC;AAED;;;;;;;;GAQG;AACH,MAAM,CAAC,KAAK,UAAU,qBAAqB,CACzC,OAAgB,EAChB,OAAe,EACf,QAAgB,EAChB,KAAmB;IAEnB,MAAM,GAAG,GAAG,CAAC,MAAM,mBAAmB,CAAC,OAAO,EAAE,OAAO,EAAE,KAAK,CAAC,CAAC,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC;IAClF,MAAM,CAAC,IAAI,EAAE,GAAG,MAAM,CAAC,GAAG,GAAG,CAAC;IAC9B,IAAI,IAAI,KAAK,SAAS,EAAE,CAAC;QACvB,MAAM,OAAO,GAAG,MAAM,CAAC,MAAM,GAAG,CAAC,CAAC;QAClC,OAAO;YACL,QAAQ,EAAE,OAAO;YACjB,IAAI,EAAE,iBAAiB;YACvB,OAAO,EACL,2BAA2B,QAAQ,KAAK,IAAI,iBAAiB,OAAO,CAAC,CAAC,CAAC,aAAa,MAAM,CAAC,IAAI,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,IAAI;gBACjH,0CAA0C,OAAO,CAAC,CAAC,CAAC,aAAa,CAAC,CAAC,CAAC,IAAI,0CAA0C;gBAClH,GAAG,OAAO,CAAC,CAAC,CAAC,aAAa,OAAO,QAAQ,GAAG,CAAC,IAAI,CAAC,IAAI,CAAC,EAAE,CAAC,CAAC,CAAC,GAAG,IAAI,YAAY,OAAO,GAAG,SAAS;gBAClG,6BAA6B,IAAI,KAAK,OAAO,CAAC,CAAC,CAAC,sBAAsB,MAAM,CAAC,IAAI,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,oBAAoB;SACtH,CAAC;IACJ,CAAC;IACD,MAAM,KAAK,GAAG,QAAQ,CAAC,OAAO,EAAE,CAAC,MAAM,YAAY,CAAC,OAAO,EAAE,KAAK,CAAC,CAAC,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC;IACvF,OAAO;QACL,QAAQ,EAAE,OAAO;QACjB,IAAI,EAAE,iBAAiB;QACvB,OAAO,EACL,2BAA2B,QAAQ,GAAG;YACtC,CAAC,KAAK,CAAC,MAAM,GAAG,CAAC;gBACf,CAAC,CAAC,kBAAkB,KAAK,CAAC,IAAI,CAAC,IAAI,CAAC,GAAG;gBACvC,CAAC,CAAC,0CAA0C,CAAC;KAClD,CAAC;AACJ,CAAC","sourcesContent":["/**\n * A service an active feature INTRODUCES, as `loam validate` meets it — named\n * by the repository's own binding, or named on the command line.\n *\n * Until the feature archives, such a service's only documents are the\n * feature's `specs/<service>/`: `services/` learns the name at the merge\n * (`core/projection/introduced.ts`, the one answer to \"introduces\" every\n * command reads). `validate` looked only under `services/`, so in that\n * service's own repository a bare `loam validate` refused with\n * `service.unknown` and \"Did you mean: payment-service?\" — it read the newest\n * service in the fleet as a typo, while `loam status` and `loam doctor` in the\n * same repository named the feature that introduces it.\n *\n * Two readings, two answers:\n *\n * the binding (a bare `loam validate`) → the introducing features are\n * validated in the service's place. They hold every document the service\n * has, so theirs is the grade there is to give, and it is `--feature`'s\n * grade target for target — never a slice of it, which would be a green\n * over whatever the slice left out. The run says first which features it\n * validated and why, in `show`'s words for the same fact, so a reader never\n * takes a feature's findings for the service's own.\n * a name (`--service <id>`, the positional) → still a service target, which\n * does not exist, so still `service.unknown` at error: `--service` forces\n * the service reading by contract. Only the prose changes — it names the\n * feature and the command that validates its documents, where it used to\n * offer the nearest living id as the correction.\n *\n * A name nothing introduces keeps the near-miss refusal, byte for byte.\n */\nimport { closeIds } from \"../../../core/c4/arch.js\";\nimport type { FleetContext } from \"../../../core/fleet-context.js\";\nimport type { DocsDir } from \"../../../core/kernel/ids/dirs.js\";\nimport { introducingFeatures } from \"../../../core/projection/introduced.js\";\nimport type { FeatureEntry } from \"../../../core/repo/entries.js\";\nimport { listServices } from \"../../../core/repo/repo.js\";\nimport type { ServiceTarget } from \"../../../core/repo/service-target.js\";\nimport type { Finding } from \"../../../core/vocabulary/report.js\";\n\n/** A bound service a bare run validates through the features that introduce it. */\nexport interface IntroducedBinding {\n /** The service loam.json names. */\n service: string;\n /** `introducingFeatures`' answer, ordered by id — never empty. */\n features: FeatureEntry[];\n}\n\n/**\n * The features a bare run validates in place of the bound service — or null\n * when the binding names a living service, or a name nothing introduces, and\n * the run grades it as a service (`service.unknown` and all) exactly as before.\n */\nexport async function introducedBinding(\n docsDir: DocsDir,\n resolved: Extract<ServiceTarget, { ok: true }>,\n fleet: FleetContext,\n): Promise<IntroducedBinding | null> {\n // An enumerated directory is living. `introducingFeatures` would say so too,\n // after reading every feature to learn it.\n if (resolved.dir !== undefined) return null;\n const features = await introducingFeatures(docsDir, resolved.id, fleet);\n return features.length === 0 ? null : { service: resolved.id, features };\n}\n\n/**\n * The two lines a bare run prints above the report: the first is `loam show\n * <service>`'s header for the same state, the second says what was validated\n * instead and why those features.\n */\nexport function introducedLead({ service, features }: IntroducedBinding): string[] {\n const ids = features.map((f) => f.id);\n const several = ids.length > 1;\n const listed = several ? `${ids.slice(0, -1).join(\", \")} and ${ids.slice(-1).join(\"\")}` : ids.join(\"\");\n return [\n `${service} introduced by ${ids.join(\", \")} (in flight) — not in the living docs until ${several ? \"one of them\" : \"it\"} archives`,\n ` validating ${listed} instead: ${several ? \"their\" : \"its\"} specs/${service}/ are this service's documents until then`,\n ];\n}\n\n/**\n * The `--json` payload's `boundService`: why `targets` holds features and no\n * service. The keys are `show`'s and `steps`' for the same fact — `living:\n * false` and `introducedBy` — so a consumer that reads one reads them all.\n */\nexport function introducedJson({ service, features }: IntroducedBinding): {\n id: string;\n living: false;\n introducedBy: string[];\n} {\n return { id: service, living: false, introducedBy: features.map((f) => f.id) };\n}\n\n/**\n * `service.unknown`: no directory under `services/` answers to the name. The\n * hint depends on why. A feature in flight introduces it → name the feature\n * and the command that validates its documents, in `loam doctor`'s words for\n * the same state. Something close is living → the near-miss, which is what\n * makes a typo diagnosable. Neither → where to look. Never `loam adopt`, in any\n * branch: adopting a typo would document the misspelling, and adopting an\n * introduced service would write `services/<id>/` ahead of the merge.\n */\nexport async function unknownServiceFinding(\n docsDir: DocsDir,\n service: string,\n treePath: string,\n fleet: FleetContext,\n): Promise<Finding> {\n const ids = (await introducingFeatures(docsDir, service, fleet)).map((f) => f.id);\n const [lead, ...others] = ids;\n if (lead !== undefined) {\n const several = others.length > 0;\n return {\n severity: \"error\",\n code: \"service.unknown\",\n message:\n `No service directory at ${treePath}: ${lead} introduces it${several ? ` (so does ${others.join(\", \")})` : \"\"}, ` +\n `and it is not in the living docs until ${several ? \"one of them\" : lead} archives. Until then its documents are ` +\n `${several ? `the specs/${service}/ of ${ids.join(\", \")}` : `${lead}'s specs/${service}/`} — run ` +\n `\\`loam validate --feature ${lead}\\`${several ? `, and likewise for ${others.join(\", \")},` : \"\"} to validate them.`,\n };\n }\n const close = closeIds(service, (await listServices(docsDir, fleet)).map((s) => s.id));\n return {\n severity: \"error\",\n code: \"service.unknown\",\n message:\n `No service directory at ${treePath}.` +\n (close.length > 0\n ? ` Did you mean: ${close.join(\", \")}?`\n : \" `loam list services` shows what exists.\"),\n };\n}\n"]}
|
|
@@ -13,7 +13,7 @@
|
|
|
13
13
|
* requirements and answer in findings.
|
|
14
14
|
*/
|
|
15
15
|
import {} from "../../../core/vocabulary/report.js";
|
|
16
|
-
import { requirementsMissingScenarios } from "../../../core/document/scenarios.js";
|
|
16
|
+
import { assertionlessFindings, examplesFindings, missingScenariosFinding, requirementsMissingScenarios, steplessFindings, } from "../../../core/document/scenarios.js";
|
|
17
17
|
import { requirementIdProblems } from "../../../core/document/spec.js";
|
|
18
18
|
import { closeIds, coversCandidates, entryResolves, parseCoversEntry, } from "../../../core/c4/arch.js";
|
|
19
19
|
import {} from "../../../core/permissions/permissions.js";
|
|
@@ -243,12 +243,22 @@ export function coverageFinding(label, reqs) {
|
|
|
243
243
|
message: `${label} covered (${reqs.length} requirement${reqs.length === 1 ? "" : "s"}, all with scenarios)`,
|
|
244
244
|
};
|
|
245
245
|
}
|
|
246
|
-
return {
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
246
|
+
return { ...missingScenariosFinding(label, missing), text: { detailPrefix: "- " } };
|
|
247
|
+
}
|
|
248
|
+
/**
|
|
249
|
+
* The scenario-shape run of the battery every requirement document gets, in
|
|
250
|
+
* report order — stepless, assertionless, examples — spelled once for the four
|
|
251
|
+
* documents the header names, so they cannot drift apart. What differs between
|
|
252
|
+
* the four stays at each call site: a delta's coverage finding carries its
|
|
253
|
+
* service as `subject`, a living spec.md with no requirements reports
|
|
254
|
+
* `spec.no-requirements` in coverage's place, and duplicate names and
|
|
255
|
+
* requirement ids are graded on LIVING documents (`duplicateRequirementFindings`).
|
|
256
|
+
*/
|
|
257
|
+
export function scenarioShapeFindings(label, subject, reqs) {
|
|
258
|
+
return [
|
|
259
|
+
...steplessFindings(label, subject, reqs),
|
|
260
|
+
...assertionlessFindings(label, subject, reqs),
|
|
261
|
+
...examplesFindings(label, subject, reqs),
|
|
262
|
+
];
|
|
253
263
|
}
|
|
254
264
|
//# sourceMappingURL=requirements.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"requirements.js","sourceRoot":"","sources":["../../../../src/commands/validate/checks/requirements.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * What a requirement document owes, checked the same way wherever it lives.\n *\n * The same `### Requirement:` grammar is authored in four places — a service's\n * spec.md and arch.spec.md, and a feature's per-service deltas of both — and\n * every breach below carries from a delta into the living document through the\n * archive merge. So the checks are one set called from both target kinds\n * (`service/` and `feature/`): grading a delta more leniently than the file it\n * is about to become is how a defect acquires a signature saying it was\n * reviewed.\n *\n * None of these read the filesystem and none of them print — they take parsed\n * requirements and answer in findings.\n */\nimport { type Finding } from \"../../../core/vocabulary/report.js\";\nimport { requirementsMissingScenarios } from \"../../../core/document/scenarios.js\";\nimport { requirementIdProblems, type Requirement } from \"../../../core/document/spec.js\";\nimport {\n closeIds,\n coversCandidates,\n entryResolves,\n parseCoversEntry,\n type CoverageScope,\n type CoversEntry,\n} from \"../../../core/c4/arch.js\";\nimport { type Vocabulary } from \"../../../core/permissions/permissions.js\";\nimport { type Operation } from \"../../../core/openapi/doc.js\";\n\n/** The parsed Covers entries of every requirement that will live (REMOVED covers nothing). */\nexport function coversEntries(reqs: Requirement[]): CoversEntry[] {\n return reqs.filter((r) => r.kind !== \"REMOVED\").flatMap((r) => r.covers.map(parseCoversEntry));\n}\n\n/**\n * `spec.duplicate-requirement` — two `### Requirement:` blocks with one name in\n * one LIVING document. Nothing else catches it, and the merge algebra\n * (applyRequirementDelta) matches by name and edits only the FIRST match: a\n * later archive rewrites one copy and the other survives as a stale snapshot\n * of whatever the requirement used to say. Per file on purpose — spec.md and\n * arch.spec.md are separate requirement namespaces (their merges never cross\n * files), so one name appearing in both is legal and unflagged.\n */\nexport function duplicateRequirementFindings(reqs: Requirement[], where: string, subject: string): Finding[] {\n const counts = new Map<string, number>();\n for (const r of reqs) counts.set(r.name, (counts.get(r.name) ?? 0) + 1);\n return [...counts]\n .filter(([, n]) => n > 1)\n .map(([name, n]) => ({\n severity: \"error\" as const,\n code: \"spec.duplicate-requirement\",\n subject,\n message: `${where}: requirement '${name}' is defined ${n} times — a merge edits only the first, every other copy lives on stale; keep exactly one`,\n }));\n}\n\n/** Stable IDs are optional, but once authored they must select exactly one requirement. */\nexport function requirementIdFindings(reqs: Requirement[], where: string, subject: string): Finding[] {\n return requirementIdProblems(reqs).map((problem) => {\n if (problem.kind === \"invalid\") {\n return {\n severity: \"error\" as const,\n code: \"spec.requirement-id-invalid\",\n subject,\n message: `${where}: requirement '${problem.requirement}' has invalid Requirement-ID '${problem.value}' — use 1-128 characters matching [A-Za-z][A-Za-z0-9._-]*`,\n };\n }\n if (problem.kind === \"repeated\") {\n return {\n severity: \"error\" as const,\n code: \"spec.requirement-id-repeated\",\n subject,\n message: `${where}: requirement '${problem.requirement}' declares Requirement-ID ${problem.values.length} times — identity must be declared exactly once`,\n };\n }\n return {\n severity: \"error\" as const,\n code: \"spec.requirement-id-duplicate\",\n subject,\n message: `${where}: Requirement-ID '${problem.id}' is shared by ${problem.requirements.map((name) => `'${name}'`).join(\", \")} — one ID may identify only one requirement`,\n };\n });\n}\n\n/**\n * The two list lines of the requirement grammar, exactly as core/document/spec.ts spells\n * them (mirrored here, not exported from there, because the parser's grammar is\n * its own; a drift shows up as this check counting differently than the parser\n * assigns). A SECOND matching line in one requirement body REPLACES the first —\n * assignment, not append, the documented keep-last quirk — so the author's\n * \"long list in two lines\" pattern silently loses its first line.\n */\nconst OPERATIONS_LINE_RE = /^\\s*Operations?:\\s*(.+?)\\s*$/i;\nconst COVERS_LINE_RE = /^\\s*Covers?:\\s*(.+?)\\s*$/i;\n\n/**\n * `spec.repeated-operations` / `spec.repeated-covers` — warn on the silent\n * loss, keep the keep-last semantics (changing them would re-read every spec\n * in the fleet). Scenario bodies never count: the parser only assigns from the\n * requirement's own body lines, and `Requirement.text` is exactly those.\n * REMOVED requirements are exempt the way coversEntries exempts them — content\n * on its way out obliges nothing.\n */\nexport function repeatedListLineFindings(reqs: Requirement[], where: string, subject: string): Finding[] {\n const out: Finding[] = [];\n for (const r of reqs) {\n if (r.kind === \"REMOVED\") continue;\n for (const { re, label, code } of [\n { re: OPERATIONS_LINE_RE, label: \"Operations:\", code: \"spec.repeated-operations\" },\n { re: COVERS_LINE_RE, label: \"Covers:\", code: \"spec.repeated-covers\" },\n ]) {\n const n = r.text.filter((line) => re.test(line)).length;\n if (n < 2) continue;\n out.push({\n severity: \"warn\",\n code,\n subject,\n message: `${where}: requirement '${r.name}' has ${n} '${label}' lines — the last REPLACES the others (assignment, not append), the earlier list is silently lost; merge them into one comma-separated line`,\n });\n }\n }\n return out;\n}\n\n/**\n * `covers.unknown` — the typo guard on the Covers: line. Warn, not error: the\n * axis is advisory end to end, and a wrong id already costs its author the\n * coverage they wrote the line for. The hint offers only real ids (closeIds's\n * rule), and says where resolution looked when there is nothing close.\n * `healthUnreadable` mutes the alert:/sli: forms only: against a health.yaml\n * nobody could read, \"did you mean\" is a false diagnosis of a typo —\n * health.invalid (emitted by the service target) is the honest one.\n */\n/** Which document is being graded, and under whose name the finding is filed. */\nexport interface CoverageTarget {\n where: string;\n subject: string;\n}\n\nexport function coversUnknownFindings(\n reqs: Requirement[],\n target: CoverageTarget,\n scope: CoverageScope,\n healthUnreadable = false,\n): Finding[] {\n const { where, subject } = target;\n const out: Finding[] = [];\n for (const r of reqs) {\n if (r.kind === \"REMOVED\") continue;\n for (const raw of r.covers) {\n const entry = parseCoversEntry(raw);\n if (healthUnreadable && (entry.form === \"alert\" || entry.form === \"sli\")) continue;\n if (entryResolves(entry, scope)) continue;\n const close = coversCandidates(entry, scope);\n out.push({\n severity: \"warn\",\n code: \"covers.unknown\",\n subject,\n message:\n `${where}: requirement '${r.name}' — Covers: '${raw}' resolves to nothing` +\n (close.length > 0\n ? `. Did you mean: ${close.join(\", \")}?`\n : \" in the model, the landscape or health.yaml\"),\n });\n }\n }\n return out;\n}\n\n/**\n * `permissions.unknown` — the typo guard on the `Requires:` line, and an ERROR\n * where its `Covers:` sibling is a warning.\n *\n * The two differ because what they promise differs. `Covers:` is advisory end\n * to end: a wrong id costs its author the coverage they wrote the line for and\n * nothing else. `Requires:` names authorization the service actually enforces,\n * and the failure it guards is the one an unaided reader cannot see — an\n * invented permission reads exactly like a real one, in the requirement, in the\n * generated scenario, and in the test somebody then writes against it. Its\n * grade is `Operations:`', not `Covers:`'.\n *\n * Silent on ONE case only: an unreadable vocabulary, where\n * `permissions.invalid` is the honest finding and grading every entry against a\n * file nobody could parse is the false cascade `healthUnreadable` exists to\n * stop one axis over. A MISSING vocabulary is not that case — the `Requires:`\n * line is itself the opt-in, and a line that resolves against nothing while\n * reading like a join is the failure this whole axis exists to prevent.\n */\nexport function requiresUnknownFindings(\n reqs: Requirement[],\n target: CoverageTarget,\n vocabulary: Vocabulary,\n): Finding[] {\n const { where, subject } = target;\n if (vocabulary.invalid !== undefined) return [];\n const known = [...vocabulary.byId.keys()];\n const out: Finding[] = [];\n for (const r of reqs) {\n if (r.kind === \"REMOVED\") continue;\n for (const raw of r.requires) {\n if (vocabulary.byId.has(raw)) continue;\n const close = closeIds(raw, known);\n out.push({\n severity: \"error\",\n code: \"permissions.unknown\",\n subject,\n message:\n `${where}: requirement '${r.name}' — Requires: '${raw}' is not declared in architecture/permissions.yaml` +\n (!vocabulary.present\n ? \", which does not exist. The `Requires:` line is the opt-in: create the fleet vocabulary with this permission under its subject kind, or drop the line\"\n : close.length > 0\n ? `. Did you mean: ${close.join(\", \")}?`\n : \". Entries are '<subject>/<permission>'; declare it there, or fix the spelling\"),\n });\n }\n }\n return out;\n}\n\n/**\n * `api.response-ungoverned` — a declared 4xx/5xx response no scenario reaches.\n *\n * The gap this closes is the one a whole adoption can fall into: `api.ungoverned`\n * grades OPERATIONS against requirements, so an endpoint with one happy-path\n * requirement and twelve declared failure codes is fully governed by every\n * check loam had. The refusals are where a service's decision layer surfaces —\n * a permission denied, a field combination rejected, a state that forbids the\n * transition — and the contract is the only artifact in the corpus that already\n * enumerates them.\n *\n * A scenario \"reaches\" a code when the code appears as a standalone number\n * anywhere in its body, `Examples` rows included — which is why this arrived\n * with outline support and not before: a status column is exactly where a\n * matrix puts its codes, and before it those rows were prose in a description.\n * The match is textual and therefore generous by design. It answers \"did\n * anybody write this case down\", not \"is this scenario correct\", and the\n * failure it is built to catch is the code nobody mentioned at all. Warn, like\n * its sibling: a service may legitimately declare a code its scenarios describe\n * in words, and an error would teach authors to paste numbers into prose.\n *\n * Operations no requirement governs are skipped: `api.ungoverned` already\n * names those, and reporting each of their codes as well is one defect\n * counted twice.\n */\nexport function responseGovernanceFindings(\n service: string,\n ops: Operation[],\n livingReqs: Requirement[],\n): Finding[] {\n const ungoverned: string[] = [];\n for (const op of ops) {\n if (op.failureCodes.length === 0) continue;\n const governing = livingReqs.filter((r) => r.kind !== \"REMOVED\" && r.operations.includes(op.id));\n if (governing.length === 0) continue;\n const body = governing.flatMap((r) => r.scenarios.flatMap((s) => s.lines)).join(\"\\n\");\n const missing = op.failureCodes.filter((code) => !new RegExp(`(?<!\\\\d)${code}(?!\\\\d)`).test(body));\n if (missing.length > 0) ungoverned.push(`${op.id}: ${missing.join(\", \")}`);\n }\n if (ungoverned.length === 0) return [];\n return [\n {\n severity: \"warn\",\n code: \"api.response-ungoverned\",\n subject: service,\n message: `${service}: ${ungoverned.length} operation(s) declare a failure response no scenario reaches — write the refusal as a scenario, or as a row in one`,\n details: ungoverned,\n },\n ];\n}\n\nexport function coverageFinding(label: string, reqs: Requirement[]): Finding {\n const missing = requirementsMissingScenarios(reqs);\n if (missing.length === 0) {\n return {\n severity: \"ok\",\n code: \"requirements.covered\",\n message: `${label} covered (${reqs.length} requirement${reqs.length === 1 ? \"\" : \"s\"}, all with scenarios)`,\n };\n }\n return {\n severity: \"error\",\n code: \"requirements.missing-scenarios\",\n message: `${label}: ${missing.length} requirement(s) without a scenario`,\n details: missing.map((r) => r.name),\n text: { detailPrefix: \"- \" },\n };\n}\n"]}
|
|
1
|
+
{"version":3,"file":"requirements.js","sourceRoot":"","sources":["../../../../src/commands/validate/checks/requirements.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * What a requirement document owes, checked the same way wherever it lives.\n *\n * The same `### Requirement:` grammar is authored in four places — a service's\n * spec.md and arch.spec.md, and a feature's per-service deltas of both — and\n * every breach below carries from a delta into the living document through the\n * archive merge. So the checks are one set called from both target kinds\n * (`service/` and `feature/`): grading a delta more leniently than the file it\n * is about to become is how a defect acquires a signature saying it was\n * reviewed.\n *\n * None of these read the filesystem and none of them print — they take parsed\n * requirements and answer in findings.\n */\nimport { type Finding } from \"../../../core/vocabulary/report.js\";\nimport {\n assertionlessFindings,\n examplesFindings,\n missingScenariosFinding,\n requirementsMissingScenarios,\n steplessFindings,\n} from \"../../../core/document/scenarios.js\";\nimport { requirementIdProblems, type Requirement } from \"../../../core/document/spec.js\";\nimport {\n closeIds,\n coversCandidates,\n entryResolves,\n parseCoversEntry,\n type CoverageScope,\n type CoversEntry,\n} from \"../../../core/c4/arch.js\";\nimport { type Vocabulary } from \"../../../core/permissions/permissions.js\";\nimport { type Operation } from \"../../../core/openapi/doc.js\";\n\n/** The parsed Covers entries of every requirement that will live (REMOVED covers nothing). */\nexport function coversEntries(reqs: Requirement[]): CoversEntry[] {\n return reqs.filter((r) => r.kind !== \"REMOVED\").flatMap((r) => r.covers.map(parseCoversEntry));\n}\n\n/**\n * `spec.duplicate-requirement` — two `### Requirement:` blocks with one name in\n * one LIVING document. Nothing else catches it, and the merge algebra\n * (applyRequirementDelta) matches by name and edits only the FIRST match: a\n * later archive rewrites one copy and the other survives as a stale snapshot\n * of whatever the requirement used to say. Per file on purpose — spec.md and\n * arch.spec.md are separate requirement namespaces (their merges never cross\n * files), so one name appearing in both is legal and unflagged.\n */\nexport function duplicateRequirementFindings(reqs: Requirement[], where: string, subject: string): Finding[] {\n const counts = new Map<string, number>();\n for (const r of reqs) counts.set(r.name, (counts.get(r.name) ?? 0) + 1);\n return [...counts]\n .filter(([, n]) => n > 1)\n .map(([name, n]) => ({\n severity: \"error\" as const,\n code: \"spec.duplicate-requirement\",\n subject,\n message: `${where}: requirement '${name}' is defined ${n} times — a merge edits only the first, every other copy lives on stale; keep exactly one`,\n }));\n}\n\n/** Stable IDs are optional, but once authored they must select exactly one requirement. */\nexport function requirementIdFindings(reqs: Requirement[], where: string, subject: string): Finding[] {\n return requirementIdProblems(reqs).map((problem) => {\n if (problem.kind === \"invalid\") {\n return {\n severity: \"error\" as const,\n code: \"spec.requirement-id-invalid\",\n subject,\n message: `${where}: requirement '${problem.requirement}' has invalid Requirement-ID '${problem.value}' — use 1-128 characters matching [A-Za-z][A-Za-z0-9._-]*`,\n };\n }\n if (problem.kind === \"repeated\") {\n return {\n severity: \"error\" as const,\n code: \"spec.requirement-id-repeated\",\n subject,\n message: `${where}: requirement '${problem.requirement}' declares Requirement-ID ${problem.values.length} times — identity must be declared exactly once`,\n };\n }\n return {\n severity: \"error\" as const,\n code: \"spec.requirement-id-duplicate\",\n subject,\n message: `${where}: Requirement-ID '${problem.id}' is shared by ${problem.requirements.map((name) => `'${name}'`).join(\", \")} — one ID may identify only one requirement`,\n };\n });\n}\n\n/**\n * The two list lines of the requirement grammar, exactly as core/document/spec.ts spells\n * them (mirrored here, not exported from there, because the parser's grammar is\n * its own; a drift shows up as this check counting differently than the parser\n * assigns). A SECOND matching line in one requirement body REPLACES the first —\n * assignment, not append, the documented keep-last quirk — so the author's\n * \"long list in two lines\" pattern silently loses its first line.\n */\nconst OPERATIONS_LINE_RE = /^\\s*Operations?:\\s*(.+?)\\s*$/i;\nconst COVERS_LINE_RE = /^\\s*Covers?:\\s*(.+?)\\s*$/i;\n\n/**\n * `spec.repeated-operations` / `spec.repeated-covers` — warn on the silent\n * loss, keep the keep-last semantics (changing them would re-read every spec\n * in the fleet). Scenario bodies never count: the parser only assigns from the\n * requirement's own body lines, and `Requirement.text` is exactly those.\n * REMOVED requirements are exempt the way coversEntries exempts them — content\n * on its way out obliges nothing.\n */\nexport function repeatedListLineFindings(reqs: Requirement[], where: string, subject: string): Finding[] {\n const out: Finding[] = [];\n for (const r of reqs) {\n if (r.kind === \"REMOVED\") continue;\n for (const { re, label, code } of [\n { re: OPERATIONS_LINE_RE, label: \"Operations:\", code: \"spec.repeated-operations\" },\n { re: COVERS_LINE_RE, label: \"Covers:\", code: \"spec.repeated-covers\" },\n ]) {\n const n = r.text.filter((line) => re.test(line)).length;\n if (n < 2) continue;\n out.push({\n severity: \"warn\",\n code,\n subject,\n message: `${where}: requirement '${r.name}' has ${n} '${label}' lines — the last REPLACES the others (assignment, not append), the earlier list is silently lost; merge them into one comma-separated line`,\n });\n }\n }\n return out;\n}\n\n/**\n * `covers.unknown` — the typo guard on the Covers: line. Warn, not error: the\n * axis is advisory end to end, and a wrong id already costs its author the\n * coverage they wrote the line for. The hint offers only real ids (closeIds's\n * rule), and says where resolution looked when there is nothing close.\n * `healthUnreadable` mutes the alert:/sli: forms only: against a health.yaml\n * nobody could read, \"did you mean\" is a false diagnosis of a typo —\n * health.invalid (emitted by the service target) is the honest one.\n */\n/** Which document is being graded, and under whose name the finding is filed. */\nexport interface CoverageTarget {\n where: string;\n subject: string;\n}\n\nexport function coversUnknownFindings(\n reqs: Requirement[],\n target: CoverageTarget,\n scope: CoverageScope,\n healthUnreadable = false,\n): Finding[] {\n const { where, subject } = target;\n const out: Finding[] = [];\n for (const r of reqs) {\n if (r.kind === \"REMOVED\") continue;\n for (const raw of r.covers) {\n const entry = parseCoversEntry(raw);\n if (healthUnreadable && (entry.form === \"alert\" || entry.form === \"sli\")) continue;\n if (entryResolves(entry, scope)) continue;\n const close = coversCandidates(entry, scope);\n out.push({\n severity: \"warn\",\n code: \"covers.unknown\",\n subject,\n message:\n `${where}: requirement '${r.name}' — Covers: '${raw}' resolves to nothing` +\n (close.length > 0\n ? `. Did you mean: ${close.join(\", \")}?`\n : \" in the model, the landscape or health.yaml\"),\n });\n }\n }\n return out;\n}\n\n/**\n * `permissions.unknown` — the typo guard on the `Requires:` line, and an ERROR\n * where its `Covers:` sibling is a warning.\n *\n * The two differ because what they promise differs. `Covers:` is advisory end\n * to end: a wrong id costs its author the coverage they wrote the line for and\n * nothing else. `Requires:` names authorization the service actually enforces,\n * and the failure it guards is the one an unaided reader cannot see — an\n * invented permission reads exactly like a real one, in the requirement, in the\n * generated scenario, and in the test somebody then writes against it. Its\n * grade is `Operations:`', not `Covers:`'.\n *\n * Silent on ONE case only: an unreadable vocabulary, where\n * `permissions.invalid` is the honest finding and grading every entry against a\n * file nobody could parse is the false cascade `healthUnreadable` exists to\n * stop one axis over. A MISSING vocabulary is not that case — the `Requires:`\n * line is itself the opt-in, and a line that resolves against nothing while\n * reading like a join is the failure this whole axis exists to prevent.\n */\nexport function requiresUnknownFindings(\n reqs: Requirement[],\n target: CoverageTarget,\n vocabulary: Vocabulary,\n): Finding[] {\n const { where, subject } = target;\n if (vocabulary.invalid !== undefined) return [];\n const known = [...vocabulary.byId.keys()];\n const out: Finding[] = [];\n for (const r of reqs) {\n if (r.kind === \"REMOVED\") continue;\n for (const raw of r.requires) {\n if (vocabulary.byId.has(raw)) continue;\n const close = closeIds(raw, known);\n out.push({\n severity: \"error\",\n code: \"permissions.unknown\",\n subject,\n message:\n `${where}: requirement '${r.name}' — Requires: '${raw}' is not declared in architecture/permissions.yaml` +\n (!vocabulary.present\n ? \", which does not exist. The `Requires:` line is the opt-in: create the fleet vocabulary with this permission under its subject kind, or drop the line\"\n : close.length > 0\n ? `. Did you mean: ${close.join(\", \")}?`\n : \". Entries are '<subject>/<permission>'; declare it there, or fix the spelling\"),\n });\n }\n }\n return out;\n}\n\n/**\n * `api.response-ungoverned` — a declared 4xx/5xx response no scenario reaches.\n *\n * The gap this closes is the one a whole adoption can fall into: `api.ungoverned`\n * grades OPERATIONS against requirements, so an endpoint with one happy-path\n * requirement and twelve declared failure codes is fully governed by every\n * check loam had. The refusals are where a service's decision layer surfaces —\n * a permission denied, a field combination rejected, a state that forbids the\n * transition — and the contract is the only artifact in the corpus that already\n * enumerates them.\n *\n * A scenario \"reaches\" a code when the code appears as a standalone number\n * anywhere in its body, `Examples` rows included — which is why this arrived\n * with outline support and not before: a status column is exactly where a\n * matrix puts its codes, and before it those rows were prose in a description.\n * The match is textual and therefore generous by design. It answers \"did\n * anybody write this case down\", not \"is this scenario correct\", and the\n * failure it is built to catch is the code nobody mentioned at all. Warn, like\n * its sibling: a service may legitimately declare a code its scenarios describe\n * in words, and an error would teach authors to paste numbers into prose.\n *\n * Operations no requirement governs are skipped: `api.ungoverned` already\n * names those, and reporting each of their codes as well is one defect\n * counted twice.\n */\nexport function responseGovernanceFindings(\n service: string,\n ops: Operation[],\n livingReqs: Requirement[],\n): Finding[] {\n const ungoverned: string[] = [];\n for (const op of ops) {\n if (op.failureCodes.length === 0) continue;\n const governing = livingReqs.filter((r) => r.kind !== \"REMOVED\" && r.operations.includes(op.id));\n if (governing.length === 0) continue;\n const body = governing.flatMap((r) => r.scenarios.flatMap((s) => s.lines)).join(\"\\n\");\n const missing = op.failureCodes.filter((code) => !new RegExp(`(?<!\\\\d)${code}(?!\\\\d)`).test(body));\n if (missing.length > 0) ungoverned.push(`${op.id}: ${missing.join(\", \")}`);\n }\n if (ungoverned.length === 0) return [];\n return [\n {\n severity: \"warn\",\n code: \"api.response-ungoverned\",\n subject: service,\n message: `${service}: ${ungoverned.length} operation(s) declare a failure response no scenario reaches — write the refusal as a scenario, or as a row in one`,\n details: ungoverned,\n },\n ];\n}\n\nexport function coverageFinding(label: string, reqs: Requirement[]): Finding {\n const missing = requirementsMissingScenarios(reqs);\n if (missing.length === 0) {\n return {\n severity: \"ok\",\n code: \"requirements.covered\",\n message: `${label} covered (${reqs.length} requirement${reqs.length === 1 ? \"\" : \"s\"}, all with scenarios)`,\n };\n }\n return { ...missingScenariosFinding(label, missing), text: { detailPrefix: \"- \" } };\n}\n\n/**\n * The scenario-shape run of the battery every requirement document gets, in\n * report order — stepless, assertionless, examples — spelled once for the four\n * documents the header names, so they cannot drift apart. What differs between\n * the four stays at each call site: a delta's coverage finding carries its\n * service as `subject`, a living spec.md with no requirements reports\n * `spec.no-requirements` in coverage's place, and duplicate names and\n * requirement ids are graded on LIVING documents (`duplicateRequirementFindings`).\n */\nexport function scenarioShapeFindings(label: string, subject: string, reqs: Requirement[]): Finding[] {\n return [\n ...steplessFindings(label, subject, reqs),\n ...assertionlessFindings(label, subject, reqs),\n ...examplesFindings(label, subject, reqs),\n ];\n}\n"]}
|
|
@@ -6,7 +6,6 @@ import { listField, readFrontmatter } from "../../../core/document/frontmatter.j
|
|
|
6
6
|
import {} from "../../../core/vocabulary/report.js";
|
|
7
7
|
import { missingSources, patternSources, unsafeSources } from "../../../core/provenance/sources.js";
|
|
8
8
|
import { emptySourcesMessage, expandSourceFiles } from "../../../core/provenance/stamp.js";
|
|
9
|
-
import { UNVERIFIABLE } from "./vocabulary.js";
|
|
10
9
|
/**
|
|
11
10
|
* The two things a `sources` list can be that `serviceProvenance` cannot say.
|
|
12
11
|
*
|
|
@@ -45,11 +44,21 @@ export async function sourceScopeFindings(docsDir, service, repoDir, fleet) {
|
|
|
45
44
|
// arch axis. The refusal and the finding must be the same sentence.
|
|
46
45
|
const label = file === "spec.md" ? service : `${service}: ${file}`;
|
|
47
46
|
if (repoDir === undefined) {
|
|
47
|
+
// A literal, as at the rollup's counting site (../report.ts), and for
|
|
48
|
+
// the reason given there: the stable-code collector reads only literal
|
|
49
|
+
// `code:` slots. This used to be a shared constant, which kept the two
|
|
50
|
+
// spellings equal but hid the emission itself from the collector — the
|
|
51
|
+
// code was guarded only because the rollup's text marker spelled it.
|
|
52
|
+
//
|
|
53
|
+
// `ok` still, but never PRINTED as ✓: the check above did not run, and
|
|
54
|
+
// a check mark beside "nothing here can resolve them" read as a pass.
|
|
55
|
+
// The hint changes the glyph and nothing a machine or an exit code reads.
|
|
48
56
|
out.push({
|
|
49
57
|
severity: "ok",
|
|
50
|
-
code:
|
|
58
|
+
code: "sources.unverifiable-from-here",
|
|
51
59
|
subject: service,
|
|
52
60
|
message: `${label}: ${sources.length} source(s) declared, but this is not ${service}'s repository — nothing here can resolve them. Run \`loam validate --service ${service}\` from inside it.`,
|
|
61
|
+
text: { marker: "not-checked" },
|
|
53
62
|
});
|
|
54
63
|
continue;
|
|
55
64
|
}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"sources.js","sourceRoot":"","sources":["../../../../src/commands/validate/checks/sources.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,UAAU,EAAE,MAAM,SAAS,CAAC;AACrC,OAAO,EAAwB,MAAM,qCAAqC,CAAC;AAC3E,OAAO,EAAE,SAAS,EAAE,MAAM,6BAA6B,CAAC;AAExD,OAAO,EAAE,kBAAkB,EAAE,MAAM,sCAAsC,CAAC;AAC1E,OAAO,EAAE,SAAS,EAAE,eAAe,EAAE,MAAM,uCAAuC,CAAC;AACnF,OAAO,EAAgB,MAAM,oCAAoC,CAAC;AAClE,OAAO,EAAE,cAAc,EAAE,cAAc,EAAE,aAAa,EAAE,MAAM,qCAAqC,CAAC;AACpG,OAAO,EAAE,mBAAmB,EAAE,iBAAiB,EAAE,MAAM,mCAAmC,CAAC;
|
|
1
|
+
{"version":3,"file":"sources.js","sourceRoot":"","sources":["../../../../src/commands/validate/checks/sources.ts"],"names":[],"mappings":"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","sourcesContent":["import { existsSync } from \"node:fs\";\nimport { type PathableService } from \"../../../core/kernel/ids/service.js\";\nimport { SPEC_AXES } from \"../../../core/repo/paths.js\";\nimport type { FleetContext } from \"../../../core/fleet-context.js\";\nimport { locateServicePaths } from \"../../../core/repo/service-target.js\";\nimport { listField, readFrontmatter } from \"../../../core/document/frontmatter.js\";\nimport { type Finding } from \"../../../core/vocabulary/report.js\";\nimport { missingSources, patternSources, unsafeSources } from \"../../../core/provenance/sources.js\";\nimport { emptySourcesMessage, expandSourceFiles } from \"../../../core/provenance/stamp.js\";\nimport type { DocsDir } from \"../../../core/kernel/ids/dirs.js\";\n\n/**\n * The two things a `sources` list can be that `serviceProvenance` cannot say.\n *\n * `sources.unverifiable-from-here` — the spec names sources and loam is NOT in\n * that service's repository, so every sources check (existence, digest,\n * staleness) is skipped. serviceProvenance returns an empty list in that case\n * and the silence read as \"checked and fine\". It used to be counted only under\n * `--all` and printed as one rollup line, which meant `validate --service X`\n * run from the docs repo — the single most common way anyone looks at one\n * service — reported nothing at all about its own blind spot. Severity `ok`,\n * deliberately: nothing is WRONG with the docs, the check simply cannot run\n * here, and grading it a warning would make a fully documented fleet\n * permanently yellow and `--strict` permanently red in the docs repo's CI.\n *\n * `sources.empty` — the paths exist, and expand to no files at all: an empty\n * directory, or a tree the repository itself ignores. A digest over nothing\n * never changes, so the stamp would read as current forever. `loam vouch`\n * already refuses to stamp it; until now `validate` said nothing, so an author\n * got a green run followed by a refusal, two commands contradicting each other\n * about one document. The sentence comes from `emptySourcesMessage`, the same\n * definition vouch refuses with, under the label vouch uses.\n */\nexport async function sourceScopeFindings(\n docsDir: DocsDir,\n service: PathableService,\n repoDir: string | undefined,\n fleet: FleetContext,\n): Promise<Finding[]> {\n const paths = await locateServicePaths(docsDir, service, fleet);\n const out: Finding[] = [];\n // The axis pair is SPEC_AXES', not this function's: `serviceProvenance` grades\n // the same two files from the same list, and a scope check that walked a\n // shorter list than the grading would go quiet on the axis it forgot.\n for (const { path, file } of SPEC_AXES.map((axis) => ({ path: paths[axis.key], file: axis.file }))) {\n if (!existsSync(path)) continue;\n const sources = listField(await readFrontmatter(path), \"sources\");\n if (sources.length === 0) continue;\n // vouch's own labelling: a bare service id for spec.md, qualified for the\n // arch axis. The refusal and the finding must be the same sentence.\n const label = file === \"spec.md\" ? service : `${service}: ${file}`;\n if (repoDir === undefined) {\n // A literal, as at the rollup's counting site (../report.ts), and for\n // the reason given there: the stable-code collector reads only literal\n // `code:` slots. This used to be a shared constant, which kept the two\n // spellings equal but hid the emission itself from the collector — the\n // code was guarded only because the rollup's text marker spelled it.\n //\n // `ok` still, but never PRINTED as ✓: the check above did not run, and\n // a check mark beside \"nothing here can resolve them\" read as a pass.\n // The hint changes the glyph and nothing a machine or an exit code reads.\n out.push({\n severity: \"ok\",\n code: \"sources.unverifiable-from-here\",\n subject: service,\n message: `${label}: ${sources.length} source(s) declared, but this is not ${service}'s repository — nothing here can resolve them. Run \\`loam validate --service ${service}\\` from inside it.`,\n text: { marker: \"not-checked\" },\n });\n continue;\n }\n // Every other shape of broken list is serviceProvenance's to grade, and\n // grading them twice would send an author fixing one thing from two\n // findings. \"Covers no files\" is only meaningful once the paths are real.\n if (\n patternSources(sources).length > 0 ||\n unsafeSources(repoDir, sources).length > 0 ||\n missingSources(repoDir, sources).length > 0\n ) {\n continue;\n }\n const expansion = await expandSourceFiles(repoDir, sources, label);\n if (expansion.files.length > 0) continue;\n out.push({\n severity: \"warn\",\n code: \"sources.empty\",\n subject: service,\n message: expansion.empty ?? emptySourcesMessage(label, sources),\n });\n }\n return out;\n}\n"]}
|
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
import { entryResolves, parseCoversEntry } from "../../../core/c4/arch.js";
|
|
2
|
+
import { stageMergedProject } from "../../../core/c4/project/staged.js";
|
|
3
|
+
import { elementService, serviceResolver } from "../../../core/c4/resolve/service.js";
|
|
4
|
+
/**
|
|
5
|
+
* The post-merge map, computed at most once and only when asked — `null` when
|
|
6
|
+
* it cannot be had. Pre-routing on purpose (no extending models passed): an
|
|
7
|
+
* addition the archive routes into a service's model keeps the fqn it has on
|
|
8
|
+
* the map, and living scope reads model and map as one corpus, so the
|
|
9
|
+
* pre-routing preview holds every id a `Covers:` line can name after archive.
|
|
10
|
+
*/
|
|
11
|
+
export function mergedMapOf(source) {
|
|
12
|
+
let merged;
|
|
13
|
+
return () => (merged ??= (async () => {
|
|
14
|
+
const staged = await stageMergedProject({
|
|
15
|
+
docsDir: source.docsDir,
|
|
16
|
+
featureDir: source.featureDir,
|
|
17
|
+
featureId: source.featureId,
|
|
18
|
+
documents: [],
|
|
19
|
+
load: (path) => source.fleet.loadLikeC4(path),
|
|
20
|
+
});
|
|
21
|
+
return staged.kind === "read" ? { doc: staged.doc, svcOf: serviceResolver(staged.doc.elements, source.known) } : null;
|
|
22
|
+
})());
|
|
23
|
+
}
|
|
24
|
+
/**
|
|
25
|
+
* The `Covers:` spelling for a new element that still resolves once it is
|
|
26
|
+
* living: the service it stands for, else the id the merge gives it — matched in
|
|
27
|
+
* the preview by kind, title and owning service, and offered only when exactly
|
|
28
|
+
* one element answers. The delta's own id is the fallback, never a guess.
|
|
29
|
+
*/
|
|
30
|
+
export async function survivingElementCover(e, delta, merged) {
|
|
31
|
+
// An explicit binding is spliced verbatim, and living scope matches an
|
|
32
|
+
// element entry by it (`namesElement`), so it survives whether or not the
|
|
33
|
+
// service has a directory yet; a title only where it names a known service.
|
|
34
|
+
const service = elementService(e);
|
|
35
|
+
if (e.service !== undefined || delta.known.has(service))
|
|
36
|
+
return service;
|
|
37
|
+
const map = await merged();
|
|
38
|
+
if (map === null)
|
|
39
|
+
return e.id;
|
|
40
|
+
const owner = delta.svcOf(e.id);
|
|
41
|
+
const [landed, ...others] = map.doc.elements.filter((m) => m.kind === e.kind && m.title === e.title && map.svcOf(m.id) === owner);
|
|
42
|
+
return landed !== undefined && others.length === 0 ? landed.id : e.id;
|
|
43
|
+
}
|
|
44
|
+
/** The `Covers:` spelling for a new edge — its endpoints' service names, which both scopes resolve alike. */
|
|
45
|
+
export function survivingEdgeCover(r, svcOf) {
|
|
46
|
+
return `${svcOf(r.source)} -> ${svcOf(r.target)}`;
|
|
47
|
+
}
|
|
48
|
+
/** Does every element name this entry spells belong to a service? Then it is settled — names survive the merge. */
|
|
49
|
+
function namesServicesOnly(entry, known) {
|
|
50
|
+
if (entry.form === "element")
|
|
51
|
+
return known.has(entry.id);
|
|
52
|
+
if (entry.form === "edge")
|
|
53
|
+
return known.has(entry.source) && known.has(entry.target);
|
|
54
|
+
return true;
|
|
55
|
+
}
|
|
56
|
+
/**
|
|
57
|
+
* `covers.unknown` for the entries feature scope resolves only through the
|
|
58
|
+
* delta's own ids, which the map the merge leaves does not declare. Entries
|
|
59
|
+
* that resolve nowhere are `coversUnknownFindings`' to report, so they are
|
|
60
|
+
* skipped here: one breach, one finding.
|
|
61
|
+
*/
|
|
62
|
+
export async function deltaSpelledCovers(input) {
|
|
63
|
+
const { service, featureScope } = input;
|
|
64
|
+
const known = featureScope.known ?? new Set();
|
|
65
|
+
const out = [];
|
|
66
|
+
for (const r of input.reqs) {
|
|
67
|
+
if (r.kind === "REMOVED")
|
|
68
|
+
continue;
|
|
69
|
+
for (const raw of r.covers) {
|
|
70
|
+
const entry = parseCoversEntry(raw);
|
|
71
|
+
if (entry.form !== "element" && entry.form !== "edge")
|
|
72
|
+
continue;
|
|
73
|
+
if (!entryResolves(entry, featureScope) || namesServicesOnly(entry, known))
|
|
74
|
+
continue;
|
|
75
|
+
const model = await input.model();
|
|
76
|
+
const after = (elements, relationships) => ({
|
|
77
|
+
...featureScope,
|
|
78
|
+
elements: [...elements, ...model.elements],
|
|
79
|
+
relationships: [...relationships, ...model.relationships],
|
|
80
|
+
});
|
|
81
|
+
if (entryResolves(entry, after(input.living.elements, input.living.relationships)))
|
|
82
|
+
continue;
|
|
83
|
+
const map = await input.merged();
|
|
84
|
+
if (map === null || entryResolves(entry, after(map.doc.elements, map.doc.relationships)))
|
|
85
|
+
continue;
|
|
86
|
+
const declared = entry.form === "element" ? input.elements.find((e) => e.id === entry.id) : undefined;
|
|
87
|
+
const named = entry.form === "edge"
|
|
88
|
+
? `${input.svcOf(entry.source)} -> ${input.svcOf(entry.target)}`
|
|
89
|
+
: declared === undefined
|
|
90
|
+
? entry.id
|
|
91
|
+
: await survivingElementCover(declared, { svcOf: input.svcOf, known }, input.merged);
|
|
92
|
+
out.push({
|
|
93
|
+
severity: "warn",
|
|
94
|
+
code: "covers.unknown",
|
|
95
|
+
subject: service,
|
|
96
|
+
message: `${service}: arch.spec.md: requirement '${r.name}' — Covers: '${raw}' resolves only through this ` +
|
|
97
|
+
`feature's delta.likec4, whose ids are its own spelling: the map this merge leaves declares no such ` +
|
|
98
|
+
`${entry.form === "edge" ? "edge" : "element"}, so the next \`loam validate --all\` reports covers.unknown ` +
|
|
99
|
+
`on it. Name it the way the living map does — '${named}' resolves in both`,
|
|
100
|
+
});
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
return out;
|
|
104
|
+
}
|
|
105
|
+
//# sourceMappingURL=covers-after.js.map
|