@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":"parse.js","sourceRoot":"","sources":["../../../src/core/document/parse.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * Markdown in: headings, fences, and the one `## Requirements` cut point.\n *\n * The cut point is spelled once here on purpose. The archive cut, archive's\n * stray-requirement guard and the delta-shape exemption used to spell the same\n * invariant three different ways, and one of them was a PREFIX match that also\n * hit `## Requirements Extra`.\n *\n * Fence tracking is why this is not a set of regexes at the call sites: a\n * `### Requirement:` inside a fenced code block is prose about a requirement,\n * not one, and every heading walk here has to agree about that.\n */\nimport { readFile } from \"node:fs/promises\";\nimport { decodeDocument } from \"../kernel/document-bytes.js\";\nimport { fenceTracker } from \"../kernel/fences.js\";\nimport {\n BASED_ON_LINE_RE, CAPABILITY_LINE_RE, KIND_RE, REALIZES_LINE_RE, REQUIREMENT_ID_LINE_RE,\n type DeltaKind, type Requirement, type Scenario,\n} from \"./spec.js\";\n\nconst REQ_RE = /^###\\s+Requirement:\\s*(.+?)\\s*$/;\nconst SCN_RE = /^####\\s+Scenario:\\s*(.+?)\\s*$/;\n\n/**\n * THE requirements section: the one H2 a living spec keeps its requirements\n * under, the one section `loam archive` rewrites, and the one heading a delta\n * may legally quote the living state beneath. One definition on purpose — the\n * archive cut point, archive's stray-requirement guard, and the delta-shape\n * exemption (core/delta/document.ts) used to spell this invariant three different ways,\n * and the cut point's substring `indexOf(\"\\n## Requirements\")` was a PREFIX\n * match that also hit `## Requirements Extra`. Full-line match on a trimmed\n * heading, with KIND_RE's case and interior-whitespace tolerance.\n */\nconst REQUIREMENTS_SECTION_RE = /^##\\s+Requirements\\s*$/i;\n\n/** Is this H2 heading line (as sectionHeadings/`Requirement.section` spell it) the `## Requirements` heading? */\nexport function isRequirementsHeading(heading: string): boolean {\n return REQUIREMENTS_SECTION_RE.test(heading.trim());\n}\n\n/**\n * Strip a leading UTF-8 BOM (U+FEFF), which editors on Windows write unasked.\n *\n * It has to happen here, at the two functions that take raw markdown, rather than\n * at each `readFile` call: those are spread across half the commands, so any one of\n * them could forget, and the failure is invisible. One byte was enough to void an\n * entire delta — `KIND_RE` is anchored `^##`, delta files routinely open with\n * `## MODIFIED Requirements` on line 1, and behind a BOM that line stopped being a\n * heading, so every requirement in the file fell back to BASE and\n * `applyRequirementDelta` skips BASE. Archive merged nothing and said nothing.\n * OpenSpec's own reader strips it for exactly this reason.\n *\n * Only at position 0: elsewhere U+FEFF is a zero-width no-break space and belongs\n * to the author's text.\n */\nfunction stripBom(md: string): string {\n return md.charCodeAt(0) === 0xfeff ? md.slice(1) : md;\n}\n\n/** Every H2 heading outside a fenced block, with its 1-based line number. */\nexport function sectionHeadings(md: string): Array<{ text: string; line: number }> {\n const fenced = fenceTracker();\n const out: Array<{ text: string; line: number }> = [];\n stripBom(md).split(/\\r?\\n/).forEach((line, i) => {\n if (fenced(line)) return;\n if (/^##\\s+/.test(line) && !line.startsWith(\"###\")) out.push({ text: line.trim(), line: i + 1 });\n });\n return out;\n}\n\n/**\n * Split a living spec around its requirements RUN — the stretch of\n * `### Requirement:` blocks inside the `## Requirements` section — so a merge\n * can rewrite exactly that and nothing else. All three slices reassemble the\n * input byte-for-byte (`head + run + tail === text`):\n *\n * - `head` — everything before the first requirement in the section: the\n * intro, the heading line itself, any prose under the heading.\n * - `run` — from the first `### Requirement:` line to the section's end (the\n * next `## ` heading, or EOF). Empty when the section holds no requirements.\n * - `tail` — everything from the section's end onward.\n *\n * Returns null when the document has no `## Requirements` heading at all.\n * Only the FIRST matching heading opens the section; a caller that must not\n * lose content behind a duplicate (archive) counts matches via\n * sectionHeadings() and refuses. Heading detection matches parseRequirements:\n * fenced lines are never structure, and a leading BOM hides no heading — but\n * the slices themselves are raw bytes, BOM and CRLF included.\n */\nexport function splitRequirementsSection(\n text: string,\n): { head: string; run: string; tail: string } | null {\n const fenced = fenceTracker();\n let inSection = false;\n let sectionStart = -1;\n let runStart = -1;\n let sectionEnd = text.length;\n let offset = 0;\n while (offset < text.length) {\n const nl = text.indexOf(\"\\n\", offset);\n const lineEnd = nl === -1 ? text.length : nl + 1;\n let line = text.slice(offset, nl === -1 ? text.length : nl);\n if (line.endsWith(\"\\r\")) line = line.slice(0, -1);\n // Position 0 only, mirroring stripBom: elsewhere U+FEFF is content.\n if (offset === 0 && line.charCodeAt(0) === 0xfeff) line = line.slice(1);\n if (!fenced(line)) {\n if (/^##\\s+/.test(line) && !line.startsWith(\"###\")) {\n if (inSection) {\n sectionEnd = offset;\n inSection = false;\n } else if (sectionStart === -1 && isRequirementsHeading(line)) {\n sectionStart = offset;\n inSection = true;\n }\n } else if (inSection && runStart === -1 && REQ_RE.test(line)) {\n runStart = offset;\n }\n }\n offset = lineEnd;\n }\n if (sectionStart === -1) return null;\n const cut = runStart === -1 ? sectionEnd : runStart;\n return { head: text.slice(0, cut), run: text.slice(cut, sectionEnd), tail: text.slice(sectionEnd) };\n}\n\n/**\n * A requirement document from disk as text — decoded, or refused by name.\n *\n * The read that belongs with the parser, because `readFile(path, \"utf8\")` and\n * `parseRequirements` together are a trap: a UTF-16 spec.md decodes to a string\n * in which no line matches `^###\\s+Requirement:`, so the parser returns [] and\n * the document grades as an empty baseline instead of an unreadable one. Every\n * question loam asks of a spec — is it covered, does the delta merge, what does\n * the C4 spine touch — is then answered about a file nobody read.\n *\n * It THROWS (`NotUtf8DocumentError`, which names the path), unlike\n * `readFrontmatter` next door, because the two failures are not the same\n * failure. Frontmatter is read by the fleet enumeration, where one bad file\n * must not cost the other 119 services, so there it is a flag. A requirement\n * document is read by ONE target, and the caller is the only one who knows what\n * to do about it: a read-only command turns it into a finding on that target\n * (validate's `guarded`, which names the file and keeps grading the rest), a\n * command that writes refuses outright rather than computing over mojibake.\n */\nexport async function readRequirementsDocument(path: string): Promise<string> {\n return decodeDocument(await readFile(path), path);\n}\n\n/** Parse all requirements (with their scenarios and delta kind) from a markdown doc. */\nexport function parseRequirements(md: string): Requirement[] {\n const out: Requirement[] = [];\n let kind: DeltaKind = \"BASE\";\n let section: string | undefined;\n let req: Requirement | null = null;\n let scn: Scenario | null = null;\n const fenced = fenceTracker();\n\n for (const [index, line] of stripBom(md).split(/\\r?\\n/).entries()) {\n if (fenced(line)) {\n // Fenced content, marker included: body of whatever is open, never structure.\n if (scn) scn.lines.push(line);\n else if (req) req.text.push(line);\n continue;\n }\n // Any H2 heading ends the current requirement/scenario capture — section prose\n // must not leak into the previous scenario's body.\n if (/^##\\s+/.test(line) && !line.startsWith(\"###\")) {\n req = null;\n scn = null;\n section = line.trim();\n const mk = KIND_RE.exec(line);\n // A non-delta H2 (## Notes, ## Requirements…) starts an unrelated section:\n // requirements under it are plain BASE, not part of a stale delta section.\n kind = mk ? (mk[1]!.toUpperCase() as DeltaKind) : \"BASE\";\n continue;\n }\n const mr = REQ_RE.exec(line);\n if (mr) {\n req = {\n kind,\n name: mr[1]!,\n text: [],\n operations: [],\n covers: [],\n publishes: [],\n consumes: [],\n requires: [],\n capabilities: [],\n realizes: [],\n scenarios: [],\n section,\n line: index + 1,\n };\n out.push(req);\n scn = null;\n continue;\n }\n const ms = SCN_RE.exec(line);\n if (ms && req) {\n scn = { name: ms[1]!, lines: [] };\n req.scenarios.push(scn);\n continue;\n }\n if (scn) {\n scn.lines.push(line);\n } else if (req) {\n req.text.push(line);\n const mi = REQUIREMENT_ID_LINE_RE.exec(line);\n if (mi) req.id = mi[1]!.trim();\n // Same keep-last quirk as Operations:/Covers: below — a repeated line is\n // a document problem, reported as one (`delta.baseline-invalid`), never\n // resolved differently by whoever happens to be reading.\n const mb = BASED_ON_LINE_RE.exec(line);\n if (mb) req.basedOn = mb[1]!.trim();\n const mo = /^\\s*Operations?:\\s*(.+?)\\s*$/i.exec(line);\n if (mo) req.operations = mo[1]!.split(\",\").map((s) => s.trim()).filter((s) => s.length > 0);\n // The Covers: mirror — same placement (requirement body, not a scenario),\n // same comma grammar, and deliberately the same quirk: a second line\n // REPLACES the first (assignment, not append), so the two lists can never\n // drift apart in how they read a repeated line.\n const mc = /^\\s*Covers?:\\s*(.+?)\\s*$/i.exec(line);\n if (mc) req.covers = mc[1]!.split(\",\").map((s) => s.trim()).filter((s) => s.length > 0);\n // The event axis, same grammar and the same keep-last quirk again. Note\n // what is NOT happening here: the line stays in `req.text` like every\n // other body line (the push above is unconditional), so it round-trips\n // through `serializeRequirements` and rides inside `requirementDigest`\n // exactly as `Operations:` does. That is what makes this parse purely\n // additive — no living document's digest moves, and no `Based-On:` pin\n // goes stale because loam learned to read one more line.\n const mp = /^\\s*Publishes?:\\s*(.+?)\\s*$/i.exec(line);\n if (mp) req.publishes = mp[1]!.split(\",\").map((s) => s.trim()).filter((s) => s.length > 0);\n const mcon = /^\\s*Consumes?:\\s*(.+?)\\s*$/i.exec(line);\n if (mcon) req.consumes = mcon[1]!.split(\",\").map((s) => s.trim()).filter((s) => s.length > 0);\n // The authorization axis. Same grammar, same keep-last quirk, same purely\n // additive parse — the line rides in `req.text` and therefore inside\n // `requirementDigest`, so no living document moves because loam learned\n // to read it.\n const mreq = /^\\s*Requires?:\\s*(.+?)\\s*$/i.exec(line);\n if (mreq) req.requires = mreq[1]!.split(\",\").map((s) => s.trim()).filter((s) => s.length > 0);\n // The capability axis. Same grammar, same keep-last quirk, same purely\n // additive parse as Requires: above — but note the OPT-IN differs: the\n // vocabulary FILE (architecture/capabilities.yaml) is the opt-in for this\n // axis, not the line, so entries parsed here grade as nothing at all\n // until a fleet writes that file (core/capabilities/findings.ts).\n const mcap = CAPABILITY_LINE_RE.exec(line);\n if (mcap) req.capabilities = mcap[1]!.split(\",\").map((s) => s.trim()).filter((s) => s.length > 0);\n // The join INTO the authored business tree: `<capability-id>#<Requirement-ID>`\n // entries, same comma grammar and same keep-last quirk as every line\n // above. Split into its two halves nowhere near here —\n // `core/capabilities/realizes/join.ts` owns that, because the rollup and\n // the grades must not be able to disagree about where the separator is.\n const mrl = REALIZES_LINE_RE.exec(line);\n if (mrl) req.realizes = mrl[1]!.split(\",\").map((s) => s.trim()).filter((s) => s.length > 0);\n }\n }\n\n return out;\n}\n"]}
|
|
1
|
+
{"version":3,"file":"parse.js","sourceRoot":"","sources":["../../../src/core/document/parse.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;GAWG;AACH,OAAO,EAAE,QAAQ,EAAE,MAAM,kBAAkB,CAAC;AAC5C,OAAO,EAAE,cAAc,EAAE,MAAM,6BAA6B,CAAC;AAC7D,OAAO,EAAE,YAAY,EAAE,MAAM,qBAAqB,CAAC;AACnD,OAAO,EACL,gBAAgB,EAAE,kBAAkB,EAAE,OAAO,EAAE,gBAAgB,EAAE,sBAAsB,GAExF,MAAM,WAAW,CAAC;AAEnB,MAAM,MAAM,GAAG,iCAAiC,CAAC;AACjD,MAAM,MAAM,GAAG,+BAA+B,CAAC;AAE/C;;;;;;;;;GASG;AACH,MAAM,uBAAuB,GAAG,yBAAyB,CAAC;AAE1D,iHAAiH;AACjH,MAAM,UAAU,qBAAqB,CAAC,OAAe;IACnD,OAAO,uBAAuB,CAAC,IAAI,CAAC,OAAO,CAAC,IAAI,EAAE,CAAC,CAAC;AACtD,CAAC;AAED;;;;;;;;;;;;;;GAcG;AACH,SAAS,QAAQ,CAAC,EAAU;IAC1B,OAAO,EAAE,CAAC,UAAU,CAAC,CAAC,CAAC,KAAK,MAAM,CAAC,CAAC,CAAC,EAAE,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,EAAE,CAAC;AACxD,CAAC;AAED,6EAA6E;AAC7E,MAAM,UAAU,eAAe,CAAC,EAAU;IACxC,MAAM,MAAM,GAAG,YAAY,EAAE,CAAC;IAC9B,MAAM,GAAG,GAA0C,EAAE,CAAC;IACtD,QAAQ,CAAC,EAAE,CAAC,CAAC,KAAK,CAAC,OAAO,CAAC,CAAC,OAAO,CAAC,CAAC,IAAI,EAAE,CAAC,EAAE,EAAE;QAC9C,IAAI,MAAM,CAAC,IAAI,CAAC;YAAE,OAAO;QACzB,IAAI,QAAQ,CAAC,IAAI,CAAC,IAAI,CAAC,IAAI,CAAC,IAAI,CAAC,UAAU,CAAC,KAAK,CAAC;YAAE,GAAG,CAAC,IAAI,CAAC,EAAE,IAAI,EAAE,IAAI,CAAC,IAAI,EAAE,EAAE,IAAI,EAAE,CAAC,GAAG,CAAC,EAAE,CAAC,CAAC;IACnG,CAAC,CAAC,CAAC;IACH,OAAO,GAAG,CAAC;AACb,CAAC;AAED;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA6BG;AACH,MAAM,UAAU,wBAAwB,CACtC,IAAY;IAEZ,MAAM,MAAM,GAAG,YAAY,EAAE,CAAC;IAC9B,IAAI,SAAS,GAAG,KAAK,CAAC;IACtB,IAAI,YAAY,GAAG,CAAC,CAAC,CAAC;IACtB,MAAM,MAAM,GAAa,EAAE,CAAC;IAC5B,IAAI,UAAU,GAAG,IAAI,CAAC,MAAM,CAAC;IAC7B,IAAI,MAAM,GAAG,CAAC,CAAC;IACf,OAAO,MAAM,GAAG,IAAI,CAAC,MAAM,EAAE,CAAC;QAC5B,MAAM,EAAE,GAAG,IAAI,CAAC,OAAO,CAAC,IAAI,EAAE,MAAM,CAAC,CAAC;QACtC,MAAM,OAAO,GAAG,EAAE,KAAK,CAAC,CAAC,CAAC,CAAC,CAAC,IAAI,CAAC,MAAM,CAAC,CAAC,CAAC,EAAE,GAAG,CAAC,CAAC;QACjD,IAAI,IAAI,GAAG,IAAI,CAAC,KAAK,CAAC,MAAM,EAAE,EAAE,KAAK,CAAC,CAAC,CAAC,CAAC,CAAC,IAAI,CAAC,MAAM,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC;QAC5D,IAAI,IAAI,CAAC,QAAQ,CAAC,IAAI,CAAC;YAAE,IAAI,GAAG,IAAI,CAAC,KAAK,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,CAAC;QAClD,oEAAoE;QACpE,IAAI,MAAM,KAAK,CAAC,IAAI,IAAI,CAAC,UAAU,CAAC,CAAC,CAAC,KAAK,MAAM;YAAE,IAAI,GAAG,IAAI,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC;QACxE,IAAI,CAAC,MAAM,CAAC,IAAI,CAAC,EAAE,CAAC;YAClB,IAAI,QAAQ,CAAC,IAAI,CAAC,IAAI,CAAC,IAAI,CAAC,IAAI,CAAC,UAAU,CAAC,KAAK,CAAC,EAAE,CAAC;gBACnD,IAAI,SAAS,EAAE,CAAC;oBACd,UAAU,GAAG,MAAM,CAAC;oBACpB,SAAS,GAAG,KAAK,CAAC;gBACpB,CAAC;qBAAM,IAAI,YAAY,KAAK,CAAC,CAAC,IAAI,qBAAqB,CAAC,IAAI,CAAC,EAAE,CAAC;oBAC9D,YAAY,GAAG,MAAM,CAAC;oBACtB,SAAS,GAAG,IAAI,CAAC;gBACnB,CAAC;YACH,CAAC;iBAAM,IAAI,SAAS,IAAI,MAAM,CAAC,IAAI,CAAC,IAAI,CAAC,EAAE,CAAC;gBAC1C,MAAM,CAAC,IAAI,CAAC,MAAM,CAAC,CAAC;YACtB,CAAC;QACH,CAAC;QACD,MAAM,GAAG,OAAO,CAAC;IACnB,CAAC;IACD,IAAI,YAAY,KAAK,CAAC,CAAC;QAAE,OAAO,IAAI,CAAC;IACrC,MAAM,GAAG,GAAG,MAAM,CAAC,CAAC,CAAC,IAAI,UAAU,CAAC;IACpC,OAAO;QACL,IAAI,EAAE,IAAI,CAAC,KAAK,CAAC,CAAC,EAAE,GAAG,CAAC;QACxB,GAAG,EAAE,IAAI,CAAC,KAAK,CAAC,GAAG,EAAE,UAAU,CAAC;QAChC,IAAI,EAAE,IAAI,CAAC,KAAK,CAAC,UAAU,CAAC;QAC5B,YAAY,EAAE,MAAM,CAAC,GAAG,CAAC,CAAC,EAAE,EAAE,CAAC,EAAE,EAAE,CAAC,IAAI,CAAC,KAAK,CAAC,EAAE,EAAE,MAAM,CAAC,CAAC,GAAG,CAAC,CAAC,IAAI,UAAU,CAAC,CAAC;KACjF,CAAC;AACJ,CAAC;AAED;;;;;;;;;;;;;;;;;;GAkBG;AACH,MAAM,CAAC,KAAK,UAAU,wBAAwB,CAAC,IAAY;IACzD,OAAO,cAAc,CAAC,MAAM,QAAQ,CAAC,IAAI,CAAC,EAAE,IAAI,CAAC,CAAC;AACpD,CAAC;AAED,wFAAwF;AACxF,MAAM,UAAU,iBAAiB,CAAC,EAAU;IAC1C,MAAM,GAAG,GAAkB,EAAE,CAAC;IAC9B,IAAI,IAAI,GAAc,MAAM,CAAC;IAC7B,IAAI,OAA2B,CAAC;IAChC,IAAI,GAAG,GAAuB,IAAI,CAAC;IACnC,IAAI,GAAG,GAAoB,IAAI,CAAC;IAChC,MAAM,MAAM,GAAG,YAAY,EAAE,CAAC;IAE9B,KAAK,MAAM,CAAC,KAAK,EAAE,IAAI,CAAC,IAAI,QAAQ,CAAC,EAAE,CAAC,CAAC,KAAK,CAAC,OAAO,CAAC,CAAC,OAAO,EAAE,EAAE,CAAC;QAClE,IAAI,MAAM,CAAC,IAAI,CAAC,EAAE,CAAC;YACjB,8EAA8E;YAC9E,IAAI,GAAG;gBAAE,GAAG,CAAC,KAAK,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;iBACzB,IAAI,GAAG;gBAAE,GAAG,CAAC,IAAI,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;YAClC,SAAS;QACX,CAAC;QACD,+EAA+E;QAC/E,mDAAmD;QACnD,IAAI,QAAQ,CAAC,IAAI,CAAC,IAAI,CAAC,IAAI,CAAC,IAAI,CAAC,UAAU,CAAC,KAAK,CAAC,EAAE,CAAC;YACnD,GAAG,GAAG,IAAI,CAAC;YACX,GAAG,GAAG,IAAI,CAAC;YACX,OAAO,GAAG,IAAI,CAAC,IAAI,EAAE,CAAC;YACtB,MAAM,EAAE,GAAG,OAAO,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;YAC9B,2EAA2E;YAC3E,2EAA2E;YAC3E,IAAI,GAAG,EAAE,CAAC,CAAC,CAAE,EAAE,CAAC,CAAC,CAAE,CAAC,WAAW,EAAgB,CAAC,CAAC,CAAC,MAAM,CAAC;YACzD,SAAS;QACX,CAAC;QACD,MAAM,EAAE,GAAG,MAAM,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;QAC7B,IAAI,EAAE,EAAE,CAAC;YACP,GAAG,GAAG;gBACJ,IAAI;gBACJ,IAAI,EAAE,EAAE,CAAC,CAAC,CAAE;gBACZ,IAAI,EAAE,EAAE;gBACR,UAAU,EAAE,EAAE;gBACd,MAAM,EAAE,EAAE;gBACV,SAAS,EAAE,EAAE;gBACb,QAAQ,EAAE,EAAE;gBACZ,QAAQ,EAAE,EAAE;gBACZ,YAAY,EAAE,EAAE;gBAChB,QAAQ,EAAE,EAAE;gBACZ,SAAS,EAAE,EAAE;gBACb,OAAO;gBACP,IAAI,EAAE,KAAK,GAAG,CAAC;aAChB,CAAC;YACF,GAAG,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC;YACd,GAAG,GAAG,IAAI,CAAC;YACX,SAAS;QACX,CAAC;QACD,MAAM,EAAE,GAAG,MAAM,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;QAC7B,IAAI,EAAE,IAAI,GAAG,EAAE,CAAC;YACd,gEAAgE;YAChE,uEAAuE;YACvE,uEAAuE;YACvE,wEAAwE;YACxE,yEAAyE;YACzE,yEAAyE;YACzE,mCAAmC;YACnC,GAAG,GAAG,EAAE,IAAI,EAAE,EAAE,CAAC,CAAC,CAAE,EAAE,KAAK,EAAE,EAAE,EAAE,IAAI,EAAE,KAAK,GAAG,CAAC,EAAE,CAAC;YACnD,GAAG,CAAC,SAAS,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC;YACxB,SAAS;QACX,CAAC;QACD,IAAI,GAAG,EAAE,CAAC;YACR,GAAG,CAAC,KAAK,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;QACvB,CAAC;aAAM,IAAI,GAAG,EAAE,CAAC;YACf,GAAG,CAAC,IAAI,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;YACpB,MAAM,EAAE,GAAG,sBAAsB,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;YAC7C,IAAI,EAAE;gBAAE,GAAG,CAAC,EAAE,GAAG,EAAE,CAAC,CAAC,CAAE,CAAC,IAAI,EAAE,CAAC;YAC/B,yEAAyE;YACzE,wEAAwE;YACxE,yDAAyD;YACzD,MAAM,EAAE,GAAG,gBAAgB,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;YACvC,IAAI,EAAE;gBAAE,GAAG,CAAC,OAAO,GAAG,EAAE,CAAC,CAAC,CAAE,CAAC,IAAI,EAAE,CAAC;YACpC,MAAM,EAAE,GAAG,+BAA+B,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;YACtD,IAAI,EAAE;gBAAE,GAAG,CAAC,UAAU,GAAG,EAAE,CAAC,CAAC,CAAE,CAAC,KAAK,CAAC,GAAG,CAAC,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,IAAI,EAAE,CAAC,CAAC,MAAM,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,MAAM,GAAG,CAAC,CAAC,CAAC;YAC5F,0EAA0E;YAC1E,qEAAqE;YACrE,0EAA0E;YAC1E,gDAAgD;YAChD,MAAM,EAAE,GAAG,2BAA2B,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;YAClD,IAAI,EAAE;gBAAE,GAAG,CAAC,MAAM,GAAG,EAAE,CAAC,CAAC,CAAE,CAAC,KAAK,CAAC,GAAG,CAAC,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,IAAI,EAAE,CAAC,CAAC,MAAM,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,MAAM,GAAG,CAAC,CAAC,CAAC;YACxF,wEAAwE;YACxE,sEAAsE;YACtE,uEAAuE;YACvE,uEAAuE;YACvE,sEAAsE;YACtE,uEAAuE;YACvE,yDAAyD;YACzD,MAAM,EAAE,GAAG,8BAA8B,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;YACrD,IAAI,EAAE;gBAAE,GAAG,CAAC,SAAS,GAAG,EAAE,CAAC,CAAC,CAAE,CAAC,KAAK,CAAC,GAAG,CAAC,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,IAAI,EAAE,CAAC,CAAC,MAAM,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,MAAM,GAAG,CAAC,CAAC,CAAC;YAC3F,MAAM,IAAI,GAAG,6BAA6B,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;YACtD,IAAI,IAAI;gBAAE,GAAG,CAAC,QAAQ,GAAG,IAAI,CAAC,CAAC,CAAE,CAAC,KAAK,CAAC,GAAG,CAAC,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,IAAI,EAAE,CAAC,CAAC,MAAM,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,MAAM,GAAG,CAAC,CAAC,CAAC;YAC9F,0EAA0E;YAC1E,qEAAqE;YACrE,wEAAwE;YACxE,cAAc;YACd,MAAM,IAAI,GAAG,6BAA6B,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;YACtD,IAAI,IAAI;gBAAE,GAAG,CAAC,QAAQ,GAAG,IAAI,CAAC,CAAC,CAAE,CAAC,KAAK,CAAC,GAAG,CAAC,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,IAAI,EAAE,CAAC,CAAC,MAAM,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,MAAM,GAAG,CAAC,CAAC,CAAC;YAC9F,uEAAuE;YACvE,uEAAuE;YACvE,0EAA0E;YAC1E,qEAAqE;YACrE,kEAAkE;YAClE,MAAM,IAAI,GAAG,kBAAkB,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;YAC3C,IAAI,IAAI;gBAAE,GAAG,CAAC,YAAY,GAAG,IAAI,CAAC,CAAC,CAAE,CAAC,KAAK,CAAC,GAAG,CAAC,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,IAAI,EAAE,CAAC,CAAC,MAAM,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,MAAM,GAAG,CAAC,CAAC,CAAC;YAClG,+EAA+E;YAC/E,qEAAqE;YACrE,uDAAuD;YACvD,yEAAyE;YACzE,wEAAwE;YACxE,MAAM,GAAG,GAAG,gBAAgB,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;YACxC,IAAI,GAAG;gBAAE,GAAG,CAAC,QAAQ,GAAG,GAAG,CAAC,CAAC,CAAE,CAAC,KAAK,CAAC,GAAG,CAAC,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,IAAI,EAAE,CAAC,CAAC,MAAM,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,MAAM,GAAG,CAAC,CAAC,CAAC;QAC9F,CAAC;IACH,CAAC;IAED,OAAO,GAAG,CAAC;AACb,CAAC","sourcesContent":["/**\n * Markdown in: headings, fences, and the one `## Requirements` cut point.\n *\n * The cut point is spelled once here on purpose. The archive cut, archive's\n * stray-requirement guard and the delta-shape exemption used to spell the same\n * invariant three different ways, and one of them was a PREFIX match that also\n * hit `## Requirements Extra`.\n *\n * Fence tracking is why this is not a set of regexes at the call sites: a\n * `### Requirement:` inside a fenced code block is prose about a requirement,\n * not one, and every heading walk here has to agree about that.\n */\nimport { readFile } from \"node:fs/promises\";\nimport { decodeDocument } from \"../kernel/document-bytes.js\";\nimport { fenceTracker } from \"../kernel/fences.js\";\nimport {\n BASED_ON_LINE_RE, CAPABILITY_LINE_RE, KIND_RE, REALIZES_LINE_RE, REQUIREMENT_ID_LINE_RE,\n type DeltaKind, type Requirement, type Scenario,\n} from \"./spec.js\";\n\nconst REQ_RE = /^###\\s+Requirement:\\s*(.+?)\\s*$/;\nconst SCN_RE = /^####\\s+Scenario:\\s*(.+?)\\s*$/;\n\n/**\n * THE requirements section: the one H2 a living spec keeps its requirements\n * under, the one section `loam archive` rewrites, and the one heading a delta\n * may legally quote the living state beneath. One definition on purpose — the\n * archive cut point, archive's stray-requirement guard, and the delta-shape\n * exemption (core/delta/document.ts) used to spell this invariant three different ways,\n * and the cut point's substring `indexOf(\"\\n## Requirements\")` was a PREFIX\n * match that also hit `## Requirements Extra`. Full-line match on a trimmed\n * heading, with KIND_RE's case and interior-whitespace tolerance.\n */\nconst REQUIREMENTS_SECTION_RE = /^##\\s+Requirements\\s*$/i;\n\n/** Is this H2 heading line (as sectionHeadings/`Requirement.section` spell it) the `## Requirements` heading? */\nexport function isRequirementsHeading(heading: string): boolean {\n return REQUIREMENTS_SECTION_RE.test(heading.trim());\n}\n\n/**\n * Strip a leading UTF-8 BOM (U+FEFF), which editors on Windows write unasked.\n *\n * It has to happen here, at the two functions that take raw markdown, rather than\n * at each `readFile` call: those are spread across half the commands, so any one of\n * them could forget, and the failure is invisible. One byte was enough to void an\n * entire delta — `KIND_RE` is anchored `^##`, delta files routinely open with\n * `## MODIFIED Requirements` on line 1, and behind a BOM that line stopped being a\n * heading, so every requirement in the file fell back to BASE and\n * `applyRequirementDelta` skips BASE. Archive merged nothing and said nothing.\n * OpenSpec's own reader strips it for exactly this reason.\n *\n * Only at position 0: elsewhere U+FEFF is a zero-width no-break space and belongs\n * to the author's text.\n */\nfunction stripBom(md: string): string {\n return md.charCodeAt(0) === 0xfeff ? md.slice(1) : md;\n}\n\n/** Every H2 heading outside a fenced block, with its 1-based line number. */\nexport function sectionHeadings(md: string): Array<{ text: string; line: number }> {\n const fenced = fenceTracker();\n const out: Array<{ text: string; line: number }> = [];\n stripBom(md).split(/\\r?\\n/).forEach((line, i) => {\n if (fenced(line)) return;\n if (/^##\\s+/.test(line) && !line.startsWith(\"###\")) out.push({ text: line.trim(), line: i + 1 });\n });\n return out;\n}\n\n/**\n * Split a living spec around its requirements RUN — the stretch of\n * `### Requirement:` blocks inside the `## Requirements` section — so a merge\n * can rewrite exactly that and nothing else. All three slices reassemble the\n * input byte-for-byte (`head + run + tail === text`):\n *\n * - `head` — everything before the first requirement in the section: the\n * intro, the heading line itself, any prose under the heading.\n * - `run` — from the first `### Requirement:` line to the section's end (the\n * next `## ` heading, or EOF). Empty when the section holds no requirements.\n * - `tail` — everything from the section's end onward.\n *\n * `requirements` is the run cut again at every `### Requirement:` heading —\n * one raw slice per requirement, from its heading up to the next one (or the\n * run's end), so `requirements.join(\"\") === run`. A slice therefore carries\n * whatever the author wrote below the requirement before the next heading,\n * blank lines included, which is exactly the text parseRequirements attributes\n * to that requirement. It exists so archive can keep the bytes of every\n * requirement a delta did not touch instead of re-serializing the whole run:\n * one ADDED requirement used to reflow every untouched requirement written\n * without a blank line under its heading, and every one of those moves landed\n * under the document's vouch stamp.\n *\n * Returns null when the document has no `## Requirements` heading at all.\n * Only the FIRST matching heading opens the section; a caller that must not\n * lose content behind a duplicate (archive) counts matches via\n * sectionHeadings() and refuses. Heading detection matches parseRequirements:\n * fenced lines are never structure, and a leading BOM hides no heading — but\n * the slices themselves are raw bytes, BOM and CRLF included.\n */\nexport function splitRequirementsSection(\n text: string,\n): { head: string; run: string; tail: string; requirements: string[] } | null {\n const fenced = fenceTracker();\n let inSection = false;\n let sectionStart = -1;\n const starts: number[] = [];\n let sectionEnd = text.length;\n let offset = 0;\n while (offset < text.length) {\n const nl = text.indexOf(\"\\n\", offset);\n const lineEnd = nl === -1 ? text.length : nl + 1;\n let line = text.slice(offset, nl === -1 ? text.length : nl);\n if (line.endsWith(\"\\r\")) line = line.slice(0, -1);\n // Position 0 only, mirroring stripBom: elsewhere U+FEFF is content.\n if (offset === 0 && line.charCodeAt(0) === 0xfeff) line = line.slice(1);\n if (!fenced(line)) {\n if (/^##\\s+/.test(line) && !line.startsWith(\"###\")) {\n if (inSection) {\n sectionEnd = offset;\n inSection = false;\n } else if (sectionStart === -1 && isRequirementsHeading(line)) {\n sectionStart = offset;\n inSection = true;\n }\n } else if (inSection && REQ_RE.test(line)) {\n starts.push(offset);\n }\n }\n offset = lineEnd;\n }\n if (sectionStart === -1) return null;\n const cut = starts[0] ?? sectionEnd;\n return {\n head: text.slice(0, cut),\n run: text.slice(cut, sectionEnd),\n tail: text.slice(sectionEnd),\n requirements: starts.map((at, i) => text.slice(at, starts[i + 1] ?? sectionEnd)),\n };\n}\n\n/**\n * A requirement document from disk as text — decoded, or refused by name.\n *\n * The read that belongs with the parser, because `readFile(path, \"utf8\")` and\n * `parseRequirements` together are a trap: a UTF-16 spec.md decodes to a string\n * in which no line matches `^###\\s+Requirement:`, so the parser returns [] and\n * the document grades as an empty baseline instead of an unreadable one. Every\n * question loam asks of a spec — is it covered, does the delta merge, what does\n * the C4 spine touch — is then answered about a file nobody read.\n *\n * It THROWS (`NotUtf8DocumentError`, which names the path), unlike\n * `readFrontmatter` next door, because the two failures are not the same\n * failure. Frontmatter is read by the fleet enumeration, where one bad file\n * must not cost the other 119 services, so there it is a flag. A requirement\n * document is read by ONE target, and the caller is the only one who knows what\n * to do about it: a read-only command turns it into a finding on that target\n * (validate's `guarded`, which names the file and keeps grading the rest), a\n * command that writes refuses outright rather than computing over mojibake.\n */\nexport async function readRequirementsDocument(path: string): Promise<string> {\n return decodeDocument(await readFile(path), path);\n}\n\n/** Parse all requirements (with their scenarios and delta kind) from a markdown doc. */\nexport function parseRequirements(md: string): Requirement[] {\n const out: Requirement[] = [];\n let kind: DeltaKind = \"BASE\";\n let section: string | undefined;\n let req: Requirement | null = null;\n let scn: Scenario | null = null;\n const fenced = fenceTracker();\n\n for (const [index, line] of stripBom(md).split(/\\r?\\n/).entries()) {\n if (fenced(line)) {\n // Fenced content, marker included: body of whatever is open, never structure.\n if (scn) scn.lines.push(line);\n else if (req) req.text.push(line);\n continue;\n }\n // Any H2 heading ends the current requirement/scenario capture — section prose\n // must not leak into the previous scenario's body.\n if (/^##\\s+/.test(line) && !line.startsWith(\"###\")) {\n req = null;\n scn = null;\n section = line.trim();\n const mk = KIND_RE.exec(line);\n // A non-delta H2 (## Notes, ## Requirements…) starts an unrelated section:\n // requirements under it are plain BASE, not part of a stale delta section.\n kind = mk ? (mk[1]!.toUpperCase() as DeltaKind) : \"BASE\";\n continue;\n }\n const mr = REQ_RE.exec(line);\n if (mr) {\n req = {\n kind,\n name: mr[1]!,\n text: [],\n operations: [],\n covers: [],\n publishes: [],\n consumes: [],\n requires: [],\n capabilities: [],\n realizes: [],\n scenarios: [],\n section,\n line: index + 1,\n };\n out.push(req);\n scn = null;\n continue;\n }\n const ms = SCN_RE.exec(line);\n if (ms && req) {\n // `index + 1` is the heading's own 1-based line, exactly as the\n // requirement capture above records its `### Requirement:` — body line\n // `lines[i]` then sits at `line + 1 + i`, because the capture below is\n // contiguous from here to the next heading. Purely additive: nothing in\n // `serializeRequirements` reads it, so `requirementDigest` cannot see it\n // and no `Based-On:` pin in any living document moves because the parser\n // learned where a scenario starts.\n scn = { name: ms[1]!, lines: [], line: index + 1 };\n req.scenarios.push(scn);\n continue;\n }\n if (scn) {\n scn.lines.push(line);\n } else if (req) {\n req.text.push(line);\n const mi = REQUIREMENT_ID_LINE_RE.exec(line);\n if (mi) req.id = mi[1]!.trim();\n // Same keep-last quirk as Operations:/Covers: below — a repeated line is\n // a document problem, reported as one (`delta.baseline-invalid`), never\n // resolved differently by whoever happens to be reading.\n const mb = BASED_ON_LINE_RE.exec(line);\n if (mb) req.basedOn = mb[1]!.trim();\n const mo = /^\\s*Operations?:\\s*(.+?)\\s*$/i.exec(line);\n if (mo) req.operations = mo[1]!.split(\",\").map((s) => s.trim()).filter((s) => s.length > 0);\n // The Covers: mirror — same placement (requirement body, not a scenario),\n // same comma grammar, and deliberately the same quirk: a second line\n // REPLACES the first (assignment, not append), so the two lists can never\n // drift apart in how they read a repeated line.\n const mc = /^\\s*Covers?:\\s*(.+?)\\s*$/i.exec(line);\n if (mc) req.covers = mc[1]!.split(\",\").map((s) => s.trim()).filter((s) => s.length > 0);\n // The event axis, same grammar and the same keep-last quirk again. Note\n // what is NOT happening here: the line stays in `req.text` like every\n // other body line (the push above is unconditional), so it round-trips\n // through `serializeRequirements` and rides inside `requirementDigest`\n // exactly as `Operations:` does. That is what makes this parse purely\n // additive — no living document's digest moves, and no `Based-On:` pin\n // goes stale because loam learned to read one more line.\n const mp = /^\\s*Publishes?:\\s*(.+?)\\s*$/i.exec(line);\n if (mp) req.publishes = mp[1]!.split(\",\").map((s) => s.trim()).filter((s) => s.length > 0);\n const mcon = /^\\s*Consumes?:\\s*(.+?)\\s*$/i.exec(line);\n if (mcon) req.consumes = mcon[1]!.split(\",\").map((s) => s.trim()).filter((s) => s.length > 0);\n // The authorization axis. Same grammar, same keep-last quirk, same purely\n // additive parse — the line rides in `req.text` and therefore inside\n // `requirementDigest`, so no living document moves because loam learned\n // to read it.\n const mreq = /^\\s*Requires?:\\s*(.+?)\\s*$/i.exec(line);\n if (mreq) req.requires = mreq[1]!.split(\",\").map((s) => s.trim()).filter((s) => s.length > 0);\n // The capability axis. Same grammar, same keep-last quirk, same purely\n // additive parse as Requires: above — but note the OPT-IN differs: the\n // vocabulary FILE (architecture/capabilities.yaml) is the opt-in for this\n // axis, not the line, so entries parsed here grade as nothing at all\n // until a fleet writes that file (core/capabilities/findings.ts).\n const mcap = CAPABILITY_LINE_RE.exec(line);\n if (mcap) req.capabilities = mcap[1]!.split(\",\").map((s) => s.trim()).filter((s) => s.length > 0);\n // The join INTO the authored business tree: `<capability-id>#<Requirement-ID>`\n // entries, same comma grammar and same keep-last quirk as every line\n // above. Split into its two halves nowhere near here —\n // `core/capabilities/realizes/join.ts` owns that, because the rollup and\n // the grades must not be able to disagree about where the separator is.\n const mrl = REALIZES_LINE_RE.exec(line);\n if (mrl) req.realizes = mrl[1]!.split(\",\").map((s) => s.trim()).filter((s) => s.length > 0);\n }\n }\n\n return out;\n}\n"]}
|
|
@@ -13,6 +13,23 @@ import {} from "./spec.js";
|
|
|
13
13
|
export function requirementsMissingScenarios(reqs) {
|
|
14
14
|
return reqs.filter((r) => r.kind !== "REMOVED" && r.scenarios.length === 0);
|
|
15
15
|
}
|
|
16
|
+
/**
|
|
17
|
+
* `requirements.missing-scenarios` over the requirements the rule above
|
|
18
|
+
* returned — one spelling for `loam validate` (its `coverageFinding`) and
|
|
19
|
+
* `loam status` (`core/status/feature/feature.ts`), because status's rule is
|
|
20
|
+
* "never greener than the gates" and a second spelling of the same finding is
|
|
21
|
+
* how the two would come to disagree. `subject` names the service when the
|
|
22
|
+
* caller has one.
|
|
23
|
+
*/
|
|
24
|
+
export function missingScenariosFinding(label, bare, subject) {
|
|
25
|
+
return {
|
|
26
|
+
severity: "error",
|
|
27
|
+
code: "requirements.missing-scenarios",
|
|
28
|
+
...(subject === undefined ? {} : { subject }),
|
|
29
|
+
message: `${label}: ${bare.length} requirement(s) without a scenario`,
|
|
30
|
+
details: bare.map((r) => r.name),
|
|
31
|
+
};
|
|
32
|
+
}
|
|
16
33
|
/**
|
|
17
34
|
* Scenarios that satisfy the coverage rule above and test nothing.
|
|
18
35
|
*
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"scenarios.js","sourceRoot":"","sources":["../../../src/core/document/scenarios.ts"],"names":[],"mappings":"AAAA;;;;;;;;GAQG;AACH,OAAO,EAAE,eAAe,EAAoB,MAAM,wBAAwB,CAAC;AAE3E,OAAO,EAAoB,MAAM,WAAW,CAAC;AAE7C,+FAA+F;AAC/F,MAAM,UAAU,4BAA4B,CAAC,IAAmB;IAC9D,OAAO,IAAI,CAAC,MAAM,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,IAAI,KAAK,SAAS,IAAI,CAAC,CAAC,SAAS,CAAC,MAAM,KAAK,CAAC,CAAC,CAAC;AAC9E,CAAC;AAQD;;;;;;;;;;;;;;;;GAgBG;AACH,MAAM,UAAU,iBAAiB,CAAC,IAAmB;IACnD,MAAM,GAAG,GAAuB,EAAE,CAAC;IACnC,KAAK,MAAM,CAAC,IAAI,IAAI,EAAE,CAAC;QACrB,IAAI,CAAC,CAAC,IAAI,KAAK,SAAS;YAAE,SAAS;QACnC,KAAK,MAAM,CAAC,IAAI,CAAC,CAAC,SAAS,EAAE,CAAC;YAC5B,IAAI,eAAe,CAAC,CAAC,CAAC,KAAK,CAAC,CAAC,KAAK,CAAC,MAAM,KAAK,CAAC;gBAAE,GAAG,CAAC,IAAI,CAAC,EAAE,WAAW,EAAE,CAAC,CAAC,IAAI,EAAE,QAAQ,EAAE,CAAC,CAAC,IAAI,EAAE,CAAC,CAAC;QACvG,CAAC;IACH,CAAC;IACD,OAAO,GAAG,CAAC;AACb,CAAC;AAED;;;;;;;;GAQG;AACH,MAAM,UAAU,gBAAgB,CAAC,KAAa,EAAE,OAAe,EAAE,IAAmB;IAClF,MAAM,IAAI,GAAG,iBAAiB,CAAC,IAAI,CAAC,CAAC;IACrC,IAAI,IAAI,CAAC,MAAM,KAAK,CAAC;QAAE,OAAO,EAAE,CAAC;IACjC,OAAO;QACL;YACE,QAAQ,EAAE,OAAO;YACjB,IAAI,EAAE,gCAAgC;YACtC,OAAO;YACP,OAAO,EACL,GAAG,KAAK,KAAK,IAAI,CAAC,MAAM,0DAA0D;gBAClF,yGAAyG;gBACzG,sGAAsG;YACxG,OAAO,EAAE,IAAI,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,GAAG,CAAC,CAAC,WAAW,MAAM,CAAC,CAAC,QAAQ,EAAE,CAAC;YAC5D,IAAI,EAAE,EAAE,YAAY,EAAE,IAAI,EAAE;SAC7B;KACF,CAAC;AACJ,CAAC;AAED;;;;;;;;;;;;;;;;GAgBG;AACH,MAAM,UAAU,sBAAsB,CAAC,IAAmB;IACxD,MAAM,GAAG,GAAuB,EAAE,CAAC;IACnC,KAAK,MAAM,CAAC,IAAI,IAAI,EAAE,CAAC;QACrB,IAAI,CAAC,CAAC,IAAI,KAAK,SAAS;YAAE,SAAS;QACnC,KAAK,MAAM,CAAC,IAAI,CAAC,CAAC,SAAS,EAAE,CAAC;YAC5B,MAAM,EAAE,KAAK,EAAE,GAAG,eAAe,CAAC,CAAC,CAAC,KAAK,CAAC,CAAC;YAC3C,yEAAyE;YACzE,0EAA0E;YAC1E,4BAA4B;YAC5B,IAAI,KAAK,CAAC,MAAM,GAAG,CAAC,IAAI,CAAC,KAAK,CAAC,IAAI,CAAC,CAAC,EAAE,EAAE,EAAE,CAAC,SAAS,CAAC,IAAI,CAAC,EAAE,CAAC,IAAI,CAAC,CAAC,EAAE,CAAC;gBACrE,GAAG,CAAC,IAAI,CAAC,EAAE,WAAW,EAAE,CAAC,CAAC,IAAI,EAAE,QAAQ,EAAE,CAAC,CAAC,IAAI,EAAE,CAAC,CAAC;YACtD,CAAC;QACH,CAAC;IACH,CAAC;IACD,OAAO,GAAG,CAAC;AACb,CAAC;AAYD,MAAM,cAAc,GAAG,eAAe,CAAC;AAEvC;;;;;;;GAOG;AACH,SAAS,gBAAgB,CAAC,KAAoB;IAC5C,MAAM,IAAI,GAAG,IAAI,GAAG,EAAU,CAAC;IAC/B,MAAM,IAAI,GAAG,CAAC,IAAY,EAAQ,EAAE;QAClC,KAAK,MAAM,CAAC,IAAI,IAAI,CAAC,QAAQ,CAAC,cAAc,CAAC;YAAE,IAAI,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC,CAAE,CAAC,CAAC;IACjE,CAAC,CAAC;IACF,KAAK,MAAM,EAAE,IAAI,KAAK,EAAE,CAAC;QACvB,IAAI,CAAC,EAAE,CAAC,IAAI,CAAC,CAAC;QACd,KAAK,MAAM,CAAC,IAAI,EAAE,CAAC,SAAS,EAAE,KAAK,IAAI,EAAE;YAAE,IAAI,CAAC,CAAC,CAAC,CAAC;QACnD,KAAK,MAAM,GAAG,IAAI,EAAE,CAAC,KAAK,IAAI,EAAE;YAAE,KAAK,MAAM,IAAI,IAAI,GAAG;gBAAE,IAAI,CAAC,IAAI,CAAC,CAAC;IACvE,CAAC;IACD,OAAO,IAAI,CAAC;AACd,CAAC;AAED;;;;;;;;;;;;;;;GAeG;AACH,MAAM,UAAU,kBAAkB,CAAC,IAAmB;IACpD,MAAM,GAAG,GAAuB,EAAE,CAAC;IACnC,KAAK,MAAM,CAAC,IAAI,IAAI,EAAE,CAAC;QACrB,IAAI,CAAC,CAAC,IAAI,KAAK,SAAS;YAAE,SAAS;QACnC,KAAK,MAAM,CAAC,IAAI,CAAC,CAAC,SAAS,EAAE,CAAC;YAC5B,MAAM,EAAE,KAAK,EAAE,QAAQ,EAAE,GAAG,eAAe,CAAC,CAAC,CAAC,KAAK,CAAC,CAAC;YACrD,IAAI,QAAQ,KAAK,IAAI;gBAAE,SAAS;YAChC,MAAM,IAAI,GAAG,gBAAgB,CAAC,KAAK,CAAC,CAAC;YACrC,MAAM,OAAO,GAAG,IAAI,GAAG,CAAC,QAAQ,CAAC,MAAM,CAAC,CAAC;YACzC,MAAM,OAAO,GAAG,CAAC,GAAG,IAAI,CAAC,CAAC,MAAM,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,OAAO,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC,CAAC,IAAI,EAAE,CAAC;YAChE,MAAM,YAAY,GAAG,QAAQ,CAAC,MAAM,CAAC,MAAM,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC,CAAC;YACjE,IAAI,OAAO,CAAC,MAAM,GAAG,CAAC,IAAI,YAAY,CAAC,MAAM,GAAG,CAAC,EAAE,CAAC;gBAClD,GAAG,CAAC,IAAI,CAAC,EAAE,WAAW,EAAE,CAAC,CAAC,IAAI,EAAE,QAAQ,EAAE,CAAC,CAAC,IAAI,EAAE,OAAO,EAAE,YAAY,EAAE,CAAC,CAAC;YAC7E,CAAC;QACH,CAAC;IACH,CAAC;IACD,OAAO,GAAG,CAAC;AACb,CAAC;AAED;;;;;GAKG;AACH,MAAM,UAAU,gBAAgB,CAAC,KAAa,EAAE,OAAe,EAAE,IAAmB;IAClF,MAAM,GAAG,GAAG,kBAAkB,CAAC,IAAI,CAAC,CAAC;IACrC,MAAM,QAAQ,GAAc,EAAE,CAAC;IAC/B,MAAM,OAAO,GAAG,GAAG,CAAC,MAAM,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,OAAO,CAAC,MAAM,GAAG,CAAC,CAAC,CAAC;IACxD,IAAI,OAAO,CAAC,MAAM,GAAG,CAAC,EAAE,CAAC;QACvB,QAAQ,CAAC,IAAI,CAAC;YACZ,QAAQ,EAAE,OAAO;YACjB,IAAI,EAAE,+BAA+B;YACrC,OAAO;YACP,OAAO,EACL,GAAG,KAAK,KAAK,OAAO,CAAC,MAAM,yEAAyE;gBACpG,qFAAqF;gBACrF,qEAAqE;YACvE,OAAO,EAAE,OAAO,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,GAAG,CAAC,CAAC,WAAW,MAAM,CAAC,CAAC,QAAQ,KAAK,CAAC,CAAC,OAAO,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC,IAAI,CAAC,IAAI,CAAC,EAAE,CAAC;YAC7G,IAAI,EAAE,EAAE,YAAY,EAAE,IAAI,EAAE;SAC7B,CAAC,CAAC;IACL,CAAC;IACD,MAAM,IAAI,GAAG,GAAG,CAAC,MAAM,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,YAAY,CAAC,MAAM,GAAG,CAAC,CAAC,CAAC;IAC1D,IAAI,IAAI,CAAC,MAAM,GAAG,CAAC,EAAE,CAAC;QACpB,QAAQ,CAAC,IAAI,CAAC;YACZ,QAAQ,EAAE,MAAM;YAChB,IAAI,EAAE,oCAAoC;YAC1C,OAAO;YACP,OAAO,EACL,GAAG,KAAK,KAAK,IAAI,CAAC,MAAM,kEAAkE;gBAC1F,6GAA6G;gBAC7G,+CAA+C;YACjD,OAAO,EAAE,IAAI,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,GAAG,CAAC,CAAC,WAAW,MAAM,CAAC,CAAC,QAAQ,KAAK,CAAC,CAAC,YAAY,CAAC,IAAI,CAAC,IAAI,CAAC,EAAE,CAAC;YAC1F,IAAI,EAAE,EAAE,YAAY,EAAE,IAAI,EAAE;SAC7B,CAAC,CAAC;IACL,CAAC;IACD,OAAO,QAAQ,CAAC;AAClB,CAAC;AAED,wFAAwF;AACxF,MAAM,UAAU,qBAAqB,CAAC,KAAa,EAAE,OAAe,EAAE,IAAmB;IACvF,MAAM,IAAI,GAAG,sBAAsB,CAAC,IAAI,CAAC,CAAC;IAC1C,IAAI,IAAI,CAAC,MAAM,KAAK,CAAC;QAAE,OAAO,EAAE,CAAC;IACjC,OAAO;QACL;YACE,QAAQ,EAAE,OAAO;YACjB,IAAI,EAAE,qCAAqC;YAC3C,OAAO;YACP,OAAO,EACL,GAAG,KAAK,KAAK,IAAI,CAAC,MAAM,4CAA4C;gBACpE,gGAAgG;gBAChG,qGAAqG;YACvG,OAAO,EAAE,IAAI,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,GAAG,CAAC,CAAC,WAAW,MAAM,CAAC,CAAC,QAAQ,EAAE,CAAC;YAC5D,IAAI,EAAE,EAAE,YAAY,EAAE,IAAI,EAAE;SAC7B;KACF,CAAC;AACJ,CAAC","sourcesContent":["/**\n * What a requirement's scenarios are MISSING, and where they contradict\n * themselves — the coverage and shape questions `validate` asks of every spec.\n *\n * Separate from `./parse.ts` because these grade a parsed document rather than\n * produce one, and separate from `./spec.ts` because they answer to\n * `vocabulary/report.ts`: a stepless scenario is a Finding with a subject and a\n * label, and that shape belongs to the report, not to the requirement.\n */\nimport { scenarioGherkin, type EmittedStep } from \"../vocabulary/steps.js\";\nimport type { Finding } from \"../vocabulary/report.js\";\nimport { type Requirement } from \"./spec.js\";\n\n/** Requirements with no scenario — the OpenSpec coverage rule (every requirement needs ≥1). */\nexport function requirementsMissingScenarios(reqs: Requirement[]): Requirement[] {\n return reqs.filter((r) => r.kind !== \"REMOVED\" && r.scenarios.length === 0);\n}\n\n/** A `#### Scenario:` heading whose body holds nothing a runner would execute. */\nexport interface SteplessScenario {\n requirement: string;\n scenario: string;\n}\n\n/**\n * Scenarios that satisfy the coverage rule above and test nothing.\n *\n * `requirementsMissingScenarios` counts HEADINGS, so a `#### Scenario:` with no\n * recognizable Given/When/Then line satisfied the fleet gate's coverage claim\n * outright — and it is not a hypothetical shape: it is what `loam new`\n * scaffolds and what an agent writes when it puts the criteria in prose.\n * Downstream the same emptiness is fatal twice over: cucumber runs a stepless\n * scenario vacuously green, and `loam verify --results` can never confirm it.\n *\n * The detector is `loam gherkin`'s own — `scenarioGherkin` decides what a step\n * IS, and a second opinion here would mean the gate and the emitted suite could\n * disagree about whether a scenario has any. That import is the one place these\n * two modules are circular (gherkin.ts parses requirements); both sides export\n * only function declarations, which are hoisted, so neither evaluation order\n * can observe a half-built module.\n */\nexport function steplessScenarios(reqs: Requirement[]): SteplessScenario[] {\n const out: SteplessScenario[] = [];\n for (const r of reqs) {\n if (r.kind === \"REMOVED\") continue;\n for (const s of r.scenarios) {\n if (scenarioGherkin(s.lines).steps.length === 0) out.push({ requirement: r.name, scenario: s.name });\n }\n }\n return out;\n}\n\n/**\n * The stepless half of the coverage check, as findings — the missing-scenario\n * half stays where it is (`requirementsMissingScenarios`), because the two are\n * different states with different fixes: one requirement owes acceptance\n * criteria, the other owes STEPS in criteria somebody has already written.\n *\n * `label` is the coverage check's own label (`payment-service: requirements`),\n * so both halves of one document name it the same way.\n */\nexport function steplessFindings(label: string, subject: string, reqs: Requirement[]): Finding[] {\n const bare = steplessScenarios(reqs);\n if (bare.length === 0) return [];\n return [\n {\n severity: \"error\",\n code: \"requirements.stepless-scenario\",\n subject,\n message:\n `${label}: ${bare.length} scenario(s) have a heading and no recognizable steps — ` +\n `they satisfy the coverage rule and test nothing. Cucumber runs a stepless scenario vacuously green and ` +\n `\\`loam verify --results\\` can never confirm it. Write the body as \\`- **Given/When/Then**\\` bullets.`,\n details: bare.map((s) => `${s.requirement} — ${s.scenario}`),\n text: { detailPrefix: \"- \" },\n },\n ];\n}\n\n/**\n * Scenarios that have steps and assert NOTHING — a body of Given and When with\n * no Then anywhere in it.\n *\n * The third state, and the one that survives both checks above: the requirement\n * has a scenario, the scenario has steps, and the run can only ever confirm that\n * the arrangement did not throw. `And`/`But` are continuations, so their meaning\n * is decided by the keyword they follow — with no `Then` in the body there is\n * nothing for them to continue but the setup.\n *\n * It matters more the more evidence rests on the suite. A stepless scenario is\n * at least visible as an empty body; this one reads like a real test, emits real\n * steps, runs green, and answers a `scenario.tested` claim to the same standard\n * as a scenario that checks twenty things. Under a runner-neutral answer sheet —\n * one `{digest, status}` entry, no steps to count — this document-side check is\n * the only thing left that can tell the two apart.\n */\nexport function assertionlessScenarios(reqs: Requirement[]): SteplessScenario[] {\n const out: SteplessScenario[] = [];\n for (const r of reqs) {\n if (r.kind === \"REMOVED\") continue;\n for (const s of r.scenarios) {\n const { steps } = scenarioGherkin(s.lines);\n // Disjoint from stepless by construction: a body with no steps at all is\n // that finding's, and reporting one scenario under both codes would offer\n // two fixes for one defect.\n if (steps.length > 0 && !steps.some((st) => /^Then\\b/.test(st.text))) {\n out.push({ requirement: r.name, scenario: s.name });\n }\n }\n }\n return out;\n}\n\n/** One scenario's disagreement between its `Examples` columns and its `<placeholders>`. */\nexport interface ExamplesMismatch {\n requirement: string;\n scenario: string;\n /** `<placeholders>` a step uses that no column supplies. */\n unbound: string[];\n /** Columns no step, docstring or data-table cell refers to. */\n unreferenced: string[];\n}\n\nconst PLACEHOLDER_RE = /<([^<>\\s]+)>/g;\n\n/**\n * Every `<placeholder>` a scenario's steps reach for, arguments included.\n *\n * Docstrings and data tables are scanned too because cucumber substitutes into\n * them: `<fee>` inside a request payload is a real reference, and a check that\n * read only the step lines would call its column unreferenced and invite the\n * author to delete the column the payload depends on.\n */\nfunction placeholdersUsed(steps: EmittedStep[]): Set<string> {\n const used = new Set<string>();\n const scan = (text: string): void => {\n for (const m of text.matchAll(PLACEHOLDER_RE)) used.add(m[1]!);\n };\n for (const st of steps) {\n scan(st.text);\n for (const l of st.docstring?.lines ?? []) scan(l);\n for (const row of st.table ?? []) for (const cell of row) scan(cell);\n }\n return used;\n}\n\n/**\n * Scenarios whose `Examples` header and `<placeholders>` disagree.\n *\n * Only scenarios that HAVE an Examples table are considered, and that scoping is\n * load-bearing twice over: a plain scenario's `<angle brackets>` are prose (a\n * generic, a placeholder in a URL template, loam's own scaffold sentinels), and\n * a fleet that never writes an outline must never earn a finding from this.\n *\n * The two halves are different defects. An UNBOUND placeholder is an error the\n * runner cannot repair: cucumber substitutes nothing, so the step definition\n * receives the literal text `<fee>` and either fails obscurely or — worse —\n * matches and asserts against a string nobody meant. An UNREFERENCED column is a\n * warning, because it is usually the fossil of a rename: the column still lists\n * six cases, the step that used it now names something else, and the outline\n * runs six identical passes that read as six cases of coverage.\n */\nexport function examplesMismatches(reqs: Requirement[]): ExamplesMismatch[] {\n const out: ExamplesMismatch[] = [];\n for (const r of reqs) {\n if (r.kind === \"REMOVED\") continue;\n for (const s of r.scenarios) {\n const { steps, examples } = scenarioGherkin(s.lines);\n if (examples === null) continue;\n const used = placeholdersUsed(steps);\n const columns = new Set(examples.header);\n const unbound = [...used].filter((p) => !columns.has(p)).sort();\n const unreferenced = examples.header.filter((c) => !used.has(c));\n if (unbound.length > 0 || unreferenced.length > 0) {\n out.push({ requirement: r.name, scenario: s.name, unbound, unreferenced });\n }\n }\n }\n return out;\n}\n\n/**\n * The Examples half, as findings — two codes rather than one with two\n * severities, because a machine cannot branch on a severity that depends on the\n * case, and the fixes differ: one adds a column, the other removes one or\n * restores the reference that used to use it.\n */\nexport function examplesFindings(label: string, subject: string, reqs: Requirement[]): Finding[] {\n const all = examplesMismatches(reqs);\n const findings: Finding[] = [];\n const unbound = all.filter((m) => m.unbound.length > 0);\n if (unbound.length > 0) {\n findings.push({\n severity: \"error\",\n code: \"requirements.examples-unbound\",\n subject,\n message:\n `${label}: ${unbound.length} scenario(s) use a \\`<placeholder>\\` no \\`Examples\\` column supplies — ` +\n `cucumber substitutes nothing, so the step receives the literal angle-bracket text. ` +\n `Add the column, or correct the placeholder to one the header names.`,\n details: unbound.map((m) => `${m.requirement} — ${m.scenario}: ${m.unbound.map((p) => `<${p}>`).join(\", \")}`),\n text: { detailPrefix: \"- \" },\n });\n }\n const dead = all.filter((m) => m.unreferenced.length > 0);\n if (dead.length > 0) {\n findings.push({\n severity: \"warn\",\n code: \"requirements.examples-unreferenced\",\n subject,\n message:\n `${label}: ${dead.length} scenario(s) declare an \\`Examples\\` column no step refers to — ` +\n `usually the fossil of a rename, and the outline then runs its rows as identical passes that read as cases. ` +\n `Reference the column from a step, or drop it.`,\n details: dead.map((m) => `${m.requirement} — ${m.scenario}: ${m.unreferenced.join(\", \")}`),\n text: { detailPrefix: \"- \" },\n });\n }\n return findings;\n}\n\n/** The assertionless half, as findings — same label and subject as its two siblings. */\nexport function assertionlessFindings(label: string, subject: string, reqs: Requirement[]): Finding[] {\n const mute = assertionlessScenarios(reqs);\n if (mute.length === 0) return [];\n return [\n {\n severity: \"error\",\n code: \"requirements.assertionless-scenario\",\n subject,\n message:\n `${label}: ${mute.length} scenario(s) have steps and no \\`Then\\` — ` +\n `they arrange and act, and assert nothing. The run confirms only that the setup did not throw, ` +\n `and the claim it answers is confirmed on that. Add the \\`- **Then**\\` the scenario was written for.`,\n details: mute.map((s) => `${s.requirement} — ${s.scenario}`),\n text: { detailPrefix: \"- \" },\n },\n ];\n}\n"]}
|
|
1
|
+
{"version":3,"file":"scenarios.js","sourceRoot":"","sources":["../../../src/core/document/scenarios.ts"],"names":[],"mappings":"AAAA;;;;;;;;GAQG;AACH,OAAO,EAAE,eAAe,EAAoB,MAAM,wBAAwB,CAAC;AAE3E,OAAO,EAAoB,MAAM,WAAW,CAAC;AAE7C,+FAA+F;AAC/F,MAAM,UAAU,4BAA4B,CAAC,IAAmB;IAC9D,OAAO,IAAI,CAAC,MAAM,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,IAAI,KAAK,SAAS,IAAI,CAAC,CAAC,SAAS,CAAC,MAAM,KAAK,CAAC,CAAC,CAAC;AAC9E,CAAC;AAED;;;;;;;GAOG;AACH,MAAM,UAAU,uBAAuB,CAAC,KAAa,EAAE,IAA4B,EAAE,OAAgB;IACnG,OAAO;QACL,QAAQ,EAAE,OAAO;QACjB,IAAI,EAAE,gCAAgC;QACtC,GAAG,CAAC,OAAO,KAAK,SAAS,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,EAAE,OAAO,EAAE,CAAC;QAC7C,OAAO,EAAE,GAAG,KAAK,KAAK,IAAI,CAAC,MAAM,oCAAoC;QACrE,OAAO,EAAE,IAAI,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,IAAI,CAAC;KACjC,CAAC;AACJ,CAAC;AAQD;;;;;;;;;;;;;;;;GAgBG;AACH,MAAM,UAAU,iBAAiB,CAAC,IAAmB;IACnD,MAAM,GAAG,GAAuB,EAAE,CAAC;IACnC,KAAK,MAAM,CAAC,IAAI,IAAI,EAAE,CAAC;QACrB,IAAI,CAAC,CAAC,IAAI,KAAK,SAAS;YAAE,SAAS;QACnC,KAAK,MAAM,CAAC,IAAI,CAAC,CAAC,SAAS,EAAE,CAAC;YAC5B,IAAI,eAAe,CAAC,CAAC,CAAC,KAAK,CAAC,CAAC,KAAK,CAAC,MAAM,KAAK,CAAC;gBAAE,GAAG,CAAC,IAAI,CAAC,EAAE,WAAW,EAAE,CAAC,CAAC,IAAI,EAAE,QAAQ,EAAE,CAAC,CAAC,IAAI,EAAE,CAAC,CAAC;QACvG,CAAC;IACH,CAAC;IACD,OAAO,GAAG,CAAC;AACb,CAAC;AAED;;;;;;;;GAQG;AACH,MAAM,UAAU,gBAAgB,CAAC,KAAa,EAAE,OAAe,EAAE,IAAmB;IAClF,MAAM,IAAI,GAAG,iBAAiB,CAAC,IAAI,CAAC,CAAC;IACrC,IAAI,IAAI,CAAC,MAAM,KAAK,CAAC;QAAE,OAAO,EAAE,CAAC;IACjC,OAAO;QACL;YACE,QAAQ,EAAE,OAAO;YACjB,IAAI,EAAE,gCAAgC;YACtC,OAAO;YACP,OAAO,EACL,GAAG,KAAK,KAAK,IAAI,CAAC,MAAM,0DAA0D;gBAClF,yGAAyG;gBACzG,sGAAsG;YACxG,OAAO,EAAE,IAAI,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,GAAG,CAAC,CAAC,WAAW,MAAM,CAAC,CAAC,QAAQ,EAAE,CAAC;YAC5D,IAAI,EAAE,EAAE,YAAY,EAAE,IAAI,EAAE;SAC7B;KACF,CAAC;AACJ,CAAC;AAED;;;;;;;;;;;;;;;;GAgBG;AACH,MAAM,UAAU,sBAAsB,CAAC,IAAmB;IACxD,MAAM,GAAG,GAAuB,EAAE,CAAC;IACnC,KAAK,MAAM,CAAC,IAAI,IAAI,EAAE,CAAC;QACrB,IAAI,CAAC,CAAC,IAAI,KAAK,SAAS;YAAE,SAAS;QACnC,KAAK,MAAM,CAAC,IAAI,CAAC,CAAC,SAAS,EAAE,CAAC;YAC5B,MAAM,EAAE,KAAK,EAAE,GAAG,eAAe,CAAC,CAAC,CAAC,KAAK,CAAC,CAAC;YAC3C,yEAAyE;YACzE,0EAA0E;YAC1E,4BAA4B;YAC5B,IAAI,KAAK,CAAC,MAAM,GAAG,CAAC,IAAI,CAAC,KAAK,CAAC,IAAI,CAAC,CAAC,EAAE,EAAE,EAAE,CAAC,SAAS,CAAC,IAAI,CAAC,EAAE,CAAC,IAAI,CAAC,CAAC,EAAE,CAAC;gBACrE,GAAG,CAAC,IAAI,CAAC,EAAE,WAAW,EAAE,CAAC,CAAC,IAAI,EAAE,QAAQ,EAAE,CAAC,CAAC,IAAI,EAAE,CAAC,CAAC;YACtD,CAAC;QACH,CAAC;IACH,CAAC;IACD,OAAO,GAAG,CAAC;AACb,CAAC;AAYD,MAAM,cAAc,GAAG,eAAe,CAAC;AAEvC;;;;;;;GAOG;AACH,SAAS,gBAAgB,CAAC,KAAoB;IAC5C,MAAM,IAAI,GAAG,IAAI,GAAG,EAAU,CAAC;IAC/B,MAAM,IAAI,GAAG,CAAC,IAAY,EAAQ,EAAE;QAClC,KAAK,MAAM,CAAC,IAAI,IAAI,CAAC,QAAQ,CAAC,cAAc,CAAC;YAAE,IAAI,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC,CAAE,CAAC,CAAC;IACjE,CAAC,CAAC;IACF,KAAK,MAAM,EAAE,IAAI,KAAK,EAAE,CAAC;QACvB,IAAI,CAAC,EAAE,CAAC,IAAI,CAAC,CAAC;QACd,KAAK,MAAM,CAAC,IAAI,EAAE,CAAC,SAAS,EAAE,KAAK,IAAI,EAAE;YAAE,IAAI,CAAC,CAAC,CAAC,CAAC;QACnD,KAAK,MAAM,GAAG,IAAI,EAAE,CAAC,KAAK,IAAI,EAAE;YAAE,KAAK,MAAM,IAAI,IAAI,GAAG;gBAAE,IAAI,CAAC,IAAI,CAAC,CAAC;IACvE,CAAC;IACD,OAAO,IAAI,CAAC;AACd,CAAC;AAED;;;;;;;;;;;;;;;GAeG;AACH,MAAM,UAAU,kBAAkB,CAAC,IAAmB;IACpD,MAAM,GAAG,GAAuB,EAAE,CAAC;IACnC,KAAK,MAAM,CAAC,IAAI,IAAI,EAAE,CAAC;QACrB,IAAI,CAAC,CAAC,IAAI,KAAK,SAAS;YAAE,SAAS;QACnC,KAAK,MAAM,CAAC,IAAI,CAAC,CAAC,SAAS,EAAE,CAAC;YAC5B,MAAM,EAAE,KAAK,EAAE,QAAQ,EAAE,GAAG,eAAe,CAAC,CAAC,CAAC,KAAK,CAAC,CAAC;YACrD,IAAI,QAAQ,KAAK,IAAI;gBAAE,SAAS;YAChC,MAAM,IAAI,GAAG,gBAAgB,CAAC,KAAK,CAAC,CAAC;YACrC,MAAM,OAAO,GAAG,IAAI,GAAG,CAAC,QAAQ,CAAC,MAAM,CAAC,CAAC;YACzC,MAAM,OAAO,GAAG,CAAC,GAAG,IAAI,CAAC,CAAC,MAAM,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,OAAO,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC,CAAC,IAAI,EAAE,CAAC;YAChE,MAAM,YAAY,GAAG,QAAQ,CAAC,MAAM,CAAC,MAAM,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC,CAAC;YACjE,IAAI,OAAO,CAAC,MAAM,GAAG,CAAC,IAAI,YAAY,CAAC,MAAM,GAAG,CAAC,EAAE,CAAC;gBAClD,GAAG,CAAC,IAAI,CAAC,EAAE,WAAW,EAAE,CAAC,CAAC,IAAI,EAAE,QAAQ,EAAE,CAAC,CAAC,IAAI,EAAE,OAAO,EAAE,YAAY,EAAE,CAAC,CAAC;YAC7E,CAAC;QACH,CAAC;IACH,CAAC;IACD,OAAO,GAAG,CAAC;AACb,CAAC;AAED;;;;;GAKG;AACH,MAAM,UAAU,gBAAgB,CAAC,KAAa,EAAE,OAAe,EAAE,IAAmB;IAClF,MAAM,GAAG,GAAG,kBAAkB,CAAC,IAAI,CAAC,CAAC;IACrC,MAAM,QAAQ,GAAc,EAAE,CAAC;IAC/B,MAAM,OAAO,GAAG,GAAG,CAAC,MAAM,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,OAAO,CAAC,MAAM,GAAG,CAAC,CAAC,CAAC;IACxD,IAAI,OAAO,CAAC,MAAM,GAAG,CAAC,EAAE,CAAC;QACvB,QAAQ,CAAC,IAAI,CAAC;YACZ,QAAQ,EAAE,OAAO;YACjB,IAAI,EAAE,+BAA+B;YACrC,OAAO;YACP,OAAO,EACL,GAAG,KAAK,KAAK,OAAO,CAAC,MAAM,yEAAyE;gBACpG,qFAAqF;gBACrF,qEAAqE;YACvE,OAAO,EAAE,OAAO,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,GAAG,CAAC,CAAC,WAAW,MAAM,CAAC,CAAC,QAAQ,KAAK,CAAC,CAAC,OAAO,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC,IAAI,CAAC,IAAI,CAAC,EAAE,CAAC;YAC7G,IAAI,EAAE,EAAE,YAAY,EAAE,IAAI,EAAE;SAC7B,CAAC,CAAC;IACL,CAAC;IACD,MAAM,IAAI,GAAG,GAAG,CAAC,MAAM,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,YAAY,CAAC,MAAM,GAAG,CAAC,CAAC,CAAC;IAC1D,IAAI,IAAI,CAAC,MAAM,GAAG,CAAC,EAAE,CAAC;QACpB,QAAQ,CAAC,IAAI,CAAC;YACZ,QAAQ,EAAE,MAAM;YAChB,IAAI,EAAE,oCAAoC;YAC1C,OAAO;YACP,OAAO,EACL,GAAG,KAAK,KAAK,IAAI,CAAC,MAAM,kEAAkE;gBAC1F,6GAA6G;gBAC7G,+CAA+C;YACjD,OAAO,EAAE,IAAI,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,GAAG,CAAC,CAAC,WAAW,MAAM,CAAC,CAAC,QAAQ,KAAK,CAAC,CAAC,YAAY,CAAC,IAAI,CAAC,IAAI,CAAC,EAAE,CAAC;YAC1F,IAAI,EAAE,EAAE,YAAY,EAAE,IAAI,EAAE;SAC7B,CAAC,CAAC;IACL,CAAC;IACD,OAAO,QAAQ,CAAC;AAClB,CAAC;AAED,wFAAwF;AACxF,MAAM,UAAU,qBAAqB,CAAC,KAAa,EAAE,OAAe,EAAE,IAAmB;IACvF,MAAM,IAAI,GAAG,sBAAsB,CAAC,IAAI,CAAC,CAAC;IAC1C,IAAI,IAAI,CAAC,MAAM,KAAK,CAAC;QAAE,OAAO,EAAE,CAAC;IACjC,OAAO;QACL;YACE,QAAQ,EAAE,OAAO;YACjB,IAAI,EAAE,qCAAqC;YAC3C,OAAO;YACP,OAAO,EACL,GAAG,KAAK,KAAK,IAAI,CAAC,MAAM,4CAA4C;gBACpE,gGAAgG;gBAChG,qGAAqG;YACvG,OAAO,EAAE,IAAI,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,GAAG,CAAC,CAAC,WAAW,MAAM,CAAC,CAAC,QAAQ,EAAE,CAAC;YAC5D,IAAI,EAAE,EAAE,YAAY,EAAE,IAAI,EAAE;SAC7B;KACF,CAAC;AACJ,CAAC","sourcesContent":["/**\n * What a requirement's scenarios are MISSING, and where they contradict\n * themselves — the coverage and shape questions `validate` asks of every spec.\n *\n * Separate from `./parse.ts` because these grade a parsed document rather than\n * produce one, and separate from `./spec.ts` because they answer to\n * `vocabulary/report.ts`: a stepless scenario is a Finding with a subject and a\n * label, and that shape belongs to the report, not to the requirement.\n */\nimport { scenarioGherkin, type EmittedStep } from \"../vocabulary/steps.js\";\nimport type { Finding } from \"../vocabulary/report.js\";\nimport { type Requirement } from \"./spec.js\";\n\n/** Requirements with no scenario — the OpenSpec coverage rule (every requirement needs ≥1). */\nexport function requirementsMissingScenarios(reqs: Requirement[]): Requirement[] {\n return reqs.filter((r) => r.kind !== \"REMOVED\" && r.scenarios.length === 0);\n}\n\n/**\n * `requirements.missing-scenarios` over the requirements the rule above\n * returned — one spelling for `loam validate` (its `coverageFinding`) and\n * `loam status` (`core/status/feature/feature.ts`), because status's rule is\n * \"never greener than the gates\" and a second spelling of the same finding is\n * how the two would come to disagree. `subject` names the service when the\n * caller has one.\n */\nexport function missingScenariosFinding(label: string, bare: readonly Requirement[], subject?: string): Finding {\n return {\n severity: \"error\",\n code: \"requirements.missing-scenarios\",\n ...(subject === undefined ? {} : { subject }),\n message: `${label}: ${bare.length} requirement(s) without a scenario`,\n details: bare.map((r) => r.name),\n };\n}\n\n/** A `#### Scenario:` heading whose body holds nothing a runner would execute. */\nexport interface SteplessScenario {\n requirement: string;\n scenario: string;\n}\n\n/**\n * Scenarios that satisfy the coverage rule above and test nothing.\n *\n * `requirementsMissingScenarios` counts HEADINGS, so a `#### Scenario:` with no\n * recognizable Given/When/Then line satisfied the fleet gate's coverage claim\n * outright — and it is not a hypothetical shape: it is what `loam new`\n * scaffolds and what an agent writes when it puts the criteria in prose.\n * Downstream the same emptiness is fatal twice over: cucumber runs a stepless\n * scenario vacuously green, and `loam verify --results` can never confirm it.\n *\n * The detector is `loam gherkin`'s own — `scenarioGherkin` decides what a step\n * IS, and a second opinion here would mean the gate and the emitted suite could\n * disagree about whether a scenario has any. That import is the one place these\n * two modules are circular (gherkin.ts parses requirements); both sides export\n * only function declarations, which are hoisted, so neither evaluation order\n * can observe a half-built module.\n */\nexport function steplessScenarios(reqs: Requirement[]): SteplessScenario[] {\n const out: SteplessScenario[] = [];\n for (const r of reqs) {\n if (r.kind === \"REMOVED\") continue;\n for (const s of r.scenarios) {\n if (scenarioGherkin(s.lines).steps.length === 0) out.push({ requirement: r.name, scenario: s.name });\n }\n }\n return out;\n}\n\n/**\n * The stepless half of the coverage check, as findings — the missing-scenario\n * half stays where it is (`requirementsMissingScenarios`), because the two are\n * different states with different fixes: one requirement owes acceptance\n * criteria, the other owes STEPS in criteria somebody has already written.\n *\n * `label` is the coverage check's own label (`payment-service: requirements`),\n * so both halves of one document name it the same way.\n */\nexport function steplessFindings(label: string, subject: string, reqs: Requirement[]): Finding[] {\n const bare = steplessScenarios(reqs);\n if (bare.length === 0) return [];\n return [\n {\n severity: \"error\",\n code: \"requirements.stepless-scenario\",\n subject,\n message:\n `${label}: ${bare.length} scenario(s) have a heading and no recognizable steps — ` +\n `they satisfy the coverage rule and test nothing. Cucumber runs a stepless scenario vacuously green and ` +\n `\\`loam verify --results\\` can never confirm it. Write the body as \\`- **Given/When/Then**\\` bullets.`,\n details: bare.map((s) => `${s.requirement} — ${s.scenario}`),\n text: { detailPrefix: \"- \" },\n },\n ];\n}\n\n/**\n * Scenarios that have steps and assert NOTHING — a body of Given and When with\n * no Then anywhere in it.\n *\n * The third state, and the one that survives both checks above: the requirement\n * has a scenario, the scenario has steps, and the run can only ever confirm that\n * the arrangement did not throw. `And`/`But` are continuations, so their meaning\n * is decided by the keyword they follow — with no `Then` in the body there is\n * nothing for them to continue but the setup.\n *\n * It matters more the more evidence rests on the suite. A stepless scenario is\n * at least visible as an empty body; this one reads like a real test, emits real\n * steps, runs green, and answers a `scenario.tested` claim to the same standard\n * as a scenario that checks twenty things. Under a runner-neutral answer sheet —\n * one `{digest, status}` entry, no steps to count — this document-side check is\n * the only thing left that can tell the two apart.\n */\nexport function assertionlessScenarios(reqs: Requirement[]): SteplessScenario[] {\n const out: SteplessScenario[] = [];\n for (const r of reqs) {\n if (r.kind === \"REMOVED\") continue;\n for (const s of r.scenarios) {\n const { steps } = scenarioGherkin(s.lines);\n // Disjoint from stepless by construction: a body with no steps at all is\n // that finding's, and reporting one scenario under both codes would offer\n // two fixes for one defect.\n if (steps.length > 0 && !steps.some((st) => /^Then\\b/.test(st.text))) {\n out.push({ requirement: r.name, scenario: s.name });\n }\n }\n }\n return out;\n}\n\n/** One scenario's disagreement between its `Examples` columns and its `<placeholders>`. */\nexport interface ExamplesMismatch {\n requirement: string;\n scenario: string;\n /** `<placeholders>` a step uses that no column supplies. */\n unbound: string[];\n /** Columns no step, docstring or data-table cell refers to. */\n unreferenced: string[];\n}\n\nconst PLACEHOLDER_RE = /<([^<>\\s]+)>/g;\n\n/**\n * Every `<placeholder>` a scenario's steps reach for, arguments included.\n *\n * Docstrings and data tables are scanned too because cucumber substitutes into\n * them: `<fee>` inside a request payload is a real reference, and a check that\n * read only the step lines would call its column unreferenced and invite the\n * author to delete the column the payload depends on.\n */\nfunction placeholdersUsed(steps: EmittedStep[]): Set<string> {\n const used = new Set<string>();\n const scan = (text: string): void => {\n for (const m of text.matchAll(PLACEHOLDER_RE)) used.add(m[1]!);\n };\n for (const st of steps) {\n scan(st.text);\n for (const l of st.docstring?.lines ?? []) scan(l);\n for (const row of st.table ?? []) for (const cell of row) scan(cell);\n }\n return used;\n}\n\n/**\n * Scenarios whose `Examples` header and `<placeholders>` disagree.\n *\n * Only scenarios that HAVE an Examples table are considered, and that scoping is\n * load-bearing twice over: a plain scenario's `<angle brackets>` are prose (a\n * generic, a placeholder in a URL template, loam's own scaffold sentinels), and\n * a fleet that never writes an outline must never earn a finding from this.\n *\n * The two halves are different defects. An UNBOUND placeholder is an error the\n * runner cannot repair: cucumber substitutes nothing, so the step definition\n * receives the literal text `<fee>` and either fails obscurely or — worse —\n * matches and asserts against a string nobody meant. An UNREFERENCED column is a\n * warning, because it is usually the fossil of a rename: the column still lists\n * six cases, the step that used it now names something else, and the outline\n * runs six identical passes that read as six cases of coverage.\n */\nexport function examplesMismatches(reqs: Requirement[]): ExamplesMismatch[] {\n const out: ExamplesMismatch[] = [];\n for (const r of reqs) {\n if (r.kind === \"REMOVED\") continue;\n for (const s of r.scenarios) {\n const { steps, examples } = scenarioGherkin(s.lines);\n if (examples === null) continue;\n const used = placeholdersUsed(steps);\n const columns = new Set(examples.header);\n const unbound = [...used].filter((p) => !columns.has(p)).sort();\n const unreferenced = examples.header.filter((c) => !used.has(c));\n if (unbound.length > 0 || unreferenced.length > 0) {\n out.push({ requirement: r.name, scenario: s.name, unbound, unreferenced });\n }\n }\n }\n return out;\n}\n\n/**\n * The Examples half, as findings — two codes rather than one with two\n * severities, because a machine cannot branch on a severity that depends on the\n * case, and the fixes differ: one adds a column, the other removes one or\n * restores the reference that used to use it.\n */\nexport function examplesFindings(label: string, subject: string, reqs: Requirement[]): Finding[] {\n const all = examplesMismatches(reqs);\n const findings: Finding[] = [];\n const unbound = all.filter((m) => m.unbound.length > 0);\n if (unbound.length > 0) {\n findings.push({\n severity: \"error\",\n code: \"requirements.examples-unbound\",\n subject,\n message:\n `${label}: ${unbound.length} scenario(s) use a \\`<placeholder>\\` no \\`Examples\\` column supplies — ` +\n `cucumber substitutes nothing, so the step receives the literal angle-bracket text. ` +\n `Add the column, or correct the placeholder to one the header names.`,\n details: unbound.map((m) => `${m.requirement} — ${m.scenario}: ${m.unbound.map((p) => `<${p}>`).join(\", \")}`),\n text: { detailPrefix: \"- \" },\n });\n }\n const dead = all.filter((m) => m.unreferenced.length > 0);\n if (dead.length > 0) {\n findings.push({\n severity: \"warn\",\n code: \"requirements.examples-unreferenced\",\n subject,\n message:\n `${label}: ${dead.length} scenario(s) declare an \\`Examples\\` column no step refers to — ` +\n `usually the fossil of a rename, and the outline then runs its rows as identical passes that read as cases. ` +\n `Reference the column from a step, or drop it.`,\n details: dead.map((m) => `${m.requirement} — ${m.scenario}: ${m.unreferenced.join(\", \")}`),\n text: { detailPrefix: \"- \" },\n });\n }\n return findings;\n}\n\n/** The assertionless half, as findings — same label and subject as its two siblings. */\nexport function assertionlessFindings(label: string, subject: string, reqs: Requirement[]): Finding[] {\n const mute = assertionlessScenarios(reqs);\n if (mute.length === 0) return [];\n return [\n {\n severity: \"error\",\n code: \"requirements.assertionless-scenario\",\n subject,\n message:\n `${label}: ${mute.length} scenario(s) have steps and no \\`Then\\` — ` +\n `they arrange and act, and assert nothing. The run confirms only that the setup did not throw, ` +\n `and the claim it answers is confirmed on that. Add the \\`- **Then**\\` the scenario was written for.`,\n details: mute.map((s) => `${s.requirement} — ${s.scenario}`),\n text: { detailPrefix: \"- \" },\n },\n ];\n}\n"]}
|
|
@@ -178,10 +178,10 @@ export function withoutRealizesPins(requirement) {
|
|
|
178
178
|
* The identity of a requirement's CONTENT: sha256 over its canonical
|
|
179
179
|
* serialization, truncated like every other digest loam stamps.
|
|
180
180
|
*
|
|
181
|
-
* Canonical, not raw bytes, and deliberately:
|
|
182
|
-
*
|
|
183
|
-
* make a baseline go stale over reflowed blank lines — a false alarm on
|
|
184
|
-
* check whose whole value is that it only fires when something
|
|
181
|
+
* Canonical, not raw bytes, and deliberately: archive writes every requirement
|
|
182
|
+
* a delta adds or replaces through `serializeRequirements`, so hashing bytes
|
|
183
|
+
* would make a baseline go stale over reflowed blank lines — a false alarm on
|
|
184
|
+
* the one check whose whole value is that it only fires when something moved.
|
|
185
185
|
* What the hash therefore covers is exactly what survives a round trip: the
|
|
186
186
|
* heading, the body (`Requirement-ID:` and `Operations:`/`Covers:` lines
|
|
187
187
|
* included), and every scenario with its Given/When/Then lines. `kind` and
|
|
@@ -231,11 +231,4 @@ export function serializeRequirements(reqs) {
|
|
|
231
231
|
}
|
|
232
232
|
return chunks.join("\n\n").trim() + "\n";
|
|
233
233
|
}
|
|
234
|
-
/**
|
|
235
|
-
* A delta requirement as it lands in the living document, with the bookkeeping
|
|
236
|
-
* that belongs to the DELTA left behind: the `Based-On:` pin (a claim about
|
|
237
|
-
* which living version this was written against — meaningless once it IS the
|
|
238
|
-
* living version, and poison for the next feature's baseline, which would then
|
|
239
|
-
* hash the previous feature's pin) and the line it was parsed from.
|
|
240
|
-
*/
|
|
241
234
|
//# sourceMappingURL=spec.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"spec.js","sourceRoot":"","sources":["../../../src/core/document/spec.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * What a requirement IS, what makes one the same requirement twice, and the one\n * canonical way it is written back out.\n *\n * Living specs use `## Requirements` with `### Requirement:` + `#### Scenario:`.\n * Feature deltas group requirements under `## ADDED|MODIFIED|REMOVED\n * Requirements`. Scenarios are the acceptance criteria (Given/When/Then) and\n * the source for tests.\n *\n * Identity and serialization cannot be separated here: `requirementDigest` is\n * defined AS the sha256 of the serialized requirement without its baseline, so\n * a second module owning the writer would mean the digest and the bytes it\n * claims to describe could disagree. `./parse.ts` turns markdown into these,\n * `./apply.ts` folds a delta into a living set of them.\n */\nimport { createHash } from \"node:crypto\";\n\nexport type DeltaKind = \"ADDED\" | \"MODIFIED\" | \"REMOVED\" | \"BASE\";\n\nexport interface Scenario {\n name: string;\n lines: string[];\n}\n\nexport interface Requirement {\n /** BASE for a living spec; ADDED/MODIFIED/REMOVED inside a feature delta. */\n kind: DeltaKind;\n /**\n * Stable identity from a `Requirement-ID:` body line. Optional so existing\n * OpenSpec-compatible documents continue to use their heading as identity.\n */\n id?: string;\n /**\n * The living requirement this delta was WRITTEN AGAINST, from a `Based-On:`\n * body line — `requirementDigest` of that requirement at authoring time.\n *\n * Delta-only, and meaningful on MODIFIED/REMOVED alone: those two address a\n * requirement that already exists, and a MODIFIED carries its full new text,\n * so archive does not merge the living wording — it REPLACES it. Two features\n * rewriting one requirement therefore lose the loser's text outright, and the\n * in-flight warning (`delta.modified-conflict`) cannot see the case that\n * actually happens: once the first archives it leaves `changes/`, the second\n * goes green again, and its stale text lands on top without a word. This is\n * the pin that closes that window — the same trick as `sources_digest`\n * (core/provenance/stamp.ts), one requirement wide.\n *\n * Absent on requirements adopted from OpenSpec, which never had the line;\n * those keep the older, weaker protection and are told so once (warn).\n */\n basedOn?: string;\n name: string;\n text: string[];\n /** OpenAPI operationIds this requirement governs, from an `Operations:` line. */\n operations: string[];\n /**\n * What this requirement's scenarios exercise, from a `Covers:` line — the\n * architecture analog of `Operations:`. Entries are C4 element ids, edges\n * (`source -> target`), or health signals (`alert:<id>` / `sli:<id>`);\n * core/c4/arch.ts owns the grammar and the resolution. Parsed everywhere for one\n * grammar's sake, meaningful in arch.spec.md.\n */\n covers: string[];\n /**\n * Authorization this requirement is gated on, from a `Requires:` line —\n * `<subject>/<permission>` entries declared in `architecture/permissions.yaml`.\n *\n * A third join beside `Operations:` and `Covers:`, and it exists because the\n * other two cannot carry it. A permission is not an operation (one operation\n * is gated by several, and the same permission gates several operations) and\n * it is not a C4 element (it is a domain fact, not a structural one). The\n * subject half is what the OpenAPI security model cannot express at all:\n * `security` names what the CALLER must hold, while a permission is as often\n * checked on an entity inside the request — the profile a message is sent\n * from, the org a resource belongs to.\n */\n requires: string[];\n /**\n * Fleet capabilities this requirement realizes part of, from a `Capability:`\n * (also accepted: `Capabilities:`) line — ids declared in\n * `architecture/capabilities.yaml`.\n *\n * A fourth join beside `Operations:`, `Covers:` and `Requires:`, and a LIST\n * because the relation is many-to-many in both directions: one requirement\n * commonly closes part of two capabilities, and a capability is realized by\n * many requirements across several services — a single-valued field would\n * force authors to pick a lie. Neither existing line can carry it: a\n * capability is not an operation (it is behaviour above any one endpoint)\n * and not a C4 element (it is a business fact, not a structural one).\n * The parse is additive exactly as `requires` is — the line rides in\n * `req.text` and therefore inside `requirementDigest`, so no living\n * document's digest moves merely because loam learned to read it.\n */\n capabilities: string[];\n /**\n * Capability REQUIREMENTS this requirement realizes part of, from a\n * `Realizes:` line — entries spelled `<capability-id>#<Requirement-ID>`,\n * resolving against `capabilities/<cap>/spec.md`.\n *\n * A fifth join, and the one that makes the business tree checkable rather\n * than merely present. `Capability:` beside it answers a DIFFERENT question\n * and neither replaces the other: `Capability: checkout` says this\n * requirement is part of that capability, which is a claim about theme;\n * `Realizes: checkout#CHECKOUT-CHARGE-ONCE` says it is part of what makes one\n * named promise true, which is a claim loam can grade in both directions —\n * an entry that resolves to nothing, and a capability requirement nothing\n * realizes.\n *\n * WHY A COMPOSITE ENTRY RATHER THAN TWO LINES. A `Requirement-ID` is only\n * unique inside its own document, so the capability half is not decoration —\n * it is what makes the target addressable at all. Two lines would let a\n * requirement name three capabilities and four ids with nothing saying which\n * belongs to which.\n *\n * WHY THE SEPARATOR IS THE LAST `#` AND NOT THE FIRST. The requirement half\n * has a strict grammar (`REQUIREMENT_ID_RE`) that excludes `#`, while the\n * capability half is a YAML key and a directory name and is not constrained\n * here at all. Splitting at the last `#` is therefore unambiguous for every\n * capability id there is; splitting at the first would mis-parse any id that\n * contained one, and would do it silently.\n *\n * Parsed additively exactly as `capabilities` is — the line rides in\n * `req.text` and therefore inside `requirementDigest`, so no living\n * document's digest moves merely because loam learned to read it.\n */\n realizes: string[];\n /**\n * AsyncAPI message names this requirement governs on the PRODUCING side, from\n * a `Publishes:` line — the event-axis analog of `Operations:`.\n *\n * Two lines rather than one, because the async spine is directional in a way\n * the HTTP one is not: `Publishes:` resolves against this service's OWN\n * contract, while `Consumes:` names a message another service declares, and\n * only a fleet-wide view can say whether anybody does. Grading them alike\n * would either excuse a typo in a producer or point a consumer's error at the\n * wrong repository.\n */\n publishes: string[];\n /** AsyncAPI message names this requirement consumes, from a `Consumes:` line. See `publishes`. */\n consumes: string[];\n scenarios: Scenario[];\n /**\n * The H2 heading of its SOURCE DOCUMENT this requirement was parsed under,\n * verbatim (`## Behavior`), or absent if it preceded every heading. Records where\n * the text came from, so it stays true after a merge re-homes the requirement.\n *\n * `kind` alone cannot explain why a requirement is BASE, and the two BASE cases\n * differ completely: under `## Requirements` a delta is legally quoting the\n * living state, while under `## Behavior` the author wrote a change that archive\n * will silently not merge. `delta.requirement-not-merged` tells them apart.\n */\n section?: string;\n /**\n * 1-based line of this requirement's `### Requirement:` heading in its source\n * document. Body line `text[i]` is therefore at line `line + 1 + i` — the\n * capture is contiguous from the heading to the first scenario — which is what\n * lets `loam rebase` rewrite one `Based-On:` line by surgery instead of\n * reserializing the document and flattening the author's sections and prose.\n */\n line?: number;\n}\n\nexport const KIND_RE = /^##\\s+(ADDED|MODIFIED|REMOVED)\\s+Requirements\\s*$/i;\nexport const REQUIREMENT_ID_LINE_RE = /^\\s*Requirement-ID:\\s*(.*?)\\s*$/i;\nexport const BASED_ON_LINE_RE = /^\\s*Based-On:\\s*(.*?)\\s*$/i;\n\n/**\n * The `Capability:` body line (also accepted: `Capabilities:`), exactly as the\n * parser reads it — `./parse.ts` consumes the capture, migrate-openspec's\n * materializer tests presence with it before appending its own line. One\n * spelling on purpose: two copies of a grammar is how a \"presence\" test and\n * the parse quietly stop agreeing about what counts as the line.\n */\nexport const CAPABILITY_LINE_RE = /^\\s*Capabilit(?:y|ies):\\s*(.+?)\\s*$/i;\n\n/**\n * The `Realizes:` body line. One spelling only — no `Realize:` singular\n * alternative, unlike `Operations?:` and the rest — because the verb already\n * reads correctly for one entry and for six, and the plural forms in the older\n * lines exist to forgive a NOUN that changes shape. A grammar with an\n * alternative nobody needs is one more thing two readers can disagree about.\n */\nexport const REALIZES_LINE_RE = /^\\s*Realizes:\\s*(.+?)\\s*$/i;\n\n/**\n * The separator between a `Realizes:` target and the digest of the capability\n * requirement it was written against — `checkout#CHK-1@9f2c1a4b`.\n *\n * `@` and not a second `#`, because the two halves are read by different rules:\n * `splitRealizesEntry` takes the LAST `#` to find the requirement half, so a\n * second one would move that boundary and re-parse every existing entry. `@`\n * is also excluded from `REQUIREMENT_ID_RE`, which is what makes the split\n * unambiguous rather than a convention — an id can never contain one.\n *\n * NOBODY TYPES THIS. `loam rebase --living` writes it and rewrites it; a human\n * writes `Realizes: checkout#CHK-1` and is never asked for more. An entry\n * without a pin is not a defect and never becomes one — it is a claim that has\n * not been pinned yet, and it grades exactly as it did before pins existed.\n */\nexport const REALIZES_PIN_SEPARATOR = \"@\";\n\n/**\n * One `Realizes:` entry split into the target it names and the digest it was\n * written against, or `pin: null` when it carries none.\n *\n * A suffix that is not a well-formed digest is NOT a pin and NOT a refusal:\n * the whole entry stays the target, so `checkout#CHK-1@nonsense` resolves — or\n * fails to resolve — exactly as it would have before, under\n * `capability.realizes-unknown`, whose message names the entry the author\n * actually typed. Inventing a second refusal for one mistake is the failure\n * `splitRealizesEntry` already refuses two lines up.\n */\nexport function splitRealizesPin(entry: string): { target: string; pin: string | null } {\n const at = entry.lastIndexOf(REALIZES_PIN_SEPARATOR);\n if (at <= 0) return { target: entry, pin: null };\n const suffix = entry.slice(at + 1);\n if (!REQUIREMENT_DIGEST_RE.test(suffix)) return { target: entry, pin: null };\n return { target: entry.slice(0, at), pin: suffix };\n}\n\n/** Portable, review-friendly stable IDs. Case-sensitive by design. */\nexport const REQUIREMENT_ID_RE = /^[A-Za-z][A-Za-z0-9._-]{0,127}$/;\n\n/**\n * How much of the sha256 a `Based-On:` line carries — the length\n * `sources_digest` already uses (core/provenance/stamp.ts), for one reason a reader\n * can hold: every digest loam writes into a document reads the same.\n */\nexport const REQUIREMENT_DIGEST_LENGTH = 16;\n\n/** The value shape of a `Based-On:` line, exactly as `requirementDigest` writes it. */\nexport const REQUIREMENT_DIGEST_RE = new RegExp(`^[0-9a-f]{${REQUIREMENT_DIGEST_LENGTH}}$`);\n\nexport type RequirementIdProblem =\n | { kind: \"invalid\"; requirement: string; value: string }\n | { kind: \"repeated\"; requirement: string; values: string[] }\n | { kind: \"duplicate\"; id: string; requirements: string[] };\n\n/** Every authored Requirement-ID value in a requirement body, including blanks. */\nexport function requirementIdDeclarations(requirement: Requirement): string[] {\n return requirement.text.flatMap((line) => {\n const match = REQUIREMENT_ID_LINE_RE.exec(line);\n return match === null ? [] : [match[1]!.trim()];\n });\n}\n\n/** Structural ID problems within one spec/delta document. */\nexport function requirementIdProblems(reqs: Requirement[]): RequirementIdProblem[] {\n const problems: RequirementIdProblem[] = [];\n const owners = new Map<string, string[]>();\n for (const requirement of reqs) {\n const declarations = requirementIdDeclarations(requirement);\n const values = declarations.length > 0\n ? declarations\n : requirement.id === undefined\n ? []\n : [requirement.id];\n if (declarations.length > 1) {\n problems.push({ kind: \"repeated\", requirement: requirement.name, values: declarations });\n }\n for (const value of values) {\n if (!REQUIREMENT_ID_RE.test(value)) {\n problems.push({ kind: \"invalid\", requirement: requirement.name, value });\n }\n }\n // A repeated declaration is already an error. Do not also pretend its\n // keep-last parse result is an unambiguous document identity.\n if (values.length !== 1 || !REQUIREMENT_ID_RE.test(values[0]!)) continue;\n const names = owners.get(values[0]!) ?? [];\n names.push(requirement.name);\n owners.set(values[0]!, names);\n }\n for (const [id, requirements] of owners) {\n if (requirements.length > 1) problems.push({ kind: \"duplicate\", id, requirements });\n }\n return problems;\n}\n\n/** Every authored `Based-On:` value in a requirement body, including blanks. */\nexport function basedOnDeclarations(requirement: Requirement): string[] {\n return requirement.text.flatMap((line) => {\n const match = BASED_ON_LINE_RE.exec(line);\n return match === null ? [] : [match[1]!.trim()];\n });\n}\n\n/**\n * The requirement without its baseline marker — what a digest is taken over,\n * and what archive merges into the living document.\n *\n * `Based-On:` is a statement ABOUT a delta (\"this was written against that\"),\n * never part of the requirement it describes, and both consequences of\n * forgetting that are bad. Left in the digest input, a requirement's digest\n * would depend on the pin pointing at it, so no baseline could ever be\n * self-consistent. Left in the merged text — `applyRequirementDelta` copies the\n * delta's body into living wholesale — the living document would grow a pin to\n * a version of itself, and the NEXT feature's baseline would be taken over a\n * document containing the previous feature's bookkeeping.\n */\nexport function withoutBaseline(requirement: Requirement): Requirement {\n const { basedOn: _dropped, ...rest } = requirement;\n return { ...rest, text: requirement.text.filter((line) => !BASED_ON_LINE_RE.test(line)) };\n}\n\n/**\n * The requirement with every `Realizes:` pin stripped back to its bare target —\n * the second thing a digest is taken over, for the same reason as the first.\n *\n * `withoutBaseline`'s argument, one axis over: a pin is a statement ABOUT a\n * join (\"this claim was written against that version of the promise\"), never\n * part of the requirement making the claim. Left in the digest input it would\n * make re-pinning a content change, so every requirement realizing a capability\n * would go stale against ITS OWN consumers the moment the capability moved —\n * one edit cascading through the corpus, which is the failure the pin exists to\n * report rather than to cause.\n *\n * The line is kept and only the suffix removed, unlike `withoutBaseline` which\n * drops the whole line: the target ids ARE content, and a requirement that\n * stops naming the promise it keeps is a different requirement.\n */\nexport function withoutRealizesPins(requirement: Requirement): Requirement {\n return {\n ...requirement,\n text: requirement.text.map((line) => {\n const match = REALIZES_LINE_RE.exec(line);\n if (match === null) return line;\n const bare = match[1]!\n .split(\",\")\n .map((entry) => splitRealizesPin(entry.trim()).target)\n .join(\", \");\n return line.replace(match[1]!, bare);\n }),\n };\n}\n\n/**\n * The identity of a requirement's CONTENT: sha256 over its canonical\n * serialization, truncated like every other digest loam stamps.\n *\n * Canonical, not raw bytes, and deliberately: a living spec is rewritten by\n * `serializeRequirements` on every archive, so hashing the file's bytes would\n * make a baseline go stale over reflowed blank lines — a false alarm on the one\n * check whose whole value is that it only fires when something real moved.\n * What the hash therefore covers is exactly what survives a round trip: the\n * heading, the body (`Requirement-ID:` and `Operations:`/`Covers:` lines\n * included), and every scenario with its Given/When/Then lines. `kind` and\n * `section` are not serialized and so are not hashed — a requirement does not\n * change because the document quoting it moved it under another heading.\n *\n * The two normalizations are the two kinds of bookkeeping a requirement can\n * carry about its own joins — the baseline pointing AT it, and the `Realizes:`\n * pins pointing OUT of it. Both are stripped for the same reason: a digest that\n * moved when bookkeeping moved would make every pin self-invalidating.\n */\nexport function requirementDigest(requirement: Requirement): string {\n return createHash(\"sha256\")\n .update(serializeRequirements([withoutRealizesPins(withoutBaseline(requirement))]), \"utf8\")\n .digest(\"hex\")\n .slice(0, REQUIREMENT_DIGEST_LENGTH);\n}\n\n/** Serialize requirements back to OpenSpec markdown (`### Requirement:` + `#### Scenario:`). */\nexport function serializeRequirements(reqs: Requirement[]): string {\n // Framing (blank lines between sections) is normalized here; body content is\n // only edge-trimmed, never collapsed — blank lines inside a scenario (e.g. in\n // fenced code blocks) are verbatim content.\n const chunks: string[] = [];\n for (const r of reqs) {\n const chunk: string[] = [`### Requirement: ${r.name}`];\n const body = [...r.text];\n // Parsed documents already carry the authored line in `text`. This branch\n // also makes programmatically constructed Requirements serialize fully.\n if (r.id !== undefined && !body.some((line) => REQUIREMENT_ID_LINE_RE.test(line))) {\n body.unshift(`Requirement-ID: ${r.id}`);\n }\n // Directly under the identity it pins, which is where `loam rebase` writes\n // it too — the two lines are read together, so they are never apart.\n if (r.basedOn !== undefined && !body.some((line) => BASED_ON_LINE_RE.test(line))) {\n body.splice(body.findIndex((line) => REQUIREMENT_ID_LINE_RE.test(line)) + 1, 0, `Based-On: ${r.basedOn}`);\n }\n const text = body.join(\"\\n\").trim();\n if (text) chunk.push(\"\", text);\n for (const s of r.scenarios) {\n chunk.push(\"\", `#### Scenario: ${s.name}`);\n const body = s.lines.join(\"\\n\").trim();\n if (body) chunk.push(body);\n }\n chunks.push(chunk.join(\"\\n\"));\n }\n return chunks.join(\"\\n\\n\").trim() + \"\\n\";\n}\n\n/**\n * A delta requirement as it lands in the living document, with the bookkeeping\n * that belongs to the DELTA left behind: the `Based-On:` pin (a claim about\n * which living version this was written against — meaningless once it IS the\n * living version, and poison for the next feature's baseline, which would then\n * hash the previous feature's pin) and the line it was parsed from.\n */\n"]}
|
|
1
|
+
{"version":3,"file":"spec.js","sourceRoot":"","sources":["../../../src/core/document/spec.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * What a requirement IS, what makes one the same requirement twice, and the one\n * canonical way it is written back out.\n *\n * Living specs use `## Requirements` with `### Requirement:` + `#### Scenario:`.\n * Feature deltas group requirements under `## ADDED|MODIFIED|REMOVED\n * Requirements`. Scenarios are the acceptance criteria (Given/When/Then) and\n * the source for tests.\n *\n * Identity and serialization cannot be separated here: `requirementDigest` is\n * defined AS the sha256 of the serialized requirement without its baseline, so\n * a second module owning the writer would mean the digest and the bytes it\n * claims to describe could disagree. `./parse.ts` turns markdown into these,\n * `./apply.ts` folds a delta into a living set of them.\n */\nimport { createHash } from \"node:crypto\";\n\nexport type DeltaKind = \"ADDED\" | \"MODIFIED\" | \"REMOVED\" | \"BASE\";\n\nexport interface Scenario {\n name: string;\n lines: string[];\n /**\n * 1-based line of this scenario's `#### Scenario:` heading in its source document\n * — `Requirement.line` one level down, so body line `lines[i]` sits at `line + 1 + i`.\n *\n * NOT part of identity, and that is load-bearing: `serializeRequirements` writes a\n * scenario from `name` and `lines` alone and `requirementDigest` is the hash OF that\n * serialization, so recording a position moves no digest and no living `Based-On:`\n * pin goes stale because the parser learned to count lines.\n */\n line?: number;\n}\n\nexport interface Requirement {\n /** BASE for a living spec; ADDED/MODIFIED/REMOVED inside a feature delta. */\n kind: DeltaKind;\n /**\n * Stable identity from a `Requirement-ID:` body line. Optional so existing\n * OpenSpec-compatible documents continue to use their heading as identity.\n */\n id?: string;\n /**\n * The living requirement this delta was WRITTEN AGAINST, from a `Based-On:`\n * body line — `requirementDigest` of that requirement at authoring time.\n *\n * Delta-only, and meaningful on MODIFIED/REMOVED alone: those two address a\n * requirement that already exists, and a MODIFIED carries its full new text,\n * so archive does not merge the living wording — it REPLACES it. Two features\n * rewriting one requirement therefore lose the loser's text outright, and the\n * in-flight warning (`delta.modified-conflict`) cannot see the case that\n * actually happens: once the first archives it leaves `changes/`, the second\n * goes green again, and its stale text lands on top without a word. This is\n * the pin that closes that window — the same trick as `sources_digest`\n * (core/provenance/stamp.ts), one requirement wide.\n *\n * Absent on requirements adopted from OpenSpec, which never had the line;\n * those keep the older, weaker protection and are told so once (warn).\n */\n basedOn?: string;\n name: string;\n text: string[];\n /** OpenAPI operationIds this requirement governs, from an `Operations:` line. */\n operations: string[];\n /**\n * What this requirement's scenarios exercise, from a `Covers:` line — the\n * architecture analog of `Operations:`. Entries are C4 element ids, edges\n * (`source -> target`), or health signals (`alert:<id>` / `sli:<id>`);\n * core/c4/arch.ts owns the grammar and the resolution. Parsed everywhere for one\n * grammar's sake, meaningful in arch.spec.md.\n */\n covers: string[];\n /**\n * Authorization this requirement is gated on, from a `Requires:` line —\n * `<subject>/<permission>` entries declared in `architecture/permissions.yaml`.\n *\n * A third join beside `Operations:` and `Covers:`, and it exists because the\n * other two cannot carry it. A permission is not an operation (one operation\n * is gated by several, and the same permission gates several operations) and\n * it is not a C4 element (it is a domain fact, not a structural one). The\n * subject half is what the OpenAPI security model cannot express at all:\n * `security` names what the CALLER must hold, while a permission is as often\n * checked on an entity inside the request — the profile a message is sent\n * from, the org a resource belongs to.\n */\n requires: string[];\n /**\n * Fleet capabilities this requirement realizes part of, from a `Capability:`\n * (also accepted: `Capabilities:`) line — ids declared in\n * `architecture/capabilities.yaml`.\n *\n * A fourth join beside `Operations:`, `Covers:` and `Requires:`, and a LIST\n * because the relation is many-to-many in both directions: one requirement\n * commonly closes part of two capabilities, and a capability is realized by\n * many requirements across several services — a single-valued field would\n * force authors to pick a lie. Neither existing line can carry it: a\n * capability is not an operation (it is behaviour above any one endpoint)\n * and not a C4 element (it is a business fact, not a structural one).\n * The parse is additive exactly as `requires` is — the line rides in\n * `req.text` and therefore inside `requirementDigest`, so no living\n * document's digest moves merely because loam learned to read it.\n */\n capabilities: string[];\n /**\n * Capability REQUIREMENTS this requirement realizes part of, from a\n * `Realizes:` line — entries spelled `<capability-id>#<Requirement-ID>`,\n * resolving against `capabilities/<cap>/spec.md`.\n *\n * A fifth join, and the one that makes the business tree checkable rather\n * than merely present. `Capability:` beside it answers a DIFFERENT question\n * and neither replaces the other: `Capability: checkout` says this\n * requirement is part of that capability, which is a claim about theme;\n * `Realizes: checkout#CHECKOUT-CHARGE-ONCE` says it is part of what makes one\n * named promise true, which is a claim loam can grade in both directions —\n * an entry that resolves to nothing, and a capability requirement nothing\n * realizes.\n *\n * WHY A COMPOSITE ENTRY RATHER THAN TWO LINES. A `Requirement-ID` is only\n * unique inside its own document, so the capability half is not decoration —\n * it is what makes the target addressable at all. Two lines would let a\n * requirement name three capabilities and four ids with nothing saying which\n * belongs to which.\n *\n * WHY THE SEPARATOR IS THE LAST `#` AND NOT THE FIRST. The requirement half\n * has a strict grammar (`REQUIREMENT_ID_RE`) that excludes `#`, while the\n * capability half is a YAML key and a directory name and is not constrained\n * here at all. Splitting at the last `#` is therefore unambiguous for every\n * capability id there is; splitting at the first would mis-parse any id that\n * contained one, and would do it silently.\n *\n * Parsed additively exactly as `capabilities` is — the line rides in\n * `req.text` and therefore inside `requirementDigest`, so no living\n * document's digest moves merely because loam learned to read it.\n */\n realizes: string[];\n /**\n * AsyncAPI message names this requirement governs on the PRODUCING side, from\n * a `Publishes:` line — the event-axis analog of `Operations:`.\n *\n * Two lines rather than one, because the async spine is directional in a way\n * the HTTP one is not: `Publishes:` resolves against this service's OWN\n * contract, while `Consumes:` names a message another service declares, and\n * only a fleet-wide view can say whether anybody does. Grading them alike\n * would either excuse a typo in a producer or point a consumer's error at the\n * wrong repository.\n */\n publishes: string[];\n /** AsyncAPI message names this requirement consumes, from a `Consumes:` line. See `publishes`. */\n consumes: string[];\n scenarios: Scenario[];\n /**\n * The H2 heading of its SOURCE DOCUMENT this requirement was parsed under,\n * verbatim (`## Behavior`), or absent if it preceded every heading. Records where\n * the text came from, so it stays true after a merge re-homes the requirement.\n *\n * `kind` alone cannot explain why a requirement is BASE, and the two BASE cases\n * differ completely: under `## Requirements` a delta is legally quoting the\n * living state, while under `## Behavior` the author wrote a change that archive\n * will silently not merge. `delta.requirement-not-merged` tells them apart.\n */\n section?: string;\n /**\n * 1-based line of this requirement's `### Requirement:` heading in its source\n * document. Body line `text[i]` is therefore at line `line + 1 + i` — the\n * capture is contiguous from the heading to the first scenario — which is what\n * lets `loam rebase` rewrite one `Based-On:` line by surgery instead of\n * reserializing the document and flattening the author's sections and prose.\n */\n line?: number;\n}\n\nexport const KIND_RE = /^##\\s+(ADDED|MODIFIED|REMOVED)\\s+Requirements\\s*$/i;\nexport const REQUIREMENT_ID_LINE_RE = /^\\s*Requirement-ID:\\s*(.*?)\\s*$/i;\nexport const BASED_ON_LINE_RE = /^\\s*Based-On:\\s*(.*?)\\s*$/i;\n\n/**\n * The `Capability:` body line (also accepted: `Capabilities:`), exactly as the\n * parser reads it — `./parse.ts` consumes the capture, migrate-openspec's\n * materializer tests presence with it before appending its own line. One\n * spelling on purpose: two copies of a grammar is how a \"presence\" test and\n * the parse quietly stop agreeing about what counts as the line.\n */\nexport const CAPABILITY_LINE_RE = /^\\s*Capabilit(?:y|ies):\\s*(.+?)\\s*$/i;\n\n/**\n * The `Realizes:` body line. One spelling only — no `Realize:` singular\n * alternative, unlike `Operations?:` and the rest — because the verb already\n * reads correctly for one entry and for six, and the plural forms in the older\n * lines exist to forgive a NOUN that changes shape. A grammar with an\n * alternative nobody needs is one more thing two readers can disagree about.\n */\nexport const REALIZES_LINE_RE = /^\\s*Realizes:\\s*(.+?)\\s*$/i;\n\n/**\n * The separator between a `Realizes:` target and the digest of the capability\n * requirement it was written against — `checkout#CHK-1@9f2c1a4b`.\n *\n * `@` and not a second `#`, because the two halves are read by different rules:\n * `splitRealizesEntry` takes the LAST `#` to find the requirement half, so a\n * second one would move that boundary and re-parse every existing entry. `@`\n * is also excluded from `REQUIREMENT_ID_RE`, which is what makes the split\n * unambiguous rather than a convention — an id can never contain one.\n *\n * NOBODY TYPES THIS. `loam rebase --living` writes it and rewrites it; a human\n * writes `Realizes: checkout#CHK-1` and is never asked for more. An entry\n * without a pin is not a defect and never becomes one — it is a claim that has\n * not been pinned yet, and it grades exactly as it did before pins existed.\n */\nexport const REALIZES_PIN_SEPARATOR = \"@\";\n\n/**\n * One `Realizes:` entry split into the target it names and the digest it was\n * written against, or `pin: null` when it carries none.\n *\n * A suffix that is not a well-formed digest is NOT a pin and NOT a refusal:\n * the whole entry stays the target, so `checkout#CHK-1@nonsense` resolves — or\n * fails to resolve — exactly as it would have before, under\n * `capability.realizes-unknown`, whose message names the entry the author\n * actually typed. Inventing a second refusal for one mistake is the failure\n * `splitRealizesEntry` already refuses two lines up.\n */\nexport function splitRealizesPin(entry: string): { target: string; pin: string | null } {\n const at = entry.lastIndexOf(REALIZES_PIN_SEPARATOR);\n if (at <= 0) return { target: entry, pin: null };\n const suffix = entry.slice(at + 1);\n if (!REQUIREMENT_DIGEST_RE.test(suffix)) return { target: entry, pin: null };\n return { target: entry.slice(0, at), pin: suffix };\n}\n\n/** Portable, review-friendly stable IDs. Case-sensitive by design. */\nexport const REQUIREMENT_ID_RE = /^[A-Za-z][A-Za-z0-9._-]{0,127}$/;\n\n/**\n * How much of the sha256 a `Based-On:` line carries — the length\n * `sources_digest` already uses (core/provenance/stamp.ts), for one reason a reader\n * can hold: every digest loam writes into a document reads the same.\n */\nexport const REQUIREMENT_DIGEST_LENGTH = 16;\n\n/** The value shape of a `Based-On:` line, exactly as `requirementDigest` writes it. */\nexport const REQUIREMENT_DIGEST_RE = new RegExp(`^[0-9a-f]{${REQUIREMENT_DIGEST_LENGTH}}$`);\n\nexport type RequirementIdProblem =\n | { kind: \"invalid\"; requirement: string; value: string }\n | { kind: \"repeated\"; requirement: string; values: string[] }\n | { kind: \"duplicate\"; id: string; requirements: string[] };\n\n/** Every authored Requirement-ID value in a requirement body, including blanks. */\nexport function requirementIdDeclarations(requirement: Requirement): string[] {\n return requirement.text.flatMap((line) => {\n const match = REQUIREMENT_ID_LINE_RE.exec(line);\n return match === null ? [] : [match[1]!.trim()];\n });\n}\n\n/** Structural ID problems within one spec/delta document. */\nexport function requirementIdProblems(reqs: Requirement[]): RequirementIdProblem[] {\n const problems: RequirementIdProblem[] = [];\n const owners = new Map<string, string[]>();\n for (const requirement of reqs) {\n const declarations = requirementIdDeclarations(requirement);\n const values = declarations.length > 0\n ? declarations\n : requirement.id === undefined\n ? []\n : [requirement.id];\n if (declarations.length > 1) {\n problems.push({ kind: \"repeated\", requirement: requirement.name, values: declarations });\n }\n for (const value of values) {\n if (!REQUIREMENT_ID_RE.test(value)) {\n problems.push({ kind: \"invalid\", requirement: requirement.name, value });\n }\n }\n // A repeated declaration is already an error. Do not also pretend its\n // keep-last parse result is an unambiguous document identity.\n if (values.length !== 1 || !REQUIREMENT_ID_RE.test(values[0]!)) continue;\n const names = owners.get(values[0]!) ?? [];\n names.push(requirement.name);\n owners.set(values[0]!, names);\n }\n for (const [id, requirements] of owners) {\n if (requirements.length > 1) problems.push({ kind: \"duplicate\", id, requirements });\n }\n return problems;\n}\n\n/** Every authored `Based-On:` value in a requirement body, including blanks. */\nexport function basedOnDeclarations(requirement: Requirement): string[] {\n return requirement.text.flatMap((line) => {\n const match = BASED_ON_LINE_RE.exec(line);\n return match === null ? [] : [match[1]!.trim()];\n });\n}\n\n/**\n * The requirement without its baseline marker — what a digest is taken over,\n * and what archive merges into the living document.\n *\n * `Based-On:` is a statement ABOUT a delta (\"this was written against that\"),\n * never part of the requirement it describes, and both consequences of\n * forgetting that are bad. Left in the digest input, a requirement's digest\n * would depend on the pin pointing at it, so no baseline could ever be\n * self-consistent. Left in the merged text — `applyRequirementDelta` copies the\n * delta's body into living wholesale — the living document would grow a pin to\n * a version of itself, and the NEXT feature's baseline would be taken over a\n * document containing the previous feature's bookkeeping.\n */\nexport function withoutBaseline(requirement: Requirement): Requirement {\n const { basedOn: _dropped, ...rest } = requirement;\n return { ...rest, text: requirement.text.filter((line) => !BASED_ON_LINE_RE.test(line)) };\n}\n\n/**\n * The requirement with every `Realizes:` pin stripped back to its bare target —\n * the second thing a digest is taken over, for the same reason as the first.\n *\n * `withoutBaseline`'s argument, one axis over: a pin is a statement ABOUT a\n * join (\"this claim was written against that version of the promise\"), never\n * part of the requirement making the claim. Left in the digest input it would\n * make re-pinning a content change, so every requirement realizing a capability\n * would go stale against ITS OWN consumers the moment the capability moved —\n * one edit cascading through the corpus, which is the failure the pin exists to\n * report rather than to cause.\n *\n * The line is kept and only the suffix removed, unlike `withoutBaseline` which\n * drops the whole line: the target ids ARE content, and a requirement that\n * stops naming the promise it keeps is a different requirement.\n */\nexport function withoutRealizesPins(requirement: Requirement): Requirement {\n return {\n ...requirement,\n text: requirement.text.map((line) => {\n const match = REALIZES_LINE_RE.exec(line);\n if (match === null) return line;\n const bare = match[1]!\n .split(\",\")\n .map((entry) => splitRealizesPin(entry.trim()).target)\n .join(\", \");\n return line.replace(match[1]!, bare);\n }),\n };\n}\n\n/**\n * The identity of a requirement's CONTENT: sha256 over its canonical\n * serialization, truncated like every other digest loam stamps.\n *\n * Canonical, not raw bytes, and deliberately: archive writes every requirement\n * a delta adds or replaces through `serializeRequirements`, so hashing bytes\n * would make a baseline go stale over reflowed blank lines — a false alarm on\n * the one check whose whole value is that it only fires when something moved.\n * What the hash therefore covers is exactly what survives a round trip: the\n * heading, the body (`Requirement-ID:` and `Operations:`/`Covers:` lines\n * included), and every scenario with its Given/When/Then lines. `kind` and\n * `section` are not serialized and so are not hashed — a requirement does not\n * change because the document quoting it moved it under another heading.\n *\n * The two normalizations are the two kinds of bookkeeping a requirement can\n * carry about its own joins — the baseline pointing AT it, and the `Realizes:`\n * pins pointing OUT of it. Both are stripped for the same reason: a digest that\n * moved when bookkeeping moved would make every pin self-invalidating.\n */\nexport function requirementDigest(requirement: Requirement): string {\n return createHash(\"sha256\")\n .update(serializeRequirements([withoutRealizesPins(withoutBaseline(requirement))]), \"utf8\")\n .digest(\"hex\")\n .slice(0, REQUIREMENT_DIGEST_LENGTH);\n}\n\n/** Serialize requirements back to OpenSpec markdown (`### Requirement:` + `#### Scenario:`). */\nexport function serializeRequirements(reqs: Requirement[]): string {\n // Framing (blank lines between sections) is normalized here; body content is\n // only edge-trimmed, never collapsed — blank lines inside a scenario (e.g. in\n // fenced code blocks) are verbatim content.\n const chunks: string[] = [];\n for (const r of reqs) {\n const chunk: string[] = [`### Requirement: ${r.name}`];\n const body = [...r.text];\n // Parsed documents already carry the authored line in `text`. This branch\n // also makes programmatically constructed Requirements serialize fully.\n if (r.id !== undefined && !body.some((line) => REQUIREMENT_ID_LINE_RE.test(line))) {\n body.unshift(`Requirement-ID: ${r.id}`);\n }\n // Directly under the identity it pins, which is where `loam rebase` writes\n // it too — the two lines are read together, so they are never apart.\n if (r.basedOn !== undefined && !body.some((line) => BASED_ON_LINE_RE.test(line))) {\n body.splice(body.findIndex((line) => REQUIREMENT_ID_LINE_RE.test(line)) + 1, 0, `Based-On: ${r.basedOn}`);\n }\n const text = body.join(\"\\n\").trim();\n if (text) chunk.push(\"\", text);\n for (const s of r.scenarios) {\n chunk.push(\"\", `#### Scenario: ${s.name}`);\n const body = s.lines.join(\"\\n\").trim();\n if (body) chunk.push(body);\n }\n chunks.push(chunk.join(\"\\n\"));\n }\n return chunks.join(\"\\n\\n\").trim() + \"\\n\";\n}\n"]}
|
|
@@ -46,6 +46,16 @@ function searchDirs(start) {
|
|
|
46
46
|
current = parent;
|
|
47
47
|
}
|
|
48
48
|
}
|
|
49
|
+
/**
|
|
50
|
+
* The files loam wrote into the repository this config governs and owns there,
|
|
51
|
+
* relative to its root: loam.json itself and every agent file `agentFiles`
|
|
52
|
+
* records. `sources.unwalked` leaves them out of its denominator — a check
|
|
53
|
+
* that counted loam's own bookkeeping as a part of the service nobody read
|
|
54
|
+
* fired in every service repo the moment `loam init` had run.
|
|
55
|
+
*/
|
|
56
|
+
export function loamOwnedFiles(config) {
|
|
57
|
+
return [CONFIG_FILENAME, ...Object.keys(config.agentFiles ?? {})];
|
|
58
|
+
}
|
|
49
59
|
/** Where a config written HERE goes — never a discovered one. `init` and `saveConfig` use this. */
|
|
50
60
|
export function localConfigPath(cwd = process.cwd()) {
|
|
51
61
|
return resolve(cwd, CONFIG_FILENAME);
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"config.js","sourceRoot":"","sources":["../../../src/core/envelope/config.ts"],"names":[],"mappings":"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","sourcesContent":["import { readFile, writeFile } from \"node:fs/promises\";\nimport { existsSync } from \"node:fs\";\nimport { dirname, join, parse as parsePath, resolve } from \"node:path\";\nimport { resolveInside } from \"../kernel/path-safety.js\";\nimport { docsDirOf, type DocsDir } from \"../kernel/ids/dirs.js\";\nimport { parseServiceId, type ServiceId } from \"../kernel/ids/service.js\";\n\n/** Local config, committed at the root of a service repo (or the docs repo itself). */\nexport const CONFIG_FILENAME = \"loam.json\";\n\nexport interface LoamConfig {\n /**\n * Path to the single shared docs repo (the source of truth). A LOADED config\n * carries it resolved — `parseConfig` resolves the file's spelling against\n * the config file's own directory, which is what earns the brand. The\n * spelling itself is `docsDirAsWritten`, and the shape `saveConfig` persists\n * (`StoredConfig`) keeps it a plain `string`: the stored form is deliberately\n * unresolved (see `storedDocsDir` in commands/init/options.ts), and branding\n * it would state a resolution that never happened.\n */\n docsDir: DocsDir;\n /**\n * Canonical id of the service in the current repo, if this is a service repo.\n *\n * Branded because `parseConfig` below is the one place that checks it, so\n * every command reading `config.service` is reading a name that has already\n * passed the grammar — which is why the config arm of\n * `opts.service ?? config.service` needs no second parse.\n */\n service?: ServiceId;\n /**\n * Where Gherkin lives in this service repo, relative to the repo root\n * (default \"features\" — the cucumber convention). `loam gherkin` writes only\n * inside `<gherkinDir>/loam/`; the rest of the directory stays the team's.\n * Left unresolved on purpose: commands resolve it against the repo they run\n * in, which is where the config file lives.\n */\n gherkinDir?: string;\n /**\n * Where this repo's BUILD writes its contract documents, relative to the\n * repo root — the springdoc/FastAPI/NestJS output, not a second copy anybody\n * maintains.\n *\n * loam reads these and compares a digest; it never writes them, never copies\n * them into the docs repo, and never derives meaning from them. That is what\n * keeps the field on the right side of the no-extractor line: it parses a\n * standard document somebody else generated, at a path a human named, in\n * exactly the way `verify --results` already ingests a cucumber report.\n *\n * The committed `services/<id>/openapi.yaml` stays the contract, and the copy\n * stays a human `cp` reviewed in a pull request. Automating the copy would\n * make the docs-repo document a cache of the build; leaving it manual and\n * merely REPORTING the divergence keeps it a document somebody agreed to.\n */\n contracts?: { openapi?: string };\n /**\n * The agent tools `loam init` has written command and skill files for in this\n * repo, by AGENT_TOOLS id.\n *\n * Recorded because the two ways a generated file can be absent look identical\n * on disk: the binary grew a command nobody has re-run `init` for, or nobody\n * ever selected that tool here. Only this list tells them apart, and `loam\n * doctor` needs the difference to know which absence is worth reporting.\n *\n * ACCUMULATED, never replaced — a later `init --tools cursor` does not\n * un-write the claude files an earlier run left on disk, so the record of\n * what this repo holds has to keep them too.\n *\n * Optional, and unvalidated against the registry on purpose: a config written\n * by an older binary has no list at all, and one written by a NEWER binary\n * may name a tool this one has never heard of. Neither may refuse to load.\n */\n agentTools?: string[];\n /**\n * Digests of generated agent files loam may safely refresh. Keys are\n * repo-relative portable paths; a file whose bytes differ from its recorded\n * digest is human-owned and is never overwritten.\n */\n agentFiles?: Record<string, string>;\n /** Which workflow subset `loam init` manages; absent means the compatible full set. */\n agentProfile?: \"full\" | \"service\" | \"docs\";\n /**\n * The directory the config was actually FOUND in — the repo root, which is\n * not necessarily the cwd now that discovery walks upward. Everything a\n * command resolves against \"this repo\" (gherkinDir, relative `sources:`)\n * belongs here, not against process.cwd(): running `loam gherkin` from\n * `src/deep/` must write the same files as running it from the root.\n *\n * Derived, never stored: `saveConfig` strips it, so it can never be spelled\n * in loam.json and drift from where the file really is.\n */\n root?: string;\n /**\n * `docsDir` exactly as the file spells it, before resolution. Kept because\n * \"relative\" and \"absolute\" are not recoverable from the resolved path, and\n * the difference is the whole point: an absolute docsDir in a COMMITTED\n * config is a path that only exists on the machine that ran `loam init`.\n *\n * Derived, never stored — same contract as `root`.\n */\n docsDirAsWritten?: string;\n}\n\n/**\n * A loam.json that exists but cannot be believed. Carries the field at fault\n * (null when the whole document is), because \"which field\" is the difference\n * between a fix and a guess — and because both `loadConfig` and `loam doctor`\n * report the same sentence from it.\n */\nexport class ConfigError extends Error {\n constructor(\n readonly field: string | null,\n message: string,\n ) {\n super(message);\n this.name = \"ConfigError\";\n }\n}\n\n/**\n * The directories a config search may look in: `start`, then each ancestor, up\n * to AND INCLUDING the git root — or the filesystem root when there is no git\n * repo above.\n *\n * The git root is the stop because it is the boundary the user already thinks\n * in: a loam.json outside it belongs to some other checkout (or, worse, to a\n * home directory), and silently adopting it would point a service repo's\n * commands at a docs repo nobody in this repo ever chose.\n */\nfunction searchDirs(start: string): string[] {\n const out: string[] = [];\n let current = resolve(start);\n const root = parsePath(current).root;\n for (;;) {\n out.push(current);\n // `.git` is a directory in a normal clone and a FILE in a worktree or\n // submodule — existsSync answers both without caring which.\n if (existsSync(join(current, \".git\"))) return out;\n const parent = dirname(current);\n if (parent === current || current === root) return out;\n current = parent;\n }\n}\n\n/** Where a config written HERE goes — never a discovered one. `init` and `saveConfig` use this. */\nexport function localConfigPath(cwd: string = process.cwd()): string {\n return resolve(cwd, CONFIG_FILENAME);\n}\n\n/**\n * The nearest loam.json at or above `cwd`, or null. Exported because `init` has\n * to be able to ask a different question from \"is there one here\": a config in\n * an ancestor already governs this directory, and a second one written below it\n * would quietly shadow the first for every command run from the subtree.\n */\nexport function findConfigPath(cwd: string = process.cwd()): string | null {\n for (const dir of searchDirs(cwd)) {\n const candidate = join(dir, CONFIG_FILENAME);\n if (existsSync(candidate)) return candidate;\n }\n return null;\n}\n\n/**\n * The config that governs `cwd`: the discovered one, or — when there is none —\n * where it would have to be written. The fallback keeps this total, which is\n * what lets a caller distinguish \"absent\" from \"present but unloadable\" with a\n * single existsSync (see `reportNoConfig`).\n */\nexport function configPath(cwd: string = process.cwd()): string {\n return findConfigPath(cwd) ?? localConfigPath(cwd);\n}\n\n/**\n * THE config validator. One implementation, because two of them are two\n * opinions: `loam doctor` used to carry its own copy, and a config doctor\n * called healthy could still be one every other command refused to load.\n *\n * `configDir` is the directory the file lives in — every relative path in the\n * document is resolved against it, so the doc comments above stay true no\n * matter where the caller happens to be standing.\n */\nexport function parseConfig(raw: string, configDir: string): LoamConfig {\n const file = join(configDir, CONFIG_FILENAME);\n let value: unknown;\n try {\n value = JSON.parse(raw);\n } catch (err) {\n throw new ConfigError(null, `${file} is not valid JSON: ${err instanceof Error ? err.message : String(err)}`);\n }\n // Checked before any field access: `JSON.parse(\"null\")` and `JSON.parse(\"5\")`\n // are valid JSON, and reaching for `.docsDir` on them is how a config typo\n // used to surface as a raw Node TypeError instead of a named field.\n if (value === null || typeof value !== \"object\" || Array.isArray(value)) {\n throw new ConfigError(null, `${file} must contain a JSON object.`);\n }\n const record = value as Record<string, unknown>;\n\n if (typeof record.docsDir !== \"string\" || record.docsDir === \"\") {\n throw new ConfigError(\"docsDir\", `${file}: \"docsDir\" must be a non-empty string.`);\n }\n // Same discipline as docsDir: a malformed fact refuses the whole config\n // rather than being silently defaulted over — `5` or `\"\"` here is a typo,\n // and defaulting would send generated files somewhere nobody chose.\n // `parseServiceId` rather than a bare check: the same call both refuses the\n // malformed value and produces the branded one below, so there is no window\n // in which a checked name is still typed as an unchecked string. It takes a\n // `string`, hence the shape guard first — the id grammar's own message is\n // about spelling, and `5` is not a spelling.\n let service: ServiceId | undefined;\n if (record.service !== undefined) {\n if (typeof record.service !== \"string\") {\n throw new ConfigError(\"service\", `${file}: \"service\" must be a string.`);\n }\n const parsed = parseServiceId(record.service, '\"service\"');\n if (!parsed.ok) throw new ConfigError(\"service\", `${file}: ${parsed.problem}`);\n service = parsed.id;\n }\n if (record.gherkinDir !== undefined\n && (typeof record.gherkinDir !== \"string\" || record.gherkinDir === \"\")) {\n throw new ConfigError(\"gherkinDir\", `${file}: \"gherkinDir\" must be a non-empty string when present.`);\n }\n const contracts = readContracts(record.contracts, file);\n // Shape only. Which ids are legal is the registry's question, and asking it\n // here would make a config written by a newer binary — one that knows a tool\n // this one does not — unloadable for every command, not just for `init`.\n if (record.agentTools !== undefined\n && (!Array.isArray(record.agentTools)\n || record.agentTools.some((t) => typeof t !== \"string\" || t === \"\"))) {\n throw new ConfigError(\n \"agentTools\",\n `${file}: \"agentTools\" must be an array of non-empty strings when present.`,\n );\n }\n if (record.agentFiles !== undefined) {\n if (record.agentFiles === null || typeof record.agentFiles !== \"object\" || Array.isArray(record.agentFiles)) {\n throw new ConfigError(\"agentFiles\", `${file}: \"agentFiles\" must be a path-to-digest mapping when present.`);\n }\n const entries = Object.entries(record.agentFiles as Record<string, unknown>);\n if (entries.some(([path, digest]) => path === \"\" || typeof digest !== \"string\" || !/^[a-f0-9]{64}$/.test(digest))) {\n throw new ConfigError(\n \"agentFiles\",\n `${file}: \"agentFiles\" must map non-empty paths to lowercase sha256 digests.`,\n );\n }\n }\n if (record.agentProfile !== undefined\n && record.agentProfile !== \"full\"\n && record.agentProfile !== \"service\"\n && record.agentProfile !== \"docs\") {\n throw new ConfigError(\n \"agentProfile\",\n `${file}: \"agentProfile\" must be one of: full, service, docs.`,\n );\n }\n if (typeof record.gherkinDir === \"string\") {\n // Validate the owned output directory, not merely its spelling. This also\n // catches an otherwise-contained path whose existing parent is a symlink\n // out of the service repo.\n try {\n resolveInside(configDir, join(record.gherkinDir, \"loam\"), `\"gherkinDir\"`);\n } catch (err) {\n throw new ConfigError(\"gherkinDir\", `${file}: ${err instanceof Error ? err.message : String(err)}`);\n }\n }\n\n return {\n // Resolved here, against the file's own directory, so the doc comment on\n // `docsDir` is true no matter where a caller later resolves the path from —\n // which is exactly the provenance `docsDirOf` demands.\n docsDir: docsDirOf(resolve(configDir, record.docsDir)),\n ...(service === undefined ? {} : { service }),\n ...(record.gherkinDir === undefined ? {} : { gherkinDir: record.gherkinDir as string }),\n ...(contracts === undefined ? {} : { contracts }),\n ...(record.agentTools === undefined ? {} : { agentTools: record.agentTools as string[] }),\n ...(record.agentFiles === undefined\n ? {}\n : { agentFiles: record.agentFiles as Record<string, string> }),\n ...(record.agentProfile === undefined\n ? {}\n : { agentProfile: record.agentProfile as \"full\" | \"service\" | \"docs\" }),\n root: resolve(configDir),\n docsDirAsWritten: record.docsDir,\n };\n}\n\n/**\n * Every way asking for the config can come out, as data. It used to be\n * `LoamConfig | null` with a `console.error` on the invalid arm — core's one\n * print outside the envelope adapter, and a null that collapsed \"no config\"\n * and \"a config nobody can read\" into one answer `reportNoConfig` then had to\n * re-derive from the filesystem (a TOCTOU: the file could change between the\n * load and the re-check). The union says which case it is, carries the parse\n * problem for the command layer to render, and prints nothing.\n */\nexport type ConfigLoad =\n | { kind: \"loaded\"; config: LoamConfig }\n | { kind: \"absent\" }\n | { kind: \"invalid\"; path: string; problem: string };\n\nexport async function loadConfig(cwd: string = process.cwd()): Promise<ConfigLoad> {\n const p = findConfigPath(cwd);\n if (p === null) return { kind: \"absent\" };\n try {\n // The read belongs INSIDE the try: `existsSync` only says the name is\n // taken, not that it names a file this process can read. A loam.json that\n // is a directory (EISDIR), one whose permissions refuse us (EACCES), or one\n // deleted between the search and the read (ENOENT) all fail here rather\n // than in `parseConfig` — and read outside, each of them escaped every\n // command as an `internal` crash carrying a bare errno. Caught here they\n // join the same \"exists but cannot be believed\" arm the parse failures take.\n return { kind: \"loaded\", config: parseConfig(await readFile(p, \"utf8\"), dirname(p)) };\n } catch (err) {\n return { kind: \"invalid\", path: p, problem: err instanceof Error ? err.message : String(err) };\n }\n}\n\n/**\n * What `saveConfig` persists: `LoamConfig` with `docsDir` back to a plain\n * `string`, because the stored form is the SPELLING (deliberately relative —\n * `storedDocsDir`), not the resolved `DocsDir` a loaded config carries. The\n * two derived fields ride along as optional so a caller may spread a loaded\n * config forward — `saveConfig` strips them on the way out either way.\n */\nexport type StoredConfig = Omit<LoamConfig, \"docsDir\"> & { docsDir: string };\n\n/**\n * Write the config for the repo rooted at `cwd`. The derived fields are dropped\n * on the way out: they are facts about where the file was found and how it was\n * spelled, and a stored copy could only ever disagree with the file itself.\n */\nexport async function saveConfig(config: StoredConfig, cwd: string = process.cwd()): Promise<string> {\n const p = localConfigPath(cwd);\n const { root: _root, docsDirAsWritten: _asWritten, ...stored } = config;\n await writeFile(p, JSON.stringify(stored, null, 2) + \"\\n\", \"utf8\");\n return p;\n}\n\n/**\n * The optional `contracts` block, validated to shape only.\n *\n * SHAPE ONLY, exactly as `agentTools` is, and for a sharper version of the same\n * reason: containment is not checked here because a build output legitimately\n * sits outside the repo root's own subtree in some layouts, and the reader that\n * resolves it is where an escaping path must be refused — with the service in\n * hand, so the refusal can name it. A config that throws takes down every\n * command, including the ones with nothing to do with contracts.\n */\nfunction readContracts(value: unknown, file: string): { openapi?: string } | undefined {\n if (value === undefined) return undefined;\n if (typeof value !== \"object\" || value === null || Array.isArray(value)) {\n throw new ConfigError(\"contracts\", `${file}: \"contracts\" must be a mapping with an optional \"openapi\" path.`);\n }\n const record = value as Record<string, unknown>;\n // Only `openapi` today. The event axis gets a key when it gets a check —\n // a config key nothing reads is worse than a missing one, because a fleet\n // sets it and believes something is being graded.\n const out: { openapi?: string } = {};\n for (const key of [\"openapi\"] as const) {\n const spelled = record[key];\n if (spelled === undefined) continue;\n if (typeof spelled !== \"string\" || spelled === \"\") {\n throw new ConfigError(\"contracts\", `${file}: \"contracts.${key}\" must be a non-empty string when present.`);\n }\n out[key] = spelled;\n }\n return out.openapi === undefined ? undefined : out;\n}\n"]}
|
|
1
|
+
{"version":3,"file":"config.js","sourceRoot":"","sources":["../../../src/core/envelope/config.ts"],"names":[],"mappings":"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","sourcesContent":["import { readFile, writeFile } from \"node:fs/promises\";\nimport { existsSync } from \"node:fs\";\nimport { dirname, join, parse as parsePath, resolve } from \"node:path\";\nimport { resolveInside } from \"../kernel/path-safety.js\";\nimport { docsDirOf, type DocsDir } from \"../kernel/ids/dirs.js\";\nimport { parseServiceId, type ServiceId } from \"../kernel/ids/service.js\";\n\n/** Local config, committed at the root of a service repo (or the docs repo itself). */\nexport const CONFIG_FILENAME = \"loam.json\";\n\nexport interface LoamConfig {\n /**\n * Path to the single shared docs repo (the source of truth). A LOADED config\n * carries it resolved — `parseConfig` resolves the file's spelling against\n * the config file's own directory, which is what earns the brand. The\n * spelling itself is `docsDirAsWritten`, and the shape `saveConfig` persists\n * (`StoredConfig`) keeps it a plain `string`: the stored form is deliberately\n * unresolved (see `storedDocsDir` in commands/init/options.ts), and branding\n * it would state a resolution that never happened.\n */\n docsDir: DocsDir;\n /**\n * Canonical id of the service in the current repo, if this is a service repo.\n *\n * Branded because `parseConfig` below is the one place that checks it, so\n * every command reading `config.service` is reading a name that has already\n * passed the grammar — which is why the config arm of\n * `opts.service ?? config.service` needs no second parse.\n */\n service?: ServiceId;\n /**\n * Where Gherkin lives in this service repo, relative to the repo root\n * (default \"features\" — the cucumber convention). `loam gherkin` writes only\n * inside `<gherkinDir>/loam/`; the rest of the directory stays the team's.\n * Left unresolved on purpose: commands resolve it against the repo they run\n * in, which is where the config file lives.\n */\n gherkinDir?: string;\n /**\n * Where this repo's BUILD writes its contract documents, relative to the\n * repo root — the springdoc/FastAPI/NestJS output, not a second copy anybody\n * maintains.\n *\n * loam reads these and compares a digest; it never writes them, never copies\n * them into the docs repo, and never derives meaning from them. That is what\n * keeps the field on the right side of the no-extractor line: it parses a\n * standard document somebody else generated, at a path a human named, in\n * exactly the way `verify --results` already ingests a cucumber report.\n *\n * The committed `services/<id>/openapi.yaml` stays the contract, and the copy\n * stays a human `cp` reviewed in a pull request. Automating the copy would\n * make the docs-repo document a cache of the build; leaving it manual and\n * merely REPORTING the divergence keeps it a document somebody agreed to.\n */\n contracts?: { openapi?: string };\n /**\n * The agent tools `loam init` has written command and skill files for in this\n * repo, by AGENT_TOOLS id.\n *\n * Recorded because the two ways a generated file can be absent look identical\n * on disk: the binary grew a command nobody has re-run `init` for, or nobody\n * ever selected that tool here. Only this list tells them apart, and `loam\n * doctor` needs the difference to know which absence is worth reporting.\n *\n * ACCUMULATED, never replaced — a later `init --tools cursor` does not\n * un-write the claude files an earlier run left on disk, so the record of\n * what this repo holds has to keep them too.\n *\n * Optional, and unvalidated against the registry on purpose: a config written\n * by an older binary has no list at all, and one written by a NEWER binary\n * may name a tool this one has never heard of. Neither may refuse to load.\n */\n agentTools?: string[];\n /**\n * Digests of generated agent files loam may safely refresh. Keys are\n * repo-relative portable paths; a file whose bytes differ from its recorded\n * digest is human-owned and is never overwritten.\n */\n agentFiles?: Record<string, string>;\n /** Which workflow subset `loam init` manages; absent means the compatible full set. */\n agentProfile?: \"full\" | \"service\" | \"docs\";\n /**\n * The directory the config was actually FOUND in — the repo root, which is\n * not necessarily the cwd now that discovery walks upward. Everything a\n * command resolves against \"this repo\" (gherkinDir, relative `sources:`)\n * belongs here, not against process.cwd(): running `loam gherkin` from\n * `src/deep/` must write the same files as running it from the root.\n *\n * Derived, never stored: `saveConfig` strips it, so it can never be spelled\n * in loam.json and drift from where the file really is.\n */\n root?: string;\n /**\n * `docsDir` exactly as the file spells it, before resolution. Kept because\n * \"relative\" and \"absolute\" are not recoverable from the resolved path, and\n * the difference is the whole point: an absolute docsDir in a COMMITTED\n * config is a path that only exists on the machine that ran `loam init`.\n *\n * Derived, never stored — same contract as `root`.\n */\n docsDirAsWritten?: string;\n}\n\n/**\n * A loam.json that exists but cannot be believed. Carries the field at fault\n * (null when the whole document is), because \"which field\" is the difference\n * between a fix and a guess — and because both `loadConfig` and `loam doctor`\n * report the same sentence from it.\n */\nexport class ConfigError extends Error {\n constructor(\n readonly field: string | null,\n message: string,\n ) {\n super(message);\n this.name = \"ConfigError\";\n }\n}\n\n/**\n * The directories a config search may look in: `start`, then each ancestor, up\n * to AND INCLUDING the git root — or the filesystem root when there is no git\n * repo above.\n *\n * The git root is the stop because it is the boundary the user already thinks\n * in: a loam.json outside it belongs to some other checkout (or, worse, to a\n * home directory), and silently adopting it would point a service repo's\n * commands at a docs repo nobody in this repo ever chose.\n */\nfunction searchDirs(start: string): string[] {\n const out: string[] = [];\n let current = resolve(start);\n const root = parsePath(current).root;\n for (;;) {\n out.push(current);\n // `.git` is a directory in a normal clone and a FILE in a worktree or\n // submodule — existsSync answers both without caring which.\n if (existsSync(join(current, \".git\"))) return out;\n const parent = dirname(current);\n if (parent === current || current === root) return out;\n current = parent;\n }\n}\n\n/**\n * The files loam wrote into the repository this config governs and owns there,\n * relative to its root: loam.json itself and every agent file `agentFiles`\n * records. `sources.unwalked` leaves them out of its denominator — a check\n * that counted loam's own bookkeeping as a part of the service nobody read\n * fired in every service repo the moment `loam init` had run.\n */\nexport function loamOwnedFiles(config: LoamConfig): string[] {\n return [CONFIG_FILENAME, ...Object.keys(config.agentFiles ?? {})];\n}\n\n/** Where a config written HERE goes — never a discovered one. `init` and `saveConfig` use this. */\nexport function localConfigPath(cwd: string = process.cwd()): string {\n return resolve(cwd, CONFIG_FILENAME);\n}\n\n/**\n * The nearest loam.json at or above `cwd`, or null. Exported because `init` has\n * to be able to ask a different question from \"is there one here\": a config in\n * an ancestor already governs this directory, and a second one written below it\n * would quietly shadow the first for every command run from the subtree.\n */\nexport function findConfigPath(cwd: string = process.cwd()): string | null {\n for (const dir of searchDirs(cwd)) {\n const candidate = join(dir, CONFIG_FILENAME);\n if (existsSync(candidate)) return candidate;\n }\n return null;\n}\n\n/**\n * The config that governs `cwd`: the discovered one, or — when there is none —\n * where it would have to be written. The fallback keeps this total, which is\n * what lets a caller distinguish \"absent\" from \"present but unloadable\" with a\n * single existsSync (see `reportNoConfig`).\n */\nexport function configPath(cwd: string = process.cwd()): string {\n return findConfigPath(cwd) ?? localConfigPath(cwd);\n}\n\n/**\n * THE config validator. One implementation, because two of them are two\n * opinions: `loam doctor` used to carry its own copy, and a config doctor\n * called healthy could still be one every other command refused to load.\n *\n * `configDir` is the directory the file lives in — every relative path in the\n * document is resolved against it, so the doc comments above stay true no\n * matter where the caller happens to be standing.\n */\nexport function parseConfig(raw: string, configDir: string): LoamConfig {\n const file = join(configDir, CONFIG_FILENAME);\n let value: unknown;\n try {\n value = JSON.parse(raw);\n } catch (err) {\n throw new ConfigError(null, `${file} is not valid JSON: ${err instanceof Error ? err.message : String(err)}`);\n }\n // Checked before any field access: `JSON.parse(\"null\")` and `JSON.parse(\"5\")`\n // are valid JSON, and reaching for `.docsDir` on them is how a config typo\n // used to surface as a raw Node TypeError instead of a named field.\n if (value === null || typeof value !== \"object\" || Array.isArray(value)) {\n throw new ConfigError(null, `${file} must contain a JSON object.`);\n }\n const record = value as Record<string, unknown>;\n\n if (typeof record.docsDir !== \"string\" || record.docsDir === \"\") {\n throw new ConfigError(\"docsDir\", `${file}: \"docsDir\" must be a non-empty string.`);\n }\n // Same discipline as docsDir: a malformed fact refuses the whole config\n // rather than being silently defaulted over — `5` or `\"\"` here is a typo,\n // and defaulting would send generated files somewhere nobody chose.\n // `parseServiceId` rather than a bare check: the same call both refuses the\n // malformed value and produces the branded one below, so there is no window\n // in which a checked name is still typed as an unchecked string. It takes a\n // `string`, hence the shape guard first — the id grammar's own message is\n // about spelling, and `5` is not a spelling.\n let service: ServiceId | undefined;\n if (record.service !== undefined) {\n if (typeof record.service !== \"string\") {\n throw new ConfigError(\"service\", `${file}: \"service\" must be a string.`);\n }\n const parsed = parseServiceId(record.service, '\"service\"');\n if (!parsed.ok) throw new ConfigError(\"service\", `${file}: ${parsed.problem}`);\n service = parsed.id;\n }\n if (record.gherkinDir !== undefined\n && (typeof record.gherkinDir !== \"string\" || record.gherkinDir === \"\")) {\n throw new ConfigError(\"gherkinDir\", `${file}: \"gherkinDir\" must be a non-empty string when present.`);\n }\n const contracts = readContracts(record.contracts, file);\n // Shape only. Which ids are legal is the registry's question, and asking it\n // here would make a config written by a newer binary — one that knows a tool\n // this one does not — unloadable for every command, not just for `init`.\n if (record.agentTools !== undefined\n && (!Array.isArray(record.agentTools)\n || record.agentTools.some((t) => typeof t !== \"string\" || t === \"\"))) {\n throw new ConfigError(\n \"agentTools\",\n `${file}: \"agentTools\" must be an array of non-empty strings when present.`,\n );\n }\n if (record.agentFiles !== undefined) {\n if (record.agentFiles === null || typeof record.agentFiles !== \"object\" || Array.isArray(record.agentFiles)) {\n throw new ConfigError(\"agentFiles\", `${file}: \"agentFiles\" must be a path-to-digest mapping when present.`);\n }\n const entries = Object.entries(record.agentFiles as Record<string, unknown>);\n if (entries.some(([path, digest]) => path === \"\" || typeof digest !== \"string\" || !/^[a-f0-9]{64}$/.test(digest))) {\n throw new ConfigError(\n \"agentFiles\",\n `${file}: \"agentFiles\" must map non-empty paths to lowercase sha256 digests.`,\n );\n }\n }\n if (record.agentProfile !== undefined\n && record.agentProfile !== \"full\"\n && record.agentProfile !== \"service\"\n && record.agentProfile !== \"docs\") {\n throw new ConfigError(\n \"agentProfile\",\n `${file}: \"agentProfile\" must be one of: full, service, docs.`,\n );\n }\n if (typeof record.gherkinDir === \"string\") {\n // Validate the owned output directory, not merely its spelling. This also\n // catches an otherwise-contained path whose existing parent is a symlink\n // out of the service repo.\n try {\n resolveInside(configDir, join(record.gherkinDir, \"loam\"), `\"gherkinDir\"`);\n } catch (err) {\n throw new ConfigError(\"gherkinDir\", `${file}: ${err instanceof Error ? err.message : String(err)}`);\n }\n }\n\n return {\n // Resolved here, against the file's own directory, so the doc comment on\n // `docsDir` is true no matter where a caller later resolves the path from —\n // which is exactly the provenance `docsDirOf` demands.\n docsDir: docsDirOf(resolve(configDir, record.docsDir)),\n ...(service === undefined ? {} : { service }),\n ...(record.gherkinDir === undefined ? {} : { gherkinDir: record.gherkinDir as string }),\n ...(contracts === undefined ? {} : { contracts }),\n ...(record.agentTools === undefined ? {} : { agentTools: record.agentTools as string[] }),\n ...(record.agentFiles === undefined\n ? {}\n : { agentFiles: record.agentFiles as Record<string, string> }),\n ...(record.agentProfile === undefined\n ? {}\n : { agentProfile: record.agentProfile as \"full\" | \"service\" | \"docs\" }),\n root: resolve(configDir),\n docsDirAsWritten: record.docsDir,\n };\n}\n\n/**\n * Every way asking for the config can come out, as data. It used to be\n * `LoamConfig | null` with a `console.error` on the invalid arm — core's one\n * print outside the envelope adapter, and a null that collapsed \"no config\"\n * and \"a config nobody can read\" into one answer `reportNoConfig` then had to\n * re-derive from the filesystem (a TOCTOU: the file could change between the\n * load and the re-check). The union says which case it is, carries the parse\n * problem for the command layer to render, and prints nothing.\n */\nexport type ConfigLoad =\n | { kind: \"loaded\"; config: LoamConfig }\n | { kind: \"absent\" }\n | { kind: \"invalid\"; path: string; problem: string };\n\nexport async function loadConfig(cwd: string = process.cwd()): Promise<ConfigLoad> {\n const p = findConfigPath(cwd);\n if (p === null) return { kind: \"absent\" };\n try {\n // The read belongs INSIDE the try: `existsSync` only says the name is\n // taken, not that it names a file this process can read. A loam.json that\n // is a directory (EISDIR), one whose permissions refuse us (EACCES), or one\n // deleted between the search and the read (ENOENT) all fail here rather\n // than in `parseConfig` — and read outside, each of them escaped every\n // command as an `internal` crash carrying a bare errno. Caught here they\n // join the same \"exists but cannot be believed\" arm the parse failures take.\n return { kind: \"loaded\", config: parseConfig(await readFile(p, \"utf8\"), dirname(p)) };\n } catch (err) {\n return { kind: \"invalid\", path: p, problem: err instanceof Error ? err.message : String(err) };\n }\n}\n\n/**\n * What `saveConfig` persists: `LoamConfig` with `docsDir` back to a plain\n * `string`, because the stored form is the SPELLING (deliberately relative —\n * `storedDocsDir`), not the resolved `DocsDir` a loaded config carries. The\n * two derived fields ride along as optional so a caller may spread a loaded\n * config forward — `saveConfig` strips them on the way out either way.\n */\nexport type StoredConfig = Omit<LoamConfig, \"docsDir\"> & { docsDir: string };\n\n/**\n * Write the config for the repo rooted at `cwd`. The derived fields are dropped\n * on the way out: they are facts about where the file was found and how it was\n * spelled, and a stored copy could only ever disagree with the file itself.\n */\nexport async function saveConfig(config: StoredConfig, cwd: string = process.cwd()): Promise<string> {\n const p = localConfigPath(cwd);\n const { root: _root, docsDirAsWritten: _asWritten, ...stored } = config;\n await writeFile(p, JSON.stringify(stored, null, 2) + \"\\n\", \"utf8\");\n return p;\n}\n\n/**\n * The optional `contracts` block, validated to shape only.\n *\n * SHAPE ONLY, exactly as `agentTools` is, and for a sharper version of the same\n * reason: containment is not checked here because a build output legitimately\n * sits outside the repo root's own subtree in some layouts, and the reader that\n * resolves it is where an escaping path must be refused — with the service in\n * hand, so the refusal can name it. A config that throws takes down every\n * command, including the ones with nothing to do with contracts.\n */\nfunction readContracts(value: unknown, file: string): { openapi?: string } | undefined {\n if (value === undefined) return undefined;\n if (typeof value !== \"object\" || value === null || Array.isArray(value)) {\n throw new ConfigError(\"contracts\", `${file}: \"contracts\" must be a mapping with an optional \"openapi\" path.`);\n }\n const record = value as Record<string, unknown>;\n // Only `openapi` today. The event axis gets a key when it gets a check —\n // a config key nothing reads is worse than a missing one, because a fleet\n // sets it and believes something is being graded.\n const out: { openapi?: string } = {};\n for (const key of [\"openapi\"] as const) {\n const spelled = record[key];\n if (spelled === undefined) continue;\n if (typeof spelled !== \"string\" || spelled === \"\") {\n throw new ConfigError(\"contracts\", `${file}: \"contracts.${key}\" must be a non-empty string when present.`);\n }\n out[key] = spelled;\n }\n return out.openapi === undefined ? undefined : out;\n}\n"]}
|
|
@@ -21,6 +21,7 @@
|
|
|
21
21
|
* where a key mirrors a frontmatter field verbatim; camelCase everywhere else.
|
|
22
22
|
*/
|
|
23
23
|
import { relative } from "node:path";
|
|
24
|
+
import { loadConfig } from "./config.js";
|
|
24
25
|
import { LOAM_VERSION } from "./version.js";
|
|
25
26
|
/**
|
|
26
27
|
* Version of the top-level JSON envelope, independent of the CLI/package
|
|
@@ -147,6 +148,22 @@ export function fail(json, code, message) {
|
|
|
147
148
|
sayExplain(code);
|
|
148
149
|
process.exitCode = 1;
|
|
149
150
|
}
|
|
151
|
+
/**
|
|
152
|
+
* A refusal or failure with its stable `--json` code attached, for the commands
|
|
153
|
+
* whose refusals are THROWN — from a plan phase or a rollback path several
|
|
154
|
+
* calls below the one handler that prints — rather than returned. The handler
|
|
155
|
+
* `fail`s with `code`, so a merge that could not be computed reports as a merge
|
|
156
|
+
* failure rather than as `internal`. What anything ELSE that escapes means is
|
|
157
|
+
* each handler's to say. One class, because archive, unarchive and
|
|
158
|
+
* migrate-openspec had each written this same one.
|
|
159
|
+
*/
|
|
160
|
+
export class CodedFailure extends Error {
|
|
161
|
+
code;
|
|
162
|
+
constructor(code, message) {
|
|
163
|
+
super(message);
|
|
164
|
+
this.code = code;
|
|
165
|
+
}
|
|
166
|
+
}
|
|
150
167
|
/** The message every command prints when there is no loam.json. */
|
|
151
168
|
const NO_CONFIG_MESSAGE = "No loam.json found. Run `loam init --docs <dir>` first.";
|
|
152
169
|
/**
|
|
@@ -172,4 +189,21 @@ export function reportNoConfig(json, load) {
|
|
|
172
189
|
}
|
|
173
190
|
fail(json, "no-config", NO_CONFIG_MESSAGE);
|
|
174
191
|
}
|
|
192
|
+
/**
|
|
193
|
+
* The config a command action stands on, or null once `reportNoConfig` has
|
|
194
|
+
* refused — the caller's whole answer to null is `return`. Every command that
|
|
195
|
+
* needs a wired repo opens with this question, and it was spelled as the same
|
|
196
|
+
* six lines in each of them; one spelling is what keeps the absent/invalid
|
|
197
|
+
* split above from being forgotten in a twenty-fourth. `init` and `doctor` do
|
|
198
|
+
* not come through here: a missing or broken config is their subject, not a
|
|
199
|
+
* reason to stop.
|
|
200
|
+
*/
|
|
201
|
+
export async function configOrRefuse(json) {
|
|
202
|
+
const loaded = await loadConfig();
|
|
203
|
+
if (loaded.kind !== "loaded") {
|
|
204
|
+
reportNoConfig(json, loaded);
|
|
205
|
+
return null;
|
|
206
|
+
}
|
|
207
|
+
return loaded.config;
|
|
208
|
+
}
|
|
175
209
|
//# sourceMappingURL=json.js.map
|