@ybotok/loam 0.2.0-alpha.1 → 0.2.0-alpha.13
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +3448 -0
- package/COMPARISON.md +91 -3
- package/CONTRIBUTING.md +37 -0
- package/MIGRATING-from-OpenSpec.md +47 -23
- package/QUICKSTART.md +189 -12
- package/README.md +274 -65
- package/ROADMAP.md +572 -31
- package/SCHEMA.md +1709 -155
- package/WORKFLOW.md +298 -72
- package/dist/cli.js +26 -8
- package/dist/cli.js.map +1 -1
- package/dist/commands/adopt/adopt.js +5 -10
- package/dist/commands/adopt/adopt.js.map +1 -1
- package/dist/commands/adopt/render.js +50 -9
- package/dist/commands/adopt/render.js.map +1 -1
- package/dist/commands/archive/archive.js +16 -19
- package/dist/commands/archive/archive.js.map +1 -1
- package/dist/commands/archive/closing.js +42 -0
- package/dist/commands/archive/closing.js.map +1 -0
- package/dist/commands/archive/plan/contracts/asyncapi.js +36 -42
- package/dist/commands/archive/plan/contracts/asyncapi.js.map +1 -1
- package/dist/commands/archive/plan/contracts/consumers.js +56 -0
- package/dist/commands/archive/plan/contracts/consumers.js.map +1 -0
- package/dist/commands/archive/plan/contracts/openapi.js +77 -43
- package/dist/commands/archive/plan/contracts/openapi.js.map +1 -1
- package/dist/commands/archive/plan/contracts/verdict-lines.js +103 -0
- package/dist/commands/archive/plan/contracts/verdict-lines.js.map +1 -0
- package/dist/commands/archive/plan/document/head.js +151 -0
- package/dist/commands/archive/plan/document/head.js.map +1 -0
- package/dist/commands/archive/plan/document/links.js +284 -0
- package/dist/commands/archive/plan/document/links.js.map +1 -0
- package/dist/commands/archive/plan/document/run.js +136 -0
- package/dist/commands/archive/plan/document/run.js.map +1 -0
- package/dist/commands/archive/plan/gate.js +21 -19
- package/dist/commands/archive/plan/gate.js.map +1 -1
- package/dist/commands/archive/plan/landscape.js +167 -30
- package/dist/commands/archive/plan/landscape.js.map +1 -1
- package/dist/commands/archive/plan/model/extending.js +85 -0
- package/dist/commands/archive/plan/model/extending.js.map +1 -0
- package/dist/commands/archive/plan/refusal.js +14 -19
- package/dist/commands/archive/plan/refusal.js.map +1 -1
- package/dist/commands/archive/plan/specs.js +137 -56
- package/dist/commands/archive/plan/specs.js.map +1 -1
- package/dist/commands/archive/plan/state.js +3 -0
- package/dist/commands/archive/plan/state.js.map +1 -1
- package/dist/commands/archive/run.js +134 -30
- package/dist/commands/archive/run.js.map +1 -1
- package/dist/commands/archive/shipped.js +49 -0
- package/dist/commands/archive/shipped.js.map +1 -0
- package/dist/commands/context/context.js +85 -18
- package/dist/commands/context/context.js.map +1 -1
- package/dist/commands/context/print.js +29 -7
- package/dist/commands/context/print.js.map +1 -1
- package/dist/commands/delta/delta.js +102 -14
- package/dist/commands/delta/delta.js.map +1 -1
- package/dist/commands/delta/print.js +17 -1
- package/dist/commands/delta/print.js.map +1 -1
- package/dist/commands/dependencies.js +3 -7
- package/dist/commands/dependencies.js.map +1 -1
- package/dist/commands/diff/diff.js +20 -27
- package/dist/commands/diff/diff.js.map +1 -1
- package/dist/commands/doctor.js +60 -1
- package/dist/commands/doctor.js.map +1 -1
- package/dist/commands/explain/explain.js +89 -13
- package/dist/commands/explain/explain.js.map +1 -1
- package/dist/commands/explore.js +16 -10
- package/dist/commands/explore.js.map +1 -1
- package/dist/commands/gate/gate.js +45 -8
- package/dist/commands/gate/gate.js.map +1 -1
- package/dist/commands/gate/print.js +15 -3
- package/dist/commands/gate/print.js.map +1 -1
- package/dist/commands/gherkin/commit.js +13 -54
- package/dist/commands/gherkin/commit.js.map +1 -1
- package/dist/commands/gherkin/gherkin.js +36 -20
- package/dist/commands/gherkin/gherkin.js.map +1 -1
- package/dist/commands/gherkin/reconcile.js +10 -13
- package/dist/commands/gherkin/reconcile.js.map +1 -1
- package/dist/commands/init/first-hour.js +44 -0
- package/dist/commands/init/first-hour.js.map +1 -1
- package/dist/commands/init/init.js +18 -22
- package/dist/commands/init/init.js.map +1 -1
- package/dist/commands/list/campaign/campaign.js +4 -6
- package/dist/commands/list/campaign/campaign.js.map +1 -1
- package/dist/commands/list/json.js +5 -7
- package/dist/commands/list/json.js.map +1 -1
- package/dist/commands/list/list.js +44 -29
- package/dist/commands/list/list.js.map +1 -1
- package/dist/commands/list/print.js +52 -9
- package/dist/commands/list/print.js.map +1 -1
- package/dist/commands/list/suites/suites.js +140 -0
- package/dist/commands/list/suites/suites.js.map +1 -0
- package/dist/commands/list/views.js +37 -34
- package/dist/commands/list/views.js.map +1 -1
- package/dist/commands/mcp/dispatch.js +91 -36
- package/dist/commands/mcp/dispatch.js.map +1 -1
- package/dist/commands/mcp/mcp.js +56 -15
- package/dist/commands/mcp/mcp.js.map +1 -1
- package/dist/commands/mcp/serve.js.map +1 -1
- package/dist/commands/migrate-openspec/materialize/active.js +27 -22
- package/dist/commands/migrate-openspec/materialize/active.js.map +1 -1
- package/dist/commands/migrate-openspec/materialize/stage.js +4 -3
- package/dist/commands/migrate-openspec/materialize/stage.js.map +1 -1
- package/dist/commands/migrate-openspec/materialize/target.js +2 -2
- package/dist/commands/migrate-openspec/materialize/target.js.map +1 -1
- package/dist/commands/migrate-openspec/openspec/decisions.js +9 -14
- package/dist/commands/migrate-openspec/openspec/decisions.js.map +1 -1
- package/dist/commands/migrate-openspec/openspec/error.js +7 -8
- package/dist/commands/migrate-openspec/openspec/error.js.map +1 -1
- package/dist/commands/migrate-openspec/openspec/mapping.js +1 -9
- package/dist/commands/migrate-openspec/openspec/mapping.js.map +1 -1
- package/dist/commands/migrate-openspec/openspec/paths.js +3 -6
- package/dist/commands/migrate-openspec/openspec/paths.js.map +1 -1
- package/dist/commands/new/capability-template.js +118 -0
- package/dist/commands/new/capability-template.js.map +1 -0
- package/dist/commands/new/commit.js +8 -22
- package/dist/commands/new/commit.js.map +1 -1
- package/dist/commands/new/modifies.js +206 -0
- package/dist/commands/new/modifies.js.map +1 -0
- package/dist/commands/new/new.js +59 -14
- package/dist/commands/new/new.js.map +1 -1
- package/dist/commands/new/templates.js +48 -124
- package/dist/commands/new/templates.js.map +1 -1
- package/dist/commands/open.js +3 -7
- package/dist/commands/open.js.map +1 -1
- package/dist/commands/policy/gate.js +27 -0
- package/dist/commands/policy/gate.js.map +1 -1
- package/dist/commands/rebase/plan.js +2 -2
- package/dist/commands/rebase/plan.js.map +1 -1
- package/dist/commands/rebase/rebase.js +60 -35
- package/dist/commands/rebase/rebase.js.map +1 -1
- package/dist/commands/seed/commit.js +55 -26
- package/dist/commands/seed/commit.js.map +1 -1
- package/dist/commands/seed/plan.js +158 -13
- package/dist/commands/seed/plan.js.map +1 -1
- package/dist/commands/seed/seed.js +13 -11
- package/dist/commands/seed/seed.js.map +1 -1
- package/dist/commands/show/feature.js +9 -5
- package/dist/commands/show/feature.js.map +1 -1
- package/dist/commands/show/introduced.js +157 -0
- package/dist/commands/show/introduced.js.map +1 -0
- package/dist/commands/show/marks.js +35 -6
- package/dist/commands/show/marks.js.map +1 -1
- package/dist/commands/show/service.js +41 -33
- package/dist/commands/show/service.js.map +1 -1
- package/dist/commands/show/show.js +17 -7
- package/dist/commands/show/show.js.map +1 -1
- package/dist/commands/status/fleet-next.js +90 -0
- package/dist/commands/status/fleet-next.js.map +1 -0
- package/dist/commands/status/json.js +2 -0
- package/dist/commands/status/json.js.map +1 -1
- package/dist/commands/status/print.js +89 -37
- package/dist/commands/status/print.js.map +1 -1
- package/dist/commands/status/status.js +3 -7
- package/dist/commands/status/status.js.map +1 -1
- package/dist/commands/steps/steps.js +103 -23
- package/dist/commands/steps/steps.js.map +1 -1
- package/dist/commands/subsystem/edit.js +7 -7
- package/dist/commands/subsystem/edit.js.map +1 -1
- package/dist/commands/subsystem/move.js +106 -46
- package/dist/commands/subsystem/move.js.map +1 -1
- package/dist/commands/subsystem/query.js +2 -4
- package/dist/commands/subsystem/query.js.map +1 -1
- package/dist/commands/subsystem/subsystem.js +4 -7
- package/dist/commands/subsystem/subsystem.js.map +1 -1
- package/dist/commands/subsystem/sync.js +289 -74
- package/dist/commands/subsystem/sync.js.map +1 -1
- package/dist/commands/subsystem/txn/exclude.js +132 -0
- package/dist/commands/subsystem/txn/exclude.js.map +1 -0
- package/dist/commands/subsystem/txn/links.js +128 -0
- package/dist/commands/subsystem/txn/links.js.map +1 -0
- package/dist/commands/subsystem/txn/report.js +174 -0
- package/dist/commands/subsystem/txn/report.js.map +1 -0
- package/dist/commands/subsystem/txn/txn.js +147 -45
- package/dist/commands/subsystem/txn/txn.js.map +1 -1
- package/dist/commands/subsystem/txn/views.js +200 -21
- package/dist/commands/subsystem/txn/views.js.map +1 -1
- package/dist/commands/tasks/actions.js +107 -0
- package/dist/commands/tasks/actions.js.map +1 -0
- package/dist/commands/tasks/render.js +253 -0
- package/dist/commands/tasks/render.js.map +1 -0
- package/dist/commands/tasks/tasks.js +119 -0
- package/dist/commands/tasks/tasks.js.map +1 -0
- package/dist/commands/unarchive/manifest.js +42 -26
- package/dist/commands/unarchive/manifest.js.map +1 -1
- package/dist/commands/unarchive/restore.js +83 -33
- package/dist/commands/unarchive/restore.js.map +1 -1
- package/dist/commands/unarchive/unarchive.js +22 -30
- package/dist/commands/unarchive/unarchive.js.map +1 -1
- package/dist/commands/validate/arch-coverage.js +78 -27
- package/dist/commands/validate/arch-coverage.js.map +1 -1
- package/dist/commands/validate/checks/fleet-shape.js +34 -126
- package/dist/commands/validate/checks/fleet-shape.js.map +1 -1
- package/dist/commands/validate/checks/health-deps.js +9 -6
- package/dist/commands/validate/checks/health-deps.js.map +1 -1
- package/dist/commands/validate/checks/introduced.js +102 -0
- package/dist/commands/validate/checks/introduced.js.map +1 -0
- package/dist/commands/validate/checks/requirements.js +18 -8
- package/dist/commands/validate/checks/requirements.js.map +1 -1
- package/dist/commands/validate/checks/sources.js +11 -2
- package/dist/commands/validate/checks/sources.js.map +1 -1
- package/dist/commands/validate/coverage/covers-after.js +105 -0
- package/dist/commands/validate/coverage/covers-after.js.map +1 -0
- package/dist/commands/validate/feature.js +26 -20
- package/dist/commands/validate/feature.js.map +1 -1
- package/dist/commands/validate/fleet/census.js +37 -13
- package/dist/commands/validate/fleet/census.js.map +1 -1
- package/dist/commands/validate/fleet/landscape.js +144 -126
- package/dist/commands/validate/fleet/landscape.js.map +1 -1
- package/dist/commands/validate/fleet/load.js +66 -0
- package/dist/commands/validate/fleet/load.js.map +1 -1
- package/dist/commands/validate/fleet/map/attest.js +98 -0
- package/dist/commands/validate/fleet/map/attest.js.map +1 -0
- package/dist/commands/validate/fleet/map/bindings.js +107 -0
- package/dist/commands/validate/fleet/map/bindings.js.map +1 -0
- package/dist/commands/validate/fleet/map/consumers.js +244 -0
- package/dist/commands/validate/fleet/map/consumers.js.map +1 -0
- package/dist/commands/validate/fleet/map/isolation.js +116 -0
- package/dist/commands/validate/fleet/map/isolation.js.map +1 -0
- package/dist/commands/validate/fleet/map/usecases.js +54 -0
- package/dist/commands/validate/fleet/map/usecases.js.map +1 -0
- package/dist/commands/validate/fleet/obligations.js +44 -14
- package/dist/commands/validate/fleet/obligations.js.map +1 -1
- package/dist/commands/validate/fleet/scorecard/print.js +10 -2
- package/dist/commands/validate/fleet/scorecard/print.js.map +1 -1
- package/dist/commands/validate/fleet/scorecard/scorecard.js +17 -11
- package/dist/commands/validate/fleet/scorecard/scorecard.js.map +1 -1
- package/dist/commands/validate/fleet/usecases/place.js.map +1 -1
- package/dist/commands/validate/fleet/views/fleet-project.js +217 -0
- package/dist/commands/validate/fleet/views/fleet-project.js.map +1 -0
- package/dist/commands/validate/fleet/views/ids.js +50 -15
- package/dist/commands/validate/fleet/views/ids.js.map +1 -1
- package/dist/commands/validate/fleet/views/projects.js +278 -0
- package/dist/commands/validate/fleet/views/projects.js.map +1 -0
- package/dist/commands/validate/fleet/views/root-documents.js +83 -0
- package/dist/commands/validate/fleet/views/root-documents.js.map +1 -0
- package/dist/commands/validate/fleet/views/stale.js +40 -47
- package/dist/commands/validate/fleet/views/stale.js.map +1 -1
- package/dist/commands/validate/links/corpus.js +1 -4
- package/dist/commands/validate/links/corpus.js.map +1 -1
- package/dist/commands/validate/links/glossary.js.map +1 -1
- package/dist/commands/validate/render/repeats.js +63 -0
- package/dist/commands/validate/render/repeats.js.map +1 -0
- package/dist/commands/validate/render/rollups.js +115 -0
- package/dist/commands/validate/render/rollups.js.map +1 -0
- package/dist/commands/validate/render/text.js +199 -0
- package/dist/commands/validate/render/text.js.map +1 -0
- package/dist/commands/validate/report.js +36 -160
- package/dist/commands/validate/report.js.map +1 -1
- package/dist/commands/validate/scope.js +113 -0
- package/dist/commands/validate/scope.js.map +1 -0
- package/dist/commands/validate/service/api.js +21 -107
- package/dist/commands/validate/service/api.js.map +1 -1
- package/dist/commands/validate/service/contracts/generated.js +134 -0
- package/dist/commands/validate/service/contracts/generated.js.map +1 -0
- package/dist/commands/validate/service/events/events.js +36 -14
- package/dist/commands/validate/service/events/events.js.map +1 -1
- package/dist/commands/validate/service/events/message-producers.js +1 -1
- package/dist/commands/validate/service/events/message-producers.js.map +1 -1
- package/dist/commands/validate/service/evidence-pins.js +56 -10
- package/dist/commands/validate/service/evidence-pins.js.map +1 -1
- package/dist/commands/validate/service/model/diverged.js +226 -0
- package/dist/commands/validate/service/model/diverged.js.map +1 -0
- package/dist/commands/validate/service/model/grade.js +163 -0
- package/dist/commands/validate/service/model/grade.js.map +1 -0
- package/dist/commands/validate/service/service.js +121 -61
- package/dist/commands/validate/service/service.js.map +1 -1
- package/dist/commands/validate/service/specs.js +88 -28
- package/dist/commands/validate/service/specs.js.map +1 -1
- package/dist/commands/validate/service/spine.js +38 -3
- package/dist/commands/validate/service/spine.js.map +1 -1
- package/dist/commands/validate/service/usecases/tags.js +113 -0
- package/dist/commands/validate/service/usecases/tags.js.map +1 -0
- package/dist/commands/validate/service/usecases/usecases.js +209 -0
- package/dist/commands/validate/service/usecases/usecases.js.map +1 -0
- package/dist/commands/validate/validate.js +85 -60
- package/dist/commands/validate/validate.js.map +1 -1
- package/dist/commands/verify/cross/diff.js +2 -6
- package/dist/commands/verify/cross/diff.js.map +1 -1
- package/dist/commands/verify/evidence/contract.js +1 -1
- package/dist/commands/verify/evidence/contract.js.map +1 -1
- package/dist/commands/verify/evidence/read.js +12 -3
- package/dist/commands/verify/evidence/read.js.map +1 -1
- package/dist/commands/verify/frozen.js +45 -23
- package/dist/commands/verify/frozen.js.map +1 -1
- package/dist/commands/verify/record.js +69 -17
- package/dist/commands/verify/record.js.map +1 -1
- package/dist/commands/verify/report.js +27 -44
- package/dist/commands/verify/report.js.map +1 -1
- package/dist/commands/verify/results.js +112 -30
- package/dist/commands/verify/results.js.map +1 -1
- package/dist/commands/verify/verify.js +55 -52
- package/dist/commands/verify/verify.js.map +1 -1
- package/dist/commands/vouch/pack/pack.js +2 -2
- package/dist/commands/vouch/pack/pack.js.map +1 -1
- package/dist/commands/vouch/pack/print.js +1 -6
- package/dist/commands/vouch/pack/print.js.map +1 -1
- package/dist/commands/vouch/pack/sections.js +1 -2
- package/dist/commands/vouch/pack/sections.js.map +1 -1
- package/dist/commands/vouch/run.js +26 -81
- package/dist/commands/vouch/run.js.map +1 -1
- package/dist/commands/vouch/sample/plan.js +20 -34
- package/dist/commands/vouch/sample/plan.js.map +1 -1
- package/dist/commands/vouch/vet/verify.js +39 -7
- package/dist/commands/vouch/vet/verify.js.map +1 -1
- package/dist/commands/vouch/vouch.js +10 -17
- package/dist/commands/vouch/vouch.js.map +1 -1
- package/dist/core/agent/agents-md/command-map.js +81 -106
- package/dist/core/agent/agents-md/command-map.js.map +1 -1
- package/dist/core/agent/agents-md/map/explain.js +21 -9
- package/dist/core/agent/agents-md/map/explain.js.map +1 -1
- package/dist/core/agent/agents-md/map/lenses/context.js +17 -8
- package/dist/core/agent/agents-md/map/lenses/context.js.map +1 -1
- package/dist/core/agent/agents-md/map/lenses/diff.js +33 -20
- package/dist/core/agent/agents-md/map/lenses/diff.js.map +1 -1
- package/dist/core/agent/agents-md/map/lenses/gate.js +13 -5
- package/dist/core/agent/agents-md/map/lenses/gate.js.map +1 -1
- package/dist/core/agent/agents-md/map/mcp.js +10 -7
- package/dist/core/agent/agents-md/map/mcp.js.map +1 -1
- package/dist/core/agent/agents-md/map/status.js +24 -8
- package/dist/core/agent/agents-md/map/status.js.map +1 -1
- package/dist/core/agent/agents-md/map/subsystem.js +80 -8
- package/dist/core/agent/agents-md/map/subsystem.js.map +1 -1
- package/dist/core/agent/agents-md/map/usecases.js +88 -10
- package/dist/core/agent/agents-md/map/usecases.js.map +1 -1
- package/dist/core/agent/agents-md/refusals.js +49 -22
- package/dist/core/agent/agents-md/refusals.js.map +1 -1
- package/dist/core/agent/agents-md.js +39 -6
- package/dist/core/agent/agents-md.js.map +1 -1
- package/dist/core/agent/agents-stamp.js +10 -0
- package/dist/core/agent/agents-stamp.js.map +1 -1
- package/dist/core/agent/protocol.js +1 -1
- package/dist/core/agent/protocol.js.map +1 -1
- package/dist/core/agent/scaffold.js +10 -15
- package/dist/core/agent/scaffold.js.map +1 -1
- package/dist/core/agent/support/report.js +46 -5
- package/dist/core/agent/support/report.js.map +1 -1
- package/dist/core/agent/tools/dialects.js +1 -0
- package/dist/core/agent/tools/dialects.js.map +1 -1
- package/dist/core/agent/tools/registry.js +41 -76
- package/dist/core/agent/tools/registry.js.map +1 -1
- package/dist/core/agent/workflows/adopt.js +158 -11
- package/dist/core/agent/workflows/adopt.js.map +1 -1
- package/dist/core/agent/workflows/check.js +78 -39
- package/dist/core/agent/workflows/check.js.map +1 -1
- package/dist/core/agent/workflows/closing.js +88 -27
- package/dist/core/agent/workflows/closing.js.map +1 -1
- package/dist/core/agent/workflows/feature.js +128 -13
- package/dist/core/agent/workflows/feature.js.map +1 -1
- package/dist/core/agent/workflows/implement.js +96 -13
- package/dist/core/agent/workflows/implement.js.map +1 -1
- package/dist/core/agent/workflows/reference/authoring.js +106 -4
- package/dist/core/agent/workflows/reference/authoring.js.map +1 -1
- package/dist/core/agent/workflows/reference/codes.js +8 -6
- package/dist/core/agent/workflows/reference/codes.js.map +1 -1
- package/dist/core/agent/workflows/reference/done-check.js +37 -15
- package/dist/core/agent/workflows/reference/done-check.js.map +1 -1
- package/dist/core/agent/workflows/reference/drawing/drawing.js +253 -0
- package/dist/core/agent/workflows/reference/drawing/drawing.js.map +1 -0
- package/dist/core/agent/workflows/reference/reference.js +6 -3
- package/dist/core/agent/workflows/reference/reference.js.map +1 -1
- package/dist/core/agent/workflows/reference/spine.js +68 -27
- package/dist/core/agent/workflows/reference/spine.js.map +1 -1
- package/dist/core/asyncapi/baseline/plan.js +14 -81
- package/dist/core/asyncapi/baseline/plan.js.map +1 -1
- package/dist/core/asyncapi/digest.js +30 -25
- package/dist/core/asyncapi/digest.js.map +1 -1
- package/dist/core/asyncapi/merge/components.js +5 -4
- package/dist/core/asyncapi/merge/components.js.map +1 -1
- package/dist/core/asyncapi/merge/error.js +7 -2
- package/dist/core/asyncapi/merge/error.js.map +1 -1
- package/dist/core/asyncapi/merge/markers.js +32 -50
- package/dist/core/asyncapi/merge/markers.js.map +1 -1
- package/dist/core/asyncapi/merge/merge.js +33 -53
- package/dist/core/asyncapi/merge/merge.js.map +1 -1
- package/dist/core/asyncapi/merge/pin.js +12 -36
- package/dist/core/asyncapi/merge/pin.js.map +1 -1
- package/dist/core/asyncapi/model.js.map +1 -1
- package/dist/core/asyncapi/read.js +36 -12
- package/dist/core/asyncapi/read.js.map +1 -1
- package/dist/core/brief/brief.js +96 -36
- package/dist/core/brief/brief.js.map +1 -1
- package/dist/core/brief/checks.js +138 -3
- package/dist/core/brief/checks.js.map +1 -1
- package/dist/core/brief/landscape.js +209 -130
- package/dist/core/brief/landscape.js.map +1 -1
- package/dist/core/brief/map/extend.js +84 -0
- package/dist/core/brief/map/extend.js.map +1 -0
- package/dist/core/brief/map/owed.js +266 -0
- package/dist/core/brief/map/owed.js.map +1 -0
- package/dist/core/brief/targets.js +109 -31
- package/dist/core/brief/targets.js.map +1 -1
- package/dist/core/brief/unchecked.js +14 -3
- package/dist/core/brief/unchecked.js.map +1 -1
- package/dist/core/c4/arch.js +90 -182
- package/dist/core/c4/arch.js.map +1 -1
- package/dist/core/c4/direction/direction.js +114 -0
- package/dist/core/c4/direction/direction.js.map +1 -0
- package/dist/core/c4/likec4.js +169 -14
- package/dist/core/c4/likec4.js.map +1 -1
- package/dist/core/c4/locate/unlocated.js +150 -0
- package/dist/core/c4/locate/unlocated.js.map +1 -0
- package/dist/core/c4/parsed/deployment.js +160 -0
- package/dist/core/c4/parsed/deployment.js.map +1 -0
- package/dist/core/c4/parsed/dynamic-views.js.map +1 -1
- package/dist/core/c4/parsed/values.js +54 -1
- package/dist/core/c4/parsed/values.js.map +1 -1
- package/dist/core/c4/parsed/view-ids.js +39 -1
- package/dist/core/c4/parsed/view-ids.js.map +1 -1
- package/dist/core/c4/prefetch/fleet.js +134 -0
- package/dist/core/c4/prefetch/fleet.js.map +1 -0
- package/dist/core/c4/project/architecture.js +123 -7
- package/dist/core/c4/project/architecture.js.map +1 -1
- package/dist/core/c4/project/batch.js +140 -0
- package/dist/core/c4/project/batch.js.map +1 -0
- package/dist/core/c4/project/documents.js +43 -12
- package/dist/core/c4/project/documents.js.map +1 -1
- package/dist/core/c4/project/load.js +79 -43
- package/dist/core/c4/project/load.js.map +1 -1
- package/dist/core/c4/project/staged.js +277 -0
- package/dist/core/c4/project/staged.js.map +1 -0
- package/dist/core/c4/resolve/attested.js +0 -0
- package/dist/core/c4/resolve/attested.js.map +1 -0
- package/dist/core/c4/resolve/external.js +51 -0
- package/dist/core/c4/resolve/external.js.map +1 -0
- package/dist/core/c4/resolve/service.js +78 -17
- package/dist/core/c4/resolve/service.js.map +1 -1
- package/dist/core/c4/resolve/steps.js +290 -0
- package/dist/core/c4/resolve/steps.js.map +1 -0
- package/dist/core/c4/resolve/systems.js +154 -0
- package/dist/core/c4/resolve/systems.js.map +1 -0
- package/dist/core/c4/root-project/exclude.js +352 -0
- package/dist/core/c4/root-project/exclude.js.map +1 -0
- package/dist/core/c4/seed/template.js +65 -1
- package/dist/core/c4/seed/template.js.map +1 -1
- package/dist/core/c4/service-model/created/models.js +96 -0
- package/dist/core/c4/service-model/created/models.js.map +1 -0
- package/dist/core/c4/service-model/documents.js +47 -0
- package/dist/core/c4/service-model/documents.js.map +1 -0
- package/dist/core/c4/service-model/fleet/extending.js +82 -0
- package/dist/core/c4/service-model/fleet/extending.js.map +1 -0
- package/dist/core/c4/service-model/load.js +165 -0
- package/dist/core/c4/service-model/load.js.map +1 -0
- package/dist/core/c4/service-model/renderer.js +128 -0
- package/dist/core/c4/service-model/renderer.js.map +1 -0
- package/dist/core/c4/service-model/shape.js +106 -0
- package/dist/core/c4/service-model/shape.js.map +1 -0
- package/dist/core/c4/service-model/slice.js +188 -0
- package/dist/core/c4/service-model/slice.js.map +1 -0
- package/dist/core/c4/source-mask.js +297 -10
- package/dist/core/c4/source-mask.js.map +1 -1
- package/dist/core/c4/source-scan.js +222 -141
- package/dist/core/c4/source-scan.js.map +1 -1
- package/dist/core/c4/splice/authored-source.js +79 -13
- package/dist/core/c4/splice/authored-source.js.map +1 -1
- package/dist/core/c4/splice/contract.js +20 -0
- package/dist/core/c4/splice/contract.js.map +1 -1
- package/dist/core/c4/splice/delta-blocks.js +20 -20
- package/dist/core/c4/splice/delta-blocks.js.map +1 -1
- package/dist/core/c4/splice/identity/declared.js +87 -0
- package/dist/core/c4/splice/identity/declared.js.map +1 -0
- package/dist/core/c4/splice/identity/edges.js +37 -0
- package/dist/core/c4/splice/identity/edges.js.map +1 -1
- package/dist/core/c4/splice/identity/existing.js +80 -0
- package/dist/core/c4/splice/identity/existing.js.map +1 -0
- package/dist/core/c4/splice/identity/interior.js +80 -0
- package/dist/core/c4/splice/identity/interior.js.map +1 -0
- package/dist/core/c4/splice/identity/owner.js +50 -0
- package/dist/core/c4/splice/identity/owner.js.map +1 -0
- package/dist/core/c4/splice/landscape-merge.js +182 -114
- package/dist/core/c4/splice/landscape-merge.js.map +1 -1
- package/dist/core/c4/splice/model/merge.js +151 -0
- package/dist/core/c4/splice/model/merge.js.map +1 -0
- package/dist/core/c4/splice/placement.js +160 -56
- package/dist/core/c4/splice/placement.js.map +1 -1
- package/dist/core/c4/splice/text/spot.js +131 -0
- package/dist/core/c4/splice/text/spot.js.map +1 -0
- package/dist/core/c4/workspace.js +13 -19
- package/dist/core/c4/workspace.js.map +1 -1
- package/dist/core/capabilities/capabilities.js +6 -6
- package/dist/core/capabilities/capabilities.js.map +1 -1
- package/dist/core/capabilities/delta/doc.js +7 -10
- package/dist/core/capabilities/delta/doc.js.map +1 -1
- package/dist/core/capabilities/delta/uncovered.js +2 -2
- package/dist/core/capabilities/delta/uncovered.js.map +1 -1
- package/dist/core/capabilities/findings.js +18 -22
- package/dist/core/capabilities/findings.js.map +1 -1
- package/dist/core/capabilities/realizes/findings.js +12 -19
- package/dist/core/capabilities/realizes/findings.js.map +1 -1
- package/dist/core/capabilities/realizes/join.js +0 -20
- package/dist/core/capabilities/realizes/join.js.map +1 -1
- package/dist/core/capabilities/rollup.js +6 -0
- package/dist/core/capabilities/rollup.js.map +1 -1
- package/dist/core/capabilities/tree.js +6 -28
- package/dist/core/capabilities/tree.js.map +1 -1
- package/dist/core/capabilities/usecase-join.js +3 -13
- package/dist/core/capabilities/usecase-join.js.map +1 -1
- package/dist/core/coherence/authoring/scaffold.js +11 -3
- package/dist/core/coherence/authoring/scaffold.js.map +1 -1
- package/dist/core/coherence/authoring/sentinels.js +2 -1
- package/dist/core/coherence/authoring/sentinels.js.map +1 -1
- package/dist/core/coherence/calls/called.js +52 -0
- package/dist/core/coherence/calls/called.js.map +1 -0
- package/dist/core/coherence/coherence.js +103 -22
- package/dist/core/coherence/coherence.js.map +1 -1
- package/dist/core/coherence/consumed/operations.js +70 -0
- package/dist/core/coherence/consumed/operations.js.map +1 -0
- package/dist/core/coherence/consumed/refusal.js +38 -0
- package/dist/core/coherence/consumed/refusal.js.map +1 -0
- package/dist/core/coherence/covers/health.js +85 -0
- package/dist/core/coherence/covers/health.js.map +1 -0
- package/dist/core/coherence/declared.js +60 -34
- package/dist/core/coherence/declared.js.map +1 -1
- package/dist/core/coherence/events/declared.js +44 -59
- package/dist/core/coherence/events/declared.js.map +1 -1
- package/dist/core/coherence/events/events.js +147 -16
- package/dist/core/coherence/events/events.js.map +1 -1
- package/dist/core/coherence/events/grades.js +31 -8
- package/dist/core/coherence/events/grades.js.map +1 -1
- package/dist/core/coherence/events/lookups.js +28 -87
- package/dist/core/coherence/events/lookups.js.map +1 -1
- package/dist/core/coherence/events/produced.js +100 -0
- package/dist/core/coherence/events/produced.js.map +1 -0
- package/dist/core/coherence/lookups.js +18 -78
- package/dist/core/coherence/lookups.js.map +1 -1
- package/dist/core/coherence/retired/archived.js +285 -0
- package/dist/core/coherence/retired/archived.js.map +1 -0
- package/dist/core/coherence/retired/event-pair.js +62 -0
- package/dist/core/coherence/retired/event-pair.js.map +1 -0
- package/dist/core/coherence/retired/merged.js +79 -0
- package/dist/core/coherence/retired/merged.js.map +1 -0
- package/dist/core/coherence/retired/retirement.js +49 -0
- package/dist/core/coherence/retired/retirement.js.map +1 -0
- package/dist/core/coherence/retired/revival/arrivals.js +131 -0
- package/dist/core/coherence/retired/revival/arrivals.js.map +1 -0
- package/dist/core/coherence/retired/revival/revived.js +297 -0
- package/dist/core/coherence/retired/revival/revived.js.map +1 -0
- package/dist/core/consumers/after.js +113 -0
- package/dist/core/consumers/after.js.map +1 -0
- package/dist/core/consumers/consumers.js +296 -0
- package/dist/core/consumers/consumers.js.map +1 -0
- package/dist/core/consumers/current.js +211 -0
- package/dist/core/consumers/current.js.map +1 -0
- package/dist/core/consumers/scan.js +46 -0
- package/dist/core/consumers/scan.js.map +1 -0
- package/dist/core/consumers/state.js +27 -0
- package/dist/core/consumers/state.js.map +1 -0
- package/dist/core/delta/delta.js +51 -6
- package/dist/core/delta/delta.js.map +1 -1
- package/dist/core/dependencies/fanin.js +2 -2
- package/dist/core/dependencies/fanin.js.map +1 -1
- package/dist/core/deployment/delta.js +191 -0
- package/dist/core/deployment/delta.js.map +1 -0
- package/dist/core/deployment/objects.js +145 -0
- package/dist/core/deployment/objects.js.map +1 -0
- package/dist/core/diff/base-git.js +94 -12
- package/dist/core/diff/base-git.js.map +1 -1
- package/dist/core/diff/base-state.js +49 -21
- package/dist/core/diff/base-state.js.map +1 -1
- package/dist/core/diff/joins.js +136 -0
- package/dist/core/diff/joins.js.map +1 -0
- package/dist/core/diff/scope/changed-paths.js +22 -6
- package/dist/core/diff/scope/changed-paths.js.map +1 -1
- package/dist/core/diff/scope/edges.js +82 -0
- package/dist/core/diff/scope/edges.js.map +1 -0
- package/dist/core/diff/semantic.js +100 -31
- package/dist/core/diff/semantic.js.map +1 -1
- package/dist/core/docs.js +31 -22
- package/dist/core/docs.js.map +1 -1
- package/dist/core/doctor/agents.js +34 -18
- package/dist/core/doctor/agents.js.map +1 -1
- package/dist/core/doctor/config.js +7 -3
- package/dist/core/doctor/config.js.map +1 -1
- package/dist/core/doctor/doctor.js +91 -11
- package/dist/core/doctor/doctor.js.map +1 -1
- package/dist/core/doctor/report.js.map +1 -1
- package/dist/core/doctor/reports/scan.js +242 -0
- package/dist/core/doctor/reports/scan.js.map +1 -0
- package/dist/core/document/apply.js +158 -11
- package/dist/core/document/apply.js.map +1 -1
- package/dist/core/document/frontmatter.js +55 -0
- package/dist/core/document/frontmatter.js.map +1 -1
- package/dist/core/document/parse.js +29 -6
- package/dist/core/document/parse.js.map +1 -1
- package/dist/core/document/scenarios.js +17 -0
- package/dist/core/document/scenarios.js.map +1 -1
- package/dist/core/document/spec.js +4 -11
- package/dist/core/document/spec.js.map +1 -1
- package/dist/core/envelope/config.js +10 -0
- package/dist/core/envelope/config.js.map +1 -1
- package/dist/core/envelope/json.js +34 -0
- package/dist/core/envelope/json.js.map +1 -1
- package/dist/core/explain/families.js +26 -21
- package/dist/core/explain/families.js.map +1 -1
- package/dist/core/explain/lookup.js +14 -1
- package/dist/core/explain/lookup.js.map +1 -1
- package/dist/core/explain/refusals.js +5 -5
- package/dist/core/explain/refusals.js.map +1 -1
- package/dist/core/explain/terms/documents.js +227 -0
- package/dist/core/explain/terms/documents.js.map +1 -0
- package/dist/core/explain/terms/done-check.js +144 -0
- package/dist/core/explain/terms/done-check.js.map +1 -0
- package/dist/core/explain/terms/entry.js +14 -0
- package/dist/core/explain/terms/entry.js.map +1 -0
- package/dist/core/explain/terms/stages.js +71 -0
- package/dist/core/explain/terms/stages.js.map +1 -0
- package/dist/core/explain/terms/tasks.js +97 -0
- package/dist/core/explain/terms/tasks.js.map +1 -0
- package/dist/core/explain/terms.js +68 -109
- package/dist/core/explain/terms.js.map +1 -1
- package/dist/core/explore/explore.js +40 -10
- package/dist/core/explore/explore.js.map +1 -1
- package/dist/core/fleet-context.js +81 -95
- package/dist/core/fleet-context.js.map +1 -1
- package/dist/core/gate/checks.js +11 -4
- package/dist/core/gate/checks.js.map +1 -1
- package/dist/core/gate/partners.js +69 -28
- package/dist/core/gate/partners.js.map +1 -1
- package/dist/core/gate/report.js.map +1 -1
- package/dist/core/gate/verification.js +75 -12
- package/dist/core/gate/verification.js.map +1 -1
- package/dist/core/gherkin/stale.js +13 -37
- package/dist/core/gherkin/stale.js.map +1 -1
- package/dist/core/glossary/delta.js +10 -7
- package/dist/core/glossary/delta.js.map +1 -1
- package/dist/core/glossary/tree.js +6 -19
- package/dist/core/glossary/tree.js.map +1 -1
- package/dist/core/kernel/concurrency.js +19 -0
- package/dist/core/kernel/concurrency.js.map +1 -1
- package/dist/core/kernel/ids/fqn/ancestors.js +65 -0
- package/dist/core/kernel/ids/fqn/ancestors.js.map +1 -0
- package/dist/core/kernel/path-safety.js +38 -0
- package/dist/core/kernel/path-safety.js.map +1 -1
- package/dist/core/kernel/records.js +11 -0
- package/dist/core/kernel/records.js.map +1 -1
- package/dist/core/links/backlinks.js +1 -3
- package/dist/core/links/backlinks.js.map +1 -1
- package/dist/core/links/corpus.js +51 -37
- package/dist/core/links/corpus.js.map +1 -1
- package/dist/core/links/findings.js.map +1 -1
- package/dist/core/links/parse.js +38 -9
- package/dist/core/links/parse.js.map +1 -1
- package/dist/core/mcp/argv.js +101 -0
- package/dist/core/mcp/argv.js.map +1 -1
- package/dist/core/mcp/author-tools.js +2 -1
- package/dist/core/mcp/author-tools.js.map +1 -1
- package/dist/core/mcp/protocol.js +2 -2
- package/dist/core/mcp/protocol.js.map +1 -1
- package/dist/core/mcp/tools.js +43 -33
- package/dist/core/mcp/tools.js.map +1 -1
- package/dist/core/openapi/baseline/gate.js +53 -12
- package/dist/core/openapi/baseline/gate.js.map +1 -1
- package/dist/core/openapi/baseline/plan.js +40 -10
- package/dist/core/openapi/baseline/plan.js.map +1 -1
- package/dist/core/openapi/baseline/record.js +0 -1
- package/dist/core/openapi/baseline/record.js.map +1 -1
- package/dist/core/openapi/contract-text.js +33 -0
- package/dist/core/openapi/contract-text.js.map +1 -0
- package/dist/core/openapi/contract-tree.js +301 -0
- package/dist/core/openapi/contract-tree.js.map +1 -0
- package/dist/core/openapi/depth.js +38 -24
- package/dist/core/openapi/depth.js.map +1 -1
- package/dist/core/openapi/digest.js +52 -2
- package/dist/core/openapi/digest.js.map +1 -1
- package/dist/core/openapi/doc.js +12 -4
- package/dist/core/openapi/doc.js.map +1 -1
- package/dist/core/openapi/merge/components.js +25 -60
- package/dist/core/openapi/merge/components.js.map +1 -1
- package/dist/core/openapi/merge/error.js +3 -13
- package/dist/core/openapi/merge/error.js.map +1 -1
- package/dist/core/openapi/merge/markers.js +26 -30
- package/dist/core/openapi/merge/markers.js.map +1 -1
- package/dist/core/openapi/merge/merge.js +28 -52
- package/dist/core/openapi/merge/merge.js.map +1 -1
- package/dist/core/openapi/merge/pin.js +17 -35
- package/dist/core/openapi/merge/pin.js.map +1 -1
- package/dist/core/openspec/decide/verdict.js.map +1 -1
- package/dist/core/openspec/inventory.js +4 -5
- package/dist/core/openspec/inventory.js.map +1 -1
- package/dist/core/openspec/model/mapping.js +30 -28
- package/dist/core/openspec/model/mapping.js.map +1 -1
- package/dist/core/openspec/model/model.js +12 -7
- package/dist/core/openspec/model/model.js.map +1 -1
- package/dist/core/openspec/scan/changes.js +10 -4
- package/dist/core/openspec/scan/changes.js.map +1 -1
- package/dist/core/openspec/scan/config.js +7 -16
- package/dist/core/openspec/scan/config.js.map +1 -1
- package/dist/core/openspec/scan/shape.js +63 -19
- package/dist/core/openspec/scan/shape.js.map +1 -1
- package/dist/core/openspec/scan/walk.js +21 -38
- package/dist/core/openspec/scan/walk.js.map +1 -1
- package/dist/core/pack/living.js +43 -4
- package/dist/core/pack/living.js.map +1 -1
- package/dist/core/pack/pack.js +50 -6
- package/dist/core/pack/pack.js.map +1 -1
- package/dist/core/projection/arch-slice.js +2 -2
- package/dist/core/projection/arch-slice.js.map +1 -1
- package/dist/core/projection/introduced.js +14 -0
- package/dist/core/projection/introduced.js.map +1 -0
- package/dist/core/provenance/findings.js +48 -12
- package/dist/core/provenance/findings.js.map +1 -1
- package/dist/core/provenance/git.js +33 -57
- package/dist/core/provenance/git.js.map +1 -1
- package/dist/core/provenance/gitq/moves.js +71 -114
- package/dist/core/provenance/gitq/moves.js.map +1 -1
- package/dist/core/provenance/gitq/vouched-ancestor.js +2 -2
- package/dist/core/provenance/gitq/vouched-ancestor.js.map +1 -1
- package/dist/core/provenance/sources.js +23 -6
- package/dist/core/provenance/sources.js.map +1 -1
- package/dist/core/provenance/stamp.js +5 -17
- package/dist/core/provenance/stamp.js.map +1 -1
- package/dist/core/repo/authored/paths.js +18 -0
- package/dist/core/repo/authored/paths.js.map +1 -1
- package/dist/core/repo/entries.js.map +1 -1
- package/dist/core/repo/paths.js +92 -6
- package/dist/core/repo/paths.js.map +1 -1
- package/dist/core/repo/repo.js +74 -14
- package/dist/core/repo/repo.js.map +1 -1
- package/dist/core/repo/tree/find.js +12 -3
- package/dist/core/repo/tree/find.js.map +1 -1
- package/dist/core/repo/tree/fs.js +27 -1
- package/dist/core/repo/tree/fs.js.map +1 -1
- package/dist/core/repo/tree/render/body.js +258 -0
- package/dist/core/repo/tree/render/body.js.map +1 -0
- package/dist/core/repo/tree/render/projects.js +182 -0
- package/dist/core/repo/tree/render/projects.js.map +1 -0
- package/dist/core/repo/tree/render/service/header.js +125 -0
- package/dist/core/repo/tree/render/service/header.js.map +1 -0
- package/dist/core/repo/tree/render/service/peers.js +28 -0
- package/dist/core/repo/tree/render/service/peers.js.map +1 -0
- package/dist/core/repo/tree/render/service/view-id.js +67 -0
- package/dist/core/repo/tree/render/service/view-id.js.map +1 -0
- package/dist/core/repo/tree/render/service/views.js +78 -0
- package/dist/core/repo/tree/render/service/views.js.map +1 -0
- package/dist/core/repo/tree/render/stale.js +42 -0
- package/dist/core/repo/tree/render/stale.js.map +1 -0
- package/dist/core/repo/tree/render/text.js +84 -0
- package/dist/core/repo/tree/render/text.js.map +1 -0
- package/dist/core/repo/tree/render/views.js +293 -0
- package/dist/core/repo/tree/render/views.js.map +1 -0
- package/dist/core/repo/tree/walk.js +53 -32
- package/dist/core/repo/tree/walk.js.map +1 -1
- package/dist/core/results.js +47 -12
- package/dist/core/results.js.map +1 -1
- package/dist/core/scaffold/landscape.js +121 -13
- package/dist/core/scaffold/landscape.js.map +1 -1
- package/dist/core/scaffold/legacy.js +271 -0
- package/dist/core/scaffold/legacy.js.map +1 -0
- package/dist/core/staging/commit.js +26 -15
- package/dist/core/staging/commit.js.map +1 -1
- package/dist/core/staging/lock.js +74 -12
- package/dist/core/staging/lock.js.map +1 -1
- package/dist/core/staging/recovery/intent.js +2 -5
- package/dist/core/staging/recovery/intent.js.map +1 -1
- package/dist/core/staging/recovery/recover.js +84 -6
- package/dist/core/staging/recovery/recover.js.map +1 -1
- package/dist/core/staging/snapshot/manifest.js +188 -0
- package/dist/core/staging/snapshot/manifest.js.map +1 -0
- package/dist/core/staging/{snapshot.js → snapshot/snapshot.js} +39 -45
- package/dist/core/staging/snapshot/snapshot.js.map +1 -0
- package/dist/core/staging/txn/forward.js +24 -14
- package/dist/core/staging/txn/forward.js.map +1 -1
- package/dist/core/staging/writes.js.map +1 -1
- package/dist/core/status/actions/execution.js +11 -0
- package/dist/core/status/actions/execution.js.map +1 -1
- package/dist/core/status/contracts.js +39 -15
- package/dist/core/status/contracts.js.map +1 -1
- package/dist/core/status/feature/artifacts.js +33 -8
- package/dist/core/status/feature/artifacts.js.map +1 -1
- package/dist/core/status/feature/feature.js +24 -17
- package/dist/core/status/feature/feature.js.map +1 -1
- package/dist/core/status/feature/next.js +20 -7
- package/dist/core/status/feature/next.js.map +1 -1
- package/dist/core/status/feature/words.js +96 -0
- package/dist/core/status/feature/words.js.map +1 -0
- package/dist/core/status/fleet/fleet.js +43 -14
- package/dist/core/status/fleet/fleet.js.map +1 -1
- package/dist/core/status/fleet/next.js +153 -58
- package/dist/core/status/fleet/next.js.map +1 -1
- package/dist/core/status/report.js.map +1 -1
- package/dist/core/status/scan.js.map +1 -1
- package/dist/core/status/verification.js +30 -15
- package/dist/core/status/verification.js.map +1 -1
- package/dist/core/tasks/packet.js +199 -0
- package/dist/core/tasks/packet.js.map +1 -0
- package/dist/core/tasks/record.js +43 -0
- package/dist/core/tasks/record.js.map +1 -0
- package/dist/core/tasks/sources.js +181 -0
- package/dist/core/tasks/sources.js.map +1 -0
- package/dist/core/tasks/stamps.js +76 -0
- package/dist/core/tasks/stamps.js.map +1 -0
- package/dist/core/tasks/unit.js +0 -0
- package/dist/core/tasks/unit.js.map +1 -0
- package/dist/core/usecases/capability.js +11 -14
- package/dist/core/usecases/capability.js.map +1 -1
- package/dist/core/usecases/delta/claims.js +31 -7
- package/dist/core/usecases/delta/claims.js.map +1 -1
- package/dist/core/usecases/delta/flows.js +5 -7
- package/dist/core/usecases/delta/flows.js.map +1 -1
- package/dist/core/usecases/delta/modified.js +315 -0
- package/dist/core/usecases/delta/modified.js.map +1 -0
- package/dist/core/usecases/delta/overlay.js +32 -191
- package/dist/core/usecases/delta/overlay.js.map +1 -1
- package/dist/core/usecases/fleet.js +195 -36
- package/dist/core/usecases/fleet.js.map +1 -1
- package/dist/{commands/validate/fleet/usecases → core/usecases/grade}/capability-tag.js +8 -9
- package/dist/core/usecases/grade/capability-tag.js.map +1 -0
- package/dist/core/usecases/grade/feature.js +88 -0
- package/dist/core/usecases/grade/feature.js.map +1 -0
- package/dist/{commands/validate/fleet/usecases → core/usecases/grade}/requirement-tag.js +3 -4
- package/dist/core/usecases/grade/requirement-tag.js.map +1 -0
- package/dist/{commands/validate/fleet/usecases → core/usecases/grade}/steps.js +102 -33
- package/dist/core/usecases/grade/steps.js.map +1 -0
- package/dist/{commands/validate/fleet/usecases → core/usecases/grade}/usecases.js +23 -10
- package/dist/core/usecases/grade/usecases.js.map +1 -0
- package/dist/core/usecases/operations.js +26 -14
- package/dist/core/usecases/operations.js.map +1 -1
- package/dist/core/usecases/place.js.map +1 -1
- package/dist/core/usecases/service/exclude.js +119 -0
- package/dist/core/usecases/service/exclude.js.map +1 -0
- package/dist/core/usecases/service/flows.js +164 -0
- package/dist/core/usecases/service/flows.js.map +1 -0
- package/dist/core/usecases/suites/participants.js +83 -0
- package/dist/core/usecases/suites/participants.js.map +1 -0
- package/dist/core/usecases/suites/suites.js +45 -0
- package/dist/core/usecases/suites/suites.js.map +1 -0
- package/dist/core/verify/answers.js +12 -5
- package/dist/core/verify/answers.js.map +1 -1
- package/dist/core/verify/build.js +102 -82
- package/dist/core/verify/build.js.map +1 -1
- package/dist/core/verify/checklist.js +70 -12
- package/dist/core/verify/checklist.js.map +1 -1
- package/dist/core/verify/location.js +67 -0
- package/dist/core/verify/location.js.map +1 -0
- package/dist/core/verify/pins/lint.js +3 -2
- package/dist/core/verify/pins/lint.js.map +1 -1
- package/dist/core/verify/pins/pin.js +1 -1
- package/dist/core/verify/pins/pin.js.map +1 -1
- package/dist/core/verify/read/file.js +224 -0
- package/dist/core/verify/read/file.js.map +1 -0
- package/dist/core/verify/{file.js → read/shape.js} +16 -104
- package/dist/core/verify/read/shape.js.map +1 -0
- package/dist/core/verify/read/union.js +105 -0
- package/dist/core/verify/read/union.js.map +1 -0
- package/dist/core/verify/record.js +35 -12
- package/dist/core/verify/record.js.map +1 -1
- package/dist/core/verify/store/commit.js +40 -30
- package/dist/core/verify/store/commit.js.map +1 -1
- package/dist/core/verify/store/render.js +8 -4
- package/dist/core/verify/store/render.js.map +1 -1
- package/dist/core/vocabulary/issue.js +4 -0
- package/dist/core/vocabulary/issue.js.map +1 -1
- package/dist/core/vocabulary/maturity.js +75 -4
- package/dist/core/vocabulary/maturity.js.map +1 -1
- package/dist/core/vocabulary/report.js +16 -1
- package/dist/core/vocabulary/report.js.map +1 -1
- package/examples/README.md +54 -11
- package/examples/docs/architecture/landscape.likec4 +62 -6
- package/examples/docs/architecture/services.likec4 +70 -0
- package/examples/docs/architecture/subsystems.likec4 +7 -0
- package/examples/docs/architecture/usecases/order-notification.likec4 +16 -7
- package/examples/docs/features/FEAT-101-payment-splitting/specs/payment-service/asyncapi.yaml +11 -8
- package/examples/docs/features/FEAT-101-payment-splitting/specs/payment-split-service/openapi.yaml +6 -6
- package/examples/docs/features/FEAT-112-retire-order-v1/specs/order-service/openapi.yaml +4 -1
- package/examples/docs/features/archive/FEAT-088-refunds/specs/payment-service/openapi.yaml +13 -10
- package/examples/docs/likec4.config.json +0 -1
- package/examples/docs/services/checkout-web/model.likec4 +39 -56
- package/examples/docs/services/order-service/arch.spec.md +13 -6
- package/examples/docs/services/order-service/asyncapi.yaml +3 -2
- package/examples/docs/services/order-service/model.likec4 +58 -70
- package/examples/docs/services/payment-service/arch.spec.md +7 -4
- package/examples/docs/services/payment-service/model.likec4 +38 -37
- package/examples/docs/services/platform/identity-service/arch.spec.md +7 -4
- package/examples/docs/services/platform/identity-service/model.likec4 +35 -37
- package/examples/docs/services/platform/notification-service/asyncapi.yaml +2 -1
- package/examples/docs/services/platform/notification-service/health.yaml +8 -3
- package/examples/docs/services/platform/notification-service/model.likec4 +43 -64
- package/package.json +1 -1
- package/dist/commands/validate/checks/vocabulary.js +0 -31
- package/dist/commands/validate/checks/vocabulary.js.map +0 -1
- package/dist/commands/validate/fleet/usecases/capability-tag.js.map +0 -1
- package/dist/commands/validate/fleet/usecases/requirement-tag.js.map +0 -1
- package/dist/commands/validate/fleet/usecases/steps.js.map +0 -1
- package/dist/commands/validate/fleet/usecases/usecases.js.map +0 -1
- package/dist/core/agent/agents-md/artifacts.js +0 -220
- package/dist/core/agent/agents-md/artifacts.js.map +0 -1
- package/dist/core/agent/agents-md/cycle.js +0 -165
- package/dist/core/agent/agents-md/cycle.js.map +0 -1
- package/dist/core/agent/agents-md/shipped/archive-gate.js +0 -98
- package/dist/core/agent/agents-md/shipped/archive-gate.js.map +0 -1
- package/dist/core/agent/agents-md/spine.js +0 -40
- package/dist/core/agent/agents-md/spine.js.map +0 -1
- package/dist/core/coherence/events/merged.js +0 -40
- package/dist/core/coherence/events/merged.js.map +0 -1
- package/dist/core/diff/current-state.js +0 -96
- package/dist/core/diff/current-state.js.map +0 -1
- package/dist/core/diff/victims.js +0 -247
- package/dist/core/diff/victims.js.map +0 -1
- package/dist/core/repo/tree/views.js +0 -172
- package/dist/core/repo/tree/views.js.map +0 -1
- package/dist/core/staging/snapshot.js.map +0 -1
- package/dist/core/verify/file.js.map +0 -1
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"scan.js","sourceRoot":"","sources":["../../../../src/core/doctor/reports/scan.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * The problem reports a repository has collected: how many there are, what\n * state each one is in, and which ordinal the next one takes.\n *\n * A sub-package because `doctor/` is at its five-file limit, and a scan of its\n * own because this is the one directory loam reads that loam never writes. The\n * `loam-report` protocol has always asked a repository to accumulate a corpus\n * under `loam-reports/` and then never looked at it — eleven reports landed in\n * this repository in two days, three of them already closed, and every bit of\n * that state lived outside the files, in whoever remembered. The directory is\n * loam's own convention and its contents are loam's own format, so counting\n * them is a fact loam can read as cheaply as it reads `agentFiles`. Reading is\n * the whole of it: nothing here transmits anything, which is the constraint the\n * protocol is built around.\n *\n * Everything below is best-effort on purpose. A report is a hand-written\n * Markdown file, so an absent directory, an unreadable one, a name written\n * before the numbering existed and a file with no `Status:` line are all\n * ordinary states of a healthy repository. None of them is a finding, and\n * `doctor`'s `healthy` does not move for anything in here — a repo with eleven\n * open reports is not broken, it is one that has been paying attention.\n */\nimport { readdir, readFile } from \"node:fs/promises\";\nimport { join } from \"node:path\";\nimport {\n type ProblemReportEntry,\n type ProblemReports,\n type ProblemReportStatus,\n} from \"../report.js\";\n\n/**\n * The conventional NAME of the directory the `loam-report` protocol asks a\n * repository to accumulate, spelled once — the segment `scanReports` joins onto\n * the resolved root to reach it.\n *\n * A name rather than a path, because two readers want different things out of\n * it and both are right: `ProblemReports.dir` carries the joined ABSOLUTE\n * location, because the protocol tells an agent to write there and a relative\n * spelling leaves it guessing which repository \"relative\" meant (`../report.ts`\n * records that), while `commands/doctor.ts`'s human row prints this constant, so\n * a person standing in the repository reads `loam-reports/` — what the protocol,\n * the reports themselves and every page that mentions them say.\n */\nexport const REPORTS_DIR = \"loam-reports\";\n\n/**\n * The leading ordinal of `012-2026-09-03-slug.md`. Three digits or more, so the\n * thousandth report widens the field instead of losing its number.\n */\nconst ORDINAL = /^(\\d{3,})-/;\n\n/**\n * A name written before the numbering existed: `2026-09-03-slug.md`, the shape\n * eleven reports in this repository already carry.\n *\n * Checked FIRST, and this is the whole reason it exists as its own pattern: a\n * four-digit year satisfies `ORDINAL` perfectly, so a legacy name read as an\n * ordinal is report number 2026 — and the next report loam hands out is then\n * 2027, in a directory whose highest real ordinal is 3. A legacy name is still\n * a report; it just has no number, and contributes none.\n */\nconst LEGACY_DATE_NAME = /^\\d{4}-\\d{2}-\\d{2}-/;\n\n/** The ordinal a report's file name claims, or null when the name predates numbering. */\nfunction ordinalOf(file: string): string | null {\n if (LEGACY_DATE_NAME.test(file)) return null;\n return ORDINAL.exec(file)?.[1] ?? null;\n}\n\n/**\n * The template's `- Status:` line, matched in the header FIELD BLOCK alone (see\n * `headerFields`). Only the first word is captured: the vocabulary's two\n * composite values carry prose after it (`fixed in 0.2.0-alpha.5`,\n * `superseded by 010`) that an author writes for a reader, and parsing further\n * would make loam an authority on a sentence it does not own.\n */\nconst STATUS_LINE = /^[ \\t]*-[ \\t]*Status:[ \\t]*(\\S+)/m;\n\n/** A fenced block's delimiter line, either spelling. */\nconst FENCE = /^[ \\t]*(`{3,}|~{3,})/;\n\n/**\n * An indented code block's line — the OTHER way markdown quotes code, and the\n * way `loam instructions loam-report` prints its own template (the four-space\n * block in ../../agent/support/report.ts). A fence-only reader counted such a\n * report open off the template's `- Status: open`.\n */\nconst INDENTED = /^(?: {4}|\\t)/;\n\n/** A section heading — the line that ends the header field block. */\nconst SECTION = /^##[ \\t]/;\n\n/**\n * The report's header field block: every line above the first `## ` heading,\n * with quoted code — fenced or indented — dropped.\n *\n * Matched anywhere in the file, `- Status:` convicted the wrong reports twice\n * over. A report that QUOTES the protocol template — which several do, because\n * an agent reporting that the protocol is wrong pastes the protocol — carries a\n * `- Status: open` inside a fence and was counted open however its own header\n * read; and a report whose body discusses a status line later on was read the\n * same way. The status a report claims is a header FIELD, written beside\n * `- Classification:` above the first section, so that is the only place it is\n * read from. An unterminated fence swallows the rest of the file rather than\n * flipping back — the safe direction, since the alternative is reading code as\n * a field.\n *\n * Two shapes defeated the first version of that, and both are the same quoting\n * case: a NESTED fence, where the outer ```` opens and the inner ``` was taken\n * for its close, leaving the quoted block read as fields; and an INDENTED\n * block, which the protocol itself uses and whose indented `## Summary` never\n * ends the header either. So a fence is closed only by a run of the same\n * character at least as long as the one that opened it, and an indented block\n * (four spaces after a blank line, through the blank lines inside it) is\n * dropped whole.\n */\nfunction headerFields(text: string): string {\n const kept: string[] = [];\n let fence: string | null = null;\n let indented = false;\n let afterBlank = true;\n for (const line of text.split(/\\r?\\n/)) {\n if (fence !== null) {\n const close = FENCE.exec(line)?.[1];\n if (close !== undefined && close[0] === fence[0] && close.length >= fence.length) fence = null;\n continue;\n }\n const blank = line.trim() === \"\";\n if (indented) {\n if (blank || INDENTED.test(line)) continue;\n indented = false;\n }\n const open = FENCE.exec(line)?.[1];\n if (open !== undefined) {\n fence = open;\n afterBlank = false;\n continue;\n }\n if (afterBlank && INDENTED.test(line)) {\n indented = true;\n continue;\n }\n if (SECTION.test(line)) break;\n kept.push(line);\n afterBlank = blank;\n }\n return kept.join(\"\\n\");\n}\n\n/** The words a `Status:` line may say that loam can count. */\nconst STATED: readonly ProblemReportStatus[] = [\"open\", \"sent\", \"fixed\", \"superseded\"];\n\n/**\n * The ordinal the next report takes: one past the highest present, never one\n * past the count.\n *\n * Those two differ exactly when the directory has a gap or a legacy name in it\n * — which this directory does, since the numbering arrived after eleven\n * reports had already been written — and counting instead of maximising is how\n * a next ordinal starts handing out numbers that are already taken.\n */\nfunction nextOrdinal(entries: readonly ProblemReportEntry[]): string {\n let highest = 0;\n for (const entry of entries) {\n if (entry.ordinal === null) continue;\n const value = Number.parseInt(entry.ordinal, 10);\n if (value > highest) highest = value;\n }\n return String(highest + 1).padStart(3, \"0\");\n}\n\n/**\n * What one report's own `Status:` line says, or `unstated`.\n *\n * Unreadable reads as unstated rather than throwing: a report saved as UTF-16,\n * a file the process cannot open, or a directory somebody named `notes.md` are\n * all things a diagnostic must survive. `doctor` is the command that has to be\n * able to describe a broken repository without becoming the next thing that\n * breaks in it.\n */\nasync function statusOf(path: string): Promise<ProblemReportStatus> {\n let text: string;\n try {\n text = await readFile(path, \"utf8\");\n } catch {\n // Unreadable is not a state the file claims — it is a state of the read.\n return \"unstated\";\n }\n const word = STATUS_LINE.exec(headerFields(text))?.[1]?.toLowerCase();\n return STATED.find((known) => known === word) ?? \"unstated\";\n}\n\n/**\n * Read `<root>/loam-reports/`, where `root` is the directory holding the\n * `loam.json` that resolved — the repository the reports were written at the\n * root of — falling back, when nothing parsed, to the directory of the\n * `loam.json` loam FOUND (`doctor.ts` says why that and not the cwd), and to the\n * run directory only when there is no config file anywhere above it.\n *\n * Every `*.md` in the directory is an entry, including a hand-written index\n * page: which files in there are \"really\" reports is a second naming\n * convention, and inventing one would make loam disagree with a directory\n * listing about what the directory holds. An unnumbered entry contributes\n * nothing to `next`, so an index costs the corpus nothing either.\n */\nexport async function scanReports(root: string): Promise<ProblemReports> {\n const dir = join(root, REPORTS_DIR);\n let names: string[];\n try {\n names = (await readdir(dir)).filter((name) => name.toLowerCase().endsWith(\".md\"));\n } catch (error) {\n // Absent is the state of every repository that has never had to write a\n // report, and it is the common one — `present: false`, nothing to say. Any\n // other errno means the directory IS there and this process could not list\n // it, which is a different sentence: the corpus exists and loam cannot see\n // into it, so `present` stays true and the counts stay honestly empty.\n const code = (error as NodeJS.ErrnoException).code;\n const present = code !== \"ENOENT\" && code !== \"ENOTDIR\";\n return { dir, present, total: 0, next: \"001\", entries: [] };\n }\n\n // Sorted here rather than left in readdir order, which is the filesystem's and\n // differs between one machine and another — and sorted by the ORDINAL the name\n // claims, numerically, because report order is what a reader scanning the list\n // expects and byte order stops being it at exactly the point the widening rule\n // was written for: `1000-…` sorts before `998-…`, so a directory that has\n // passed its thousandth report reads 1000, 1001, 998, 999 (verification\n // 2026-09-04, second pass). Unnumbered names — the shape reports carried before\n // the numbering existed — contribute no ordinal, so they sort last among\n // themselves by name, where a reader looking for \"the next one\" is not looking.\n const rank = (file: string): number => {\n const ordinal = ordinalOf(file);\n return ordinal === null ? Number.POSITIVE_INFINITY : Number.parseInt(ordinal, 10);\n };\n names.sort((a, b) => rank(a) - rank(b) || (a < b ? -1 : a > b ? 1 : 0));\n // Read together, like `staleAgentFiles` does: a report corpus is a flat\n // directory of small hand-written files, so there is nothing here worth a\n // concurrency cap, and `Promise.all` preserves the order just established.\n const entries = await Promise.all(names.map(async (file): Promise<ProblemReportEntry> => ({\n file,\n ordinal: ordinalOf(file),\n status: await statusOf(join(dir, file)),\n })));\n\n return {\n dir,\n present: true,\n total: entries.length,\n next: nextOrdinal(entries),\n entries,\n };\n}\n"]}
|
|
@@ -6,8 +6,15 @@
|
|
|
6
6
|
* `Based-On:` pin, which is a claim about which living version this was written
|
|
7
7
|
* against — meaningless once it IS the living version, and poison for the next
|
|
8
8
|
* feature's baseline, which would otherwise hash the previous feature's pin.
|
|
9
|
+
*
|
|
10
|
+
* It is also where the merge says what it TAKES AWAY (`replacementsOf`,
|
|
11
|
+
* `droppedParts`), because only the algebra knows which living requirement a
|
|
12
|
+
* delta requirement selects: a MODIFIED carries its full new text and replaces
|
|
13
|
+
* the living requirement whole, so every scenario and every join line it does
|
|
14
|
+
* not restate is gone after the archive — and a second reading of "which one
|
|
15
|
+
* does this select" anywhere else would be a second answer free to disagree.
|
|
9
16
|
*/
|
|
10
|
-
import { requirementIdProblems, withoutBaseline } from "./spec.js";
|
|
17
|
+
import { requirementIdProblems, splitRealizesPin, withoutBaseline } from "./spec.js";
|
|
11
18
|
/**
|
|
12
19
|
* A delta requirement as it lands in the living document, with the bookkeeping
|
|
13
20
|
* that belongs to the DELTA left behind: the `Based-On:` pin (a claim about
|
|
@@ -19,18 +26,71 @@ function asLiving(d) {
|
|
|
19
26
|
const { line: _dropped, ...rest } = withoutBaseline(d);
|
|
20
27
|
return { ...rest, kind: "BASE" };
|
|
21
28
|
}
|
|
22
|
-
/**
|
|
29
|
+
/**
|
|
30
|
+
* Apply a feature's ADDED/MODIFIED/REMOVED requirements onto a living requirement set.
|
|
31
|
+
*
|
|
32
|
+
* A living requirement the delta does not touch comes back as the SAME object
|
|
33
|
+
* it went in as (a living spec's requirements are already BASE), and that
|
|
34
|
+
* identity is load-bearing: archive keys each requirement's raw bytes on it, so
|
|
35
|
+
* an untouched requirement is written back exactly as its author wrote it and
|
|
36
|
+
* only the ones this function replaced or added are serialized.
|
|
37
|
+
*/
|
|
23
38
|
export function applyRequirementDelta(living, delta) {
|
|
39
|
+
return foldDelta(living, delta).merged;
|
|
40
|
+
}
|
|
41
|
+
/**
|
|
42
|
+
* Every living requirement the delta replaces or removes, in delta order — the
|
|
43
|
+
* walk `applyRequirementDelta` takes, so the two cannot disagree about which
|
|
44
|
+
* requirement a MODIFIED selected. Throws exactly when the merge would.
|
|
45
|
+
*/
|
|
46
|
+
export function replacementsOf(living, delta) {
|
|
47
|
+
return foldDelta(living, delta).replaced;
|
|
48
|
+
}
|
|
49
|
+
function foldDelta(living, delta) {
|
|
24
50
|
const malformed = [...requirementIdProblems(living), ...requirementIdProblems(delta)];
|
|
25
51
|
if (malformed.length > 0) {
|
|
26
52
|
throw new Error("cannot merge requirements with invalid, repeated, or duplicate Requirement-ID declarations");
|
|
27
53
|
}
|
|
28
|
-
let result = living.map((r) => ({ ...r, kind: "BASE" }));
|
|
54
|
+
let result = living.map((r) => (r.kind === "BASE" ? r : { ...r, kind: "BASE" }));
|
|
55
|
+
const replaced = [];
|
|
56
|
+
// What each requirement the fold has PRODUCED stands for: the replacement
|
|
57
|
+
// record it belongs to, or null for one this delta added. A delta may select
|
|
58
|
+
// one requirement twice (nothing refuses a repeated heading in a delta), and
|
|
59
|
+
// without this the second step reported the first step's text as "living" —
|
|
60
|
+
// a drop the merge never makes. With it every record stays pre-merge to
|
|
61
|
+
// post-merge, so what `droppedParts` reads is the net change.
|
|
62
|
+
const born = new Map();
|
|
63
|
+
const record = (kind, previous, merged) => {
|
|
64
|
+
const earlier = born.get(previous);
|
|
65
|
+
if (earlier === undefined) {
|
|
66
|
+
const entry = { kind, living: previous, merged };
|
|
67
|
+
replaced.push(entry);
|
|
68
|
+
if (merged !== null)
|
|
69
|
+
born.set(merged, entry);
|
|
70
|
+
}
|
|
71
|
+
else if (earlier === null) {
|
|
72
|
+
// Added by this delta, so nothing living is replaced by rewriting it.
|
|
73
|
+
if (merged !== null)
|
|
74
|
+
born.set(merged, null);
|
|
75
|
+
}
|
|
76
|
+
else {
|
|
77
|
+
earlier.merged = merged;
|
|
78
|
+
if (merged === null)
|
|
79
|
+
earlier.kind = "REMOVED";
|
|
80
|
+
else
|
|
81
|
+
born.set(merged, earlier);
|
|
82
|
+
}
|
|
83
|
+
};
|
|
84
|
+
const add = (merged) => {
|
|
85
|
+
born.set(merged, null);
|
|
86
|
+
result.push(merged);
|
|
87
|
+
};
|
|
29
88
|
for (const d of delta) {
|
|
30
89
|
// BASE is not a delta kind — a requirement outside an ADDED/MODIFIED/REMOVED
|
|
31
90
|
// section (e.g. quoted under ## Notes) is documentation, not a change.
|
|
32
91
|
if (d.kind === "BASE")
|
|
33
92
|
continue;
|
|
93
|
+
const kind = d.kind;
|
|
34
94
|
if (d.id !== undefined) {
|
|
35
95
|
const idMatches = result
|
|
36
96
|
.map((r, i) => (r.id === d.id ? i : -1))
|
|
@@ -44,17 +104,25 @@ export function applyRequirementDelta(living, delta) {
|
|
|
44
104
|
}
|
|
45
105
|
if (d.kind === "REMOVED") {
|
|
46
106
|
const selected = new Set(idMatches);
|
|
107
|
+
for (const i of idMatches)
|
|
108
|
+
record(kind, result[i], null);
|
|
47
109
|
result = result.filter((_, i) => !selected.has(i));
|
|
48
110
|
}
|
|
49
111
|
else {
|
|
50
112
|
const merged = asLiving(d);
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
113
|
+
const at = idMatches[0];
|
|
114
|
+
if (at === undefined)
|
|
115
|
+
add(merged);
|
|
116
|
+
else {
|
|
117
|
+
record(kind, result[at], merged);
|
|
118
|
+
result[at] = merged;
|
|
119
|
+
}
|
|
55
120
|
}
|
|
56
121
|
}
|
|
57
122
|
else if (d.kind === "REMOVED") {
|
|
123
|
+
for (const r of result)
|
|
124
|
+
if (r.name === d.name)
|
|
125
|
+
record(kind, r, null);
|
|
58
126
|
result = result.filter((r) => r.name !== d.name);
|
|
59
127
|
}
|
|
60
128
|
else {
|
|
@@ -66,12 +134,91 @@ export function applyRequirementDelta(living, delta) {
|
|
|
66
134
|
...asLiving(d),
|
|
67
135
|
...(inheritedId === undefined ? {} : { id: inheritedId }),
|
|
68
136
|
};
|
|
69
|
-
if (i
|
|
137
|
+
if (i < 0)
|
|
138
|
+
add(merged);
|
|
139
|
+
else {
|
|
140
|
+
record(kind, result[i], merged);
|
|
70
141
|
result[i] = merged;
|
|
71
|
-
|
|
72
|
-
result.push(merged);
|
|
142
|
+
}
|
|
73
143
|
}
|
|
74
144
|
}
|
|
75
|
-
return result;
|
|
145
|
+
return { merged: result, replaced };
|
|
146
|
+
}
|
|
147
|
+
/**
|
|
148
|
+
* The join lines a requirement carries, keyed by `Requirement` field and
|
|
149
|
+
* spelled as the body line writes them — every list the parser reads out of a
|
|
150
|
+
* requirement's text, in one order, so the archive plan, `loam delta` and their
|
|
151
|
+
* `--json` rows name a dropped entry the same way.
|
|
152
|
+
*/
|
|
153
|
+
export const DIRECTIVE_LINES = {
|
|
154
|
+
operations: "Operations",
|
|
155
|
+
covers: "Covers",
|
|
156
|
+
requires: "Requires",
|
|
157
|
+
capabilities: "Capability",
|
|
158
|
+
realizes: "Realizes",
|
|
159
|
+
publishes: "Publishes",
|
|
160
|
+
consumes: "Consumes",
|
|
161
|
+
};
|
|
162
|
+
/**
|
|
163
|
+
* What a replacement drops: the living scenarios its new text does not carry,
|
|
164
|
+
* by name, and each join-line entry it does not restate, verbatim.
|
|
165
|
+
*
|
|
166
|
+
* VERBATIM AND BY NAME, and that is a reading, not a judgement — with one
|
|
167
|
+
* exception for bookkeeping: a `Realizes:` entry is compared, and reported, by
|
|
168
|
+
* the target it names, its `@<digest>` pin set aside. A scenario
|
|
169
|
+
* renamed is reported dropped, and a `Covers:` entry respelled from an element
|
|
170
|
+
* id to the service name is reported dropped too, though either may mean the
|
|
171
|
+
* same thing — this lists what the living text says that the merged text does
|
|
172
|
+
* not, and the author decides. What it must never be again is the old answer,
|
|
173
|
+
* which was nothing: a MODIFIED that restated one of two scenarios merged, and
|
|
174
|
+
* the second vanished under the archive's exit 0.
|
|
175
|
+
*/
|
|
176
|
+
export function droppedParts(replacement) {
|
|
177
|
+
const { living, merged } = replacement;
|
|
178
|
+
const keptScenarios = new Set((merged?.scenarios ?? []).map((s) => s.name));
|
|
179
|
+
const directives = {};
|
|
180
|
+
for (const key of Object.keys(DIRECTIVE_LINES)) {
|
|
181
|
+
// A `Realizes:` entry is compared by the target it names: the `@<digest>`
|
|
182
|
+
// pin `loam rebase --living` writes is bookkeeping, and a replacement that
|
|
183
|
+
// restates the target unpinned has kept the join, not dropped it.
|
|
184
|
+
const spell = key === "realizes" ? (entry) => splitRealizesPin(entry).target : (entry) => entry;
|
|
185
|
+
const kept = new Set((merged?.[key] ?? []).map(spell));
|
|
186
|
+
const gone = [...new Set(living[key].map(spell))].filter((entry) => !kept.has(entry));
|
|
187
|
+
if (gone.length > 0)
|
|
188
|
+
directives[key] = gone;
|
|
189
|
+
}
|
|
190
|
+
return { scenarios: living.scenarios.map((s) => s.name).filter((name) => !keptScenarios.has(name)), directives };
|
|
191
|
+
}
|
|
192
|
+
/** Does this replacement take anything away at all? */
|
|
193
|
+
export function dropsAnything(parts) {
|
|
194
|
+
return parts.scenarios.length > 0 || Object.keys(parts.directives).length > 0;
|
|
195
|
+
}
|
|
196
|
+
/**
|
|
197
|
+
* Every replacement that takes something away: a removal always (the
|
|
198
|
+
* requirement itself is what goes), a MODIFIED or a landing ADDED whenever its
|
|
199
|
+
* new text restates less than the living one held.
|
|
200
|
+
*/
|
|
201
|
+
export function droppedBy(living, delta) {
|
|
202
|
+
return replacementsOf(living, delta).flatMap((replacement) => {
|
|
203
|
+
const parts = droppedParts(replacement);
|
|
204
|
+
if (replacement.kind !== "REMOVED" && !dropsAnything(parts))
|
|
205
|
+
return [];
|
|
206
|
+
const { name, id } = replacement.living;
|
|
207
|
+
return [{ kind: replacement.kind, requirement: name, ...(id === undefined ? {} : { id }), ...parts }];
|
|
208
|
+
});
|
|
209
|
+
}
|
|
210
|
+
/** One dropped row in words — the line the archive plan and `loam delta` both print. */
|
|
211
|
+
export function droppedSentence(row) {
|
|
212
|
+
const who = row.id === undefined ? `'${row.requirement}'` : `${row.id} '${row.requirement}'`;
|
|
213
|
+
const lost = [
|
|
214
|
+
...(row.scenarios.length === 0 ? [] : [`scenario ${row.scenarios.map((s) => `'${s}'`).join(", ")}`]),
|
|
215
|
+
...Object.keys(DIRECTIVE_LINES).flatMap((key) => {
|
|
216
|
+
const gone = row.directives[key];
|
|
217
|
+
return gone === undefined ? [] : [`${DIRECTIVE_LINES[key]}: ${gone.join(", ")}`];
|
|
218
|
+
}),
|
|
219
|
+
];
|
|
220
|
+
if (row.kind === "REMOVED")
|
|
221
|
+
return lost.length === 0 ? `${who} is removed` : `${who} is removed, and with it ${lost.join("; ")}`;
|
|
222
|
+
return `${who} is replaced whole, and its new text does not carry ${lost.join("; ")}`;
|
|
76
223
|
}
|
|
77
224
|
//# sourceMappingURL=apply.js.map
|
|
@@ -1 +1 @@
|
|
|
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/** Apply a feature's ADDED/MODIFIED/REMOVED requirements onto a living requirement set. */\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\" 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"]}
|
|
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 *\n * It is also where the merge says what it TAKES AWAY (`replacementsOf`,\n * `droppedParts`), because only the algebra knows which living requirement a\n * delta requirement selects: a MODIFIED carries its full new text and replaces\n * the living requirement whole, so every scenario and every join line it does\n * not restate is gone after the archive — and a second reading of \"which one\n * does this select\" anywhere else would be a second answer free to disagree.\n */\nimport { requirementIdProblems, splitRealizesPin, 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/** One living requirement a delta requirement takes the place of, or takes away. */\nexport interface Replacement {\n /**\n * What the delta did to it, NET: MODIFIED or REMOVED — or ADDED, when it lands on a\n * requirement that already exists. A requirement the delta selects twice keeps\n * one record, REMOVED if its last step removed it.\n */\n kind: Exclude<DeltaKind, \"BASE\">;\n /** The living requirement as it stood before the merge. */\n living: Requirement;\n /** What lands in its place, or null for a removal. */\n merged: Requirement | null;\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 return foldDelta(living, delta).merged;\n}\n\n/**\n * Every living requirement the delta replaces or removes, in delta order — the\n * walk `applyRequirementDelta` takes, so the two cannot disagree about which\n * requirement a MODIFIED selected. Throws exactly when the merge would.\n */\nexport function replacementsOf(living: Requirement[], delta: Requirement[]): Replacement[] {\n return foldDelta(living, delta).replaced;\n}\n\nfunction foldDelta(living: Requirement[], delta: Requirement[]): { merged: Requirement[]; replaced: Replacement[] } {\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 const replaced: Replacement[] = [];\n // What each requirement the fold has PRODUCED stands for: the replacement\n // record it belongs to, or null for one this delta added. A delta may select\n // one requirement twice (nothing refuses a repeated heading in a delta), and\n // without this the second step reported the first step's text as \"living\" —\n // a drop the merge never makes. With it every record stays pre-merge to\n // post-merge, so what `droppedParts` reads is the net change.\n const born = new Map<Requirement, Replacement | null>();\n const record = (kind: Replacement[\"kind\"], previous: Requirement, merged: Requirement | null): void => {\n const earlier = born.get(previous);\n if (earlier === undefined) {\n const entry: Replacement = { kind, living: previous, merged };\n replaced.push(entry);\n if (merged !== null) born.set(merged, entry);\n } else if (earlier === null) {\n // Added by this delta, so nothing living is replaced by rewriting it.\n if (merged !== null) born.set(merged, null);\n } else {\n earlier.merged = merged;\n if (merged === null) earlier.kind = \"REMOVED\";\n else born.set(merged, earlier);\n }\n };\n const add = (merged: Requirement): void => {\n born.set(merged, null);\n result.push(merged);\n };\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 const kind = d.kind;\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 for (const i of idMatches) record(kind, result[i]!, null);\n result = result.filter((_, i) => !selected.has(i));\n } else {\n const merged: Requirement = asLiving(d);\n const at = idMatches[0];\n if (at === undefined) add(merged);\n else {\n record(kind, result[at]!, merged);\n result[at] = merged;\n }\n }\n } else if (d.kind === \"REMOVED\") {\n for (const r of result) if (r.name === d.name) record(kind, r, null);\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) add(merged);\n else {\n record(kind, result[i]!, merged);\n result[i] = merged;\n }\n }\n }\n return { merged: result, replaced };\n}\n\n/**\n * The join lines a requirement carries, keyed by `Requirement` field and\n * spelled as the body line writes them — every list the parser reads out of a\n * requirement's text, in one order, so the archive plan, `loam delta` and their\n * `--json` rows name a dropped entry the same way.\n */\nexport const DIRECTIVE_LINES = {\n operations: \"Operations\",\n covers: \"Covers\",\n requires: \"Requires\",\n capabilities: \"Capability\",\n realizes: \"Realizes\",\n publishes: \"Publishes\",\n consumes: \"Consumes\",\n} as const;\n\nexport type DirectiveKey = keyof typeof DIRECTIVE_LINES;\n\n/** What one replacement takes out of the living documents. */\nexport interface DroppedParts {\n /** Living scenario names the replacement does not carry — every one of them, for a removal. */\n scenarios: string[];\n /** Per join line, the living entries the replacement does not restate. Only lines that lose something are present. */\n directives: Partial<Record<DirectiveKey, string[]>>;\n}\n\n/**\n * What a replacement drops: the living scenarios its new text does not carry,\n * by name, and each join-line entry it does not restate, verbatim.\n *\n * VERBATIM AND BY NAME, and that is a reading, not a judgement — with one\n * exception for bookkeeping: a `Realizes:` entry is compared, and reported, by\n * the target it names, its `@<digest>` pin set aside. A scenario\n * renamed is reported dropped, and a `Covers:` entry respelled from an element\n * id to the service name is reported dropped too, though either may mean the\n * same thing — this lists what the living text says that the merged text does\n * not, and the author decides. What it must never be again is the old answer,\n * which was nothing: a MODIFIED that restated one of two scenarios merged, and\n * the second vanished under the archive's exit 0.\n */\nexport function droppedParts(replacement: Replacement): DroppedParts {\n const { living, merged } = replacement;\n const keptScenarios = new Set((merged?.scenarios ?? []).map((s) => s.name));\n const directives: Partial<Record<DirectiveKey, string[]>> = {};\n for (const key of Object.keys(DIRECTIVE_LINES) as DirectiveKey[]) {\n // A `Realizes:` entry is compared by the target it names: the `@<digest>`\n // pin `loam rebase --living` writes is bookkeeping, and a replacement that\n // restates the target unpinned has kept the join, not dropped it.\n const spell = key === \"realizes\" ? (entry: string): string => splitRealizesPin(entry).target : (entry: string): string => entry;\n const kept = new Set((merged?.[key] ?? []).map(spell));\n const gone = [...new Set(living[key].map(spell))].filter((entry) => !kept.has(entry));\n if (gone.length > 0) directives[key] = gone;\n }\n return { scenarios: living.scenarios.map((s) => s.name).filter((name) => !keptScenarios.has(name)), directives };\n}\n\n/** Does this replacement take anything away at all? */\nexport function dropsAnything(parts: DroppedParts): boolean {\n return parts.scenarios.length > 0 || Object.keys(parts.directives).length > 0;\n}\n\n/** One replacement that takes something away, as both the archive plan and `loam delta` report it. */\nexport interface DroppedRow extends DroppedParts {\n kind: Replacement[\"kind\"];\n /** The LIVING requirement's heading and id — what a reader finds in the file today. */\n requirement: string;\n id?: string;\n}\n\n/**\n * Every replacement that takes something away: a removal always (the\n * requirement itself is what goes), a MODIFIED or a landing ADDED whenever its\n * new text restates less than the living one held.\n */\nexport function droppedBy(living: Requirement[], delta: Requirement[]): DroppedRow[] {\n return replacementsOf(living, delta).flatMap((replacement) => {\n const parts = droppedParts(replacement);\n if (replacement.kind !== \"REMOVED\" && !dropsAnything(parts)) return [];\n const { name, id } = replacement.living;\n return [{ kind: replacement.kind, requirement: name, ...(id === undefined ? {} : { id }), ...parts }];\n });\n}\n\n/** One dropped row in words — the line the archive plan and `loam delta` both print. */\nexport function droppedSentence(row: DroppedRow): string {\n const who = row.id === undefined ? `'${row.requirement}'` : `${row.id} '${row.requirement}'`;\n const lost = [\n ...(row.scenarios.length === 0 ? [] : [`scenario ${row.scenarios.map((s) => `'${s}'`).join(\", \")}`]),\n ...(Object.keys(DIRECTIVE_LINES) as DirectiveKey[]).flatMap((key) => {\n const gone = row.directives[key];\n return gone === undefined ? [] : [`${DIRECTIVE_LINES[key]}: ${gone.join(\", \")}`];\n }),\n ];\n if (row.kind === \"REMOVED\") return lost.length === 0 ? `${who} is removed` : `${who} is removed, and with it ${lost.join(\"; \")}`;\n return `${who} is replaced whole, and its new text does not carry ${lost.join(\"; \")}`;\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
|
}
|