@ybotok/loam 0.1.0-beta.3 → 0.2.0-alpha.1
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 +2548 -86
- package/COMPARISON.md +80 -63
- package/CONTRIBUTING.md +84 -0
- package/MIGRATING-from-OpenSpec.md +213 -54
- package/QUICKSTART.md +125 -0
- package/README.md +450 -150
- package/ROADMAP.md +494 -0
- package/SCHEMA.md +2355 -256
- package/SECURITY.md +21 -3
- package/WORKFLOW.md +701 -0
- package/dist/cli.js +113 -19
- package/dist/cli.js.map +1 -1
- package/dist/commands/adopt/adopt.js +209 -0
- package/dist/commands/adopt/adopt.js.map +1 -0
- package/dist/commands/adopt/render.js +268 -0
- package/dist/commands/adopt/render.js.map +1 -0
- package/dist/commands/archive/archive.js +77 -0
- package/dist/commands/archive/archive.js.map +1 -0
- package/dist/commands/archive/plan/contracts/asyncapi.js +178 -0
- package/dist/commands/archive/plan/contracts/asyncapi.js.map +1 -0
- package/dist/commands/archive/plan/contracts/openapi.js +159 -0
- package/dist/commands/archive/plan/contracts/openapi.js.map +1 -0
- package/dist/commands/archive/plan/gate.js +317 -0
- package/dist/commands/archive/plan/gate.js.map +1 -0
- package/dist/commands/archive/plan/landscape.js +161 -0
- package/dist/commands/archive/plan/landscape.js.map +1 -0
- package/dist/commands/archive/plan/refusal.js +111 -0
- package/dist/commands/archive/plan/refusal.js.map +1 -0
- package/dist/commands/archive/plan/specs.js +237 -0
- package/dist/commands/archive/plan/specs.js.map +1 -0
- package/dist/commands/archive/plan/state.js +27 -0
- package/dist/commands/archive/plan/state.js.map +1 -0
- package/dist/commands/archive/run.js +251 -0
- package/dist/commands/archive/run.js.map +1 -0
- package/dist/commands/context/context.js +99 -0
- package/dist/commands/context/context.js.map +1 -0
- package/dist/commands/context/print.js +283 -0
- package/dist/commands/context/print.js.map +1 -0
- package/dist/commands/delta/delta.js +279 -0
- package/dist/commands/delta/delta.js.map +1 -0
- package/dist/commands/delta/print.js +205 -0
- package/dist/commands/delta/print.js.map +1 -0
- package/dist/commands/dependencies.js +9 -7
- package/dist/commands/dependencies.js.map +1 -1
- package/dist/commands/diff/diff.js +93 -0
- package/dist/commands/diff/diff.js.map +1 -0
- package/dist/commands/diff/print.js +38 -0
- package/dist/commands/diff/print.js.map +1 -0
- package/dist/commands/doctor.js +5 -4
- package/dist/commands/doctor.js.map +1 -1
- package/dist/commands/explain/explain.js +151 -0
- package/dist/commands/explain/explain.js.map +1 -0
- package/dist/commands/explore.js +29 -24
- package/dist/commands/explore.js.map +1 -1
- package/dist/commands/gate/gate.js +115 -0
- package/dist/commands/gate/gate.js.map +1 -0
- package/dist/commands/gate/print.js +54 -0
- package/dist/commands/gate/print.js.map +1 -0
- package/dist/commands/gherkin/commit.js +146 -0
- package/dist/commands/gherkin/commit.js.map +1 -0
- package/dist/commands/gherkin/gherkin.js +246 -0
- package/dist/commands/gherkin/gherkin.js.map +1 -0
- package/dist/commands/gherkin/reconcile.js +149 -0
- package/dist/commands/gherkin/reconcile.js.map +1 -0
- package/dist/commands/gherkin/render.js +77 -0
- package/dist/commands/gherkin/render.js.map +1 -0
- package/dist/commands/init/example.js +204 -0
- package/dist/commands/init/example.js.map +1 -0
- package/dist/commands/init/first-hour.js +28 -0
- package/dist/commands/init/first-hour.js.map +1 -0
- package/dist/commands/{init.js → init/init.js} +146 -71
- package/dist/commands/init/init.js.map +1 -0
- package/dist/commands/init/mcp.js +131 -0
- package/dist/commands/init/mcp.js.map +1 -0
- package/dist/commands/init/options.js +78 -0
- package/dist/commands/init/options.js.map +1 -0
- package/dist/commands/instructions.js +72 -14
- package/dist/commands/instructions.js.map +1 -1
- package/dist/commands/list/campaign/campaign.js +65 -0
- package/dist/commands/list/campaign/campaign.js.map +1 -0
- package/dist/commands/list/campaign/owners.js +65 -0
- package/dist/commands/list/campaign/owners.js.map +1 -0
- package/dist/commands/list/json.js +157 -0
- package/dist/commands/list/json.js.map +1 -0
- package/dist/commands/list/list.js +343 -0
- package/dist/commands/list/list.js.map +1 -0
- package/dist/commands/list/print.js +301 -0
- package/dist/commands/list/print.js.map +1 -0
- package/dist/commands/list/review.js +120 -0
- package/dist/commands/list/review.js.map +1 -0
- package/dist/commands/list/views.js +105 -0
- package/dist/commands/list/views.js.map +1 -0
- package/dist/commands/mcp/dispatch.js +183 -0
- package/dist/commands/mcp/dispatch.js.map +1 -0
- package/dist/commands/mcp/mcp.js +34 -0
- package/dist/commands/mcp/mcp.js.map +1 -0
- package/dist/commands/mcp/serve.js +55 -0
- package/dist/commands/mcp/serve.js.map +1 -0
- package/dist/commands/migrate-openspec/materialize/active.js +170 -0
- package/dist/commands/migrate-openspec/materialize/active.js.map +1 -0
- package/dist/commands/migrate-openspec/materialize/feature.js +41 -0
- package/dist/commands/migrate-openspec/materialize/feature.js.map +1 -0
- package/dist/commands/migrate-openspec/materialize/living.js +62 -0
- package/dist/commands/migrate-openspec/materialize/living.js.map +1 -0
- package/dist/commands/migrate-openspec/materialize/stage.js +142 -0
- package/dist/commands/migrate-openspec/materialize/stage.js.map +1 -0
- package/dist/commands/migrate-openspec/materialize/target.js +219 -0
- package/dist/commands/migrate-openspec/materialize/target.js.map +1 -0
- package/dist/commands/migrate-openspec/migrate-openspec.js +142 -0
- package/dist/commands/migrate-openspec/migrate-openspec.js.map +1 -0
- package/dist/commands/migrate-openspec/openspec/decisions.js +91 -0
- package/dist/commands/migrate-openspec/openspec/decisions.js.map +1 -0
- package/dist/commands/migrate-openspec/openspec/error.js +19 -0
- package/dist/commands/migrate-openspec/openspec/error.js.map +1 -0
- package/dist/commands/migrate-openspec/openspec/mapping.js +170 -0
- package/dist/commands/migrate-openspec/openspec/mapping.js.map +1 -0
- package/dist/commands/migrate-openspec/openspec/paths.js +43 -0
- package/dist/commands/migrate-openspec/openspec/paths.js.map +1 -0
- package/dist/commands/migrate-openspec/openspec/read.js +49 -0
- package/dist/commands/migrate-openspec/openspec/read.js.map +1 -0
- package/dist/commands/migrate-openspec/print.js +89 -0
- package/dist/commands/migrate-openspec/print.js.map +1 -0
- package/dist/commands/new/commit.js +76 -0
- package/dist/commands/new/commit.js.map +1 -0
- package/dist/commands/new/new.js +283 -0
- package/dist/commands/new/new.js.map +1 -0
- package/dist/commands/new/templates.js +355 -0
- package/dist/commands/new/templates.js.map +1 -0
- package/dist/commands/open.js +203 -0
- package/dist/commands/open.js.map +1 -0
- package/dist/commands/policy/format.js +152 -0
- package/dist/commands/policy/format.js.map +1 -0
- package/dist/commands/{docs-repo-gate.js → policy/gate.js} +11 -11
- package/dist/commands/policy/gate.js.map +1 -0
- package/dist/commands/rebase/edit.js +106 -0
- package/dist/commands/rebase/edit.js.map +1 -0
- package/dist/commands/rebase/living.js +185 -0
- package/dist/commands/rebase/living.js.map +1 -0
- package/dist/commands/rebase/plan.js +245 -0
- package/dist/commands/rebase/plan.js.map +1 -0
- package/dist/commands/rebase/rebase.js +275 -0
- package/dist/commands/rebase/rebase.js.map +1 -0
- package/dist/commands/seed/commit.js +192 -0
- package/dist/commands/seed/commit.js.map +1 -0
- package/dist/commands/seed/plan.js +133 -0
- package/dist/commands/seed/plan.js.map +1 -0
- package/dist/commands/seed/seed.js +172 -0
- package/dist/commands/seed/seed.js.map +1 -0
- package/dist/commands/show/feature.js +178 -0
- package/dist/commands/show/feature.js.map +1 -0
- package/dist/commands/show/marks.js +24 -0
- package/dist/commands/show/marks.js.map +1 -0
- package/dist/commands/{show.js → show/service.js} +63 -152
- package/dist/commands/show/service.js.map +1 -0
- package/dist/commands/show/show.js +77 -0
- package/dist/commands/show/show.js.map +1 -0
- package/dist/commands/status/json.js +45 -0
- package/dist/commands/status/json.js.map +1 -0
- package/dist/commands/status/print.js +157 -0
- package/dist/commands/status/print.js.map +1 -0
- package/dist/commands/status/status.js +119 -0
- package/dist/commands/status/status.js.map +1 -0
- package/dist/commands/steps/steps.js +189 -0
- package/dist/commands/steps/steps.js.map +1 -0
- package/dist/commands/subsystem/edit.js +192 -0
- package/dist/commands/subsystem/edit.js.map +1 -0
- package/dist/commands/subsystem/move.js +265 -0
- package/dist/commands/subsystem/move.js.map +1 -0
- package/dist/commands/subsystem/query.js +115 -0
- package/dist/commands/subsystem/query.js.map +1 -0
- package/dist/commands/subsystem/subsystem.js +101 -0
- package/dist/commands/subsystem/subsystem.js.map +1 -0
- package/dist/commands/subsystem/sync.js +103 -0
- package/dist/commands/subsystem/sync.js.map +1 -0
- package/dist/commands/subsystem/txn/txn.js +94 -0
- package/dist/commands/subsystem/txn/txn.js.map +1 -0
- package/dist/commands/subsystem/txn/views.js +70 -0
- package/dist/commands/subsystem/txn/views.js.map +1 -0
- package/dist/commands/unarchive/manifest.js +160 -0
- package/dist/commands/unarchive/manifest.js.map +1 -0
- package/dist/commands/{unarchive.js → unarchive/restore.js} +47 -160
- package/dist/commands/unarchive/restore.js.map +1 -0
- package/dist/commands/unarchive/unarchive.js +67 -0
- package/dist/commands/unarchive/unarchive.js.map +1 -0
- package/dist/commands/validate/arch-coverage.js +194 -0
- package/dist/commands/validate/arch-coverage.js.map +1 -0
- package/dist/commands/validate/checks/fleet-shape.js +296 -0
- package/dist/commands/validate/checks/fleet-shape.js.map +1 -0
- package/dist/commands/validate/checks/health-deps.js +55 -0
- package/dist/commands/validate/checks/health-deps.js.map +1 -0
- package/dist/commands/validate/checks/requirements.js +254 -0
- package/dist/commands/validate/checks/requirements.js.map +1 -0
- package/dist/commands/validate/checks/sources.js +76 -0
- package/dist/commands/validate/checks/sources.js.map +1 -0
- package/dist/commands/validate/checks/vocabulary.js +31 -0
- package/dist/commands/validate/checks/vocabulary.js.map +1 -0
- package/dist/commands/validate/feature.js +216 -0
- package/dist/commands/validate/feature.js.map +1 -0
- package/dist/commands/validate/fleet/census.js +69 -0
- package/dist/commands/validate/fleet/census.js.map +1 -0
- package/dist/commands/validate/fleet/kind-tags.js +39 -0
- package/dist/commands/validate/fleet/kind-tags.js.map +1 -0
- package/dist/commands/validate/fleet/landscape.js +353 -0
- package/dist/commands/validate/fleet/landscape.js.map +1 -0
- package/dist/commands/validate/fleet/load.js +52 -0
- package/dist/commands/validate/fleet/load.js.map +1 -0
- package/dist/commands/validate/fleet/obligations.js +236 -0
- package/dist/commands/validate/fleet/obligations.js.map +1 -0
- package/dist/commands/validate/fleet/scorecard/adoption.js +102 -0
- package/dist/commands/validate/fleet/scorecard/adoption.js.map +1 -0
- package/dist/commands/validate/fleet/scorecard/contracts.js +98 -0
- package/dist/commands/validate/fleet/scorecard/contracts.js.map +1 -0
- package/dist/commands/validate/fleet/scorecard/print.js +63 -0
- package/dist/commands/validate/fleet/scorecard/print.js.map +1 -0
- package/dist/commands/validate/fleet/scorecard/scorecard.js +156 -0
- package/dist/commands/validate/fleet/scorecard/scorecard.js.map +1 -0
- package/dist/commands/validate/fleet/usecases/capability-tag.js +94 -0
- package/dist/commands/validate/fleet/usecases/capability-tag.js.map +1 -0
- package/dist/commands/validate/fleet/usecases/place.js +13 -0
- package/dist/commands/validate/fleet/usecases/place.js.map +1 -0
- package/dist/commands/validate/fleet/usecases/requirement-tag.js +95 -0
- package/dist/commands/validate/fleet/usecases/requirement-tag.js.map +1 -0
- package/dist/commands/validate/fleet/usecases/steps.js +197 -0
- package/dist/commands/validate/fleet/usecases/steps.js.map +1 -0
- package/dist/commands/validate/fleet/usecases/usecases.js +99 -0
- package/dist/commands/validate/fleet/usecases/usecases.js.map +1 -0
- package/dist/commands/validate/fleet/views/ids.js +40 -0
- package/dist/commands/validate/fleet/views/ids.js.map +1 -0
- package/dist/commands/validate/fleet/views/stale.js +59 -0
- package/dist/commands/validate/fleet/views/stale.js.map +1 -0
- package/dist/commands/validate/links/corpus.js +92 -0
- package/dist/commands/validate/links/corpus.js.map +1 -0
- package/dist/commands/validate/links/glossary.js +75 -0
- package/dist/commands/validate/links/glossary.js.map +1 -0
- package/dist/commands/validate/report.js +269 -0
- package/dist/commands/validate/report.js.map +1 -0
- package/dist/commands/validate/service/api.js +338 -0
- package/dist/commands/validate/service/api.js.map +1 -0
- package/dist/commands/validate/service/events/events.js +219 -0
- package/dist/commands/validate/service/events/events.js.map +1 -0
- package/dist/commands/validate/service/events/message-producers.js +77 -0
- package/dist/commands/validate/service/events/message-producers.js.map +1 -0
- package/dist/commands/validate/service/evidence-pins.js +131 -0
- package/dist/commands/validate/service/evidence-pins.js.map +1 -0
- package/dist/commands/validate/service/service.js +250 -0
- package/dist/commands/validate/service/service.js.map +1 -0
- package/dist/commands/validate/service/specs.js +219 -0
- package/dist/commands/validate/service/specs.js.map +1 -0
- package/dist/commands/validate/service/spine.js +113 -0
- package/dist/commands/validate/service/spine.js.map +1 -0
- package/dist/commands/validate/validate.js +338 -0
- package/dist/commands/validate/validate.js.map +1 -0
- package/dist/commands/verify/cross/diff.js +182 -0
- package/dist/commands/verify/cross/diff.js.map +1 -0
- package/dist/commands/verify/evidence/contract.js +32 -0
- package/dist/commands/verify/evidence/contract.js.map +1 -0
- package/dist/commands/verify/evidence/pins.js +52 -0
- package/dist/commands/verify/evidence/pins.js.map +1 -0
- package/dist/commands/verify/evidence/read.js +72 -0
- package/dist/commands/verify/evidence/read.js.map +1 -0
- package/dist/commands/verify/frozen.js +241 -0
- package/dist/commands/verify/frozen.js.map +1 -0
- package/dist/commands/verify/record.js +226 -0
- package/dist/commands/verify/record.js.map +1 -0
- package/dist/commands/verify/report.js +223 -0
- package/dist/commands/verify/report.js.map +1 -0
- package/dist/commands/verify/results.js +246 -0
- package/dist/commands/verify/results.js.map +1 -0
- package/dist/commands/verify/verify.js +217 -0
- package/dist/commands/verify/verify.js.map +1 -0
- package/dist/commands/vouch/contract.js +11 -0
- package/dist/commands/vouch/contract.js.map +1 -0
- package/dist/commands/vouch/pack/contract.js +2 -0
- package/dist/commands/vouch/pack/contract.js.map +1 -0
- package/dist/commands/vouch/pack/pack.js +266 -0
- package/dist/commands/vouch/pack/pack.js.map +1 -0
- package/dist/commands/vouch/pack/print.js +271 -0
- package/dist/commands/vouch/pack/print.js.map +1 -0
- package/dist/commands/vouch/pack/sections.js +142 -0
- package/dist/commands/vouch/pack/sections.js.map +1 -0
- package/dist/commands/vouch/prompt/confirm.js +46 -0
- package/dist/commands/vouch/prompt/confirm.js.map +1 -0
- package/dist/commands/vouch/report.js +101 -0
- package/dist/commands/vouch/report.js.map +1 -0
- package/dist/commands/vouch/run.js +285 -0
- package/dist/commands/vouch/run.js.map +1 -0
- package/dist/commands/vouch/sample/plan.js +125 -0
- package/dist/commands/vouch/sample/plan.js.map +1 -0
- package/dist/commands/vouch/sample/print.js +109 -0
- package/dist/commands/vouch/sample/print.js.map +1 -0
- package/dist/commands/vouch/vet/verify.js +123 -0
- package/dist/commands/vouch/vet/verify.js.map +1 -0
- package/dist/commands/vouch/vouch.js +176 -0
- package/dist/commands/vouch/vouch.js.map +1 -0
- package/dist/core/agent/agents-md/artifacts.js +220 -0
- package/dist/core/agent/agents-md/artifacts.js.map +1 -0
- package/dist/core/agent/agents-md/command-map.js +324 -0
- package/dist/core/agent/agents-md/command-map.js.map +1 -0
- package/dist/core/agent/agents-md/cycle.js +165 -0
- package/dist/core/agent/agents-md/cycle.js.map +1 -0
- package/dist/core/agent/agents-md/map/explain.js +34 -0
- package/dist/core/agent/agents-md/map/explain.js.map +1 -0
- package/dist/core/agent/agents-md/map/lenses/context.js +41 -0
- package/dist/core/agent/agents-md/map/lenses/context.js.map +1 -0
- package/dist/core/agent/agents-md/map/lenses/diff.js +46 -0
- package/dist/core/agent/agents-md/map/lenses/diff.js.map +1 -0
- package/dist/core/agent/agents-md/map/lenses/gate.js +57 -0
- package/dist/core/agent/agents-md/map/lenses/gate.js.map +1 -0
- package/dist/core/agent/agents-md/map/mcp.js +41 -0
- package/dist/core/agent/agents-md/map/mcp.js.map +1 -0
- package/dist/core/agent/agents-md/map/status.js +62 -0
- package/dist/core/agent/agents-md/map/status.js.map +1 -0
- package/dist/core/agent/agents-md/map/subsystem.js +32 -0
- package/dist/core/agent/agents-md/map/subsystem.js.map +1 -0
- package/dist/core/agent/agents-md/map/usecases.js +60 -0
- package/dist/core/agent/agents-md/map/usecases.js.map +1 -0
- package/dist/core/agent/agents-md/refusals.js +246 -0
- package/dist/core/agent/agents-md/refusals.js.map +1 -0
- package/dist/core/agent/agents-md/shipped/archive-gate.js +98 -0
- package/dist/core/agent/agents-md/shipped/archive-gate.js.map +1 -0
- package/dist/core/agent/agents-md/spine.js +40 -0
- package/dist/core/agent/agents-md/spine.js.map +1 -0
- package/dist/core/agent/agents-md.js +170 -0
- package/dist/core/agent/agents-md.js.map +1 -0
- package/dist/core/{agents-stamp.js → agent/agents-stamp.js} +50 -0
- package/dist/core/agent/agents-stamp.js.map +1 -0
- package/dist/core/agent/contract.js +2 -0
- package/dist/core/agent/contract.js.map +1 -0
- package/dist/core/agent/protocol.js +193 -0
- package/dist/core/agent/protocol.js.map +1 -0
- package/dist/core/agent/scaffold.js +259 -0
- package/dist/core/agent/scaffold.js.map +1 -0
- package/dist/core/agent/support/report.js +103 -0
- package/dist/core/agent/support/report.js.map +1 -0
- package/dist/core/agent/tools/dialects.js +150 -0
- package/dist/core/agent/tools/dialects.js.map +1 -0
- package/dist/core/agent/tools/registry.js +228 -0
- package/dist/core/agent/tools/registry.js.map +1 -0
- package/dist/core/agent/workflows/adopt.js +133 -0
- package/dist/core/agent/workflows/adopt.js.map +1 -0
- package/dist/core/agent/workflows/check.js +334 -0
- package/dist/core/agent/workflows/check.js.map +1 -0
- package/dist/core/agent/workflows/closing.js +262 -0
- package/dist/core/agent/workflows/closing.js.map +1 -0
- package/dist/core/agent/workflows/feature.js +105 -0
- package/dist/core/agent/workflows/feature.js.map +1 -0
- package/dist/core/agent/workflows/implement.js +105 -0
- package/dist/core/agent/workflows/implement.js.map +1 -0
- package/dist/core/agent/workflows/reference/authoring.js +209 -0
- package/dist/core/agent/workflows/reference/authoring.js.map +1 -0
- package/dist/core/agent/workflows/reference/codes.js +65 -0
- package/dist/core/agent/workflows/reference/codes.js.map +1 -0
- package/dist/core/agent/workflows/reference/done-check.js +188 -0
- package/dist/core/agent/workflows/reference/done-check.js.map +1 -0
- package/dist/core/agent/workflows/reference/reference.js +16 -0
- package/dist/core/agent/workflows/reference/reference.js.map +1 -0
- package/dist/core/agent/workflows/reference/spine.js +297 -0
- package/dist/core/agent/workflows/reference/spine.js.map +1 -0
- package/dist/core/asyncapi/baseline/plan.js +110 -0
- package/dist/core/asyncapi/baseline/plan.js.map +1 -0
- package/dist/core/asyncapi/baseline/surfaces.js +54 -0
- package/dist/core/asyncapi/baseline/surfaces.js.map +1 -0
- package/dist/core/asyncapi/depth.js +164 -0
- package/dist/core/asyncapi/depth.js.map +1 -0
- package/dist/core/asyncapi/digest.js +141 -0
- package/dist/core/asyncapi/digest.js.map +1 -0
- package/dist/core/asyncapi/merge/components.js +91 -0
- package/dist/core/asyncapi/merge/components.js.map +1 -0
- package/dist/core/asyncapi/merge/error.js +33 -0
- package/dist/core/asyncapi/merge/error.js.map +1 -0
- package/dist/core/asyncapi/merge/markers.js +308 -0
- package/dist/core/asyncapi/merge/markers.js.map +1 -0
- package/dist/core/asyncapi/merge/merge.js +215 -0
- package/dist/core/asyncapi/merge/merge.js.map +1 -0
- package/dist/core/asyncapi/merge/pin.js +107 -0
- package/dist/core/asyncapi/merge/pin.js.map +1 -0
- package/dist/core/asyncapi/model.js +11 -0
- package/dist/core/asyncapi/model.js.map +1 -0
- package/dist/core/asyncapi/producers.js +50 -0
- package/dist/core/asyncapi/producers.js.map +1 -0
- package/dist/core/{asyncapi.js → asyncapi/read.js} +95 -109
- package/dist/core/asyncapi/read.js.map +1 -0
- package/dist/core/brief/brief.js +225 -0
- package/dist/core/brief/brief.js.map +1 -0
- package/dist/core/brief/checks.js +250 -0
- package/dist/core/brief/checks.js.map +1 -0
- package/dist/core/brief/landscape.js +194 -0
- package/dist/core/brief/landscape.js.map +1 -0
- package/dist/core/brief/targets.js +226 -0
- package/dist/core/brief/targets.js.map +1 -0
- package/dist/core/brief/unchecked.js +47 -0
- package/dist/core/brief/unchecked.js.map +1 -0
- package/dist/core/c4/arch.js +282 -0
- package/dist/core/c4/arch.js.map +1 -0
- package/dist/core/c4/likec4.js +88 -0
- package/dist/core/c4/likec4.js.map +1 -0
- package/dist/core/c4/parsed/dynamic-views.js +132 -0
- package/dist/core/c4/parsed/dynamic-views.js.map +1 -0
- package/dist/core/c4/parsed/specification.js +78 -0
- package/dist/core/c4/parsed/specification.js.map +1 -0
- package/dist/core/c4/parsed/values.js +57 -0
- package/dist/core/c4/parsed/values.js.map +1 -0
- package/dist/core/c4/parsed/view-ids.js +75 -0
- package/dist/core/c4/parsed/view-ids.js.map +1 -0
- package/dist/core/c4/project/architecture.js +45 -0
- package/dist/core/c4/project/architecture.js.map +1 -0
- package/dist/core/c4/project/documents.js +58 -0
- package/dist/core/c4/project/documents.js.map +1 -0
- package/dist/core/c4/project/load.js +183 -0
- package/dist/core/c4/project/load.js.map +1 -0
- package/dist/core/c4/resolve/service.js +113 -0
- package/dist/core/c4/resolve/service.js.map +1 -0
- package/dist/core/c4/seed/fleet-file.js +201 -0
- package/dist/core/c4/seed/fleet-file.js.map +1 -0
- package/dist/core/c4/seed/items.js +91 -0
- package/dist/core/c4/seed/items.js.map +1 -0
- package/dist/core/c4/seed/stamp.js +48 -0
- package/dist/core/c4/seed/stamp.js.map +1 -0
- package/dist/core/c4/seed/template.js +109 -0
- package/dist/core/c4/seed/template.js.map +1 -0
- package/dist/core/c4/source-mask.js +82 -0
- package/dist/core/c4/source-mask.js.map +1 -0
- package/dist/core/c4/source-scan.js +227 -0
- package/dist/core/c4/source-scan.js.map +1 -0
- package/dist/core/c4/splice/authored-source.js +101 -0
- package/dist/core/c4/splice/authored-source.js.map +1 -0
- package/dist/core/c4/splice/contract.js +10 -0
- package/dist/core/c4/splice/contract.js.map +1 -0
- package/dist/core/c4/splice/delta-blocks.js +126 -0
- package/dist/core/c4/splice/delta-blocks.js.map +1 -0
- package/dist/core/c4/splice/identity/edges.js +36 -0
- package/dist/core/c4/splice/identity/edges.js.map +1 -0
- package/dist/core/c4/splice/landscape-merge.js +269 -0
- package/dist/core/c4/splice/landscape-merge.js.map +1 -0
- package/dist/core/c4/splice/placement.js +194 -0
- package/dist/core/c4/splice/placement.js.map +1 -0
- package/dist/core/c4/workspace.js +172 -0
- package/dist/core/c4/workspace.js.map +1 -0
- package/dist/core/capabilities/authoring/notes.js +141 -0
- package/dist/core/capabilities/authoring/notes.js.map +1 -0
- package/dist/core/capabilities/capabilities.js +124 -0
- package/dist/core/capabilities/capabilities.js.map +1 -0
- package/dist/core/capabilities/delta/doc.js +50 -0
- package/dist/core/capabilities/delta/doc.js.map +1 -0
- package/dist/core/capabilities/delta/overlay.js +129 -0
- package/dist/core/capabilities/delta/overlay.js.map +1 -0
- package/dist/core/capabilities/delta/summary.js +57 -0
- package/dist/core/capabilities/delta/summary.js.map +1 -0
- package/dist/core/capabilities/delta/tree.js +43 -0
- package/dist/core/capabilities/delta/tree.js.map +1 -0
- package/dist/core/capabilities/delta/uncovered.js +222 -0
- package/dist/core/capabilities/delta/uncovered.js.map +1 -0
- package/dist/core/capabilities/findings.js +342 -0
- package/dist/core/capabilities/findings.js.map +1 -0
- package/dist/core/capabilities/realizes/findings.js +181 -0
- package/dist/core/capabilities/realizes/findings.js.map +1 -0
- package/dist/core/capabilities/realizes/join.js +143 -0
- package/dist/core/capabilities/realizes/join.js.map +1 -0
- package/dist/core/capabilities/rollup.js +199 -0
- package/dist/core/capabilities/rollup.js.map +1 -0
- package/dist/core/capabilities/tree.js +126 -0
- package/dist/core/capabilities/tree.js.map +1 -0
- package/dist/core/capabilities/usecase-join.js +277 -0
- package/dist/core/capabilities/usecase-join.js.map +1 -0
- package/dist/core/coherence/authoring/scaffold.js +177 -0
- package/dist/core/coherence/authoring/scaffold.js.map +1 -0
- package/dist/core/coherence/authoring/sentinels.js +74 -0
- package/dist/core/coherence/authoring/sentinels.js.map +1 -0
- package/dist/core/coherence/coherence.js +251 -0
- package/dist/core/coherence/coherence.js.map +1 -0
- package/dist/core/coherence/declared.js +269 -0
- package/dist/core/coherence/declared.js.map +1 -0
- package/dist/core/coherence/events/declared.js +206 -0
- package/dist/core/coherence/events/declared.js.map +1 -0
- package/dist/core/coherence/events/events.js +204 -0
- package/dist/core/coherence/events/events.js.map +1 -0
- package/dist/core/coherence/events/grades.js +166 -0
- package/dist/core/coherence/events/grades.js.map +1 -0
- package/dist/core/coherence/events/lookups.js +122 -0
- package/dist/core/coherence/events/lookups.js.map +1 -0
- package/dist/core/coherence/events/merged.js +40 -0
- package/dist/core/coherence/events/merged.js.map +1 -0
- package/dist/core/coherence/living.js +198 -0
- package/dist/core/coherence/living.js.map +1 -0
- package/dist/core/coherence/lookups.js +155 -0
- package/dist/core/coherence/lookups.js.map +1 -0
- package/dist/core/coherence/pending.js +39 -0
- package/dist/core/coherence/pending.js.map +1 -0
- package/dist/core/conflict-markers.js +60 -0
- package/dist/core/conflict-markers.js.map +1 -0
- package/dist/core/delta/claims.js +73 -0
- package/dist/core/delta/claims.js.map +1 -0
- package/dist/core/delta/delta.js +296 -0
- package/dist/core/delta/delta.js.map +1 -0
- package/dist/core/delta/document.js +177 -0
- package/dist/core/delta/document.js.map +1 -0
- package/dist/core/delta/scope.js +48 -0
- package/dist/core/delta/scope.js.map +1 -0
- package/dist/core/delta/select.js +237 -0
- package/dist/core/delta/select.js.map +1 -0
- package/dist/core/dependencies/dependencies.js +141 -0
- package/dist/core/dependencies/dependencies.js.map +1 -0
- package/dist/core/dependencies/facts.js +165 -0
- package/dist/core/dependencies/facts.js.map +1 -0
- package/dist/core/dependencies/fanin.js +88 -0
- package/dist/core/dependencies/fanin.js.map +1 -0
- package/dist/core/dependencies/graph.js +106 -0
- package/dist/core/dependencies/graph.js.map +1 -0
- package/dist/core/diff/base-git.js +148 -0
- package/dist/core/diff/base-git.js.map +1 -0
- package/dist/core/diff/base-state.js +209 -0
- package/dist/core/diff/base-state.js.map +1 -0
- package/dist/core/diff/current-state.js +96 -0
- package/dist/core/diff/current-state.js.map +1 -0
- package/dist/core/diff/scope/changed-paths.js +188 -0
- package/dist/core/diff/scope/changed-paths.js.map +1 -0
- package/dist/core/diff/semantic.js +243 -0
- package/dist/core/diff/semantic.js.map +1 -0
- package/dist/core/diff/victims.js +247 -0
- package/dist/core/diff/victims.js.map +1 -0
- package/dist/core/docs.js +7 -64
- package/dist/core/docs.js.map +1 -1
- package/dist/core/doctor/agents.js +222 -0
- package/dist/core/doctor/agents.js.map +1 -0
- package/dist/core/doctor/config.js +135 -0
- package/dist/core/doctor/config.js.map +1 -0
- package/dist/core/doctor/doctor.js +275 -0
- package/dist/core/doctor/doctor.js.map +1 -0
- package/dist/core/doctor/report.js +13 -0
- package/dist/core/doctor/report.js.map +1 -0
- package/dist/core/doctor/residue.js +110 -0
- package/dist/core/doctor/residue.js.map +1 -0
- package/dist/core/document/apply.js +77 -0
- package/dist/core/document/apply.js.map +1 -0
- package/dist/core/{frontmatter.js → document/frontmatter.js} +48 -2
- package/dist/core/document/frontmatter.js.map +1 -0
- package/dist/core/document/grammar/directives.js +143 -0
- package/dist/core/document/grammar/directives.js.map +1 -0
- package/dist/core/document/parse.js +263 -0
- package/dist/core/document/parse.js.map +1 -0
- package/dist/core/document/scenarios.js +221 -0
- package/dist/core/document/scenarios.js.map +1 -0
- package/dist/core/document/spec.js +241 -0
- package/dist/core/document/spec.js.map +1 -0
- package/dist/core/{config.js → envelope/config.js} +79 -18
- package/dist/core/envelope/config.js.map +1 -0
- package/dist/core/envelope/json.js +175 -0
- package/dist/core/envelope/json.js.map +1 -0
- package/dist/core/{version.js → envelope/version.js} +3 -3
- package/dist/core/envelope/version.js.map +1 -0
- package/dist/core/explain/families.js +175 -0
- package/dist/core/explain/families.js.map +1 -0
- package/dist/core/explain/fix-tables.js +178 -0
- package/dist/core/explain/fix-tables.js.map +1 -0
- package/dist/core/explain/lookup.js +181 -0
- package/dist/core/explain/lookup.js.map +1 -0
- package/dist/core/explain/refusals.js +49 -0
- package/dist/core/explain/refusals.js.map +1 -0
- package/dist/core/explain/terms.js +144 -0
- package/dist/core/explain/terms.js.map +1 -0
- package/dist/core/explore/describe.js +105 -0
- package/dist/core/explore/describe.js.map +1 -0
- package/dist/core/{explore.js → explore/explore.js} +50 -101
- package/dist/core/explore/explore.js.map +1 -0
- package/dist/core/fleet-context.js +120 -78
- package/dist/core/fleet-context.js.map +1 -1
- package/dist/core/gate/checks.js +201 -0
- package/dist/core/gate/checks.js.map +1 -0
- package/dist/core/gate/partners.js +205 -0
- package/dist/core/gate/partners.js.map +1 -0
- package/dist/core/gate/report.js +47 -0
- package/dist/core/gate/report.js.map +1 -0
- package/dist/core/gate/verification.js +113 -0
- package/dist/core/gate/verification.js.map +1 -0
- package/dist/core/gherkin/digest.js +60 -0
- package/dist/core/gherkin/digest.js.map +1 -0
- package/dist/core/gherkin/emit.js +177 -0
- package/dist/core/gherkin/emit.js.map +1 -0
- package/dist/core/gherkin/read.js +76 -0
- package/dist/core/gherkin/read.js.map +1 -0
- package/dist/core/gherkin/stale.js +234 -0
- package/dist/core/gherkin/stale.js.map +1 -0
- package/dist/core/gherkin/stamp.js +77 -0
- package/dist/core/gherkin/stamp.js.map +1 -0
- package/dist/core/gherkin/steps/catalogue.js +154 -0
- package/dist/core/gherkin/steps/catalogue.js.map +1 -0
- package/dist/core/gherkin/steps/inventory.js +98 -0
- package/dist/core/gherkin/steps/inventory.js.map +1 -0
- package/dist/core/gherkin/steps/phrase.js +72 -0
- package/dist/core/gherkin/steps/phrase.js.map +1 -0
- package/dist/core/glossary/delta.js +106 -0
- package/dist/core/glossary/delta.js.map +1 -0
- package/dist/core/glossary/tree.js +86 -0
- package/dist/core/glossary/tree.js.map +1 -0
- package/dist/core/kernel/concurrency.js.map +1 -0
- package/dist/core/{document-bytes.js → kernel/document-bytes.js} +1 -1
- package/dist/core/kernel/document-bytes.js.map +1 -0
- package/dist/core/kernel/fences.js +42 -0
- package/dist/core/kernel/fences.js.map +1 -0
- package/dist/core/kernel/ids/capability.js +72 -0
- package/dist/core/kernel/ids/capability.js.map +1 -0
- package/dist/core/kernel/ids/dirs.js +33 -0
- package/dist/core/kernel/ids/dirs.js.map +1 -0
- package/dist/core/kernel/ids/feature.js +75 -0
- package/dist/core/kernel/ids/feature.js.map +1 -0
- package/dist/core/kernel/ids/service.js +156 -0
- package/dist/core/kernel/ids/service.js.map +1 -0
- package/dist/core/kernel/ids/subsystem.js +63 -0
- package/dist/core/kernel/ids/subsystem.js.map +1 -0
- package/dist/core/{path-safety.js → kernel/path-safety.js} +18 -4
- package/dist/core/kernel/path-safety.js.map +1 -0
- package/dist/core/kernel/records.js.map +1 -0
- package/dist/core/links/backlinks.js +68 -0
- package/dist/core/links/backlinks.js.map +1 -0
- package/dist/core/links/case.js +74 -0
- package/dist/core/links/case.js.map +1 -0
- package/dist/core/links/corpus.js +104 -0
- package/dist/core/links/corpus.js.map +1 -0
- package/dist/core/links/findings.js +110 -0
- package/dist/core/links/findings.js.map +1 -0
- package/dist/core/links/parse.js +236 -0
- package/dist/core/links/parse.js.map +1 -0
- package/dist/core/mcp/argv.js +111 -0
- package/dist/core/mcp/argv.js.map +1 -0
- package/dist/core/mcp/author-tools.js +72 -0
- package/dist/core/mcp/author-tools.js.map +1 -0
- package/dist/core/mcp/framing.js +46 -0
- package/dist/core/mcp/framing.js.map +1 -0
- package/dist/core/mcp/protocol.js +280 -0
- package/dist/core/mcp/protocol.js.map +1 -0
- package/dist/core/mcp/tools.js +314 -0
- package/dist/core/mcp/tools.js.map +1 -0
- package/dist/core/obligations/obligations.js +106 -0
- package/dist/core/obligations/obligations.js.map +1 -0
- package/dist/core/openapi/baseline/gate.js +166 -0
- package/dist/core/openapi/baseline/gate.js.map +1 -0
- package/dist/core/openapi/baseline/plan.js +112 -0
- package/dist/core/openapi/baseline/plan.js.map +1 -0
- package/dist/core/openapi/baseline/record.js +175 -0
- package/dist/core/openapi/baseline/record.js.map +1 -0
- package/dist/core/openapi/depth.js +122 -0
- package/dist/core/openapi/depth.js.map +1 -0
- package/dist/core/openapi/digest.js +189 -0
- package/dist/core/openapi/digest.js.map +1 -0
- package/dist/core/{openapi.js → openapi/doc.js} +44 -106
- package/dist/core/openapi/doc.js.map +1 -0
- package/dist/core/openapi/merge/components.js +234 -0
- package/dist/core/openapi/merge/components.js.map +1 -0
- package/dist/core/openapi/merge/error.js +26 -0
- package/dist/core/openapi/merge/error.js.map +1 -0
- package/dist/core/openapi/merge/markers.js +208 -0
- package/dist/core/openapi/merge/markers.js.map +1 -0
- package/dist/core/openapi/merge/merge.js +256 -0
- package/dist/core/openapi/merge/merge.js.map +1 -0
- package/dist/core/openapi/merge/pin.js +128 -0
- package/dist/core/openapi/merge/pin.js.map +1 -0
- package/dist/core/openspec/decide/capabilities.js +244 -0
- package/dist/core/openspec/decide/capabilities.js.map +1 -0
- package/dist/core/openspec/decide/dispositions.js +63 -0
- package/dist/core/openspec/decide/dispositions.js.map +1 -0
- package/dist/core/openspec/decide/renames.js +179 -0
- package/dist/core/openspec/decide/renames.js.map +1 -0
- package/dist/core/openspec/decide/verdict.js +84 -0
- package/dist/core/openspec/decide/verdict.js.map +1 -0
- package/dist/core/openspec/inventory.js +221 -0
- package/dist/core/openspec/inventory.js.map +1 -0
- package/dist/core/openspec/model/mapping.js +76 -0
- package/dist/core/openspec/model/mapping.js.map +1 -0
- package/dist/core/openspec/model/model.js +65 -0
- package/dist/core/openspec/model/model.js.map +1 -0
- package/dist/core/openspec/scan/changes.js +235 -0
- package/dist/core/openspec/scan/changes.js.map +1 -0
- package/dist/core/openspec/scan/config.js +200 -0
- package/dist/core/openspec/scan/config.js.map +1 -0
- package/dist/core/openspec/scan/shape.js +260 -0
- package/dist/core/openspec/scan/shape.js.map +1 -0
- package/dist/core/openspec/scan/walk.js +133 -0
- package/dist/core/openspec/scan/walk.js.map +1 -0
- package/dist/core/openspec/scan/workspace.js +71 -0
- package/dist/core/openspec/scan/workspace.js.map +1 -0
- package/dist/core/owners/codeowners.js +141 -0
- package/dist/core/owners/codeowners.js.map +1 -0
- package/dist/core/pack/features.js +120 -0
- package/dist/core/pack/features.js.map +1 -0
- package/dist/core/pack/joins.js +112 -0
- package/dist/core/pack/joins.js.map +1 -0
- package/dist/core/pack/living.js +118 -0
- package/dist/core/pack/living.js.map +1 -0
- package/dist/core/pack/pack.js +76 -0
- package/dist/core/pack/pack.js.map +1 -0
- package/dist/core/permissions/permissions.js +87 -0
- package/dist/core/permissions/permissions.js.map +1 -0
- package/dist/core/projection/api.js +84 -0
- package/dist/core/projection/api.js.map +1 -0
- package/dist/core/projection/arch-slice.js +63 -0
- package/dist/core/projection/arch-slice.js.map +1 -0
- package/dist/core/projection/events.js +34 -0
- package/dist/core/projection/events.js.map +1 -0
- package/dist/core/provenance/findings.js +208 -0
- package/dist/core/provenance/findings.js.map +1 -0
- package/dist/core/provenance/git.js +225 -0
- package/dist/core/provenance/git.js.map +1 -0
- package/dist/core/provenance/gitq/moves.js +182 -0
- package/dist/core/provenance/gitq/moves.js.map +1 -0
- package/dist/core/provenance/gitq/vouched-ancestor.js +179 -0
- package/dist/core/provenance/gitq/vouched-ancestor.js.map +1 -0
- package/dist/core/provenance/sample/scope.js +238 -0
- package/dist/core/provenance/sample/scope.js.map +1 -0
- package/dist/core/provenance/sample/sections.js +61 -0
- package/dist/core/provenance/sample/sections.js.map +1 -0
- package/dist/core/provenance/sources.js +263 -0
- package/dist/core/provenance/sources.js.map +1 -0
- package/dist/core/provenance/stamp.js +232 -0
- package/dist/core/provenance/stamp.js.map +1 -0
- package/dist/core/provenance/walk.js +157 -0
- package/dist/core/provenance/walk.js.map +1 -0
- package/dist/core/repo/authored/paths.js +192 -0
- package/dist/core/repo/authored/paths.js.map +1 -0
- package/dist/core/repo/entries.js +126 -0
- package/dist/core/repo/entries.js.map +1 -0
- package/dist/core/repo/paths.js +236 -0
- package/dist/core/repo/paths.js.map +1 -0
- package/dist/core/repo/repo.js +248 -0
- package/dist/core/repo/repo.js.map +1 -0
- package/dist/core/repo/service-target.js +153 -0
- package/dist/core/repo/service-target.js.map +1 -0
- package/dist/core/repo/state.js +58 -0
- package/dist/core/repo/state.js.map +1 -0
- package/dist/core/repo/tree/find.js +52 -0
- package/dist/core/repo/tree/find.js.map +1 -0
- package/dist/core/repo/tree/fs.js +64 -0
- package/dist/core/repo/tree/fs.js.map +1 -0
- package/dist/core/repo/tree/marker.js +61 -0
- package/dist/core/repo/tree/marker.js.map +1 -0
- package/dist/core/repo/tree/views.js +172 -0
- package/dist/core/repo/tree/views.js.map +1 -0
- package/dist/core/repo/tree/walk.js +234 -0
- package/dist/core/repo/tree/walk.js.map +1 -0
- package/dist/core/results.js +99 -2
- package/dist/core/results.js.map +1 -1
- package/dist/core/scaffold/landscape.js +131 -0
- package/dist/core/scaffold/landscape.js.map +1 -0
- package/dist/core/scaffold/readme.js +84 -0
- package/dist/core/scaffold/readme.js.map +1 -0
- package/dist/core/staging/commit.js +241 -0
- package/dist/core/staging/commit.js.map +1 -0
- package/dist/core/staging/interrupted.js +96 -0
- package/dist/core/staging/interrupted.js.map +1 -0
- package/dist/core/staging/lock.js +206 -0
- package/dist/core/staging/lock.js.map +1 -0
- package/dist/core/staging/recovery/finding.js +35 -0
- package/dist/core/staging/recovery/finding.js.map +1 -0
- package/dist/core/staging/recovery/intent.js +98 -0
- package/dist/core/staging/recovery/intent.js.map +1 -0
- package/dist/core/staging/recovery/recover.js +126 -0
- package/dist/core/staging/recovery/recover.js.map +1 -0
- package/dist/core/staging/recovery/residue.js +62 -0
- package/dist/core/staging/recovery/residue.js.map +1 -0
- package/dist/core/staging/snapshot.js +202 -0
- package/dist/core/staging/snapshot.js.map +1 -0
- package/dist/core/staging/txn/forward.js +171 -0
- package/dist/core/staging/txn/forward.js.map +1 -0
- package/dist/core/staging/txn/journal.js +139 -0
- package/dist/core/staging/txn/journal.js.map +1 -0
- package/dist/core/staging/txn/transaction.js +75 -0
- package/dist/core/staging/txn/transaction.js.map +1 -0
- package/dist/core/staging/writes.js +113 -0
- package/dist/core/staging/writes.js.map +1 -0
- package/dist/core/status/actions/execution.js +49 -0
- package/dist/core/status/actions/execution.js.map +1 -0
- package/dist/core/status/contracts.js +102 -0
- package/dist/core/status/contracts.js.map +1 -0
- package/dist/core/status/feature/artifacts.js +171 -0
- package/dist/core/status/feature/artifacts.js.map +1 -0
- package/dist/core/status/feature/feature.js +207 -0
- package/dist/core/status/feature/feature.js.map +1 -0
- package/dist/core/status/feature/next.js +150 -0
- package/dist/core/status/feature/next.js.map +1 -0
- package/dist/core/status/feature/state.js +21 -0
- package/dist/core/status/feature/state.js.map +1 -0
- package/dist/core/status/fleet/fleet.js +187 -0
- package/dist/core/status/fleet/fleet.js.map +1 -0
- package/dist/core/status/fleet/next.js +207 -0
- package/dist/core/status/fleet/next.js.map +1 -0
- package/dist/core/status/interrupted.js +97 -0
- package/dist/core/status/interrupted.js.map +1 -0
- package/dist/core/status/report.js +31 -0
- package/dist/core/status/report.js.map +1 -0
- package/dist/core/status/scan.js +41 -0
- package/dist/core/status/scan.js.map +1 -0
- package/dist/core/status/verification.js +201 -0
- package/dist/core/status/verification.js.map +1 -0
- package/dist/core/usecases/capability.js +169 -0
- package/dist/core/usecases/capability.js.map +1 -0
- package/dist/core/usecases/delta/claims.js +116 -0
- package/dist/core/usecases/delta/claims.js.map +1 -0
- package/dist/core/usecases/delta/flows.js +103 -0
- package/dist/core/usecases/delta/flows.js.map +1 -0
- package/dist/core/usecases/delta/overlay.js +235 -0
- package/dist/core/usecases/delta/overlay.js.map +1 -0
- package/dist/core/usecases/fleet.js +137 -0
- package/dist/core/usecases/fleet.js.map +1 -0
- package/dist/core/usecases/operations.js +111 -0
- package/dist/core/usecases/operations.js.map +1 -0
- package/dist/core/usecases/place.js +44 -0
- package/dist/core/usecases/place.js.map +1 -0
- package/dist/core/usecases/touch.js +101 -0
- package/dist/core/usecases/touch.js.map +1 -0
- package/dist/core/verify/answers.js +173 -0
- package/dist/core/verify/answers.js.map +1 -0
- package/dist/core/verify/build.js +150 -0
- package/dist/core/verify/build.js.map +1 -0
- package/dist/core/verify/checklist.js +201 -0
- package/dist/core/verify/checklist.js.map +1 -0
- package/dist/core/verify/claims/identity.js +61 -0
- package/dist/core/verify/claims/identity.js.map +1 -0
- package/dist/core/verify/cross/diff.js +97 -0
- package/dist/core/verify/cross/diff.js.map +1 -0
- package/dist/core/verify/evidence/contract.js +169 -0
- package/dist/core/verify/evidence/contract.js.map +1 -0
- package/dist/core/verify/file.js +257 -0
- package/dist/core/verify/file.js.map +1 -0
- package/dist/core/verify/pins/lint.js +157 -0
- package/dist/core/verify/pins/lint.js.map +1 -0
- package/dist/core/verify/pins/pin.js +95 -0
- package/dist/core/verify/pins/pin.js.map +1 -0
- package/dist/core/verify/record.js +142 -0
- package/dist/core/verify/record.js.map +1 -0
- package/dist/core/verify/store/commit.js +89 -0
- package/dist/core/verify/store/commit.js.map +1 -0
- package/dist/core/verify/store/render.js +57 -0
- package/dist/core/verify/store/render.js.map +1 -0
- package/dist/core/vocabulary/health.js +91 -0
- package/dist/core/vocabulary/health.js.map +1 -0
- package/dist/core/vocabulary/issue.js +55 -0
- package/dist/core/vocabulary/issue.js.map +1 -0
- package/dist/core/vocabulary/maturity.js +145 -0
- package/dist/core/vocabulary/maturity.js.map +1 -0
- package/dist/core/{report.js → vocabulary/report.js} +30 -2
- package/dist/core/vocabulary/report.js.map +1 -0
- package/dist/core/vocabulary/steps.js +266 -0
- package/dist/core/vocabulary/steps.js.map +1 -0
- package/dist/core/workspace/discover.js +239 -0
- package/dist/core/workspace/discover.js.map +1 -0
- package/dist/core/workspace/render.js +52 -0
- package/dist/core/workspace/render.js.map +1 -0
- package/examples/README.md +189 -0
- package/examples/docs/architecture/adrs/0001-transactional-outbox.md +33 -0
- package/examples/docs/architecture/capabilities.yaml +31 -0
- package/examples/docs/architecture/landscape.likec4 +280 -0
- package/examples/docs/architecture/obligations.yaml +23 -0
- package/examples/docs/architecture/permissions.yaml +50 -0
- package/examples/docs/architecture/subsystems.likec4 +11 -0
- package/examples/docs/architecture/usecases/order-notification.likec4 +65 -0
- package/examples/docs/capabilities/checkout/spec.md +55 -0
- package/examples/docs/capabilities/identity/tokens/spec.md +42 -0
- package/examples/docs/capabilities/order-notifications/spec.md +43 -0
- package/examples/docs/features/FEAT-101-payment-splitting/adrs/0001-dedicated-split-service.md +18 -0
- package/examples/docs/features/FEAT-101-payment-splitting/delta.likec4 +94 -0
- package/examples/docs/features/FEAT-101-payment-splitting/intent.md +44 -0
- package/examples/docs/features/FEAT-101-payment-splitting/specs/checkout-web/spec.md +21 -0
- package/examples/docs/features/FEAT-101-payment-splitting/specs/payment-service/asyncapi.yaml +129 -0
- package/examples/docs/features/FEAT-101-payment-splitting/specs/payment-service/spec.md +36 -0
- package/examples/docs/features/FEAT-101-payment-splitting/specs/payment-split-service/arch.spec.md +40 -0
- package/examples/docs/features/FEAT-101-payment-splitting/specs/payment-split-service/openapi.yaml +116 -0
- package/examples/docs/features/FEAT-101-payment-splitting/specs/payment-split-service/spec.md +55 -0
- package/examples/docs/features/FEAT-112-retire-order-v1/intent.md +35 -0
- package/examples/docs/features/FEAT-112-retire-order-v1/specs/order-service/openapi.yaml +28 -0
- package/examples/docs/features/FEAT-112-retire-order-v1/specs/order-service/spec.md +17 -0
- package/examples/docs/features/archive/FEAT-088-refunds/.loam-before/files/architecture/landscape.likec4 +253 -0
- package/examples/docs/features/archive/FEAT-088-refunds/.loam-before/files/services/order-service/spec.md +121 -0
- package/examples/docs/features/archive/FEAT-088-refunds/.loam-before/files/services/payment-service/openapi.yaml +111 -0
- package/examples/docs/features/archive/FEAT-088-refunds/.loam-before/files/services/payment-service/spec.md +58 -0
- package/examples/docs/features/archive/FEAT-088-refunds/.loam-before/manifest.json +32 -0
- package/examples/docs/features/archive/FEAT-088-refunds/adrs/0001-refunds-stay-in-payment-service.md +26 -0
- package/examples/docs/features/archive/FEAT-088-refunds/delta.likec4 +28 -0
- package/examples/docs/features/archive/FEAT-088-refunds/intent.md +26 -0
- package/examples/docs/features/archive/FEAT-088-refunds/specs/order-service/spec.md +37 -0
- package/examples/docs/features/archive/FEAT-088-refunds/specs/payment-service/openapi.yaml +70 -0
- package/examples/docs/features/archive/FEAT-088-refunds/specs/payment-service/spec.md +23 -0
- package/examples/docs/features/archive/FEAT-088-refunds/verification.yaml +76 -0
- package/examples/docs/features/archive/FEAT-120-refund-notification/.loam-before/files/architecture/landscape.likec4 +275 -0
- package/examples/docs/features/archive/FEAT-120-refund-notification/.loam-before/files/services/payment-service/asyncapi.yaml +96 -0
- package/examples/docs/features/archive/FEAT-120-refund-notification/.loam-before/files/services/payment-service/spec.md +82 -0
- package/examples/docs/features/archive/FEAT-120-refund-notification/.loam-before/files/services/platform/notification-service/asyncapi.yaml +164 -0
- package/examples/docs/features/archive/FEAT-120-refund-notification/.loam-before/files/services/platform/notification-service/spec.md +91 -0
- package/examples/docs/features/archive/FEAT-120-refund-notification/.loam-before/manifest.json +46 -0
- package/examples/docs/features/archive/FEAT-120-refund-notification/delta.likec4 +36 -0
- package/examples/docs/features/archive/FEAT-120-refund-notification/intent.md +22 -0
- package/examples/docs/features/archive/FEAT-120-refund-notification/scenario-report.json +10 -0
- package/examples/docs/features/archive/FEAT-120-refund-notification/specs/notification-service/asyncapi.yaml +208 -0
- package/examples/docs/features/archive/FEAT-120-refund-notification/specs/notification-service/spec.md +24 -0
- package/examples/docs/features/archive/FEAT-120-refund-notification/specs/payment-service/asyncapi.yaml +137 -0
- package/examples/docs/features/archive/FEAT-120-refund-notification/specs/payment-service/spec.md +31 -0
- package/examples/docs/features/archive/FEAT-120-refund-notification/verification.yaml +84 -0
- package/examples/docs/glossary/order.md +15 -0
- package/examples/docs/glossary/payments/authorization.md +14 -0
- package/examples/docs/likec4.config.json +9 -0
- package/examples/docs/loam.json +3 -0
- package/examples/docs/services/checkout-web/model.likec4 +109 -0
- package/examples/docs/services/checkout-web/spec.md +91 -0
- package/examples/docs/services/checkout-web/ui/pages/checkout.page.yaml +40 -0
- package/examples/docs/services/checkout-web/ui/pages/order-status.page.yaml +24 -0
- package/examples/docs/services/checkout-web/ui/pages/split-payment.page.yaml +26 -0
- package/examples/docs/services/order-service/adrs/0001-orchestrated-order-lifecycle.md +52 -0
- package/examples/docs/services/order-service/arch.spec.md +127 -0
- package/examples/docs/services/order-service/asyncapi.yaml +108 -0
- package/examples/docs/services/order-service/health.yaml +57 -0
- package/examples/docs/services/order-service/model.likec4 +138 -0
- package/examples/docs/services/order-service/openapi.yaml +257 -0
- package/examples/docs/services/order-service/runbook.md +62 -0
- package/examples/docs/services/order-service/spec.md +134 -0
- package/examples/docs/services/payment-service/adrs/0001-payments-outbox.md +18 -0
- package/examples/docs/services/payment-service/adrs/0002-idempotency-keys-on-authorization.md +27 -0
- package/examples/docs/services/payment-service/arch.spec.md +73 -0
- package/examples/docs/services/payment-service/asyncapi.yaml +130 -0
- package/examples/docs/services/payment-service/health.yaml +39 -0
- package/examples/docs/services/payment-service/model.likec4 +67 -0
- package/examples/docs/services/payment-service/openapi.yaml +151 -0
- package/examples/docs/services/payment-service/runbook.md +37 -0
- package/examples/docs/services/payment-service/spec.md +88 -0
- package/examples/docs/services/platform/identity-service/adrs/0001-token-introspection-over-local-validation.md +60 -0
- package/examples/docs/services/platform/identity-service/arch.spec.md +117 -0
- package/examples/docs/services/platform/identity-service/health.yaml +70 -0
- package/examples/docs/services/platform/identity-service/model.likec4 +83 -0
- package/examples/docs/services/platform/identity-service/openapi.yaml +197 -0
- package/examples/docs/services/platform/identity-service/runbook.md +95 -0
- package/examples/docs/services/platform/identity-service/spec.md +119 -0
- package/examples/docs/services/platform/notification-service/asyncapi.yaml +199 -0
- package/examples/docs/services/platform/notification-service/health.yaml +35 -0
- package/examples/docs/services/platform/notification-service/model.likec4 +118 -0
- package/examples/docs/services/platform/notification-service/runbook.md +48 -0
- package/examples/docs/services/platform/notification-service/spec.md +112 -0
- package/examples/docs/services/platform/subsystem.yaml +2 -0
- package/examples/fleet.yaml +57 -0
- package/package.json +19 -6
- package/dist/commands/adopt.js +0 -240
- package/dist/commands/adopt.js.map +0 -1
- package/dist/commands/archive.js +0 -1313
- package/dist/commands/archive.js.map +0 -1
- package/dist/commands/delta.js +0 -388
- package/dist/commands/delta.js.map +0 -1
- package/dist/commands/docs-repo-gate.js.map +0 -1
- package/dist/commands/format.js +0 -17
- package/dist/commands/format.js.map +0 -1
- package/dist/commands/gherkin.js +0 -309
- package/dist/commands/gherkin.js.map +0 -1
- package/dist/commands/init.js.map +0 -1
- package/dist/commands/list.js +0 -295
- package/dist/commands/list.js.map +0 -1
- package/dist/commands/migrate-openspec.js +0 -950
- package/dist/commands/migrate-openspec.js.map +0 -1
- package/dist/commands/new.js +0 -413
- package/dist/commands/new.js.map +0 -1
- package/dist/commands/rebase.js +0 -356
- package/dist/commands/rebase.js.map +0 -1
- package/dist/commands/show.js.map +0 -1
- package/dist/commands/status.js +0 -262
- package/dist/commands/status.js.map +0 -1
- package/dist/commands/unarchive.js.map +0 -1
- package/dist/commands/validate.js +0 -1773
- package/dist/commands/validate.js.map +0 -1
- package/dist/commands/verify.js +0 -775
- package/dist/commands/verify.js.map +0 -1
- package/dist/commands/vouch.js +0 -421
- package/dist/commands/vouch.js.map +0 -1
- package/dist/core/agent.js +0 -2173
- package/dist/core/agent.js.map +0 -1
- package/dist/core/agents-stamp.js.map +0 -1
- package/dist/core/arch.js +0 -130
- package/dist/core/arch.js.map +0 -1
- package/dist/core/asyncapi.js.map +0 -1
- package/dist/core/brief.js +0 -755
- package/dist/core/brief.js.map +0 -1
- package/dist/core/coherence.js +0 -577
- package/dist/core/coherence.js.map +0 -1
- package/dist/core/concurrency.js.map +0 -1
- package/dist/core/config.js.map +0 -1
- package/dist/core/delta.js +0 -476
- package/dist/core/delta.js.map +0 -1
- package/dist/core/dependencies.js +0 -356
- package/dist/core/dependencies.js.map +0 -1
- package/dist/core/doctor.js +0 -596
- package/dist/core/doctor.js.map +0 -1
- package/dist/core/document-bytes.js.map +0 -1
- package/dist/core/explore.js.map +0 -1
- package/dist/core/frontmatter.js.map +0 -1
- package/dist/core/gherkin.js +0 -463
- package/dist/core/gherkin.js.map +0 -1
- package/dist/core/health.js +0 -67
- package/dist/core/health.js.map +0 -1
- package/dist/core/ids.js +0 -122
- package/dist/core/ids.js.map +0 -1
- package/dist/core/issue.js +0 -9
- package/dist/core/issue.js.map +0 -1
- package/dist/core/json.js +0 -74
- package/dist/core/json.js.map +0 -1
- package/dist/core/likec4.js +0 -489
- package/dist/core/likec4.js.map +0 -1
- package/dist/core/maturity.js +0 -66
- package/dist/core/maturity.js.map +0 -1
- package/dist/core/openapi-merge.js +0 -552
- package/dist/core/openapi-merge.js.map +0 -1
- package/dist/core/openapi.js.map +0 -1
- package/dist/core/openspec-inventory.js +0 -1440
- package/dist/core/openspec-inventory.js.map +0 -1
- package/dist/core/path-safety.js.map +0 -1
- package/dist/core/provenance.js +0 -787
- package/dist/core/provenance.js.map +0 -1
- package/dist/core/records.js.map +0 -1
- package/dist/core/repo.js +0 -438
- package/dist/core/repo.js.map +0 -1
- package/dist/core/report.js.map +0 -1
- package/dist/core/spec.js +0 -505
- package/dist/core/spec.js.map +0 -1
- package/dist/core/staging.js +0 -890
- package/dist/core/staging.js.map +0 -1
- package/dist/core/status.js +0 -1132
- package/dist/core/status.js.map +0 -1
- package/dist/core/steps.js +0 -72
- package/dist/core/steps.js.map +0 -1
- package/dist/core/verify.js +0 -807
- package/dist/core/verify.js.map +0 -1
- package/dist/core/version.js.map +0 -1
- /package/dist/core/{concurrency.js → kernel/concurrency.js} +0 -0
- /package/dist/core/{records.js → kernel/records.js} +0 -0
|
@@ -0,0 +1,221 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* What a requirement's scenarios are MISSING, and where they contradict
|
|
3
|
+
* themselves — the coverage and shape questions `validate` asks of every spec.
|
|
4
|
+
*
|
|
5
|
+
* Separate from `./parse.ts` because these grade a parsed document rather than
|
|
6
|
+
* produce one, and separate from `./spec.ts` because they answer to
|
|
7
|
+
* `vocabulary/report.ts`: a stepless scenario is a Finding with a subject and a
|
|
8
|
+
* label, and that shape belongs to the report, not to the requirement.
|
|
9
|
+
*/
|
|
10
|
+
import { scenarioGherkin } from "../vocabulary/steps.js";
|
|
11
|
+
import {} from "./spec.js";
|
|
12
|
+
/** Requirements with no scenario — the OpenSpec coverage rule (every requirement needs ≥1). */
|
|
13
|
+
export function requirementsMissingScenarios(reqs) {
|
|
14
|
+
return reqs.filter((r) => r.kind !== "REMOVED" && r.scenarios.length === 0);
|
|
15
|
+
}
|
|
16
|
+
/**
|
|
17
|
+
* Scenarios that satisfy the coverage rule above and test nothing.
|
|
18
|
+
*
|
|
19
|
+
* `requirementsMissingScenarios` counts HEADINGS, so a `#### Scenario:` with no
|
|
20
|
+
* recognizable Given/When/Then line satisfied the fleet gate's coverage claim
|
|
21
|
+
* outright — and it is not a hypothetical shape: it is what `loam new`
|
|
22
|
+
* scaffolds and what an agent writes when it puts the criteria in prose.
|
|
23
|
+
* Downstream the same emptiness is fatal twice over: cucumber runs a stepless
|
|
24
|
+
* scenario vacuously green, and `loam verify --results` can never confirm it.
|
|
25
|
+
*
|
|
26
|
+
* The detector is `loam gherkin`'s own — `scenarioGherkin` decides what a step
|
|
27
|
+
* IS, and a second opinion here would mean the gate and the emitted suite could
|
|
28
|
+
* disagree about whether a scenario has any. That import is the one place these
|
|
29
|
+
* two modules are circular (gherkin.ts parses requirements); both sides export
|
|
30
|
+
* only function declarations, which are hoisted, so neither evaluation order
|
|
31
|
+
* can observe a half-built module.
|
|
32
|
+
*/
|
|
33
|
+
export function steplessScenarios(reqs) {
|
|
34
|
+
const out = [];
|
|
35
|
+
for (const r of reqs) {
|
|
36
|
+
if (r.kind === "REMOVED")
|
|
37
|
+
continue;
|
|
38
|
+
for (const s of r.scenarios) {
|
|
39
|
+
if (scenarioGherkin(s.lines).steps.length === 0)
|
|
40
|
+
out.push({ requirement: r.name, scenario: s.name });
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
return out;
|
|
44
|
+
}
|
|
45
|
+
/**
|
|
46
|
+
* The stepless half of the coverage check, as findings — the missing-scenario
|
|
47
|
+
* half stays where it is (`requirementsMissingScenarios`), because the two are
|
|
48
|
+
* different states with different fixes: one requirement owes acceptance
|
|
49
|
+
* criteria, the other owes STEPS in criteria somebody has already written.
|
|
50
|
+
*
|
|
51
|
+
* `label` is the coverage check's own label (`payment-service: requirements`),
|
|
52
|
+
* so both halves of one document name it the same way.
|
|
53
|
+
*/
|
|
54
|
+
export function steplessFindings(label, subject, reqs) {
|
|
55
|
+
const bare = steplessScenarios(reqs);
|
|
56
|
+
if (bare.length === 0)
|
|
57
|
+
return [];
|
|
58
|
+
return [
|
|
59
|
+
{
|
|
60
|
+
severity: "error",
|
|
61
|
+
code: "requirements.stepless-scenario",
|
|
62
|
+
subject,
|
|
63
|
+
message: `${label}: ${bare.length} scenario(s) have a heading and no recognizable steps — ` +
|
|
64
|
+
`they satisfy the coverage rule and test nothing. Cucumber runs a stepless scenario vacuously green and ` +
|
|
65
|
+
`\`loam verify --results\` can never confirm it. Write the body as \`- **Given/When/Then**\` bullets.`,
|
|
66
|
+
details: bare.map((s) => `${s.requirement} — ${s.scenario}`),
|
|
67
|
+
text: { detailPrefix: "- " },
|
|
68
|
+
},
|
|
69
|
+
];
|
|
70
|
+
}
|
|
71
|
+
/**
|
|
72
|
+
* Scenarios that have steps and assert NOTHING — a body of Given and When with
|
|
73
|
+
* no Then anywhere in it.
|
|
74
|
+
*
|
|
75
|
+
* The third state, and the one that survives both checks above: the requirement
|
|
76
|
+
* has a scenario, the scenario has steps, and the run can only ever confirm that
|
|
77
|
+
* the arrangement did not throw. `And`/`But` are continuations, so their meaning
|
|
78
|
+
* is decided by the keyword they follow — with no `Then` in the body there is
|
|
79
|
+
* nothing for them to continue but the setup.
|
|
80
|
+
*
|
|
81
|
+
* It matters more the more evidence rests on the suite. A stepless scenario is
|
|
82
|
+
* at least visible as an empty body; this one reads like a real test, emits real
|
|
83
|
+
* steps, runs green, and answers a `scenario.tested` claim to the same standard
|
|
84
|
+
* as a scenario that checks twenty things. Under a runner-neutral answer sheet —
|
|
85
|
+
* one `{digest, status}` entry, no steps to count — this document-side check is
|
|
86
|
+
* the only thing left that can tell the two apart.
|
|
87
|
+
*/
|
|
88
|
+
export function assertionlessScenarios(reqs) {
|
|
89
|
+
const out = [];
|
|
90
|
+
for (const r of reqs) {
|
|
91
|
+
if (r.kind === "REMOVED")
|
|
92
|
+
continue;
|
|
93
|
+
for (const s of r.scenarios) {
|
|
94
|
+
const { steps } = scenarioGherkin(s.lines);
|
|
95
|
+
// Disjoint from stepless by construction: a body with no steps at all is
|
|
96
|
+
// that finding's, and reporting one scenario under both codes would offer
|
|
97
|
+
// two fixes for one defect.
|
|
98
|
+
if (steps.length > 0 && !steps.some((st) => /^Then\b/.test(st.text))) {
|
|
99
|
+
out.push({ requirement: r.name, scenario: s.name });
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
return out;
|
|
104
|
+
}
|
|
105
|
+
const PLACEHOLDER_RE = /<([^<>\s]+)>/g;
|
|
106
|
+
/**
|
|
107
|
+
* Every `<placeholder>` a scenario's steps reach for, arguments included.
|
|
108
|
+
*
|
|
109
|
+
* Docstrings and data tables are scanned too because cucumber substitutes into
|
|
110
|
+
* them: `<fee>` inside a request payload is a real reference, and a check that
|
|
111
|
+
* read only the step lines would call its column unreferenced and invite the
|
|
112
|
+
* author to delete the column the payload depends on.
|
|
113
|
+
*/
|
|
114
|
+
function placeholdersUsed(steps) {
|
|
115
|
+
const used = new Set();
|
|
116
|
+
const scan = (text) => {
|
|
117
|
+
for (const m of text.matchAll(PLACEHOLDER_RE))
|
|
118
|
+
used.add(m[1]);
|
|
119
|
+
};
|
|
120
|
+
for (const st of steps) {
|
|
121
|
+
scan(st.text);
|
|
122
|
+
for (const l of st.docstring?.lines ?? [])
|
|
123
|
+
scan(l);
|
|
124
|
+
for (const row of st.table ?? [])
|
|
125
|
+
for (const cell of row)
|
|
126
|
+
scan(cell);
|
|
127
|
+
}
|
|
128
|
+
return used;
|
|
129
|
+
}
|
|
130
|
+
/**
|
|
131
|
+
* Scenarios whose `Examples` header and `<placeholders>` disagree.
|
|
132
|
+
*
|
|
133
|
+
* Only scenarios that HAVE an Examples table are considered, and that scoping is
|
|
134
|
+
* load-bearing twice over: a plain scenario's `<angle brackets>` are prose (a
|
|
135
|
+
* generic, a placeholder in a URL template, loam's own scaffold sentinels), and
|
|
136
|
+
* a fleet that never writes an outline must never earn a finding from this.
|
|
137
|
+
*
|
|
138
|
+
* The two halves are different defects. An UNBOUND placeholder is an error the
|
|
139
|
+
* runner cannot repair: cucumber substitutes nothing, so the step definition
|
|
140
|
+
* receives the literal text `<fee>` and either fails obscurely or — worse —
|
|
141
|
+
* matches and asserts against a string nobody meant. An UNREFERENCED column is a
|
|
142
|
+
* warning, because it is usually the fossil of a rename: the column still lists
|
|
143
|
+
* six cases, the step that used it now names something else, and the outline
|
|
144
|
+
* runs six identical passes that read as six cases of coverage.
|
|
145
|
+
*/
|
|
146
|
+
export function examplesMismatches(reqs) {
|
|
147
|
+
const out = [];
|
|
148
|
+
for (const r of reqs) {
|
|
149
|
+
if (r.kind === "REMOVED")
|
|
150
|
+
continue;
|
|
151
|
+
for (const s of r.scenarios) {
|
|
152
|
+
const { steps, examples } = scenarioGherkin(s.lines);
|
|
153
|
+
if (examples === null)
|
|
154
|
+
continue;
|
|
155
|
+
const used = placeholdersUsed(steps);
|
|
156
|
+
const columns = new Set(examples.header);
|
|
157
|
+
const unbound = [...used].filter((p) => !columns.has(p)).sort();
|
|
158
|
+
const unreferenced = examples.header.filter((c) => !used.has(c));
|
|
159
|
+
if (unbound.length > 0 || unreferenced.length > 0) {
|
|
160
|
+
out.push({ requirement: r.name, scenario: s.name, unbound, unreferenced });
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
return out;
|
|
165
|
+
}
|
|
166
|
+
/**
|
|
167
|
+
* The Examples half, as findings — two codes rather than one with two
|
|
168
|
+
* severities, because a machine cannot branch on a severity that depends on the
|
|
169
|
+
* case, and the fixes differ: one adds a column, the other removes one or
|
|
170
|
+
* restores the reference that used to use it.
|
|
171
|
+
*/
|
|
172
|
+
export function examplesFindings(label, subject, reqs) {
|
|
173
|
+
const all = examplesMismatches(reqs);
|
|
174
|
+
const findings = [];
|
|
175
|
+
const unbound = all.filter((m) => m.unbound.length > 0);
|
|
176
|
+
if (unbound.length > 0) {
|
|
177
|
+
findings.push({
|
|
178
|
+
severity: "error",
|
|
179
|
+
code: "requirements.examples-unbound",
|
|
180
|
+
subject,
|
|
181
|
+
message: `${label}: ${unbound.length} scenario(s) use a \`<placeholder>\` no \`Examples\` column supplies — ` +
|
|
182
|
+
`cucumber substitutes nothing, so the step receives the literal angle-bracket text. ` +
|
|
183
|
+
`Add the column, or correct the placeholder to one the header names.`,
|
|
184
|
+
details: unbound.map((m) => `${m.requirement} — ${m.scenario}: ${m.unbound.map((p) => `<${p}>`).join(", ")}`),
|
|
185
|
+
text: { detailPrefix: "- " },
|
|
186
|
+
});
|
|
187
|
+
}
|
|
188
|
+
const dead = all.filter((m) => m.unreferenced.length > 0);
|
|
189
|
+
if (dead.length > 0) {
|
|
190
|
+
findings.push({
|
|
191
|
+
severity: "warn",
|
|
192
|
+
code: "requirements.examples-unreferenced",
|
|
193
|
+
subject,
|
|
194
|
+
message: `${label}: ${dead.length} scenario(s) declare an \`Examples\` column no step refers to — ` +
|
|
195
|
+
`usually the fossil of a rename, and the outline then runs its rows as identical passes that read as cases. ` +
|
|
196
|
+
`Reference the column from a step, or drop it.`,
|
|
197
|
+
details: dead.map((m) => `${m.requirement} — ${m.scenario}: ${m.unreferenced.join(", ")}`),
|
|
198
|
+
text: { detailPrefix: "- " },
|
|
199
|
+
});
|
|
200
|
+
}
|
|
201
|
+
return findings;
|
|
202
|
+
}
|
|
203
|
+
/** The assertionless half, as findings — same label and subject as its two siblings. */
|
|
204
|
+
export function assertionlessFindings(label, subject, reqs) {
|
|
205
|
+
const mute = assertionlessScenarios(reqs);
|
|
206
|
+
if (mute.length === 0)
|
|
207
|
+
return [];
|
|
208
|
+
return [
|
|
209
|
+
{
|
|
210
|
+
severity: "error",
|
|
211
|
+
code: "requirements.assertionless-scenario",
|
|
212
|
+
subject,
|
|
213
|
+
message: `${label}: ${mute.length} scenario(s) have steps and no \`Then\` — ` +
|
|
214
|
+
`they arrange and act, and assert nothing. The run confirms only that the setup did not throw, ` +
|
|
215
|
+
`and the claim it answers is confirmed on that. Add the \`- **Then**\` the scenario was written for.`,
|
|
216
|
+
details: mute.map((s) => `${s.requirement} — ${s.scenario}`),
|
|
217
|
+
text: { detailPrefix: "- " },
|
|
218
|
+
},
|
|
219
|
+
];
|
|
220
|
+
}
|
|
221
|
+
//# sourceMappingURL=scenarios.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"scenarios.js","sourceRoot":"","sources":["../../../src/core/document/scenarios.ts"],"names":[],"mappings":"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","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"]}
|
|
@@ -0,0 +1,241 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* What a requirement IS, what makes one the same requirement twice, and the one
|
|
3
|
+
* canonical way it is written back out.
|
|
4
|
+
*
|
|
5
|
+
* Living specs use `## Requirements` with `### Requirement:` + `#### Scenario:`.
|
|
6
|
+
* Feature deltas group requirements under `## ADDED|MODIFIED|REMOVED
|
|
7
|
+
* Requirements`. Scenarios are the acceptance criteria (Given/When/Then) and
|
|
8
|
+
* the source for tests.
|
|
9
|
+
*
|
|
10
|
+
* Identity and serialization cannot be separated here: `requirementDigest` is
|
|
11
|
+
* defined AS the sha256 of the serialized requirement without its baseline, so
|
|
12
|
+
* a second module owning the writer would mean the digest and the bytes it
|
|
13
|
+
* claims to describe could disagree. `./parse.ts` turns markdown into these,
|
|
14
|
+
* `./apply.ts` folds a delta into a living set of them.
|
|
15
|
+
*/
|
|
16
|
+
import { createHash } from "node:crypto";
|
|
17
|
+
export const KIND_RE = /^##\s+(ADDED|MODIFIED|REMOVED)\s+Requirements\s*$/i;
|
|
18
|
+
export const REQUIREMENT_ID_LINE_RE = /^\s*Requirement-ID:\s*(.*?)\s*$/i;
|
|
19
|
+
export const BASED_ON_LINE_RE = /^\s*Based-On:\s*(.*?)\s*$/i;
|
|
20
|
+
/**
|
|
21
|
+
* The `Capability:` body line (also accepted: `Capabilities:`), exactly as the
|
|
22
|
+
* parser reads it — `./parse.ts` consumes the capture, migrate-openspec's
|
|
23
|
+
* materializer tests presence with it before appending its own line. One
|
|
24
|
+
* spelling on purpose: two copies of a grammar is how a "presence" test and
|
|
25
|
+
* the parse quietly stop agreeing about what counts as the line.
|
|
26
|
+
*/
|
|
27
|
+
export const CAPABILITY_LINE_RE = /^\s*Capabilit(?:y|ies):\s*(.+?)\s*$/i;
|
|
28
|
+
/**
|
|
29
|
+
* The `Realizes:` body line. One spelling only — no `Realize:` singular
|
|
30
|
+
* alternative, unlike `Operations?:` and the rest — because the verb already
|
|
31
|
+
* reads correctly for one entry and for six, and the plural forms in the older
|
|
32
|
+
* lines exist to forgive a NOUN that changes shape. A grammar with an
|
|
33
|
+
* alternative nobody needs is one more thing two readers can disagree about.
|
|
34
|
+
*/
|
|
35
|
+
export const REALIZES_LINE_RE = /^\s*Realizes:\s*(.+?)\s*$/i;
|
|
36
|
+
/**
|
|
37
|
+
* The separator between a `Realizes:` target and the digest of the capability
|
|
38
|
+
* requirement it was written against — `checkout#CHK-1@9f2c1a4b`.
|
|
39
|
+
*
|
|
40
|
+
* `@` and not a second `#`, because the two halves are read by different rules:
|
|
41
|
+
* `splitRealizesEntry` takes the LAST `#` to find the requirement half, so a
|
|
42
|
+
* second one would move that boundary and re-parse every existing entry. `@`
|
|
43
|
+
* is also excluded from `REQUIREMENT_ID_RE`, which is what makes the split
|
|
44
|
+
* unambiguous rather than a convention — an id can never contain one.
|
|
45
|
+
*
|
|
46
|
+
* NOBODY TYPES THIS. `loam rebase --living` writes it and rewrites it; a human
|
|
47
|
+
* writes `Realizes: checkout#CHK-1` and is never asked for more. An entry
|
|
48
|
+
* without a pin is not a defect and never becomes one — it is a claim that has
|
|
49
|
+
* not been pinned yet, and it grades exactly as it did before pins existed.
|
|
50
|
+
*/
|
|
51
|
+
export const REALIZES_PIN_SEPARATOR = "@";
|
|
52
|
+
/**
|
|
53
|
+
* One `Realizes:` entry split into the target it names and the digest it was
|
|
54
|
+
* written against, or `pin: null` when it carries none.
|
|
55
|
+
*
|
|
56
|
+
* A suffix that is not a well-formed digest is NOT a pin and NOT a refusal:
|
|
57
|
+
* the whole entry stays the target, so `checkout#CHK-1@nonsense` resolves — or
|
|
58
|
+
* fails to resolve — exactly as it would have before, under
|
|
59
|
+
* `capability.realizes-unknown`, whose message names the entry the author
|
|
60
|
+
* actually typed. Inventing a second refusal for one mistake is the failure
|
|
61
|
+
* `splitRealizesEntry` already refuses two lines up.
|
|
62
|
+
*/
|
|
63
|
+
export function splitRealizesPin(entry) {
|
|
64
|
+
const at = entry.lastIndexOf(REALIZES_PIN_SEPARATOR);
|
|
65
|
+
if (at <= 0)
|
|
66
|
+
return { target: entry, pin: null };
|
|
67
|
+
const suffix = entry.slice(at + 1);
|
|
68
|
+
if (!REQUIREMENT_DIGEST_RE.test(suffix))
|
|
69
|
+
return { target: entry, pin: null };
|
|
70
|
+
return { target: entry.slice(0, at), pin: suffix };
|
|
71
|
+
}
|
|
72
|
+
/** Portable, review-friendly stable IDs. Case-sensitive by design. */
|
|
73
|
+
export const REQUIREMENT_ID_RE = /^[A-Za-z][A-Za-z0-9._-]{0,127}$/;
|
|
74
|
+
/**
|
|
75
|
+
* How much of the sha256 a `Based-On:` line carries — the length
|
|
76
|
+
* `sources_digest` already uses (core/provenance/stamp.ts), for one reason a reader
|
|
77
|
+
* can hold: every digest loam writes into a document reads the same.
|
|
78
|
+
*/
|
|
79
|
+
export const REQUIREMENT_DIGEST_LENGTH = 16;
|
|
80
|
+
/** The value shape of a `Based-On:` line, exactly as `requirementDigest` writes it. */
|
|
81
|
+
export const REQUIREMENT_DIGEST_RE = new RegExp(`^[0-9a-f]{${REQUIREMENT_DIGEST_LENGTH}}$`);
|
|
82
|
+
/** Every authored Requirement-ID value in a requirement body, including blanks. */
|
|
83
|
+
export function requirementIdDeclarations(requirement) {
|
|
84
|
+
return requirement.text.flatMap((line) => {
|
|
85
|
+
const match = REQUIREMENT_ID_LINE_RE.exec(line);
|
|
86
|
+
return match === null ? [] : [match[1].trim()];
|
|
87
|
+
});
|
|
88
|
+
}
|
|
89
|
+
/** Structural ID problems within one spec/delta document. */
|
|
90
|
+
export function requirementIdProblems(reqs) {
|
|
91
|
+
const problems = [];
|
|
92
|
+
const owners = new Map();
|
|
93
|
+
for (const requirement of reqs) {
|
|
94
|
+
const declarations = requirementIdDeclarations(requirement);
|
|
95
|
+
const values = declarations.length > 0
|
|
96
|
+
? declarations
|
|
97
|
+
: requirement.id === undefined
|
|
98
|
+
? []
|
|
99
|
+
: [requirement.id];
|
|
100
|
+
if (declarations.length > 1) {
|
|
101
|
+
problems.push({ kind: "repeated", requirement: requirement.name, values: declarations });
|
|
102
|
+
}
|
|
103
|
+
for (const value of values) {
|
|
104
|
+
if (!REQUIREMENT_ID_RE.test(value)) {
|
|
105
|
+
problems.push({ kind: "invalid", requirement: requirement.name, value });
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
// A repeated declaration is already an error. Do not also pretend its
|
|
109
|
+
// keep-last parse result is an unambiguous document identity.
|
|
110
|
+
if (values.length !== 1 || !REQUIREMENT_ID_RE.test(values[0]))
|
|
111
|
+
continue;
|
|
112
|
+
const names = owners.get(values[0]) ?? [];
|
|
113
|
+
names.push(requirement.name);
|
|
114
|
+
owners.set(values[0], names);
|
|
115
|
+
}
|
|
116
|
+
for (const [id, requirements] of owners) {
|
|
117
|
+
if (requirements.length > 1)
|
|
118
|
+
problems.push({ kind: "duplicate", id, requirements });
|
|
119
|
+
}
|
|
120
|
+
return problems;
|
|
121
|
+
}
|
|
122
|
+
/** Every authored `Based-On:` value in a requirement body, including blanks. */
|
|
123
|
+
export function basedOnDeclarations(requirement) {
|
|
124
|
+
return requirement.text.flatMap((line) => {
|
|
125
|
+
const match = BASED_ON_LINE_RE.exec(line);
|
|
126
|
+
return match === null ? [] : [match[1].trim()];
|
|
127
|
+
});
|
|
128
|
+
}
|
|
129
|
+
/**
|
|
130
|
+
* The requirement without its baseline marker — what a digest is taken over,
|
|
131
|
+
* and what archive merges into the living document.
|
|
132
|
+
*
|
|
133
|
+
* `Based-On:` is a statement ABOUT a delta ("this was written against that"),
|
|
134
|
+
* never part of the requirement it describes, and both consequences of
|
|
135
|
+
* forgetting that are bad. Left in the digest input, a requirement's digest
|
|
136
|
+
* would depend on the pin pointing at it, so no baseline could ever be
|
|
137
|
+
* self-consistent. Left in the merged text — `applyRequirementDelta` copies the
|
|
138
|
+
* delta's body into living wholesale — the living document would grow a pin to
|
|
139
|
+
* a version of itself, and the NEXT feature's baseline would be taken over a
|
|
140
|
+
* document containing the previous feature's bookkeeping.
|
|
141
|
+
*/
|
|
142
|
+
export function withoutBaseline(requirement) {
|
|
143
|
+
const { basedOn: _dropped, ...rest } = requirement;
|
|
144
|
+
return { ...rest, text: requirement.text.filter((line) => !BASED_ON_LINE_RE.test(line)) };
|
|
145
|
+
}
|
|
146
|
+
/**
|
|
147
|
+
* The requirement with every `Realizes:` pin stripped back to its bare target —
|
|
148
|
+
* the second thing a digest is taken over, for the same reason as the first.
|
|
149
|
+
*
|
|
150
|
+
* `withoutBaseline`'s argument, one axis over: a pin is a statement ABOUT a
|
|
151
|
+
* join ("this claim was written against that version of the promise"), never
|
|
152
|
+
* part of the requirement making the claim. Left in the digest input it would
|
|
153
|
+
* make re-pinning a content change, so every requirement realizing a capability
|
|
154
|
+
* would go stale against ITS OWN consumers the moment the capability moved —
|
|
155
|
+
* one edit cascading through the corpus, which is the failure the pin exists to
|
|
156
|
+
* report rather than to cause.
|
|
157
|
+
*
|
|
158
|
+
* The line is kept and only the suffix removed, unlike `withoutBaseline` which
|
|
159
|
+
* drops the whole line: the target ids ARE content, and a requirement that
|
|
160
|
+
* stops naming the promise it keeps is a different requirement.
|
|
161
|
+
*/
|
|
162
|
+
export function withoutRealizesPins(requirement) {
|
|
163
|
+
return {
|
|
164
|
+
...requirement,
|
|
165
|
+
text: requirement.text.map((line) => {
|
|
166
|
+
const match = REALIZES_LINE_RE.exec(line);
|
|
167
|
+
if (match === null)
|
|
168
|
+
return line;
|
|
169
|
+
const bare = match[1]
|
|
170
|
+
.split(",")
|
|
171
|
+
.map((entry) => splitRealizesPin(entry.trim()).target)
|
|
172
|
+
.join(", ");
|
|
173
|
+
return line.replace(match[1], bare);
|
|
174
|
+
}),
|
|
175
|
+
};
|
|
176
|
+
}
|
|
177
|
+
/**
|
|
178
|
+
* The identity of a requirement's CONTENT: sha256 over its canonical
|
|
179
|
+
* serialization, truncated like every other digest loam stamps.
|
|
180
|
+
*
|
|
181
|
+
* Canonical, not raw bytes, and deliberately: a living spec is rewritten by
|
|
182
|
+
* `serializeRequirements` on every archive, so hashing the file's bytes would
|
|
183
|
+
* make a baseline go stale over reflowed blank lines — a false alarm on the one
|
|
184
|
+
* check whose whole value is that it only fires when something real moved.
|
|
185
|
+
* What the hash therefore covers is exactly what survives a round trip: the
|
|
186
|
+
* heading, the body (`Requirement-ID:` and `Operations:`/`Covers:` lines
|
|
187
|
+
* included), and every scenario with its Given/When/Then lines. `kind` and
|
|
188
|
+
* `section` are not serialized and so are not hashed — a requirement does not
|
|
189
|
+
* change because the document quoting it moved it under another heading.
|
|
190
|
+
*
|
|
191
|
+
* The two normalizations are the two kinds of bookkeeping a requirement can
|
|
192
|
+
* carry about its own joins — the baseline pointing AT it, and the `Realizes:`
|
|
193
|
+
* pins pointing OUT of it. Both are stripped for the same reason: a digest that
|
|
194
|
+
* moved when bookkeeping moved would make every pin self-invalidating.
|
|
195
|
+
*/
|
|
196
|
+
export function requirementDigest(requirement) {
|
|
197
|
+
return createHash("sha256")
|
|
198
|
+
.update(serializeRequirements([withoutRealizesPins(withoutBaseline(requirement))]), "utf8")
|
|
199
|
+
.digest("hex")
|
|
200
|
+
.slice(0, REQUIREMENT_DIGEST_LENGTH);
|
|
201
|
+
}
|
|
202
|
+
/** Serialize requirements back to OpenSpec markdown (`### Requirement:` + `#### Scenario:`). */
|
|
203
|
+
export function serializeRequirements(reqs) {
|
|
204
|
+
// Framing (blank lines between sections) is normalized here; body content is
|
|
205
|
+
// only edge-trimmed, never collapsed — blank lines inside a scenario (e.g. in
|
|
206
|
+
// fenced code blocks) are verbatim content.
|
|
207
|
+
const chunks = [];
|
|
208
|
+
for (const r of reqs) {
|
|
209
|
+
const chunk = [`### Requirement: ${r.name}`];
|
|
210
|
+
const body = [...r.text];
|
|
211
|
+
// Parsed documents already carry the authored line in `text`. This branch
|
|
212
|
+
// also makes programmatically constructed Requirements serialize fully.
|
|
213
|
+
if (r.id !== undefined && !body.some((line) => REQUIREMENT_ID_LINE_RE.test(line))) {
|
|
214
|
+
body.unshift(`Requirement-ID: ${r.id}`);
|
|
215
|
+
}
|
|
216
|
+
// Directly under the identity it pins, which is where `loam rebase` writes
|
|
217
|
+
// it too — the two lines are read together, so they are never apart.
|
|
218
|
+
if (r.basedOn !== undefined && !body.some((line) => BASED_ON_LINE_RE.test(line))) {
|
|
219
|
+
body.splice(body.findIndex((line) => REQUIREMENT_ID_LINE_RE.test(line)) + 1, 0, `Based-On: ${r.basedOn}`);
|
|
220
|
+
}
|
|
221
|
+
const text = body.join("\n").trim();
|
|
222
|
+
if (text)
|
|
223
|
+
chunk.push("", text);
|
|
224
|
+
for (const s of r.scenarios) {
|
|
225
|
+
chunk.push("", `#### Scenario: ${s.name}`);
|
|
226
|
+
const body = s.lines.join("\n").trim();
|
|
227
|
+
if (body)
|
|
228
|
+
chunk.push(body);
|
|
229
|
+
}
|
|
230
|
+
chunks.push(chunk.join("\n"));
|
|
231
|
+
}
|
|
232
|
+
return chunks.join("\n\n").trim() + "\n";
|
|
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
|
+
//# sourceMappingURL=spec.js.map
|
|
@@ -0,0 +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"]}
|