@ybotok/loam 0.2.0-alpha.6 → 0.2.0-alpha.9
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +1477 -1
- package/COMPARISON.md +91 -3
- package/CONTRIBUTING.md +37 -0
- package/MIGRATING-from-OpenSpec.md +40 -22
- package/QUICKSTART.md +181 -9
- package/README.md +256 -57
- package/ROADMAP.md +108 -11
- package/SCHEMA.md +721 -130
- package/WORKFLOW.md +272 -62
- package/dist/cli.js +26 -8
- package/dist/cli.js.map +1 -1
- package/dist/commands/adopt/adopt.js +5 -10
- package/dist/commands/adopt/adopt.js.map +1 -1
- package/dist/commands/archive/archive.js +15 -18
- package/dist/commands/archive/archive.js.map +1 -1
- package/dist/commands/archive/plan/contracts/asyncapi.js +27 -41
- package/dist/commands/archive/plan/contracts/asyncapi.js.map +1 -1
- package/dist/commands/archive/plan/contracts/consumers.js +56 -0
- package/dist/commands/archive/plan/contracts/consumers.js.map +1 -0
- package/dist/commands/archive/plan/contracts/openapi.js +25 -42
- package/dist/commands/archive/plan/contracts/openapi.js.map +1 -1
- package/dist/commands/archive/plan/contracts/verdict-lines.js +41 -0
- package/dist/commands/archive/plan/contracts/verdict-lines.js.map +1 -0
- package/dist/commands/archive/plan/gate.js +3 -7
- package/dist/commands/archive/plan/gate.js.map +1 -1
- package/dist/commands/archive/plan/landscape.js +4 -4
- package/dist/commands/archive/plan/landscape.js.map +1 -1
- package/dist/commands/archive/plan/refusal.js +8 -18
- package/dist/commands/archive/plan/refusal.js.map +1 -1
- package/dist/commands/archive/plan/specs.js +134 -25
- package/dist/commands/archive/plan/specs.js.map +1 -1
- package/dist/commands/archive/plan/state.js +1 -0
- package/dist/commands/archive/plan/state.js.map +1 -1
- package/dist/commands/archive/run.js +82 -33
- package/dist/commands/archive/run.js.map +1 -1
- package/dist/commands/archive/shipped.js +49 -0
- package/dist/commands/archive/shipped.js.map +1 -0
- package/dist/commands/context/context.js +4 -7
- package/dist/commands/context/context.js.map +1 -1
- package/dist/commands/context/print.js +2 -2
- package/dist/commands/context/print.js.map +1 -1
- package/dist/commands/delta/delta.js +44 -12
- package/dist/commands/delta/delta.js.map +1 -1
- package/dist/commands/delta/print.js +1 -1
- package/dist/commands/delta/print.js.map +1 -1
- package/dist/commands/dependencies.js +3 -7
- package/dist/commands/dependencies.js.map +1 -1
- package/dist/commands/diff/diff.js +20 -27
- package/dist/commands/diff/diff.js.map +1 -1
- package/dist/commands/explore.js +4 -8
- package/dist/commands/explore.js.map +1 -1
- package/dist/commands/gate/gate.js +45 -8
- package/dist/commands/gate/gate.js.map +1 -1
- package/dist/commands/gate/print.js +15 -3
- package/dist/commands/gate/print.js.map +1 -1
- package/dist/commands/gherkin/commit.js +13 -54
- package/dist/commands/gherkin/commit.js.map +1 -1
- package/dist/commands/gherkin/gherkin.js +36 -20
- package/dist/commands/gherkin/gherkin.js.map +1 -1
- package/dist/commands/gherkin/reconcile.js +10 -13
- package/dist/commands/gherkin/reconcile.js.map +1 -1
- package/dist/commands/init/first-hour.js +44 -0
- package/dist/commands/init/first-hour.js.map +1 -1
- package/dist/commands/init/init.js +18 -22
- package/dist/commands/init/init.js.map +1 -1
- package/dist/commands/list/campaign/campaign.js +4 -6
- package/dist/commands/list/campaign/campaign.js.map +1 -1
- package/dist/commands/list/json.js +0 -4
- package/dist/commands/list/json.js.map +1 -1
- package/dist/commands/list/list.js +38 -23
- package/dist/commands/list/list.js.map +1 -1
- package/dist/commands/list/print.js +38 -6
- package/dist/commands/list/print.js.map +1 -1
- package/dist/commands/list/suites/suites.js +140 -0
- package/dist/commands/list/suites/suites.js.map +1 -0
- package/dist/commands/list/views.js +28 -33
- package/dist/commands/list/views.js.map +1 -1
- package/dist/commands/mcp/dispatch.js +91 -36
- package/dist/commands/mcp/dispatch.js.map +1 -1
- package/dist/commands/mcp/mcp.js +56 -15
- package/dist/commands/mcp/mcp.js.map +1 -1
- package/dist/commands/mcp/serve.js.map +1 -1
- package/dist/commands/migrate-openspec/materialize/active.js +27 -22
- package/dist/commands/migrate-openspec/materialize/active.js.map +1 -1
- package/dist/commands/migrate-openspec/materialize/stage.js +4 -3
- package/dist/commands/migrate-openspec/materialize/stage.js.map +1 -1
- package/dist/commands/migrate-openspec/materialize/target.js +2 -2
- package/dist/commands/migrate-openspec/materialize/target.js.map +1 -1
- package/dist/commands/migrate-openspec/openspec/decisions.js +9 -14
- package/dist/commands/migrate-openspec/openspec/decisions.js.map +1 -1
- package/dist/commands/migrate-openspec/openspec/error.js +7 -8
- package/dist/commands/migrate-openspec/openspec/error.js.map +1 -1
- package/dist/commands/migrate-openspec/openspec/mapping.js +1 -9
- package/dist/commands/migrate-openspec/openspec/mapping.js.map +1 -1
- package/dist/commands/migrate-openspec/openspec/paths.js +3 -6
- package/dist/commands/migrate-openspec/openspec/paths.js.map +1 -1
- package/dist/commands/new/capability-template.js +118 -0
- package/dist/commands/new/capability-template.js.map +1 -0
- package/dist/commands/new/commit.js +8 -22
- package/dist/commands/new/commit.js.map +1 -1
- package/dist/commands/new/modifies.js +206 -0
- package/dist/commands/new/modifies.js.map +1 -0
- package/dist/commands/new/new.js +59 -14
- package/dist/commands/new/new.js.map +1 -1
- package/dist/commands/new/templates.js +48 -124
- package/dist/commands/new/templates.js.map +1 -1
- package/dist/commands/open.js +3 -7
- package/dist/commands/open.js.map +1 -1
- package/dist/commands/policy/gate.js +27 -0
- package/dist/commands/policy/gate.js.map +1 -1
- package/dist/commands/rebase/plan.js +2 -2
- package/dist/commands/rebase/plan.js.map +1 -1
- package/dist/commands/rebase/rebase.js +60 -35
- package/dist/commands/rebase/rebase.js.map +1 -1
- package/dist/commands/seed/commit.js +55 -26
- package/dist/commands/seed/commit.js.map +1 -1
- package/dist/commands/seed/plan.js +64 -13
- package/dist/commands/seed/plan.js.map +1 -1
- package/dist/commands/seed/seed.js +6 -9
- package/dist/commands/seed/seed.js.map +1 -1
- package/dist/commands/show/show.js +3 -7
- package/dist/commands/show/show.js.map +1 -1
- package/dist/commands/status/print.js +13 -4
- package/dist/commands/status/print.js.map +1 -1
- package/dist/commands/status/status.js +3 -7
- package/dist/commands/status/status.js.map +1 -1
- package/dist/commands/steps/steps.js +3 -7
- package/dist/commands/steps/steps.js.map +1 -1
- package/dist/commands/subsystem/edit.js +7 -7
- package/dist/commands/subsystem/edit.js.map +1 -1
- package/dist/commands/subsystem/move.js +60 -25
- package/dist/commands/subsystem/move.js.map +1 -1
- package/dist/commands/subsystem/query.js +2 -4
- package/dist/commands/subsystem/query.js.map +1 -1
- package/dist/commands/subsystem/subsystem.js +4 -7
- package/dist/commands/subsystem/subsystem.js.map +1 -1
- package/dist/commands/subsystem/sync.js +141 -71
- package/dist/commands/subsystem/sync.js.map +1 -1
- package/dist/commands/subsystem/txn/report.js +55 -5
- package/dist/commands/subsystem/txn/report.js.map +1 -1
- package/dist/commands/subsystem/txn/txn.js +121 -45
- package/dist/commands/subsystem/txn/txn.js.map +1 -1
- package/dist/commands/subsystem/txn/views.js +82 -12
- package/dist/commands/subsystem/txn/views.js.map +1 -1
- package/dist/commands/tasks/render.js +116 -0
- package/dist/commands/tasks/render.js.map +1 -0
- package/dist/commands/tasks/tasks.js +113 -0
- package/dist/commands/tasks/tasks.js.map +1 -0
- package/dist/commands/unarchive/manifest.js +6 -9
- package/dist/commands/unarchive/manifest.js.map +1 -1
- package/dist/commands/unarchive/restore.js +17 -28
- package/dist/commands/unarchive/restore.js.map +1 -1
- package/dist/commands/unarchive/unarchive.js +22 -30
- package/dist/commands/unarchive/unarchive.js.map +1 -1
- package/dist/commands/validate/arch-coverage.js +6 -15
- package/dist/commands/validate/arch-coverage.js.map +1 -1
- package/dist/commands/validate/checks/fleet-shape.js +4 -49
- package/dist/commands/validate/checks/fleet-shape.js.map +1 -1
- package/dist/commands/validate/checks/health-deps.js +9 -6
- package/dist/commands/validate/checks/health-deps.js.map +1 -1
- package/dist/commands/validate/checks/requirements.js +18 -8
- package/dist/commands/validate/checks/requirements.js.map +1 -1
- package/dist/commands/validate/checks/sources.js +6 -2
- package/dist/commands/validate/checks/sources.js.map +1 -1
- package/dist/commands/validate/feature.js +23 -19
- package/dist/commands/validate/feature.js.map +1 -1
- package/dist/commands/validate/fleet/census.js +26 -3
- package/dist/commands/validate/fleet/census.js.map +1 -1
- package/dist/commands/validate/fleet/landscape.js +35 -32
- package/dist/commands/validate/fleet/landscape.js.map +1 -1
- package/dist/commands/validate/fleet/map/attest.js +7 -12
- package/dist/commands/validate/fleet/map/attest.js.map +1 -1
- package/dist/commands/validate/fleet/map/bindings.js +16 -5
- package/dist/commands/validate/fleet/map/bindings.js.map +1 -1
- package/dist/commands/validate/fleet/map/consumers.js +43 -122
- package/dist/commands/validate/fleet/map/consumers.js.map +1 -1
- package/dist/commands/validate/fleet/map/usecases.js +5 -11
- package/dist/commands/validate/fleet/map/usecases.js.map +1 -1
- package/dist/commands/validate/fleet/obligations.js +1 -5
- package/dist/commands/validate/fleet/obligations.js.map +1 -1
- package/dist/commands/validate/fleet/scorecard/print.js +5 -1
- package/dist/commands/validate/fleet/scorecard/print.js.map +1 -1
- package/dist/commands/validate/fleet/scorecard/scorecard.js +17 -11
- package/dist/commands/validate/fleet/scorecard/scorecard.js.map +1 -1
- package/dist/commands/validate/fleet/usecases/usecases.js +6 -4
- package/dist/commands/validate/fleet/usecases/usecases.js.map +1 -1
- package/dist/commands/validate/fleet/views/fleet-project.js +1 -1
- package/dist/commands/validate/fleet/views/fleet-project.js.map +1 -1
- package/dist/commands/validate/fleet/views/ids.js +33 -7
- package/dist/commands/validate/fleet/views/ids.js.map +1 -1
- package/dist/commands/validate/fleet/views/root-documents.js +17 -35
- package/dist/commands/validate/fleet/views/root-documents.js.map +1 -1
- package/dist/commands/validate/fleet/views/stale.js +31 -14
- package/dist/commands/validate/fleet/views/stale.js.map +1 -1
- package/dist/commands/validate/links/corpus.js +1 -4
- package/dist/commands/validate/links/corpus.js.map +1 -1
- package/dist/commands/validate/links/glossary.js.map +1 -1
- package/dist/commands/validate/report.js +5 -4
- package/dist/commands/validate/report.js.map +1 -1
- package/dist/commands/validate/scope.js +113 -0
- package/dist/commands/validate/scope.js.map +1 -0
- package/dist/commands/validate/service/api.js +16 -104
- package/dist/commands/validate/service/api.js.map +1 -1
- package/dist/commands/validate/service/contracts/generated.js +134 -0
- package/dist/commands/validate/service/contracts/generated.js.map +1 -0
- package/dist/commands/validate/service/events/events.js +1 -1
- package/dist/commands/validate/service/events/events.js.map +1 -1
- package/dist/commands/validate/service/events/message-producers.js +1 -1
- package/dist/commands/validate/service/events/message-producers.js.map +1 -1
- package/dist/commands/validate/service/evidence-pins.js +56 -10
- package/dist/commands/validate/service/evidence-pins.js.map +1 -1
- package/dist/commands/validate/service/service.js +16 -26
- package/dist/commands/validate/service/service.js.map +1 -1
- package/dist/commands/validate/service/specs.js +31 -38
- package/dist/commands/validate/service/specs.js.map +1 -1
- package/dist/commands/validate/service/spine.js +8 -11
- package/dist/commands/validate/service/spine.js.map +1 -1
- package/dist/commands/validate/service/usecases/usecases.js +3 -3
- package/dist/commands/validate/service/usecases/usecases.js.map +1 -1
- package/dist/commands/validate/validate.js +44 -26
- package/dist/commands/validate/validate.js.map +1 -1
- package/dist/commands/verify/cross/diff.js +2 -6
- package/dist/commands/verify/cross/diff.js.map +1 -1
- package/dist/commands/verify/evidence/contract.js +1 -1
- package/dist/commands/verify/evidence/contract.js.map +1 -1
- package/dist/commands/verify/evidence/read.js +12 -3
- package/dist/commands/verify/evidence/read.js.map +1 -1
- package/dist/commands/verify/frozen.js +45 -23
- package/dist/commands/verify/frozen.js.map +1 -1
- package/dist/commands/verify/record.js +69 -17
- package/dist/commands/verify/record.js.map +1 -1
- package/dist/commands/verify/report.js +27 -44
- package/dist/commands/verify/report.js.map +1 -1
- package/dist/commands/verify/results.js +112 -30
- package/dist/commands/verify/results.js.map +1 -1
- package/dist/commands/verify/verify.js +54 -51
- package/dist/commands/verify/verify.js.map +1 -1
- package/dist/commands/vouch/pack/pack.js +2 -2
- package/dist/commands/vouch/pack/pack.js.map +1 -1
- package/dist/commands/vouch/pack/print.js +1 -6
- package/dist/commands/vouch/pack/print.js.map +1 -1
- package/dist/commands/vouch/pack/sections.js +1 -2
- package/dist/commands/vouch/pack/sections.js.map +1 -1
- package/dist/commands/vouch/run.js +26 -81
- package/dist/commands/vouch/run.js.map +1 -1
- package/dist/commands/vouch/sample/plan.js +20 -34
- package/dist/commands/vouch/sample/plan.js.map +1 -1
- package/dist/commands/vouch/vet/verify.js +39 -7
- package/dist/commands/vouch/vet/verify.js.map +1 -1
- package/dist/commands/vouch/vouch.js +10 -17
- package/dist/commands/vouch/vouch.js.map +1 -1
- package/dist/core/agent/agents-md/command-map.js +57 -102
- package/dist/core/agent/agents-md/command-map.js.map +1 -1
- package/dist/core/agent/agents-md/map/explain.js +4 -3
- package/dist/core/agent/agents-md/map/explain.js.map +1 -1
- package/dist/core/agent/agents-md/map/lenses/context.js +4 -3
- package/dist/core/agent/agents-md/map/lenses/context.js.map +1 -1
- package/dist/core/agent/agents-md/map/lenses/diff.js +33 -20
- package/dist/core/agent/agents-md/map/lenses/diff.js.map +1 -1
- package/dist/core/agent/agents-md/map/lenses/gate.js +13 -5
- package/dist/core/agent/agents-md/map/lenses/gate.js.map +1 -1
- package/dist/core/agent/agents-md/map/mcp.js +10 -7
- package/dist/core/agent/agents-md/map/mcp.js.map +1 -1
- package/dist/core/agent/agents-md/map/status.js +9 -3
- package/dist/core/agent/agents-md/map/status.js.map +1 -1
- package/dist/core/agent/agents-md/map/subsystem.js +31 -17
- package/dist/core/agent/agents-md/map/subsystem.js.map +1 -1
- package/dist/core/agent/agents-md/map/usecases.js +24 -6
- package/dist/core/agent/agents-md/map/usecases.js.map +1 -1
- package/dist/core/agent/agents-md/refusals.js +37 -21
- package/dist/core/agent/agents-md/refusals.js.map +1 -1
- package/dist/core/agent/agents-md.js +25 -10
- package/dist/core/agent/agents-md.js.map +1 -1
- package/dist/core/agent/agents-stamp.js +10 -0
- package/dist/core/agent/agents-stamp.js.map +1 -1
- package/dist/core/agent/scaffold.js +10 -15
- package/dist/core/agent/scaffold.js.map +1 -1
- package/dist/core/agent/tools/dialects.js +1 -0
- package/dist/core/agent/tools/dialects.js.map +1 -1
- package/dist/core/agent/tools/registry.js +41 -76
- package/dist/core/agent/tools/registry.js.map +1 -1
- package/dist/core/agent/workflows/adopt.js +5 -3
- package/dist/core/agent/workflows/adopt.js.map +1 -1
- package/dist/core/agent/workflows/check.js +25 -22
- package/dist/core/agent/workflows/check.js.map +1 -1
- package/dist/core/agent/workflows/closing.js +54 -21
- package/dist/core/agent/workflows/closing.js.map +1 -1
- package/dist/core/agent/workflows/feature.js +79 -11
- package/dist/core/agent/workflows/feature.js.map +1 -1
- package/dist/core/agent/workflows/implement.js +91 -13
- package/dist/core/agent/workflows/implement.js.map +1 -1
- package/dist/core/agent/workflows/reference/authoring.js +3 -1
- package/dist/core/agent/workflows/reference/authoring.js.map +1 -1
- package/dist/core/agent/workflows/reference/codes.js +8 -6
- package/dist/core/agent/workflows/reference/codes.js.map +1 -1
- package/dist/core/agent/workflows/reference/done-check.js +37 -15
- package/dist/core/agent/workflows/reference/done-check.js.map +1 -1
- package/dist/core/agent/workflows/reference/reference.js.map +1 -1
- package/dist/core/agent/workflows/reference/spine.js +62 -24
- package/dist/core/agent/workflows/reference/spine.js.map +1 -1
- package/dist/core/asyncapi/baseline/plan.js +14 -81
- package/dist/core/asyncapi/baseline/plan.js.map +1 -1
- package/dist/core/asyncapi/digest.js +30 -25
- package/dist/core/asyncapi/digest.js.map +1 -1
- package/dist/core/asyncapi/merge/components.js +5 -4
- package/dist/core/asyncapi/merge/components.js.map +1 -1
- package/dist/core/asyncapi/merge/error.js +7 -2
- package/dist/core/asyncapi/merge/error.js.map +1 -1
- package/dist/core/asyncapi/merge/markers.js +30 -50
- package/dist/core/asyncapi/merge/markers.js.map +1 -1
- package/dist/core/asyncapi/merge/merge.js +31 -53
- package/dist/core/asyncapi/merge/merge.js.map +1 -1
- package/dist/core/asyncapi/merge/pin.js +10 -36
- package/dist/core/asyncapi/merge/pin.js.map +1 -1
- package/dist/core/asyncapi/model.js.map +1 -1
- package/dist/core/asyncapi/read.js +36 -12
- package/dist/core/asyncapi/read.js.map +1 -1
- package/dist/core/brief/brief.js +30 -25
- package/dist/core/brief/brief.js.map +1 -1
- package/dist/core/brief/landscape.js +8 -12
- package/dist/core/brief/landscape.js.map +1 -1
- package/dist/core/brief/map/extend.js +8 -5
- package/dist/core/brief/map/extend.js.map +1 -1
- package/dist/core/brief/map/owed.js +8 -26
- package/dist/core/brief/map/owed.js.map +1 -1
- package/dist/core/brief/targets.js +79 -5
- package/dist/core/brief/targets.js.map +1 -1
- package/dist/core/brief/unchecked.js +1 -1
- package/dist/core/brief/unchecked.js.map +1 -1
- package/dist/core/c4/likec4.js +147 -16
- package/dist/core/c4/likec4.js.map +1 -1
- package/dist/core/c4/locate/unlocated.js +150 -0
- package/dist/core/c4/locate/unlocated.js.map +1 -0
- package/dist/core/c4/parsed/values.js +54 -1
- package/dist/core/c4/parsed/values.js.map +1 -1
- package/dist/core/c4/prefetch/fleet.js +1 -9
- package/dist/core/c4/prefetch/fleet.js.map +1 -1
- package/dist/core/c4/project/architecture.js +31 -9
- package/dist/core/c4/project/architecture.js.map +1 -1
- package/dist/core/c4/project/documents.js +36 -10
- package/dist/core/c4/project/documents.js.map +1 -1
- package/dist/core/c4/project/load.js +14 -21
- package/dist/core/c4/project/load.js.map +1 -1
- package/dist/core/c4/project/staged.js +7 -3
- package/dist/core/c4/project/staged.js.map +1 -1
- package/dist/core/c4/resolve/external.js +51 -0
- package/dist/core/c4/resolve/external.js.map +1 -0
- package/dist/core/c4/resolve/service.js +25 -1
- package/dist/core/c4/resolve/service.js.map +1 -1
- package/dist/core/c4/seed/template.js +15 -1
- package/dist/core/c4/seed/template.js.map +1 -1
- package/dist/core/c4/service-model/shape.js +2 -22
- package/dist/core/c4/service-model/shape.js.map +1 -1
- package/dist/core/c4/source-mask.js +288 -4
- package/dist/core/c4/source-mask.js.map +1 -1
- package/dist/core/c4/source-scan.js +203 -147
- package/dist/core/c4/source-scan.js.map +1 -1
- package/dist/core/c4/splice/authored-source.js +19 -0
- package/dist/core/c4/splice/authored-source.js.map +1 -1
- package/dist/core/c4/splice/delta-blocks.js +5 -18
- package/dist/core/c4/splice/delta-blocks.js.map +1 -1
- package/dist/core/c4/splice/landscape-merge.js +13 -8
- package/dist/core/c4/splice/landscape-merge.js.map +1 -1
- package/dist/core/c4/splice/model/merge.js +10 -6
- package/dist/core/c4/splice/model/merge.js.map +1 -1
- package/dist/core/c4/splice/placement.js +108 -18
- package/dist/core/c4/splice/placement.js.map +1 -1
- package/dist/core/c4/workspace.js +13 -22
- package/dist/core/c4/workspace.js.map +1 -1
- package/dist/core/capabilities/capabilities.js +6 -6
- package/dist/core/capabilities/capabilities.js.map +1 -1
- package/dist/core/capabilities/delta/doc.js +7 -10
- package/dist/core/capabilities/delta/doc.js.map +1 -1
- package/dist/core/capabilities/findings.js +15 -20
- package/dist/core/capabilities/findings.js.map +1 -1
- package/dist/core/capabilities/realizes/findings.js +12 -19
- package/dist/core/capabilities/realizes/findings.js.map +1 -1
- package/dist/core/capabilities/realizes/join.js +0 -20
- package/dist/core/capabilities/realizes/join.js.map +1 -1
- package/dist/core/capabilities/rollup.js +6 -0
- package/dist/core/capabilities/rollup.js.map +1 -1
- package/dist/core/capabilities/tree.js +6 -28
- package/dist/core/capabilities/tree.js.map +1 -1
- package/dist/core/capabilities/usecase-join.js +3 -13
- package/dist/core/capabilities/usecase-join.js.map +1 -1
- package/dist/core/coherence/authoring/scaffold.js +11 -3
- package/dist/core/coherence/authoring/scaffold.js.map +1 -1
- package/dist/core/coherence/authoring/sentinels.js +2 -1
- package/dist/core/coherence/authoring/sentinels.js.map +1 -1
- package/dist/core/coherence/coherence.js +30 -16
- package/dist/core/coherence/coherence.js.map +1 -1
- package/dist/core/coherence/consumed/operations.js +70 -0
- package/dist/core/coherence/consumed/operations.js.map +1 -0
- package/dist/core/coherence/consumed/refusal.js +38 -0
- package/dist/core/coherence/consumed/refusal.js.map +1 -0
- package/dist/core/coherence/declared.js +60 -34
- package/dist/core/coherence/declared.js.map +1 -1
- package/dist/core/coherence/events/declared.js +44 -59
- package/dist/core/coherence/events/declared.js.map +1 -1
- package/dist/core/coherence/events/events.js +119 -8
- package/dist/core/coherence/events/events.js.map +1 -1
- package/dist/core/coherence/events/grades.js +31 -8
- package/dist/core/coherence/events/grades.js.map +1 -1
- package/dist/core/coherence/events/lookups.js +14 -82
- package/dist/core/coherence/events/lookups.js.map +1 -1
- package/dist/core/coherence/lookups.js +18 -78
- package/dist/core/coherence/lookups.js.map +1 -1
- package/dist/core/coherence/retired/archived.js +285 -0
- package/dist/core/coherence/retired/archived.js.map +1 -0
- package/dist/core/coherence/retired/event-pair.js +62 -0
- package/dist/core/coherence/retired/event-pair.js.map +1 -0
- package/dist/core/coherence/retired/merged.js +79 -0
- package/dist/core/coherence/retired/merged.js.map +1 -0
- package/dist/core/coherence/retired/retirement.js +49 -0
- package/dist/core/coherence/retired/retirement.js.map +1 -0
- package/dist/core/coherence/retired/revival/arrivals.js +131 -0
- package/dist/core/coherence/retired/revival/arrivals.js.map +1 -0
- package/dist/core/coherence/retired/revival/revived.js +297 -0
- package/dist/core/coherence/retired/revival/revived.js.map +1 -0
- package/dist/core/consumers/after.js +113 -0
- package/dist/core/consumers/after.js.map +1 -0
- package/dist/core/consumers/consumers.js +296 -0
- package/dist/core/consumers/consumers.js.map +1 -0
- package/dist/core/consumers/current.js +211 -0
- package/dist/core/consumers/current.js.map +1 -0
- package/dist/core/consumers/scan.js +46 -0
- package/dist/core/consumers/scan.js.map +1 -0
- package/dist/core/consumers/state.js +27 -0
- package/dist/core/consumers/state.js.map +1 -0
- package/dist/core/delta/delta.js +1 -1
- package/dist/core/delta/delta.js.map +1 -1
- package/dist/core/deployment/delta.js +15 -16
- package/dist/core/deployment/delta.js.map +1 -1
- package/dist/core/diff/base-git.js +94 -12
- package/dist/core/diff/base-git.js.map +1 -1
- package/dist/core/diff/base-state.js +28 -16
- package/dist/core/diff/base-state.js.map +1 -1
- package/dist/core/diff/joins.js +136 -0
- package/dist/core/diff/joins.js.map +1 -0
- package/dist/core/diff/scope/changed-paths.js +22 -6
- package/dist/core/diff/scope/changed-paths.js.map +1 -1
- package/dist/core/diff/scope/edges.js +82 -0
- package/dist/core/diff/scope/edges.js.map +1 -0
- package/dist/core/diff/semantic.js +100 -31
- package/dist/core/diff/semantic.js.map +1 -1
- package/dist/core/doctor/agents.js +34 -18
- package/dist/core/doctor/agents.js.map +1 -1
- package/dist/core/doctor/report.js.map +1 -1
- package/dist/core/document/apply.js +10 -2
- package/dist/core/document/apply.js.map +1 -1
- package/dist/core/document/frontmatter.js +55 -0
- package/dist/core/document/frontmatter.js.map +1 -1
- package/dist/core/document/parse.js +29 -6
- package/dist/core/document/parse.js.map +1 -1
- package/dist/core/document/scenarios.js +17 -0
- package/dist/core/document/scenarios.js.map +1 -1
- package/dist/core/document/spec.js +4 -11
- package/dist/core/document/spec.js.map +1 -1
- package/dist/core/envelope/config.js +10 -0
- package/dist/core/envelope/config.js.map +1 -1
- package/dist/core/envelope/json.js +34 -0
- package/dist/core/envelope/json.js.map +1 -1
- package/dist/core/explain/families.js +17 -12
- package/dist/core/explain/families.js.map +1 -1
- package/dist/core/explain/refusals.js +3 -3
- package/dist/core/explain/refusals.js.map +1 -1
- package/dist/core/explain/terms.js +18 -9
- package/dist/core/explain/terms.js.map +1 -1
- package/dist/core/explore/explore.js +2 -7
- package/dist/core/explore/explore.js.map +1 -1
- package/dist/core/fleet-context.js +34 -95
- package/dist/core/fleet-context.js.map +1 -1
- package/dist/core/gate/checks.js +10 -3
- package/dist/core/gate/checks.js.map +1 -1
- package/dist/core/gate/partners.js +64 -19
- package/dist/core/gate/partners.js.map +1 -1
- package/dist/core/gate/report.js.map +1 -1
- package/dist/core/gate/verification.js +75 -12
- package/dist/core/gate/verification.js.map +1 -1
- package/dist/core/gherkin/stale.js +13 -37
- package/dist/core/gherkin/stale.js.map +1 -1
- package/dist/core/glossary/tree.js +6 -19
- package/dist/core/glossary/tree.js.map +1 -1
- package/dist/core/kernel/concurrency.js +19 -0
- package/dist/core/kernel/concurrency.js.map +1 -1
- package/dist/core/kernel/records.js +11 -0
- package/dist/core/kernel/records.js.map +1 -1
- package/dist/core/links/backlinks.js +1 -3
- package/dist/core/links/backlinks.js.map +1 -1
- package/dist/core/links/corpus.js +5 -36
- package/dist/core/links/corpus.js.map +1 -1
- package/dist/core/mcp/argv.js +101 -0
- package/dist/core/mcp/argv.js.map +1 -1
- package/dist/core/mcp/author-tools.js +2 -1
- package/dist/core/mcp/author-tools.js.map +1 -1
- package/dist/core/mcp/protocol.js +2 -2
- package/dist/core/mcp/protocol.js.map +1 -1
- package/dist/core/mcp/tools.js +43 -33
- package/dist/core/mcp/tools.js.map +1 -1
- package/dist/core/openapi/baseline/gate.js +53 -12
- package/dist/core/openapi/baseline/gate.js.map +1 -1
- package/dist/core/openapi/baseline/plan.js +22 -8
- package/dist/core/openapi/baseline/plan.js.map +1 -1
- package/dist/core/openapi/baseline/record.js +0 -1
- package/dist/core/openapi/baseline/record.js.map +1 -1
- package/dist/core/openapi/contract-text.js +33 -0
- package/dist/core/openapi/contract-text.js.map +1 -0
- package/dist/core/openapi/contract-tree.js +56 -0
- package/dist/core/openapi/contract-tree.js.map +1 -0
- package/dist/core/openapi/depth.js +38 -24
- package/dist/core/openapi/depth.js.map +1 -1
- package/dist/core/openapi/digest.js +52 -2
- package/dist/core/openapi/digest.js.map +1 -1
- package/dist/core/openapi/doc.js +12 -4
- package/dist/core/openapi/doc.js.map +1 -1
- package/dist/core/openapi/merge/components.js +25 -60
- package/dist/core/openapi/merge/components.js.map +1 -1
- package/dist/core/openapi/merge/error.js +3 -13
- package/dist/core/openapi/merge/error.js.map +1 -1
- package/dist/core/openapi/merge/markers.js +26 -30
- package/dist/core/openapi/merge/markers.js.map +1 -1
- package/dist/core/openapi/merge/merge.js +26 -52
- package/dist/core/openapi/merge/merge.js.map +1 -1
- package/dist/core/openapi/merge/pin.js +15 -35
- package/dist/core/openapi/merge/pin.js.map +1 -1
- package/dist/core/openspec/decide/verdict.js.map +1 -1
- package/dist/core/openspec/inventory.js +4 -5
- package/dist/core/openspec/inventory.js.map +1 -1
- package/dist/core/openspec/model/mapping.js +30 -28
- package/dist/core/openspec/model/mapping.js.map +1 -1
- package/dist/core/openspec/model/model.js +12 -7
- package/dist/core/openspec/model/model.js.map +1 -1
- package/dist/core/openspec/scan/changes.js +10 -4
- package/dist/core/openspec/scan/changes.js.map +1 -1
- package/dist/core/openspec/scan/config.js +7 -16
- package/dist/core/openspec/scan/config.js.map +1 -1
- package/dist/core/openspec/scan/shape.js +63 -19
- package/dist/core/openspec/scan/shape.js.map +1 -1
- package/dist/core/openspec/scan/walk.js +21 -38
- package/dist/core/openspec/scan/walk.js.map +1 -1
- package/dist/core/pack/living.js +2 -16
- package/dist/core/pack/living.js.map +1 -1
- package/dist/core/provenance/findings.js +48 -12
- package/dist/core/provenance/findings.js.map +1 -1
- package/dist/core/provenance/git.js +33 -57
- package/dist/core/provenance/git.js.map +1 -1
- package/dist/core/provenance/gitq/moves.js +71 -114
- package/dist/core/provenance/gitq/moves.js.map +1 -1
- package/dist/core/provenance/gitq/vouched-ancestor.js +2 -2
- package/dist/core/provenance/gitq/vouched-ancestor.js.map +1 -1
- package/dist/core/provenance/sources.js +23 -6
- package/dist/core/provenance/sources.js.map +1 -1
- package/dist/core/provenance/stamp.js +5 -17
- package/dist/core/provenance/stamp.js.map +1 -1
- package/dist/core/repo/entries.js.map +1 -1
- package/dist/core/repo/paths.js +34 -29
- package/dist/core/repo/paths.js.map +1 -1
- package/dist/core/repo/repo.js +24 -14
- package/dist/core/repo/repo.js.map +1 -1
- package/dist/core/repo/tree/find.js +12 -3
- package/dist/core/repo/tree/find.js.map +1 -1
- package/dist/core/repo/tree/fs.js +27 -1
- package/dist/core/repo/tree/fs.js.map +1 -1
- package/dist/core/repo/tree/render/body.js +45 -6
- package/dist/core/repo/tree/render/body.js.map +1 -1
- package/dist/core/repo/tree/render/service/header.js +125 -0
- package/dist/core/repo/tree/render/service/header.js.map +1 -0
- package/dist/core/repo/tree/render/service/peers.js +28 -0
- package/dist/core/repo/tree/render/service/peers.js.map +1 -0
- package/dist/core/repo/tree/render/service/view-id.js +67 -0
- package/dist/core/repo/tree/render/service/view-id.js.map +1 -0
- package/dist/core/repo/tree/render/service/views.js +78 -0
- package/dist/core/repo/tree/render/service/views.js.map +1 -0
- package/dist/core/repo/tree/render/stale.js +19 -2
- package/dist/core/repo/tree/render/stale.js.map +1 -1
- package/dist/core/repo/tree/render/views.js +102 -33
- package/dist/core/repo/tree/render/views.js.map +1 -1
- package/dist/core/repo/tree/walk.js +8 -30
- package/dist/core/repo/tree/walk.js.map +1 -1
- package/dist/core/results.js +47 -12
- package/dist/core/results.js.map +1 -1
- package/dist/core/staging/commit.js +10 -12
- package/dist/core/staging/commit.js.map +1 -1
- package/dist/core/staging/lock.js +74 -12
- package/dist/core/staging/lock.js.map +1 -1
- package/dist/core/staging/recovery/intent.js +2 -5
- package/dist/core/staging/recovery/intent.js.map +1 -1
- package/dist/core/staging/recovery/recover.js +60 -1
- package/dist/core/staging/recovery/recover.js.map +1 -1
- package/dist/core/staging/snapshot/manifest.js +152 -0
- package/dist/core/staging/snapshot/manifest.js.map +1 -0
- package/dist/core/staging/{snapshot.js → snapshot/snapshot.js} +10 -34
- package/dist/core/staging/snapshot/snapshot.js.map +1 -0
- package/dist/core/status/actions/execution.js +11 -0
- package/dist/core/status/actions/execution.js.map +1 -1
- package/dist/core/status/feature/artifacts.js +2 -2
- package/dist/core/status/feature/artifacts.js.map +1 -1
- package/dist/core/status/feature/feature.js +5 -11
- package/dist/core/status/feature/feature.js.map +1 -1
- package/dist/core/status/fleet/fleet.js +6 -2
- package/dist/core/status/fleet/fleet.js.map +1 -1
- package/dist/core/status/fleet/next.js +26 -3
- package/dist/core/status/fleet/next.js.map +1 -1
- package/dist/core/status/report.js.map +1 -1
- package/dist/core/status/verification.js +30 -15
- package/dist/core/status/verification.js.map +1 -1
- package/dist/core/tasks/packet.js +174 -0
- package/dist/core/tasks/packet.js.map +1 -0
- package/dist/core/tasks/record.js +43 -0
- package/dist/core/tasks/record.js.map +1 -0
- package/dist/core/tasks/sources.js +181 -0
- package/dist/core/tasks/sources.js.map +1 -0
- package/dist/core/tasks/stamps.js +76 -0
- package/dist/core/tasks/stamps.js.map +1 -0
- package/dist/core/tasks/unit.js +0 -0
- package/dist/core/tasks/unit.js.map +1 -0
- package/dist/core/usecases/capability.js +11 -14
- package/dist/core/usecases/capability.js.map +1 -1
- package/dist/core/usecases/delta/flows.js +5 -7
- package/dist/core/usecases/delta/flows.js.map +1 -1
- package/dist/core/usecases/fleet.js +178 -31
- package/dist/core/usecases/fleet.js.map +1 -1
- package/dist/core/usecases/operations.js +5 -4
- package/dist/core/usecases/operations.js.map +1 -1
- package/dist/core/usecases/service/flows.js +9 -6
- package/dist/core/usecases/service/flows.js.map +1 -1
- package/dist/core/usecases/suites/participants.js +83 -0
- package/dist/core/usecases/suites/participants.js.map +1 -0
- package/dist/core/usecases/suites/suites.js +45 -0
- package/dist/core/usecases/suites/suites.js.map +1 -0
- package/dist/core/verify/answers.js +12 -5
- package/dist/core/verify/answers.js.map +1 -1
- package/dist/core/verify/build.js +102 -82
- package/dist/core/verify/build.js.map +1 -1
- package/dist/core/verify/checklist.js +45 -10
- package/dist/core/verify/checklist.js.map +1 -1
- package/dist/core/verify/location.js +67 -0
- package/dist/core/verify/location.js.map +1 -0
- package/dist/core/verify/pins/lint.js +3 -2
- package/dist/core/verify/pins/lint.js.map +1 -1
- package/dist/core/verify/pins/pin.js +1 -1
- package/dist/core/verify/pins/pin.js.map +1 -1
- package/dist/core/verify/read/file.js +224 -0
- package/dist/core/verify/read/file.js.map +1 -0
- package/dist/core/verify/{file.js → read/shape.js} +16 -104
- package/dist/core/verify/read/shape.js.map +1 -0
- package/dist/core/verify/read/union.js +105 -0
- package/dist/core/verify/read/union.js.map +1 -0
- package/dist/core/verify/record.js +35 -12
- package/dist/core/verify/record.js.map +1 -1
- package/dist/core/verify/store/commit.js +40 -30
- package/dist/core/verify/store/commit.js.map +1 -1
- package/dist/core/verify/store/render.js +8 -4
- package/dist/core/verify/store/render.js.map +1 -1
- package/dist/core/vocabulary/issue.js +1 -0
- package/dist/core/vocabulary/issue.js.map +1 -1
- package/dist/core/vocabulary/maturity.js +53 -3
- package/dist/core/vocabulary/maturity.js.map +1 -1
- package/examples/README.md +28 -9
- package/examples/docs/architecture/landscape.likec4 +21 -7
- package/examples/docs/architecture/services.likec4 +70 -0
- package/examples/docs/architecture/subsystems.likec4 +3 -0
- package/examples/docs/architecture/usecases/order-notification.likec4 +6 -2
- package/examples/docs/features/FEAT-101-payment-splitting/specs/payment-service/asyncapi.yaml +11 -8
- package/examples/docs/features/FEAT-101-payment-splitting/specs/payment-split-service/openapi.yaml +6 -6
- package/examples/docs/features/archive/FEAT-088-refunds/specs/payment-service/openapi.yaml +13 -10
- package/examples/docs/services/order-service/asyncapi.yaml +3 -2
- package/examples/docs/services/order-service/model.likec4 +10 -8
- package/examples/docs/services/payment-service/model.likec4 +6 -4
- package/examples/docs/services/platform/identity-service/model.likec4 +9 -7
- package/examples/docs/services/platform/notification-service/asyncapi.yaml +2 -1
- package/examples/docs/services/platform/notification-service/model.likec4 +4 -5
- package/package.json +1 -1
- package/dist/commands/validate/checks/vocabulary.js +0 -16
- package/dist/commands/validate/checks/vocabulary.js.map +0 -1
- package/dist/core/agent/agents-md/artifacts.js +0 -221
- package/dist/core/agent/agents-md/artifacts.js.map +0 -1
- package/dist/core/agent/agents-md/cycle.js +0 -180
- package/dist/core/agent/agents-md/cycle.js.map +0 -1
- package/dist/core/agent/agents-md/shipped/archive-gate.js +0 -98
- package/dist/core/agent/agents-md/shipped/archive-gate.js.map +0 -1
- package/dist/core/agent/agents-md/spine.js +0 -40
- package/dist/core/agent/agents-md/spine.js.map +0 -1
- package/dist/core/coherence/events/merged.js +0 -40
- package/dist/core/coherence/events/merged.js.map +0 -1
- package/dist/core/diff/current-state.js +0 -96
- package/dist/core/diff/current-state.js.map +0 -1
- package/dist/core/diff/victims.js +0 -256
- package/dist/core/diff/victims.js.map +0 -1
- package/dist/core/staging/snapshot.js.map +0 -1
- package/dist/core/verify/file.js.map +0 -1
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"report.js","sourceRoot":"","sources":["../../../src/core/doctor/report.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;GAUG;AACH,OAAO,EAAyB,MAAM,gCAAgC,CAAC","sourcesContent":["/**\n * What a diagnosis IS: the finding shape, the report shape, and the agent\n * surface inside it.\n *\n * A module of its own because five checks fill this in and the command prints\n * it, so every one of them agrees about what a blocker means here — something\n * that stops loam running at all, as against a warning, which is something a\n * person should know before trusting an answer. That distinction is the whole\n * value of `doctor`, and it lives in the type rather than in five checks'\n * judgement.\n */\nimport { type WritePathResidue } from \"../staging/recovery/residue.js\";\n\nexport type DoctorSeverity = \"blocker\" | \"warning\";\n\nexport interface DoctorFinding {\n severity: DoctorSeverity;\n code: string;\n message: string;\n /**\n * The next command or edit that clears this finding. Mandatory, not optional:\n * a diagnostic that names a problem without naming its fix is a diagnostic\n * the reader has to research, and `doctor` is what someone runs precisely\n * because they do not yet know what loam wants from them.\n */\n fix: string;\n}\n\nexport interface DoctorReport {\n healthy: boolean;\n runtime: {\n package: \"@ybotok/loam\";\n version: string;\n node: string;\n platform: NodeJS.Platform;\n arch: string;\n };\n config: {\n path: string;\n status: \"missing\" | \"invalid\" | \"valid\";\n error: string | null;\n };\n docs: {\n path: string | null;\n exists: boolean;\n readable: boolean;\n writable: boolean;\n servicesDir: boolean;\n landscape: boolean;\n };\n counts: { services: number; activeFeatures: number };\n currentService: {\n configured: string | null;\n status: \"unbound\" | \"matched\" | \"unknown\";\n };\n /** The agent surface of this repo — see `inspectAgentSurface`. */\n agents: AgentSurface;\n /**\n * The problem reports this repo has collected — see `scanReports`. State,\n * like `docs` and `counts`, and never a finding: a repository with eleven\n * open reports is not a broken repository, it is one that has been paying\n * attention. `healthy` does not move for anything in here.\n */\n reports: ProblemReports;\n /**\n * What a write that did not finish left in the docs repo, or null when\n * `docsDir` never resolved. Reported as state, like `docs` and `counts`: a\n * lock somebody is legitimately holding right now is a fact, not a complaint.\n */\n writePath: WritePathResidue | null;\n /**\n * The same scan over the service repo's owned `<gherkinDir>/loam/` root —\n * the one writer whose journal is not in the docs repo. Null when this repo\n * is not a service repo, or the root does not exist. Additive: a consumer\n * of the original envelope never sees a key it did not ask about removed.\n */\n serviceWritePath: WritePathResidue | null;\n findings: DoctorFinding[];\n}\n\n/**\n * Where one problem report got to, as its own `Status:` line says.\n *\n * `fixed` and `superseded` are the FIRST WORD of a longer line (`fixed in\n * 0.2.0-alpha.5`, `superseded by 010`), because the rest of that line is prose\n * an author writes for a reader and loam has no business parsing. `unstated`\n * covers both a report written before the field existed and one whose value is\n * a word this vocabulary does not know — loam counts what it can read and says\n * so, rather than inventing a sixth bucket for every synonym somebody types.\n */\nexport type ProblemReportStatus = \"open\" | \"sent\" | \"fixed\" | \"superseded\" | \"unstated\";\n\nexport interface ProblemReportEntry {\n /** The file's own name inside the reports directory — `012-2026-09-03-slug.md`. */\n file: string;\n /**\n * The leading zero-padded ordinal the protocol now asks for, or null for a\n * name written before it did (`2026-09-03-slug.md`). A report with no ordinal\n * is still a report: this directory predates the numbering.\n */\n ordinal: string | null;\n status: ProblemReportStatus;\n}\n\n/** The report corpus a repository has collected — see `scanReports`. */\nexport interface ProblemReports {\n /**\n * The ABSOLUTE path of the directory — `loam-reports/` beside the `loam.json`\n * that resolved, else beside the directory `doctor` ran in. Absolute because\n * the protocol tells an agent to write there, and a repo-relative spelling\n * left it to guess which repository \"relative\" meant: `doctor` is the\n * preflight that runs precisely when the config may not resolve, and in a\n * service repository the config's root is that repository, not the docs one.\n */\n dir: string;\n /** Whether that directory exists at all. A repo with no reports is normal. */\n present: boolean;\n total: number;\n /**\n * The ordinal the next report takes: one more than the highest present,\n * padded to at least three digits, `001` over an empty or absent directory —\n * it WIDENS past three rather than reusing a number once a corpus gets\n * there, which is what every page saying \"at least three digits\" describes.\n * This is the field the `loam-report` protocol tells an agent to read.\n */\n next: string;\n entries: ProblemReportEntry[];\n}\n\nexport interface AgentSurface {\n /** Tool ids this repo has an agent surface for. Empty is legal (`init --no-commands`). */\n tools: string[];\n /** Workflow subset managed for those tools. */\n profile: \"full\" | \"service\" | \"docs\";\n /** Whether `tools` came from loam.json's own record or from what is on disk. */\n toolsSource: \"config\" | \"disk\";\n /** How many files the running binary would lay down for `tools`. */\n plannedFiles: number;\n /** Repo-relative paths of those files that are not there. */\n missingFiles: string[];\n /**\n * Repo-relative paths of the ones that ARE there
|
|
1
|
+
{"version":3,"file":"report.js","sourceRoot":"","sources":["../../../src/core/doctor/report.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;GAUG;AACH,OAAO,EAAyB,MAAM,gCAAgC,CAAC","sourcesContent":["/**\n * What a diagnosis IS: the finding shape, the report shape, and the agent\n * surface inside it.\n *\n * A module of its own because five checks fill this in and the command prints\n * it, so every one of them agrees about what a blocker means here — something\n * that stops loam running at all, as against a warning, which is something a\n * person should know before trusting an answer. That distinction is the whole\n * value of `doctor`, and it lives in the type rather than in five checks'\n * judgement.\n */\nimport { type WritePathResidue } from \"../staging/recovery/residue.js\";\n\nexport type DoctorSeverity = \"blocker\" | \"warning\";\n\nexport interface DoctorFinding {\n severity: DoctorSeverity;\n code: string;\n message: string;\n /**\n * The next command or edit that clears this finding. Mandatory, not optional:\n * a diagnostic that names a problem without naming its fix is a diagnostic\n * the reader has to research, and `doctor` is what someone runs precisely\n * because they do not yet know what loam wants from them.\n */\n fix: string;\n}\n\nexport interface DoctorReport {\n healthy: boolean;\n runtime: {\n package: \"@ybotok/loam\";\n version: string;\n node: string;\n platform: NodeJS.Platform;\n arch: string;\n };\n config: {\n path: string;\n status: \"missing\" | \"invalid\" | \"valid\";\n error: string | null;\n };\n docs: {\n path: string | null;\n exists: boolean;\n readable: boolean;\n writable: boolean;\n servicesDir: boolean;\n landscape: boolean;\n };\n counts: { services: number; activeFeatures: number };\n currentService: {\n configured: string | null;\n status: \"unbound\" | \"matched\" | \"unknown\";\n };\n /** The agent surface of this repo — see `inspectAgentSurface`. */\n agents: AgentSurface;\n /**\n * The problem reports this repo has collected — see `scanReports`. State,\n * like `docs` and `counts`, and never a finding: a repository with eleven\n * open reports is not a broken repository, it is one that has been paying\n * attention. `healthy` does not move for anything in here.\n */\n reports: ProblemReports;\n /**\n * What a write that did not finish left in the docs repo, or null when\n * `docsDir` never resolved. Reported as state, like `docs` and `counts`: a\n * lock somebody is legitimately holding right now is a fact, not a complaint.\n */\n writePath: WritePathResidue | null;\n /**\n * The same scan over the service repo's owned `<gherkinDir>/loam/` root —\n * the one writer whose journal is not in the docs repo. Null when this repo\n * is not a service repo, or the root does not exist. Additive: a consumer\n * of the original envelope never sees a key it did not ask about removed.\n */\n serviceWritePath: WritePathResidue | null;\n findings: DoctorFinding[];\n}\n\n/**\n * Where one problem report got to, as its own `Status:` line says.\n *\n * `fixed` and `superseded` are the FIRST WORD of a longer line (`fixed in\n * 0.2.0-alpha.5`, `superseded by 010`), because the rest of that line is prose\n * an author writes for a reader and loam has no business parsing. `unstated`\n * covers both a report written before the field existed and one whose value is\n * a word this vocabulary does not know — loam counts what it can read and says\n * so, rather than inventing a sixth bucket for every synonym somebody types.\n */\nexport type ProblemReportStatus = \"open\" | \"sent\" | \"fixed\" | \"superseded\" | \"unstated\";\n\nexport interface ProblemReportEntry {\n /** The file's own name inside the reports directory — `012-2026-09-03-slug.md`. */\n file: string;\n /**\n * The leading zero-padded ordinal the protocol now asks for, or null for a\n * name written before it did (`2026-09-03-slug.md`). A report with no ordinal\n * is still a report: this directory predates the numbering.\n */\n ordinal: string | null;\n status: ProblemReportStatus;\n}\n\n/** The report corpus a repository has collected — see `scanReports`. */\nexport interface ProblemReports {\n /**\n * The ABSOLUTE path of the directory — `loam-reports/` beside the `loam.json`\n * that resolved, else beside the directory `doctor` ran in. Absolute because\n * the protocol tells an agent to write there, and a repo-relative spelling\n * left it to guess which repository \"relative\" meant: `doctor` is the\n * preflight that runs precisely when the config may not resolve, and in a\n * service repository the config's root is that repository, not the docs one.\n */\n dir: string;\n /** Whether that directory exists at all. A repo with no reports is normal. */\n present: boolean;\n total: number;\n /**\n * The ordinal the next report takes: one more than the highest present,\n * padded to at least three digits, `001` over an empty or absent directory —\n * it WIDENS past three rather than reusing a number once a corpus gets\n * there, which is what every page saying \"at least three digits\" describes.\n * This is the field the `loam-report` protocol tells an agent to read.\n */\n next: string;\n entries: ProblemReportEntry[];\n}\n\nexport interface AgentSurface {\n /** Tool ids this repo has an agent surface for. Empty is legal (`init --no-commands`). */\n tools: string[];\n /** Workflow subset managed for those tools. */\n profile: \"full\" | \"service\" | \"docs\";\n /** Whether `tools` came from loam.json's own record or from what is on disk. */\n toolsSource: \"config\" | \"disk\";\n /** How many files the running binary would lay down for `tools`. */\n plannedFiles: number;\n /** Repo-relative paths of those files that are not there. */\n missingFiles: string[];\n /**\n * Repo-relative paths of the ones that ARE there whose body differs from the\n * one this binary writes AND whose version stamp is missing or older than\n * this binary — see `staleAgentFiles`. A file differing only in its stamp is\n * not listed.\n */\n staleFiles: string[];\n stamp: {\n /** Where the docs repo's AGENTS.md is, or null when `docsDir` did not resolve. */\n path: string | null;\n /** Whether it could be read at all — absent and unstamped are different facts. */\n present: boolean;\n /** The version the stamp names, null when there is none (or an unparseable one). */\n version: string | null;\n /** Whether `agents.stale` fired: the stamp is missing or trails this binary. */\n stale: boolean;\n };\n}\n"]}
|
|
@@ -19,13 +19,21 @@ function asLiving(d) {
|
|
|
19
19
|
const { line: _dropped, ...rest } = withoutBaseline(d);
|
|
20
20
|
return { ...rest, kind: "BASE" };
|
|
21
21
|
}
|
|
22
|
-
/**
|
|
22
|
+
/**
|
|
23
|
+
* Apply a feature's ADDED/MODIFIED/REMOVED requirements onto a living requirement set.
|
|
24
|
+
*
|
|
25
|
+
* A living requirement the delta does not touch comes back as the SAME object
|
|
26
|
+
* it went in as (a living spec's requirements are already BASE), and that
|
|
27
|
+
* identity is load-bearing: archive keys each requirement's raw bytes on it, so
|
|
28
|
+
* an untouched requirement is written back exactly as its author wrote it and
|
|
29
|
+
* only the ones this function replaced or added are serialized.
|
|
30
|
+
*/
|
|
23
31
|
export function applyRequirementDelta(living, delta) {
|
|
24
32
|
const malformed = [...requirementIdProblems(living), ...requirementIdProblems(delta)];
|
|
25
33
|
if (malformed.length > 0) {
|
|
26
34
|
throw new Error("cannot merge requirements with invalid, repeated, or duplicate Requirement-ID declarations");
|
|
27
35
|
}
|
|
28
|
-
let result = living.map((r) => ({ ...r, kind: "BASE" }));
|
|
36
|
+
let result = living.map((r) => (r.kind === "BASE" ? r : { ...r, kind: "BASE" }));
|
|
29
37
|
for (const d of delta) {
|
|
30
38
|
// BASE is not a delta kind — a requirement outside an ADDED/MODIFIED/REMOVED
|
|
31
39
|
// section (e.g. quoted under ## Notes) is documentation, not a change.
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"apply.js","sourceRoot":"","sources":["../../../src/core/document/apply.ts"],"names":[],"mappings":"AAAA;;;;;;;;GAQG;AACH,OAAO,EAAE,qBAAqB,EAAE,eAAe,EAAoC,MAAM,WAAW,CAAC;AAErG;;;;;;GAMG;AACH,SAAS,QAAQ,CAAC,CAAc;IAC9B,MAAM,EAAE,IAAI,EAAE,QAAQ,EAAE,GAAG,IAAI,EAAE,GAAG,eAAe,CAAC,CAAC,CAAC,CAAC;IACvD,OAAO,EAAE,GAAG,IAAI,EAAE,IAAI,EAAE,MAAM,EAAE,CAAC;AACnC,CAAC;AAED
|
|
1
|
+
{"version":3,"file":"apply.js","sourceRoot":"","sources":["../../../src/core/document/apply.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * A feature's ADDED/MODIFIED/REMOVED requirements folded onto a living set.\n *\n * The one thing `loam archive` does to a spec, and a module of its own because\n * it is where a requirement stops being a delta: `asLiving` drops the\n * `Based-On:` pin, which is a claim about which living version this was written\n * against — meaningless once it IS the living version, and poison for the next\n * feature's baseline, which would otherwise hash the previous feature's pin.\n */\nimport { requirementIdProblems, withoutBaseline, type DeltaKind, type Requirement } from \"./spec.js\";\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 */\nfunction asLiving(d: Requirement): Requirement {\n const { line: _dropped, ...rest } = withoutBaseline(d);\n return { ...rest, kind: \"BASE\" };\n}\n\n/**\n * Apply a feature's ADDED/MODIFIED/REMOVED requirements onto a living requirement set.\n *\n * A living requirement the delta does not touch comes back as the SAME object\n * it went in as (a living spec's requirements are already BASE), and that\n * identity is load-bearing: archive keys each requirement's raw bytes on it, so\n * an untouched requirement is written back exactly as its author wrote it and\n * only the ones this function replaced or added are serialized.\n */\nexport function applyRequirementDelta(living: Requirement[], delta: Requirement[]): Requirement[] {\n const malformed = [...requirementIdProblems(living), ...requirementIdProblems(delta)];\n if (malformed.length > 0) {\n throw new Error(\"cannot merge requirements with invalid, repeated, or duplicate Requirement-ID declarations\");\n }\n let result: Requirement[] = living.map((r) => (r.kind === \"BASE\" ? r : { ...r, kind: \"BASE\" as DeltaKind }));\n for (const d of delta) {\n // BASE is not a delta kind — a requirement outside an ADDED/MODIFIED/REMOVED\n // section (e.g. quoted under ## Notes) is documentation, not a change.\n if (d.kind === \"BASE\") continue;\n if (d.id !== undefined) {\n const idMatches = result\n .map((r, i) => (r.id === d.id ? i : -1))\n .filter((i) => i >= 0);\n const nameMatches = result\n .map((r, i) => (r.name === d.name ? i : -1))\n .filter((i) => i >= 0);\n const otherNameMatches = nameMatches.filter((i) => !idMatches.includes(i));\n if (idMatches.length > 1 || otherNameMatches.length > 0) {\n throw new Error(\n `ambiguous requirement identity for '${d.name}' (${d.id}): Requirement-ID and heading select different living requirements`,\n );\n }\n if (d.kind === \"REMOVED\") {\n const selected = new Set(idMatches);\n result = result.filter((_, i) => !selected.has(i));\n } else {\n const merged: Requirement = asLiving(d);\n if (idMatches.length === 1) result[idMatches[0]!] = merged;\n else result.push(merged);\n }\n } else if (d.kind === \"REMOVED\") {\n result = result.filter((r) => r.name !== d.name);\n } else {\n const i = result.findIndex((r) => r.name === d.name);\n // A legacy delta may still modify an ID-bearing living requirement by\n // its exact heading. Keep the identity while the repository migrates.\n const inheritedId = i >= 0 ? result[i]!.id : undefined;\n const merged: Requirement = {\n ...asLiving(d),\n ...(inheritedId === undefined ? {} : { id: inheritedId }),\n };\n if (i >= 0) result[i] = merged;\n else result.push(merged);\n }\n }\n return result;\n}\n"]}
|
|
@@ -6,6 +6,7 @@
|
|
|
6
6
|
* are the only deterministic tie to reality: everything else loam checks is
|
|
7
7
|
* internal consistency, which a fluent enough fiction satisfies perfectly.
|
|
8
8
|
*/
|
|
9
|
+
import { createHash } from "node:crypto";
|
|
9
10
|
import { existsSync } from "node:fs";
|
|
10
11
|
import { readFile } from "node:fs/promises";
|
|
11
12
|
import { isMap, parse, parseDocument } from "yaml";
|
|
@@ -188,6 +189,60 @@ export function rawBody(source) {
|
|
|
188
189
|
const at = bounds(md);
|
|
189
190
|
return at === null ? md : md.slice(at.bodyStart);
|
|
190
191
|
}
|
|
192
|
+
/**
|
|
193
|
+
* The stamp `loam vouch` writes into `content_digest`: sha256 of the
|
|
194
|
+
* document's own BODY — every byte after the frontmatter block (below the
|
|
195
|
+
* closing `---` line and its newline; `rawBody` is the one definition of that
|
|
196
|
+
* cut) — first 16 hex characters, the sources recipe's length.
|
|
197
|
+
*
|
|
198
|
+
* Byte-exact, no normalization. Body-only is load-bearing: vouch itself
|
|
199
|
+
* rewrites the frontmatter as it stamps, and a later frontmatter-only edit
|
|
200
|
+
* (another vouch, a corrected owner) must not read as the document moving.
|
|
201
|
+
*
|
|
202
|
+
* It lived in core/provenance/stamp.ts beside the sources recipe until the
|
|
203
|
+
* fleet ENUMERATION needed it: `loam list` and `loam status` read a document
|
|
204
|
+
* edited under its vouch off `ServiceEntry` (core/repo/repo.ts), and core/repo
|
|
205
|
+
* cannot import core/provenance — that package resolves service paths through
|
|
206
|
+
* core/repo. Here, beside the cut it hashes, both can reach it.
|
|
207
|
+
*/
|
|
208
|
+
export function contentDigest(source) {
|
|
209
|
+
return createHash("sha256").update(rawBody(source), "utf8").digest("hex").slice(0, 16);
|
|
210
|
+
}
|
|
211
|
+
/**
|
|
212
|
+
* The `content_digest` a document's body is held to, or undefined when there
|
|
213
|
+
* is nothing to hold it to — `content.stale`'s precondition, spelled ONCE so
|
|
214
|
+
* the validate finding (core/provenance/findings.ts) and the fleet enumeration
|
|
215
|
+
* cannot disagree about which documents are stamped.
|
|
216
|
+
*
|
|
217
|
+
* Only a `verified` document: a draft's words are nobody's vouch yet. And a
|
|
218
|
+
* verified document with no stamp predates the field — quiet on purpose: the
|
|
219
|
+
* fix is re-vouching, and grading years of pre-feature vouches as suspect would
|
|
220
|
+
* drown the fleet in warnings nobody chose.
|
|
221
|
+
*
|
|
222
|
+
* Split from `movedUnderVouch` below so a reader that has only the header in
|
|
223
|
+
* hand — the fleet enumeration — can learn there is nothing to compare before
|
|
224
|
+
* it pays for reading the body.
|
|
225
|
+
*/
|
|
226
|
+
export function contentStamp(fm) {
|
|
227
|
+
return stringField(fm, "status") === "verified" ? stringField(fm, "content_digest") : undefined;
|
|
228
|
+
}
|
|
229
|
+
/**
|
|
230
|
+
* The one condition `content.stale` fires on: a `verified` document whose body
|
|
231
|
+
* no longer hashes to the `content_digest` its vouch stamped.
|
|
232
|
+
*
|
|
233
|
+
* One spelling for three readers that must give the same answer. `loam
|
|
234
|
+
* validate` grades it (core/provenance/findings.ts); the fleet enumeration
|
|
235
|
+
* reads it off every service (core/repo/repo.ts) so `loam list` and `loam
|
|
236
|
+
* status` cannot call fresh a document validate calls stale; and `loam archive`
|
|
237
|
+
* asks it of the document it is ABOUT to write, before the bytes exist, so it
|
|
238
|
+
* can name every vouched spec its merge moves under the stamp — and whose owner
|
|
239
|
+
* then owes a re-vouch. Two spellings would let the archive's list, the listing
|
|
240
|
+
* and the next validate disagree about which documents went stale.
|
|
241
|
+
*/
|
|
242
|
+
export function movedUnderVouch(fm, raw) {
|
|
243
|
+
const stamped = contentStamp(fm);
|
|
244
|
+
return stamped !== undefined && contentDigest(raw) !== stamped;
|
|
245
|
+
}
|
|
191
246
|
/**
|
|
192
247
|
* Read a markdown file's frontmatter; a missing file reads as absent.
|
|
193
248
|
*
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"frontmatter.js","sourceRoot":"","sources":["../../../src/core/document/frontmatter.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * YAML frontmatter — the provenance of an artifact.\n *\n * `status` says whether a human has vouched for the document, `sources` says\n * which code it was written from. When the prose is written by an agent, these\n * are the only deterministic tie to reality: everything else loam checks is\n * internal consistency, which a fluent enough fiction satisfies perfectly.\n */\nimport { existsSync } from \"node:fs\";\nimport { readFile } from \"node:fs/promises\";\nimport { isMap, parse, parseDocument } from \"yaml\";\nimport { decodeDocument, NotUtf8DocumentError } from \"../kernel/document-bytes.js\";\n\nexport interface Frontmatter {\n /** True when a terminated `---` block was found, even if it was empty. */\n present: boolean;\n /**\n * True when the block was found but its YAML cannot be read as a mapping —\n * a parse error, or a scalar/sequence document. `data` is then {}, which is\n * NOT the same fact as an empty header: the fields are unreadable, not\n * absent, and a checker that grades them \"missing\" sends the author adding\n * lines to a block YAML refuses to parse. Validate turns this flag into\n * `frontmatter.malformed`; the writer (withFrontmatterFields) has its own\n * replace-don't-merge rule for the same state.\n */\n malformed: boolean;\n /**\n * Why the FILE could not be read as text at all, or absent when it could —\n * set only by `readFrontmatter`, since a caller that already holds a string\n * has nothing left to decode.\n *\n * A third state, not a shade of the other two: `present: false` here means\n * nobody could look, where everywhere else it means nobody wrote a header.\n * A checker must ask this first, or it reports \"owner/status/sources missing\"\n * over a file whose lines are right there — the same false cascade\n * `malformed` exists to stop, one layer lower down.\n */\n unreadable?: string;\n /** The parsed mapping, or {} when absent or malformed. */\n data: Record<string, unknown>;\n /** The document with the frontmatter removed. */\n body: string;\n}\n\n/** Where the header sits in the raw text. */\ninterface Bounds {\n /** First character of the YAML text. */\n start: number;\n /** One past its last character — the newline before the closing fence included. */\n end: number;\n /** First character of the body. */\n bodyStart: number;\n}\n\n/**\n * Drop a leading byte-order mark.\n *\n * With one in front, the opening `---` is no longer at position 0, so the whole\n * header reads as absent: a documented artifact silently becomes an\n * undocumented one — no owner, no status, no `sources` — and nothing says why.\n * The same blindness `spec.ts` had at `^##`.\n *\n * Only at position 0: elsewhere U+FEFF is a zero-width no-break space and is the\n * author's content. Callers strip before measuring, so the offsets `bounds`\n * returns index the same string the caller then slices.\n */\nfunction stripBom(text: string): string {\n return text.charCodeAt(0) === 0xfeff ? text.slice(1) : text;\n}\n\n/**\n * A fence is a LINE of exactly three dashes. Trailing whitespace is tolerated\n * (`\\s` also swallows the CR of a CRLF file), trailing text is not: `--- title`\n * is prose, `----` is a horizontal rule. Matching the substring `---` instead —\n * as this used to — made both of those fences, so a stray rule closed the\n * header early and a `--- note` closer silently ate its own trailing text.\n */\nconst FENCE_RE = /^---\\s*$/;\n\n/**\n * Locate the frontmatter block. The opener is the very first line, the closer\n * the first subsequent fence line — a later horizontal rule is body, not a\n * second fence. No closer means an unterminated opener: not frontmatter at all.\n *\n * The reader and the writer both go through this, so they can never disagree\n * about which bytes are the header — the writer's promise to leave the body\n * untouched depends on that being one decision, not two.\n */\nfunction bounds(md: string): Bounds | null {\n const firstNl = md.indexOf(\"\\n\");\n if (!FENCE_RE.test(firstNl === -1 ? md : md.slice(0, firstNl))) return null;\n // A lone `---` with nothing after it is an unterminated opener.\n if (firstNl === -1) return null;\n const start = firstNl + 1;\n for (let lineStart = start; ; ) {\n const nl = md.indexOf(\"\\n\", lineStart);\n const line = nl === -1 ? md.slice(lineStart) : md.slice(lineStart, nl);\n if (FENCE_RE.test(line)) {\n // An empty block (`---\\n---`) has no text between the fences: start === end.\n return { start, end: lineStart, bodyStart: nl === -1 ? md.length : nl + 1 };\n }\n if (nl === -1) return null;\n lineStart = nl + 1;\n }\n}\n\n/**\n * Split a markdown document into its frontmatter and body. A document with no\n * frontmatter is returned whole rather than truncated.\n */\nexport function parseFrontmatter(source: string): Frontmatter {\n const md = stripBom(source);\n const at = bounds(md);\n if (at === null) return { present: false, malformed: false, data: {}, body: md };\n\n const yamlText = md.slice(at.start, at.end);\n const body = md.slice(at.bodyStart).trimStart();\n\n let data: Record<string, unknown> = {};\n let malformed = false;\n try {\n const parsed: unknown = parse(yamlText);\n if (parsed && typeof parsed === \"object\" && !Array.isArray(parsed)) {\n data = parsed as Record<string, unknown>;\n } else if (parsed !== null) {\n // A scalar or sequence header holds no fields to read — as unreadable as\n // broken YAML. null is the empty block (`---\\n---`): legal, and empty.\n malformed = true;\n }\n } catch {\n // A bad header must not make the document unreadable (the body is still the\n // body, and never crashes a command) — but the failure travels as a flag,\n // so the caller can say `frontmatter.malformed` instead of the false\n // cascade \"owner/status missing\" the silent {} used to produce.\n malformed = true;\n }\n return { present: true, malformed, data, body };\n}\n\n/** A field as text. Non-string scalars (a bare date, a number) are stringified. */\nexport function stringField(fm: Frontmatter, key: string): string | undefined {\n const v = fm.data[key];\n if (v === undefined || v === null) return undefined;\n if (typeof v === \"string\") return v;\n if (typeof v === \"number\" || typeof v === \"boolean\") return String(v);\n if (v instanceof Date) return v.toISOString().slice(0, 10);\n return undefined;\n}\n\n/** A field as a list. A lone scalar counts as a list of one. */\nexport function listField(fm: Frontmatter, key: string): string[] {\n const v = fm.data[key];\n if (v === undefined || v === null) return [];\n if (Array.isArray(v)) return v.map((x) => String(x)).filter((s) => s.length > 0);\n const one = stringField(fm, key);\n return one === undefined ? [] : [one];\n}\n\n/**\n * Return `md` with these frontmatter fields set and everything below the closing\n * fence byte-identical.\n *\n * The header is edited through yaml's document model rather than rebuilt from\n * the parsed data: keys keep their order and their comments, fields nobody named\n * are left exactly as they were, and a value that would read back as something\n * else — a digest of all digits, say — gets quoted for us. Stamping a document\n * is not a licence to reformat it: the diff a reviewer sees should be the lines\n * that changed, and nothing else.\n *\n * A document with no frontmatter gets one, and the text that was there becomes\n * the body. A header that does not parse (or is not a mapping) is replaced\n * rather than merged into — it cannot be edited without guessing what it meant.\n *\n * Keys named in `remove` are DELETED. That is not a convenience: `loam vouch`\n * stamps `vouch_scope` when a person read only a sample of the document, and\n * a full vouch afterwards has to take that claim back out. Left behind, a\n * stale scope would keep a fully-read document reading as sampled forever —\n * on `loam list`, in `loam validate`'s `sources.sampled-vouch`, and in the\n * frontmatter a person opens — which is the same class of lie as a stamp with\n * nothing behind it, pointing the other way.\n *\n * Deletion is a separate LIST rather than a null value in `fields`, and the\n * difference is the compiler's: `string | null` is the most common field shape\n * in this codebase (`stringField(fm, k) ?? null` appears in a dozen readers),\n * so a writer that stamped a field it had just read back — `{ owner }` — would\n * silently delete `owner` on every document that never had one, where today\n * that line does not compile. Clearing a key is something a call site says,\n * never something a value happens to be.\n */\nexport function withFrontmatterFields(\n source: string,\n fields: Record<string, string>,\n remove: readonly string[] = [],\n): string {\n // Stamping a BOM-prefixed document drops the BOM. It sits above the header, so\n // the promise about the body still holds — and leaving it would re-break the\n // file for every parser anchored at position 0, including ours.\n const md = stripBom(source);\n const at = bounds(md);\n const parsed = parseDocument(at === null ? \"\" : md.slice(at.start, at.end));\n const editable = parsed.errors.length === 0 && (parsed.contents === null || isMap(parsed.contents));\n const doc = editable ? parsed : parseDocument(\"\");\n\n for (const [key, value] of Object.entries(fields)) doc.set(key, value);\n // Guarded, not because a missing key is a problem — deleting one that is not\n // there is a no-op — but because yaml's `Document.delete` THROWS (\"Expected\n // a YAML collection as document contents\") when the document has no mapping\n // at all: the empty header of a document that had none, and the replacement\n // built for a header that would not parse. Both are states this writer\n // reaches on purpose, and in both there is nothing to clear.\n if (isMap(doc.contents)) for (const key of remove) doc.delete(key);\n const yamlText = doc.toString();\n\n if (at === null) return `---\\n${yamlText}---\\n\\n${md}`;\n return md.slice(0, at.start) + yamlText + md.slice(at.end);\n}\n\n/**\n * The document's body, byte-exact: everything after the frontmatter block —\n * the closing `---` line and its newline belong to the HEADER, every byte\n * after them (blank lines included) is body. A document with no frontmatter is\n * all body. This is what `content_digest` hashes, and it deliberately does NOT\n * go through `parseFrontmatter` (whose `body` is trimmed for callers that want\n * text): the digest's writer and its checker must slice the same bytes, so\n * both go through here.\n */\nexport function rawBody(source: string): string {\n const md = stripBom(source);\n const at = bounds(md);\n return at === null ? md : md.slice(at.bodyStart);\n}\n\n/**\n * Read a markdown file's frontmatter; a missing file reads as absent.\n *\n * Bytes in, decoded through `decodeDocument`, because `readFile(path, \"utf8\")`\n * turns a UTF-16 spec.md into mojibake whose opening `---` is no longer at\n * position 0 — so the header reads as ABSENT, and `status`, `owner` and\n * `sources` all come back missing over a file that spells every one of them.\n * The same blindness `stripBom` exists to stop, one encoding wider.\n *\n * The failure travels as a FLAG rather than an exception, which is how every\n * reader here degrades (`readOpenapi`'s `unreadable`) and is load-bearing at\n * exactly this call: this is the read behind the fleet ENUMERATION\n * (repo.ts `listServices`), so throwing would turn one PowerShell-saved file\n * into a `loam list` that reports nothing about the other 119 services.\n * Parsers never diagnose; a checker grades the flag.\n */\nexport async function readFrontmatter(path: string): Promise<Frontmatter> {\n if (!existsSync(path)) return { present: false, malformed: false, data: {}, body: \"\" };\n let text: string;\n try {\n text = decodeDocument(await readFile(path), path);\n } catch (err) {\n if (!(err instanceof NotUtf8DocumentError)) throw err;\n return { present: false, malformed: false, unreadable: err.message, data: {}, body: \"\" };\n }\n return parseFrontmatter(text);\n}\n\n/** The statuses each kind of artifact may carry. */\nexport const SERVICE_STATUSES = [\"draft\", \"verified\"] as const;\nexport const FEATURE_STATUSES = [\"proposed\", \"in_progress\", \"built\", \"done\"] as const;\n\n/**\n * A document's prose with any leading frontmatter block dropped — what\n * `loam delta` and `loam context` print as a feature's intent.\n *\n * Moved verbatim from `commands/delta/slices.ts` when the context pack became\n * its second caller. It deliberately keeps its own, looser fence rule\n * (`\\n---` as a substring, not `bounds()`'s line-exact match) rather than\n * delegating to `parseFrontmatter(md).body`: the two agree on every header\n * loam writes, but the delta brief's output is pinned byte-for-byte by its\n * tests, and unifying the rules is a behaviour change to make deliberately,\n * not as a side effect of a move.\n */\nexport function stripFrontmatter(md: string): string {\n if (!md.startsWith(\"---\")) return md;\n const close = md.indexOf(\"\\n---\", 3);\n if (close === -1) return md;\n const nl = md.indexOf(\"\\n\", close + 1);\n return nl === -1 ? \"\" : md.slice(nl + 1).trimStart();\n}\n"]}
|
|
1
|
+
{"version":3,"file":"frontmatter.js","sourceRoot":"","sources":["../../../src/core/document/frontmatter.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * YAML frontmatter — the provenance of an artifact.\n *\n * `status` says whether a human has vouched for the document, `sources` says\n * which code it was written from. When the prose is written by an agent, these\n * are the only deterministic tie to reality: everything else loam checks is\n * internal consistency, which a fluent enough fiction satisfies perfectly.\n */\nimport { createHash } from \"node:crypto\";\nimport { existsSync } from \"node:fs\";\nimport { readFile } from \"node:fs/promises\";\nimport { isMap, parse, parseDocument } from \"yaml\";\nimport { decodeDocument, NotUtf8DocumentError } from \"../kernel/document-bytes.js\";\n\nexport interface Frontmatter {\n /** True when a terminated `---` block was found, even if it was empty. */\n present: boolean;\n /**\n * True when the block was found but its YAML cannot be read as a mapping —\n * a parse error, or a scalar/sequence document. `data` is then {}, which is\n * NOT the same fact as an empty header: the fields are unreadable, not\n * absent, and a checker that grades them \"missing\" sends the author adding\n * lines to a block YAML refuses to parse. Validate turns this flag into\n * `frontmatter.malformed`; the writer (withFrontmatterFields) has its own\n * replace-don't-merge rule for the same state.\n */\n malformed: boolean;\n /**\n * Why the FILE could not be read as text at all, or absent when it could —\n * set only by `readFrontmatter`, since a caller that already holds a string\n * has nothing left to decode.\n *\n * A third state, not a shade of the other two: `present: false` here means\n * nobody could look, where everywhere else it means nobody wrote a header.\n * A checker must ask this first, or it reports \"owner/status/sources missing\"\n * over a file whose lines are right there — the same false cascade\n * `malformed` exists to stop, one layer lower down.\n */\n unreadable?: string;\n /** The parsed mapping, or {} when absent or malformed. */\n data: Record<string, unknown>;\n /** The document with the frontmatter removed. */\n body: string;\n}\n\n/** Where the header sits in the raw text. */\ninterface Bounds {\n /** First character of the YAML text. */\n start: number;\n /** One past its last character — the newline before the closing fence included. */\n end: number;\n /** First character of the body. */\n bodyStart: number;\n}\n\n/**\n * Drop a leading byte-order mark.\n *\n * With one in front, the opening `---` is no longer at position 0, so the whole\n * header reads as absent: a documented artifact silently becomes an\n * undocumented one — no owner, no status, no `sources` — and nothing says why.\n * The same blindness `spec.ts` had at `^##`.\n *\n * Only at position 0: elsewhere U+FEFF is a zero-width no-break space and is the\n * author's content. Callers strip before measuring, so the offsets `bounds`\n * returns index the same string the caller then slices.\n */\nfunction stripBom(text: string): string {\n return text.charCodeAt(0) === 0xfeff ? text.slice(1) : text;\n}\n\n/**\n * A fence is a LINE of exactly three dashes. Trailing whitespace is tolerated\n * (`\\s` also swallows the CR of a CRLF file), trailing text is not: `--- title`\n * is prose, `----` is a horizontal rule. Matching the substring `---` instead —\n * as this used to — made both of those fences, so a stray rule closed the\n * header early and a `--- note` closer silently ate its own trailing text.\n */\nconst FENCE_RE = /^---\\s*$/;\n\n/**\n * Locate the frontmatter block. The opener is the very first line, the closer\n * the first subsequent fence line — a later horizontal rule is body, not a\n * second fence. No closer means an unterminated opener: not frontmatter at all.\n *\n * The reader and the writer both go through this, so they can never disagree\n * about which bytes are the header — the writer's promise to leave the body\n * untouched depends on that being one decision, not two.\n */\nfunction bounds(md: string): Bounds | null {\n const firstNl = md.indexOf(\"\\n\");\n if (!FENCE_RE.test(firstNl === -1 ? md : md.slice(0, firstNl))) return null;\n // A lone `---` with nothing after it is an unterminated opener.\n if (firstNl === -1) return null;\n const start = firstNl + 1;\n for (let lineStart = start; ; ) {\n const nl = md.indexOf(\"\\n\", lineStart);\n const line = nl === -1 ? md.slice(lineStart) : md.slice(lineStart, nl);\n if (FENCE_RE.test(line)) {\n // An empty block (`---\\n---`) has no text between the fences: start === end.\n return { start, end: lineStart, bodyStart: nl === -1 ? md.length : nl + 1 };\n }\n if (nl === -1) return null;\n lineStart = nl + 1;\n }\n}\n\n/**\n * Split a markdown document into its frontmatter and body. A document with no\n * frontmatter is returned whole rather than truncated.\n */\nexport function parseFrontmatter(source: string): Frontmatter {\n const md = stripBom(source);\n const at = bounds(md);\n if (at === null) return { present: false, malformed: false, data: {}, body: md };\n\n const yamlText = md.slice(at.start, at.end);\n const body = md.slice(at.bodyStart).trimStart();\n\n let data: Record<string, unknown> = {};\n let malformed = false;\n try {\n const parsed: unknown = parse(yamlText);\n if (parsed && typeof parsed === \"object\" && !Array.isArray(parsed)) {\n data = parsed as Record<string, unknown>;\n } else if (parsed !== null) {\n // A scalar or sequence header holds no fields to read — as unreadable as\n // broken YAML. null is the empty block (`---\\n---`): legal, and empty.\n malformed = true;\n }\n } catch {\n // A bad header must not make the document unreadable (the body is still the\n // body, and never crashes a command) — but the failure travels as a flag,\n // so the caller can say `frontmatter.malformed` instead of the false\n // cascade \"owner/status missing\" the silent {} used to produce.\n malformed = true;\n }\n return { present: true, malformed, data, body };\n}\n\n/** A field as text. Non-string scalars (a bare date, a number) are stringified. */\nexport function stringField(fm: Frontmatter, key: string): string | undefined {\n const v = fm.data[key];\n if (v === undefined || v === null) return undefined;\n if (typeof v === \"string\") return v;\n if (typeof v === \"number\" || typeof v === \"boolean\") return String(v);\n if (v instanceof Date) return v.toISOString().slice(0, 10);\n return undefined;\n}\n\n/** A field as a list. A lone scalar counts as a list of one. */\nexport function listField(fm: Frontmatter, key: string): string[] {\n const v = fm.data[key];\n if (v === undefined || v === null) return [];\n if (Array.isArray(v)) return v.map((x) => String(x)).filter((s) => s.length > 0);\n const one = stringField(fm, key);\n return one === undefined ? [] : [one];\n}\n\n/**\n * Return `md` with these frontmatter fields set and everything below the closing\n * fence byte-identical.\n *\n * The header is edited through yaml's document model rather than rebuilt from\n * the parsed data: keys keep their order and their comments, fields nobody named\n * are left exactly as they were, and a value that would read back as something\n * else — a digest of all digits, say — gets quoted for us. Stamping a document\n * is not a licence to reformat it: the diff a reviewer sees should be the lines\n * that changed, and nothing else.\n *\n * A document with no frontmatter gets one, and the text that was there becomes\n * the body. A header that does not parse (or is not a mapping) is replaced\n * rather than merged into — it cannot be edited without guessing what it meant.\n *\n * Keys named in `remove` are DELETED. That is not a convenience: `loam vouch`\n * stamps `vouch_scope` when a person read only a sample of the document, and\n * a full vouch afterwards has to take that claim back out. Left behind, a\n * stale scope would keep a fully-read document reading as sampled forever —\n * on `loam list`, in `loam validate`'s `sources.sampled-vouch`, and in the\n * frontmatter a person opens — which is the same class of lie as a stamp with\n * nothing behind it, pointing the other way.\n *\n * Deletion is a separate LIST rather than a null value in `fields`, and the\n * difference is the compiler's: `string | null` is the most common field shape\n * in this codebase (`stringField(fm, k) ?? null` appears in a dozen readers),\n * so a writer that stamped a field it had just read back — `{ owner }` — would\n * silently delete `owner` on every document that never had one, where today\n * that line does not compile. Clearing a key is something a call site says,\n * never something a value happens to be.\n */\nexport function withFrontmatterFields(\n source: string,\n fields: Record<string, string>,\n remove: readonly string[] = [],\n): string {\n // Stamping a BOM-prefixed document drops the BOM. It sits above the header, so\n // the promise about the body still holds — and leaving it would re-break the\n // file for every parser anchored at position 0, including ours.\n const md = stripBom(source);\n const at = bounds(md);\n const parsed = parseDocument(at === null ? \"\" : md.slice(at.start, at.end));\n const editable = parsed.errors.length === 0 && (parsed.contents === null || isMap(parsed.contents));\n const doc = editable ? parsed : parseDocument(\"\");\n\n for (const [key, value] of Object.entries(fields)) doc.set(key, value);\n // Guarded, not because a missing key is a problem — deleting one that is not\n // there is a no-op — but because yaml's `Document.delete` THROWS (\"Expected\n // a YAML collection as document contents\") when the document has no mapping\n // at all: the empty header of a document that had none, and the replacement\n // built for a header that would not parse. Both are states this writer\n // reaches on purpose, and in both there is nothing to clear.\n if (isMap(doc.contents)) for (const key of remove) doc.delete(key);\n const yamlText = doc.toString();\n\n if (at === null) return `---\\n${yamlText}---\\n\\n${md}`;\n return md.slice(0, at.start) + yamlText + md.slice(at.end);\n}\n\n/**\n * The document's body, byte-exact: everything after the frontmatter block —\n * the closing `---` line and its newline belong to the HEADER, every byte\n * after them (blank lines included) is body. A document with no frontmatter is\n * all body. This is what `content_digest` hashes, and it deliberately does NOT\n * go through `parseFrontmatter` (whose `body` is trimmed for callers that want\n * text): the digest's writer and its checker must slice the same bytes, so\n * both go through here.\n */\nexport function rawBody(source: string): string {\n const md = stripBom(source);\n const at = bounds(md);\n return at === null ? md : md.slice(at.bodyStart);\n}\n\n/**\n * The stamp `loam vouch` writes into `content_digest`: sha256 of the\n * document's own BODY — every byte after the frontmatter block (below the\n * closing `---` line and its newline; `rawBody` is the one definition of that\n * cut) — first 16 hex characters, the sources recipe's length.\n *\n * Byte-exact, no normalization. Body-only is load-bearing: vouch itself\n * rewrites the frontmatter as it stamps, and a later frontmatter-only edit\n * (another vouch, a corrected owner) must not read as the document moving.\n *\n * It lived in core/provenance/stamp.ts beside the sources recipe until the\n * fleet ENUMERATION needed it: `loam list` and `loam status` read a document\n * edited under its vouch off `ServiceEntry` (core/repo/repo.ts), and core/repo\n * cannot import core/provenance — that package resolves service paths through\n * core/repo. Here, beside the cut it hashes, both can reach it.\n */\nexport function contentDigest(source: string): string {\n return createHash(\"sha256\").update(rawBody(source), \"utf8\").digest(\"hex\").slice(0, 16);\n}\n\n/**\n * The `content_digest` a document's body is held to, or undefined when there\n * is nothing to hold it to — `content.stale`'s precondition, spelled ONCE so\n * the validate finding (core/provenance/findings.ts) and the fleet enumeration\n * cannot disagree about which documents are stamped.\n *\n * Only a `verified` document: a draft's words are nobody's vouch yet. And a\n * verified document with no stamp predates the field — quiet on purpose: the\n * fix is re-vouching, and grading years of pre-feature vouches as suspect would\n * drown the fleet in warnings nobody chose.\n *\n * Split from `movedUnderVouch` below so a reader that has only the header in\n * hand — the fleet enumeration — can learn there is nothing to compare before\n * it pays for reading the body.\n */\nexport function contentStamp(fm: Frontmatter): string | undefined {\n return stringField(fm, \"status\") === \"verified\" ? stringField(fm, \"content_digest\") : undefined;\n}\n\n/**\n * The one condition `content.stale` fires on: a `verified` document whose body\n * no longer hashes to the `content_digest` its vouch stamped.\n *\n * One spelling for three readers that must give the same answer. `loam\n * validate` grades it (core/provenance/findings.ts); the fleet enumeration\n * reads it off every service (core/repo/repo.ts) so `loam list` and `loam\n * status` cannot call fresh a document validate calls stale; and `loam archive`\n * asks it of the document it is ABOUT to write, before the bytes exist, so it\n * can name every vouched spec its merge moves under the stamp — and whose owner\n * then owes a re-vouch. Two spellings would let the archive's list, the listing\n * and the next validate disagree about which documents went stale.\n */\nexport function movedUnderVouch(fm: Frontmatter, raw: string): boolean {\n const stamped = contentStamp(fm);\n return stamped !== undefined && contentDigest(raw) !== stamped;\n}\n\n/**\n * Read a markdown file's frontmatter; a missing file reads as absent.\n *\n * Bytes in, decoded through `decodeDocument`, because `readFile(path, \"utf8\")`\n * turns a UTF-16 spec.md into mojibake whose opening `---` is no longer at\n * position 0 — so the header reads as ABSENT, and `status`, `owner` and\n * `sources` all come back missing over a file that spells every one of them.\n * The same blindness `stripBom` exists to stop, one encoding wider.\n *\n * The failure travels as a FLAG rather than an exception, which is how every\n * reader here degrades (`readOpenapi`'s `unreadable`) and is load-bearing at\n * exactly this call: this is the read behind the fleet ENUMERATION\n * (repo.ts `listServices`), so throwing would turn one PowerShell-saved file\n * into a `loam list` that reports nothing about the other 119 services.\n * Parsers never diagnose; a checker grades the flag.\n */\nexport async function readFrontmatter(path: string): Promise<Frontmatter> {\n if (!existsSync(path)) return { present: false, malformed: false, data: {}, body: \"\" };\n let text: string;\n try {\n text = decodeDocument(await readFile(path), path);\n } catch (err) {\n if (!(err instanceof NotUtf8DocumentError)) throw err;\n return { present: false, malformed: false, unreadable: err.message, data: {}, body: \"\" };\n }\n return parseFrontmatter(text);\n}\n\n/** The statuses each kind of artifact may carry. */\nexport const SERVICE_STATUSES = [\"draft\", \"verified\"] as const;\nexport const FEATURE_STATUSES = [\"proposed\", \"in_progress\", \"built\", \"done\"] as const;\n\n/**\n * A document's prose with any leading frontmatter block dropped — what\n * `loam delta` and `loam context` print as a feature's intent.\n *\n * Moved verbatim from `commands/delta/slices.ts` when the context pack became\n * its second caller. It deliberately keeps its own, looser fence rule\n * (`\\n---` as a substring, not `bounds()`'s line-exact match) rather than\n * delegating to `parseFrontmatter(md).body`: the two agree on every header\n * loam writes, but the delta brief's output is pinned byte-for-byte by its\n * tests, and unifying the rules is a behaviour change to make deliberately,\n * not as a side effect of a move.\n */\nexport function stripFrontmatter(md: string): string {\n if (!md.startsWith(\"---\")) return md;\n const close = md.indexOf(\"\\n---\", 3);\n if (close === -1) return md;\n const nl = md.indexOf(\"\\n\", close + 1);\n return nl === -1 ? \"\" : md.slice(nl + 1).trimStart();\n}\n"]}
|
|
@@ -73,6 +73,17 @@ export function sectionHeadings(md) {
|
|
|
73
73
|
* next `## ` heading, or EOF). Empty when the section holds no requirements.
|
|
74
74
|
* - `tail` — everything from the section's end onward.
|
|
75
75
|
*
|
|
76
|
+
* `requirements` is the run cut again at every `### Requirement:` heading —
|
|
77
|
+
* one raw slice per requirement, from its heading up to the next one (or the
|
|
78
|
+
* run's end), so `requirements.join("") === run`. A slice therefore carries
|
|
79
|
+
* whatever the author wrote below the requirement before the next heading,
|
|
80
|
+
* blank lines included, which is exactly the text parseRequirements attributes
|
|
81
|
+
* to that requirement. It exists so archive can keep the bytes of every
|
|
82
|
+
* requirement a delta did not touch instead of re-serializing the whole run:
|
|
83
|
+
* one ADDED requirement used to reflow every untouched requirement written
|
|
84
|
+
* without a blank line under its heading, and every one of those moves landed
|
|
85
|
+
* under the document's vouch stamp.
|
|
86
|
+
*
|
|
76
87
|
* Returns null when the document has no `## Requirements` heading at all.
|
|
77
88
|
* Only the FIRST matching heading opens the section; a caller that must not
|
|
78
89
|
* lose content behind a duplicate (archive) counts matches via
|
|
@@ -84,7 +95,7 @@ export function splitRequirementsSection(text) {
|
|
|
84
95
|
const fenced = fenceTracker();
|
|
85
96
|
let inSection = false;
|
|
86
97
|
let sectionStart = -1;
|
|
87
|
-
|
|
98
|
+
const starts = [];
|
|
88
99
|
let sectionEnd = text.length;
|
|
89
100
|
let offset = 0;
|
|
90
101
|
while (offset < text.length) {
|
|
@@ -107,16 +118,21 @@ export function splitRequirementsSection(text) {
|
|
|
107
118
|
inSection = true;
|
|
108
119
|
}
|
|
109
120
|
}
|
|
110
|
-
else if (inSection &&
|
|
111
|
-
|
|
121
|
+
else if (inSection && REQ_RE.test(line)) {
|
|
122
|
+
starts.push(offset);
|
|
112
123
|
}
|
|
113
124
|
}
|
|
114
125
|
offset = lineEnd;
|
|
115
126
|
}
|
|
116
127
|
if (sectionStart === -1)
|
|
117
128
|
return null;
|
|
118
|
-
const cut =
|
|
119
|
-
return {
|
|
129
|
+
const cut = starts[0] ?? sectionEnd;
|
|
130
|
+
return {
|
|
131
|
+
head: text.slice(0, cut),
|
|
132
|
+
run: text.slice(cut, sectionEnd),
|
|
133
|
+
tail: text.slice(sectionEnd),
|
|
134
|
+
requirements: starts.map((at, i) => text.slice(at, starts[i + 1] ?? sectionEnd)),
|
|
135
|
+
};
|
|
120
136
|
}
|
|
121
137
|
/**
|
|
122
138
|
* A requirement document from disk as text — decoded, or refused by name.
|
|
@@ -192,7 +208,14 @@ export function parseRequirements(md) {
|
|
|
192
208
|
}
|
|
193
209
|
const ms = SCN_RE.exec(line);
|
|
194
210
|
if (ms && req) {
|
|
195
|
-
|
|
211
|
+
// `index + 1` is the heading's own 1-based line, exactly as the
|
|
212
|
+
// requirement capture above records its `### Requirement:` — body line
|
|
213
|
+
// `lines[i]` then sits at `line + 1 + i`, because the capture below is
|
|
214
|
+
// contiguous from here to the next heading. Purely additive: nothing in
|
|
215
|
+
// `serializeRequirements` reads it, so `requirementDigest` cannot see it
|
|
216
|
+
// and no `Based-On:` pin in any living document moves because the parser
|
|
217
|
+
// learned where a scenario starts.
|
|
218
|
+
scn = { name: ms[1], lines: [], line: index + 1 };
|
|
196
219
|
req.scenarios.push(scn);
|
|
197
220
|
continue;
|
|
198
221
|
}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"parse.js","sourceRoot":"","sources":["../../../src/core/document/parse.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * Markdown in: headings, fences, and the one `## Requirements` cut point.\n *\n * The cut point is spelled once here on purpose. The archive cut, archive's\n * stray-requirement guard and the delta-shape exemption used to spell the same\n * invariant three different ways, and one of them was a PREFIX match that also\n * hit `## Requirements Extra`.\n *\n * Fence tracking is why this is not a set of regexes at the call sites: a\n * `### Requirement:` inside a fenced code block is prose about a requirement,\n * not one, and every heading walk here has to agree about that.\n */\nimport { readFile } from \"node:fs/promises\";\nimport { decodeDocument } from \"../kernel/document-bytes.js\";\nimport { fenceTracker } from \"../kernel/fences.js\";\nimport {\n BASED_ON_LINE_RE, CAPABILITY_LINE_RE, KIND_RE, REALIZES_LINE_RE, REQUIREMENT_ID_LINE_RE,\n type DeltaKind, type Requirement, type Scenario,\n} from \"./spec.js\";\n\nconst REQ_RE = /^###\\s+Requirement:\\s*(.+?)\\s*$/;\nconst SCN_RE = /^####\\s+Scenario:\\s*(.+?)\\s*$/;\n\n/**\n * THE requirements section: the one H2 a living spec keeps its requirements\n * under, the one section `loam archive` rewrites, and the one heading a delta\n * may legally quote the living state beneath. One definition on purpose — the\n * archive cut point, archive's stray-requirement guard, and the delta-shape\n * exemption (core/delta/document.ts) used to spell this invariant three different ways,\n * and the cut point's substring `indexOf(\"\\n## Requirements\")` was a PREFIX\n * match that also hit `## Requirements Extra`. Full-line match on a trimmed\n * heading, with KIND_RE's case and interior-whitespace tolerance.\n */\nconst REQUIREMENTS_SECTION_RE = /^##\\s+Requirements\\s*$/i;\n\n/** Is this H2 heading line (as sectionHeadings/`Requirement.section` spell it) the `## Requirements` heading? */\nexport function isRequirementsHeading(heading: string): boolean {\n return REQUIREMENTS_SECTION_RE.test(heading.trim());\n}\n\n/**\n * Strip a leading UTF-8 BOM (U+FEFF), which editors on Windows write unasked.\n *\n * It has to happen here, at the two functions that take raw markdown, rather than\n * at each `readFile` call: those are spread across half the commands, so any one of\n * them could forget, and the failure is invisible. One byte was enough to void an\n * entire delta — `KIND_RE` is anchored `^##`, delta files routinely open with\n * `## MODIFIED Requirements` on line 1, and behind a BOM that line stopped being a\n * heading, so every requirement in the file fell back to BASE and\n * `applyRequirementDelta` skips BASE. Archive merged nothing and said nothing.\n * OpenSpec's own reader strips it for exactly this reason.\n *\n * Only at position 0: elsewhere U+FEFF is a zero-width no-break space and belongs\n * to the author's text.\n */\nfunction stripBom(md: string): string {\n return md.charCodeAt(0) === 0xfeff ? md.slice(1) : md;\n}\n\n/** Every H2 heading outside a fenced block, with its 1-based line number. */\nexport function sectionHeadings(md: string): Array<{ text: string; line: number }> {\n const fenced = fenceTracker();\n const out: Array<{ text: string; line: number }> = [];\n stripBom(md).split(/\\r?\\n/).forEach((line, i) => {\n if (fenced(line)) return;\n if (/^##\\s+/.test(line) && !line.startsWith(\"###\")) out.push({ text: line.trim(), line: i + 1 });\n });\n return out;\n}\n\n/**\n * Split a living spec around its requirements RUN — the stretch of\n * `### Requirement:` blocks inside the `## Requirements` section — so a merge\n * can rewrite exactly that and nothing else. All three slices reassemble the\n * input byte-for-byte (`head + run + tail === text`):\n *\n * - `head` — everything before the first requirement in the section: the\n * intro, the heading line itself, any prose under the heading.\n * - `run` — from the first `### Requirement:` line to the section's end (the\n * next `## ` heading, or EOF). Empty when the section holds no requirements.\n * - `tail` — everything from the section's end onward.\n *\n * Returns null when the document has no `## Requirements` heading at all.\n * Only the FIRST matching heading opens the section; a caller that must not\n * lose content behind a duplicate (archive) counts matches via\n * sectionHeadings() and refuses. Heading detection matches parseRequirements:\n * fenced lines are never structure, and a leading BOM hides no heading — but\n * the slices themselves are raw bytes, BOM and CRLF included.\n */\nexport function splitRequirementsSection(\n text: string,\n): { head: string; run: string; tail: string } | null {\n const fenced = fenceTracker();\n let inSection = false;\n let sectionStart = -1;\n let runStart = -1;\n let sectionEnd = text.length;\n let offset = 0;\n while (offset < text.length) {\n const nl = text.indexOf(\"\\n\", offset);\n const lineEnd = nl === -1 ? text.length : nl + 1;\n let line = text.slice(offset, nl === -1 ? text.length : nl);\n if (line.endsWith(\"\\r\")) line = line.slice(0, -1);\n // Position 0 only, mirroring stripBom: elsewhere U+FEFF is content.\n if (offset === 0 && line.charCodeAt(0) === 0xfeff) line = line.slice(1);\n if (!fenced(line)) {\n if (/^##\\s+/.test(line) && !line.startsWith(\"###\")) {\n if (inSection) {\n sectionEnd = offset;\n inSection = false;\n } else if (sectionStart === -1 && isRequirementsHeading(line)) {\n sectionStart = offset;\n inSection = true;\n }\n } else if (inSection && runStart === -1 && REQ_RE.test(line)) {\n runStart = offset;\n }\n }\n offset = lineEnd;\n }\n if (sectionStart === -1) return null;\n const cut = runStart === -1 ? sectionEnd : runStart;\n return { head: text.slice(0, cut), run: text.slice(cut, sectionEnd), tail: text.slice(sectionEnd) };\n}\n\n/**\n * A requirement document from disk as text — decoded, or refused by name.\n *\n * The read that belongs with the parser, because `readFile(path, \"utf8\")` and\n * `parseRequirements` together are a trap: a UTF-16 spec.md decodes to a string\n * in which no line matches `^###\\s+Requirement:`, so the parser returns [] and\n * the document grades as an empty baseline instead of an unreadable one. Every\n * question loam asks of a spec — is it covered, does the delta merge, what does\n * the C4 spine touch — is then answered about a file nobody read.\n *\n * It THROWS (`NotUtf8DocumentError`, which names the path), unlike\n * `readFrontmatter` next door, because the two failures are not the same\n * failure. Frontmatter is read by the fleet enumeration, where one bad file\n * must not cost the other 119 services, so there it is a flag. A requirement\n * document is read by ONE target, and the caller is the only one who knows what\n * to do about it: a read-only command turns it into a finding on that target\n * (validate's `guarded`, which names the file and keeps grading the rest), a\n * command that writes refuses outright rather than computing over mojibake.\n */\nexport async function readRequirementsDocument(path: string): Promise<string> {\n return decodeDocument(await readFile(path), path);\n}\n\n/** Parse all requirements (with their scenarios and delta kind) from a markdown doc. */\nexport function parseRequirements(md: string): Requirement[] {\n const out: Requirement[] = [];\n let kind: DeltaKind = \"BASE\";\n let section: string | undefined;\n let req: Requirement | null = null;\n let scn: Scenario | null = null;\n const fenced = fenceTracker();\n\n for (const [index, line] of stripBom(md).split(/\\r?\\n/).entries()) {\n if (fenced(line)) {\n // Fenced content, marker included: body of whatever is open, never structure.\n if (scn) scn.lines.push(line);\n else if (req) req.text.push(line);\n continue;\n }\n // Any H2 heading ends the current requirement/scenario capture — section prose\n // must not leak into the previous scenario's body.\n if (/^##\\s+/.test(line) && !line.startsWith(\"###\")) {\n req = null;\n scn = null;\n section = line.trim();\n const mk = KIND_RE.exec(line);\n // A non-delta H2 (## Notes, ## Requirements…) starts an unrelated section:\n // requirements under it are plain BASE, not part of a stale delta section.\n kind = mk ? (mk[1]!.toUpperCase() as DeltaKind) : \"BASE\";\n continue;\n }\n const mr = REQ_RE.exec(line);\n if (mr) {\n req = {\n kind,\n name: mr[1]!,\n text: [],\n operations: [],\n covers: [],\n publishes: [],\n consumes: [],\n requires: [],\n capabilities: [],\n realizes: [],\n scenarios: [],\n section,\n line: index + 1,\n };\n out.push(req);\n scn = null;\n continue;\n }\n const ms = SCN_RE.exec(line);\n if (ms && req) {\n scn = { name: ms[1]!, lines: [] };\n req.scenarios.push(scn);\n continue;\n }\n if (scn) {\n scn.lines.push(line);\n } else if (req) {\n req.text.push(line);\n const mi = REQUIREMENT_ID_LINE_RE.exec(line);\n if (mi) req.id = mi[1]!.trim();\n // Same keep-last quirk as Operations:/Covers: below — a repeated line is\n // a document problem, reported as one (`delta.baseline-invalid`), never\n // resolved differently by whoever happens to be reading.\n const mb = BASED_ON_LINE_RE.exec(line);\n if (mb) req.basedOn = mb[1]!.trim();\n const mo = /^\\s*Operations?:\\s*(.+?)\\s*$/i.exec(line);\n if (mo) req.operations = mo[1]!.split(\",\").map((s) => s.trim()).filter((s) => s.length > 0);\n // The Covers: mirror — same placement (requirement body, not a scenario),\n // same comma grammar, and deliberately the same quirk: a second line\n // REPLACES the first (assignment, not append), so the two lists can never\n // drift apart in how they read a repeated line.\n const mc = /^\\s*Covers?:\\s*(.+?)\\s*$/i.exec(line);\n if (mc) req.covers = mc[1]!.split(\",\").map((s) => s.trim()).filter((s) => s.length > 0);\n // The event axis, same grammar and the same keep-last quirk again. Note\n // what is NOT happening here: the line stays in `req.text` like every\n // other body line (the push above is unconditional), so it round-trips\n // through `serializeRequirements` and rides inside `requirementDigest`\n // exactly as `Operations:` does. That is what makes this parse purely\n // additive — no living document's digest moves, and no `Based-On:` pin\n // goes stale because loam learned to read one more line.\n const mp = /^\\s*Publishes?:\\s*(.+?)\\s*$/i.exec(line);\n if (mp) req.publishes = mp[1]!.split(\",\").map((s) => s.trim()).filter((s) => s.length > 0);\n const mcon = /^\\s*Consumes?:\\s*(.+?)\\s*$/i.exec(line);\n if (mcon) req.consumes = mcon[1]!.split(\",\").map((s) => s.trim()).filter((s) => s.length > 0);\n // The authorization axis. Same grammar, same keep-last quirk, same purely\n // additive parse — the line rides in `req.text` and therefore inside\n // `requirementDigest`, so no living document moves because loam learned\n // to read it.\n const mreq = /^\\s*Requires?:\\s*(.+?)\\s*$/i.exec(line);\n if (mreq) req.requires = mreq[1]!.split(\",\").map((s) => s.trim()).filter((s) => s.length > 0);\n // The capability axis. Same grammar, same keep-last quirk, same purely\n // additive parse as Requires: above — but note the OPT-IN differs: the\n // vocabulary FILE (architecture/capabilities.yaml) is the opt-in for this\n // axis, not the line, so entries parsed here grade as nothing at all\n // until a fleet writes that file (core/capabilities/findings.ts).\n const mcap = CAPABILITY_LINE_RE.exec(line);\n if (mcap) req.capabilities = mcap[1]!.split(\",\").map((s) => s.trim()).filter((s) => s.length > 0);\n // The join INTO the authored business tree: `<capability-id>#<Requirement-ID>`\n // entries, same comma grammar and same keep-last quirk as every line\n // above. Split into its two halves nowhere near here —\n // `core/capabilities/realizes/join.ts` owns that, because the rollup and\n // the grades must not be able to disagree about where the separator is.\n const mrl = REALIZES_LINE_RE.exec(line);\n if (mrl) req.realizes = mrl[1]!.split(\",\").map((s) => s.trim()).filter((s) => s.length > 0);\n }\n }\n\n return out;\n}\n"]}
|
|
1
|
+
{"version":3,"file":"parse.js","sourceRoot":"","sources":["../../../src/core/document/parse.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * Markdown in: headings, fences, and the one `## Requirements` cut point.\n *\n * The cut point is spelled once here on purpose. The archive cut, archive's\n * stray-requirement guard and the delta-shape exemption used to spell the same\n * invariant three different ways, and one of them was a PREFIX match that also\n * hit `## Requirements Extra`.\n *\n * Fence tracking is why this is not a set of regexes at the call sites: a\n * `### Requirement:` inside a fenced code block is prose about a requirement,\n * not one, and every heading walk here has to agree about that.\n */\nimport { readFile } from \"node:fs/promises\";\nimport { decodeDocument } from \"../kernel/document-bytes.js\";\nimport { fenceTracker } from \"../kernel/fences.js\";\nimport {\n BASED_ON_LINE_RE, CAPABILITY_LINE_RE, KIND_RE, REALIZES_LINE_RE, REQUIREMENT_ID_LINE_RE,\n type DeltaKind, type Requirement, type Scenario,\n} from \"./spec.js\";\n\nconst REQ_RE = /^###\\s+Requirement:\\s*(.+?)\\s*$/;\nconst SCN_RE = /^####\\s+Scenario:\\s*(.+?)\\s*$/;\n\n/**\n * THE requirements section: the one H2 a living spec keeps its requirements\n * under, the one section `loam archive` rewrites, and the one heading a delta\n * may legally quote the living state beneath. One definition on purpose — the\n * archive cut point, archive's stray-requirement guard, and the delta-shape\n * exemption (core/delta/document.ts) used to spell this invariant three different ways,\n * and the cut point's substring `indexOf(\"\\n## Requirements\")` was a PREFIX\n * match that also hit `## Requirements Extra`. Full-line match on a trimmed\n * heading, with KIND_RE's case and interior-whitespace tolerance.\n */\nconst REQUIREMENTS_SECTION_RE = /^##\\s+Requirements\\s*$/i;\n\n/** Is this H2 heading line (as sectionHeadings/`Requirement.section` spell it) the `## Requirements` heading? */\nexport function isRequirementsHeading(heading: string): boolean {\n return REQUIREMENTS_SECTION_RE.test(heading.trim());\n}\n\n/**\n * Strip a leading UTF-8 BOM (U+FEFF), which editors on Windows write unasked.\n *\n * It has to happen here, at the two functions that take raw markdown, rather than\n * at each `readFile` call: those are spread across half the commands, so any one of\n * them could forget, and the failure is invisible. One byte was enough to void an\n * entire delta — `KIND_RE` is anchored `^##`, delta files routinely open with\n * `## MODIFIED Requirements` on line 1, and behind a BOM that line stopped being a\n * heading, so every requirement in the file fell back to BASE and\n * `applyRequirementDelta` skips BASE. Archive merged nothing and said nothing.\n * OpenSpec's own reader strips it for exactly this reason.\n *\n * Only at position 0: elsewhere U+FEFF is a zero-width no-break space and belongs\n * to the author's text.\n */\nfunction stripBom(md: string): string {\n return md.charCodeAt(0) === 0xfeff ? md.slice(1) : md;\n}\n\n/** Every H2 heading outside a fenced block, with its 1-based line number. */\nexport function sectionHeadings(md: string): Array<{ text: string; line: number }> {\n const fenced = fenceTracker();\n const out: Array<{ text: string; line: number }> = [];\n stripBom(md).split(/\\r?\\n/).forEach((line, i) => {\n if (fenced(line)) return;\n if (/^##\\s+/.test(line) && !line.startsWith(\"###\")) out.push({ text: line.trim(), line: i + 1 });\n });\n return out;\n}\n\n/**\n * Split a living spec around its requirements RUN — the stretch of\n * `### Requirement:` blocks inside the `## Requirements` section — so a merge\n * can rewrite exactly that and nothing else. All three slices reassemble the\n * input byte-for-byte (`head + run + tail === text`):\n *\n * - `head` — everything before the first requirement in the section: the\n * intro, the heading line itself, any prose under the heading.\n * - `run` — from the first `### Requirement:` line to the section's end (the\n * next `## ` heading, or EOF). Empty when the section holds no requirements.\n * - `tail` — everything from the section's end onward.\n *\n * `requirements` is the run cut again at every `### Requirement:` heading —\n * one raw slice per requirement, from its heading up to the next one (or the\n * run's end), so `requirements.join(\"\") === run`. A slice therefore carries\n * whatever the author wrote below the requirement before the next heading,\n * blank lines included, which is exactly the text parseRequirements attributes\n * to that requirement. It exists so archive can keep the bytes of every\n * requirement a delta did not touch instead of re-serializing the whole run:\n * one ADDED requirement used to reflow every untouched requirement written\n * without a blank line under its heading, and every one of those moves landed\n * under the document's vouch stamp.\n *\n * Returns null when the document has no `## Requirements` heading at all.\n * Only the FIRST matching heading opens the section; a caller that must not\n * lose content behind a duplicate (archive) counts matches via\n * sectionHeadings() and refuses. Heading detection matches parseRequirements:\n * fenced lines are never structure, and a leading BOM hides no heading — but\n * the slices themselves are raw bytes, BOM and CRLF included.\n */\nexport function splitRequirementsSection(\n text: string,\n): { head: string; run: string; tail: string; requirements: string[] } | null {\n const fenced = fenceTracker();\n let inSection = false;\n let sectionStart = -1;\n const starts: number[] = [];\n let sectionEnd = text.length;\n let offset = 0;\n while (offset < text.length) {\n const nl = text.indexOf(\"\\n\", offset);\n const lineEnd = nl === -1 ? text.length : nl + 1;\n let line = text.slice(offset, nl === -1 ? text.length : nl);\n if (line.endsWith(\"\\r\")) line = line.slice(0, -1);\n // Position 0 only, mirroring stripBom: elsewhere U+FEFF is content.\n if (offset === 0 && line.charCodeAt(0) === 0xfeff) line = line.slice(1);\n if (!fenced(line)) {\n if (/^##\\s+/.test(line) && !line.startsWith(\"###\")) {\n if (inSection) {\n sectionEnd = offset;\n inSection = false;\n } else if (sectionStart === -1 && isRequirementsHeading(line)) {\n sectionStart = offset;\n inSection = true;\n }\n } else if (inSection && REQ_RE.test(line)) {\n starts.push(offset);\n }\n }\n offset = lineEnd;\n }\n if (sectionStart === -1) return null;\n const cut = starts[0] ?? sectionEnd;\n return {\n head: text.slice(0, cut),\n run: text.slice(cut, sectionEnd),\n tail: text.slice(sectionEnd),\n requirements: starts.map((at, i) => text.slice(at, starts[i + 1] ?? sectionEnd)),\n };\n}\n\n/**\n * A requirement document from disk as text — decoded, or refused by name.\n *\n * The read that belongs with the parser, because `readFile(path, \"utf8\")` and\n * `parseRequirements` together are a trap: a UTF-16 spec.md decodes to a string\n * in which no line matches `^###\\s+Requirement:`, so the parser returns [] and\n * the document grades as an empty baseline instead of an unreadable one. Every\n * question loam asks of a spec — is it covered, does the delta merge, what does\n * the C4 spine touch — is then answered about a file nobody read.\n *\n * It THROWS (`NotUtf8DocumentError`, which names the path), unlike\n * `readFrontmatter` next door, because the two failures are not the same\n * failure. Frontmatter is read by the fleet enumeration, where one bad file\n * must not cost the other 119 services, so there it is a flag. A requirement\n * document is read by ONE target, and the caller is the only one who knows what\n * to do about it: a read-only command turns it into a finding on that target\n * (validate's `guarded`, which names the file and keeps grading the rest), a\n * command that writes refuses outright rather than computing over mojibake.\n */\nexport async function readRequirementsDocument(path: string): Promise<string> {\n return decodeDocument(await readFile(path), path);\n}\n\n/** Parse all requirements (with their scenarios and delta kind) from a markdown doc. */\nexport function parseRequirements(md: string): Requirement[] {\n const out: Requirement[] = [];\n let kind: DeltaKind = \"BASE\";\n let section: string | undefined;\n let req: Requirement | null = null;\n let scn: Scenario | null = null;\n const fenced = fenceTracker();\n\n for (const [index, line] of stripBom(md).split(/\\r?\\n/).entries()) {\n if (fenced(line)) {\n // Fenced content, marker included: body of whatever is open, never structure.\n if (scn) scn.lines.push(line);\n else if (req) req.text.push(line);\n continue;\n }\n // Any H2 heading ends the current requirement/scenario capture — section prose\n // must not leak into the previous scenario's body.\n if (/^##\\s+/.test(line) && !line.startsWith(\"###\")) {\n req = null;\n scn = null;\n section = line.trim();\n const mk = KIND_RE.exec(line);\n // A non-delta H2 (## Notes, ## Requirements…) starts an unrelated section:\n // requirements under it are plain BASE, not part of a stale delta section.\n kind = mk ? (mk[1]!.toUpperCase() as DeltaKind) : \"BASE\";\n continue;\n }\n const mr = REQ_RE.exec(line);\n if (mr) {\n req = {\n kind,\n name: mr[1]!,\n text: [],\n operations: [],\n covers: [],\n publishes: [],\n consumes: [],\n requires: [],\n capabilities: [],\n realizes: [],\n scenarios: [],\n section,\n line: index + 1,\n };\n out.push(req);\n scn = null;\n continue;\n }\n const ms = SCN_RE.exec(line);\n if (ms && req) {\n // `index + 1` is the heading's own 1-based line, exactly as the\n // requirement capture above records its `### Requirement:` — body line\n // `lines[i]` then sits at `line + 1 + i`, because the capture below is\n // contiguous from here to the next heading. Purely additive: nothing in\n // `serializeRequirements` reads it, so `requirementDigest` cannot see it\n // and no `Based-On:` pin in any living document moves because the parser\n // learned where a scenario starts.\n scn = { name: ms[1]!, lines: [], line: index + 1 };\n req.scenarios.push(scn);\n continue;\n }\n if (scn) {\n scn.lines.push(line);\n } else if (req) {\n req.text.push(line);\n const mi = REQUIREMENT_ID_LINE_RE.exec(line);\n if (mi) req.id = mi[1]!.trim();\n // Same keep-last quirk as Operations:/Covers: below — a repeated line is\n // a document problem, reported as one (`delta.baseline-invalid`), never\n // resolved differently by whoever happens to be reading.\n const mb = BASED_ON_LINE_RE.exec(line);\n if (mb) req.basedOn = mb[1]!.trim();\n const mo = /^\\s*Operations?:\\s*(.+?)\\s*$/i.exec(line);\n if (mo) req.operations = mo[1]!.split(\",\").map((s) => s.trim()).filter((s) => s.length > 0);\n // The Covers: mirror — same placement (requirement body, not a scenario),\n // same comma grammar, and deliberately the same quirk: a second line\n // REPLACES the first (assignment, not append), so the two lists can never\n // drift apart in how they read a repeated line.\n const mc = /^\\s*Covers?:\\s*(.+?)\\s*$/i.exec(line);\n if (mc) req.covers = mc[1]!.split(\",\").map((s) => s.trim()).filter((s) => s.length > 0);\n // The event axis, same grammar and the same keep-last quirk again. Note\n // what is NOT happening here: the line stays in `req.text` like every\n // other body line (the push above is unconditional), so it round-trips\n // through `serializeRequirements` and rides inside `requirementDigest`\n // exactly as `Operations:` does. That is what makes this parse purely\n // additive — no living document's digest moves, and no `Based-On:` pin\n // goes stale because loam learned to read one more line.\n const mp = /^\\s*Publishes?:\\s*(.+?)\\s*$/i.exec(line);\n if (mp) req.publishes = mp[1]!.split(\",\").map((s) => s.trim()).filter((s) => s.length > 0);\n const mcon = /^\\s*Consumes?:\\s*(.+?)\\s*$/i.exec(line);\n if (mcon) req.consumes = mcon[1]!.split(\",\").map((s) => s.trim()).filter((s) => s.length > 0);\n // The authorization axis. Same grammar, same keep-last quirk, same purely\n // additive parse — the line rides in `req.text` and therefore inside\n // `requirementDigest`, so no living document moves because loam learned\n // to read it.\n const mreq = /^\\s*Requires?:\\s*(.+?)\\s*$/i.exec(line);\n if (mreq) req.requires = mreq[1]!.split(\",\").map((s) => s.trim()).filter((s) => s.length > 0);\n // The capability axis. Same grammar, same keep-last quirk, same purely\n // additive parse as Requires: above — but note the OPT-IN differs: the\n // vocabulary FILE (architecture/capabilities.yaml) is the opt-in for this\n // axis, not the line, so entries parsed here grade as nothing at all\n // until a fleet writes that file (core/capabilities/findings.ts).\n const mcap = CAPABILITY_LINE_RE.exec(line);\n if (mcap) req.capabilities = mcap[1]!.split(\",\").map((s) => s.trim()).filter((s) => s.length > 0);\n // The join INTO the authored business tree: `<capability-id>#<Requirement-ID>`\n // entries, same comma grammar and same keep-last quirk as every line\n // above. Split into its two halves nowhere near here —\n // `core/capabilities/realizes/join.ts` owns that, because the rollup and\n // the grades must not be able to disagree about where the separator is.\n const mrl = REALIZES_LINE_RE.exec(line);\n if (mrl) req.realizes = mrl[1]!.split(\",\").map((s) => s.trim()).filter((s) => s.length > 0);\n }\n }\n\n return out;\n}\n"]}
|
|
@@ -13,6 +13,23 @@ import {} from "./spec.js";
|
|
|
13
13
|
export function requirementsMissingScenarios(reqs) {
|
|
14
14
|
return reqs.filter((r) => r.kind !== "REMOVED" && r.scenarios.length === 0);
|
|
15
15
|
}
|
|
16
|
+
/**
|
|
17
|
+
* `requirements.missing-scenarios` over the requirements the rule above
|
|
18
|
+
* returned — one spelling for `loam validate` (its `coverageFinding`) and
|
|
19
|
+
* `loam status` (`core/status/feature/feature.ts`), because status's rule is
|
|
20
|
+
* "never greener than the gates" and a second spelling of the same finding is
|
|
21
|
+
* how the two would come to disagree. `subject` names the service when the
|
|
22
|
+
* caller has one.
|
|
23
|
+
*/
|
|
24
|
+
export function missingScenariosFinding(label, bare, subject) {
|
|
25
|
+
return {
|
|
26
|
+
severity: "error",
|
|
27
|
+
code: "requirements.missing-scenarios",
|
|
28
|
+
...(subject === undefined ? {} : { subject }),
|
|
29
|
+
message: `${label}: ${bare.length} requirement(s) without a scenario`,
|
|
30
|
+
details: bare.map((r) => r.name),
|
|
31
|
+
};
|
|
32
|
+
}
|
|
16
33
|
/**
|
|
17
34
|
* Scenarios that satisfy the coverage rule above and test nothing.
|
|
18
35
|
*
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"scenarios.js","sourceRoot":"","sources":["../../../src/core/document/scenarios.ts"],"names":[],"mappings":"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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"]}
|
|
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/**\n * `requirements.missing-scenarios` over the requirements the rule above\n * returned — one spelling for `loam validate` (its `coverageFinding`) and\n * `loam status` (`core/status/feature/feature.ts`), because status's rule is\n * \"never greener than the gates\" and a second spelling of the same finding is\n * how the two would come to disagree. `subject` names the service when the\n * caller has one.\n */\nexport function missingScenariosFinding(label: string, bare: readonly Requirement[], subject?: string): Finding {\n return {\n severity: \"error\",\n code: \"requirements.missing-scenarios\",\n ...(subject === undefined ? {} : { subject }),\n message: `${label}: ${bare.length} requirement(s) without a scenario`,\n details: bare.map((r) => r.name),\n };\n}\n\n/** A `#### Scenario:` heading whose body holds nothing a runner would execute. */\nexport interface SteplessScenario {\n requirement: string;\n scenario: string;\n}\n\n/**\n * Scenarios that satisfy the coverage rule above and test nothing.\n *\n * `requirementsMissingScenarios` counts HEADINGS, so a `#### Scenario:` with no\n * recognizable Given/When/Then line satisfied the fleet gate's coverage claim\n * outright — and it is not a hypothetical shape: it is what `loam new`\n * scaffolds and what an agent writes when it puts the criteria in prose.\n * Downstream the same emptiness is fatal twice over: cucumber runs a stepless\n * scenario vacuously green, and `loam verify --results` can never confirm it.\n *\n * The detector is `loam gherkin`'s own — `scenarioGherkin` decides what a step\n * IS, and a second opinion here would mean the gate and the emitted suite could\n * disagree about whether a scenario has any. That import is the one place these\n * two modules are circular (gherkin.ts parses requirements); both sides export\n * only function declarations, which are hoisted, so neither evaluation order\n * can observe a half-built module.\n */\nexport function steplessScenarios(reqs: Requirement[]): SteplessScenario[] {\n const out: SteplessScenario[] = [];\n for (const r of reqs) {\n if (r.kind === \"REMOVED\") continue;\n for (const s of r.scenarios) {\n if (scenarioGherkin(s.lines).steps.length === 0) out.push({ requirement: r.name, scenario: s.name });\n }\n }\n return out;\n}\n\n/**\n * The stepless half of the coverage check, as findings — the missing-scenario\n * half stays where it is (`requirementsMissingScenarios`), because the two are\n * different states with different fixes: one requirement owes acceptance\n * criteria, the other owes STEPS in criteria somebody has already written.\n *\n * `label` is the coverage check's own label (`payment-service: requirements`),\n * so both halves of one document name it the same way.\n */\nexport function steplessFindings(label: string, subject: string, reqs: Requirement[]): Finding[] {\n const bare = steplessScenarios(reqs);\n if (bare.length === 0) return [];\n return [\n {\n severity: \"error\",\n code: \"requirements.stepless-scenario\",\n subject,\n message:\n `${label}: ${bare.length} scenario(s) have a heading and no recognizable steps — ` +\n `they satisfy the coverage rule and test nothing. Cucumber runs a stepless scenario vacuously green and ` +\n `\\`loam verify --results\\` can never confirm it. Write the body as \\`- **Given/When/Then**\\` bullets.`,\n details: bare.map((s) => `${s.requirement} — ${s.scenario}`),\n text: { detailPrefix: \"- \" },\n },\n ];\n}\n\n/**\n * Scenarios that have steps and assert NOTHING — a body of Given and When with\n * no Then anywhere in it.\n *\n * The third state, and the one that survives both checks above: the requirement\n * has a scenario, the scenario has steps, and the run can only ever confirm that\n * the arrangement did not throw. `And`/`But` are continuations, so their meaning\n * is decided by the keyword they follow — with no `Then` in the body there is\n * nothing for them to continue but the setup.\n *\n * It matters more the more evidence rests on the suite. A stepless scenario is\n * at least visible as an empty body; this one reads like a real test, emits real\n * steps, runs green, and answers a `scenario.tested` claim to the same standard\n * as a scenario that checks twenty things. Under a runner-neutral answer sheet —\n * one `{digest, status}` entry, no steps to count — this document-side check is\n * the only thing left that can tell the two apart.\n */\nexport function assertionlessScenarios(reqs: Requirement[]): SteplessScenario[] {\n const out: SteplessScenario[] = [];\n for (const r of reqs) {\n if (r.kind === \"REMOVED\") continue;\n for (const s of r.scenarios) {\n const { steps } = scenarioGherkin(s.lines);\n // Disjoint from stepless by construction: a body with no steps at all is\n // that finding's, and reporting one scenario under both codes would offer\n // two fixes for one defect.\n if (steps.length > 0 && !steps.some((st) => /^Then\\b/.test(st.text))) {\n out.push({ requirement: r.name, scenario: s.name });\n }\n }\n }\n return out;\n}\n\n/** One scenario's disagreement between its `Examples` columns and its `<placeholders>`. */\nexport interface ExamplesMismatch {\n requirement: string;\n scenario: string;\n /** `<placeholders>` a step uses that no column supplies. */\n unbound: string[];\n /** Columns no step, docstring or data-table cell refers to. */\n unreferenced: string[];\n}\n\nconst PLACEHOLDER_RE = /<([^<>\\s]+)>/g;\n\n/**\n * Every `<placeholder>` a scenario's steps reach for, arguments included.\n *\n * Docstrings and data tables are scanned too because cucumber substitutes into\n * them: `<fee>` inside a request payload is a real reference, and a check that\n * read only the step lines would call its column unreferenced and invite the\n * author to delete the column the payload depends on.\n */\nfunction placeholdersUsed(steps: EmittedStep[]): Set<string> {\n const used = new Set<string>();\n const scan = (text: string): void => {\n for (const m of text.matchAll(PLACEHOLDER_RE)) used.add(m[1]!);\n };\n for (const st of steps) {\n scan(st.text);\n for (const l of st.docstring?.lines ?? []) scan(l);\n for (const row of st.table ?? []) for (const cell of row) scan(cell);\n }\n return used;\n}\n\n/**\n * Scenarios whose `Examples` header and `<placeholders>` disagree.\n *\n * Only scenarios that HAVE an Examples table are considered, and that scoping is\n * load-bearing twice over: a plain scenario's `<angle brackets>` are prose (a\n * generic, a placeholder in a URL template, loam's own scaffold sentinels), and\n * a fleet that never writes an outline must never earn a finding from this.\n *\n * The two halves are different defects. An UNBOUND placeholder is an error the\n * runner cannot repair: cucumber substitutes nothing, so the step definition\n * receives the literal text `<fee>` and either fails obscurely or — worse —\n * matches and asserts against a string nobody meant. An UNREFERENCED column is a\n * warning, because it is usually the fossil of a rename: the column still lists\n * six cases, the step that used it now names something else, and the outline\n * runs six identical passes that read as six cases of coverage.\n */\nexport function examplesMismatches(reqs: Requirement[]): ExamplesMismatch[] {\n const out: ExamplesMismatch[] = [];\n for (const r of reqs) {\n if (r.kind === \"REMOVED\") continue;\n for (const s of r.scenarios) {\n const { steps, examples } = scenarioGherkin(s.lines);\n if (examples === null) continue;\n const used = placeholdersUsed(steps);\n const columns = new Set(examples.header);\n const unbound = [...used].filter((p) => !columns.has(p)).sort();\n const unreferenced = examples.header.filter((c) => !used.has(c));\n if (unbound.length > 0 || unreferenced.length > 0) {\n out.push({ requirement: r.name, scenario: s.name, unbound, unreferenced });\n }\n }\n }\n return out;\n}\n\n/**\n * The Examples half, as findings — two codes rather than one with two\n * severities, because a machine cannot branch on a severity that depends on the\n * case, and the fixes differ: one adds a column, the other removes one or\n * restores the reference that used to use it.\n */\nexport function examplesFindings(label: string, subject: string, reqs: Requirement[]): Finding[] {\n const all = examplesMismatches(reqs);\n const findings: Finding[] = [];\n const unbound = all.filter((m) => m.unbound.length > 0);\n if (unbound.length > 0) {\n findings.push({\n severity: \"error\",\n code: \"requirements.examples-unbound\",\n subject,\n message:\n `${label}: ${unbound.length} scenario(s) use a \\`<placeholder>\\` no \\`Examples\\` column supplies — ` +\n `cucumber substitutes nothing, so the step receives the literal angle-bracket text. ` +\n `Add the column, or correct the placeholder to one the header names.`,\n details: unbound.map((m) => `${m.requirement} — ${m.scenario}: ${m.unbound.map((p) => `<${p}>`).join(\", \")}`),\n text: { detailPrefix: \"- \" },\n });\n }\n const dead = all.filter((m) => m.unreferenced.length > 0);\n if (dead.length > 0) {\n findings.push({\n severity: \"warn\",\n code: \"requirements.examples-unreferenced\",\n subject,\n message:\n `${label}: ${dead.length} scenario(s) declare an \\`Examples\\` column no step refers to — ` +\n `usually the fossil of a rename, and the outline then runs its rows as identical passes that read as cases. ` +\n `Reference the column from a step, or drop it.`,\n details: dead.map((m) => `${m.requirement} — ${m.scenario}: ${m.unreferenced.join(\", \")}`),\n text: { detailPrefix: \"- \" },\n });\n }\n return findings;\n}\n\n/** The assertionless half, as findings — same label and subject as its two siblings. */\nexport function assertionlessFindings(label: string, subject: string, reqs: Requirement[]): Finding[] {\n const mute = assertionlessScenarios(reqs);\n if (mute.length === 0) return [];\n return [\n {\n severity: \"error\",\n code: \"requirements.assertionless-scenario\",\n subject,\n message:\n `${label}: ${mute.length} scenario(s) have steps and no \\`Then\\` — ` +\n `they arrange and act, and assert nothing. The run confirms only that the setup did not throw, ` +\n `and the claim it answers is confirmed on that. Add the \\`- **Then**\\` the scenario was written for.`,\n details: mute.map((s) => `${s.requirement} — ${s.scenario}`),\n text: { detailPrefix: \"- \" },\n },\n ];\n}\n"]}
|
|
@@ -178,10 +178,10 @@ export function withoutRealizesPins(requirement) {
|
|
|
178
178
|
* The identity of a requirement's CONTENT: sha256 over its canonical
|
|
179
179
|
* serialization, truncated like every other digest loam stamps.
|
|
180
180
|
*
|
|
181
|
-
* Canonical, not raw bytes, and deliberately:
|
|
182
|
-
*
|
|
183
|
-
* make a baseline go stale over reflowed blank lines — a false alarm on
|
|
184
|
-
* check whose whole value is that it only fires when something
|
|
181
|
+
* Canonical, not raw bytes, and deliberately: archive writes every requirement
|
|
182
|
+
* a delta adds or replaces through `serializeRequirements`, so hashing bytes
|
|
183
|
+
* would make a baseline go stale over reflowed blank lines — a false alarm on
|
|
184
|
+
* the one check whose whole value is that it only fires when something moved.
|
|
185
185
|
* What the hash therefore covers is exactly what survives a round trip: the
|
|
186
186
|
* heading, the body (`Requirement-ID:` and `Operations:`/`Covers:` lines
|
|
187
187
|
* included), and every scenario with its Given/When/Then lines. `kind` and
|
|
@@ -231,11 +231,4 @@ export function serializeRequirements(reqs) {
|
|
|
231
231
|
}
|
|
232
232
|
return chunks.join("\n\n").trim() + "\n";
|
|
233
233
|
}
|
|
234
|
-
/**
|
|
235
|
-
* A delta requirement as it lands in the living document, with the bookkeeping
|
|
236
|
-
* that belongs to the DELTA left behind: the `Based-On:` pin (a claim about
|
|
237
|
-
* which living version this was written against — meaningless once it IS the
|
|
238
|
-
* living version, and poison for the next feature's baseline, which would then
|
|
239
|
-
* hash the previous feature's pin) and the line it was parsed from.
|
|
240
|
-
*/
|
|
241
234
|
//# sourceMappingURL=spec.js.map
|