@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
|
@@ -0,0 +1,211 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The living fleet as it stands on disk, read through the invocation's
|
|
3
|
+
* `FleetContext` into the SAME `ServiceState` shape `core/diff/base-state.ts`
|
|
4
|
+
* builds from git — one shape, so `core/diff/semantic.ts` joins the two sides
|
|
5
|
+
* without caring which one came from the object store.
|
|
6
|
+
*
|
|
7
|
+
* It was `loam diff`'s working-tree side (`core/diff/current-state.ts`) and
|
|
8
|
+
* still is; it moved here because the consumer index (`./consumers.ts`) reads
|
|
9
|
+
* the same fleet for every caller — the diff, the coherence gate and the
|
|
10
|
+
* archive planner — and a reader that only the diff could import was why the
|
|
11
|
+
* gate kept a copy of its own.
|
|
12
|
+
*
|
|
13
|
+
* Containment discipline, per subject and per axis: an artifact that EXISTS
|
|
14
|
+
* but cannot be read (non-UTF-8 spec bytes, a directory sitting where a file
|
|
15
|
+
* belongs, broken contract YAML) degrades that one axis to `unreadable` with
|
|
16
|
+
* its path, and the hash of its bytes when there are bytes to hash — never a
|
|
17
|
+
* thrown enumeration, never an empty list a diff would read as "everything
|
|
18
|
+
* was removed". Absence stays absence: a file deleted
|
|
19
|
+
* between the existence probe and the read is classified by its errno
|
|
20
|
+
* (ENOENT), the same TOCTOU discipline `loadConfig` documents.
|
|
21
|
+
*/
|
|
22
|
+
import { existsSync } from "node:fs";
|
|
23
|
+
import { readFile } from "node:fs/promises";
|
|
24
|
+
import { resolve } from "node:path";
|
|
25
|
+
import { serviceResolver } from "../c4/resolve/service.js";
|
|
26
|
+
import { externalProducerOf } from "../c4/resolve/external.js";
|
|
27
|
+
import { readUseCases, useCasesIn } from "../usecases/fleet.js";
|
|
28
|
+
import { repoPath } from "../envelope/json.js";
|
|
29
|
+
import { landscapePath, servicePathsAt } from "../repo/paths.js";
|
|
30
|
+
import { inOrder } from "../kernel/concurrency.js";
|
|
31
|
+
import { bytesDigest, collisions } from "./state.js";
|
|
32
|
+
/**
|
|
33
|
+
* The unreadable arm, carrying the hash of the bytes that would not read
|
|
34
|
+
* (`AxisState`'s `bytes`, which the base side's `axisFrom` fills the same
|
|
35
|
+
* way). The readers this module goes through hand back a parsed document and
|
|
36
|
+
* not its bytes, so this is a second read — paid only on this path, which a
|
|
37
|
+
* healthy fleet never takes.
|
|
38
|
+
*/
|
|
39
|
+
async function unreadableAt(abs, rel, error) {
|
|
40
|
+
try {
|
|
41
|
+
return { kind: "unreadable", path: rel, error, bytes: bytesDigest(await readFile(abs)) };
|
|
42
|
+
}
|
|
43
|
+
catch {
|
|
44
|
+
// No bytes to hash (a directory where the file belongs, a file deleted
|
|
45
|
+
// since the first read): an unhashed side proves no identity, so the axis
|
|
46
|
+
// suspends exactly as it always did.
|
|
47
|
+
return { kind: "unreadable", path: rel, error };
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
async function readFailure(e, file) {
|
|
51
|
+
const errno = e;
|
|
52
|
+
if (errno.code === "ENOENT")
|
|
53
|
+
return { kind: "absent" };
|
|
54
|
+
return unreadableAt(file.abs, file.rel, e instanceof Error ? e.message : String(e));
|
|
55
|
+
}
|
|
56
|
+
/** One spec-axis file (`spec.md` / `arch.spec.md`) as parsed requirements. */
|
|
57
|
+
async function requirementsAxis(context, abs, rel) {
|
|
58
|
+
if (!existsSync(abs))
|
|
59
|
+
return { kind: "absent" };
|
|
60
|
+
try {
|
|
61
|
+
return { kind: "read", value: await context.readRequirements(abs) };
|
|
62
|
+
}
|
|
63
|
+
catch (e) {
|
|
64
|
+
// decodeDocument's non-UTF-8 refusal lands here too: bytes that would
|
|
65
|
+
// parse as zero requirements must suspend the axis, not empty it.
|
|
66
|
+
return readFailure(e, { abs, rel });
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
async function openapiAxis(context, abs, rel) {
|
|
70
|
+
if (!existsSync(abs))
|
|
71
|
+
return { kind: "absent" };
|
|
72
|
+
try {
|
|
73
|
+
const doc = await context.readOpenapi(abs);
|
|
74
|
+
return doc.unreadable
|
|
75
|
+
? await unreadableAt(abs, rel, doc.error ?? "not a readable OpenAPI document")
|
|
76
|
+
: { kind: "read", value: doc.ops.filter((op) => !op.remove) };
|
|
77
|
+
}
|
|
78
|
+
catch (e) {
|
|
79
|
+
return readFailure(e, { abs, rel });
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
/** The event contract as its declarations, and the names it sends — `sent` empty unless it read. */
|
|
83
|
+
async function asyncapiAxis(context, abs, rel) {
|
|
84
|
+
if (!existsSync(abs))
|
|
85
|
+
return { axis: { kind: "absent" }, sent: [] };
|
|
86
|
+
try {
|
|
87
|
+
const doc = await context.readAsyncapi(abs);
|
|
88
|
+
return doc.unreadable
|
|
89
|
+
? { axis: await unreadableAt(abs, rel, doc.error ?? "not a readable AsyncAPI document"), sent: [] }
|
|
90
|
+
: { axis: { kind: "read", value: doc.messages.filter((m) => m.remove !== true) }, sent: doc.sent };
|
|
91
|
+
}
|
|
92
|
+
catch (e) {
|
|
93
|
+
return { axis: await readFailure(e, { abs, rel }), sent: [] };
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
/**
|
|
97
|
+
* The current fleet, one `ServiceState` per enumerated service. The
|
|
98
|
+
* enumeration itself (the tree walk) may throw `DocsRepoUnavailableError` —
|
|
99
|
+
* that is the whole-repo refusal `docsRepoReady` fronts, not a per-subject
|
|
100
|
+
* degradation, and it stays a throw for the command layer to map.
|
|
101
|
+
*/
|
|
102
|
+
async function readCurrentFleet(docsDir, context) {
|
|
103
|
+
const entries = await context.listServices(docsDir);
|
|
104
|
+
const states = await inOrder(entries, async (entry) => {
|
|
105
|
+
const paths = servicePathsAt(entry.dir);
|
|
106
|
+
const events = await asyncapiAxis(context, paths.asyncapi, repoPath(docsDir, paths.asyncapi));
|
|
107
|
+
return {
|
|
108
|
+
id: entry.id,
|
|
109
|
+
dir: repoPath(docsDir, entry.dir),
|
|
110
|
+
spec: await requirementsAxis(context, paths.spec, repoPath(docsDir, paths.spec)),
|
|
111
|
+
archSpec: await requirementsAxis(context, paths.archSpec, repoPath(docsDir, paths.archSpec)),
|
|
112
|
+
openapi: await openapiAxis(context, paths.openapi, repoPath(docsDir, paths.openapi)),
|
|
113
|
+
asyncapi: events.axis,
|
|
114
|
+
sent: events.sent,
|
|
115
|
+
};
|
|
116
|
+
});
|
|
117
|
+
const ambiguous = collisions(states);
|
|
118
|
+
return {
|
|
119
|
+
services: new Map(states.filter((s) => !ambiguous.has(s.id)).map((s) => [s.id, s])),
|
|
120
|
+
ambiguous,
|
|
121
|
+
};
|
|
122
|
+
}
|
|
123
|
+
/**
|
|
124
|
+
* One read per invocation, keyed by the invocation's own FleetContext — the
|
|
125
|
+
* `core/coherence/retired/archived.ts` exception to the no-module-cache rule,
|
|
126
|
+
* for its reason: the key IS the per-invocation object, so two runs can never
|
|
127
|
+
* see each other's answer and the entry dies with the context. `validate --all`
|
|
128
|
+
* grades every active feature with one context, and every one of them asks the
|
|
129
|
+
* same fleet who consumes what; without this each feature re-read it. Not a
|
|
130
|
+
* memo ON FleetContext for that module's reason too: the class sits below this
|
|
131
|
+
* package, and the import such a memo needs would point up the DAG.
|
|
132
|
+
*/
|
|
133
|
+
const PER_INVOCATION = new WeakMap();
|
|
134
|
+
function memoFor(docsDir, context) {
|
|
135
|
+
let store = PER_INVOCATION.get(context);
|
|
136
|
+
if (store === undefined)
|
|
137
|
+
PER_INVOCATION.set(context, (store = new Map()));
|
|
138
|
+
const key = resolve(docsDir);
|
|
139
|
+
let memo = store.get(key);
|
|
140
|
+
if (memo === undefined)
|
|
141
|
+
store.set(key, (memo = {}));
|
|
142
|
+
return memo;
|
|
143
|
+
}
|
|
144
|
+
/** `readCurrentFleet`, read at most once per invocation. */
|
|
145
|
+
export function livingFleet(docsDir, context) {
|
|
146
|
+
const memo = memoFor(docsDir, context);
|
|
147
|
+
return (memo.fleet ??= readCurrentFleet(docsDir, context));
|
|
148
|
+
}
|
|
149
|
+
/**
|
|
150
|
+
* The map, read at most once per invocation — and read as the PROJECT, never
|
|
151
|
+
* `architecture/landscape.likec4` alone.
|
|
152
|
+
*
|
|
153
|
+
* The spine grades every edge the `architecture/` project declares
|
|
154
|
+
* (`commands/validate/service/spine.ts` reads `fleet.architecture`), and a
|
|
155
|
+
* use-case hop is attributed against that same project. A consumer scan that
|
|
156
|
+
* read one file of it answered "nobody calls it" about an edge drawn in
|
|
157
|
+
* `architecture/calls.likec4`, the gate passed, and the next `validate --all`
|
|
158
|
+
* reported `spine.op-undefined` on that very edge (review 2026-10-12, finding
|
|
159
|
+
* 2) — `core/pack/living.ts` states the same rule for the same reason. One load
|
|
160
|
+
* serves the edges and the hops: two readers of one project are two chances to
|
|
161
|
+
* disagree about which documents it holds.
|
|
162
|
+
*
|
|
163
|
+
* An unreadable project proves nothing either way: `landscape.invalid` is
|
|
164
|
+
* validate's finding to make, and neither a victim nor a doubt invented out of
|
|
165
|
+
* a parse error may point the reader at a file their change never touched. ONE
|
|
166
|
+
* fallback, and it adds only positive evidence: when the project does not parse
|
|
167
|
+
* but `landscape.likec4` does on its own — a broken use-case file beside a good
|
|
168
|
+
* map — the map's own edges still count. They are true of the map either way,
|
|
169
|
+
* and losing them would have let a removal through the gate (and a `loam diff`
|
|
170
|
+
* call it unconsumed) because of a document that has nothing to do with the
|
|
171
|
+
* edge. The use cases stay unreadable: a flow is graded against the whole
|
|
172
|
+
* project or not at all. Whether there is a map at all stays a fact about the
|
|
173
|
+
* FILE, as the pack's `present` does: a repo with no `landscape.likec4` has no
|
|
174
|
+
* map, not an empty project.
|
|
175
|
+
*/
|
|
176
|
+
export function livingMap(docsDir, context) {
|
|
177
|
+
const memo = memoFor(docsDir, context);
|
|
178
|
+
return (memo.map ??= (async () => {
|
|
179
|
+
// Every directory id the walk found, the ones two directories claim
|
|
180
|
+
// included — the services and the `ambiguous` ids of `livingFleet` together,
|
|
181
|
+
// which is the set validate resolves the map against (`known` in
|
|
182
|
+
// `commands/validate/service/service.ts`), read off the same memoised
|
|
183
|
+
// enumeration. Directory names, so the producer rule's guard (a services/
|
|
184
|
+
// directory outranks a tag) is handed what it demands.
|
|
185
|
+
const known = new Set((await context.listServices(docsDir)).map((entry) => entry.id));
|
|
186
|
+
const none = () => () => null;
|
|
187
|
+
const mapPath = landscapePath(docsDir);
|
|
188
|
+
if (!existsSync(mapPath)) {
|
|
189
|
+
return { relationships: null, resolve: (id) => id, useCases: await readUseCases({ docsDir, known }), externalProducerRule: none };
|
|
190
|
+
}
|
|
191
|
+
const doc = await context.architecture(docsDir);
|
|
192
|
+
const useCases = useCasesIn(doc, known);
|
|
193
|
+
if (doc.errors.length === 0) {
|
|
194
|
+
const resolve = serviceResolver(doc.elements, known);
|
|
195
|
+
const externalProducerRule = (materialised) => {
|
|
196
|
+
// The living set and its resolver stand whenever the merge creates no
|
|
197
|
+
// directory the walk has not already found — `loam diff`'s read, and
|
|
198
|
+
// most features'.
|
|
199
|
+
const after = materialised.every((id) => known.has(id)) ? known : new Set([...known, ...materialised]);
|
|
200
|
+
const svcOf = after === known ? resolve : serviceResolver(doc.elements, after);
|
|
201
|
+
return (message) => externalProducerOf(message, doc, svcOf, after);
|
|
202
|
+
};
|
|
203
|
+
return { relationships: doc.relationships, resolve, useCases, externalProducerRule };
|
|
204
|
+
}
|
|
205
|
+
const alone = await context.loadLikeC4(mapPath);
|
|
206
|
+
return alone.errors.length > 0
|
|
207
|
+
? { relationships: null, resolve: (id) => id, useCases, externalProducerRule: none }
|
|
208
|
+
: { relationships: alone.relationships, resolve: serviceResolver(alone.elements, known), useCases, externalProducerRule: none };
|
|
209
|
+
})());
|
|
210
|
+
}
|
|
211
|
+
//# sourceMappingURL=current.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"current.js","sourceRoot":"","sources":["../../../src/core/consumers/current.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * The living fleet as it stands on disk, read through the invocation's\n * `FleetContext` into the SAME `ServiceState` shape `core/diff/base-state.ts`\n * builds from git — one shape, so `core/diff/semantic.ts` joins the two sides\n * without caring which one came from the object store.\n *\n * It was `loam diff`'s working-tree side (`core/diff/current-state.ts`) and\n * still is; it moved here because the consumer index (`./consumers.ts`) reads\n * the same fleet for every caller — the diff, the coherence gate and the\n * archive planner — and a reader that only the diff could import was why the\n * gate kept a copy of its own.\n *\n * Containment discipline, per subject and per axis: an artifact that EXISTS\n * but cannot be read (non-UTF-8 spec bytes, a directory sitting where a file\n * belongs, broken contract YAML) degrades that one axis to `unreadable` with\n * its path, and the hash of its bytes when there are bytes to hash — never a\n * thrown enumeration, never an empty list a diff would read as \"everything\n * was removed\". Absence stays absence: a file deleted\n * between the existence probe and the read is classified by its errno\n * (ENOENT), the same TOCTOU discipline `loadConfig` documents.\n */\nimport { existsSync } from \"node:fs\";\nimport { readFile } from \"node:fs/promises\";\nimport { resolve } from \"node:path\";\nimport type { Rel } from \"../c4/likec4.js\";\nimport { serviceResolver } from \"../c4/resolve/service.js\";\nimport { externalProducerOf } from \"../c4/resolve/external.js\";\nimport { readUseCases, useCasesIn, type UseCaseScan } from \"../usecases/fleet.js\";\nimport type { Requirement } from \"../document/spec.js\";\nimport type { Operation } from \"../openapi/doc.js\";\nimport type { EventMessage } from \"../asyncapi/model.js\";\nimport { repoPath } from \"../envelope/json.js\";\nimport { landscapePath, servicePathsAt } from \"../repo/paths.js\";\nimport { inOrder } from \"../kernel/concurrency.js\";\nimport type { DocsDir } from \"../kernel/ids/dirs.js\";\nimport type { RawServiceId } from \"../kernel/ids/service.js\";\nimport type { FleetContext } from \"../fleet-context.js\";\nimport { bytesDigest, collisions, type AxisState, type ServiceState } from \"./state.js\";\n\n/**\n * One living service as the consumer index reads it: the diff's four axes, plus\n * the one fact about its event contract the index asks and the diff never\n * compares — which messages it SENDS. Current side only: the base side is read\n * for what changed, and nothing asks who produced a message at the base ref.\n */\nexport interface LivingServiceState extends ServiceState {\n /**\n * The message names an `action: send` operation of the living asyncapi.yaml\n * reaches (`AsyncapiDoc.sent`) — what this service PRODUCES. Empty unless the\n * contract reads: an unreadable one never vouches for a producer.\n */\n sent: readonly string[];\n}\n\nexport interface CurrentFleet {\n /** By service id, in the enumeration's (sorted-walk) order. Ids the `ambiguous` map claims are excluded. */\n services: Map<string, LivingServiceState>;\n /**\n * Ids more than one working-tree directory claims — `subsystem.name-collision`'s\n * live shape, which validate diagnoses. Diffing either claimant would pick a\n * winner nobody chose (a `new Map` collapse here used to keep the LAST one and\n * fabricate a whole change story about a service that never moved), so the\n * subject is suspended exactly as the base side's `ambiguous` map suspends it.\n */\n ambiguous: Map<string, string[]>;\n}\n\n/**\n * The unreadable arm, carrying the hash of the bytes that would not read\n * (`AxisState`'s `bytes`, which the base side's `axisFrom` fills the same\n * way). The readers this module goes through hand back a parsed document and\n * not its bytes, so this is a second read — paid only on this path, which a\n * healthy fleet never takes.\n */\nasync function unreadableAt(abs: string, rel: string, error: string): Promise<AxisState<never>> {\n try {\n return { kind: \"unreadable\", path: rel, error, bytes: bytesDigest(await readFile(abs)) };\n } catch {\n // No bytes to hash (a directory where the file belongs, a file deleted\n // since the first read): an unhashed side proves no identity, so the axis\n // suspends exactly as it always did.\n return { kind: \"unreadable\", path: rel, error };\n }\n}\n\nasync function readFailure(e: unknown, file: { abs: string; rel: string }): Promise<AxisState<never>> {\n const errno = e as NodeJS.ErrnoException;\n if (errno.code === \"ENOENT\") return { kind: \"absent\" };\n return unreadableAt(file.abs, file.rel, e instanceof Error ? e.message : String(e));\n}\n\n/** One spec-axis file (`spec.md` / `arch.spec.md`) as parsed requirements. */\nasync function requirementsAxis(context: FleetContext, abs: string, rel: string): Promise<AxisState<Requirement[]>> {\n if (!existsSync(abs)) return { kind: \"absent\" };\n try {\n return { kind: \"read\", value: await context.readRequirements(abs) };\n } catch (e) {\n // decodeDocument's non-UTF-8 refusal lands here too: bytes that would\n // parse as zero requirements must suspend the axis, not empty it.\n return readFailure(e, { abs, rel });\n }\n}\n\nasync function openapiAxis(context: FleetContext, abs: string, rel: string): Promise<AxisState<Operation[]>> {\n if (!existsSync(abs)) return { kind: \"absent\" };\n try {\n const doc = await context.readOpenapi(abs);\n return doc.unreadable\n ? await unreadableAt(abs, rel, doc.error ?? \"not a readable OpenAPI document\")\n : { kind: \"read\", value: doc.ops.filter((op) => !op.remove) };\n } catch (e) {\n return readFailure(e, { abs, rel });\n }\n}\n\n/** The event contract as its declarations, and the names it sends — `sent` empty unless it read. */\nasync function asyncapiAxis(\n context: FleetContext,\n abs: string,\n rel: string,\n): Promise<{ axis: AxisState<EventMessage[]>; sent: readonly string[] }> {\n if (!existsSync(abs)) return { axis: { kind: \"absent\" }, sent: [] };\n try {\n const doc = await context.readAsyncapi(abs);\n return doc.unreadable\n ? { axis: await unreadableAt(abs, rel, doc.error ?? \"not a readable AsyncAPI document\"), sent: [] }\n : { axis: { kind: \"read\", value: doc.messages.filter((m) => m.remove !== true) }, sent: doc.sent };\n } catch (e) {\n return { axis: await readFailure(e, { abs, rel }), sent: [] };\n }\n}\n\n/**\n * The current fleet, one `ServiceState` per enumerated service. The\n * enumeration itself (the tree walk) may throw `DocsRepoUnavailableError` —\n * that is the whole-repo refusal `docsRepoReady` fronts, not a per-subject\n * degradation, and it stays a throw for the command layer to map.\n */\nasync function readCurrentFleet(docsDir: DocsDir, context: FleetContext): Promise<CurrentFleet> {\n const entries = await context.listServices(docsDir);\n const states = await inOrder(entries, async (entry): Promise<LivingServiceState> => {\n const paths = servicePathsAt(entry.dir);\n const events = await asyncapiAxis(context, paths.asyncapi, repoPath(docsDir, paths.asyncapi));\n return {\n id: entry.id,\n dir: repoPath(docsDir, entry.dir),\n spec: await requirementsAxis(context, paths.spec, repoPath(docsDir, paths.spec)),\n archSpec: await requirementsAxis(context, paths.archSpec, repoPath(docsDir, paths.archSpec)),\n openapi: await openapiAxis(context, paths.openapi, repoPath(docsDir, paths.openapi)),\n asyncapi: events.axis,\n sent: events.sent,\n };\n });\n const ambiguous = collisions(states);\n return {\n services: new Map(states.filter((s) => !ambiguous.has(s.id)).map((s) => [s.id, s])),\n ambiguous,\n };\n}\n\n/**\n * The fleet map half of the living read: the `architecture/` PROJECT's edges,\n * the one resolver every join over them uses, and the use cases drawn on it.\n */\nexport interface LivingMap {\n /** Every relationship the project declares, or `null` when the map proves nothing — absent, or it does not parse. */\n relationships: readonly Rel[] | null;\n /**\n * Element → service, with the enumerated fleet riding in — the resolver the\n * spine and the step graders use. Without the fleet, an edge into a modelled\n * container `payment.api` resolves to a service called \"api\" that has never\n * existed, the join on the provider finds nothing, and the one check\n * standing between a removal and its last consumer answers \"nobody calls it\"\n * (DESIGN's serviceResolver-known row).\n */\n resolve: (id: string) => string;\n useCases: UseCaseScan;\n /**\n * The rule naming the #external element whose `publishes` edge sends a\n * message, or null — validate's own (`externalProducerOf`,\n * `core/c4/resolve/external.ts`), asked of the same project with the same\n * directory ids and a resolver built over them, so the gate and `loam diff`\n * call a message produced exactly where `spine.message-unproduced` stays\n * quiet. Null wherever the PROJECT does not parse, the fallback below\n * included: validate grades the producer against the project or not at all,\n * and an outside producer found on a map it would not accept would pass a\n * removal its next run then reports.\n *\n * `materialised` is the service ids whose `services/<id>/` a feature's merge\n * would create — empty for a reader of the fleet as it stands. They join the\n * directory ids, and the resolver is REBUILT over the union, because the\n * rule's guard (a `services/<id>/` directory outranks the tag) is asked of\n * the directories the next `validate` will see: a feature carrying\n * `specs/legacy-gateway/` creates that directory, the #external element\n * titled `legacy-gateway` — or a topic nested inside it — then resolves to\n * it, and the producer is gone. Asked over the directories as they stood,\n * the gate passed a retirement from the message's only fleet sender, and\n * after the archive `loam diff` reported `diff.message-removed-consumed` and\n * `validate --all` `spine.message-unproduced` (the verification of D1, X2).\n */\n externalProducerRule: (materialised: readonly RawServiceId[]) => (message: string) => string | null;\n}\n\n/** What one invocation has read, per docs root: each half paid for at its first question. */\ninterface Memo {\n fleet?: Promise<CurrentFleet>;\n map?: Promise<LivingMap>;\n}\n\n/**\n * One read per invocation, keyed by the invocation's own FleetContext — the\n * `core/coherence/retired/archived.ts` exception to the no-module-cache rule,\n * for its reason: the key IS the per-invocation object, so two runs can never\n * see each other's answer and the entry dies with the context. `validate --all`\n * grades every active feature with one context, and every one of them asks the\n * same fleet who consumes what; without this each feature re-read it. Not a\n * memo ON FleetContext for that module's reason too: the class sits below this\n * package, and the import such a memo needs would point up the DAG.\n */\nconst PER_INVOCATION = new WeakMap<FleetContext, Map<string, Memo>>();\n\nfunction memoFor(docsDir: DocsDir, context: FleetContext): Memo {\n let store = PER_INVOCATION.get(context);\n if (store === undefined) PER_INVOCATION.set(context, (store = new Map()));\n const key = resolve(docsDir);\n let memo = store.get(key);\n if (memo === undefined) store.set(key, (memo = {}));\n return memo;\n}\n\n/** `readCurrentFleet`, read at most once per invocation. */\nexport function livingFleet(docsDir: DocsDir, context: FleetContext): Promise<CurrentFleet> {\n const memo = memoFor(docsDir, context);\n return (memo.fleet ??= readCurrentFleet(docsDir, context));\n}\n\n/**\n * The map, read at most once per invocation — and read as the PROJECT, never\n * `architecture/landscape.likec4` alone.\n *\n * The spine grades every edge the `architecture/` project declares\n * (`commands/validate/service/spine.ts` reads `fleet.architecture`), and a\n * use-case hop is attributed against that same project. A consumer scan that\n * read one file of it answered \"nobody calls it\" about an edge drawn in\n * `architecture/calls.likec4`, the gate passed, and the next `validate --all`\n * reported `spine.op-undefined` on that very edge (review 2026-10-12, finding\n * 2) — `core/pack/living.ts` states the same rule for the same reason. One load\n * serves the edges and the hops: two readers of one project are two chances to\n * disagree about which documents it holds.\n *\n * An unreadable project proves nothing either way: `landscape.invalid` is\n * validate's finding to make, and neither a victim nor a doubt invented out of\n * a parse error may point the reader at a file their change never touched. ONE\n * fallback, and it adds only positive evidence: when the project does not parse\n * but `landscape.likec4` does on its own — a broken use-case file beside a good\n * map — the map's own edges still count. They are true of the map either way,\n * and losing them would have let a removal through the gate (and a `loam diff`\n * call it unconsumed) because of a document that has nothing to do with the\n * edge. The use cases stay unreadable: a flow is graded against the whole\n * project or not at all. Whether there is a map at all stays a fact about the\n * FILE, as the pack's `present` does: a repo with no `landscape.likec4` has no\n * map, not an empty project.\n */\nexport function livingMap(docsDir: DocsDir, context: FleetContext): Promise<LivingMap> {\n const memo = memoFor(docsDir, context);\n return (memo.map ??= (async (): Promise<LivingMap> => {\n // Every directory id the walk found, the ones two directories claim\n // included — the services and the `ambiguous` ids of `livingFleet` together,\n // which is the set validate resolves the map against (`known` in\n // `commands/validate/service/service.ts`), read off the same memoised\n // enumeration. Directory names, so the producer rule's guard (a services/\n // directory outranks a tag) is handed what it demands.\n const known: ReadonlySet<RawServiceId> = new Set((await context.listServices(docsDir)).map((entry) => entry.id));\n const none = (): ((message: string) => null) => () => null;\n const mapPath = landscapePath(docsDir);\n if (!existsSync(mapPath)) {\n return { relationships: null, resolve: (id) => id, useCases: await readUseCases({ docsDir, known }), externalProducerRule: none };\n }\n const doc = await context.architecture(docsDir);\n const useCases = useCasesIn(doc, known);\n if (doc.errors.length === 0) {\n const resolve = serviceResolver(doc.elements, known);\n const externalProducerRule = (materialised: readonly RawServiceId[]): ((message: string) => string | null) => {\n // The living set and its resolver stand whenever the merge creates no\n // directory the walk has not already found — `loam diff`'s read, and\n // most features'.\n const after = materialised.every((id) => known.has(id)) ? known : new Set([...known, ...materialised]);\n const svcOf = after === known ? resolve : serviceResolver(doc.elements, after);\n return (message) => externalProducerOf(message, doc, svcOf, after);\n };\n return { relationships: doc.relationships, resolve, useCases, externalProducerRule };\n }\n const alone = await context.loadLikeC4(mapPath);\n return alone.errors.length > 0\n ? { relationships: null, resolve: (id) => id, useCases, externalProducerRule: none }\n : { relationships: alone.relationships, resolve: serviceResolver(alone.elements, known), useCases, externalProducerRule: none };\n })());\n}\n"]}
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* What one consumer question answers, and the one sentence that phrases it.
|
|
3
|
+
*
|
|
4
|
+
* Moved out of `loam diff`'s victim index (`core/diff/victims.ts`, now
|
|
5
|
+
* `./consumers.ts`) as its own module because three readers phrase the same
|
|
6
|
+
* answer — `loam diff`'s findings, `loam archive`'s overwrite warnings and the
|
|
7
|
+
* coherence gate's refusals — and none of them should have to import the index
|
|
8
|
+
* to name its result type.
|
|
9
|
+
*/
|
|
10
|
+
/**
|
|
11
|
+
* Is this a doubt about a consumer DOCUMENT — one loam could not read, or could
|
|
12
|
+
* not tell whose it is? Those are what the removal gate refuses on, as the gate
|
|
13
|
+
* before the one index did: it read every consumer's spec itself, and a file it
|
|
14
|
+
* could not decode either stopped `validate` or was read leniently by `archive`
|
|
15
|
+
* and refused. Filing those as warnings let a removal archive at exit 0 over a
|
|
16
|
+
* Latin-1 spec.md that still named the operation (the one-index review, finding 1).
|
|
17
|
+
*
|
|
18
|
+
* The other two kinds are doubts about a DRAWING, and they stay out of the
|
|
19
|
+
* gate on purpose: `loam seed` draws every edge without an op, so refusing on
|
|
20
|
+
* an op-less caller would turn every removal on a seeded fleet red, and a
|
|
21
|
+
* contested hop is a flow the model cannot attribute (`./consumers.ts` and
|
|
22
|
+
* `core/usecases/operations.ts` say why each is a doubt and never a victim).
|
|
23
|
+
* `loam diff` needs no such split: an unreadable document carries its run to
|
|
24
|
+
* exit 1 through its own `unreadable[]` entry, the drawings do not.
|
|
25
|
+
*/
|
|
26
|
+
export function documentDoubt(s) {
|
|
27
|
+
return s.kind === "unreadable" || s.kind === "ambiguous";
|
|
28
|
+
}
|
|
29
|
+
/** What a reader's `details[]` lists for one scan: positive evidence first, then every doubt — the order `loam diff` lists them in. */
|
|
30
|
+
export function scanDetails(scan) {
|
|
31
|
+
return [...scan.victims, ...scan.suspended.map((s) => s.text)];
|
|
32
|
+
}
|
|
33
|
+
/**
|
|
34
|
+
* Three honest phrasings of one consumer scan — never a confident negative
|
|
35
|
+
* over a scan that skipped something it could not check. Beside the type it
|
|
36
|
+
* phrases rather than inside `core/diff/semantic.ts`, because `loam diff` is no longer
|
|
37
|
+
* its only reader: `loam archive`'s overwrite warnings ask the same scan of
|
|
38
|
+
* the same fleet, and one sentence for one answer is what lets a reader meet
|
|
39
|
+
* one vocabulary in both.
|
|
40
|
+
*/
|
|
41
|
+
export function consumerStance(scan) {
|
|
42
|
+
return scan.victims.length > 0 ? `${scan.victims.length} current consumer(s) still name it`
|
|
43
|
+
: scan.suspended.length > 0 ? "whether current consumers name it could not be fully answered"
|
|
44
|
+
: "no current consumer names it";
|
|
45
|
+
}
|
|
46
|
+
//# sourceMappingURL=scan.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"scan.js","sourceRoot":"","sources":["../../../src/core/consumers/scan.ts"],"names":[],"mappings":"AAAA;;;;;;;;GAQG;AAqCH;;;;;;;;;;;;;;;GAeG;AACH,MAAM,UAAU,aAAa,CAAC,CAAa;IACzC,OAAO,CAAC,CAAC,IAAI,KAAK,YAAY,IAAI,CAAC,CAAC,IAAI,KAAK,WAAW,CAAC;AAC3D,CAAC;AAED,uIAAuI;AACvI,MAAM,UAAU,WAAW,CAAC,IAAkB;IAC5C,OAAO,CAAC,GAAG,IAAI,CAAC,OAAO,EAAE,GAAG,IAAI,CAAC,SAAS,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC;AACjE,CAAC;AAED;;;;;;;GAOG;AACH,MAAM,UAAU,cAAc,CAAC,IAAkB;IAC/C,OAAO,IAAI,CAAC,OAAO,CAAC,MAAM,GAAG,CAAC,CAAC,CAAC,CAAC,GAAG,IAAI,CAAC,OAAO,CAAC,MAAM,oCAAoC;QACzF,CAAC,CAAC,IAAI,CAAC,SAAS,CAAC,MAAM,GAAG,CAAC,CAAC,CAAC,CAAC,+DAA+D;YAC7F,CAAC,CAAC,8BAA8B,CAAC;AACrC,CAAC","sourcesContent":["/**\n * What one consumer question answers, and the one sentence that phrases it.\n *\n * Moved out of `loam diff`'s victim index (`core/diff/victims.ts`, now\n * `./consumers.ts`) as its own module because three readers phrase the same\n * answer — `loam diff`'s findings, `loam archive`'s overwrite warnings and the\n * coherence gate's refusals — and none of them should have to import the index\n * to name its result type.\n */\n\n/**\n * Why one possible consumer could not be checked. Typed, because the readers do\n * not weigh the kinds alike: a doubt about a DOCUMENT (`documentDoubt`) is one\n * the removal gate refuses on, and a doubt about a DRAWING is not.\n */\nexport type SuspensionKind =\n /** A consumer's spec.md or arch.spec.md exists and loam refused to read it — non-UTF-8 bytes, a directory where the file belongs. */\n | \"unreadable\"\n /** Two directories claim the consumer's service id, so neither one's documents were scanned. */\n | \"ambiguous\"\n /** An edge into the provider drawn with no `metadata { op }`: it calls the provider, and nobody wrote down which operation. */\n | \"unlinked\"\n /** A use-case hop whose backing relationships name different operations, this one among them. */\n | \"contested\";\n\n/** One possible consumer the scan could not check, and the sentence that says which and why. */\nexport interface Suspension {\n kind: SuspensionKind;\n /** What every reader shows — the document, the directories, the edge or the hop, by name. */\n text: string;\n}\n\n/**\n * One consumer scan's answer, honest about its own coverage. `victims` is\n * positive evidence; `suspended` names the consumers whose relevant document\n * could not be read (or whose identity is ambiguous), and the callers drawn or\n * flowed too vaguely to attribute, because a scan that skipped a file loam\n * REFUSED to read must never let the caller assert \"nobody names it\" — the\n * fail-open shape this type exists to forbid.\n */\nexport interface ConsumerScan {\n victims: string[];\n suspended: Suspension[];\n}\n\n/**\n * Is this a doubt about a consumer DOCUMENT — one loam could not read, or could\n * not tell whose it is? Those are what the removal gate refuses on, as the gate\n * before the one index did: it read every consumer's spec itself, and a file it\n * could not decode either stopped `validate` or was read leniently by `archive`\n * and refused. Filing those as warnings let a removal archive at exit 0 over a\n * Latin-1 spec.md that still named the operation (the one-index review, finding 1).\n *\n * The other two kinds are doubts about a DRAWING, and they stay out of the\n * gate on purpose: `loam seed` draws every edge without an op, so refusing on\n * an op-less caller would turn every removal on a seeded fleet red, and a\n * contested hop is a flow the model cannot attribute (`./consumers.ts` and\n * `core/usecases/operations.ts` say why each is a doubt and never a victim).\n * `loam diff` needs no such split: an unreadable document carries its run to\n * exit 1 through its own `unreadable[]` entry, the drawings do not.\n */\nexport function documentDoubt(s: Suspension): boolean {\n return s.kind === \"unreadable\" || s.kind === \"ambiguous\";\n}\n\n/** What a reader's `details[]` lists for one scan: positive evidence first, then every doubt — the order `loam diff` lists them in. */\nexport function scanDetails(scan: ConsumerScan): string[] {\n return [...scan.victims, ...scan.suspended.map((s) => s.text)];\n}\n\n/**\n * Three honest phrasings of one consumer scan — never a confident negative\n * over a scan that skipped something it could not check. Beside the type it\n * phrases rather than inside `core/diff/semantic.ts`, because `loam diff` is no longer\n * its only reader: `loam archive`'s overwrite warnings ask the same scan of\n * the same fleet, and one sentence for one answer is what lets a reader meet\n * one vocabulary in both.\n */\nexport function consumerStance(scan: ConsumerScan): string {\n return scan.victims.length > 0 ? `${scan.victims.length} current consumer(s) still name it`\n : scan.suspended.length > 0 ? \"whether current consumers name it could not be fully answered\"\n : \"no current consumer names it\";\n}\n"]}
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* One service's four documents, each read or honestly not — the shape every
|
|
3
|
+
* consumer question is asked of.
|
|
4
|
+
*
|
|
5
|
+
* It began as `loam diff`'s (`core/diff/base-state.ts` held it, because the
|
|
6
|
+
* diff was its only reader), and moved here when the consumer index became
|
|
7
|
+
* the one answer the diff, the coherence gate and the archive planner share:
|
|
8
|
+
* the index reads the living fleet in this shape, so the shape lives one level
|
|
9
|
+
* below everything that asks it.
|
|
10
|
+
*/
|
|
11
|
+
import { createHash } from "node:crypto";
|
|
12
|
+
/** The `bytes` identity of an unreadable side — the same hash on both sides, so equal means byte-identical. */
|
|
13
|
+
export function bytesDigest(bytes) {
|
|
14
|
+
return createHash("sha256").update(bytes).digest("hex");
|
|
15
|
+
}
|
|
16
|
+
/**
|
|
17
|
+
* Ids claimed by more than one enumerated directory, with every claimant named
|
|
18
|
+
* — the base side's grouping (`core/diff/base-state.ts`) and `./current.ts`'s,
|
|
19
|
+
* so the two sides of a diff refuse the same ambiguity the same way.
|
|
20
|
+
*/
|
|
21
|
+
export function collisions(dirs) {
|
|
22
|
+
const byId = new Map();
|
|
23
|
+
for (const d of dirs)
|
|
24
|
+
byId.set(d.id, [...(byId.get(d.id) ?? []), d.dir]);
|
|
25
|
+
return new Map([...byId].filter(([, claimants]) => claimants.length > 1));
|
|
26
|
+
}
|
|
27
|
+
//# sourceMappingURL=state.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"state.js","sourceRoot":"","sources":["../../../src/core/consumers/state.ts"],"names":[],"mappings":"AAAA;;;;;;;;;GASG;AACH,OAAO,EAAE,UAAU,EAAE,MAAM,aAAa,CAAC;AA0BzC,+GAA+G;AAC/G,MAAM,UAAU,WAAW,CAAC,KAAa;IACvC,OAAO,UAAU,CAAC,QAAQ,CAAC,CAAC,MAAM,CAAC,KAAK,CAAC,CAAC,MAAM,CAAC,KAAK,CAAC,CAAC;AAC1D,CAAC;AAuBD;;;;GAIG;AACH,MAAM,UAAU,UAAU,CAAC,IAA4C;IACrE,MAAM,IAAI,GAAG,IAAI,GAAG,EAAoB,CAAC;IACzC,KAAK,MAAM,CAAC,IAAI,IAAI;QAAE,IAAI,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,GAAG,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,CAAC,IAAI,EAAE,CAAC,EAAE,CAAC,CAAC,GAAG,CAAC,CAAC,CAAC;IACzE,OAAO,IAAI,GAAG,CAAC,CAAC,GAAG,IAAI,CAAC,CAAC,MAAM,CAAC,CAAC,CAAC,EAAE,SAAS,CAAC,EAAE,EAAE,CAAC,SAAS,CAAC,MAAM,GAAG,CAAC,CAAC,CAAC,CAAC;AAC5E,CAAC","sourcesContent":["/**\n * One service's four documents, each read or honestly not — the shape every\n * consumer question is asked of.\n *\n * It began as `loam diff`'s (`core/diff/base-state.ts` held it, because the\n * diff was its only reader), and moved here when the consumer index became\n * the one answer the diff, the coherence gate and the archive planner share:\n * the index reads the living fleet in this shape, so the shape lives one level\n * below everything that asks it.\n */\nimport { createHash } from \"node:crypto\";\nimport type { Requirement } from \"../document/spec.js\";\nimport type { Operation } from \"../openapi/doc.js\";\nimport type { EventMessage } from \"../asyncapi/model.js\";\nimport type { RawServiceId } from \"../kernel/ids/service.js\";\n\n/**\n * One artifact's state on one side of the diff. `unreadable` is the\n * containment arm: the file exists but could not be read as its kind, so\n * every finding on this axis for this subject is SUSPENDED — \"nobody could\n * look\" must never grade as \"nothing is there\", in either direction.\n *\n * `bytes` is the sha256 of the raw file when there WERE bytes and they refused\n * to parse — absent when the bytes themselves could not be had (a failed\n * `git show`, a directory where the file belongs). It is the one fact an\n * unreadable side can still prove: that it is the same file the other side\n * holds, which `core/diff/semantic.ts`'s `comparable` turns into \"the branch\n * did not touch this contract\" (an AsyncAPI 2.x document is unreadable by policy, and\n * suspending it on every run failed every `loam diff` over a fleet that holds\n * one, whatever the branch changed).\n */\nexport type AxisState<T> =\n | { kind: \"absent\" }\n | { kind: \"read\"; value: T }\n | { kind: \"unreadable\"; path: string; error: string; bytes?: string };\n\n/** The `bytes` identity of an unreadable side — the same hash on both sides, so equal means byte-identical. */\nexport function bytesDigest(bytes: Buffer): string {\n return createHash(\"sha256\").update(bytes).digest(\"hex\");\n}\n\n/**\n * One service on one side of the diff — the same shape for the base ref and\n * the working tree (`./current.ts` builds that one, `core/diff/base-state.ts`\n * the base ref's), which is what lets `core/diff/semantic.ts` join the two\n * without caring which side came from git — and what lets the consumer index\n * (`./consumers.ts`) read the living fleet in the shape the diff already does.\n */\nexport interface ServiceState {\n /** The directory's leaf name — a name read off the tree, so the index may join it into a feature's `specs/<svc>/`. */\n id: RawServiceId;\n /** Docs-relative directory with forward slashes, e.g. `services/billing/payment-service`. */\n dir: string;\n spec: AxisState<Requirement[]>;\n /** Read on both sides for every requirement join — the outbox requirement lives here as often as in spec.md (`./consumers.ts`'s `REQUIREMENT_AXES`). */\n archSpec: AxisState<Requirement[]>;\n /** Operations with `x-loam-remove` markers filtered out: a marker is bookkeeping, never a callable operation. */\n openapi: AxisState<Operation[]>;\n /** Messages with removal markers filtered out, for the same reason. */\n asyncapi: AxisState<EventMessage[]>;\n}\n\n/**\n * Ids claimed by more than one enumerated directory, with every claimant named\n * — the base side's grouping (`core/diff/base-state.ts`) and `./current.ts`'s,\n * so the two sides of a diff refuse the same ambiguity the same way.\n */\nexport function collisions(dirs: readonly { id: string; dir: string }[]): Map<string, string[]> {\n const byId = new Map<string, string[]>();\n for (const d of dirs) byId.set(d.id, [...(byId.get(d.id) ?? []), d.dir]);\n return new Map([...byId].filter(([, claimants]) => claimants.length > 1));\n}\n"]}
|
package/dist/core/delta/delta.js
CHANGED
|
@@ -245,7 +245,7 @@ export async function deltaShapeIssues(docsDir, featureDir, featureId, context)
|
|
|
245
245
|
* there still realize a capability requirement this feature retires?
|
|
246
246
|
*
|
|
247
247
|
* The same shape and the same cost `openapi.remove-op-consumed` pays through
|
|
248
|
-
* `
|
|
248
|
+
* the consumer index (`core/consumers/`) one axis over, and for the same reason — the merge
|
|
249
249
|
* deletes the promise while the `Realizes:` line stays, so the very next
|
|
250
250
|
* `validate --all` reports a breach on a repository whose author was never in
|
|
251
251
|
* this feature. Called behind a thunk (`removedRealizedIssues` invokes it only
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"delta.js","sourceRoot":"","sources":["../../../src/core/delta/delta.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * Does the diff apply to the thing it claims to change?\n *\n * A requirement delta is a diff against a living spec, and nothing used to check\n * that it lands. Every failure mode here is silent today: MODIFIED of a\n * requirement that does not exist is merged as a creation, ADDED of one that\n * does exist REPLACES it (scenarios and all) while the author believes they are\n * adding, and a heading that nearly matches the delta grammar parses as plain\n * prose so archive merges nothing at all and says nothing about it.\n *\n * The last of those has a legal-looking cousin: a requirement under an ordinary\n * prose heading (`## Behavior`, `## Error Handling`) is BASE too, and BASE never\n * merges. Upstream OpenSpec deltas are written that way, so the answer is to name\n * what will be lost rather than to start merging prose sections.\n *\n * These run inside the archive gate, because the merge is where the damage lands.\n *\n * TWO CORPORA, ONE ALGEBRA. A feature's delta documents live in two places —\n * `specs/<svc>/{spec,arch.spec}.md` against the service tree, and\n * `capabilities/<id>/spec.md` against the authored business tree — and every\n * grade about a DOCUMENT and its living counterpart applies to both verbatim,\n * with the capability id in `subject` where a service id would be. That is not\n * a coincidence to exploit but the reason the business corpus was given deltas\n * at all: a capability document is a requirements document, so the diff that\n * changes one has exactly the same ways of not landing. A second implementation\n * would be a second set of answers to `delta.added-duplicate`.\n *\n * The exception is the four CROSS-FEATURE warnings — `delta.added-conflict`,\n * `delta.modified-conflict`, `delta.modified-pending`, `delta.removed-pending`\n * — which ask what OTHER features in flight claim. `./claims.ts` indexes the\n * service corpus only and states why; on the business axis those four are\n * silent, which is a missing warning and never a wrong answer.\n *\n * This module is only the walk: which documents get read, in what order, and how\n * each is handed a `DeltaScope`. The checks themselves are in `./document.ts`\n * (a document against itself) and `./select.ts` (the delta against the living\n * text). Their order is load-bearing and stays here, where it can be read in one\n * screen.\n */\nimport { existsSync } from \"node:fs\";\nimport { readFile } from \"node:fs/promises\";\nimport { relative } from \"node:path\";\nimport { type FleetContext } from \"../fleet-context.js\";\nimport { parseRequirements } from \"../document/parse.js\";\nimport { type Requirement } from \"../document/spec.js\";\nimport { featureSpecPaths, SPEC_AXES } from \"../repo/paths.js\";\nimport { featureUseCasesDir, livingCapabilityPaths } from \"../repo/authored/paths.js\";\nimport { readCapabilityVocabulary } from \"../capabilities/capabilities.js\";\nimport { extendingModels } from \"../c4/service-model/fleet/extending.js\";\n\nimport { flowCoverage, type FlowPromises } from \"../usecases/delta/claims.js\";\nimport { enumeratedServiceIds, locateServicePaths } from \"../repo/service-target.js\";\nimport { featureSpecServices } from \"../repo/repo.js\";\nimport { capabilityDocIssues } from \"../capabilities/delta/doc.js\";\nimport { featureCapabilityDeltas } from \"../capabilities/delta/tree.js\";\nimport {\n removedRealizedIssues,\n uncoveredIssues,\n type CapabilityDeltaDoc,\n type RealizingDoc,\n} from \"../capabilities/delta/uncovered.js\";\nimport { type Issue } from \"../vocabulary/issue.js\";\nimport { claimLookup, type ClaimLookup } from \"./claims.js\";\nimport { deltaDocumentIssues, livingDocumentIssues } from \"./document.js\";\nimport { indexLiving, type DeltaScope, type LivingIndex } from \"./scope.js\";\nimport { selectionIssues } from \"./select.js\";\nimport type { DocsDir, FeatureDir } from \"../kernel/ids/dirs.js\";\n\n/**\n * The business corpus's ONE axis. A capability document carries the promise and\n * nothing else — there is no `arch.spec.md` beside it, because \"how it is\n * built\" is precisely the altitude a capability requirement may not describe\n * (`capability.requirement-service-scoped` is that rule). It has no `key`\n * because it indexes no `ServicePaths`, which is why `DeltaScope.axis` is the\n * narrower `DeltaAxis` and not `SpecAxis`.\n */\nconst CAPABILITY_AXIS = { file: \"spec.md\" };\n\n/**\n * What every document in one feature's walk shares.\n *\n * A record rather than two more parameters because both values are about the\n * WALK and not about the document being graded — `gradeDelta` would otherwise\n * read as a four-argument function whose last two arguments never vary within\n * a run, which is how the next value gets added as a fifth.\n */\ninterface DeltaReads {\n /** What OTHER features in flight claim — lazy, so the common case never scans. */\n claims: ClaimLookup;\n /** The invocation's read index, when the caller threaded one. */\n context?: FleetContext;\n}\n\nexport async function deltaShapeIssues(\n docsDir: DocsDir,\n featureDir: FeatureDir,\n featureId: string,\n context?: FleetContext,\n): Promise<Issue[]> {\n const issues: Issue[] = [];\n const services = await featureSpecServices(featureDir, context);\n // One `existsSync` for a feature that carries no capability delta, which is\n // every feature in a fleet that has not adopted the business axis.\n const capabilities =\n context === undefined ? await featureCapabilityDeltas(featureDir) : await context.featureCapabilityDeltas(featureDir);\n // The flows this feature brings, and one `existsSync` for a feature that\n // brings none — which is every feature in a fleet that has not adopted the\n // axis. The directory, not the documents: what is needed here is whether to\n // pay for the overlay at all, and the walk that lists them happens once,\n // inside it.\n const hasFlows = existsSync(featureUseCasesDir(featureDir));\n // The early return must ask about EVERY corpus. Asking only about services\n // meant a capability-only feature — a business change with no service touched\n // yet, which is exactly what an analyst writes first — was graded by nothing\n // and archived whatever its delta said; asking only about those two would do\n // the same to a feature whose whole content is a cross-service flow, which is\n // the shape an architect answers an analyst with.\n if (services.length === 0 && capabilities.docs.length === 0 && !hasFlows) return issues;\n\n // What OTHER features in flight claim. Only built when a claim has to be\n // checked — the common case never pays for the scan.\n const reads: DeltaReads = {\n claims: claimLookup(docsDir, featureId, context),\n ...(context === undefined ? {} : { context }),\n };\n\n // The two corpora's parsed requirements, kept as the walk produces them so\n // the `Realizes:` join below can be taken without re-reading a byte. That\n // join is the one question neither corpus can answer alone — which service\n // requirement keeps which business promise — and this is the only place in\n // loam where both sides are already open (`capabilities/delta/uncovered.ts`\n // holds the rules; nothing here decides anything).\n const serviceDeltas: RealizingDoc[] = [];\n const capabilityDeltas: CapabilityDeltaDoc[] = [];\n\n // Both requirement-carrying files per service run the same checks — one code\n // path parameterized by filename, the merge's own factoring. `where` names\n // the arch file in messages so a finding cannot be chased into the wrong\n // document; spec.md keeps its historical spelling.\n for (const service of services) {\n for (const axis of SPEC_AXES) {\n const specPath = featureSpecPaths(featureDir, service)[axis.key];\n if (!existsSync(specPath)) continue;\n const scope: DeltaScope = {\n kind: \"service\",\n subject: service,\n axis,\n featureId,\n docsDir,\n specPath,\n where: axis.key === \"spec\" ? service : `${service} (arch.spec.md)`,\n // How the axis's living document is named in messages — spec.md keeps\n // the historical \"living spec\", the arch axis says which file it means.\n livingDoc: axis.key === \"spec\" ? \"living spec\" : \"living arch.spec.md\",\n };\n const graded = await gradeDelta(scope, (await locateServicePaths(docsDir, service, context))[axis.key], reads);\n issues.push(...graded.issues);\n serviceDeltas.push({ service, file: axis.file, reqs: graded.reqs });\n }\n }\n\n for (const doc of capabilities.docs) {\n const scope: DeltaScope = {\n kind: \"capability\",\n subject: doc.id,\n axis: CAPABILITY_AXIS,\n featureId,\n docsDir,\n specPath: doc.spec,\n // `capability <id>`, not `capabilities/<id>` — the second reads as the\n // LIVING directory, and every message this labels is about the FEATURE's\n // delta of it. Sending an author to edit the wrong one of two files with\n // the same name is the whole reason `where` and `livingDoc` are separate.\n where: `capability ${doc.id}`,\n livingDoc: `living capabilities/${doc.id}/spec.md`,\n };\n // Absent is a real and ordinary answer on this axis: the first feature to\n // mention a capability is what creates its living document, so the whole\n // delta grades against `[]` and every requirement in it is legitimately an\n // addition.\n const graded = await gradeDelta(scope, livingCapabilityPaths(docsDir, doc.id).spec, reads);\n issues.push(...graded.issues);\n // The document's own three rules, on the requirements this delta would\n // MERGE — before the merge, where the author can still fix them. See\n // `capabilities/delta/doc.ts` for why grading only the living copy leaves a\n // hole straight through the altitude rule.\n issues.push(\n ...capabilityDocIssues(graded.reqs, {\n where: relative(docsDir, doc.spec).split(/[\\\\/]/).join(\"/\"),\n subject: doc.id,\n }),\n );\n capabilityDeltas.push({ id: doc.id, reqs: graded.reqs, living: graded.living.all });\n }\n\n // The other carrier of a promise, read once and only when this feature\n // actually brings a flow: the `dynamic view`s under `features/<FEAT>/usecases/`,\n // parsed over the map this feature's own merge would leave behind\n // (`core/usecases/delta/overlay.ts`).\n // A feature with no `usecases/` directory is a REAL empty answer, not an\n // ungraded one: there is no flow, so \"no flow keeps this\" is true and the\n // message may say it.\n const kept: FlowPromises = hasFlows\n ? await flowCoverage({\n docsDir,\n featureDir,\n featureId,\n capabilities: capabilityDeltas,\n // Read HERE, through the invocation's index, because this module holds\n // it and `core/usecases/` does not — every other read in this walk does\n // the same, and `core/repo/service-target.ts` prices a fleet\n // enumeration per call at ~13 ms on a 120-service fleet.\n vocabulary: context === undefined ? await readCapabilityVocabulary(docsDir) : await context.capabilities(docsDir),\n known: new Set(await enumeratedServiceIds(docsDir, context)),\n // A function rather than the context itself: one memoised LikeC4 read\n // does not justify an edge from `core/usecases/` to `fleet-context`, and\n // the edge would push that package up a DAG level for nothing.\n ...(context === undefined ? {} : { load: (path: string) => context.loadLikeC4(path) }),\n // The fleet's extending models, for the merge preview the overlay\n // grades against — read HERE for `vocabulary`'s reason exactly, and as\n // a thunk so the walk pays for it only when a flow is really there.\n // Without it the preview is the pre-routing map and `usecase.flow-invalid`\n // fails open on the one shape it exists for: a hop into\n // `<service-fqn>.<container>`, which the archive routes into that\n // service's own model (verification 2026-09-04, review F8).\n models: () => extendingModels(docsDir, context),\n })\n : { kind: \"read\", kept: [] };\n if (kept.kind === \"unreadable\") {\n // ERROR, and the archive gate's own `NEVER_OVERRIDABLE` keeps `--approve`\n // off it. This is the refusal that stops a flow being copied into\n // `architecture/` and the NEXT reader's `loam validate --all` carrying the\n // failure — including the case the overlay exists for, a hop naming an\n // element the merge does not land. Mechanical, so it is not loam's judgement\n // to override.\n issues.push({\n severity: \"error\",\n code: \"usecase.flow-invalid\",\n subject: featureId,\n // The opening sentence does NOT say whose file is at fault, and that is\n // deliberate: the corpus this reads is the feature's flows PLUS the living\n // `architecture/` documents they are drawn over, so the failing path can\n // be a living one — a landscape that no longer parses, a use case somebody\n // else wrote. Every message carries its own path, so a reader can place\n // it; an opening line that blamed `usecases/` would send them to the wrong\n // file first.\n message:\n `this feature's flows could not be graded against the map its own merge would leave behind, so the archive ` +\n `would land an architecture/ nothing has checked. The corpus is the feature's usecases/ AND the living ` +\n `architecture/ documents they are drawn over, so the fault may be in either — each message names its file. ` +\n `${kept.errors.length} problem(s): ${kept.errors.join(\"; \")}`,\n });\n }\n\n // The `Realizes:` join, both directions, over documents this walk has already\n // parsed. Skipped entirely when the feature carries no capability delta,\n // which is every feature in a fleet that has not adopted the business axis —\n // and the removal half asks for the living corpus only when something is\n // actually retired, so even an adopting fleet pays the fleet-wide read on the\n // features that earn it.\n if (capabilityDeltas.length > 0) {\n // The three arms travel as three, and flattening two of them to `[]` is\n // the one thing this must not do: `capability.uncovered` says in words that\n // no flow keeps the promise, so handing it an empty list for a corpus loam\n // could not read or could not resolve makes it assert a negative it never\n // evaluated — and gate on it. `claims.ts`'s header states the rule; this is\n // where a caller could break it.\n issues.push(\n ...uncoveredIssues(\n capabilityDeltas,\n serviceDeltas,\n kept.kind === \"read\"\n ? { graded: true, kept: kept.kept }\n : { graded: false, why: kept.kind === \"ungraded\" ? kept.why : kept.errors.join(\"; \") },\n ),\n );\n issues.push(\n ...(await removedRealizedIssues({\n capabilities: capabilityDeltas,\n deltas: serviceDeltas,\n living: () => livingRealizers(docsDir, context),\n })),\n );\n }\n\n return issues;\n}\n\n/**\n * Every LIVING service requirements document in the fleet, for the one question\n * that has to ask about services this feature never mentions: does anything out\n * there still realize a capability requirement this feature retires?\n *\n * The same shape and the same cost `openapi.remove-op-consumed` pays through\n * `coherence/lookups.ts` one axis over, and for the same reason — the merge\n * deletes the promise while the `Realizes:` line stays, so the very next\n * `validate --all` reports a breach on a repository whose author was never in\n * this feature. Called behind a thunk (`removedRealizedIssues` invokes it only\n * when something is retired), so a feature that removes nothing never runs it.\n */\nasync function livingRealizers(docsDir: DocsDir, context?: FleetContext): Promise<RealizingDoc[]> {\n const out: RealizingDoc[] = [];\n for (const service of await enumeratedServiceIds(docsDir, context)) {\n const paths = await locateServicePaths(docsDir, service, context);\n for (const axis of SPEC_AXES) {\n const path = paths[axis.key];\n if (!existsSync(path)) continue;\n const reqs =\n context === undefined ? parseRequirements(await readFile(path, \"utf8\")) : await context.readRequirements(path);\n out.push({ service, file: axis.file, reqs });\n }\n }\n return out;\n}\n\n/**\n * One delta document against one living document: the three passes, in the\n * order that makes their messages readable.\n *\n * The order is load-bearing. `deltaDocumentIssues` settles what the delta says\n * about itself, `livingDocumentIssues` settles whether the living text can be\n * selected in at all, and only then does `selectionIssues` compare them — a pin\n * the document pass already refused must not also be reported stale, which\n * would send its author to `loam rebase` for a problem rebase does not fix.\n *\n * The parsed delta requirements come back with the issues because the capability\n * corpus grades them a second way (`capabilityDocIssues`) and re-reading the\n * file to do it would be a second parse of bytes already in hand — and, without\n * a context, a second chance for the two passes to disagree about the document.\n * The LIVING index rides back for the same reason once more: the removal\n * direction of the `Realizes:` join has to resolve a REMOVED spelled by heading\n * into the id it retires, and that id is a fact about the living document.\n */\nasync function gradeDelta(\n scope: DeltaScope,\n livingPath: string,\n reads: DeltaReads,\n): Promise<{ issues: Issue[]; reqs: Requirement[]; living: LivingIndex }> {\n const { claims, context } = reads;\n const raw = context === undefined ? await readFile(scope.specPath, \"utf8\") : await context.readText(scope.specPath);\n const reqs = context === undefined ? parseRequirements(raw) : await context.readRequirements(scope.specPath);\n const living = indexLiving(\n !existsSync(livingPath)\n ? []\n : context === undefined\n ? parseRequirements(await readFile(livingPath, \"utf8\"))\n : await context.readRequirements(livingPath),\n );\n return {\n reqs,\n living,\n issues: [\n ...deltaDocumentIssues(scope, raw, reqs),\n ...livingDocumentIssues(scope, living),\n ...(await selectionIssues(scope, reqs, living, claims)),\n ],\n };\n}\n"]}
|
|
1
|
+
{"version":3,"file":"delta.js","sourceRoot":"","sources":["../../../src/core/delta/delta.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * Does the diff apply to the thing it claims to change?\n *\n * A requirement delta is a diff against a living spec, and nothing used to check\n * that it lands. Every failure mode here is silent today: MODIFIED of a\n * requirement that does not exist is merged as a creation, ADDED of one that\n * does exist REPLACES it (scenarios and all) while the author believes they are\n * adding, and a heading that nearly matches the delta grammar parses as plain\n * prose so archive merges nothing at all and says nothing about it.\n *\n * The last of those has a legal-looking cousin: a requirement under an ordinary\n * prose heading (`## Behavior`, `## Error Handling`) is BASE too, and BASE never\n * merges. Upstream OpenSpec deltas are written that way, so the answer is to name\n * what will be lost rather than to start merging prose sections.\n *\n * These run inside the archive gate, because the merge is where the damage lands.\n *\n * TWO CORPORA, ONE ALGEBRA. A feature's delta documents live in two places —\n * `specs/<svc>/{spec,arch.spec}.md` against the service tree, and\n * `capabilities/<id>/spec.md` against the authored business tree — and every\n * grade about a DOCUMENT and its living counterpart applies to both verbatim,\n * with the capability id in `subject` where a service id would be. That is not\n * a coincidence to exploit but the reason the business corpus was given deltas\n * at all: a capability document is a requirements document, so the diff that\n * changes one has exactly the same ways of not landing. A second implementation\n * would be a second set of answers to `delta.added-duplicate`.\n *\n * The exception is the four CROSS-FEATURE warnings — `delta.added-conflict`,\n * `delta.modified-conflict`, `delta.modified-pending`, `delta.removed-pending`\n * — which ask what OTHER features in flight claim. `./claims.ts` indexes the\n * service corpus only and states why; on the business axis those four are\n * silent, which is a missing warning and never a wrong answer.\n *\n * This module is only the walk: which documents get read, in what order, and how\n * each is handed a `DeltaScope`. The checks themselves are in `./document.ts`\n * (a document against itself) and `./select.ts` (the delta against the living\n * text). Their order is load-bearing and stays here, where it can be read in one\n * screen.\n */\nimport { existsSync } from \"node:fs\";\nimport { readFile } from \"node:fs/promises\";\nimport { relative } from \"node:path\";\nimport { type FleetContext } from \"../fleet-context.js\";\nimport { parseRequirements } from \"../document/parse.js\";\nimport { type Requirement } from \"../document/spec.js\";\nimport { featureSpecPaths, SPEC_AXES } from \"../repo/paths.js\";\nimport { featureUseCasesDir, livingCapabilityPaths } from \"../repo/authored/paths.js\";\nimport { readCapabilityVocabulary } from \"../capabilities/capabilities.js\";\nimport { extendingModels } from \"../c4/service-model/fleet/extending.js\";\n\nimport { flowCoverage, type FlowPromises } from \"../usecases/delta/claims.js\";\nimport { enumeratedServiceIds, locateServicePaths } from \"../repo/service-target.js\";\nimport { featureSpecServices } from \"../repo/repo.js\";\nimport { capabilityDocIssues } from \"../capabilities/delta/doc.js\";\nimport { featureCapabilityDeltas } from \"../capabilities/delta/tree.js\";\nimport {\n removedRealizedIssues,\n uncoveredIssues,\n type CapabilityDeltaDoc,\n type RealizingDoc,\n} from \"../capabilities/delta/uncovered.js\";\nimport { type Issue } from \"../vocabulary/issue.js\";\nimport { claimLookup, type ClaimLookup } from \"./claims.js\";\nimport { deltaDocumentIssues, livingDocumentIssues } from \"./document.js\";\nimport { indexLiving, type DeltaScope, type LivingIndex } from \"./scope.js\";\nimport { selectionIssues } from \"./select.js\";\nimport type { DocsDir, FeatureDir } from \"../kernel/ids/dirs.js\";\n\n/**\n * The business corpus's ONE axis. A capability document carries the promise and\n * nothing else — there is no `arch.spec.md` beside it, because \"how it is\n * built\" is precisely the altitude a capability requirement may not describe\n * (`capability.requirement-service-scoped` is that rule). It has no `key`\n * because it indexes no `ServicePaths`, which is why `DeltaScope.axis` is the\n * narrower `DeltaAxis` and not `SpecAxis`.\n */\nconst CAPABILITY_AXIS = { file: \"spec.md\" };\n\n/**\n * What every document in one feature's walk shares.\n *\n * A record rather than two more parameters because both values are about the\n * WALK and not about the document being graded — `gradeDelta` would otherwise\n * read as a four-argument function whose last two arguments never vary within\n * a run, which is how the next value gets added as a fifth.\n */\ninterface DeltaReads {\n /** What OTHER features in flight claim — lazy, so the common case never scans. */\n claims: ClaimLookup;\n /** The invocation's read index, when the caller threaded one. */\n context?: FleetContext;\n}\n\nexport async function deltaShapeIssues(\n docsDir: DocsDir,\n featureDir: FeatureDir,\n featureId: string,\n context?: FleetContext,\n): Promise<Issue[]> {\n const issues: Issue[] = [];\n const services = await featureSpecServices(featureDir, context);\n // One `existsSync` for a feature that carries no capability delta, which is\n // every feature in a fleet that has not adopted the business axis.\n const capabilities =\n context === undefined ? await featureCapabilityDeltas(featureDir) : await context.featureCapabilityDeltas(featureDir);\n // The flows this feature brings, and one `existsSync` for a feature that\n // brings none — which is every feature in a fleet that has not adopted the\n // axis. The directory, not the documents: what is needed here is whether to\n // pay for the overlay at all, and the walk that lists them happens once,\n // inside it.\n const hasFlows = existsSync(featureUseCasesDir(featureDir));\n // The early return must ask about EVERY corpus. Asking only about services\n // meant a capability-only feature — a business change with no service touched\n // yet, which is exactly what an analyst writes first — was graded by nothing\n // and archived whatever its delta said; asking only about those two would do\n // the same to a feature whose whole content is a cross-service flow, which is\n // the shape an architect answers an analyst with.\n if (services.length === 0 && capabilities.docs.length === 0 && !hasFlows) return issues;\n\n // What OTHER features in flight claim. Only built when a claim has to be\n // checked — the common case never pays for the scan.\n const reads: DeltaReads = {\n claims: claimLookup(docsDir, featureId, context),\n ...(context === undefined ? {} : { context }),\n };\n\n // The two corpora's parsed requirements, kept as the walk produces them so\n // the `Realizes:` join below can be taken without re-reading a byte. That\n // join is the one question neither corpus can answer alone — which service\n // requirement keeps which business promise — and this is the only place in\n // loam where both sides are already open (`capabilities/delta/uncovered.ts`\n // holds the rules; nothing here decides anything).\n const serviceDeltas: RealizingDoc[] = [];\n const capabilityDeltas: CapabilityDeltaDoc[] = [];\n\n // Both requirement-carrying files per service run the same checks — one code\n // path parameterized by filename, the merge's own factoring. `where` names\n // the arch file in messages so a finding cannot be chased into the wrong\n // document; spec.md keeps its historical spelling.\n for (const service of services) {\n for (const axis of SPEC_AXES) {\n const specPath = featureSpecPaths(featureDir, service)[axis.key];\n if (!existsSync(specPath)) continue;\n const scope: DeltaScope = {\n kind: \"service\",\n subject: service,\n axis,\n featureId,\n docsDir,\n specPath,\n where: axis.key === \"spec\" ? service : `${service} (arch.spec.md)`,\n // How the axis's living document is named in messages — spec.md keeps\n // the historical \"living spec\", the arch axis says which file it means.\n livingDoc: axis.key === \"spec\" ? \"living spec\" : \"living arch.spec.md\",\n };\n const graded = await gradeDelta(scope, (await locateServicePaths(docsDir, service, context))[axis.key], reads);\n issues.push(...graded.issues);\n serviceDeltas.push({ service, file: axis.file, reqs: graded.reqs });\n }\n }\n\n for (const doc of capabilities.docs) {\n const scope: DeltaScope = {\n kind: \"capability\",\n subject: doc.id,\n axis: CAPABILITY_AXIS,\n featureId,\n docsDir,\n specPath: doc.spec,\n // `capability <id>`, not `capabilities/<id>` — the second reads as the\n // LIVING directory, and every message this labels is about the FEATURE's\n // delta of it. Sending an author to edit the wrong one of two files with\n // the same name is the whole reason `where` and `livingDoc` are separate.\n where: `capability ${doc.id}`,\n livingDoc: `living capabilities/${doc.id}/spec.md`,\n };\n // Absent is a real and ordinary answer on this axis: the first feature to\n // mention a capability is what creates its living document, so the whole\n // delta grades against `[]` and every requirement in it is legitimately an\n // addition.\n const graded = await gradeDelta(scope, livingCapabilityPaths(docsDir, doc.id).spec, reads);\n issues.push(...graded.issues);\n // The document's own three rules, on the requirements this delta would\n // MERGE — before the merge, where the author can still fix them. See\n // `capabilities/delta/doc.ts` for why grading only the living copy leaves a\n // hole straight through the altitude rule.\n issues.push(\n ...capabilityDocIssues(graded.reqs, {\n where: relative(docsDir, doc.spec).split(/[\\\\/]/).join(\"/\"),\n subject: doc.id,\n }),\n );\n capabilityDeltas.push({ id: doc.id, reqs: graded.reqs, living: graded.living.all });\n }\n\n // The other carrier of a promise, read once and only when this feature\n // actually brings a flow: the `dynamic view`s under `features/<FEAT>/usecases/`,\n // parsed over the map this feature's own merge would leave behind\n // (`core/usecases/delta/overlay.ts`).\n // A feature with no `usecases/` directory is a REAL empty answer, not an\n // ungraded one: there is no flow, so \"no flow keeps this\" is true and the\n // message may say it.\n const kept: FlowPromises = hasFlows\n ? await flowCoverage({\n docsDir,\n featureDir,\n featureId,\n capabilities: capabilityDeltas,\n // Read HERE, through the invocation's index, because this module holds\n // it and `core/usecases/` does not — every other read in this walk does\n // the same, and `core/repo/service-target.ts` prices a fleet\n // enumeration per call at ~13 ms on a 120-service fleet.\n vocabulary: context === undefined ? await readCapabilityVocabulary(docsDir) : await context.capabilities(docsDir),\n known: new Set(await enumeratedServiceIds(docsDir, context)),\n // A function rather than the context itself: one memoised LikeC4 read\n // does not justify an edge from `core/usecases/` to `fleet-context`, and\n // the edge would push that package up a DAG level for nothing.\n ...(context === undefined ? {} : { load: (path: string) => context.loadLikeC4(path) }),\n // The fleet's extending models, for the merge preview the overlay\n // grades against — read HERE for `vocabulary`'s reason exactly, and as\n // a thunk so the walk pays for it only when a flow is really there.\n // Without it the preview is the pre-routing map and `usecase.flow-invalid`\n // fails open on the one shape it exists for: a hop into\n // `<service-fqn>.<container>`, which the archive routes into that\n // service's own model (verification 2026-09-04, review F8).\n models: () => extendingModels(docsDir, context),\n })\n : { kind: \"read\", kept: [] };\n if (kept.kind === \"unreadable\") {\n // ERROR, and the archive gate's own `NEVER_OVERRIDABLE` keeps `--approve`\n // off it. This is the refusal that stops a flow being copied into\n // `architecture/` and the NEXT reader's `loam validate --all` carrying the\n // failure — including the case the overlay exists for, a hop naming an\n // element the merge does not land. Mechanical, so it is not loam's judgement\n // to override.\n issues.push({\n severity: \"error\",\n code: \"usecase.flow-invalid\",\n subject: featureId,\n // The opening sentence does NOT say whose file is at fault, and that is\n // deliberate: the corpus this reads is the feature's flows PLUS the living\n // `architecture/` documents they are drawn over, so the failing path can\n // be a living one — a landscape that no longer parses, a use case somebody\n // else wrote. Every message carries its own path, so a reader can place\n // it; an opening line that blamed `usecases/` would send them to the wrong\n // file first.\n message:\n `this feature's flows could not be graded against the map its own merge would leave behind, so the archive ` +\n `would land an architecture/ nothing has checked. The corpus is the feature's usecases/ AND the living ` +\n `architecture/ documents they are drawn over, so the fault may be in either — each message names its file. ` +\n `${kept.errors.length} problem(s): ${kept.errors.join(\"; \")}`,\n });\n }\n\n // The `Realizes:` join, both directions, over documents this walk has already\n // parsed. Skipped entirely when the feature carries no capability delta,\n // which is every feature in a fleet that has not adopted the business axis —\n // and the removal half asks for the living corpus only when something is\n // actually retired, so even an adopting fleet pays the fleet-wide read on the\n // features that earn it.\n if (capabilityDeltas.length > 0) {\n // The three arms travel as three, and flattening two of them to `[]` is\n // the one thing this must not do: `capability.uncovered` says in words that\n // no flow keeps the promise, so handing it an empty list for a corpus loam\n // could not read or could not resolve makes it assert a negative it never\n // evaluated — and gate on it. `claims.ts`'s header states the rule; this is\n // where a caller could break it.\n issues.push(\n ...uncoveredIssues(\n capabilityDeltas,\n serviceDeltas,\n kept.kind === \"read\"\n ? { graded: true, kept: kept.kept }\n : { graded: false, why: kept.kind === \"ungraded\" ? kept.why : kept.errors.join(\"; \") },\n ),\n );\n issues.push(\n ...(await removedRealizedIssues({\n capabilities: capabilityDeltas,\n deltas: serviceDeltas,\n living: () => livingRealizers(docsDir, context),\n })),\n );\n }\n\n return issues;\n}\n\n/**\n * Every LIVING service requirements document in the fleet, for the one question\n * that has to ask about services this feature never mentions: does anything out\n * there still realize a capability requirement this feature retires?\n *\n * The same shape and the same cost `openapi.remove-op-consumed` pays through\n * the consumer index (`core/consumers/`) one axis over, and for the same reason — the merge\n * deletes the promise while the `Realizes:` line stays, so the very next\n * `validate --all` reports a breach on a repository whose author was never in\n * this feature. Called behind a thunk (`removedRealizedIssues` invokes it only\n * when something is retired), so a feature that removes nothing never runs it.\n */\nasync function livingRealizers(docsDir: DocsDir, context?: FleetContext): Promise<RealizingDoc[]> {\n const out: RealizingDoc[] = [];\n for (const service of await enumeratedServiceIds(docsDir, context)) {\n const paths = await locateServicePaths(docsDir, service, context);\n for (const axis of SPEC_AXES) {\n const path = paths[axis.key];\n if (!existsSync(path)) continue;\n const reqs =\n context === undefined ? parseRequirements(await readFile(path, \"utf8\")) : await context.readRequirements(path);\n out.push({ service, file: axis.file, reqs });\n }\n }\n return out;\n}\n\n/**\n * One delta document against one living document: the three passes, in the\n * order that makes their messages readable.\n *\n * The order is load-bearing. `deltaDocumentIssues` settles what the delta says\n * about itself, `livingDocumentIssues` settles whether the living text can be\n * selected in at all, and only then does `selectionIssues` compare them — a pin\n * the document pass already refused must not also be reported stale, which\n * would send its author to `loam rebase` for a problem rebase does not fix.\n *\n * The parsed delta requirements come back with the issues because the capability\n * corpus grades them a second way (`capabilityDocIssues`) and re-reading the\n * file to do it would be a second parse of bytes already in hand — and, without\n * a context, a second chance for the two passes to disagree about the document.\n * The LIVING index rides back for the same reason once more: the removal\n * direction of the `Realizes:` join has to resolve a REMOVED spelled by heading\n * into the id it retires, and that id is a fact about the living document.\n */\nasync function gradeDelta(\n scope: DeltaScope,\n livingPath: string,\n reads: DeltaReads,\n): Promise<{ issues: Issue[]; reqs: Requirement[]; living: LivingIndex }> {\n const { claims, context } = reads;\n const raw = context === undefined ? await readFile(scope.specPath, \"utf8\") : await context.readText(scope.specPath);\n const reqs = context === undefined ? parseRequirements(raw) : await context.readRequirements(scope.specPath);\n const living = indexLiving(\n !existsSync(livingPath)\n ? []\n : context === undefined\n ? parseRequirements(await readFile(livingPath, \"utf8\"))\n : await context.readRequirements(livingPath),\n );\n return {\n reqs,\n living,\n issues: [\n ...deltaDocumentIssues(scope, raw, reqs),\n ...livingDocumentIssues(scope, living),\n ...(await selectionIssues(scope, reqs, living, claims)),\n ],\n };\n}\n"]}
|
|
@@ -49,11 +49,11 @@
|
|
|
49
49
|
* preview, which is the one thing both refusals rest on.
|
|
50
50
|
*/
|
|
51
51
|
import { existsSync } from "node:fs";
|
|
52
|
-
import { basename, join
|
|
52
|
+
import { basename, join } from "node:path";
|
|
53
53
|
import { architectureDir } from "../c4/project/architecture.js";
|
|
54
54
|
import { stageMergedProject } from "../c4/project/staged.js";
|
|
55
55
|
import {} from "../c4/likec4.js";
|
|
56
|
-
import {
|
|
56
|
+
import { relativeDocuments } from "../c4/project/documents.js";
|
|
57
57
|
import { featureDeploymentDir } from "../repo/authored/paths.js";
|
|
58
58
|
import { reservedArchitectureName } from "../repo/paths.js";
|
|
59
59
|
import {} from "../vocabulary/issue.js";
|
|
@@ -66,10 +66,7 @@ import {} from "../vocabulary/issue.js";
|
|
|
66
66
|
* directory's existence is the whole per-feature opt-in.
|
|
67
67
|
*/
|
|
68
68
|
export async function featureDeployments(featureDir) {
|
|
69
|
-
|
|
70
|
-
const paths = await architectureDocuments(dir);
|
|
71
|
-
const base = resolve(dir);
|
|
72
|
-
return paths.map((path) => ({ rel: relative(base, path).split(/[\\/]/).join("/"), path }));
|
|
69
|
+
return relativeDocuments(featureDeploymentDir(featureDir));
|
|
73
70
|
}
|
|
74
71
|
/**
|
|
75
72
|
* The LIVING path a feature-local topology document addresses.
|