@ybotok/loam 0.2.0-alpha.7 → 0.2.0-alpha.9
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +1315 -1
- package/COMPARISON.md +91 -3
- package/CONTRIBUTING.md +37 -0
- package/MIGRATING-from-OpenSpec.md +40 -22
- package/QUICKSTART.md +181 -9
- package/README.md +256 -57
- package/ROADMAP.md +83 -7
- package/SCHEMA.md +569 -98
- package/WORKFLOW.md +272 -62
- package/dist/cli.js +26 -8
- package/dist/cli.js.map +1 -1
- package/dist/commands/adopt/adopt.js +5 -10
- package/dist/commands/adopt/adopt.js.map +1 -1
- package/dist/commands/archive/archive.js +15 -18
- package/dist/commands/archive/archive.js.map +1 -1
- package/dist/commands/archive/plan/contracts/asyncapi.js +27 -41
- package/dist/commands/archive/plan/contracts/asyncapi.js.map +1 -1
- package/dist/commands/archive/plan/contracts/consumers.js +56 -0
- package/dist/commands/archive/plan/contracts/consumers.js.map +1 -0
- package/dist/commands/archive/plan/contracts/openapi.js +25 -42
- package/dist/commands/archive/plan/contracts/openapi.js.map +1 -1
- package/dist/commands/archive/plan/contracts/verdict-lines.js +41 -0
- package/dist/commands/archive/plan/contracts/verdict-lines.js.map +1 -0
- package/dist/commands/archive/plan/gate.js +3 -7
- package/dist/commands/archive/plan/gate.js.map +1 -1
- package/dist/commands/archive/plan/landscape.js +4 -4
- package/dist/commands/archive/plan/landscape.js.map +1 -1
- package/dist/commands/archive/plan/refusal.js +8 -18
- package/dist/commands/archive/plan/refusal.js.map +1 -1
- package/dist/commands/archive/plan/specs.js +134 -25
- package/dist/commands/archive/plan/specs.js.map +1 -1
- package/dist/commands/archive/plan/state.js +1 -0
- package/dist/commands/archive/plan/state.js.map +1 -1
- package/dist/commands/archive/run.js +33 -11
- package/dist/commands/archive/run.js.map +1 -1
- package/dist/commands/archive/shipped.js +49 -0
- package/dist/commands/archive/shipped.js.map +1 -0
- package/dist/commands/context/context.js +4 -7
- package/dist/commands/context/context.js.map +1 -1
- package/dist/commands/context/print.js +2 -2
- package/dist/commands/context/print.js.map +1 -1
- package/dist/commands/delta/delta.js +44 -12
- package/dist/commands/delta/delta.js.map +1 -1
- package/dist/commands/delta/print.js +1 -1
- package/dist/commands/delta/print.js.map +1 -1
- package/dist/commands/dependencies.js +3 -7
- package/dist/commands/dependencies.js.map +1 -1
- package/dist/commands/diff/diff.js +20 -27
- package/dist/commands/diff/diff.js.map +1 -1
- package/dist/commands/explore.js +4 -8
- package/dist/commands/explore.js.map +1 -1
- package/dist/commands/gate/gate.js +45 -8
- package/dist/commands/gate/gate.js.map +1 -1
- package/dist/commands/gate/print.js +15 -3
- package/dist/commands/gate/print.js.map +1 -1
- package/dist/commands/gherkin/commit.js +13 -54
- package/dist/commands/gherkin/commit.js.map +1 -1
- package/dist/commands/gherkin/gherkin.js +36 -20
- package/dist/commands/gherkin/gherkin.js.map +1 -1
- package/dist/commands/gherkin/reconcile.js +10 -13
- package/dist/commands/gherkin/reconcile.js.map +1 -1
- package/dist/commands/init/first-hour.js +44 -0
- package/dist/commands/init/first-hour.js.map +1 -1
- package/dist/commands/init/init.js +18 -22
- package/dist/commands/init/init.js.map +1 -1
- package/dist/commands/list/campaign/campaign.js +4 -6
- package/dist/commands/list/campaign/campaign.js.map +1 -1
- package/dist/commands/list/json.js +0 -4
- package/dist/commands/list/json.js.map +1 -1
- package/dist/commands/list/list.js +38 -23
- package/dist/commands/list/list.js.map +1 -1
- package/dist/commands/list/print.js +38 -6
- package/dist/commands/list/print.js.map +1 -1
- package/dist/commands/list/suites/suites.js +140 -0
- package/dist/commands/list/suites/suites.js.map +1 -0
- package/dist/commands/list/views.js +28 -33
- package/dist/commands/list/views.js.map +1 -1
- package/dist/commands/mcp/dispatch.js +91 -36
- package/dist/commands/mcp/dispatch.js.map +1 -1
- package/dist/commands/mcp/mcp.js +56 -15
- package/dist/commands/mcp/mcp.js.map +1 -1
- package/dist/commands/mcp/serve.js.map +1 -1
- package/dist/commands/migrate-openspec/materialize/active.js +27 -22
- package/dist/commands/migrate-openspec/materialize/active.js.map +1 -1
- package/dist/commands/migrate-openspec/materialize/stage.js +4 -3
- package/dist/commands/migrate-openspec/materialize/stage.js.map +1 -1
- package/dist/commands/migrate-openspec/materialize/target.js +2 -2
- package/dist/commands/migrate-openspec/materialize/target.js.map +1 -1
- package/dist/commands/migrate-openspec/openspec/decisions.js +9 -14
- package/dist/commands/migrate-openspec/openspec/decisions.js.map +1 -1
- package/dist/commands/migrate-openspec/openspec/error.js +7 -8
- package/dist/commands/migrate-openspec/openspec/error.js.map +1 -1
- package/dist/commands/migrate-openspec/openspec/mapping.js +1 -9
- package/dist/commands/migrate-openspec/openspec/mapping.js.map +1 -1
- package/dist/commands/migrate-openspec/openspec/paths.js +3 -6
- package/dist/commands/migrate-openspec/openspec/paths.js.map +1 -1
- package/dist/commands/new/capability-template.js +118 -0
- package/dist/commands/new/capability-template.js.map +1 -0
- package/dist/commands/new/commit.js +8 -22
- package/dist/commands/new/commit.js.map +1 -1
- package/dist/commands/new/modifies.js +206 -0
- package/dist/commands/new/modifies.js.map +1 -0
- package/dist/commands/new/new.js +59 -14
- package/dist/commands/new/new.js.map +1 -1
- package/dist/commands/new/templates.js +48 -124
- package/dist/commands/new/templates.js.map +1 -1
- package/dist/commands/open.js +3 -7
- package/dist/commands/open.js.map +1 -1
- package/dist/commands/policy/gate.js +27 -0
- package/dist/commands/policy/gate.js.map +1 -1
- package/dist/commands/rebase/plan.js +2 -2
- package/dist/commands/rebase/plan.js.map +1 -1
- package/dist/commands/rebase/rebase.js +60 -35
- package/dist/commands/rebase/rebase.js.map +1 -1
- package/dist/commands/seed/commit.js +21 -25
- package/dist/commands/seed/commit.js.map +1 -1
- package/dist/commands/seed/seed.js +6 -9
- package/dist/commands/seed/seed.js.map +1 -1
- package/dist/commands/show/show.js +3 -7
- package/dist/commands/show/show.js.map +1 -1
- package/dist/commands/status/print.js +13 -4
- package/dist/commands/status/print.js.map +1 -1
- package/dist/commands/status/status.js +3 -7
- package/dist/commands/status/status.js.map +1 -1
- package/dist/commands/steps/steps.js +3 -7
- package/dist/commands/steps/steps.js.map +1 -1
- package/dist/commands/subsystem/edit.js +3 -5
- package/dist/commands/subsystem/edit.js.map +1 -1
- package/dist/commands/subsystem/move.js +5 -8
- 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 +14 -41
- package/dist/commands/subsystem/sync.js.map +1 -1
- package/dist/commands/subsystem/txn/txn.js +17 -38
- package/dist/commands/subsystem/txn/txn.js.map +1 -1
- package/dist/commands/subsystem/txn/views.js +32 -5
- package/dist/commands/subsystem/txn/views.js.map +1 -1
- package/dist/commands/tasks/render.js +116 -0
- package/dist/commands/tasks/render.js.map +1 -0
- package/dist/commands/tasks/tasks.js +113 -0
- package/dist/commands/tasks/tasks.js.map +1 -0
- package/dist/commands/unarchive/manifest.js +6 -9
- package/dist/commands/unarchive/manifest.js.map +1 -1
- package/dist/commands/unarchive/restore.js +17 -28
- package/dist/commands/unarchive/restore.js.map +1 -1
- package/dist/commands/unarchive/unarchive.js +22 -30
- package/dist/commands/unarchive/unarchive.js.map +1 -1
- package/dist/commands/validate/arch-coverage.js +6 -15
- package/dist/commands/validate/arch-coverage.js.map +1 -1
- package/dist/commands/validate/checks/fleet-shape.js +4 -49
- package/dist/commands/validate/checks/fleet-shape.js.map +1 -1
- package/dist/commands/validate/checks/requirements.js +18 -8
- package/dist/commands/validate/checks/requirements.js.map +1 -1
- package/dist/commands/validate/checks/sources.js +6 -2
- package/dist/commands/validate/checks/sources.js.map +1 -1
- package/dist/commands/validate/feature.js +23 -19
- package/dist/commands/validate/feature.js.map +1 -1
- package/dist/commands/validate/fleet/landscape.js +23 -23
- package/dist/commands/validate/fleet/landscape.js.map +1 -1
- package/dist/commands/validate/fleet/map/attest.js +6 -10
- package/dist/commands/validate/fleet/map/attest.js.map +1 -1
- package/dist/commands/validate/fleet/map/bindings.js +1 -3
- package/dist/commands/validate/fleet/map/bindings.js.map +1 -1
- package/dist/commands/validate/fleet/map/usecases.js +5 -11
- package/dist/commands/validate/fleet/map/usecases.js.map +1 -1
- package/dist/commands/validate/fleet/obligations.js +1 -5
- package/dist/commands/validate/fleet/obligations.js.map +1 -1
- package/dist/commands/validate/fleet/scorecard/print.js +5 -1
- package/dist/commands/validate/fleet/scorecard/print.js.map +1 -1
- package/dist/commands/validate/fleet/scorecard/scorecard.js +17 -11
- package/dist/commands/validate/fleet/scorecard/scorecard.js.map +1 -1
- package/dist/commands/validate/fleet/usecases/usecases.js +6 -4
- package/dist/commands/validate/fleet/usecases/usecases.js.map +1 -1
- package/dist/commands/validate/fleet/views/fleet-project.js.map +1 -1
- package/dist/commands/validate/fleet/views/root-documents.js +8 -28
- package/dist/commands/validate/fleet/views/root-documents.js.map +1 -1
- package/dist/commands/validate/links/corpus.js +1 -4
- package/dist/commands/validate/links/corpus.js.map +1 -1
- package/dist/commands/validate/links/glossary.js.map +1 -1
- package/dist/commands/validate/report.js +5 -4
- package/dist/commands/validate/report.js.map +1 -1
- package/dist/commands/validate/scope.js +113 -0
- package/dist/commands/validate/scope.js.map +1 -0
- package/dist/commands/validate/service/api.js +16 -104
- package/dist/commands/validate/service/api.js.map +1 -1
- package/dist/commands/validate/service/contracts/generated.js +134 -0
- package/dist/commands/validate/service/contracts/generated.js.map +1 -0
- package/dist/commands/validate/service/events/events.js +1 -1
- package/dist/commands/validate/service/events/events.js.map +1 -1
- package/dist/commands/validate/service/events/message-producers.js +1 -1
- package/dist/commands/validate/service/events/message-producers.js.map +1 -1
- package/dist/commands/validate/service/evidence-pins.js +56 -10
- package/dist/commands/validate/service/evidence-pins.js.map +1 -1
- package/dist/commands/validate/service/service.js +16 -26
- package/dist/commands/validate/service/service.js.map +1 -1
- package/dist/commands/validate/service/specs.js +31 -38
- package/dist/commands/validate/service/specs.js.map +1 -1
- package/dist/commands/validate/service/spine.js +8 -11
- package/dist/commands/validate/service/spine.js.map +1 -1
- package/dist/commands/validate/service/usecases/usecases.js +3 -3
- package/dist/commands/validate/service/usecases/usecases.js.map +1 -1
- package/dist/commands/validate/validate.js +44 -26
- package/dist/commands/validate/validate.js.map +1 -1
- package/dist/commands/verify/cross/diff.js +2 -6
- package/dist/commands/verify/cross/diff.js.map +1 -1
- package/dist/commands/verify/evidence/contract.js +1 -1
- package/dist/commands/verify/evidence/contract.js.map +1 -1
- package/dist/commands/verify/evidence/read.js +12 -3
- package/dist/commands/verify/evidence/read.js.map +1 -1
- package/dist/commands/verify/frozen.js +45 -23
- package/dist/commands/verify/frozen.js.map +1 -1
- package/dist/commands/verify/record.js +69 -17
- package/dist/commands/verify/record.js.map +1 -1
- package/dist/commands/verify/report.js +27 -44
- package/dist/commands/verify/report.js.map +1 -1
- package/dist/commands/verify/results.js +112 -30
- package/dist/commands/verify/results.js.map +1 -1
- package/dist/commands/verify/verify.js +54 -51
- package/dist/commands/verify/verify.js.map +1 -1
- package/dist/commands/vouch/pack/pack.js +2 -2
- package/dist/commands/vouch/pack/pack.js.map +1 -1
- package/dist/commands/vouch/pack/print.js +1 -6
- package/dist/commands/vouch/pack/print.js.map +1 -1
- package/dist/commands/vouch/pack/sections.js +1 -2
- package/dist/commands/vouch/pack/sections.js.map +1 -1
- package/dist/commands/vouch/run.js +26 -81
- package/dist/commands/vouch/run.js.map +1 -1
- package/dist/commands/vouch/sample/plan.js +20 -34
- package/dist/commands/vouch/sample/plan.js.map +1 -1
- package/dist/commands/vouch/vet/verify.js +39 -7
- package/dist/commands/vouch/vet/verify.js.map +1 -1
- package/dist/commands/vouch/vouch.js +10 -17
- package/dist/commands/vouch/vouch.js.map +1 -1
- package/dist/core/agent/agents-md/command-map.js +56 -101
- package/dist/core/agent/agents-md/command-map.js.map +1 -1
- package/dist/core/agent/agents-md/map/explain.js +4 -3
- package/dist/core/agent/agents-md/map/explain.js.map +1 -1
- package/dist/core/agent/agents-md/map/lenses/context.js +4 -3
- package/dist/core/agent/agents-md/map/lenses/context.js.map +1 -1
- package/dist/core/agent/agents-md/map/lenses/diff.js +33 -20
- package/dist/core/agent/agents-md/map/lenses/diff.js.map +1 -1
- package/dist/core/agent/agents-md/map/lenses/gate.js +13 -5
- package/dist/core/agent/agents-md/map/lenses/gate.js.map +1 -1
- package/dist/core/agent/agents-md/map/mcp.js +10 -7
- package/dist/core/agent/agents-md/map/mcp.js.map +1 -1
- package/dist/core/agent/agents-md/map/status.js +9 -3
- package/dist/core/agent/agents-md/map/status.js.map +1 -1
- package/dist/core/agent/agents-md/map/subsystem.js +4 -3
- package/dist/core/agent/agents-md/map/subsystem.js.map +1 -1
- package/dist/core/agent/agents-md/map/usecases.js +24 -6
- package/dist/core/agent/agents-md/map/usecases.js.map +1 -1
- package/dist/core/agent/agents-md/refusals.js +32 -18
- package/dist/core/agent/agents-md/refusals.js.map +1 -1
- package/dist/core/agent/agents-md.js +9 -3
- package/dist/core/agent/agents-md.js.map +1 -1
- package/dist/core/agent/agents-stamp.js +10 -0
- package/dist/core/agent/agents-stamp.js.map +1 -1
- package/dist/core/agent/scaffold.js +10 -15
- package/dist/core/agent/scaffold.js.map +1 -1
- package/dist/core/agent/tools/dialects.js +1 -0
- package/dist/core/agent/tools/dialects.js.map +1 -1
- package/dist/core/agent/tools/registry.js +41 -76
- package/dist/core/agent/tools/registry.js.map +1 -1
- package/dist/core/agent/workflows/adopt.js +5 -3
- package/dist/core/agent/workflows/adopt.js.map +1 -1
- package/dist/core/agent/workflows/check.js +20 -16
- package/dist/core/agent/workflows/check.js.map +1 -1
- package/dist/core/agent/workflows/closing.js +54 -21
- package/dist/core/agent/workflows/closing.js.map +1 -1
- package/dist/core/agent/workflows/feature.js +79 -11
- package/dist/core/agent/workflows/feature.js.map +1 -1
- package/dist/core/agent/workflows/implement.js +91 -13
- package/dist/core/agent/workflows/implement.js.map +1 -1
- package/dist/core/agent/workflows/reference/authoring.js +3 -1
- package/dist/core/agent/workflows/reference/authoring.js.map +1 -1
- package/dist/core/agent/workflows/reference/codes.js +8 -6
- package/dist/core/agent/workflows/reference/codes.js.map +1 -1
- package/dist/core/agent/workflows/reference/done-check.js +37 -15
- package/dist/core/agent/workflows/reference/done-check.js.map +1 -1
- package/dist/core/agent/workflows/reference/reference.js.map +1 -1
- package/dist/core/agent/workflows/reference/spine.js +50 -18
- package/dist/core/agent/workflows/reference/spine.js.map +1 -1
- package/dist/core/asyncapi/baseline/plan.js +14 -81
- package/dist/core/asyncapi/baseline/plan.js.map +1 -1
- package/dist/core/asyncapi/digest.js +30 -25
- package/dist/core/asyncapi/digest.js.map +1 -1
- package/dist/core/asyncapi/merge/components.js +5 -4
- package/dist/core/asyncapi/merge/components.js.map +1 -1
- package/dist/core/asyncapi/merge/error.js +7 -2
- package/dist/core/asyncapi/merge/error.js.map +1 -1
- package/dist/core/asyncapi/merge/markers.js +30 -50
- package/dist/core/asyncapi/merge/markers.js.map +1 -1
- package/dist/core/asyncapi/merge/merge.js +31 -53
- package/dist/core/asyncapi/merge/merge.js.map +1 -1
- package/dist/core/asyncapi/merge/pin.js +10 -36
- package/dist/core/asyncapi/merge/pin.js.map +1 -1
- package/dist/core/asyncapi/model.js.map +1 -1
- package/dist/core/asyncapi/read.js +36 -12
- package/dist/core/asyncapi/read.js.map +1 -1
- package/dist/core/brief/brief.js +6 -18
- package/dist/core/brief/brief.js.map +1 -1
- package/dist/core/brief/landscape.js +8 -12
- package/dist/core/brief/landscape.js.map +1 -1
- package/dist/core/brief/map/extend.js +8 -5
- package/dist/core/brief/map/extend.js.map +1 -1
- package/dist/core/brief/map/owed.js +8 -26
- package/dist/core/brief/map/owed.js.map +1 -1
- package/dist/core/brief/targets.js +15 -5
- package/dist/core/brief/targets.js.map +1 -1
- package/dist/core/c4/likec4.js +147 -16
- package/dist/core/c4/likec4.js.map +1 -1
- package/dist/core/c4/locate/unlocated.js +150 -0
- package/dist/core/c4/locate/unlocated.js.map +1 -0
- package/dist/core/c4/parsed/values.js +54 -1
- package/dist/core/c4/parsed/values.js.map +1 -1
- package/dist/core/c4/prefetch/fleet.js +1 -9
- package/dist/core/c4/prefetch/fleet.js.map +1 -1
- package/dist/core/c4/project/documents.js +31 -6
- package/dist/core/c4/project/documents.js.map +1 -1
- package/dist/core/c4/project/load.js +6 -17
- package/dist/core/c4/project/load.js.map +1 -1
- package/dist/core/c4/resolve/external.js +51 -0
- package/dist/core/c4/resolve/external.js.map +1 -0
- package/dist/core/c4/resolve/service.js +25 -1
- package/dist/core/c4/resolve/service.js.map +1 -1
- package/dist/core/c4/seed/template.js +15 -1
- package/dist/core/c4/seed/template.js.map +1 -1
- package/dist/core/c4/service-model/shape.js +2 -22
- package/dist/core/c4/service-model/shape.js.map +1 -1
- package/dist/core/c4/source-mask.js +288 -4
- package/dist/core/c4/source-mask.js.map +1 -1
- package/dist/core/c4/source-scan.js +203 -147
- package/dist/core/c4/source-scan.js.map +1 -1
- package/dist/core/c4/splice/authored-source.js +19 -0
- package/dist/core/c4/splice/authored-source.js.map +1 -1
- package/dist/core/c4/splice/delta-blocks.js +5 -18
- package/dist/core/c4/splice/delta-blocks.js.map +1 -1
- package/dist/core/c4/splice/landscape-merge.js +13 -8
- package/dist/core/c4/splice/landscape-merge.js.map +1 -1
- package/dist/core/c4/splice/model/merge.js +10 -6
- package/dist/core/c4/splice/model/merge.js.map +1 -1
- package/dist/core/c4/splice/placement.js +108 -18
- package/dist/core/c4/splice/placement.js.map +1 -1
- package/dist/core/c4/workspace.js +13 -22
- package/dist/core/c4/workspace.js.map +1 -1
- package/dist/core/capabilities/capabilities.js +6 -6
- package/dist/core/capabilities/capabilities.js.map +1 -1
- package/dist/core/capabilities/delta/doc.js +7 -10
- package/dist/core/capabilities/delta/doc.js.map +1 -1
- package/dist/core/capabilities/findings.js +15 -20
- package/dist/core/capabilities/findings.js.map +1 -1
- package/dist/core/capabilities/realizes/findings.js +12 -19
- package/dist/core/capabilities/realizes/findings.js.map +1 -1
- package/dist/core/capabilities/realizes/join.js +0 -20
- package/dist/core/capabilities/realizes/join.js.map +1 -1
- package/dist/core/capabilities/rollup.js +6 -0
- package/dist/core/capabilities/rollup.js.map +1 -1
- package/dist/core/capabilities/tree.js +6 -28
- package/dist/core/capabilities/tree.js.map +1 -1
- package/dist/core/capabilities/usecase-join.js +3 -13
- package/dist/core/capabilities/usecase-join.js.map +1 -1
- package/dist/core/coherence/authoring/scaffold.js +11 -3
- package/dist/core/coherence/authoring/scaffold.js.map +1 -1
- package/dist/core/coherence/authoring/sentinels.js +2 -1
- package/dist/core/coherence/authoring/sentinels.js.map +1 -1
- package/dist/core/coherence/coherence.js +30 -16
- package/dist/core/coherence/coherence.js.map +1 -1
- package/dist/core/coherence/consumed/operations.js +70 -0
- package/dist/core/coherence/consumed/operations.js.map +1 -0
- package/dist/core/coherence/consumed/refusal.js +38 -0
- package/dist/core/coherence/consumed/refusal.js.map +1 -0
- package/dist/core/coherence/declared.js +60 -34
- package/dist/core/coherence/declared.js.map +1 -1
- package/dist/core/coherence/events/declared.js +44 -59
- package/dist/core/coherence/events/declared.js.map +1 -1
- package/dist/core/coherence/events/events.js +119 -8
- package/dist/core/coherence/events/events.js.map +1 -1
- package/dist/core/coherence/events/grades.js +31 -8
- package/dist/core/coherence/events/grades.js.map +1 -1
- package/dist/core/coherence/events/lookups.js +14 -82
- package/dist/core/coherence/events/lookups.js.map +1 -1
- package/dist/core/coherence/lookups.js +18 -78
- package/dist/core/coherence/lookups.js.map +1 -1
- package/dist/core/coherence/retired/archived.js +285 -0
- package/dist/core/coherence/retired/archived.js.map +1 -0
- package/dist/core/coherence/retired/event-pair.js +62 -0
- package/dist/core/coherence/retired/event-pair.js.map +1 -0
- package/dist/core/coherence/retired/merged.js +79 -0
- package/dist/core/coherence/retired/merged.js.map +1 -0
- package/dist/core/coherence/retired/retirement.js +49 -0
- package/dist/core/coherence/retired/retirement.js.map +1 -0
- package/dist/core/coherence/retired/revival/arrivals.js +131 -0
- package/dist/core/coherence/retired/revival/arrivals.js.map +1 -0
- package/dist/core/coherence/retired/revival/revived.js +297 -0
- package/dist/core/coherence/retired/revival/revived.js.map +1 -0
- package/dist/core/consumers/after.js +113 -0
- package/dist/core/consumers/after.js.map +1 -0
- package/dist/core/consumers/consumers.js +296 -0
- package/dist/core/consumers/consumers.js.map +1 -0
- package/dist/core/consumers/current.js +211 -0
- package/dist/core/consumers/current.js.map +1 -0
- package/dist/core/consumers/scan.js +46 -0
- package/dist/core/consumers/scan.js.map +1 -0
- package/dist/core/consumers/state.js +27 -0
- package/dist/core/consumers/state.js.map +1 -0
- package/dist/core/delta/delta.js +1 -1
- package/dist/core/delta/delta.js.map +1 -1
- package/dist/core/deployment/delta.js +3 -6
- package/dist/core/deployment/delta.js.map +1 -1
- package/dist/core/diff/base-git.js +94 -12
- package/dist/core/diff/base-git.js.map +1 -1
- package/dist/core/diff/base-state.js +28 -16
- package/dist/core/diff/base-state.js.map +1 -1
- package/dist/core/diff/joins.js +136 -0
- package/dist/core/diff/joins.js.map +1 -0
- package/dist/core/diff/scope/changed-paths.js +22 -6
- package/dist/core/diff/scope/changed-paths.js.map +1 -1
- package/dist/core/diff/scope/edges.js +82 -0
- package/dist/core/diff/scope/edges.js.map +1 -0
- package/dist/core/diff/semantic.js +100 -31
- package/dist/core/diff/semantic.js.map +1 -1
- package/dist/core/doctor/agents.js +34 -18
- package/dist/core/doctor/agents.js.map +1 -1
- package/dist/core/doctor/report.js.map +1 -1
- package/dist/core/document/apply.js +10 -2
- package/dist/core/document/apply.js.map +1 -1
- package/dist/core/document/frontmatter.js +55 -0
- package/dist/core/document/frontmatter.js.map +1 -1
- package/dist/core/document/parse.js +29 -6
- package/dist/core/document/parse.js.map +1 -1
- package/dist/core/document/scenarios.js +17 -0
- package/dist/core/document/scenarios.js.map +1 -1
- package/dist/core/document/spec.js +4 -11
- package/dist/core/document/spec.js.map +1 -1
- package/dist/core/envelope/config.js +10 -0
- package/dist/core/envelope/config.js.map +1 -1
- package/dist/core/envelope/json.js +34 -0
- package/dist/core/envelope/json.js.map +1 -1
- package/dist/core/explain/families.js +17 -12
- package/dist/core/explain/families.js.map +1 -1
- package/dist/core/explain/refusals.js +3 -3
- package/dist/core/explain/refusals.js.map +1 -1
- package/dist/core/explain/terms.js +18 -9
- package/dist/core/explain/terms.js.map +1 -1
- package/dist/core/explore/explore.js +2 -7
- package/dist/core/explore/explore.js.map +1 -1
- package/dist/core/fleet-context.js +34 -95
- package/dist/core/fleet-context.js.map +1 -1
- package/dist/core/gate/checks.js +5 -2
- package/dist/core/gate/checks.js.map +1 -1
- package/dist/core/gate/partners.js +4 -2
- package/dist/core/gate/partners.js.map +1 -1
- package/dist/core/gate/report.js.map +1 -1
- package/dist/core/gate/verification.js +75 -12
- package/dist/core/gate/verification.js.map +1 -1
- package/dist/core/gherkin/stale.js +13 -37
- package/dist/core/gherkin/stale.js.map +1 -1
- package/dist/core/glossary/tree.js +6 -19
- package/dist/core/glossary/tree.js.map +1 -1
- package/dist/core/kernel/concurrency.js +19 -0
- package/dist/core/kernel/concurrency.js.map +1 -1
- package/dist/core/kernel/records.js +11 -0
- package/dist/core/kernel/records.js.map +1 -1
- package/dist/core/links/backlinks.js +1 -3
- package/dist/core/links/backlinks.js.map +1 -1
- package/dist/core/links/corpus.js +5 -36
- package/dist/core/links/corpus.js.map +1 -1
- package/dist/core/mcp/argv.js +101 -0
- package/dist/core/mcp/argv.js.map +1 -1
- package/dist/core/mcp/author-tools.js +2 -1
- package/dist/core/mcp/author-tools.js.map +1 -1
- package/dist/core/mcp/protocol.js +2 -2
- package/dist/core/mcp/protocol.js.map +1 -1
- package/dist/core/mcp/tools.js +43 -33
- package/dist/core/mcp/tools.js.map +1 -1
- package/dist/core/openapi/baseline/gate.js +53 -12
- package/dist/core/openapi/baseline/gate.js.map +1 -1
- package/dist/core/openapi/baseline/plan.js +22 -8
- package/dist/core/openapi/baseline/plan.js.map +1 -1
- package/dist/core/openapi/baseline/record.js +0 -1
- package/dist/core/openapi/baseline/record.js.map +1 -1
- package/dist/core/openapi/contract-text.js +33 -0
- package/dist/core/openapi/contract-text.js.map +1 -0
- package/dist/core/openapi/contract-tree.js +56 -0
- package/dist/core/openapi/contract-tree.js.map +1 -0
- package/dist/core/openapi/depth.js +38 -24
- package/dist/core/openapi/depth.js.map +1 -1
- package/dist/core/openapi/digest.js +52 -2
- package/dist/core/openapi/digest.js.map +1 -1
- package/dist/core/openapi/doc.js +12 -4
- package/dist/core/openapi/doc.js.map +1 -1
- package/dist/core/openapi/merge/components.js +25 -60
- package/dist/core/openapi/merge/components.js.map +1 -1
- package/dist/core/openapi/merge/error.js +3 -13
- package/dist/core/openapi/merge/error.js.map +1 -1
- package/dist/core/openapi/merge/markers.js +26 -30
- package/dist/core/openapi/merge/markers.js.map +1 -1
- package/dist/core/openapi/merge/merge.js +26 -52
- package/dist/core/openapi/merge/merge.js.map +1 -1
- package/dist/core/openapi/merge/pin.js +15 -35
- package/dist/core/openapi/merge/pin.js.map +1 -1
- package/dist/core/openspec/decide/verdict.js.map +1 -1
- package/dist/core/openspec/inventory.js +4 -5
- package/dist/core/openspec/inventory.js.map +1 -1
- package/dist/core/openspec/model/mapping.js +30 -28
- package/dist/core/openspec/model/mapping.js.map +1 -1
- package/dist/core/openspec/model/model.js +12 -7
- package/dist/core/openspec/model/model.js.map +1 -1
- package/dist/core/openspec/scan/changes.js +10 -4
- package/dist/core/openspec/scan/changes.js.map +1 -1
- package/dist/core/openspec/scan/config.js +7 -16
- package/dist/core/openspec/scan/config.js.map +1 -1
- package/dist/core/openspec/scan/shape.js +63 -19
- package/dist/core/openspec/scan/shape.js.map +1 -1
- package/dist/core/openspec/scan/walk.js +21 -38
- package/dist/core/openspec/scan/walk.js.map +1 -1
- package/dist/core/pack/living.js +2 -16
- package/dist/core/pack/living.js.map +1 -1
- package/dist/core/provenance/findings.js +48 -12
- package/dist/core/provenance/findings.js.map +1 -1
- package/dist/core/provenance/git.js +33 -57
- package/dist/core/provenance/git.js.map +1 -1
- package/dist/core/provenance/gitq/moves.js +71 -114
- package/dist/core/provenance/gitq/moves.js.map +1 -1
- package/dist/core/provenance/gitq/vouched-ancestor.js +2 -2
- package/dist/core/provenance/gitq/vouched-ancestor.js.map +1 -1
- package/dist/core/provenance/sources.js +23 -6
- package/dist/core/provenance/sources.js.map +1 -1
- package/dist/core/provenance/stamp.js +5 -17
- package/dist/core/provenance/stamp.js.map +1 -1
- package/dist/core/repo/entries.js.map +1 -1
- package/dist/core/repo/repo.js +24 -14
- package/dist/core/repo/repo.js.map +1 -1
- package/dist/core/repo/tree/find.js +12 -3
- package/dist/core/repo/tree/find.js.map +1 -1
- package/dist/core/repo/tree/fs.js +27 -1
- package/dist/core/repo/tree/fs.js.map +1 -1
- package/dist/core/repo/tree/walk.js +8 -30
- package/dist/core/repo/tree/walk.js.map +1 -1
- package/dist/core/results.js +47 -12
- package/dist/core/results.js.map +1 -1
- package/dist/core/staging/commit.js +10 -12
- package/dist/core/staging/commit.js.map +1 -1
- package/dist/core/staging/lock.js +74 -12
- package/dist/core/staging/lock.js.map +1 -1
- package/dist/core/staging/recovery/intent.js +2 -5
- package/dist/core/staging/recovery/intent.js.map +1 -1
- package/dist/core/staging/recovery/recover.js +60 -1
- package/dist/core/staging/recovery/recover.js.map +1 -1
- package/dist/core/staging/snapshot/manifest.js +152 -0
- package/dist/core/staging/snapshot/manifest.js.map +1 -0
- package/dist/core/staging/{snapshot.js → snapshot/snapshot.js} +10 -34
- package/dist/core/staging/snapshot/snapshot.js.map +1 -0
- package/dist/core/status/actions/execution.js +11 -0
- package/dist/core/status/actions/execution.js.map +1 -1
- package/dist/core/status/feature/artifacts.js +2 -2
- package/dist/core/status/feature/artifacts.js.map +1 -1
- package/dist/core/status/feature/feature.js +5 -11
- package/dist/core/status/feature/feature.js.map +1 -1
- package/dist/core/status/fleet/fleet.js +6 -2
- package/dist/core/status/fleet/fleet.js.map +1 -1
- package/dist/core/status/fleet/next.js +26 -3
- package/dist/core/status/fleet/next.js.map +1 -1
- package/dist/core/status/report.js.map +1 -1
- package/dist/core/status/verification.js +30 -15
- package/dist/core/status/verification.js.map +1 -1
- package/dist/core/tasks/packet.js +174 -0
- package/dist/core/tasks/packet.js.map +1 -0
- package/dist/core/tasks/record.js +43 -0
- package/dist/core/tasks/record.js.map +1 -0
- package/dist/core/tasks/sources.js +181 -0
- package/dist/core/tasks/sources.js.map +1 -0
- package/dist/core/tasks/stamps.js +76 -0
- package/dist/core/tasks/stamps.js.map +1 -0
- package/dist/core/tasks/unit.js +0 -0
- package/dist/core/tasks/unit.js.map +1 -0
- package/dist/core/usecases/capability.js +11 -14
- package/dist/core/usecases/capability.js.map +1 -1
- package/dist/core/usecases/delta/flows.js +5 -7
- package/dist/core/usecases/delta/flows.js.map +1 -1
- package/dist/core/usecases/fleet.js +178 -31
- package/dist/core/usecases/fleet.js.map +1 -1
- package/dist/core/usecases/operations.js +5 -4
- package/dist/core/usecases/operations.js.map +1 -1
- package/dist/core/usecases/service/flows.js +9 -6
- package/dist/core/usecases/service/flows.js.map +1 -1
- package/dist/core/usecases/suites/participants.js +83 -0
- package/dist/core/usecases/suites/participants.js.map +1 -0
- package/dist/core/usecases/suites/suites.js +45 -0
- package/dist/core/usecases/suites/suites.js.map +1 -0
- package/dist/core/verify/answers.js +12 -5
- package/dist/core/verify/answers.js.map +1 -1
- package/dist/core/verify/build.js +102 -82
- package/dist/core/verify/build.js.map +1 -1
- package/dist/core/verify/checklist.js +3 -0
- package/dist/core/verify/checklist.js.map +1 -1
- package/dist/core/verify/location.js +67 -0
- package/dist/core/verify/location.js.map +1 -0
- package/dist/core/verify/pins/lint.js +3 -2
- package/dist/core/verify/pins/lint.js.map +1 -1
- package/dist/core/verify/pins/pin.js +1 -1
- package/dist/core/verify/pins/pin.js.map +1 -1
- package/dist/core/verify/read/file.js +224 -0
- package/dist/core/verify/read/file.js.map +1 -0
- package/dist/core/verify/{file.js → read/shape.js} +16 -104
- package/dist/core/verify/read/shape.js.map +1 -0
- package/dist/core/verify/read/union.js +105 -0
- package/dist/core/verify/read/union.js.map +1 -0
- package/dist/core/verify/record.js +35 -12
- package/dist/core/verify/record.js.map +1 -1
- package/dist/core/verify/store/commit.js +40 -30
- package/dist/core/verify/store/commit.js.map +1 -1
- package/dist/core/verify/store/render.js +8 -4
- package/dist/core/verify/store/render.js.map +1 -1
- package/dist/core/vocabulary/issue.js +1 -0
- package/dist/core/vocabulary/issue.js.map +1 -1
- package/dist/core/vocabulary/maturity.js +12 -2
- package/dist/core/vocabulary/maturity.js.map +1 -1
- package/examples/README.md +21 -6
- package/examples/docs/architecture/landscape.likec4 +8 -0
- package/examples/docs/architecture/usecases/order-notification.likec4 +6 -2
- package/examples/docs/features/FEAT-101-payment-splitting/specs/payment-service/asyncapi.yaml +11 -8
- package/examples/docs/features/FEAT-101-payment-splitting/specs/payment-split-service/openapi.yaml +6 -6
- package/examples/docs/features/archive/FEAT-088-refunds/specs/payment-service/openapi.yaml +13 -10
- package/examples/docs/services/order-service/asyncapi.yaml +3 -2
- package/examples/docs/services/platform/notification-service/asyncapi.yaml +2 -1
- package/package.json +1 -1
- package/dist/commands/validate/checks/vocabulary.js +0 -16
- package/dist/commands/validate/checks/vocabulary.js.map +0 -1
- package/dist/core/agent/agents-md/artifacts.js +0 -221
- package/dist/core/agent/agents-md/artifacts.js.map +0 -1
- package/dist/core/agent/agents-md/cycle.js +0 -180
- package/dist/core/agent/agents-md/cycle.js.map +0 -1
- package/dist/core/agent/agents-md/shipped/archive-gate.js +0 -98
- package/dist/core/agent/agents-md/shipped/archive-gate.js.map +0 -1
- package/dist/core/agent/agents-md/spine.js +0 -40
- package/dist/core/agent/agents-md/spine.js.map +0 -1
- package/dist/core/coherence/events/merged.js +0 -40
- package/dist/core/coherence/events/merged.js.map +0 -1
- package/dist/core/diff/current-state.js +0 -96
- package/dist/core/diff/current-state.js.map +0 -1
- package/dist/core/diff/victims.js +0 -256
- package/dist/core/diff/victims.js.map +0 -1
- package/dist/core/staging/snapshot.js.map +0 -1
- package/dist/core/verify/file.js.map +0 -1
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"report.js","sourceRoot":"","sources":["../../../src/core/gate/report.ts"],"names":[],"mappings":"AAeA,8FAA8F;AAC9F,MAAM,CAAC,MAAM,WAAW,GAAG,CAAC,UAAU,EAAE,WAAW,EAAE,cAAc,EAAE,aAAa,CAAU,CAAC;
|
|
1
|
+
{"version":3,"file":"report.js","sourceRoot":"","sources":["../../../src/core/gate/report.ts"],"names":[],"mappings":"AAeA,8FAA8F;AAC9F,MAAM,CAAC,MAAM,WAAW,GAAG,CAAC,UAAU,EAAE,WAAW,EAAE,cAAc,EAAE,aAAa,CAAU,CAAC;AAoG7F;;;;;;GAMG;AACH,MAAM,UAAU,WAAW,CAAC,MAA4B;IACtD,OAAO,MAAM,CAAC,IAAI,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,QAAQ,CAAC,IAAI,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,QAAQ,KAAK,OAAO,CAAC,CAAC,CAAC,CAAC,CAAC,MAAM,CAAC,CAAC,CAAC,MAAM,CAAC;AAC9F,CAAC;AAED,MAAM,UAAU,WAAW,CAAC,MAA4B;IACtD,MAAM,EAAE,GAAG,CAAC,QAA0B,EAAU,EAAE,CAChD,MAAM,CAAC,MAAM,CAAC,CAAC,CAAC,EAAE,CAAC,EAAE,EAAE,CAAC,CAAC,GAAG,CAAC,CAAC,QAAQ,CAAC,MAAM,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,QAAQ,KAAK,QAAQ,CAAC,CAAC,MAAM,EAAE,CAAC,CAAC,CAAC;IAC3F,OAAO,EAAE,MAAM,EAAE,EAAE,CAAC,OAAO,CAAC,EAAE,QAAQ,EAAE,EAAE,CAAC,MAAM,CAAC,EAAE,CAAC;AACvD,CAAC;AAED;;;;;;;;;;GAUG;AACH,MAAM,UAAU,iBAAiB,CAAC,IAA2B,EAAE,EAAU,EAAE,GAAY;IACrF,MAAM,CAAC,GAAG,GAA4B,CAAC;IACvC,IAAI,CAAC,CAAC,IAAI,KAAK,SAAS,IAAI,OAAO,CAAC,CAAC,KAAK,KAAK,QAAQ;QAAE,MAAM,GAAG,CAAC;IACnE,MAAM,MAAM,GAAG,GAAG,YAAY,KAAK,CAAC,CAAC,CAAC,GAAG,CAAC,OAAO,CAAC,CAAC,CAAC,MAAM,CAAC,GAAG,CAAC,CAAC;IAChE,MAAM,WAAW,GACf,IAAI,KAAK,SAAS,CAAC,CAAC,CAAC,0BAA0B,CAAC,CAAC,CAAC,mCAAmC,CAAC;IACxF,OAAO;QACL,QAAQ,EAAE,OAAO;QACjB,IAAI,EAAE,IAAI,KAAK,SAAS,CAAC,CAAC,CAAC,oBAAoB,CAAC,CAAC,CAAC,oBAAoB;QACtE,OAAO,EAAE,EAAE;QACX,OAAO,EACL,GAAG,EAAE,KAAK,CAAC,CAAC,IAAI,IAAI,sBAAsB,0BAA0B,WAAW,uEAAuE,MAAM,EAAE;YAC9J,sEAAsE;YACtE,sEAAsE;YACtE,wDAAwD;YACxD,CAAC,CAAC,CAAC,IAAI,KAAK,SAAS;gBACnB,CAAC,CAAC,iGAAiG;gBACnG,CAAC,CAAC,EAAE,CAAC;KACV,CAAC;AACJ,CAAC","sourcesContent":["/**\n * The gate report — what `loam gate` computes before anything is printed.\n *\n * Shapes, verdict arithmetic and the per-subject containment classifier: no\n * filesystem, no process access, no printing. The four checks build findings\n * (`./checks.ts`, `./verification.ts`), the command layer renders them, and\n * this module is what the two agree through — the same split\n * `core/vocabulary/report.ts` gives `loam validate`, whose `Finding` shape\n * the checks reuse so one breach arrives in one shape whichever command\n * reports it.\n */\nimport type { Finding } from \"../vocabulary/report.js\";\nimport type { Maturity } from \"../vocabulary/maturity.js\";\nimport type { VerificationState } from \"../status/report.js\";\n\n/** The four checks, in report order. A closed set — the payload's `checks[].check` values. */\nexport const GATE_CHECKS = [\"partners\", \"freshness\", \"verification\", \"interrupted\"] as const;\nexport type GateCheckName = (typeof GATE_CHECKS)[number];\n\nexport interface GateCheck {\n check: GateCheckName;\n findings: Finding[];\n}\n\n/**\n * How the landscape read came out. `absent` and `invalid` are kept apart from\n * `read` because the partner set derived under either is NOT \"no partners\" —\n * it is \"could not look\", and `gate.partners-unknown` exists to keep a\n * pipeline from reading the empty list as a clean one.\n */\nexport type LandscapeRead = \"read\" | \"absent\" | \"invalid\";\n\n/**\n * What the partner is to the gated service: a `consumer` calls into it, a\n * `provider` is called by it, `both` when the landscape draws edges in both\n * directions.\n */\nexport type PartnerRole = \"consumer\" | \"provider\" | \"both\";\n\nexport interface GatePartner {\n /** The partner's resolved service name — a directory id when one exists, the drawn name otherwise. */\n service: string;\n /**\n * The partner's rung on the adoption ladder, or null when no `services/`\n * directory answers to the name at all — the state `gate.partner-undocumented`\n * names. Null is deliberately not a fifth rung: `empty` means \"exists,\n * nothing in it\", and a partner nobody adopted is a different fact.\n */\n maturity: Maturity | null;\n role: PartnerRole;\n /** The joins, sorted: `operation <op>`, `message <name>`, or the untyped edge's title. */\n via: string[];\n /** The element is tagged `#external` — somebody else's system, undocumented on purpose. */\n external: boolean;\n}\n\n/**\n * One feature touching the gated service — every active one, plus, under\n * `--since`, every one archived after the ref — with the verification\n * record's own projection beside it. Every field but `id` and `archived` is\n * `verificationState`'s (`core/status/verification.ts`) — never a second\n * derivation of \"verified\".\n */\nexport interface GateFeature extends VerificationState {\n id: string;\n /**\n * Present, and true, only on a row `--since` added: the feature shipped\n * after the ref, so its record is frozen history. Absent rather than\n * `false` on the in-flight rows, so a payload without `--since` is byte for\n * byte the one it was before the flag existed.\n */\n archived?: true;\n /**\n * Present only beside `archived`: the checklist digest `loam archive`\n * captured as the feature merged — what its frozen record was graded\n * against, so a record that had stopped answering the feature before it\n * shipped reads `stale` — or null when that archive captured none (a loam\n * that predates the capture, or a feature that had no record then). Under\n * null a record is read as frozen history, exactly as before the capture\n * existed, and the row says so rather than letting that reading pass for a\n * checked one.\n */\n checklistAtArchive?: string | null;\n}\n\n/**\n * `--since <ref>`, resolved: the docs-repo commit a deploy pipeline last\n * shipped from, and the `features/archive/` directory names that already\n * existed there AND still hold what they held then. Without it the\n * verification check reads in-flight features only, and a feature drops out of\n * the gate the moment it is archived — which under `--strict` made archiving\n * the cheapest way past an unverified-feature warning. With it, every feature\n * archived AFTER the ref is graded too: its directory absent there, or changed\n * since — a feature unarchived, edited and archived again ships again, at the\n * same path. Nothing is stored: the pipeline already knows its last deployed\n * docs commit, and git already knows what was archived at it.\n */\nexport interface GateSince {\n ref: string;\n commit: string;\n /** Archive directory names an earlier deploy already shipped exactly as they stand — the ones this gate leaves out. */\n shippedAtRef: ReadonlySet<string>;\n}\n\nexport interface GateReport {\n service: string;\n /** The `--since` bound the report was computed under, or null for in-flight features only. */\n since: { ref: string; commit: string } | null;\n landscape: LandscapeRead;\n partners: GatePartner[];\n features: GateFeature[];\n checks: GateCheck[];\n}\n\nexport type GateVerdict = \"pass\" | \"fail\";\n\n/**\n * Fail iff any check carries an error-severity finding. `--strict` never moves\n * this — like validate's `valid`, the verdict means the same thing in text and\n * JSON whatever the exit code does, so two pipelines reading one repo can\n * grade the same report differently and both be telling the truth\n * (commands/validate/validate.ts documents the stance; gate copies it).\n */\nexport function gateVerdict(checks: readonly GateCheck[]): GateVerdict {\n return checks.some((c) => c.findings.some((f) => f.severity === \"error\")) ? \"fail\" : \"pass\";\n}\n\nexport function gateSummary(checks: readonly GateCheck[]): { errors: number; warnings: number } {\n const of = (severity: \"error\" | \"warn\"): number =>\n checks.reduce((n, c) => n + c.findings.filter((f) => f.severity === severity).length, 0);\n return { errors: of(\"error\"), warnings: of(\"warn\") };\n}\n\n/**\n * Per-subject containment: one unreadable sibling degrades ONE subject's\n * answer, with its path recorded, instead of killing the report the other\n * subjects still have. The code is validate's own containment code\n * (`guarded`, commands/validate/report.ts), reused so one breach is spelled\n * one way — and it is an ERROR: an unanswerable check fails closed, because\n * \"nobody could look\" must be distinguishable from \"nothing is wrong\" in the\n * exit code as well as the payload. Anything carrying no errno and no path is\n * a real bug, and still escapes untouched (reportRepositoryUnavailable's\n * recognizer).\n */\nexport function unreadableSubject(kind: \"service\" | \"feature\", id: string, err: unknown): Finding {\n const e = err as NodeJS.ErrnoException;\n if (e.path === undefined && typeof e.errno !== \"number\") throw err;\n const reason = err instanceof Error ? err.message : String(err);\n const consequence =\n kind === \"service\" ? \"its freshness is unknown\" : \"its verification state is unknown\";\n return {\n severity: \"error\",\n code: kind === \"service\" ? \"service.unreadable\" : \"feature.unreadable\",\n subject: id,\n message:\n `${id}: ${e.path ?? \"one of its documents\"} could not be read, so ${consequence} — nobody could look, which is not the same as nothing being wrong. ${reason}` +\n // The same sentence reportRepositoryUnavailable carries, for the same\n // reason: Node reports EISDIR from read() with no path at all, so the\n // commonest malformed-docs shape arrives here nameless.\n (e.path === undefined\n ? \" The failure named no path — that is how Node reports a directory sitting where a file belongs.\"\n : \"\"),\n };\n}\n"]}
|
|
@@ -6,25 +6,52 @@
|
|
|
6
6
|
* limit; this is the one check with a per-feature phase of its own, so it is
|
|
7
7
|
* the seam.
|
|
8
8
|
*
|
|
9
|
+
* Without `--since` it reads IN-FLIGHT features only, so a feature leaves the
|
|
10
|
+
* check the moment it is archived — and archive reads no verification record,
|
|
11
|
+
* deliberately (verify never gates the merge). Under `--strict` that made
|
|
12
|
+
* archiving the cheapest way past this check's warning, on a draft service
|
|
13
|
+
* and on a vouched one alike when the feature changed only a contract
|
|
14
|
+
* (`content_digest` covers the spec bodies, not openapi.yaml/asyncapi.yaml, so
|
|
15
|
+
* freshness cannot catch it either). `--since <ref>` closes it without any
|
|
16
|
+
* deployment state: every feature archived after the docs commit the pipeline last
|
|
17
|
+
* deployed from is graded too, by the same `verificationState`, under the
|
|
18
|
+
* same code, its row marked `archived: true` — "archived after" meaning its
|
|
19
|
+
* archive directory was absent at the ref OR differs from what it held there,
|
|
20
|
+
* since a feature unarchived, edited and archived again ships again from the
|
|
21
|
+
* same path. Its record is frozen, but not ungraded: it is held to the
|
|
22
|
+
* checklist `loam archive` captured as the feature merged
|
|
23
|
+
* (`readSnapshotChecklist`), so a record that had stopped answering the
|
|
24
|
+
* feature before it shipped is `stale` — archiving it no longer reads it
|
|
25
|
+
* `verified` again. An archive that captured none (an older loam) is read as
|
|
26
|
+
* frozen history, as before, and the row says so.
|
|
27
|
+
*
|
|
9
28
|
* Report, never fail: every finding here is a warning, by the item's own
|
|
10
29
|
* design — a deploy gate that hard-failed on unverified work would teach
|
|
11
30
|
* everyone that the cheapest way past it is an empty record.
|
|
12
31
|
*/
|
|
13
32
|
import { inOrder } from "../kernel/concurrency.js";
|
|
14
33
|
import { verificationState } from "../status/verification.js";
|
|
34
|
+
import { readSnapshotChecklist } from "../staging/snapshot/manifest.js";
|
|
15
35
|
import { listFeatures } from "../repo/repo.js";
|
|
16
36
|
import { featurePaths } from "../repo/paths.js";
|
|
17
37
|
import { compareIds } from "../repo/entries.js";
|
|
18
38
|
import { unreadableSubject } from "./report.js";
|
|
19
39
|
export async function verificationCheck(req) {
|
|
20
|
-
const
|
|
40
|
+
const { since } = req;
|
|
41
|
+
const all = await listFeatures(req.docsDir, { includeArchived: since !== undefined }, req.fleet);
|
|
21
42
|
// "Touching" = carries a specs/<svc>/ delta — the same definition
|
|
22
43
|
// `status --service` answers with. A feature whose C4 delta merely draws an
|
|
23
44
|
// edge into the service is deliberately out: counting it needs a delta parse
|
|
24
|
-
// per active feature, and it can be added additively if a fleet asks.
|
|
25
|
-
|
|
45
|
+
// per active feature, and it can be added additively if a fleet asks. An
|
|
46
|
+
// archived feature keeps its specs/ under features/archive/, so the one
|
|
47
|
+
// definition answers for both sides. "Archived after the ref" is "its
|
|
48
|
+
// archive directory was not there at the ref, or is not what it was there";
|
|
49
|
+
// a feature whose directory was there UNCHANGED shipped in an earlier
|
|
50
|
+
// deploy and is not this one's to answer for (`GateSince`).
|
|
51
|
+
const touching = all
|
|
26
52
|
.filter((f) => f.services.includes(req.service))
|
|
27
|
-
.
|
|
53
|
+
.filter((f) => !f.archived || (since !== undefined && !since.shippedAtRef.has(f.dirName)))
|
|
54
|
+
.sort((a, b) => compareIds(a.id, b.id) || compareIds(a.dirName, b.dirName));
|
|
28
55
|
// COST ACCEPTED: each touching feature with a record derives its checklist,
|
|
29
56
|
// which parses its delta. That is the per-feature cost
|
|
30
57
|
// core/status/fleet/fleet.ts refuses for the WHOLE fleet; a deploy-time
|
|
@@ -34,12 +61,20 @@ export async function verificationCheck(req) {
|
|
|
34
61
|
// run, not one per feature) and the batch prefetch below genuinely seeds
|
|
35
62
|
// the delta parses. The prefetch is an accelerator only: a host that cannot
|
|
36
63
|
// batch degrades to per-feature parses with identical findings.
|
|
37
|
-
|
|
64
|
+
// An archived feature never derives a checklist (its record is frozen, see
|
|
65
|
+
// `verificationState`, and held to the one its archive captured), so
|
|
66
|
+
// prefetching its delta would parse for nobody.
|
|
67
|
+
await req.fleet.prefetchLikeC4(touching.filter((f) => !f.archived && f.has.delta).map((f) => featurePaths(f.dir).delta));
|
|
38
68
|
// inOrder caps the workspaces in flight and keeps the rows in feature order.
|
|
39
69
|
const rows = await inOrder(touching, async (f) => {
|
|
40
70
|
try {
|
|
41
|
-
|
|
42
|
-
|
|
71
|
+
if (!f.archived) {
|
|
72
|
+
const { state, notice } = await verificationState(req.docsDir, f, { fleet: req.fleet });
|
|
73
|
+
return { kind: "read", feature: f, state, notice };
|
|
74
|
+
}
|
|
75
|
+
const shipped = await readSnapshotChecklist(f.dir);
|
|
76
|
+
const { state, notice } = await verificationState(req.docsDir, f, { fleet: req.fleet, shipped });
|
|
77
|
+
return { kind: "read", feature: f, state, notice, shipped };
|
|
43
78
|
}
|
|
44
79
|
catch (err) {
|
|
45
80
|
return { kind: "unreadable", finding: unreadableSubject("feature", f.id, err) };
|
|
@@ -52,12 +87,16 @@ export async function verificationCheck(req) {
|
|
|
52
87
|
findings.push(row.finding);
|
|
53
88
|
continue;
|
|
54
89
|
}
|
|
55
|
-
features.push({
|
|
56
|
-
|
|
90
|
+
features.push({
|
|
91
|
+
id: row.feature.id,
|
|
92
|
+
...row.state,
|
|
93
|
+
...(row.shipped === undefined ? {} : { archived: true, checklistAtArchive: row.shipped }),
|
|
94
|
+
});
|
|
95
|
+
findings.push(...featureFindings(row, req.service, since?.ref));
|
|
57
96
|
}
|
|
58
97
|
return { features, findings };
|
|
59
98
|
}
|
|
60
|
-
function featureFindings(row, service) {
|
|
99
|
+
function featureFindings(row, service, since) {
|
|
61
100
|
const { feature, state, notice } = row;
|
|
62
101
|
if (state.verdict === "verified")
|
|
63
102
|
return [];
|
|
@@ -80,18 +119,42 @@ function featureFindings(row, service) {
|
|
|
80
119
|
}
|
|
81
120
|
const verify = `\`loam verify ${feature.id} --json\``;
|
|
82
121
|
const open = state.unconfirmed + state.unanswered;
|
|
122
|
+
const claimsOpen = `${open} of ${state.claims} claim(s) on ${feature.id} are open (${state.unconfirmed} unconfirmed, ${state.unanswered} unanswered)`;
|
|
83
123
|
const message = () => {
|
|
124
|
+
// A row `--since` added. Its record is frozen history: `loam verify` reads
|
|
125
|
+
// it back and refuses to re-record it (commands/verify/verify.ts), so the
|
|
126
|
+
// in-flight remedies below — derive, answer, re-derive — would send a
|
|
127
|
+
// deploy pipeline to a command that cannot run. What it CAN be told is
|
|
128
|
+
// that this deploy carries the feature unchecked, and the one way to
|
|
129
|
+
// reopen it. An archived record is `stale` only against the checklist its
|
|
130
|
+
// archive captured — the feature had moved under it before it shipped.
|
|
131
|
+
if (feature.archived) {
|
|
132
|
+
const shipped = `${feature.id} touches '${service}' and was archived after ${since ?? "the --since ref"}`;
|
|
133
|
+
const reopen = `Its record is frozen history: ${verify} reads it back, and \`loam unarchive ${feature.id}\` re-opens the feature if its claims must be answered before this deploy.`;
|
|
134
|
+
if (state.state === "absent")
|
|
135
|
+
return `${shipped} with no verification record — nothing was checked before it shipped. ${reopen}`;
|
|
136
|
+
// The file that failed may be one service's `verification/<svc>.yaml`
|
|
137
|
+
// as well as the one-file record; `loam verify` names which.
|
|
138
|
+
if (state.state === "unreadable")
|
|
139
|
+
return `${shipped} with a verification record that does not read as a record — ${verify} names the file that failed. ${reopen}`;
|
|
140
|
+
if (state.state === "stale") {
|
|
141
|
+
return `${shipped}, and it had moved after its record was written — the record answers a checklist that was no longer the one it asked when it was archived, so what shipped was not what was checked. ${reopen}`;
|
|
142
|
+
}
|
|
143
|
+
return state.claims === 0
|
|
144
|
+
? `${shipped} with a record that answers no claims at all. ${reopen}`
|
|
145
|
+
: `${shipped}, and ${claimsOpen}. ${reopen}`;
|
|
146
|
+
}
|
|
84
147
|
switch (state.state) {
|
|
85
148
|
case "absent":
|
|
86
149
|
return `${feature.id} touches '${service}' and has no verification record — nothing has been checked. Derive the checklist and answer it: ${verify}.`;
|
|
87
150
|
case "unreadable":
|
|
88
|
-
return `${feature.id}'s verification
|
|
151
|
+
return `${feature.id}'s verification record does not read as a record — ${verify} names the file that failed and its safe repair (never delete a file other services' attestations may be in); nothing will overwrite it while it is unreadable.`;
|
|
89
152
|
case "stale":
|
|
90
153
|
return `${feature.id} moved after its record was written — the answers answer a checklist that is no longer the one being asked. Re-derive and re-answer: ${verify}.`;
|
|
91
154
|
case "recorded":
|
|
92
155
|
return state.claims === 0
|
|
93
156
|
? `${feature.id}'s record answers no claims at all — nothing was asked, so nothing was checked. ${verify} re-derives the checklist.`
|
|
94
|
-
: `${
|
|
157
|
+
: `${claimsOpen} — close them with evidence: ${verify}.`;
|
|
95
158
|
default: {
|
|
96
159
|
// A fifth record state must land here as a build failure, not fall
|
|
97
160
|
// through into some rung nobody decided (verificationStatus holds the
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"verification.js","sourceRoot":"","sources":["../../../src/core/gate/verification.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * Check 3 of `loam gate`: the verification records of every active feature\n * touching the gated service, read through `verificationState`'s own graded\n * verdicts — never a re-derivation of \"verified\" (that module's header is the\n * contract). Split from `./checks.ts` when the four checks outgrew the line\n * limit; this is the one check with a per-feature phase of its own, so it is\n * the seam.\n *\n * Report, never fail: every finding here is a warning, by the item's own\n * design — a deploy gate that hard-failed on unverified work would teach\n * everyone that the cheapest way past it is an empty record.\n */\nimport { inOrder } from \"../kernel/concurrency.js\";\nimport { verificationState } from \"../status/verification.js\";\nimport { listFeatures } from \"../repo/repo.js\";\nimport { featurePaths } from \"../repo/paths.js\";\nimport { compareIds, type FeatureEntry } from \"../repo/entries.js\";\nimport type { Finding } from \"../vocabulary/report.js\";\nimport type { VerificationState } from \"../status/report.js\";\nimport type { VerifyNotice } from \"../verify/record.js\";\nimport type { FleetContext } from \"../fleet-context.js\";\nimport type { DocsDir } from \"../kernel/ids/dirs.js\";\nimport type { RawServiceId } from \"../kernel/ids/service.js\";\nimport { unreadableSubject, type GateFeature } from \"./report.js\";\n\ntype VerificationRow =\n | { kind: \"read\"; feature: FeatureEntry; state: VerificationState; notice: VerifyNotice | null }\n | { kind: \"unreadable\"; finding: Finding };\n\nexport interface VerificationCheckRequest {\n docsDir: DocsDir;\n service: RawServiceId;\n fleet: FleetContext;\n}\n\nexport async function verificationCheck(\n req: VerificationCheckRequest,\n): Promise<{ features: GateFeature[]; findings: Finding[] }> {\n const active = await listFeatures(req.docsDir, {}, req.fleet);\n // \"Touching\" = carries a specs/<svc>/ delta — the same definition\n // `status --service` answers with. A feature whose C4 delta merely draws an\n // edge into the service is deliberately out: counting it needs a delta parse\n // per active feature, and it can be added additively if a fleet asks.\n const touching = active\n .filter((f) => f.services.includes(req.service))\n .sort((a, b) => compareIds(a.id, b.id));\n // COST ACCEPTED: each touching feature with a record derives its checklist,\n // which parses its delta. That is the per-feature cost\n // core/status/fleet/fleet.ts refuses for the WHOLE fleet; a deploy-time\n // command scoped to one service pays it for that service's features only —\n // with the invocation's context threaded through `verificationState` into\n // the derivation, so its enumerations are memo hits (one fleet walk per\n // run, not one per feature) and the batch prefetch below genuinely seeds\n // the delta parses. The prefetch is an accelerator only: a host that cannot\n // batch degrades to per-feature parses with identical findings.\n await req.fleet.prefetchLikeC4(\n touching.filter((f) => f.has.delta).map((f) => featurePaths(f.dir).delta),\n );\n // inOrder caps the workspaces in flight and keeps the rows in feature order.\n const rows = await inOrder(touching, async (f): Promise<VerificationRow> => {\n try {\n const { state, notice } = await verificationState(req.docsDir, f, req.fleet);\n return { kind: \"read\", feature: f, state, notice };\n } catch (err) {\n return { kind: \"unreadable\", finding: unreadableSubject(\"feature\", f.id, err) };\n }\n });\n const features: GateFeature[] = [];\n const findings: Finding[] = [];\n for (const row of rows) {\n if (row.kind === \"unreadable\") {\n findings.push(row.finding);\n continue;\n }\n features.push({ id: row.feature.id, ...row.state });\n findings.push(...featureFindings(row, req.service));\n }\n return { features, findings };\n}\n\nfunction featureFindings(\n row: { feature: FeatureEntry; state: VerificationState; notice: VerifyNotice | null },\n service: string,\n): Finding[] {\n const { feature, state, notice } = row;\n if (state.verdict === \"verified\") return [];\n if (state.verdict === \"attested\") {\n // attestedNotice's own words — the one notice verify, list and status all\n // show, converted to a Finding rather than re-derived. The code string\n // (`verify.scenario-attested`) is spelled literally at its one emit site,\n // core/verify/record.ts, where the drift guard collects it.\n return notice === null\n ? []\n : [\n {\n severity: notice.severity,\n code: notice.code,\n subject: feature.id,\n message: notice.message,\n ...(notice.claims === undefined ? {} : { details: notice.claims }),\n },\n ];\n }\n const verify = `\\`loam verify ${feature.id} --json\\``;\n const open = state.unconfirmed + state.unanswered;\n const message = (): string => {\n switch (state.state) {\n case \"absent\":\n return `${feature.id} touches '${service}' and has no verification record — nothing has been checked. Derive the checklist and answer it: ${verify}.`;\n case \"unreadable\":\n return `${feature.id}'s verification.yaml exists but does not read as a record — repair or delete it, then re-run ${verify}; nothing will overwrite it while it is unreadable.`;\n case \"stale\":\n return `${feature.id} moved after its record was written — the answers answer a checklist that is no longer the one being asked. Re-derive and re-answer: ${verify}.`;\n case \"recorded\":\n return state.claims === 0\n ? `${feature.id}'s record answers no claims at all — nothing was asked, so nothing was checked. ${verify} re-derives the checklist.`\n : `${open} of ${state.claims} claim(s) on ${feature.id} are open (${state.unconfirmed} unconfirmed, ${state.unanswered} unanswered) — close them with evidence: ${verify}.`;\n default: {\n // A fifth record state must land here as a build failure, not fall\n // through into some rung nobody decided (verificationStatus holds the\n // same guard for the same reason).\n const unreachable: never = state.state;\n throw new Error(`gate: no wording for verification state '${String(unreachable)}'`);\n }\n }\n };\n return [\n {\n severity: \"warn\",\n code: \"gate.feature-unverified\",\n subject: feature.id,\n message: message(),\n },\n ];\n}\n"]}
|
|
1
|
+
{"version":3,"file":"verification.js","sourceRoot":"","sources":["../../../src/core/gate/verification.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * Check 3 of `loam gate`: the verification records of every active feature\n * touching the gated service, read through `verificationState`'s own graded\n * verdicts — never a re-derivation of \"verified\" (that module's header is the\n * contract). Split from `./checks.ts` when the four checks outgrew the line\n * limit; this is the one check with a per-feature phase of its own, so it is\n * the seam.\n *\n * Without `--since` it reads IN-FLIGHT features only, so a feature leaves the\n * check the moment it is archived — and archive reads no verification record,\n * deliberately (verify never gates the merge). Under `--strict` that made\n * archiving the cheapest way past this check's warning, on a draft service\n * and on a vouched one alike when the feature changed only a contract\n * (`content_digest` covers the spec bodies, not openapi.yaml/asyncapi.yaml, so\n * freshness cannot catch it either). `--since <ref>` closes it without any\n * deployment state: every feature archived after the docs commit the pipeline last\n * deployed from is graded too, by the same `verificationState`, under the\n * same code, its row marked `archived: true` — \"archived after\" meaning its\n * archive directory was absent at the ref OR differs from what it held there,\n * since a feature unarchived, edited and archived again ships again from the\n * same path. Its record is frozen, but not ungraded: it is held to the\n * checklist `loam archive` captured as the feature merged\n * (`readSnapshotChecklist`), so a record that had stopped answering the\n * feature before it shipped is `stale` — archiving it no longer reads it\n * `verified` again. An archive that captured none (an older loam) is read as\n * frozen history, as before, and the row says so.\n *\n * Report, never fail: every finding here is a warning, by the item's own\n * design — a deploy gate that hard-failed on unverified work would teach\n * everyone that the cheapest way past it is an empty record.\n */\nimport { inOrder } from \"../kernel/concurrency.js\";\nimport { verificationState } from \"../status/verification.js\";\nimport { readSnapshotChecklist } from \"../staging/snapshot/manifest.js\";\nimport { listFeatures } from \"../repo/repo.js\";\nimport { featurePaths } from \"../repo/paths.js\";\nimport { compareIds, type FeatureEntry } from \"../repo/entries.js\";\nimport type { Finding } from \"../vocabulary/report.js\";\nimport type { VerificationState } from \"../status/report.js\";\nimport type { VerifyNotice } from \"../verify/record.js\";\nimport type { FleetContext } from \"../fleet-context.js\";\nimport type { DocsDir } from \"../kernel/ids/dirs.js\";\nimport type { RawServiceId } from \"../kernel/ids/service.js\";\nimport { unreadableSubject, type GateFeature, type GateSince } from \"./report.js\";\n\n/**\n * One graded feature. `shipped` is set only on an archived row: the checklist\n * digest its archive captured, or null when it captured none.\n */\ninterface ReadRow {\n kind: \"read\";\n feature: FeatureEntry;\n state: VerificationState;\n notice: VerifyNotice | null;\n shipped?: string | null;\n}\n\ntype VerificationRow = ReadRow | { kind: \"unreadable\"; finding: Finding };\n\nexport interface VerificationCheckRequest {\n docsDir: DocsDir;\n service: RawServiceId;\n fleet: FleetContext;\n since?: GateSince;\n}\n\nexport async function verificationCheck(\n req: VerificationCheckRequest,\n): Promise<{ features: GateFeature[]; findings: Finding[] }> {\n const { since } = req;\n const all = await listFeatures(req.docsDir, { includeArchived: since !== undefined }, req.fleet);\n // \"Touching\" = carries a specs/<svc>/ delta — the same definition\n // `status --service` answers with. A feature whose C4 delta merely draws an\n // edge into the service is deliberately out: counting it needs a delta parse\n // per active feature, and it can be added additively if a fleet asks. An\n // archived feature keeps its specs/ under features/archive/, so the one\n // definition answers for both sides. \"Archived after the ref\" is \"its\n // archive directory was not there at the ref, or is not what it was there\";\n // a feature whose directory was there UNCHANGED shipped in an earlier\n // deploy and is not this one's to answer for (`GateSince`).\n const touching = all\n .filter((f) => f.services.includes(req.service))\n .filter((f) => !f.archived || (since !== undefined && !since.shippedAtRef.has(f.dirName)))\n .sort((a, b) => compareIds(a.id, b.id) || compareIds(a.dirName, b.dirName));\n // COST ACCEPTED: each touching feature with a record derives its checklist,\n // which parses its delta. That is the per-feature cost\n // core/status/fleet/fleet.ts refuses for the WHOLE fleet; a deploy-time\n // command scoped to one service pays it for that service's features only —\n // with the invocation's context threaded through `verificationState` into\n // the derivation, so its enumerations are memo hits (one fleet walk per\n // run, not one per feature) and the batch prefetch below genuinely seeds\n // the delta parses. The prefetch is an accelerator only: a host that cannot\n // batch degrades to per-feature parses with identical findings.\n // An archived feature never derives a checklist (its record is frozen, see\n // `verificationState`, and held to the one its archive captured), so\n // prefetching its delta would parse for nobody.\n await req.fleet.prefetchLikeC4(\n touching.filter((f) => !f.archived && f.has.delta).map((f) => featurePaths(f.dir).delta),\n );\n // inOrder caps the workspaces in flight and keeps the rows in feature order.\n const rows = await inOrder(touching, async (f): Promise<VerificationRow> => {\n try {\n if (!f.archived) {\n const { state, notice } = await verificationState(req.docsDir, f, { fleet: req.fleet });\n return { kind: \"read\", feature: f, state, notice };\n }\n const shipped = await readSnapshotChecklist(f.dir);\n const { state, notice } = await verificationState(req.docsDir, f, { fleet: req.fleet, shipped });\n return { kind: \"read\", feature: f, state, notice, shipped };\n } catch (err) {\n return { kind: \"unreadable\", finding: unreadableSubject(\"feature\", f.id, err) };\n }\n });\n const features: GateFeature[] = [];\n const findings: Finding[] = [];\n for (const row of rows) {\n if (row.kind === \"unreadable\") {\n findings.push(row.finding);\n continue;\n }\n features.push({\n id: row.feature.id,\n ...row.state,\n ...(row.shipped === undefined ? {} : { archived: true, checklistAtArchive: row.shipped }),\n });\n findings.push(...featureFindings(row, req.service, since?.ref));\n }\n return { features, findings };\n}\n\nfunction featureFindings(\n row: ReadRow,\n service: string,\n since: string | undefined,\n): Finding[] {\n const { feature, state, notice } = row;\n if (state.verdict === \"verified\") return [];\n if (state.verdict === \"attested\") {\n // attestedNotice's own words — the one notice verify, list and status all\n // show, converted to a Finding rather than re-derived. The code string\n // (`verify.scenario-attested`) is spelled literally at its one emit site,\n // core/verify/record.ts, where the drift guard collects it.\n return notice === null\n ? []\n : [\n {\n severity: notice.severity,\n code: notice.code,\n subject: feature.id,\n message: notice.message,\n ...(notice.claims === undefined ? {} : { details: notice.claims }),\n },\n ];\n }\n const verify = `\\`loam verify ${feature.id} --json\\``;\n const open = state.unconfirmed + state.unanswered;\n const claimsOpen = `${open} of ${state.claims} claim(s) on ${feature.id} are open (${state.unconfirmed} unconfirmed, ${state.unanswered} unanswered)`;\n const message = (): string => {\n // A row `--since` added. Its record is frozen history: `loam verify` reads\n // it back and refuses to re-record it (commands/verify/verify.ts), so the\n // in-flight remedies below — derive, answer, re-derive — would send a\n // deploy pipeline to a command that cannot run. What it CAN be told is\n // that this deploy carries the feature unchecked, and the one way to\n // reopen it. An archived record is `stale` only against the checklist its\n // archive captured — the feature had moved under it before it shipped.\n if (feature.archived) {\n const shipped = `${feature.id} touches '${service}' and was archived after ${since ?? \"the --since ref\"}`;\n const reopen = `Its record is frozen history: ${verify} reads it back, and \\`loam unarchive ${feature.id}\\` re-opens the feature if its claims must be answered before this deploy.`;\n if (state.state === \"absent\") return `${shipped} with no verification record — nothing was checked before it shipped. ${reopen}`;\n // The file that failed may be one service's `verification/<svc>.yaml`\n // as well as the one-file record; `loam verify` names which.\n if (state.state === \"unreadable\") return `${shipped} with a verification record that does not read as a record — ${verify} names the file that failed. ${reopen}`;\n if (state.state === \"stale\") {\n return `${shipped}, and it had moved after its record was written — the record answers a checklist that was no longer the one it asked when it was archived, so what shipped was not what was checked. ${reopen}`;\n }\n return state.claims === 0\n ? `${shipped} with a record that answers no claims at all. ${reopen}`\n : `${shipped}, and ${claimsOpen}. ${reopen}`;\n }\n switch (state.state) {\n case \"absent\":\n return `${feature.id} touches '${service}' and has no verification record — nothing has been checked. Derive the checklist and answer it: ${verify}.`;\n case \"unreadable\":\n return `${feature.id}'s verification record does not read as a record — ${verify} names the file that failed and its safe repair (never delete a file other services' attestations may be in); nothing will overwrite it while it is unreadable.`;\n case \"stale\":\n return `${feature.id} moved after its record was written — the answers answer a checklist that is no longer the one being asked. Re-derive and re-answer: ${verify}.`;\n case \"recorded\":\n return state.claims === 0\n ? `${feature.id}'s record answers no claims at all — nothing was asked, so nothing was checked. ${verify} re-derives the checklist.`\n : `${claimsOpen} — close them with evidence: ${verify}.`;\n default: {\n // A fifth record state must land here as a build failure, not fall\n // through into some rung nobody decided (verificationStatus holds the\n // same guard for the same reason).\n const unreachable: never = state.state;\n throw new Error(`gate: no wording for verification state '${String(unreachable)}'`);\n }\n }\n };\n return [\n {\n severity: \"warn\",\n code: \"gate.feature-unverified\",\n subject: feature.id,\n message: message(),\n },\n ];\n}\n"]}
|
|
@@ -7,13 +7,13 @@
|
|
|
7
7
|
* whole comparison is by digest rather than by name, so a reworded scenario
|
|
8
8
|
* reads as stale rather than as missing plus new.
|
|
9
9
|
*/
|
|
10
|
-
import { existsSync
|
|
11
|
-
import { readdir, readFile
|
|
10
|
+
import { existsSync } from "node:fs";
|
|
11
|
+
import { readdir, readFile } from "node:fs/promises";
|
|
12
12
|
import { join, relative, sep } from "node:path";
|
|
13
|
-
import { parseRequirements } from "../document/parse.js";
|
|
14
13
|
import { SPEC_AXES } from "../repo/paths.js";
|
|
15
14
|
import { locateServicePaths } from "../repo/service-target.js";
|
|
16
15
|
import { listFeatures } from "../repo/repo.js";
|
|
16
|
+
import { entryIs, markDescended } from "../repo/tree/fs.js";
|
|
17
17
|
import { UnsafePathError } from "../kernel/path-safety.js";
|
|
18
18
|
import { gherkinRoot, scenarioDigest } from "./stamp.js";
|
|
19
19
|
import { axisLabel } from "./emit.js";
|
|
@@ -88,10 +88,7 @@ export async function gherkinFindings(ctx) {
|
|
|
88
88
|
const state = { file: axis.file, reqNames: new Set(), digests: new Set(), scenarios: [] };
|
|
89
89
|
const path = paths[axis.key];
|
|
90
90
|
if (existsSync(path)) {
|
|
91
|
-
const
|
|
92
|
-
? parseRequirements(await readFile(path, "utf8"))
|
|
93
|
-
: await ctx.fleet.readRequirements(path);
|
|
94
|
-
for (const r of reqs) {
|
|
91
|
+
for (const r of await ctx.fleet.readRequirements(path)) {
|
|
95
92
|
if (r.kind === "REMOVED")
|
|
96
93
|
continue;
|
|
97
94
|
state.reqNames.add(r.name);
|
|
@@ -174,9 +171,10 @@ export async function gherkinFindings(ctx) {
|
|
|
174
171
|
* enumeration is part of the output contract, so the order cannot depend on
|
|
175
172
|
* the filesystem's. Dot-entries are skipped, like every other walk in loam.
|
|
176
173
|
*
|
|
177
|
-
* Symlinks are FOLLOWED
|
|
178
|
-
*
|
|
179
|
-
*
|
|
174
|
+
* Symlinks are FOLLOWED, through `repo/tree/fs.ts`'s `entryIs` — the rule
|
|
175
|
+
* every enumeration shares (brief/brief.ts's `hasEntry` too), for the same
|
|
176
|
+
* reason. A `Dirent` from `readdir(withFileTypes)` never follows one, so
|
|
177
|
+
* `isFile()` and `isDirectory()` are both false for a
|
|
180
178
|
* symlinked `.feature`: it was invisible to the orphan scan (`loam gherkin`
|
|
181
179
|
* left it behind forever) and invisible to the staleness scan (it graded as no
|
|
182
180
|
* coverage at all). Composing a suite out of links is legitimate, so they are
|
|
@@ -186,29 +184,19 @@ export async function gherkinFindings(ctx) {
|
|
|
186
184
|
export async function featureFilesUnder(root) {
|
|
187
185
|
const out = [];
|
|
188
186
|
// Following links means a cycle is now possible; every directory is entered
|
|
189
|
-
// once by its resolved path, so a loop terminates instead
|
|
187
|
+
// once by its resolved path (`markDescended`), so a loop terminates instead
|
|
188
|
+
// of hanging.
|
|
190
189
|
const entered = new Set();
|
|
191
190
|
const walk = async (dir) => {
|
|
192
|
-
if (!
|
|
191
|
+
if (!markDescended(dir, entered))
|
|
193
192
|
return;
|
|
194
|
-
let resolved;
|
|
195
|
-
try {
|
|
196
|
-
resolved = await realpath(dir);
|
|
197
|
-
}
|
|
198
|
-
catch {
|
|
199
|
-
return;
|
|
200
|
-
}
|
|
201
|
-
if (entered.has(resolved))
|
|
202
|
-
return;
|
|
203
|
-
entered.add(resolved);
|
|
204
193
|
for (const entry of await readdir(dir, { withFileTypes: true })) {
|
|
205
194
|
if (entry.name.startsWith("."))
|
|
206
195
|
continue;
|
|
207
196
|
const abs = join(dir, entry.name);
|
|
208
|
-
|
|
209
|
-
if (what === "dir")
|
|
197
|
+
if (entryIs(dir, entry, "dir"))
|
|
210
198
|
await walk(abs);
|
|
211
|
-
else if (
|
|
199
|
+
else if (entry.name.endsWith(".feature") && entryIs(dir, entry, "file"))
|
|
212
200
|
out.push(abs);
|
|
213
201
|
}
|
|
214
202
|
};
|
|
@@ -219,16 +207,4 @@ export async function featureFilesUnder(root) {
|
|
|
219
207
|
return ra < rb ? -1 : ra > rb ? 1 : 0;
|
|
220
208
|
});
|
|
221
209
|
}
|
|
222
|
-
/** What an entry IS, resolving a symlink to find out. Nothing when it dangles. */
|
|
223
|
-
function entryKind(abs, e) {
|
|
224
|
-
if (!e.isSymbolicLink())
|
|
225
|
-
return e.isDirectory() ? "dir" : e.isFile() ? "file" : null;
|
|
226
|
-
try {
|
|
227
|
-
const s = statSync(abs);
|
|
228
|
-
return s.isDirectory() ? "dir" : s.isFile() ? "file" : null;
|
|
229
|
-
}
|
|
230
|
-
catch {
|
|
231
|
-
return null;
|
|
232
|
-
}
|
|
233
|
-
}
|
|
234
210
|
//# sourceMappingURL=stale.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"stale.js","sourceRoot":"","sources":["../../../src/core/gherkin/stale.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * What `loam validate` reports about a generated suite: which scenarios moved\n * out from under it, and which were never generated at all.\n *\n * Service-repo-scoped, and silent until a suite exists — a service that never\n * ran `loam gherkin` has not opted into the axis and is not failing it. The\n * whole comparison is by digest rather than by name, so a reworded scenario\n * reads as stale rather than as missing plus new.\n */\nimport { existsSync, statSync, type Dirent } from \"node:fs\";\nimport { readdir, readFile, realpath } from \"node:fs/promises\";\nimport { join, relative, sep } from \"node:path\";\nimport { parseRequirements } from \"../document/parse.js\";\nimport type { PathableService } from \"../kernel/ids/service.js\";\nimport { SPEC_AXES } from \"../repo/paths.js\";\nimport { locateServicePaths } from \"../repo/service-target.js\";\nimport { listFeatures } from \"../repo/repo.js\";\nimport type { FleetContext } from \"../fleet-context.js\";\nimport type { Finding } from \"../vocabulary/report.js\";\nimport { UnsafePathError } from \"../kernel/path-safety.js\";\nimport { gherkinRoot, scenarioDigest } from \"./stamp.js\";\nimport { axisLabel } from \"./emit.js\";\nimport { parseStampedFeature, type StampedFeature } from \"./read.js\";\nimport type { DocsDir } from \"../kernel/ids/dirs.js\";\n\n/**\n * The gherkin freshness chain, service-repo-scoped like `sources.*`: it needs\n * the repo (the suite lives there), and it stays quiet until\n * `<gherkinDir>/loam/` exists — a service that never generated has not opted\n * in, and silence there is honest, not vacuous.\n *\n * The LIVING specs are the reference, with one carve-out. A stamped file\n * tagged with a feature still in flight answers to that feature's delta, not\n * to the living spec its requirements have not merged into yet — so it is\n * skipped here (its regeneration is `loam gherkin <FEAT>`), and the moment the\n * feature archives, its requirements ARE living requirements, digests\n * unchanged, and the same file grades current with no rewrite. A tag naming\n * no active feature (the feature was archived, or abandoned) gets no\n * exemption: the living spec is all there is to answer to.\n *\n * Three warnings, one per way the suite and the spec can disagree, judged\n * digest-first and by requirement name second:\n *\n * - `gherkin.missing` — a living scenario whose digest no stamped scenario of\n * its axis carries: the suite has no test for these words.\n * - `gherkin.stale` — a stamped scenario whose digest matches no living\n * scenario while its file's requirement still exists: the spec moved under\n * the generated file. (A reworded scenario therefore reports both: the old\n * words are stale, the new words are missing — one regeneration clears\n * both.)\n * - `gherkin.orphaned` — a whole file whose requirement no longer exists in\n * its axis's living spec; reported once per file, because every scenario in\n * it is moot together.\n *\n * Digests are content identity within one service and one axis, deliberately:\n * two identically-worded scenarios of one service's one axis share one digest,\n * so one stamped copy covers both — while the axes stay separate namespaces\n * twice over (every comparison here is axis-scoped, and the recipe salts the\n * arch axis), and the services stay separate because the recipe salts the\n * service too. That last salt is why a stamped file copied out of another\n * service's repository grades `gherkin.orphaned`/`gherkin.stale` here instead\n * of quietly counting as this service's coverage.\n *\n * All warnings — `--strict` is the CI escalation — plus one `ok` confirmation\n * (`gherkin.current`, the `sources.current` pattern) when a generated suite\n * exists and nothing disagrees.\n */\nexport async function gherkinFindings(ctx: {\n docsDir: DocsDir;\n service: PathableService;\n /** The service repo, when loam is standing in it. Undefined disables the chain, like sources.*. */\n repoDir?: string;\n gherkinDir?: string;\n fleet?: FleetContext;\n}): Promise<Finding[]> {\n if (ctx.repoDir === undefined) return [];\n let root: string;\n try {\n root = gherkinRoot(ctx.repoDir, ctx.gherkinDir);\n } catch (err) {\n if (!(err instanceof UnsafePathError)) throw err;\n return [\n {\n severity: \"error\",\n code: \"gherkin.path-outside\",\n subject: ctx.service,\n message: `The configured Gherkin output is unsafe: ${err.message}. It must stay inside the service repo.`,\n },\n ];\n }\n if (!existsSync(root)) return [];\n\n // The living reference, per axis: requirement names, and scenario digests.\n interface AxisState {\n file: string;\n reqNames: Set<string>;\n digests: Set<string>;\n scenarios: Array<{ req: string; name: string; digest: string }>;\n }\n const paths = await locateServicePaths(ctx.docsDir, ctx.service, ctx.fleet);\n const axes = new Map<\"business\" | \"arch\", AxisState>();\n for (const axis of SPEC_AXES) {\n const label = axisLabel(axis);\n const state: AxisState = { file: axis.file, reqNames: new Set(), digests: new Set(), scenarios: [] };\n const path = paths[axis.key];\n if (existsSync(path)) {\n const reqs = ctx.fleet === undefined\n ? parseRequirements(await readFile(path, \"utf8\"))\n : await ctx.fleet.readRequirements(path);\n for (const r of reqs) {\n if (r.kind === \"REMOVED\") continue;\n state.reqNames.add(r.name);\n for (const s of r.scenarios) {\n const digest = scenarioDigest(ctx.service, s.lines, label);\n state.digests.add(digest);\n state.scenarios.push({ req: r.name, name: s.name, digest });\n }\n }\n }\n axes.set(label, state);\n }\n\n // The stamped side. Every stamped digest counts toward coverage — an\n // in-flight file whose scenario matches the living words IS a test for them.\n // `Set<string>`, not the ids' own brand: this set answers membership\n // questions for tags read out of .feature files — document text.\n const active = new Set<string>((await listFeatures(ctx.docsDir, {}, ctx.fleet)).map((f) => f.id));\n const stampedDigests = { business: new Set<string>(), arch: new Set<string>() };\n const parsed: Array<{ rel: string; axis: \"business\" | \"arch\"; file: StampedFeature }> = [];\n let stampedScenarios = 0;\n for (const abs of await featureFilesUnder(root)) {\n const file = parseStampedFeature(await readFile(abs, \"utf8\"));\n if (file === null) continue;\n const axis = file.tags.includes(\"architecture\") ? \"arch\" : \"business\";\n for (const s of file.scenarios) stampedDigests[axis].add(s.digest);\n stampedScenarios += file.scenarios.length;\n parsed.push({ rel: relative(ctx.repoDir, abs).split(sep).join(\"/\"), axis, file });\n }\n\n const findings: Finding[] = [];\n const regen = `\\`loam gherkin --service ${ctx.service}\\``;\n for (const [axis, state] of axes) {\n for (const sc of state.scenarios) {\n if (stampedDigests[axis].has(sc.digest)) continue;\n findings.push({\n severity: \"warn\",\n code: \"gherkin.missing\",\n subject: ctx.service,\n message: `${ctx.service}: scenario '${sc.name}' of requirement '${sc.req}' (${state.file}) has no generated .feature scenario — the suite has no test for these words; ${regen} regenerates it`,\n });\n }\n }\n for (const { rel, axis, file } of parsed) {\n if (file.tags.some((t) => active.has(t))) continue;\n const state = axes.get(axis)!;\n if (file.featureName === null || !state.reqNames.has(file.featureName)) {\n findings.push({\n severity: \"warn\",\n code: \"gherkin.orphaned\",\n subject: ctx.service,\n message: `${ctx.service}: ${rel}: requirement '${file.featureName ?? \"(unnamed)\"}' no longer exists in the living ${state.file} — the whole file is orphaned; ${regen} removes it`,\n });\n continue;\n }\n for (const s of file.scenarios) {\n if (state.digests.has(s.digest)) continue;\n findings.push({\n severity: \"warn\",\n code: \"gherkin.stale\",\n subject: ctx.service,\n message: `${ctx.service}: ${rel}: scenario '${s.name}' matches no scenario in the living ${state.file} — the spec moved under the generated file; ${regen} regenerates it`,\n });\n }\n }\n if (findings.length === 0) {\n findings.push({\n severity: \"ok\",\n code: \"gherkin.current\",\n message: `${ctx.service}: generated gherkin current (${parsed.length} file(s), ${stampedScenarios} stamped scenario(s) agree with the living specs)`,\n });\n }\n return findings;\n}\n\n/**\n * Every `.feature` file at or under `root`, sorted by repo-relative path —\n * enumeration is part of the output contract, so the order cannot depend on\n * the filesystem's. Dot-entries are skipped, like every other walk in loam.\n *\n * Symlinks are FOLLOWED — repo.ts's `entryIs` and brief/brief.ts's `hasMarkdown` are\n * the same rule for the same reason. A `Dirent` from `readdir(withFileTypes)`\n * never follows one, so `isFile()` and `isDirectory()` are both false for a\n * symlinked `.feature`: it was invisible to the orphan scan (`loam gherkin`\n * left it behind forever) and invisible to the staleness scan (it graded as no\n * coverage at all). Composing a suite out of links is legitimate, so they are\n * followed rather than refused, and a dangling one is skipped exactly as an\n * absent file is.\n */\nexport async function featureFilesUnder(root: string): Promise<string[]> {\n const out: string[] = [];\n // Following links means a cycle is now possible; every directory is entered\n // once by its resolved path, so a loop terminates instead of hanging.\n const entered = new Set<string>();\n const walk = async (dir: string): Promise<void> => {\n if (!existsSync(dir)) return;\n let resolved: string;\n try {\n resolved = await realpath(dir);\n } catch {\n return;\n }\n if (entered.has(resolved)) return;\n entered.add(resolved);\n for (const entry of await readdir(dir, { withFileTypes: true })) {\n if (entry.name.startsWith(\".\")) continue;\n const abs = join(dir, entry.name);\n const what = entryKind(abs, entry);\n if (what === \"dir\") await walk(abs);\n else if (what === \"file\" && entry.name.endsWith(\".feature\")) out.push(abs);\n }\n };\n await walk(root);\n return out.sort((a, b) => {\n const ra = a.split(sep).join(\"/\");\n const rb = b.split(sep).join(\"/\");\n return ra < rb ? -1 : ra > rb ? 1 : 0;\n });\n}\n\n/** What an entry IS, resolving a symlink to find out. Nothing when it dangles. */\nfunction entryKind(abs: string, e: Dirent): \"dir\" | \"file\" | null {\n if (!e.isSymbolicLink()) return e.isDirectory() ? \"dir\" : e.isFile() ? \"file\" : null;\n try {\n const s = statSync(abs);\n return s.isDirectory() ? \"dir\" : s.isFile() ? \"file\" : null;\n } catch {\n return null;\n }\n}\n"]}
|
|
1
|
+
{"version":3,"file":"stale.js","sourceRoot":"","sources":["../../../src/core/gherkin/stale.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * What `loam validate` reports about a generated suite: which scenarios moved\n * out from under it, and which were never generated at all.\n *\n * Service-repo-scoped, and silent until a suite exists — a service that never\n * ran `loam gherkin` has not opted into the axis and is not failing it. The\n * whole comparison is by digest rather than by name, so a reworded scenario\n * reads as stale rather than as missing plus new.\n */\nimport { existsSync } from \"node:fs\";\nimport { readdir, readFile } from \"node:fs/promises\";\nimport { join, relative, sep } from \"node:path\";\nimport type { PathableService } from \"../kernel/ids/service.js\";\nimport { SPEC_AXES } from \"../repo/paths.js\";\nimport { locateServicePaths } from \"../repo/service-target.js\";\nimport { listFeatures } from \"../repo/repo.js\";\nimport { entryIs, markDescended } from \"../repo/tree/fs.js\";\nimport type { FleetContext } from \"../fleet-context.js\";\nimport type { Finding } from \"../vocabulary/report.js\";\nimport { UnsafePathError } from \"../kernel/path-safety.js\";\nimport { gherkinRoot, scenarioDigest } from \"./stamp.js\";\nimport { axisLabel } from \"./emit.js\";\nimport { parseStampedFeature, type StampedFeature } from \"./read.js\";\nimport type { DocsDir } from \"../kernel/ids/dirs.js\";\n\n/**\n * The gherkin freshness chain, service-repo-scoped like `sources.*`: it needs\n * the repo (the suite lives there), and it stays quiet until\n * `<gherkinDir>/loam/` exists — a service that never generated has not opted\n * in, and silence there is honest, not vacuous.\n *\n * The LIVING specs are the reference, with one carve-out. A stamped file\n * tagged with a feature still in flight answers to that feature's delta, not\n * to the living spec its requirements have not merged into yet — so it is\n * skipped here (its regeneration is `loam gherkin <FEAT>`), and the moment the\n * feature archives, its requirements ARE living requirements, digests\n * unchanged, and the same file grades current with no rewrite. A tag naming\n * no active feature (the feature was archived, or abandoned) gets no\n * exemption: the living spec is all there is to answer to.\n *\n * Three warnings, one per way the suite and the spec can disagree, judged\n * digest-first and by requirement name second:\n *\n * - `gherkin.missing` — a living scenario whose digest no stamped scenario of\n * its axis carries: the suite has no test for these words.\n * - `gherkin.stale` — a stamped scenario whose digest matches no living\n * scenario while its file's requirement still exists: the spec moved under\n * the generated file. (A reworded scenario therefore reports both: the old\n * words are stale, the new words are missing — one regeneration clears\n * both.)\n * - `gherkin.orphaned` — a whole file whose requirement no longer exists in\n * its axis's living spec; reported once per file, because every scenario in\n * it is moot together.\n *\n * Digests are content identity within one service and one axis, deliberately:\n * two identically-worded scenarios of one service's one axis share one digest,\n * so one stamped copy covers both — while the axes stay separate namespaces\n * twice over (every comparison here is axis-scoped, and the recipe salts the\n * arch axis), and the services stay separate because the recipe salts the\n * service too. That last salt is why a stamped file copied out of another\n * service's repository grades `gherkin.orphaned`/`gherkin.stale` here instead\n * of quietly counting as this service's coverage.\n *\n * All warnings — `--strict` is the CI escalation — plus one `ok` confirmation\n * (`gherkin.current`, the `sources.current` pattern) when a generated suite\n * exists and nothing disagrees.\n */\nexport async function gherkinFindings(ctx: {\n docsDir: DocsDir;\n service: PathableService;\n /** The service repo, when loam is standing in it. Undefined disables the chain, like sources.*. */\n repoDir?: string;\n gherkinDir?: string;\n fleet: FleetContext;\n}): Promise<Finding[]> {\n if (ctx.repoDir === undefined) return [];\n let root: string;\n try {\n root = gherkinRoot(ctx.repoDir, ctx.gherkinDir);\n } catch (err) {\n if (!(err instanceof UnsafePathError)) throw err;\n return [\n {\n severity: \"error\",\n code: \"gherkin.path-outside\",\n subject: ctx.service,\n message: `The configured Gherkin output is unsafe: ${err.message}. It must stay inside the service repo.`,\n },\n ];\n }\n if (!existsSync(root)) return [];\n\n // The living reference, per axis: requirement names, and scenario digests.\n interface AxisState {\n file: string;\n reqNames: Set<string>;\n digests: Set<string>;\n scenarios: Array<{ req: string; name: string; digest: string }>;\n }\n const paths = await locateServicePaths(ctx.docsDir, ctx.service, ctx.fleet);\n const axes = new Map<\"business\" | \"arch\", AxisState>();\n for (const axis of SPEC_AXES) {\n const label = axisLabel(axis);\n const state: AxisState = { file: axis.file, reqNames: new Set(), digests: new Set(), scenarios: [] };\n const path = paths[axis.key];\n if (existsSync(path)) {\n for (const r of await ctx.fleet.readRequirements(path)) {\n if (r.kind === \"REMOVED\") continue;\n state.reqNames.add(r.name);\n for (const s of r.scenarios) {\n const digest = scenarioDigest(ctx.service, s.lines, label);\n state.digests.add(digest);\n state.scenarios.push({ req: r.name, name: s.name, digest });\n }\n }\n }\n axes.set(label, state);\n }\n\n // The stamped side. Every stamped digest counts toward coverage — an\n // in-flight file whose scenario matches the living words IS a test for them.\n // `Set<string>`, not the ids' own brand: this set answers membership\n // questions for tags read out of .feature files — document text.\n const active = new Set<string>((await listFeatures(ctx.docsDir, {}, ctx.fleet)).map((f) => f.id));\n const stampedDigests = { business: new Set<string>(), arch: new Set<string>() };\n const parsed: Array<{ rel: string; axis: \"business\" | \"arch\"; file: StampedFeature }> = [];\n let stampedScenarios = 0;\n for (const abs of await featureFilesUnder(root)) {\n const file = parseStampedFeature(await readFile(abs, \"utf8\"));\n if (file === null) continue;\n const axis = file.tags.includes(\"architecture\") ? \"arch\" : \"business\";\n for (const s of file.scenarios) stampedDigests[axis].add(s.digest);\n stampedScenarios += file.scenarios.length;\n parsed.push({ rel: relative(ctx.repoDir, abs).split(sep).join(\"/\"), axis, file });\n }\n\n const findings: Finding[] = [];\n const regen = `\\`loam gherkin --service ${ctx.service}\\``;\n for (const [axis, state] of axes) {\n for (const sc of state.scenarios) {\n if (stampedDigests[axis].has(sc.digest)) continue;\n findings.push({\n severity: \"warn\",\n code: \"gherkin.missing\",\n subject: ctx.service,\n message: `${ctx.service}: scenario '${sc.name}' of requirement '${sc.req}' (${state.file}) has no generated .feature scenario — the suite has no test for these words; ${regen} regenerates it`,\n });\n }\n }\n for (const { rel, axis, file } of parsed) {\n if (file.tags.some((t) => active.has(t))) continue;\n const state = axes.get(axis)!;\n if (file.featureName === null || !state.reqNames.has(file.featureName)) {\n findings.push({\n severity: \"warn\",\n code: \"gherkin.orphaned\",\n subject: ctx.service,\n message: `${ctx.service}: ${rel}: requirement '${file.featureName ?? \"(unnamed)\"}' no longer exists in the living ${state.file} — the whole file is orphaned; ${regen} removes it`,\n });\n continue;\n }\n for (const s of file.scenarios) {\n if (state.digests.has(s.digest)) continue;\n findings.push({\n severity: \"warn\",\n code: \"gherkin.stale\",\n subject: ctx.service,\n message: `${ctx.service}: ${rel}: scenario '${s.name}' matches no scenario in the living ${state.file} — the spec moved under the generated file; ${regen} regenerates it`,\n });\n }\n }\n if (findings.length === 0) {\n findings.push({\n severity: \"ok\",\n code: \"gherkin.current\",\n message: `${ctx.service}: generated gherkin current (${parsed.length} file(s), ${stampedScenarios} stamped scenario(s) agree with the living specs)`,\n });\n }\n return findings;\n}\n\n/**\n * Every `.feature` file at or under `root`, sorted by repo-relative path —\n * enumeration is part of the output contract, so the order cannot depend on\n * the filesystem's. Dot-entries are skipped, like every other walk in loam.\n *\n * Symlinks are FOLLOWED, through `repo/tree/fs.ts`'s `entryIs` — the rule\n * every enumeration shares (brief/brief.ts's `hasEntry` too), for the same\n * reason. A `Dirent` from `readdir(withFileTypes)` never follows one, so\n * `isFile()` and `isDirectory()` are both false for a\n * symlinked `.feature`: it was invisible to the orphan scan (`loam gherkin`\n * left it behind forever) and invisible to the staleness scan (it graded as no\n * coverage at all). Composing a suite out of links is legitimate, so they are\n * followed rather than refused, and a dangling one is skipped exactly as an\n * absent file is.\n */\nexport async function featureFilesUnder(root: string): Promise<string[]> {\n const out: string[] = [];\n // Following links means a cycle is now possible; every directory is entered\n // once by its resolved path (`markDescended`), so a loop terminates instead\n // of hanging.\n const entered = new Set<string>();\n const walk = async (dir: string): Promise<void> => {\n if (!markDescended(dir, entered)) return;\n for (const entry of await readdir(dir, { withFileTypes: true })) {\n if (entry.name.startsWith(\".\")) continue;\n const abs = join(dir, entry.name);\n if (entryIs(dir, entry, \"dir\")) await walk(abs);\n else if (entry.name.endsWith(\".feature\") && entryIs(dir, entry, \"file\")) out.push(abs);\n }\n };\n await walk(root);\n return out.sort((a, b) => {\n const ra = a.split(sep).join(\"/\");\n const rb = b.split(sep).join(\"/\");\n return ra < rb ? -1 : ra > rb ? 1 : 0;\n });\n}\n"]}
|
|
@@ -24,11 +24,11 @@
|
|
|
24
24
|
* document is prose for a human, and the one thing loam reads from it — the
|
|
25
25
|
* links it writes — is read by `core/links/`, where every other document's are.
|
|
26
26
|
*/
|
|
27
|
-
import { existsSync
|
|
27
|
+
import { existsSync } from "node:fs";
|
|
28
28
|
import { readdir } from "node:fs/promises";
|
|
29
29
|
import { join } from "node:path";
|
|
30
30
|
import { compareIds } from "../repo/entries.js";
|
|
31
|
-
import { entryIs } from "../repo/tree/fs.js";
|
|
31
|
+
import { entryIs, markDescended } from "../repo/tree/fs.js";
|
|
32
32
|
/** The one filename under `glossary/` that is not a term. */
|
|
33
33
|
const INDEX_PAGE = "README.md";
|
|
34
34
|
/**
|
|
@@ -37,8 +37,9 @@ const INDEX_PAGE = "README.md";
|
|
|
37
37
|
* A fleet is expected to hold tens to low hundreds of these — a domain's
|
|
38
38
|
* vocabulary, not its data — so the cost is one `readdir` per directory and the
|
|
39
39
|
* walk's expense is a non-question. The symlink-cycle guard is the one
|
|
40
|
-
* `core/repo/tree/walk.ts` and the capability walk both carry
|
|
41
|
-
*
|
|
40
|
+
* `core/repo/tree/walk.ts` and the capability walk both carry
|
|
41
|
+
* (`core/repo/tree/fs.ts`'s `markDescended`), and for the same reason:
|
|
42
|
+
* `entryIs` follows symlinks on purpose, and a followed cycle without a
|
|
42
43
|
* visited set recurses until the stack ends.
|
|
43
44
|
*/
|
|
44
45
|
export async function readGlossary(root) {
|
|
@@ -51,7 +52,7 @@ export async function readGlossary(root) {
|
|
|
51
52
|
}
|
|
52
53
|
/** Collect the terms in one directory and walk what is beneath it. */
|
|
53
54
|
async function visit(spot, state) {
|
|
54
|
-
if (!
|
|
55
|
+
if (!markDescended(spot.dir, state.visited))
|
|
55
56
|
return;
|
|
56
57
|
const entries = (await readdir(spot.dir, { withFileTypes: true })).filter((e) => !e.name.startsWith("."));
|
|
57
58
|
for (const entry of entries.filter((e) => e.name.endsWith(".md")).sort((a, b) => compareIds(a.name, b.name))) {
|
|
@@ -69,18 +70,4 @@ async function visit(spot, state) {
|
|
|
69
70
|
await visit({ dir: join(spot.dir, child), chain: [...spot.chain, child] }, state);
|
|
70
71
|
}
|
|
71
72
|
}
|
|
72
|
-
/** Mark a directory as descended-into; false when it (or another name for it) already was. */
|
|
73
|
-
function descended(dir, state) {
|
|
74
|
-
let real;
|
|
75
|
-
try {
|
|
76
|
-
real = realpathSync(dir);
|
|
77
|
-
}
|
|
78
|
-
catch {
|
|
79
|
-
return false;
|
|
80
|
-
}
|
|
81
|
-
if (state.visited.has(real))
|
|
82
|
-
return false;
|
|
83
|
-
state.visited.add(real);
|
|
84
|
-
return true;
|
|
85
|
-
}
|
|
86
73
|
//# sourceMappingURL=tree.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"tree.js","sourceRoot":"","sources":["../../../src/core/glossary/tree.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;GAyBG;AACH,OAAO,EAAE,UAAU,EAAE,
|
|
1
|
+
{"version":3,"file":"tree.js","sourceRoot":"","sources":["../../../src/core/glossary/tree.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * The glossary tree: `glossary/<term>.md`, and the rule that a markdown file\n * under it IS a declared term.\n *\n * WHY A TREE AND NOT A YAML VOCABULARY, at length in `repo/authored/paths.ts`'s\n * `glossaryDir`: a single vocabulary file at fleet scale is unworkable, and a\n * second list is the drift `loam init`'s removed service manifest was removed\n * for. A term is prose — that is what a definition is — so by this codebase's\n * own rule it gets a file.\n *\n * PRESENCE CLASSIFIES, one tree over from `capabilities/`: a `.md` file is a\n * term, a directory is a group the walk descends through. There is no\n * \"half-created term\" state to name, which is why this walk emits nothing like\n * `capability.doc-missing`: creating a term is writing one file, and an empty\n * grouping directory declares nothing and claims nothing.\n *\n * `README.md` IS NOT A TERM, and it is the one name excluded. A glossary big\n * enough to want an index page is normal, and reporting that page as a word\n * nobody uses would make the axis's only warning fire on the document that\n * explains the axis. Excluded by name rather than by a marker field: the\n * alternative is frontmatter nobody would remember to write.\n *\n * The walk reads directories only and never opens a file. What is IN a term\n * document is prose for a human, and the one thing loam reads from it — the\n * links it writes — is read by `core/links/`, where every other document's are.\n */\nimport { existsSync } from \"node:fs\";\nimport { readdir } from \"node:fs/promises\";\nimport { join } from \"node:path\";\nimport { compareIds } from \"../repo/entries.js\";\nimport { entryIs, markDescended } from \"../repo/tree/fs.js\";\n\n/** The one filename under `glossary/` that is not a term. */\nconst INDEX_PAGE = \"README.md\";\n\n/** One defined term: what it is called, and where its definition is. */\nexport interface GlossaryTerm {\n /** The path from `glossary/` to the file, without `.md` — `order`, or `payments/order`. */\n id: string;\n /** Absolute path of the definition. */\n path: string;\n}\n\nexport interface Glossary {\n /**\n * False when `glossary/` does not exist. The directory's existence is the\n * axis's opt-in, so a fleet without one produces nothing here — not an empty\n * glossary, which would read as \"the terms went missing\" to anything counting.\n */\n present: boolean;\n /** Every term found, at any depth, ordered by id. */\n terms: GlossaryTerm[];\n}\n\n/** One directory the walk is looking at: where it is, and how it was reached. */\ninterface Spot {\n dir: string;\n /** Directory names between `glossary/` and this one, outermost first. */\n chain: string[];\n}\n\ninterface WalkState {\n terms: GlossaryTerm[];\n /** Realpaths already descended into — the symlink-cycle guard. */\n visited: Set<string>;\n}\n\n/**\n * Read the tree under `root` (`glossaryDir(docsDir)`).\n *\n * A fleet is expected to hold tens to low hundreds of these — a domain's\n * vocabulary, not its data — so the cost is one `readdir` per directory and the\n * walk's expense is a non-question. The symlink-cycle guard is the one\n * `core/repo/tree/walk.ts` and the capability walk both carry\n * (`core/repo/tree/fs.ts`'s `markDescended`), and for the same reason:\n * `entryIs` follows symlinks on purpose, and a followed cycle without a\n * visited set recurses until the stack ends.\n */\nexport async function readGlossary(root: string): Promise<Glossary> {\n if (!existsSync(root)) return { present: false, terms: [] };\n const state: WalkState = { terms: [], visited: new Set() };\n await visit({ dir: root, chain: [] }, state);\n state.terms.sort((a, b) => compareIds(a.id, b.id));\n return { present: true, terms: state.terms };\n}\n\n/** Collect the terms in one directory and walk what is beneath it. */\nasync function visit(spot: Spot, state: WalkState): Promise<void> {\n if (!markDescended(spot.dir, state.visited)) return;\n const entries = (await readdir(spot.dir, { withFileTypes: true })).filter((e) => !e.name.startsWith(\".\"));\n for (const entry of entries.filter((e) => e.name.endsWith(\".md\")).sort((a, b) => compareIds(a.name, b.name))) {\n if (entry.name === INDEX_PAGE || !entryIs(spot.dir, entry, \"file\")) continue;\n state.terms.push({\n id: [...spot.chain, entry.name.slice(0, -\".md\".length)].join(\"/\"),\n path: join(spot.dir, entry.name),\n });\n }\n // Sequential, not Promise.all: the visited set is shared mutable state and\n // the walk's correctness under symlinks rests on a child being marked before\n // its sibling asks. These trees are tens of directories at most.\n for (const child of entries.filter((e) => entryIs(spot.dir, e, \"dir\")).map((e) => e.name).sort()) {\n await visit({ dir: join(spot.dir, child), chain: [...spot.chain, child] }, state);\n }\n}\n"]}
|
|
@@ -7,6 +7,16 @@
|
|
|
7
7
|
* either opens all of them — on a big fleet that is not a faster run, it is a
|
|
8
8
|
* bigger one. The cap and the ordering guarantee travel together because a
|
|
9
9
|
* caller that wants the first cannot afford to lose the second.
|
|
10
|
+
*
|
|
11
|
+
* The third answer to "how much may happen at once" is `memo`: the same read
|
|
12
|
+
* asked for twice in one invocation happens once, both askers sharing the one
|
|
13
|
+
* in-flight promise. It lives here, in a leaf, because both read indexes —
|
|
14
|
+
* `core/fleet-context.ts` and the service-model memos it owns
|
|
15
|
+
* (`core/c4/prefetch/fleet.ts`) — spell every cache they keep with it, and
|
|
16
|
+
* neither should own the other's idiom. The read index could borrow it from
|
|
17
|
+
* the package it already imports, but then a rule about caching in general
|
|
18
|
+
* would live inside the LikeC4 adapter, and the next memo outside `c4/` would
|
|
19
|
+
* have to import that adapter to obey it.
|
|
10
20
|
*/
|
|
11
21
|
import { cpus } from "node:os";
|
|
12
22
|
/**
|
|
@@ -48,4 +58,13 @@ export async function inOrder(items, work) {
|
|
|
48
58
|
}));
|
|
49
59
|
return out;
|
|
50
60
|
}
|
|
61
|
+
/** One in-flight promise per key, created on first ask — the memo idiom, once. */
|
|
62
|
+
export function memo(store, k, make) {
|
|
63
|
+
let pending = store.get(k);
|
|
64
|
+
if (pending === undefined) {
|
|
65
|
+
pending = make();
|
|
66
|
+
store.set(k, pending);
|
|
67
|
+
}
|
|
68
|
+
return pending;
|
|
69
|
+
}
|
|
51
70
|
//# sourceMappingURL=concurrency.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"concurrency.js","sourceRoot":"","sources":["../../../src/core/kernel/concurrency.ts"],"names":[],"mappings":"AAAA
|
|
1
|
+
{"version":3,"file":"concurrency.js","sourceRoot":"","sources":["../../../src/core/kernel/concurrency.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;GAmBG;AACH,OAAO,EAAE,IAAI,EAAE,MAAM,SAAS,CAAC;AAE/B;;;;;;;;;;;;;;;;;GAiBG;AACH,MAAM,CAAC,MAAM,kBAAkB,GAAG,IAAI,CAAC,GAAG,CAAC,CAAC,EAAE,IAAI,CAAC,GAAG,CAAC,IAAI,EAAE,CAAC,MAAM,EAAE,CAAC,CAAC,CAAC,CAAC;AAE1E;;;;;;;;;GASG;AACH,MAAM,CAAC,KAAK,UAAU,OAAO,CAC3B,KAAmB,EACnB,IAA6B;IAE7B,MAAM,GAAG,GAAQ,EAAE,CAAC;IACpB,IAAI,IAAI,GAAG,CAAC,CAAC;IACb,MAAM,OAAO,GAAG,IAAI,CAAC,GAAG,CAAC,kBAAkB,EAAE,KAAK,CAAC,MAAM,CAAC,CAAC;IAC3D,MAAM,OAAO,CAAC,GAAG,CACf,KAAK,CAAC,IAAI,CAAC,EAAE,MAAM,EAAE,OAAO,EAAE,EAAE,KAAK,IAAI,EAAE;QACzC,KAAK,IAAI,CAAC,GAAG,IAAI,EAAE,EAAE,CAAC,GAAG,KAAK,CAAC,MAAM,EAAE,CAAC,GAAG,IAAI,EAAE;YAAE,GAAG,CAAC,CAAC,CAAC,GAAG,MAAM,IAAI,CAAC,KAAK,CAAC,CAAC,CAAE,CAAC,CAAC;IACpF,CAAC,CAAC,CACH,CAAC;IACF,OAAO,GAAG,CAAC;AACb,CAAC;AAED,kFAAkF;AAClF,MAAM,UAAU,IAAI,CAAI,KAA8B,EAAE,CAAS,EAAE,IAAsB;IACvF,IAAI,OAAO,GAAG,KAAK,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC;IAC3B,IAAI,OAAO,KAAK,SAAS,EAAE,CAAC;QAC1B,OAAO,GAAG,IAAI,EAAE,CAAC;QACjB,KAAK,CAAC,GAAG,CAAC,CAAC,EAAE,OAAO,CAAC,CAAC;IACxB,CAAC;IACD,OAAO,OAAO,CAAC;AACjB,CAAC","sourcesContent":["/**\n * How much of loam may happen at once, and in what order the answers come back.\n *\n * One rule, because there is one resource being rationed: a LikeC4/Langium\n * workspace. `validate --all` opens one per service and per feature, the\n * dependency graph opens one per feature, and an unbounded `Promise.all` over\n * either opens all of them — on a big fleet that is not a faster run, it is a\n * bigger one. The cap and the ordering guarantee travel together because a\n * caller that wants the first cannot afford to lose the second.\n *\n * The third answer to \"how much may happen at once\" is `memo`: the same read\n * asked for twice in one invocation happens once, both askers sharing the one\n * in-flight promise. It lives here, in a leaf, because both read indexes —\n * `core/fleet-context.ts` and the service-model memos it owns\n * (`core/c4/prefetch/fleet.ts`) — spell every cache they keep with it, and\n * neither should own the other's idiom. The read index could borrow it from\n * the package it already imports, but then a rule about caching in general\n * would live inside the LikeC4 adapter, and the next memo outside `c4/` would\n * have to import that adapter to obey it.\n */\nimport { cpus } from \"node:os\";\n\n/**\n * How many units of work may be in flight at once.\n *\n * Capped as well as scaled: each in-flight unit holds a whole LikeC4/Langium\n * workspace, so a 64-core box would open sixty-four of them at once and buy\n * memory pressure rather than throughput (peak RSS on an 80-service fleet:\n * 385 MB serial, ~500 MB pooled). That measurement is the entire justification\n * for the cap, and it is why the cap lives with the work rather than inside one\n * command: `loam dependencies` and the fleet form of `loam status` fan out over\n * the same workspaces `validate --all` does, and used to do it uncapped.\n *\n * Do not quote a speedup for this. The measured cost is CPU inside the Langium\n * parse, which one thread cannot divide however the loop is shaped; what the\n * pool overlaps is the I/O half — the spec, contract and health reads around\n * each parse. On an 80-service fleet it measures as a wash (11–13 s either way,\n * on a box that was itself loaded). The number that moves these commands is\n * workspaces per run, not awaits per loop.\n */\nexport const TARGET_CONCURRENCY = Math.max(1, Math.min(cpus().length, 8));\n\n/**\n * Run `work` over `items` with a bounded number in flight, returning the\n * results in INPUT order.\n *\n * The order is the contract, not an accident: the text report and the `--json`\n * envelope are diffed between runs and between machines, so a concurrent run\n * has to be byte-identical to the serial one for the same input. That is why\n * results are written by index — nothing is appended as it finishes, and a slow\n * item cannot reorder the fleet around it.\n */\nexport async function inOrder<T, R>(\n items: readonly T[],\n work: (item: T) => Promise<R>,\n): Promise<R[]> {\n const out: R[] = [];\n let next = 0;\n const workers = Math.min(TARGET_CONCURRENCY, items.length);\n await Promise.all(\n Array.from({ length: workers }, async () => {\n for (let i = next++; i < items.length; i = next++) out[i] = await work(items[i]!);\n }),\n );\n return out;\n}\n\n/** One in-flight promise per key, created on first ask — the memo idiom, once. */\nexport function memo<T>(store: Map<string, Promise<T>>, k: string, make: () => Promise<T>): Promise<T> {\n let pending = store.get(k);\n if (pending === undefined) {\n pending = make();\n store.set(k, pending);\n }\n return pending;\n}\n"]}
|
|
@@ -16,6 +16,17 @@ export function isRecord(value) {
|
|
|
16
16
|
export function asRecord(value) {
|
|
17
17
|
return isRecord(value) ? value : null;
|
|
18
18
|
}
|
|
19
|
+
/**
|
|
20
|
+
* A list of strings. `Array.isArray` narrows an `unknown` to `any[]`, so an
|
|
21
|
+
* element check written below it proved nothing to tsc and the elements had to
|
|
22
|
+
* be asserted back to `string` at the point of use — how a checked element
|
|
23
|
+
* type went on being asserted back in a line later. Deciding both halves in
|
|
24
|
+
* one predicate makes the narrowing carry the check. (It was written twice,
|
|
25
|
+
* once in the OpenSpec scan and once in the migration's mapping reader.)
|
|
26
|
+
*/
|
|
27
|
+
export function isStringArray(value) {
|
|
28
|
+
return Array.isArray(value) && value.every((item) => typeof item === "string");
|
|
29
|
+
}
|
|
19
30
|
/**
|
|
20
31
|
* A map whose keys come from a document somebody else wrote.
|
|
21
32
|
*
|