@ybotok/loam 0.2.0-alpha.7 → 0.2.0-alpha.9
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +1315 -1
- package/COMPARISON.md +91 -3
- package/CONTRIBUTING.md +37 -0
- package/MIGRATING-from-OpenSpec.md +40 -22
- package/QUICKSTART.md +181 -9
- package/README.md +256 -57
- package/ROADMAP.md +83 -7
- package/SCHEMA.md +569 -98
- package/WORKFLOW.md +272 -62
- package/dist/cli.js +26 -8
- package/dist/cli.js.map +1 -1
- package/dist/commands/adopt/adopt.js +5 -10
- package/dist/commands/adopt/adopt.js.map +1 -1
- package/dist/commands/archive/archive.js +15 -18
- package/dist/commands/archive/archive.js.map +1 -1
- package/dist/commands/archive/plan/contracts/asyncapi.js +27 -41
- package/dist/commands/archive/plan/contracts/asyncapi.js.map +1 -1
- package/dist/commands/archive/plan/contracts/consumers.js +56 -0
- package/dist/commands/archive/plan/contracts/consumers.js.map +1 -0
- package/dist/commands/archive/plan/contracts/openapi.js +25 -42
- package/dist/commands/archive/plan/contracts/openapi.js.map +1 -1
- package/dist/commands/archive/plan/contracts/verdict-lines.js +41 -0
- package/dist/commands/archive/plan/contracts/verdict-lines.js.map +1 -0
- package/dist/commands/archive/plan/gate.js +3 -7
- package/dist/commands/archive/plan/gate.js.map +1 -1
- package/dist/commands/archive/plan/landscape.js +4 -4
- package/dist/commands/archive/plan/landscape.js.map +1 -1
- package/dist/commands/archive/plan/refusal.js +8 -18
- package/dist/commands/archive/plan/refusal.js.map +1 -1
- package/dist/commands/archive/plan/specs.js +134 -25
- package/dist/commands/archive/plan/specs.js.map +1 -1
- package/dist/commands/archive/plan/state.js +1 -0
- package/dist/commands/archive/plan/state.js.map +1 -1
- package/dist/commands/archive/run.js +33 -11
- package/dist/commands/archive/run.js.map +1 -1
- package/dist/commands/archive/shipped.js +49 -0
- package/dist/commands/archive/shipped.js.map +1 -0
- package/dist/commands/context/context.js +4 -7
- package/dist/commands/context/context.js.map +1 -1
- package/dist/commands/context/print.js +2 -2
- package/dist/commands/context/print.js.map +1 -1
- package/dist/commands/delta/delta.js +44 -12
- package/dist/commands/delta/delta.js.map +1 -1
- package/dist/commands/delta/print.js +1 -1
- package/dist/commands/delta/print.js.map +1 -1
- package/dist/commands/dependencies.js +3 -7
- package/dist/commands/dependencies.js.map +1 -1
- package/dist/commands/diff/diff.js +20 -27
- package/dist/commands/diff/diff.js.map +1 -1
- package/dist/commands/explore.js +4 -8
- package/dist/commands/explore.js.map +1 -1
- package/dist/commands/gate/gate.js +45 -8
- package/dist/commands/gate/gate.js.map +1 -1
- package/dist/commands/gate/print.js +15 -3
- package/dist/commands/gate/print.js.map +1 -1
- package/dist/commands/gherkin/commit.js +13 -54
- package/dist/commands/gherkin/commit.js.map +1 -1
- package/dist/commands/gherkin/gherkin.js +36 -20
- package/dist/commands/gherkin/gherkin.js.map +1 -1
- package/dist/commands/gherkin/reconcile.js +10 -13
- package/dist/commands/gherkin/reconcile.js.map +1 -1
- package/dist/commands/init/first-hour.js +44 -0
- package/dist/commands/init/first-hour.js.map +1 -1
- package/dist/commands/init/init.js +18 -22
- package/dist/commands/init/init.js.map +1 -1
- package/dist/commands/list/campaign/campaign.js +4 -6
- package/dist/commands/list/campaign/campaign.js.map +1 -1
- package/dist/commands/list/json.js +0 -4
- package/dist/commands/list/json.js.map +1 -1
- package/dist/commands/list/list.js +38 -23
- package/dist/commands/list/list.js.map +1 -1
- package/dist/commands/list/print.js +38 -6
- package/dist/commands/list/print.js.map +1 -1
- package/dist/commands/list/suites/suites.js +140 -0
- package/dist/commands/list/suites/suites.js.map +1 -0
- package/dist/commands/list/views.js +28 -33
- package/dist/commands/list/views.js.map +1 -1
- package/dist/commands/mcp/dispatch.js +91 -36
- package/dist/commands/mcp/dispatch.js.map +1 -1
- package/dist/commands/mcp/mcp.js +56 -15
- package/dist/commands/mcp/mcp.js.map +1 -1
- package/dist/commands/mcp/serve.js.map +1 -1
- package/dist/commands/migrate-openspec/materialize/active.js +27 -22
- package/dist/commands/migrate-openspec/materialize/active.js.map +1 -1
- package/dist/commands/migrate-openspec/materialize/stage.js +4 -3
- package/dist/commands/migrate-openspec/materialize/stage.js.map +1 -1
- package/dist/commands/migrate-openspec/materialize/target.js +2 -2
- package/dist/commands/migrate-openspec/materialize/target.js.map +1 -1
- package/dist/commands/migrate-openspec/openspec/decisions.js +9 -14
- package/dist/commands/migrate-openspec/openspec/decisions.js.map +1 -1
- package/dist/commands/migrate-openspec/openspec/error.js +7 -8
- package/dist/commands/migrate-openspec/openspec/error.js.map +1 -1
- package/dist/commands/migrate-openspec/openspec/mapping.js +1 -9
- package/dist/commands/migrate-openspec/openspec/mapping.js.map +1 -1
- package/dist/commands/migrate-openspec/openspec/paths.js +3 -6
- package/dist/commands/migrate-openspec/openspec/paths.js.map +1 -1
- package/dist/commands/new/capability-template.js +118 -0
- package/dist/commands/new/capability-template.js.map +1 -0
- package/dist/commands/new/commit.js +8 -22
- package/dist/commands/new/commit.js.map +1 -1
- package/dist/commands/new/modifies.js +206 -0
- package/dist/commands/new/modifies.js.map +1 -0
- package/dist/commands/new/new.js +59 -14
- package/dist/commands/new/new.js.map +1 -1
- package/dist/commands/new/templates.js +48 -124
- package/dist/commands/new/templates.js.map +1 -1
- package/dist/commands/open.js +3 -7
- package/dist/commands/open.js.map +1 -1
- package/dist/commands/policy/gate.js +27 -0
- package/dist/commands/policy/gate.js.map +1 -1
- package/dist/commands/rebase/plan.js +2 -2
- package/dist/commands/rebase/plan.js.map +1 -1
- package/dist/commands/rebase/rebase.js +60 -35
- package/dist/commands/rebase/rebase.js.map +1 -1
- package/dist/commands/seed/commit.js +21 -25
- package/dist/commands/seed/commit.js.map +1 -1
- package/dist/commands/seed/seed.js +6 -9
- package/dist/commands/seed/seed.js.map +1 -1
- package/dist/commands/show/show.js +3 -7
- package/dist/commands/show/show.js.map +1 -1
- package/dist/commands/status/print.js +13 -4
- package/dist/commands/status/print.js.map +1 -1
- package/dist/commands/status/status.js +3 -7
- package/dist/commands/status/status.js.map +1 -1
- package/dist/commands/steps/steps.js +3 -7
- package/dist/commands/steps/steps.js.map +1 -1
- package/dist/commands/subsystem/edit.js +3 -5
- package/dist/commands/subsystem/edit.js.map +1 -1
- package/dist/commands/subsystem/move.js +5 -8
- package/dist/commands/subsystem/move.js.map +1 -1
- package/dist/commands/subsystem/query.js +2 -4
- package/dist/commands/subsystem/query.js.map +1 -1
- package/dist/commands/subsystem/subsystem.js +4 -7
- package/dist/commands/subsystem/subsystem.js.map +1 -1
- package/dist/commands/subsystem/sync.js +14 -41
- package/dist/commands/subsystem/sync.js.map +1 -1
- package/dist/commands/subsystem/txn/txn.js +17 -38
- package/dist/commands/subsystem/txn/txn.js.map +1 -1
- package/dist/commands/subsystem/txn/views.js +32 -5
- package/dist/commands/subsystem/txn/views.js.map +1 -1
- package/dist/commands/tasks/render.js +116 -0
- package/dist/commands/tasks/render.js.map +1 -0
- package/dist/commands/tasks/tasks.js +113 -0
- package/dist/commands/tasks/tasks.js.map +1 -0
- package/dist/commands/unarchive/manifest.js +6 -9
- package/dist/commands/unarchive/manifest.js.map +1 -1
- package/dist/commands/unarchive/restore.js +17 -28
- package/dist/commands/unarchive/restore.js.map +1 -1
- package/dist/commands/unarchive/unarchive.js +22 -30
- package/dist/commands/unarchive/unarchive.js.map +1 -1
- package/dist/commands/validate/arch-coverage.js +6 -15
- package/dist/commands/validate/arch-coverage.js.map +1 -1
- package/dist/commands/validate/checks/fleet-shape.js +4 -49
- package/dist/commands/validate/checks/fleet-shape.js.map +1 -1
- package/dist/commands/validate/checks/requirements.js +18 -8
- package/dist/commands/validate/checks/requirements.js.map +1 -1
- package/dist/commands/validate/checks/sources.js +6 -2
- package/dist/commands/validate/checks/sources.js.map +1 -1
- package/dist/commands/validate/feature.js +23 -19
- package/dist/commands/validate/feature.js.map +1 -1
- package/dist/commands/validate/fleet/landscape.js +23 -23
- package/dist/commands/validate/fleet/landscape.js.map +1 -1
- package/dist/commands/validate/fleet/map/attest.js +6 -10
- package/dist/commands/validate/fleet/map/attest.js.map +1 -1
- package/dist/commands/validate/fleet/map/bindings.js +1 -3
- package/dist/commands/validate/fleet/map/bindings.js.map +1 -1
- package/dist/commands/validate/fleet/map/usecases.js +5 -11
- package/dist/commands/validate/fleet/map/usecases.js.map +1 -1
- package/dist/commands/validate/fleet/obligations.js +1 -5
- package/dist/commands/validate/fleet/obligations.js.map +1 -1
- package/dist/commands/validate/fleet/scorecard/print.js +5 -1
- package/dist/commands/validate/fleet/scorecard/print.js.map +1 -1
- package/dist/commands/validate/fleet/scorecard/scorecard.js +17 -11
- package/dist/commands/validate/fleet/scorecard/scorecard.js.map +1 -1
- package/dist/commands/validate/fleet/usecases/usecases.js +6 -4
- package/dist/commands/validate/fleet/usecases/usecases.js.map +1 -1
- package/dist/commands/validate/fleet/views/fleet-project.js.map +1 -1
- package/dist/commands/validate/fleet/views/root-documents.js +8 -28
- package/dist/commands/validate/fleet/views/root-documents.js.map +1 -1
- package/dist/commands/validate/links/corpus.js +1 -4
- package/dist/commands/validate/links/corpus.js.map +1 -1
- package/dist/commands/validate/links/glossary.js.map +1 -1
- package/dist/commands/validate/report.js +5 -4
- package/dist/commands/validate/report.js.map +1 -1
- package/dist/commands/validate/scope.js +113 -0
- package/dist/commands/validate/scope.js.map +1 -0
- package/dist/commands/validate/service/api.js +16 -104
- package/dist/commands/validate/service/api.js.map +1 -1
- package/dist/commands/validate/service/contracts/generated.js +134 -0
- package/dist/commands/validate/service/contracts/generated.js.map +1 -0
- package/dist/commands/validate/service/events/events.js +1 -1
- package/dist/commands/validate/service/events/events.js.map +1 -1
- package/dist/commands/validate/service/events/message-producers.js +1 -1
- package/dist/commands/validate/service/events/message-producers.js.map +1 -1
- package/dist/commands/validate/service/evidence-pins.js +56 -10
- package/dist/commands/validate/service/evidence-pins.js.map +1 -1
- package/dist/commands/validate/service/service.js +16 -26
- package/dist/commands/validate/service/service.js.map +1 -1
- package/dist/commands/validate/service/specs.js +31 -38
- package/dist/commands/validate/service/specs.js.map +1 -1
- package/dist/commands/validate/service/spine.js +8 -11
- package/dist/commands/validate/service/spine.js.map +1 -1
- package/dist/commands/validate/service/usecases/usecases.js +3 -3
- package/dist/commands/validate/service/usecases/usecases.js.map +1 -1
- package/dist/commands/validate/validate.js +44 -26
- package/dist/commands/validate/validate.js.map +1 -1
- package/dist/commands/verify/cross/diff.js +2 -6
- package/dist/commands/verify/cross/diff.js.map +1 -1
- package/dist/commands/verify/evidence/contract.js +1 -1
- package/dist/commands/verify/evidence/contract.js.map +1 -1
- package/dist/commands/verify/evidence/read.js +12 -3
- package/dist/commands/verify/evidence/read.js.map +1 -1
- package/dist/commands/verify/frozen.js +45 -23
- package/dist/commands/verify/frozen.js.map +1 -1
- package/dist/commands/verify/record.js +69 -17
- package/dist/commands/verify/record.js.map +1 -1
- package/dist/commands/verify/report.js +27 -44
- package/dist/commands/verify/report.js.map +1 -1
- package/dist/commands/verify/results.js +112 -30
- package/dist/commands/verify/results.js.map +1 -1
- package/dist/commands/verify/verify.js +54 -51
- package/dist/commands/verify/verify.js.map +1 -1
- package/dist/commands/vouch/pack/pack.js +2 -2
- package/dist/commands/vouch/pack/pack.js.map +1 -1
- package/dist/commands/vouch/pack/print.js +1 -6
- package/dist/commands/vouch/pack/print.js.map +1 -1
- package/dist/commands/vouch/pack/sections.js +1 -2
- package/dist/commands/vouch/pack/sections.js.map +1 -1
- package/dist/commands/vouch/run.js +26 -81
- package/dist/commands/vouch/run.js.map +1 -1
- package/dist/commands/vouch/sample/plan.js +20 -34
- package/dist/commands/vouch/sample/plan.js.map +1 -1
- package/dist/commands/vouch/vet/verify.js +39 -7
- package/dist/commands/vouch/vet/verify.js.map +1 -1
- package/dist/commands/vouch/vouch.js +10 -17
- package/dist/commands/vouch/vouch.js.map +1 -1
- package/dist/core/agent/agents-md/command-map.js +56 -101
- package/dist/core/agent/agents-md/command-map.js.map +1 -1
- package/dist/core/agent/agents-md/map/explain.js +4 -3
- package/dist/core/agent/agents-md/map/explain.js.map +1 -1
- package/dist/core/agent/agents-md/map/lenses/context.js +4 -3
- package/dist/core/agent/agents-md/map/lenses/context.js.map +1 -1
- package/dist/core/agent/agents-md/map/lenses/diff.js +33 -20
- package/dist/core/agent/agents-md/map/lenses/diff.js.map +1 -1
- package/dist/core/agent/agents-md/map/lenses/gate.js +13 -5
- package/dist/core/agent/agents-md/map/lenses/gate.js.map +1 -1
- package/dist/core/agent/agents-md/map/mcp.js +10 -7
- package/dist/core/agent/agents-md/map/mcp.js.map +1 -1
- package/dist/core/agent/agents-md/map/status.js +9 -3
- package/dist/core/agent/agents-md/map/status.js.map +1 -1
- package/dist/core/agent/agents-md/map/subsystem.js +4 -3
- package/dist/core/agent/agents-md/map/subsystem.js.map +1 -1
- package/dist/core/agent/agents-md/map/usecases.js +24 -6
- package/dist/core/agent/agents-md/map/usecases.js.map +1 -1
- package/dist/core/agent/agents-md/refusals.js +32 -18
- package/dist/core/agent/agents-md/refusals.js.map +1 -1
- package/dist/core/agent/agents-md.js +9 -3
- package/dist/core/agent/agents-md.js.map +1 -1
- package/dist/core/agent/agents-stamp.js +10 -0
- package/dist/core/agent/agents-stamp.js.map +1 -1
- package/dist/core/agent/scaffold.js +10 -15
- package/dist/core/agent/scaffold.js.map +1 -1
- package/dist/core/agent/tools/dialects.js +1 -0
- package/dist/core/agent/tools/dialects.js.map +1 -1
- package/dist/core/agent/tools/registry.js +41 -76
- package/dist/core/agent/tools/registry.js.map +1 -1
- package/dist/core/agent/workflows/adopt.js +5 -3
- package/dist/core/agent/workflows/adopt.js.map +1 -1
- package/dist/core/agent/workflows/check.js +20 -16
- package/dist/core/agent/workflows/check.js.map +1 -1
- package/dist/core/agent/workflows/closing.js +54 -21
- package/dist/core/agent/workflows/closing.js.map +1 -1
- package/dist/core/agent/workflows/feature.js +79 -11
- package/dist/core/agent/workflows/feature.js.map +1 -1
- package/dist/core/agent/workflows/implement.js +91 -13
- package/dist/core/agent/workflows/implement.js.map +1 -1
- package/dist/core/agent/workflows/reference/authoring.js +3 -1
- package/dist/core/agent/workflows/reference/authoring.js.map +1 -1
- package/dist/core/agent/workflows/reference/codes.js +8 -6
- package/dist/core/agent/workflows/reference/codes.js.map +1 -1
- package/dist/core/agent/workflows/reference/done-check.js +37 -15
- package/dist/core/agent/workflows/reference/done-check.js.map +1 -1
- package/dist/core/agent/workflows/reference/reference.js.map +1 -1
- package/dist/core/agent/workflows/reference/spine.js +50 -18
- package/dist/core/agent/workflows/reference/spine.js.map +1 -1
- package/dist/core/asyncapi/baseline/plan.js +14 -81
- package/dist/core/asyncapi/baseline/plan.js.map +1 -1
- package/dist/core/asyncapi/digest.js +30 -25
- package/dist/core/asyncapi/digest.js.map +1 -1
- package/dist/core/asyncapi/merge/components.js +5 -4
- package/dist/core/asyncapi/merge/components.js.map +1 -1
- package/dist/core/asyncapi/merge/error.js +7 -2
- package/dist/core/asyncapi/merge/error.js.map +1 -1
- package/dist/core/asyncapi/merge/markers.js +30 -50
- package/dist/core/asyncapi/merge/markers.js.map +1 -1
- package/dist/core/asyncapi/merge/merge.js +31 -53
- package/dist/core/asyncapi/merge/merge.js.map +1 -1
- package/dist/core/asyncapi/merge/pin.js +10 -36
- package/dist/core/asyncapi/merge/pin.js.map +1 -1
- package/dist/core/asyncapi/model.js.map +1 -1
- package/dist/core/asyncapi/read.js +36 -12
- package/dist/core/asyncapi/read.js.map +1 -1
- package/dist/core/brief/brief.js +6 -18
- package/dist/core/brief/brief.js.map +1 -1
- package/dist/core/brief/landscape.js +8 -12
- package/dist/core/brief/landscape.js.map +1 -1
- package/dist/core/brief/map/extend.js +8 -5
- package/dist/core/brief/map/extend.js.map +1 -1
- package/dist/core/brief/map/owed.js +8 -26
- package/dist/core/brief/map/owed.js.map +1 -1
- package/dist/core/brief/targets.js +15 -5
- package/dist/core/brief/targets.js.map +1 -1
- package/dist/core/c4/likec4.js +147 -16
- package/dist/core/c4/likec4.js.map +1 -1
- package/dist/core/c4/locate/unlocated.js +150 -0
- package/dist/core/c4/locate/unlocated.js.map +1 -0
- package/dist/core/c4/parsed/values.js +54 -1
- package/dist/core/c4/parsed/values.js.map +1 -1
- package/dist/core/c4/prefetch/fleet.js +1 -9
- package/dist/core/c4/prefetch/fleet.js.map +1 -1
- package/dist/core/c4/project/documents.js +31 -6
- package/dist/core/c4/project/documents.js.map +1 -1
- package/dist/core/c4/project/load.js +6 -17
- package/dist/core/c4/project/load.js.map +1 -1
- package/dist/core/c4/resolve/external.js +51 -0
- package/dist/core/c4/resolve/external.js.map +1 -0
- package/dist/core/c4/resolve/service.js +25 -1
- package/dist/core/c4/resolve/service.js.map +1 -1
- package/dist/core/c4/seed/template.js +15 -1
- package/dist/core/c4/seed/template.js.map +1 -1
- package/dist/core/c4/service-model/shape.js +2 -22
- package/dist/core/c4/service-model/shape.js.map +1 -1
- package/dist/core/c4/source-mask.js +288 -4
- package/dist/core/c4/source-mask.js.map +1 -1
- package/dist/core/c4/source-scan.js +203 -147
- package/dist/core/c4/source-scan.js.map +1 -1
- package/dist/core/c4/splice/authored-source.js +19 -0
- package/dist/core/c4/splice/authored-source.js.map +1 -1
- package/dist/core/c4/splice/delta-blocks.js +5 -18
- package/dist/core/c4/splice/delta-blocks.js.map +1 -1
- package/dist/core/c4/splice/landscape-merge.js +13 -8
- package/dist/core/c4/splice/landscape-merge.js.map +1 -1
- package/dist/core/c4/splice/model/merge.js +10 -6
- package/dist/core/c4/splice/model/merge.js.map +1 -1
- package/dist/core/c4/splice/placement.js +108 -18
- package/dist/core/c4/splice/placement.js.map +1 -1
- package/dist/core/c4/workspace.js +13 -22
- package/dist/core/c4/workspace.js.map +1 -1
- package/dist/core/capabilities/capabilities.js +6 -6
- package/dist/core/capabilities/capabilities.js.map +1 -1
- package/dist/core/capabilities/delta/doc.js +7 -10
- package/dist/core/capabilities/delta/doc.js.map +1 -1
- package/dist/core/capabilities/findings.js +15 -20
- package/dist/core/capabilities/findings.js.map +1 -1
- package/dist/core/capabilities/realizes/findings.js +12 -19
- package/dist/core/capabilities/realizes/findings.js.map +1 -1
- package/dist/core/capabilities/realizes/join.js +0 -20
- package/dist/core/capabilities/realizes/join.js.map +1 -1
- package/dist/core/capabilities/rollup.js +6 -0
- package/dist/core/capabilities/rollup.js.map +1 -1
- package/dist/core/capabilities/tree.js +6 -28
- package/dist/core/capabilities/tree.js.map +1 -1
- package/dist/core/capabilities/usecase-join.js +3 -13
- package/dist/core/capabilities/usecase-join.js.map +1 -1
- package/dist/core/coherence/authoring/scaffold.js +11 -3
- package/dist/core/coherence/authoring/scaffold.js.map +1 -1
- package/dist/core/coherence/authoring/sentinels.js +2 -1
- package/dist/core/coherence/authoring/sentinels.js.map +1 -1
- package/dist/core/coherence/coherence.js +30 -16
- package/dist/core/coherence/coherence.js.map +1 -1
- package/dist/core/coherence/consumed/operations.js +70 -0
- package/dist/core/coherence/consumed/operations.js.map +1 -0
- package/dist/core/coherence/consumed/refusal.js +38 -0
- package/dist/core/coherence/consumed/refusal.js.map +1 -0
- package/dist/core/coherence/declared.js +60 -34
- package/dist/core/coherence/declared.js.map +1 -1
- package/dist/core/coherence/events/declared.js +44 -59
- package/dist/core/coherence/events/declared.js.map +1 -1
- package/dist/core/coherence/events/events.js +119 -8
- package/dist/core/coherence/events/events.js.map +1 -1
- package/dist/core/coherence/events/grades.js +31 -8
- package/dist/core/coherence/events/grades.js.map +1 -1
- package/dist/core/coherence/events/lookups.js +14 -82
- package/dist/core/coherence/events/lookups.js.map +1 -1
- package/dist/core/coherence/lookups.js +18 -78
- package/dist/core/coherence/lookups.js.map +1 -1
- package/dist/core/coherence/retired/archived.js +285 -0
- package/dist/core/coherence/retired/archived.js.map +1 -0
- package/dist/core/coherence/retired/event-pair.js +62 -0
- package/dist/core/coherence/retired/event-pair.js.map +1 -0
- package/dist/core/coherence/retired/merged.js +79 -0
- package/dist/core/coherence/retired/merged.js.map +1 -0
- package/dist/core/coherence/retired/retirement.js +49 -0
- package/dist/core/coherence/retired/retirement.js.map +1 -0
- package/dist/core/coherence/retired/revival/arrivals.js +131 -0
- package/dist/core/coherence/retired/revival/arrivals.js.map +1 -0
- package/dist/core/coherence/retired/revival/revived.js +297 -0
- package/dist/core/coherence/retired/revival/revived.js.map +1 -0
- package/dist/core/consumers/after.js +113 -0
- package/dist/core/consumers/after.js.map +1 -0
- package/dist/core/consumers/consumers.js +296 -0
- package/dist/core/consumers/consumers.js.map +1 -0
- package/dist/core/consumers/current.js +211 -0
- package/dist/core/consumers/current.js.map +1 -0
- package/dist/core/consumers/scan.js +46 -0
- package/dist/core/consumers/scan.js.map +1 -0
- package/dist/core/consumers/state.js +27 -0
- package/dist/core/consumers/state.js.map +1 -0
- package/dist/core/delta/delta.js +1 -1
- package/dist/core/delta/delta.js.map +1 -1
- package/dist/core/deployment/delta.js +3 -6
- package/dist/core/deployment/delta.js.map +1 -1
- package/dist/core/diff/base-git.js +94 -12
- package/dist/core/diff/base-git.js.map +1 -1
- package/dist/core/diff/base-state.js +28 -16
- package/dist/core/diff/base-state.js.map +1 -1
- package/dist/core/diff/joins.js +136 -0
- package/dist/core/diff/joins.js.map +1 -0
- package/dist/core/diff/scope/changed-paths.js +22 -6
- package/dist/core/diff/scope/changed-paths.js.map +1 -1
- package/dist/core/diff/scope/edges.js +82 -0
- package/dist/core/diff/scope/edges.js.map +1 -0
- package/dist/core/diff/semantic.js +100 -31
- package/dist/core/diff/semantic.js.map +1 -1
- package/dist/core/doctor/agents.js +34 -18
- package/dist/core/doctor/agents.js.map +1 -1
- package/dist/core/doctor/report.js.map +1 -1
- package/dist/core/document/apply.js +10 -2
- package/dist/core/document/apply.js.map +1 -1
- package/dist/core/document/frontmatter.js +55 -0
- package/dist/core/document/frontmatter.js.map +1 -1
- package/dist/core/document/parse.js +29 -6
- package/dist/core/document/parse.js.map +1 -1
- package/dist/core/document/scenarios.js +17 -0
- package/dist/core/document/scenarios.js.map +1 -1
- package/dist/core/document/spec.js +4 -11
- package/dist/core/document/spec.js.map +1 -1
- package/dist/core/envelope/config.js +10 -0
- package/dist/core/envelope/config.js.map +1 -1
- package/dist/core/envelope/json.js +34 -0
- package/dist/core/envelope/json.js.map +1 -1
- package/dist/core/explain/families.js +17 -12
- package/dist/core/explain/families.js.map +1 -1
- package/dist/core/explain/refusals.js +3 -3
- package/dist/core/explain/refusals.js.map +1 -1
- package/dist/core/explain/terms.js +18 -9
- package/dist/core/explain/terms.js.map +1 -1
- package/dist/core/explore/explore.js +2 -7
- package/dist/core/explore/explore.js.map +1 -1
- package/dist/core/fleet-context.js +34 -95
- package/dist/core/fleet-context.js.map +1 -1
- package/dist/core/gate/checks.js +5 -2
- package/dist/core/gate/checks.js.map +1 -1
- package/dist/core/gate/partners.js +4 -2
- package/dist/core/gate/partners.js.map +1 -1
- package/dist/core/gate/report.js.map +1 -1
- package/dist/core/gate/verification.js +75 -12
- package/dist/core/gate/verification.js.map +1 -1
- package/dist/core/gherkin/stale.js +13 -37
- package/dist/core/gherkin/stale.js.map +1 -1
- package/dist/core/glossary/tree.js +6 -19
- package/dist/core/glossary/tree.js.map +1 -1
- package/dist/core/kernel/concurrency.js +19 -0
- package/dist/core/kernel/concurrency.js.map +1 -1
- package/dist/core/kernel/records.js +11 -0
- package/dist/core/kernel/records.js.map +1 -1
- package/dist/core/links/backlinks.js +1 -3
- package/dist/core/links/backlinks.js.map +1 -1
- package/dist/core/links/corpus.js +5 -36
- package/dist/core/links/corpus.js.map +1 -1
- package/dist/core/mcp/argv.js +101 -0
- package/dist/core/mcp/argv.js.map +1 -1
- package/dist/core/mcp/author-tools.js +2 -1
- package/dist/core/mcp/author-tools.js.map +1 -1
- package/dist/core/mcp/protocol.js +2 -2
- package/dist/core/mcp/protocol.js.map +1 -1
- package/dist/core/mcp/tools.js +43 -33
- package/dist/core/mcp/tools.js.map +1 -1
- package/dist/core/openapi/baseline/gate.js +53 -12
- package/dist/core/openapi/baseline/gate.js.map +1 -1
- package/dist/core/openapi/baseline/plan.js +22 -8
- package/dist/core/openapi/baseline/plan.js.map +1 -1
- package/dist/core/openapi/baseline/record.js +0 -1
- package/dist/core/openapi/baseline/record.js.map +1 -1
- package/dist/core/openapi/contract-text.js +33 -0
- package/dist/core/openapi/contract-text.js.map +1 -0
- package/dist/core/openapi/contract-tree.js +56 -0
- package/dist/core/openapi/contract-tree.js.map +1 -0
- package/dist/core/openapi/depth.js +38 -24
- package/dist/core/openapi/depth.js.map +1 -1
- package/dist/core/openapi/digest.js +52 -2
- package/dist/core/openapi/digest.js.map +1 -1
- package/dist/core/openapi/doc.js +12 -4
- package/dist/core/openapi/doc.js.map +1 -1
- package/dist/core/openapi/merge/components.js +25 -60
- package/dist/core/openapi/merge/components.js.map +1 -1
- package/dist/core/openapi/merge/error.js +3 -13
- package/dist/core/openapi/merge/error.js.map +1 -1
- package/dist/core/openapi/merge/markers.js +26 -30
- package/dist/core/openapi/merge/markers.js.map +1 -1
- package/dist/core/openapi/merge/merge.js +26 -52
- package/dist/core/openapi/merge/merge.js.map +1 -1
- package/dist/core/openapi/merge/pin.js +15 -35
- package/dist/core/openapi/merge/pin.js.map +1 -1
- package/dist/core/openspec/decide/verdict.js.map +1 -1
- package/dist/core/openspec/inventory.js +4 -5
- package/dist/core/openspec/inventory.js.map +1 -1
- package/dist/core/openspec/model/mapping.js +30 -28
- package/dist/core/openspec/model/mapping.js.map +1 -1
- package/dist/core/openspec/model/model.js +12 -7
- package/dist/core/openspec/model/model.js.map +1 -1
- package/dist/core/openspec/scan/changes.js +10 -4
- package/dist/core/openspec/scan/changes.js.map +1 -1
- package/dist/core/openspec/scan/config.js +7 -16
- package/dist/core/openspec/scan/config.js.map +1 -1
- package/dist/core/openspec/scan/shape.js +63 -19
- package/dist/core/openspec/scan/shape.js.map +1 -1
- package/dist/core/openspec/scan/walk.js +21 -38
- package/dist/core/openspec/scan/walk.js.map +1 -1
- package/dist/core/pack/living.js +2 -16
- package/dist/core/pack/living.js.map +1 -1
- package/dist/core/provenance/findings.js +48 -12
- package/dist/core/provenance/findings.js.map +1 -1
- package/dist/core/provenance/git.js +33 -57
- package/dist/core/provenance/git.js.map +1 -1
- package/dist/core/provenance/gitq/moves.js +71 -114
- package/dist/core/provenance/gitq/moves.js.map +1 -1
- package/dist/core/provenance/gitq/vouched-ancestor.js +2 -2
- package/dist/core/provenance/gitq/vouched-ancestor.js.map +1 -1
- package/dist/core/provenance/sources.js +23 -6
- package/dist/core/provenance/sources.js.map +1 -1
- package/dist/core/provenance/stamp.js +5 -17
- package/dist/core/provenance/stamp.js.map +1 -1
- package/dist/core/repo/entries.js.map +1 -1
- package/dist/core/repo/repo.js +24 -14
- package/dist/core/repo/repo.js.map +1 -1
- package/dist/core/repo/tree/find.js +12 -3
- package/dist/core/repo/tree/find.js.map +1 -1
- package/dist/core/repo/tree/fs.js +27 -1
- package/dist/core/repo/tree/fs.js.map +1 -1
- package/dist/core/repo/tree/walk.js +8 -30
- package/dist/core/repo/tree/walk.js.map +1 -1
- package/dist/core/results.js +47 -12
- package/dist/core/results.js.map +1 -1
- package/dist/core/staging/commit.js +10 -12
- package/dist/core/staging/commit.js.map +1 -1
- package/dist/core/staging/lock.js +74 -12
- package/dist/core/staging/lock.js.map +1 -1
- package/dist/core/staging/recovery/intent.js +2 -5
- package/dist/core/staging/recovery/intent.js.map +1 -1
- package/dist/core/staging/recovery/recover.js +60 -1
- package/dist/core/staging/recovery/recover.js.map +1 -1
- package/dist/core/staging/snapshot/manifest.js +152 -0
- package/dist/core/staging/snapshot/manifest.js.map +1 -0
- package/dist/core/staging/{snapshot.js → snapshot/snapshot.js} +10 -34
- package/dist/core/staging/snapshot/snapshot.js.map +1 -0
- package/dist/core/status/actions/execution.js +11 -0
- package/dist/core/status/actions/execution.js.map +1 -1
- package/dist/core/status/feature/artifacts.js +2 -2
- package/dist/core/status/feature/artifacts.js.map +1 -1
- package/dist/core/status/feature/feature.js +5 -11
- package/dist/core/status/feature/feature.js.map +1 -1
- package/dist/core/status/fleet/fleet.js +6 -2
- package/dist/core/status/fleet/fleet.js.map +1 -1
- package/dist/core/status/fleet/next.js +26 -3
- package/dist/core/status/fleet/next.js.map +1 -1
- package/dist/core/status/report.js.map +1 -1
- package/dist/core/status/verification.js +30 -15
- package/dist/core/status/verification.js.map +1 -1
- package/dist/core/tasks/packet.js +174 -0
- package/dist/core/tasks/packet.js.map +1 -0
- package/dist/core/tasks/record.js +43 -0
- package/dist/core/tasks/record.js.map +1 -0
- package/dist/core/tasks/sources.js +181 -0
- package/dist/core/tasks/sources.js.map +1 -0
- package/dist/core/tasks/stamps.js +76 -0
- package/dist/core/tasks/stamps.js.map +1 -0
- package/dist/core/tasks/unit.js +0 -0
- package/dist/core/tasks/unit.js.map +1 -0
- package/dist/core/usecases/capability.js +11 -14
- package/dist/core/usecases/capability.js.map +1 -1
- package/dist/core/usecases/delta/flows.js +5 -7
- package/dist/core/usecases/delta/flows.js.map +1 -1
- package/dist/core/usecases/fleet.js +178 -31
- package/dist/core/usecases/fleet.js.map +1 -1
- package/dist/core/usecases/operations.js +5 -4
- package/dist/core/usecases/operations.js.map +1 -1
- package/dist/core/usecases/service/flows.js +9 -6
- package/dist/core/usecases/service/flows.js.map +1 -1
- package/dist/core/usecases/suites/participants.js +83 -0
- package/dist/core/usecases/suites/participants.js.map +1 -0
- package/dist/core/usecases/suites/suites.js +45 -0
- package/dist/core/usecases/suites/suites.js.map +1 -0
- package/dist/core/verify/answers.js +12 -5
- package/dist/core/verify/answers.js.map +1 -1
- package/dist/core/verify/build.js +102 -82
- package/dist/core/verify/build.js.map +1 -1
- package/dist/core/verify/checklist.js +3 -0
- package/dist/core/verify/checklist.js.map +1 -1
- package/dist/core/verify/location.js +67 -0
- package/dist/core/verify/location.js.map +1 -0
- package/dist/core/verify/pins/lint.js +3 -2
- package/dist/core/verify/pins/lint.js.map +1 -1
- package/dist/core/verify/pins/pin.js +1 -1
- package/dist/core/verify/pins/pin.js.map +1 -1
- package/dist/core/verify/read/file.js +224 -0
- package/dist/core/verify/read/file.js.map +1 -0
- package/dist/core/verify/{file.js → read/shape.js} +16 -104
- package/dist/core/verify/read/shape.js.map +1 -0
- package/dist/core/verify/read/union.js +105 -0
- package/dist/core/verify/read/union.js.map +1 -0
- package/dist/core/verify/record.js +35 -12
- package/dist/core/verify/record.js.map +1 -1
- package/dist/core/verify/store/commit.js +40 -30
- package/dist/core/verify/store/commit.js.map +1 -1
- package/dist/core/verify/store/render.js +8 -4
- package/dist/core/verify/store/render.js.map +1 -1
- package/dist/core/vocabulary/issue.js +1 -0
- package/dist/core/vocabulary/issue.js.map +1 -1
- package/dist/core/vocabulary/maturity.js +12 -2
- package/dist/core/vocabulary/maturity.js.map +1 -1
- package/examples/README.md +21 -6
- package/examples/docs/architecture/landscape.likec4 +8 -0
- package/examples/docs/architecture/usecases/order-notification.likec4 +6 -2
- package/examples/docs/features/FEAT-101-payment-splitting/specs/payment-service/asyncapi.yaml +11 -8
- package/examples/docs/features/FEAT-101-payment-splitting/specs/payment-split-service/openapi.yaml +6 -6
- package/examples/docs/features/archive/FEAT-088-refunds/specs/payment-service/openapi.yaml +13 -10
- package/examples/docs/services/order-service/asyncapi.yaml +3 -2
- package/examples/docs/services/platform/notification-service/asyncapi.yaml +2 -1
- package/package.json +1 -1
- package/dist/commands/validate/checks/vocabulary.js +0 -16
- package/dist/commands/validate/checks/vocabulary.js.map +0 -1
- package/dist/core/agent/agents-md/artifacts.js +0 -221
- package/dist/core/agent/agents-md/artifacts.js.map +0 -1
- package/dist/core/agent/agents-md/cycle.js +0 -180
- package/dist/core/agent/agents-md/cycle.js.map +0 -1
- package/dist/core/agent/agents-md/shipped/archive-gate.js +0 -98
- package/dist/core/agent/agents-md/shipped/archive-gate.js.map +0 -1
- package/dist/core/agent/agents-md/spine.js +0 -40
- package/dist/core/agent/agents-md/spine.js.map +0 -1
- package/dist/core/coherence/events/merged.js +0 -40
- package/dist/core/coherence/events/merged.js.map +0 -1
- package/dist/core/diff/current-state.js +0 -96
- package/dist/core/diff/current-state.js.map +0 -1
- package/dist/core/diff/victims.js +0 -256
- package/dist/core/diff/victims.js.map +0 -1
- package/dist/core/staging/snapshot.js.map +0 -1
- package/dist/core/verify/file.js.map +0 -1
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"tree.js","sourceRoot":"","sources":["../../../src/core/capabilities/tree.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAkCG;AACH,OAAO,EAAE,UAAU,EAAE,YAAY,EAAE,MAAM,SAAS,CAAC;AACnD,OAAO,EAAE,OAAO,EAAE,MAAM,kBAAkB,CAAC;AAC3C,OAAO,EAAE,IAAI,EAAE,MAAM,WAAW,CAAC;AACjC,OAAO,EAAE,UAAU,EAAE,MAAM,oBAAoB,CAAC;AAChD,OAAO,EAAE,oBAAoB,EAAE,MAAM,2BAA2B,CAAC;AACjE,OAAO,EAAE,OAAO,EAAE,MAAM,oBAAoB,CAAC;AA0C7C;;;;;;;;;GASG;AACH,MAAM,CAAC,KAAK,UAAU,kBAAkB,CAAC,IAAY;IACnD,MAAM,IAAI,GAAmB,EAAE,OAAO,EAAE,UAAU,CAAC,IAAI,CAAC,EAAE,IAAI,EAAE,EAAE,EAAE,YAAY,EAAE,EAAE,EAAE,CAAC;IACvF,IAAI,CAAC,IAAI,CAAC,OAAO;QAAE,OAAO,IAAI,CAAC;IAC/B,MAAM,KAAK,GAAc,EAAE,IAAI,EAAE,OAAO,EAAE,IAAI,GAAG,EAAE,EAAE,CAAC;IACtD,IAAI,SAAS,CAAC,IAAI,EAAE,KAAK,CAAC,EAAE,CAAC;QAC3B,KAAK,MAAM,IAAI,IAAI,MAAM,WAAW,CAAC,IAAI,CAAC,EAAE,CAAC;YAC3C,MAAM,KAAK,CAAC,EAAE,GAAG,EAAE,IAAI,CAAC,IAAI,EAAE,IAAI,CAAC,EAAE,KAAK,EAAE,CAAC,IAAI,CAAC,EAAE,EAAE,KAAK,CAAC,CAAC;QAC/D,CAAC;IACH,CAAC;IACD,IAAI,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC,CAAC,EAAE,CAAC,EAAE,EAAE,CAAC,UAAU,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC;IACjD,IAAI,CAAC,YAAY,CAAC,IAAI,CAAC,UAAU,CAAC,CAAC;IACnC,OAAO,IAAI,CAAC;AACd,CAAC;AAED;;;;;GAKG;AACH,SAAS,SAAS,CAAC,GAAW,EAAE,KAAgB;IAC9C,IAAI,IAAY,CAAC;IACjB,IAAI,CAAC;QACH,IAAI,GAAG,YAAY,CAAC,GAAG,CAAC,CAAC;IAC3B,CAAC;IAAC,MAAM,CAAC;QACP,OAAO,KAAK,CAAC;IACf,CAAC;IACD,IAAI,KAAK,CAAC,OAAO,CAAC,GAAG,CAAC,IAAI,CAAC;QAAE,OAAO,KAAK,CAAC;IAC1C,KAAK,CAAC,OAAO,CAAC,GAAG,CAAC,IAAI,CAAC,CAAC;IACxB,OAAO,IAAI,CAAC;AACd,CAAC;AAED,gFAAgF;AAChF,KAAK,UAAU,WAAW,CAAC,GAAW;IACpC,MAAM,OAAO,GAAG,MAAM,OAAO,CAAC,GAAG,EAAE,EAAE,aAAa,EAAE,IAAI,EAAE,CAAC,CAAC;IAC5D,OAAO,OAAO,CAAC,MAAM,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,CAAC,IAAI,CAAC,UAAU,CAAC,GAAG,CAAC,IAAI,OAAO,CAAC,GAAG,EAAE,CAAC,EAAE,KAAK,CAAC,CAAC,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,IAAI,CAAC,CAAC,IAAI,EAAE,CAAC;AAC5G,CAAC;AAED;;;;;;;;;;GAUG;AACH,KAAK,UAAU,KAAK,CAAC,IAAU,EAAE,KAAgB;IAC/C,MAAM,OAAO,GAAG,CAAC,MAAM,OAAO,CAAC,IAAI,CAAC,GAAG,EAAE,EAAE,aAAa,EAAE,IAAI,EAAE,CAAC,CAAC,CAAC,MAAM,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,CAAC,IAAI,CAAC,UAAU,CAAC,GAAG,CAAC,CAAC,CAAC;IAC1G,MAAM,KAAK,GAAG,oBAAoB,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC;IAC7C,MAAM,UAAU,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,IAAI,CAAC,IAAI,CAAC,GAAG,EAAE,CAAC,CAAC,IAAI,CAAC,KAAK,KAAK,CAAC,IAAI,IAAI,OAAO,CAAC,IAAI,CAAC,GAAG,EAAE,CAAC,EAAE,MAAM,CAAC,CAAC,CAAC;IAC9G,IAAI,UAAU;QAAE,KAAK,CAAC,IAAI,CAAC,IAAI,CAAC,IAAI,CAAC,EAAE,EAAE,EAAE,IAAI,CAAC,KAAK,CAAC,IAAI,CAAC,GAAG,CAAC,EAAE,GAAG,EAAE,IAAI,CAAC,GAAG,EAAE,IAAI,EAAE,KAAK,CAAC,IAAI,EAAE,CAAC,CAAC;IAEpG,IAAI,OAAO,GAAG,KAAK,CAAC;IACpB,IAAI,SAAS,CAAC,IAAI,CAAC,GAAG,EAAE,KAAK,CAAC,EAAE,CAAC;QAC/B,MAAM,IAAI,GAAG,OAAO,CAAC,MAAM,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,OAAO,CAAC,IAAI,CAAC,GAAG,EAAE,CAAC,EAAE,KAAK,CAAC,CAAC,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,IAAI,CAAC,CAAC,IAAI,EAAE,CAAC;QAC1F,KAAK,MAAM,KAAK,IAAI,IAAI,EAAE,CAAC;YACzB,2EAA2E;YAC3E,sEAAsE;YACtE,2EAA2E;YAC3E,0CAA0C;YAC1C,IAAI,MAAM,KAAK,CAAC,EAAE,GAAG,EAAE,IAAI,CAAC,IAAI,CAAC,GAAG,EAAE,KAAK,CAAC,EAAE,KAAK,EAAE,CAAC,GAAG,IAAI,CAAC,KAAK,EAAE,KAAK,CAAC,EAAE,EAAE,KAAK,CAAC;gBAAE,OAAO,GAAG,IAAI,CAAC;QACxG,CAAC;IACH,CAAC;SAAM,CAAC;QACN,OAAO,GAAG,IAAI,CAAC;IACjB,CAAC;IAED,IAAI,CAAC,UAAU,IAAI,CAAC,OAAO;QAAE,KAAK,CAAC,IAAI,CAAC,YAAY,CAAC,IAAI,CAAC,CAAC,cAAc,EAAE,GAAG,IAAI,CAAC,KAAK,CAAC,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC,CAAC;IACrG,OAAO,UAAU,IAAI,OAAO,CAAC;AAC/B,CAAC","sourcesContent":["/**\n * The capability tree: `capabilities/<id>/spec.md`, and the rule that a\n * directory holding one IS a declared capability.\n *\n * WHY A TREE AND NOT A SECOND LIST IN YAML. `architecture/capabilities.yaml`\n * declares names; it cannot carry prose, and a capability a business analyst\n * actually wrote is mostly prose. The alternative — a single vocabulary file\n * grown to hold paragraphs — is the exact drift `loam init`'s removed\n * `loam.docs.json` was removed for: a second list nothing keeps current. So the\n * DIRECTORY is the list, and the general rule it settles is that an entry with\n * prose gets a file while an entry without prose stays a line in YAML\n * (`architecture/permissions.yaml` is the principled exception — a\n * `user/profile:read` pair has no prose, and a document per pair is ceremony).\n *\n * THE VOCABULARY IS THE UNION of this tree and the YAML, and the YAML is not\n * deprecated by it. A fleet that declared twenty names and has written four\n * documents is the normal state of an adoption, and a tree that refused to\n * grade the other sixteen would make adopting the axis a cliff.\n * `./capabilities.ts` merges the two and records which side declared each id.\n *\n * PRESENCE CLASSIFIES, one tree over from `isServiceArtifactName`: a directory\n * holding the document is a capability, and one holding only other directories\n * is a group the walk descends through. Both at once is legal and normal —\n * `payments` may be a capability with `payments/refunds` nested inside it —\n * because a capability id spells its own nesting wherever it is written and the\n * tree only mirrors that. What is NOT legal is a directory that is neither:\n * `capability.doc-missing` names it, rather than letting a half-created\n * capability sit in the tree looking declared.\n *\n * The walk reads directories only. It never parses a document — the requirement\n * reader is injected by whoever grades (`./findings.ts` takes parsed\n * requirements, `commands/validate/` supplies the fleet's cached reader), for\n * the reason `./rollup.ts` states: this package is imported by\n * `core/fleet-context.ts` and may never import it back.\n */\nimport { existsSync, realpathSync } from \"node:fs\";\nimport { readdir } from \"node:fs/promises\";\nimport { join } from \"node:path\";\nimport { compareIds } from \"../repo/entries.js\";\nimport { capabilityDocPathsAt } from \"../repo/authored/paths.js\";\nimport { entryIs } from \"../repo/tree/fs.js\";\n\n/** One authored capability: where its directory is, and where its document is. */\nexport interface CapabilityDoc {\n /** The id — the directory names between `capabilities/` and this one, joined with `/`. */\n id: string;\n dir: string;\n /** `<dir>/spec.md`. Its existence is what made this a capability. */\n spec: string;\n}\n\nexport interface CapabilityTree {\n /**\n * False when `capabilities/` does not exist. The directory's existence is\n * this half of the axis's opt-in, so a fleet without one produces nothing\n * here — not an empty tree that then reads as \"the capabilities went\n * missing\" to anything counting.\n */\n present: boolean;\n /** Every capability found, at any depth, ordered by id. */\n docs: CapabilityDoc[];\n /**\n * Repo-relative paths of directories under `capabilities/` that hold no\n * document and have no capability beneath them — the half-created capability\n * `capability.doc-missing` names. Ordered, for a diff-stable finding list.\n */\n undocumented: string[];\n}\n\n/** One directory the walk is looking at: where it is, and how it was reached. */\ninterface Spot {\n dir: string;\n /** Directory names between `capabilities/` and this one, outermost first, including it. */\n chain: string[];\n}\n\ninterface WalkState {\n tree: CapabilityTree;\n /** Realpaths already DESCENDED into — the symlink-cycle guard. */\n visited: Set<string>;\n}\n\n/**\n * Read the tree under `root` (`capabilityDocsDir(docsDir)`).\n *\n * Cost is one readdir per directory, capability directories included: their own\n * listing is what classifies them, and it is the same listing that finds the\n * capabilities nested inside. A fleet is expected to hold tens of these\n * documents rather than hundreds — the corpus is sized like the landscape, not\n * like the service tree — which is what keeps a second requirement corpus\n * reviewable at all, and what makes the walk's cost a non-question.\n */\nexport async function readCapabilityTree(root: string): Promise<CapabilityTree> {\n const tree: CapabilityTree = { present: existsSync(root), docs: [], undocumented: [] };\n if (!tree.present) return tree;\n const state: WalkState = { tree, visited: new Set() };\n if (descended(root, state)) {\n for (const name of await subdirNames(root)) {\n await visit({ dir: join(root, name), chain: [name] }, state);\n }\n }\n tree.docs.sort((a, b) => compareIds(a.id, b.id));\n tree.undocumented.sort(compareIds);\n return tree;\n}\n\n/**\n * Mark a directory as descended-into; false when it (or another name for it)\n * already was. The same guard `core/repo/tree/walk.ts` carries and for the same\n * reason: `entryIs` follows symlinks on purpose, and a followed cycle without a\n * visited set recurses until the stack ends.\n */\nfunction descended(dir: string, state: WalkState): boolean {\n let real: string;\n try {\n real = realpathSync(dir);\n } catch {\n return false;\n }\n if (state.visited.has(real)) return false;\n state.visited.add(real);\n return true;\n}\n\n/** Sorted non-dot subdirectory names — the walk's deterministic child order. */\nasync function subdirNames(dir: string): Promise<string[]> {\n const entries = await readdir(dir, { withFileTypes: true });\n return entries.filter((e) => !e.name.startsWith(\".\") && entryIs(dir, e, \"dir\")).map((e) => e.name).sort();\n}\n\n/**\n * Classify one directory and walk what is inside it. Returns whether a\n * capability was found at or beneath it — the only thing a caller needs, since\n * a group directory earns no finding for being a group.\n *\n * A directory reached a SECOND time through a symlink answers `true` rather\n * than `false`, and the distinction is a quiet enumeration against a spurious\n * finding: it was already walked under its first name, so everything in it is\n * already recorded, and reporting it undocumented would name a directory whose\n * document loam has in fact already read.\n */\nasync function visit(spot: Spot, state: WalkState): Promise<boolean> {\n const entries = (await readdir(spot.dir, { withFileTypes: true })).filter((e) => !e.name.startsWith(\".\"));\n const paths = capabilityDocPathsAt(spot.dir);\n const documented = entries.some((e) => join(spot.dir, e.name) === paths.spec && entryIs(spot.dir, e, \"file\"));\n if (documented) state.tree.docs.push({ id: spot.chain.join(\"/\"), dir: spot.dir, spec: paths.spec });\n\n let beneath = false;\n if (descended(spot.dir, state)) {\n const dirs = entries.filter((e) => entryIs(spot.dir, e, \"dir\")).map((e) => e.name).sort();\n for (const child of dirs) {\n // Sequential, not Promise.all: the visited set is shared mutable state and\n // the walk's correctness under symlinks rests on a child being marked\n // before its sibling asks. These trees are tens of directories at most, so\n // there is nothing to win by racing them.\n if (await visit({ dir: join(spot.dir, child), chain: [...spot.chain, child] }, state)) beneath = true;\n }\n } else {\n beneath = true;\n }\n\n if (!documented && !beneath) state.tree.undocumented.push([\"capabilities\", ...spot.chain].join(\"/\"));\n return documented || beneath;\n}\n"]}
|
|
1
|
+
{"version":3,"file":"tree.js","sourceRoot":"","sources":["../../../src/core/capabilities/tree.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * The capability tree: `capabilities/<id>/spec.md`, and the rule that a\n * directory holding one IS a declared capability.\n *\n * WHY A TREE AND NOT A SECOND LIST IN YAML. `architecture/capabilities.yaml`\n * declares names; it cannot carry prose, and a capability a business analyst\n * actually wrote is mostly prose. The alternative — a single vocabulary file\n * grown to hold paragraphs — is the exact drift `loam init`'s removed\n * `loam.docs.json` was removed for: a second list nothing keeps current. So the\n * DIRECTORY is the list, and the general rule it settles is that an entry with\n * prose gets a file while an entry without prose stays a line in YAML\n * (`architecture/permissions.yaml` is the principled exception — a\n * `user/profile:read` pair has no prose, and a document per pair is ceremony).\n *\n * THE VOCABULARY IS THE UNION of this tree and the YAML, and the YAML is not\n * deprecated by it. A fleet that declared twenty names and has written four\n * documents is the normal state of an adoption, and a tree that refused to\n * grade the other sixteen would make adopting the axis a cliff.\n * `./capabilities.ts` merges the two and records which side declared each id.\n *\n * PRESENCE CLASSIFIES, one tree over from `isServiceArtifactName`: a directory\n * holding the document is a capability, and one holding only other directories\n * is a group the walk descends through. Both at once is legal and normal —\n * `payments` may be a capability with `payments/refunds` nested inside it —\n * because a capability id spells its own nesting wherever it is written and the\n * tree only mirrors that. What is NOT legal is a directory that is neither:\n * `capability.doc-missing` names it, rather than letting a half-created\n * capability sit in the tree looking declared.\n *\n * The walk reads directories only. It never parses a document — the requirement\n * reader is injected by whoever grades (`./findings.ts` takes parsed\n * requirements, `commands/validate/` supplies the fleet's cached reader), for\n * the reason `./rollup.ts` states: this package is imported by\n * `core/fleet-context.ts` and may never import it back.\n */\nimport { existsSync } from \"node:fs\";\nimport { readdir } from \"node:fs/promises\";\nimport { join } from \"node:path\";\nimport { compareIds } from \"../repo/entries.js\";\nimport { capabilityDocPathsAt } from \"../repo/authored/paths.js\";\nimport { entryIs, markDescended, subdirNames } from \"../repo/tree/fs.js\";\n\n/** One authored capability: where its directory is, and where its document is. */\nexport interface CapabilityDoc {\n /** The id — the directory names between `capabilities/` and this one, joined with `/`. */\n id: string;\n dir: string;\n /** `<dir>/spec.md`. Its existence is what made this a capability. */\n spec: string;\n}\n\nexport interface CapabilityTree {\n /**\n * False when `capabilities/` does not exist. The directory's existence is\n * this half of the axis's opt-in, so a fleet without one produces nothing\n * here — not an empty tree that then reads as \"the capabilities went\n * missing\" to anything counting.\n */\n present: boolean;\n /** Every capability found, at any depth, ordered by id. */\n docs: CapabilityDoc[];\n /**\n * Repo-relative paths of directories under `capabilities/` that hold no\n * document and have no capability beneath them — the half-created capability\n * `capability.doc-missing` names. Ordered, for a diff-stable finding list.\n */\n undocumented: string[];\n}\n\n/** One directory the walk is looking at: where it is, and how it was reached. */\ninterface Spot {\n dir: string;\n /** Directory names between `capabilities/` and this one, outermost first, including it. */\n chain: string[];\n}\n\ninterface WalkState {\n tree: CapabilityTree;\n /** Realpaths already DESCENDED into — the symlink-cycle guard. */\n visited: Set<string>;\n}\n\n/**\n * Read the tree under `root` (`capabilityDocsDir(docsDir)`).\n *\n * Cost is one readdir per directory, capability directories included: their own\n * listing is what classifies them, and it is the same listing that finds the\n * capabilities nested inside. A fleet is expected to hold tens of these\n * documents rather than hundreds — the corpus is sized like the landscape, not\n * like the service tree — which is what keeps a second requirement corpus\n * reviewable at all, and what makes the walk's cost a non-question.\n */\nexport async function readCapabilityTree(root: string): Promise<CapabilityTree> {\n const tree: CapabilityTree = { present: existsSync(root), docs: [], undocumented: [] };\n if (!tree.present) return tree;\n const state: WalkState = { tree, visited: new Set() };\n // The same guard `core/repo/tree/walk.ts` carries (`markDescended`), for\n // the same reason: `entryIs` follows symlinks on purpose.\n if (markDescended(root, state.visited)) {\n for (const name of await subdirNames(root)) {\n await visit({ dir: join(root, name), chain: [name] }, state);\n }\n }\n tree.docs.sort((a, b) => compareIds(a.id, b.id));\n tree.undocumented.sort(compareIds);\n return tree;\n}\n\n/**\n * Classify one directory and walk what is inside it. Returns whether a\n * capability was found at or beneath it — the only thing a caller needs, since\n * a group directory earns no finding for being a group.\n *\n * A directory reached a SECOND time through a symlink answers `true` rather\n * than `false`, and the distinction is a quiet enumeration against a spurious\n * finding: it was already walked under its first name, so everything in it is\n * already recorded, and reporting it undocumented would name a directory whose\n * document loam has in fact already read.\n */\nasync function visit(spot: Spot, state: WalkState): Promise<boolean> {\n const entries = (await readdir(spot.dir, { withFileTypes: true })).filter((e) => !e.name.startsWith(\".\"));\n const paths = capabilityDocPathsAt(spot.dir);\n const documented = entries.some((e) => join(spot.dir, e.name) === paths.spec && entryIs(spot.dir, e, \"file\"));\n if (documented) state.tree.docs.push({ id: spot.chain.join(\"/\"), dir: spot.dir, spec: paths.spec });\n\n let beneath = false;\n if (markDescended(spot.dir, state.visited)) {\n const dirs = entries.filter((e) => entryIs(spot.dir, e, \"dir\")).map((e) => e.name).sort();\n for (const child of dirs) {\n // Sequential, not Promise.all: the visited set is shared mutable state and\n // the walk's correctness under symlinks rests on a child being marked\n // before its sibling asks. These trees are tens of directories at most, so\n // there is nothing to win by racing them.\n if (await visit({ dir: join(spot.dir, child), chain: [...spot.chain, child] }, state)) beneath = true;\n }\n } else {\n beneath = true;\n }\n\n if (!documented && !beneath) state.tree.undocumented.push([\"capabilities\", ...spot.chain].join(\"/\"));\n return documented || beneath;\n}\n"]}
|
|
@@ -256,22 +256,12 @@ export function resolveRequirementTags(tags, scope, requirementsOf) {
|
|
|
256
256
|
}
|
|
257
257
|
/**
|
|
258
258
|
* Close ids for a slug nothing in the document matches — suggested against the
|
|
259
|
-
* SLUGS, then mapped back to the real ids
|
|
260
|
-
*
|
|
259
|
+
* SLUGS, then mapped back to the real ids: `closeCapabilityIds` over this
|
|
260
|
+
* document's ids, for its two reasons: comparing on slugs is what lets a typo reach an
|
|
261
261
|
* id whose awkward character sits where the typo does not, and mapping back is
|
|
262
262
|
* what keeps the suggestion writable.
|
|
263
263
|
*/
|
|
264
264
|
function closeRequirementIds(slug, ids) {
|
|
265
|
-
|
|
266
|
-
for (const id of ids) {
|
|
267
|
-
const key = tagSlug(id);
|
|
268
|
-
const found = bySlug.get(key);
|
|
269
|
-
if (found === undefined)
|
|
270
|
-
bySlug.set(key, [id]);
|
|
271
|
-
else
|
|
272
|
-
found.push(id);
|
|
273
|
-
}
|
|
274
|
-
const near = closeIds(slug, [...bySlug.keys()]);
|
|
275
|
-
return [...new Set(near.flatMap((s) => bySlug.get(s) ?? []))].sort(compareIds);
|
|
265
|
+
return closeCapabilityIds(slug, idsBySlug([...ids]));
|
|
276
266
|
}
|
|
277
267
|
//# sourceMappingURL=usecase-join.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"usecase-join.js","sourceRoot":"","sources":["../../../src/core/capabilities/usecase-join.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * The joins from a use case's tags: back to a declared capability, and to one\n * of that capability's requirements.\n *\n * TWO TAGS, ONE CLAIM. `#cap-<slug>` says which capability a flow is about;\n * `#req-<slug>` says which of its named promises the flow satisfies. The second\n * is the architect's ANSWER to the analyst's requirement, and it is the only\n * carrier a cross-service criterion has: \"I enter a login and a password and I\n * am in\" cannot be promised by any single service's spec, because each promises\n * only its own part — but a flow can, because it IS the hop sequence. That is\n * why `Realizes:` on a service requirement is not enough on its own, and why\n * this join exists at all.\n *\n * THE SECOND TAG IS SCOPED BY THE FIRST. A `Requirement-ID` is unique only\n * inside its own document, so `#req-CHK-ONCE` means nothing until the view has\n * said which capability it is about. A view carrying `#req-` with no resolved\n * `#cap-`, or with two, is refused rather than guessed at — the same stance the\n * `many` arm takes one join down.\n *\n * A LikeC4 `dynamic view` opts in to being graded as a business use case by\n * carrying a tag `#cap-<slug>`, and this module is the whole of what both tags\n * mean. It is deliberately the narrowest layer that can mean it: no file is\n * read, no finding is emitted, and nothing is thrown — a pure function over ids\n * somebody else supplies. That last property is not tidiness. Its caller runs\n * inside `loam validate --all` over every document a fleet has, and the one\n * behaviour a check added to that run may never have is failing the run it was\n * added to.\n *\n * WHY A SLUG AT ALL. Neither id is spelled in characters a tag name accepts. A\n * capability id spells its nesting inside the id — `identity/tokens` and\n * `payments/settlement` both ship in loam's own\n * `examples/docs/architecture/capabilities.yaml`, and `./capabilities.ts` keeps\n * the key exactly as the requirement writes it, slashes preserved — while a\n * `Requirement-ID` may legally contain a `.`. A LikeC4 tag name accepts exactly\n * `[A-Za-z0-9_-]` (measured at the 1.59.2 pin, pinned in\n * `test/likec4-view-shape.test.ts`), so both tags carry their id with the rest\n * flattened and both joins run over the flattened form.\n *\n * WHY THE JOIN CAN ANSWER \"MANY\". The slug is NOT injective. `payments/refunds`\n * and `payments-refunds` are two distinct, legal, declarable ids that flatten to\n * one slug, so `#cap-payments-refunds` names both and nothing in the tag can say\n * which. loam refuses and names ambiguity rather than guessing, and here the\n * cost of guessing is unusually high: a silently-picked capability attributes a\n * whole business flow — every hop, every service, every rollup built over it —\n * to the wrong owner, and goes on doing so invisibly because the wrong answer\n * looks exactly like the right one. The \"many\" arm is why this returns a union\n * at all instead of `string | undefined`.\n *\n * WHAT THE CALLER STILL OWES. The vocabulary ladder in `./findings.ts` — an\n * absent or unreadable `architecture/capabilities.yaml` means total silence, not\n * a fleet full of unresolved tags — is applied BEFORE calling here. An empty\n * `declared` list at this boundary therefore means \"the fleet declares no\n * capabilities\", which is a real answer this module will happily grade; it does\n * not mean \"there is no vocabulary\", and this module cannot tell the two apart.\n */\nimport { closeIds } from \"../c4/arch.js\";\nimport { compareIds } from \"../repo/entries.js\";\n\n/**\n * The reserved tag prefix, matched EXACTLY — case included.\n *\n * A view tag that does not start with it is not a capability claim and is\n * invisible here: `#FEAT-101`, `#platform` and `#deprecated` are the author's\n * own vocabulary and grading them would turn every existing hand-drawn view in\n * a brownfield fleet red on upgrade. The match is case-sensitive because what\n * survives it is a slug that must round-trip to a `capabilities.yaml` key, and\n * those keys are case-sensitive YAML; accepting `#Cap-checkout` here would hand\n * the lookup a slug whose case loam had already stopped believing in.\n */\nexport const CAP_TAG_PREFIX = \"cap-\";\n\n/**\n * Every character a LikeC4 tag name accepts, at the 1.59.2 pin. A WHITELIST,\n * and that is the load-bearing choice: the complement is infinite, so a rule\n * written as \"flatten a slash\" is a rule that is right about one character and\n * silent about the rest.\n *\n * Measured, not assumed (`test/likec4-view-shape.test.ts` pins it): `.`, `/`,\n * `:`, a space, `+`, `@` and a non-ASCII letter each make the DECLARATION a\n * parse error — and the failure is worse than a refusal, because the parser\n * TRUNCATES the name at the offending character. `#x-a.b` comes back as\n * `[\"x-a\"]`. Nothing in loam reads a model with errors, so the truncation\n * cannot reach a join today; it is recorded because the day something does read\n * one, a truncated tag resolves to a different, possibly real, capability.\n */\nconst TAG_NAME_SAFE = /[^A-Za-z0-9_-]/g;\n\n/**\n * An id as its tag spells it: every character a tag name cannot carry flattened\n * to a hyphen.\n *\n * Used by BOTH tags this module resolves, which is why it is no longer called\n * `capabilitySlug`. A capability id spells its nesting with `/`\n * (`identity/tokens`) and a `Requirement-ID` may legally contain `.`\n * (`REQUIREMENT_ID_RE` allows it) — two different characters, one rule, because\n * an author who learns that a tag flattens the awkward character in one id\n * should not have to learn it again for the other.\n *\n * Deliberately lossy, and the `many` arms below carry the whole cost of that:\n * two ids can produce one slug and nothing here notices.\n */\nexport function tagSlug(id: string): string {\n return id.replace(TAG_NAME_SAFE, \"-\");\n}\n\n/** The tag a claim is about, and the slug left after the prefix. */\ninterface ClaimTag {\n /** The tag verbatim, as the view declares it — no `#`, prefix still on. */\n tag: string;\n /** The tag with its reserved prefix removed: the name the join actually ran on. */\n slug: string;\n}\n\n/**\n * What one `#cap-` tag joins to. Branch on `kind`; every arm carries the tag it\n * is about, so a message can name the tag, the slug and the answer without\n * re-deriving any of them from the view.\n */\nexport type CapabilityClaim =\n /** Exactly one declared id flattens to this tag. `id` is the real id, slashes intact. */\n | (ClaimTag & { kind: \"resolved\"; id: string })\n /**\n * No declared id flattens to it. `close` holds real capability ids — names the\n * author can paste into `capabilities.yaml` or into the tag — never the slugs\n * the comparison was actually made against, because a slug is loam's spelling\n * of a name and offering it as a suggestion would be loam inventing a\n * capability id that nobody declared.\n */\n | (ClaimTag & { kind: \"none\"; close: string[] })\n /** Two or more declared ids flatten to it. Every colliding id, sorted, so a message can name them all. */\n | (ClaimTag & { kind: \"many\"; ids: string[] });\n\n/**\n * Every declared id that produces each slug.\n *\n * A map rather than a scan per tag because the \"many\" arm needs the WHOLE\n * colliding set: a lookup that stopped at the first hit could still report\n * \"resolved\" for a slug two ids answer to, which is precisely the silent wrong\n * answer this module exists to refuse.\n */\nfunction idsBySlug(declared: readonly string[]): Map<string, string[]> {\n const bySlug = new Map<string, string[]>();\n for (const id of declared) {\n const slug = tagSlug(id);\n const ids = bySlug.get(slug);\n if (ids === undefined) bySlug.set(slug, [id]);\n else ids.push(id);\n }\n return bySlug;\n}\n\n/**\n * Close names for a slug nothing declares — suggested against the SLUGS, then\n * mapped back to the ids that produced them.\n *\n * Both halves are load-bearing. Comparing against slugs is what lets\n * `#cap-identiy-tokns` reach `identity/tokens` at all: `closeIds` matches on\n * substring and shared prefix, and the slash sits where the author's typo does\n * not. Mapping back is what keeps the suggestion writable — a hint naming\n * `identity-tokens` would send the author to look for a capability declared\n * under a name that appears nowhere in `capabilities.yaml`.\n *\n * A winning slug may map back to more than one id, and the expansion is not\n * re-capped: `closeIds` caps the SLUGS it offers at five, and dropping a real\n * declared id afterwards to keep an arithmetic promise nobody made would hide\n * the very collision the \"many\" arm is here to name.\n */\nfunction closeCapabilityIds(slug: string, bySlug: ReadonlyMap<string, string[]>): string[] {\n const near = closeIds(slug, [...bySlug.keys()]);\n // `?? []` rather than `!`: `closeIds` promises every name it offers is one it\n // was given, so a miss here would mean that promise broke — and the honest\n // answer to a broken invariant inside a hint is one fewer suggestion, not a\n // `TypeError` thrown out of a `validate --all` run over somebody's fleet.\n return [...new Set(near.flatMap((s) => bySlug.get(s) ?? []))].sort(compareIds);\n}\n\n/**\n * The capability claims a view's tags make: one claim per `#cap-` tag, each\n * answered on its own terms.\n *\n * A VIEW MAY CARRY MORE THAN ONE `#cap-` TAG, and the tags are never collapsed\n * into a single verdict for the view. Both halves of that are deliberate.\n *\n * A use case realizing two capabilities is a real thing — a checkout flow that\n * also issues the identity token realizes both, and there is no honest way to\n * make its author pick one — so a second tag must NOT be read the way the \"many\"\n * arm reads a second colliding id. Those two look alike and are opposites: in\n * \"many\" the author wrote one name and loam cannot tell which id it means, here\n * the author wrote two names and meant both.\n *\n * And a view whose tags disagree — one resolving, one naming nothing — is not a\n * view that resolved. Collapsing to the first success would swallow the broken\n * tag and grade the view as if the typo were not there; collapsing to the first\n * failure would lose the capability the view genuinely does realize. One tag,\n * one verdict, is the only shape that keeps both facts, and it leaves the caller\n * free to emit one finding per failing tag while still collecting every id the\n * view resolved.\n *\n * Claims come back SORTED BY TAG rather than in the order LikeC4 handed the tags\n * over. Nothing in loam has measured that LikeC4 preserves the author's tag\n * order, and an output ordered by an unmeasured upstream detail is one that\n * reorders under a version bump — the diff-stability rule `./rollup.ts` states\n * for its rows, applied to a smaller list. A repeated tag collapses to one\n * claim, for the reason one breach earns one finding.\n *\n * An empty result means the view makes no capability claim at all — either it\n * carries no tags or none of them are reserved — which is the opt-in, not a\n * failure to resolve.\n */\nexport function resolveCapabilityTags(\n tags: readonly string[],\n declared: readonly string[],\n): CapabilityClaim[] {\n const bySlug = idsBySlug(declared);\n const claimed = [...new Set(tags.filter((tag) => tag.startsWith(CAP_TAG_PREFIX)))].sort(compareIds);\n\n return claimed.map((tag) => {\n // A bare `#cap-` slugs to the empty string, and it stays a claim on purpose:\n // the prefix is the author's opt-in however little follows it, so dropping\n // the tag as unreserved would leave a view that asked to be graded silently\n // ungraded. What that empty slug then resolves to is the lookup's business\n // like any other — `none` in every real fleet, `resolved` in the one that\n // declared an empty id, and neither is decided here.\n const slug = tag.slice(CAP_TAG_PREFIX.length);\n // Destructured rather than indexed so the three arms are told apart by the\n // shape of the match itself — no bounds assertion, and no `undefined` check\n // separate from the \"nothing declared this\" answer it already is.\n const [first, ...rest] = bySlug.get(slug) ?? [];\n if (first === undefined) return { tag, slug, kind: \"none\" as const, close: closeCapabilityIds(slug, bySlug) };\n if (rest.length === 0) return { tag, slug, kind: \"resolved\" as const, id: first };\n return { tag, slug, kind: \"many\" as const, ids: [first, ...rest].sort(compareIds) };\n });\n}\n\n/**\n * The reserved tag prefix for the requirement claim, matched EXACTLY as\n * `CAP_TAG_PREFIX` is and for the same two reasons: a tag that does not start\n * with it is the author's own vocabulary and is invisible here, and the slug\n * that survives the match must round-trip to a case-sensitive `Requirement-ID:`.\n */\nexport const REQ_TAG_PREFIX = \"req-\";\n\n/**\n * What one `#req-` tag joins to, given the capabilities the same view resolved.\n *\n * Five arms because there are five different fixes, and they are deliberately\n * the same five `RealizesClaim` draws in `./realizes/join.ts`: the two carriers\n * of one claim must fail in the same vocabulary, or an author who learns the\n * `Realizes:` messages learns nothing about the tag.\n */\nexport type RequirementClaim =\n /** Exactly one requirement of the scoping capability flattens to this tag. */\n | (ClaimTag & { kind: \"resolved\"; capability: string; id: string })\n /**\n * The view resolved no capability, or more than one, so a `Requirement-ID`\n * has no document to be unique inside. `capabilities` is what it DID resolve\n * — empty, or the two-or-more that make the scope ambiguous.\n */\n | (ClaimTag & { kind: \"unscoped\"; capabilities: string[] })\n /** The scoping capability is declared but has no `capabilities/<id>/spec.md`, so it carries no requirements. */\n | (ClaimTag & { kind: \"undocumented\"; capability: string })\n /** Its document exists and declares no requirements yet. */\n | (ClaimTag & { kind: \"empty\"; capability: string })\n /**\n * Its document declares requirements, none flattening to this slug. `close`\n * holds real `Requirement-ID`s — never the slugs the comparison ran against,\n * because a slug is loam's spelling and offering it would send the author\n * looking for an id that appears nowhere in the document.\n */\n | (ClaimTag & { kind: \"none\"; capability: string; close: string[] })\n /** Two or more of its requirement ids flatten to this slug. Every colliding id, sorted. */\n | (ClaimTag & { kind: \"many\"; capability: string; ids: string[] });\n\n/**\n * Resolve every `#req-` tag a view carries against the capability it is about.\n *\n * `scope` is the capability ids the view's OWN `#cap-` tags resolved to —\n * `resolveCapabilityTags`' `resolved` arms, and nothing else. An unresolved\n * `#cap-` tag is already `usecase.capability-unresolved`, and letting it also\n * scope a requirement lookup would build a second finding on top of a name loam\n * has just said it cannot place.\n *\n * Claims come back SORTED BY TAG for `resolveCapabilityTags`' reason: nothing in\n * loam has measured that LikeC4 preserves the author's tag order, and an output\n * ordered by an unmeasured upstream detail reorders under a version bump. A\n * repeated tag collapses to one claim, for the reason one breach earns one\n * finding.\n *\n * SEVERAL `#req-` TAGS ARE LEGAL AND NORMAL. One flow commonly satisfies two of\n * a capability's promises — a checkout flow that charges once AND honours the\n * price — and there is no honest way to make its author pick one. That is the\n * same shape `resolveCapabilityTags` allows for `#cap-`, and it is the opposite\n * of the `many` arm: there the author wrote one name loam cannot place, here\n * they wrote two and meant both.\n */\nexport function resolveRequirementTags(\n tags: readonly string[],\n scope: readonly string[],\n requirementsOf: (capability: string) => ReadonlySet<string> | undefined,\n): RequirementClaim[] {\n const claimed = [...new Set(tags.filter((tag) => tag.startsWith(REQ_TAG_PREFIX)))].sort(compareIds);\n if (claimed.length === 0) return [];\n const capability = scope.length === 1 ? scope[0]! : null;\n\n return claimed.map((tag): RequirementClaim => {\n // A bare `#req-` slugs to the empty string and stays a claim, exactly as a\n // bare `#cap-` does: the prefix is the opt-in however little follows it, and\n // dropping it would leave a view that asked to be graded silently ungraded.\n const slug = tag.slice(REQ_TAG_PREFIX.length);\n if (capability === null) return { tag, slug, kind: \"unscoped\", capabilities: [...scope].sort(compareIds) };\n const ids = requirementsOf(capability);\n if (ids === undefined) return { tag, slug, kind: \"undocumented\", capability };\n if (ids.size === 0) return { tag, slug, kind: \"empty\", capability };\n const matching = [...ids].filter((id) => tagSlug(id) === slug).sort(compareIds);\n const [first, ...rest] = matching;\n if (first === undefined) {\n return { tag, slug, kind: \"none\", capability, close: closeRequirementIds(slug, ids) };\n }\n if (rest.length === 0) return { tag, slug, kind: \"resolved\", capability, id: first };\n return { tag, slug, kind: \"many\", capability, ids: matching };\n });\n}\n\n/**\n * Close ids for a slug nothing in the document matches — suggested against the\n * SLUGS, then mapped back to the real ids, exactly as `closeCapabilityIds`\n * does and for its two reasons: comparing on slugs is what lets a typo reach an\n * id whose awkward character sits where the typo does not, and mapping back is\n * what keeps the suggestion writable.\n */\nfunction closeRequirementIds(slug: string, ids: ReadonlySet<string>): string[] {\n const bySlug = new Map<string, string[]>();\n for (const id of ids) {\n const key = tagSlug(id);\n const found = bySlug.get(key);\n if (found === undefined) bySlug.set(key, [id]);\n else found.push(id);\n }\n const near = closeIds(slug, [...bySlug.keys()]);\n return [...new Set(near.flatMap((s) => bySlug.get(s) ?? []))].sort(compareIds);\n}\n"]}
|
|
1
|
+
{"version":3,"file":"usecase-join.js","sourceRoot":"","sources":["../../../src/core/capabilities/usecase-join.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * The joins from a use case's tags: back to a declared capability, and to one\n * of that capability's requirements.\n *\n * TWO TAGS, ONE CLAIM. `#cap-<slug>` says which capability a flow is about;\n * `#req-<slug>` says which of its named promises the flow satisfies. The second\n * is the architect's ANSWER to the analyst's requirement, and it is the only\n * carrier a cross-service criterion has: \"I enter a login and a password and I\n * am in\" cannot be promised by any single service's spec, because each promises\n * only its own part — but a flow can, because it IS the hop sequence. That is\n * why `Realizes:` on a service requirement is not enough on its own, and why\n * this join exists at all.\n *\n * THE SECOND TAG IS SCOPED BY THE FIRST. A `Requirement-ID` is unique only\n * inside its own document, so `#req-CHK-ONCE` means nothing until the view has\n * said which capability it is about. A view carrying `#req-` with no resolved\n * `#cap-`, or with two, is refused rather than guessed at — the same stance the\n * `many` arm takes one join down.\n *\n * A LikeC4 `dynamic view` opts in to being graded as a business use case by\n * carrying a tag `#cap-<slug>`, and this module is the whole of what both tags\n * mean. It is deliberately the narrowest layer that can mean it: no file is\n * read, no finding is emitted, and nothing is thrown — a pure function over ids\n * somebody else supplies. That last property is not tidiness. Its caller runs\n * inside `loam validate --all` over every document a fleet has, and the one\n * behaviour a check added to that run may never have is failing the run it was\n * added to.\n *\n * WHY A SLUG AT ALL. Neither id is spelled in characters a tag name accepts. A\n * capability id spells its nesting inside the id — `identity/tokens` and\n * `payments/settlement` both ship in loam's own\n * `examples/docs/architecture/capabilities.yaml`, and `./capabilities.ts` keeps\n * the key exactly as the requirement writes it, slashes preserved — while a\n * `Requirement-ID` may legally contain a `.`. A LikeC4 tag name accepts exactly\n * `[A-Za-z0-9_-]` (measured at the 1.59.2 pin, pinned in\n * `test/likec4-view-shape.test.ts`), so both tags carry their id with the rest\n * flattened and both joins run over the flattened form.\n *\n * WHY THE JOIN CAN ANSWER \"MANY\". The slug is NOT injective. `payments/refunds`\n * and `payments-refunds` are two distinct, legal, declarable ids that flatten to\n * one slug, so `#cap-payments-refunds` names both and nothing in the tag can say\n * which. loam refuses and names ambiguity rather than guessing, and here the\n * cost of guessing is unusually high: a silently-picked capability attributes a\n * whole business flow — every hop, every service, every rollup built over it —\n * to the wrong owner, and goes on doing so invisibly because the wrong answer\n * looks exactly like the right one. The \"many\" arm is why this returns a union\n * at all instead of `string | undefined`.\n *\n * WHAT THE CALLER STILL OWES. The vocabulary ladder in `./findings.ts` — an\n * absent or unreadable `architecture/capabilities.yaml` means total silence, not\n * a fleet full of unresolved tags — is applied BEFORE calling here. An empty\n * `declared` list at this boundary therefore means \"the fleet declares no\n * capabilities\", which is a real answer this module will happily grade; it does\n * not mean \"there is no vocabulary\", and this module cannot tell the two apart.\n */\nimport { closeIds } from \"../c4/arch.js\";\nimport { compareIds } from \"../repo/entries.js\";\n\n/**\n * The reserved tag prefix, matched EXACTLY — case included.\n *\n * A view tag that does not start with it is not a capability claim and is\n * invisible here: `#FEAT-101`, `#platform` and `#deprecated` are the author's\n * own vocabulary and grading them would turn every existing hand-drawn view in\n * a brownfield fleet red on upgrade. The match is case-sensitive because what\n * survives it is a slug that must round-trip to a `capabilities.yaml` key, and\n * those keys are case-sensitive YAML; accepting `#Cap-checkout` here would hand\n * the lookup a slug whose case loam had already stopped believing in.\n */\nexport const CAP_TAG_PREFIX = \"cap-\";\n\n/**\n * Every character a LikeC4 tag name accepts, at the 1.59.2 pin. A WHITELIST,\n * and that is the load-bearing choice: the complement is infinite, so a rule\n * written as \"flatten a slash\" is a rule that is right about one character and\n * silent about the rest.\n *\n * Measured, not assumed (`test/likec4-view-shape.test.ts` pins it): `.`, `/`,\n * `:`, a space, `+`, `@` and a non-ASCII letter each make the DECLARATION a\n * parse error — and the failure is worse than a refusal, because the parser\n * TRUNCATES the name at the offending character. `#x-a.b` comes back as\n * `[\"x-a\"]`. Nothing in loam reads a model with errors, so the truncation\n * cannot reach a join today; it is recorded because the day something does read\n * one, a truncated tag resolves to a different, possibly real, capability.\n */\nconst TAG_NAME_SAFE = /[^A-Za-z0-9_-]/g;\n\n/**\n * An id as its tag spells it: every character a tag name cannot carry flattened\n * to a hyphen.\n *\n * Used by BOTH tags this module resolves, which is why it is no longer called\n * `capabilitySlug`. A capability id spells its nesting with `/`\n * (`identity/tokens`) and a `Requirement-ID` may legally contain `.`\n * (`REQUIREMENT_ID_RE` allows it) — two different characters, one rule, because\n * an author who learns that a tag flattens the awkward character in one id\n * should not have to learn it again for the other.\n *\n * Deliberately lossy, and the `many` arms below carry the whole cost of that:\n * two ids can produce one slug and nothing here notices.\n */\nexport function tagSlug(id: string): string {\n return id.replace(TAG_NAME_SAFE, \"-\");\n}\n\n/** The tag a claim is about, and the slug left after the prefix. */\ninterface ClaimTag {\n /** The tag verbatim, as the view declares it — no `#`, prefix still on. */\n tag: string;\n /** The tag with its reserved prefix removed: the name the join actually ran on. */\n slug: string;\n}\n\n/**\n * What one `#cap-` tag joins to. Branch on `kind`; every arm carries the tag it\n * is about, so a message can name the tag, the slug and the answer without\n * re-deriving any of them from the view.\n */\nexport type CapabilityClaim =\n /** Exactly one declared id flattens to this tag. `id` is the real id, slashes intact. */\n | (ClaimTag & { kind: \"resolved\"; id: string })\n /**\n * No declared id flattens to it. `close` holds real capability ids — names the\n * author can paste into `capabilities.yaml` or into the tag — never the slugs\n * the comparison was actually made against, because a slug is loam's spelling\n * of a name and offering it as a suggestion would be loam inventing a\n * capability id that nobody declared.\n */\n | (ClaimTag & { kind: \"none\"; close: string[] })\n /** Two or more declared ids flatten to it. Every colliding id, sorted, so a message can name them all. */\n | (ClaimTag & { kind: \"many\"; ids: string[] });\n\n/**\n * Every declared id that produces each slug.\n *\n * A map rather than a scan per tag because the \"many\" arm needs the WHOLE\n * colliding set: a lookup that stopped at the first hit could still report\n * \"resolved\" for a slug two ids answer to, which is precisely the silent wrong\n * answer this module exists to refuse.\n */\nfunction idsBySlug(declared: readonly string[]): Map<string, string[]> {\n const bySlug = new Map<string, string[]>();\n for (const id of declared) {\n const slug = tagSlug(id);\n const ids = bySlug.get(slug);\n if (ids === undefined) bySlug.set(slug, [id]);\n else ids.push(id);\n }\n return bySlug;\n}\n\n/**\n * Close names for a slug nothing declares — suggested against the SLUGS, then\n * mapped back to the ids that produced them.\n *\n * Both halves are load-bearing. Comparing against slugs is what lets\n * `#cap-identiy-tokns` reach `identity/tokens` at all: `closeIds` matches on\n * substring and shared prefix, and the slash sits where the author's typo does\n * not. Mapping back is what keeps the suggestion writable — a hint naming\n * `identity-tokens` would send the author to look for a capability declared\n * under a name that appears nowhere in `capabilities.yaml`.\n *\n * A winning slug may map back to more than one id, and the expansion is not\n * re-capped: `closeIds` caps the SLUGS it offers at five, and dropping a real\n * declared id afterwards to keep an arithmetic promise nobody made would hide\n * the very collision the \"many\" arm is here to name.\n */\nfunction closeCapabilityIds(slug: string, bySlug: ReadonlyMap<string, string[]>): string[] {\n const near = closeIds(slug, [...bySlug.keys()]);\n // `?? []` rather than `!`: `closeIds` promises every name it offers is one it\n // was given, so a miss here would mean that promise broke — and the honest\n // answer to a broken invariant inside a hint is one fewer suggestion, not a\n // `TypeError` thrown out of a `validate --all` run over somebody's fleet.\n return [...new Set(near.flatMap((s) => bySlug.get(s) ?? []))].sort(compareIds);\n}\n\n/**\n * The capability claims a view's tags make: one claim per `#cap-` tag, each\n * answered on its own terms.\n *\n * A VIEW MAY CARRY MORE THAN ONE `#cap-` TAG, and the tags are never collapsed\n * into a single verdict for the view. Both halves of that are deliberate.\n *\n * A use case realizing two capabilities is a real thing — a checkout flow that\n * also issues the identity token realizes both, and there is no honest way to\n * make its author pick one — so a second tag must NOT be read the way the \"many\"\n * arm reads a second colliding id. Those two look alike and are opposites: in\n * \"many\" the author wrote one name and loam cannot tell which id it means, here\n * the author wrote two names and meant both.\n *\n * And a view whose tags disagree — one resolving, one naming nothing — is not a\n * view that resolved. Collapsing to the first success would swallow the broken\n * tag and grade the view as if the typo were not there; collapsing to the first\n * failure would lose the capability the view genuinely does realize. One tag,\n * one verdict, is the only shape that keeps both facts, and it leaves the caller\n * free to emit one finding per failing tag while still collecting every id the\n * view resolved.\n *\n * Claims come back SORTED BY TAG rather than in the order LikeC4 handed the tags\n * over. Nothing in loam has measured that LikeC4 preserves the author's tag\n * order, and an output ordered by an unmeasured upstream detail is one that\n * reorders under a version bump — the diff-stability rule `./rollup.ts` states\n * for its rows, applied to a smaller list. A repeated tag collapses to one\n * claim, for the reason one breach earns one finding.\n *\n * An empty result means the view makes no capability claim at all — either it\n * carries no tags or none of them are reserved — which is the opt-in, not a\n * failure to resolve.\n */\nexport function resolveCapabilityTags(\n tags: readonly string[],\n declared: readonly string[],\n): CapabilityClaim[] {\n const bySlug = idsBySlug(declared);\n const claimed = [...new Set(tags.filter((tag) => tag.startsWith(CAP_TAG_PREFIX)))].sort(compareIds);\n\n return claimed.map((tag) => {\n // A bare `#cap-` slugs to the empty string, and it stays a claim on purpose:\n // the prefix is the author's opt-in however little follows it, so dropping\n // the tag as unreserved would leave a view that asked to be graded silently\n // ungraded. What that empty slug then resolves to is the lookup's business\n // like any other — `none` in every real fleet, `resolved` in the one that\n // declared an empty id, and neither is decided here.\n const slug = tag.slice(CAP_TAG_PREFIX.length);\n // Destructured rather than indexed so the three arms are told apart by the\n // shape of the match itself — no bounds assertion, and no `undefined` check\n // separate from the \"nothing declared this\" answer it already is.\n const [first, ...rest] = bySlug.get(slug) ?? [];\n if (first === undefined) return { tag, slug, kind: \"none\" as const, close: closeCapabilityIds(slug, bySlug) };\n if (rest.length === 0) return { tag, slug, kind: \"resolved\" as const, id: first };\n return { tag, slug, kind: \"many\" as const, ids: [first, ...rest].sort(compareIds) };\n });\n}\n\n/**\n * The reserved tag prefix for the requirement claim, matched EXACTLY as\n * `CAP_TAG_PREFIX` is and for the same two reasons: a tag that does not start\n * with it is the author's own vocabulary and is invisible here, and the slug\n * that survives the match must round-trip to a case-sensitive `Requirement-ID:`.\n */\nexport const REQ_TAG_PREFIX = \"req-\";\n\n/**\n * What one `#req-` tag joins to, given the capabilities the same view resolved.\n *\n * Five arms because there are five different fixes, and they are deliberately\n * the same five `RealizesClaim` draws in `./realizes/join.ts`: the two carriers\n * of one claim must fail in the same vocabulary, or an author who learns the\n * `Realizes:` messages learns nothing about the tag.\n */\nexport type RequirementClaim =\n /** Exactly one requirement of the scoping capability flattens to this tag. */\n | (ClaimTag & { kind: \"resolved\"; capability: string; id: string })\n /**\n * The view resolved no capability, or more than one, so a `Requirement-ID`\n * has no document to be unique inside. `capabilities` is what it DID resolve\n * — empty, or the two-or-more that make the scope ambiguous.\n */\n | (ClaimTag & { kind: \"unscoped\"; capabilities: string[] })\n /** The scoping capability is declared but has no `capabilities/<id>/spec.md`, so it carries no requirements. */\n | (ClaimTag & { kind: \"undocumented\"; capability: string })\n /** Its document exists and declares no requirements yet. */\n | (ClaimTag & { kind: \"empty\"; capability: string })\n /**\n * Its document declares requirements, none flattening to this slug. `close`\n * holds real `Requirement-ID`s — never the slugs the comparison ran against,\n * because a slug is loam's spelling and offering it would send the author\n * looking for an id that appears nowhere in the document.\n */\n | (ClaimTag & { kind: \"none\"; capability: string; close: string[] })\n /** Two or more of its requirement ids flatten to this slug. Every colliding id, sorted. */\n | (ClaimTag & { kind: \"many\"; capability: string; ids: string[] });\n\n/**\n * Resolve every `#req-` tag a view carries against the capability it is about.\n *\n * `scope` is the capability ids the view's OWN `#cap-` tags resolved to —\n * `resolveCapabilityTags`' `resolved` arms, and nothing else. An unresolved\n * `#cap-` tag is already `usecase.capability-unresolved`, and letting it also\n * scope a requirement lookup would build a second finding on top of a name loam\n * has just said it cannot place.\n *\n * Claims come back SORTED BY TAG for `resolveCapabilityTags`' reason: nothing in\n * loam has measured that LikeC4 preserves the author's tag order, and an output\n * ordered by an unmeasured upstream detail reorders under a version bump. A\n * repeated tag collapses to one claim, for the reason one breach earns one\n * finding.\n *\n * SEVERAL `#req-` TAGS ARE LEGAL AND NORMAL. One flow commonly satisfies two of\n * a capability's promises — a checkout flow that charges once AND honours the\n * price — and there is no honest way to make its author pick one. That is the\n * same shape `resolveCapabilityTags` allows for `#cap-`, and it is the opposite\n * of the `many` arm: there the author wrote one name loam cannot place, here\n * they wrote two and meant both.\n */\nexport function resolveRequirementTags(\n tags: readonly string[],\n scope: readonly string[],\n requirementsOf: (capability: string) => ReadonlySet<string> | undefined,\n): RequirementClaim[] {\n const claimed = [...new Set(tags.filter((tag) => tag.startsWith(REQ_TAG_PREFIX)))].sort(compareIds);\n if (claimed.length === 0) return [];\n const capability = scope.length === 1 ? scope[0]! : null;\n\n return claimed.map((tag): RequirementClaim => {\n // A bare `#req-` slugs to the empty string and stays a claim, exactly as a\n // bare `#cap-` does: the prefix is the opt-in however little follows it, and\n // dropping it would leave a view that asked to be graded silently ungraded.\n const slug = tag.slice(REQ_TAG_PREFIX.length);\n if (capability === null) return { tag, slug, kind: \"unscoped\", capabilities: [...scope].sort(compareIds) };\n const ids = requirementsOf(capability);\n if (ids === undefined) return { tag, slug, kind: \"undocumented\", capability };\n if (ids.size === 0) return { tag, slug, kind: \"empty\", capability };\n const matching = [...ids].filter((id) => tagSlug(id) === slug).sort(compareIds);\n const [first, ...rest] = matching;\n if (first === undefined) {\n return { tag, slug, kind: \"none\", capability, close: closeRequirementIds(slug, ids) };\n }\n if (rest.length === 0) return { tag, slug, kind: \"resolved\", capability, id: first };\n return { tag, slug, kind: \"many\", capability, ids: matching };\n });\n}\n\n/**\n * Close ids for a slug nothing in the document matches — suggested against the\n * SLUGS, then mapped back to the real ids: `closeCapabilityIds` over this\n * document's ids, for its two reasons: comparing on slugs is what lets a typo reach an\n * id whose awkward character sits where the typo does not, and mapping back is\n * what keeps the suggestion writable.\n */\nfunction closeRequirementIds(slug: string, ids: ReadonlySet<string>): string[] {\n return closeCapabilityIds(slug, idsBySlug([...ids]));\n}\n"]}
|
|
@@ -152,10 +152,18 @@ async function intentIssue(scope, issues) {
|
|
|
152
152
|
return;
|
|
153
153
|
issues.push({
|
|
154
154
|
...empty,
|
|
155
|
-
message: `intent.md says nothing yet — its Why and Scope are still the scaffold's comments. Write why ${scope.featureId} exists before archiving it into the living docs, or archive with --approve.`,
|
|
155
|
+
message: `intent.md says nothing yet — its Why and Scope are still the scaffold's comments (questions under \`## Open questions\` are not a Why). Write why ${scope.featureId} exists before archiving it into the living docs, or archive with --approve.`,
|
|
156
156
|
});
|
|
157
157
|
}
|
|
158
|
-
/**
|
|
158
|
+
/**
|
|
159
|
+
* The scaffold's `## Open questions` section, up to the next heading of its
|
|
160
|
+
* level or above. loam-feature's analyze step WRITES there, so prose in it is
|
|
161
|
+
* an agent's questions about the feature, never the reason it exists — and
|
|
162
|
+
* counting it would let a feature whose Why nobody wrote clear the one gate
|
|
163
|
+
* that asks for it, as soon as its author did the step that asks questions.
|
|
164
|
+
*/
|
|
165
|
+
const OPEN_QUESTIONS_SECTION = /^##[ \t]+open questions[ \t]*\n[\s\S]*?(?=^#{1,2}[ \t]|(?![\s\S]))/gim;
|
|
166
|
+
/** Is there any authored text outside frontmatter, HTML comments, headings and the open questions? */
|
|
159
167
|
function hasProse(text) {
|
|
160
168
|
// Normalized BEFORE the fence regex, the discipline document/parse.ts
|
|
161
169
|
// applies to every other markdown read: `---\r` is not `---`, so a CRLF
|
|
@@ -169,7 +177,7 @@ function hasProse(text) {
|
|
|
169
177
|
const fm = /^---\n[\s\S]*?\n---\n/.exec(body);
|
|
170
178
|
if (fm !== null)
|
|
171
179
|
body = body.slice(fm[0].length);
|
|
172
|
-
body = body.replace(/<!--[\s\S]*?-->/g, "");
|
|
180
|
+
body = body.replace(/<!--[\s\S]*?-->/g, "").replace(OPEN_QUESTIONS_SECTION, "");
|
|
173
181
|
return body
|
|
174
182
|
.split("\n")
|
|
175
183
|
.some((line) => line.trim().length > 0 && !line.trimStart().startsWith("#"));
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"scaffold.js","sourceRoot":"","sources":["../../../../src/core/coherence/authoring/scaffold.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * The gate between `loam new` and `loam archive`: a scaffold nobody edited must\n * not become the living truth.\n *\n * `loam new` writes documents whose whole point is to be replaced — a TODO\n * requirement heading, angle-bracket step fill-ins, a TODO service description,\n * an intent whose Why/Scope are comments. Every one of them parsed as real\n * content, validated clean, and archived at exit 0, which put literal `TODO —\n * name the behaviour` into a living spec the rest of the fleet then reads as a\n * requirement somebody meant. These checks refuse that merge.\n *\n * Both are warnings that GATE (issue.ts: severity says the DOCUMENT is legal —\n * it is, it parses — gating says the MERGE is unsafe). `--approve` overrides,\n * as with every judgment about the feature.\n *\n * BOTH DELTA CORPORA ARE READ, because `loam new` scaffolds into both. A\n * `--capability` run writes `features/<FEAT>/capabilities/<id>/spec.md` from a\n * template whose example sits inside an HTML comment exactly like the two spec\n * templates', and an author who copies that block out and archives without\n * editing it would publish `TODO — name the promise` into a LIVING capability\n * document — which is strictly worse than the service case this gate was built\n * for: a capability document outlives every service that realizes it, and a\n * `Realizes:` line pointed at the placeholder's id would then be a join to a\n * promise nobody wrote. The capability deltas are read HERE rather than handed\n * in through `AuthoringScope`, because this is a walk over the feature's own\n * documents and the scope already names the feature; the read is one\n * `existsSync` for the fleets that have not adopted the axis, and the memo when\n * a context was threaded.\n */\nimport { existsSync } from \"node:fs\";\nimport { readFile } from \"node:fs/promises\";\nimport type { Elem } from \"../../c4/likec4.js\";\nimport type { PathableService } from \"../../kernel/ids/service.js\";\nimport type { Issue } from \"../../vocabulary/issue.js\";\nimport type { FleetContext } from \"../../fleet-context.js\";\nimport type { Requirement } from \"../../document/spec.js\";\nimport { parseRequirements } from \"../../document/parse.js\";\nimport { featureCapabilityDeltas } from \"../../capabilities/delta/tree.js\";\nimport { featurePaths, featureSpecPaths, SPEC_AXES } from \"../../repo/paths.js\";\nimport type { FeatureDir } from \"../../kernel/ids/dirs.js\";\nimport {\n BODY_SENTINELS,\n HEADING_SENTINELS,\n SERVICE_DESCRIPTION_SENTINEL,\n} from \"./sentinels.js\";\n\n/** What the placeholder gate reads: the feature's own documents, nothing else. */\nexport interface AuthoringScope {\n featureDir: FeatureDir;\n featureId: string;\n /**\n * Every element whose authored block the landscape merge would SPLICE — the\n * tagged elements and everything nested inside their blocks. The scope is\n * the splice's, not the tag's, for `c4.service-binding-invalid`'s reason\n * (coherence.ts): the merge carries a tagged element's block over byte for\n * byte, untagged children included, so a TODO description on a nested child\n * reaches the fleet map exactly as its tagged parent's would.\n */\n splicedEls: Elem[];\n svcNames: PathableService[];\n context?: FleetContext;\n}\n\nexport async function authoringIssues(scope: AuthoringScope): Promise<Issue[]> {\n const issues: Issue[] = [];\n await intentIssue(scope, issues);\n\n for (const e of scope.splicedEls) {\n if (e.description !== SERVICE_DESCRIPTION_SENTINEL) continue;\n issues.push({\n severity: \"warn\",\n gates: true,\n code: \"scaffold.placeholder\",\n message: `delta.likec4: '${e.title}' (${e.id}) still carries the scaffolded description '${SERVICE_DESCRIPTION_SENTINEL}' — the merge would publish it to the fleet map. Say what the service owns, or archive with --approve.`,\n });\n }\n\n for (const svc of scope.svcNames) {\n const paths = featureSpecPaths(scope.featureDir, svc);\n for (const axis of SPEC_AXES) {\n const path = paths[axis.key];\n if (!existsSync(path)) continue;\n const found = unauthored(await requirementsOf(scope, path));\n if (found.size === 0) continue;\n issues.push({\n severity: \"warn\",\n gates: true,\n code: \"scaffold.placeholder\",\n subject: svc,\n message: `${svc}: ${axis.file} still carries scaffold placeholder(s) nobody authored — ${quote(found)}. The merge would write them into the living spec as requirements somebody meant. Replace them, or archive with --approve.`,\n });\n }\n }\n\n // The business corpus. `subject` is the capability id — a THIRD provenance\n // for this code's subject, after a service id and none at all — and the\n // artifact table resolves it by MEMBERSHIP against the capability documents\n // the feature carries (`status/feature/artifacts.ts`), the same resolution\n // every `delta.*` issue on this axis already relies on. So the capability's\n // own row turns draft, and a service row does too only where a fleet holds a\n // service and a capability of one name, which is that module's deliberately\n // pessimistic answer rather than a mis-attribution here.\n const capabilities =\n scope.context === undefined\n ? await featureCapabilityDeltas(scope.featureDir)\n : await scope.context.featureCapabilityDeltas(scope.featureDir);\n for (const doc of capabilities.docs) {\n const found = unauthored(await requirementsOf(scope, doc.spec));\n if (found.size === 0) continue;\n issues.push({\n severity: \"warn\",\n gates: true,\n code: \"scaffold.placeholder\",\n subject: doc.id,\n message: `capability ${doc.id}: spec.md still carries scaffold placeholder(s) nobody authored — ${quote(found)}. The merge would write them into the living capabilities/${doc.id}/spec.md as promises somebody meant — and a capability document outlives every service that realizes it. Replace them, or archive with --approve.`,\n });\n }\n return issues;\n}\n\n/** One delta document's requirements, through the invocation's read index when there is one. */\nasync function requirementsOf(scope: AuthoringScope, path: string): Promise<Requirement[]> {\n if (scope.context === undefined) return parseRequirements(await readFile(path, \"utf8\"));\n return scope.context.readRequirements(path);\n}\n\n/**\n * The scaffold strings a delta's MERGING requirements still carry.\n *\n * ADDED/MODIFIED only: those are the texts this MERGE would write. A BASE\n * requirement is a quote of the living state — if a sentinel sits there, some\n * earlier archive published it and this feature is not the author to gate on it\n * — and a REMOVED one is leaving the document anyway.\n */\nfunction unauthored(reqs: Requirement[]): Set<string> {\n const found = new Set<string>();\n for (const r of reqs) {\n if (r.kind === \"ADDED\" || r.kind === \"MODIFIED\") sentinelsIn(r, found);\n }\n return found;\n}\n\n/** The found sentinels as a message fragment — one spelling, so the two corpora read alike. */\nfunction quote(found: ReadonlySet<string>): string {\n return [...found].map((s) => `'${s}'`).join(\", \");\n}\n\n/** The exact template strings this requirement still carries, if any. */\nfunction sentinelsIn(r: Requirement, found: Set<string>): void {\n for (const heading of HEADING_SENTINELS) {\n if (r.name === heading) found.add(heading);\n for (const s of r.scenarios) {\n if (s.name === heading) found.add(heading);\n }\n }\n const body = [r.text.join(\"\\n\"), ...r.scenarios.map((s) => s.lines.join(\"\\n\"))].join(\"\\n\");\n for (const marker of BODY_SENTINELS) {\n if (body.includes(marker)) found.add(marker);\n }\n}\n\n/**\n * Does intent.md SAY anything? The scaffold's Why/Scope bodies are HTML\n * comments, so \"strip frontmatter, comments and headings, look for prose\" is\n * exactly the question \"did a person write the sentence the reviewer reads\n * first\". A missing file gates the same way: a feature with no stated intent\n * is not less anonymous for not having the file.\n */\nasync function intentIssue(scope: AuthoringScope, issues: Issue[]): Promise<void> {\n const path = featurePaths(scope.featureDir).intent;\n const empty = { severity: \"warn\", gates: true, code: \"intent.empty\" } as const;\n if (!existsSync(path)) {\n issues.push({\n ...empty,\n message: `intent.md is missing — nothing says why ${scope.featureId} exists. Write the Why before archiving it into the living docs, or archive with --approve.`,\n });\n return;\n }\n if (hasProse(await readFile(path, \"utf8\"))) return;\n issues.push({\n ...empty,\n message: `intent.md says nothing yet — its Why and Scope are still the scaffold's comments. Write why ${scope.featureId} exists before archiving it into the living docs, or archive with --approve.`,\n });\n}\n\n/** Is there any authored text outside frontmatter, HTML comments and headings? */\nfunction hasProse(text: string): boolean {\n // Normalized BEFORE the fence regex, the discipline document/parse.ts\n // applies to every other markdown read: `---\\r` is not `---`, so a CRLF\n // rewrite (or a BOM some Windows editor prefixed) kept the frontmatter,\n // which then counted as prose — and the untouched scaffold archived at\n // exit 0 through the exact gate built to refuse it.\n // The BOM is spelled as an escape, never as the raw character — a raw one is\n // invisible in review and to grep, which is how it would survive a rewrite.\n let body = text.replace(/^\\uFEFF/, \"\").replace(/\\r\\n/g, \"\\n\");\n // Frontmatter: the leading `---` block only, exactly where the scaffold puts it.\n const fm = /^---\\n[\\s\\S]*?\\n---\\n/.exec(body);\n if (fm !== null) body = body.slice(fm[0].length);\n body = body.replace(/<!--[\\s\\S]*?-->/g, \"\");\n return body\n .split(\"\\n\")\n .some((line) => line.trim().length > 0 && !line.trimStart().startsWith(\"#\"));\n}\n"]}
|
|
1
|
+
{"version":3,"file":"scaffold.js","sourceRoot":"","sources":["../../../../src/core/coherence/authoring/scaffold.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * The gate between `loam new` and `loam archive`: a scaffold nobody edited must\n * not become the living truth.\n *\n * `loam new` writes documents whose whole point is to be replaced — a TODO\n * requirement heading, angle-bracket step fill-ins, a TODO service description,\n * an intent whose Why/Scope are comments. Every one of them parsed as real\n * content, validated clean, and archived at exit 0, which put literal `TODO —\n * name the behaviour` into a living spec the rest of the fleet then reads as a\n * requirement somebody meant. These checks refuse that merge.\n *\n * Both are warnings that GATE (issue.ts: severity says the DOCUMENT is legal —\n * it is, it parses — gating says the MERGE is unsafe). `--approve` overrides,\n * as with every judgment about the feature.\n *\n * BOTH DELTA CORPORA ARE READ, because `loam new` scaffolds into both. A\n * `--capability` run writes `features/<FEAT>/capabilities/<id>/spec.md` from a\n * template whose example sits inside an HTML comment exactly like the two spec\n * templates', and an author who copies that block out and archives without\n * editing it would publish `TODO — name the promise` into a LIVING capability\n * document — which is strictly worse than the service case this gate was built\n * for: a capability document outlives every service that realizes it, and a\n * `Realizes:` line pointed at the placeholder's id would then be a join to a\n * promise nobody wrote. The capability deltas are read HERE rather than handed\n * in through `AuthoringScope`, because this is a walk over the feature's own\n * documents and the scope already names the feature; the read is one\n * `existsSync` for the fleets that have not adopted the axis, and the memo when\n * a context was threaded.\n */\nimport { existsSync } from \"node:fs\";\nimport { readFile } from \"node:fs/promises\";\nimport type { Elem } from \"../../c4/likec4.js\";\nimport type { PathableService } from \"../../kernel/ids/service.js\";\nimport type { Issue } from \"../../vocabulary/issue.js\";\nimport type { FleetContext } from \"../../fleet-context.js\";\nimport type { Requirement } from \"../../document/spec.js\";\nimport { parseRequirements } from \"../../document/parse.js\";\nimport { featureCapabilityDeltas } from \"../../capabilities/delta/tree.js\";\nimport { featurePaths, featureSpecPaths, SPEC_AXES } from \"../../repo/paths.js\";\nimport type { FeatureDir } from \"../../kernel/ids/dirs.js\";\nimport {\n BODY_SENTINELS,\n HEADING_SENTINELS,\n SERVICE_DESCRIPTION_SENTINEL,\n} from \"./sentinels.js\";\n\n/** What the placeholder gate reads: the feature's own documents, nothing else. */\nexport interface AuthoringScope {\n featureDir: FeatureDir;\n featureId: string;\n /**\n * Every element whose authored block the landscape merge would SPLICE — the\n * tagged elements and everything nested inside their blocks. The scope is\n * the splice's, not the tag's, for `c4.service-binding-invalid`'s reason\n * (coherence.ts): the merge carries a tagged element's block over byte for\n * byte, untagged children included, so a TODO description on a nested child\n * reaches the fleet map exactly as its tagged parent's would.\n */\n splicedEls: Elem[];\n svcNames: PathableService[];\n context?: FleetContext;\n}\n\nexport async function authoringIssues(scope: AuthoringScope): Promise<Issue[]> {\n const issues: Issue[] = [];\n await intentIssue(scope, issues);\n\n for (const e of scope.splicedEls) {\n if (e.description !== SERVICE_DESCRIPTION_SENTINEL) continue;\n issues.push({\n severity: \"warn\",\n gates: true,\n code: \"scaffold.placeholder\",\n message: `delta.likec4: '${e.title}' (${e.id}) still carries the scaffolded description '${SERVICE_DESCRIPTION_SENTINEL}' — the merge would publish it to the fleet map. Say what the service owns, or archive with --approve.`,\n });\n }\n\n for (const svc of scope.svcNames) {\n const paths = featureSpecPaths(scope.featureDir, svc);\n for (const axis of SPEC_AXES) {\n const path = paths[axis.key];\n if (!existsSync(path)) continue;\n const found = unauthored(await requirementsOf(scope, path));\n if (found.size === 0) continue;\n issues.push({\n severity: \"warn\",\n gates: true,\n code: \"scaffold.placeholder\",\n subject: svc,\n message: `${svc}: ${axis.file} still carries scaffold placeholder(s) nobody authored — ${quote(found)}. The merge would write them into the living spec as requirements somebody meant. Replace them, or archive with --approve.`,\n });\n }\n }\n\n // The business corpus. `subject` is the capability id — a THIRD provenance\n // for this code's subject, after a service id and none at all — and the\n // artifact table resolves it by MEMBERSHIP against the capability documents\n // the feature carries (`status/feature/artifacts.ts`), the same resolution\n // every `delta.*` issue on this axis already relies on. So the capability's\n // own row turns draft, and a service row does too only where a fleet holds a\n // service and a capability of one name, which is that module's deliberately\n // pessimistic answer rather than a mis-attribution here.\n const capabilities =\n scope.context === undefined\n ? await featureCapabilityDeltas(scope.featureDir)\n : await scope.context.featureCapabilityDeltas(scope.featureDir);\n for (const doc of capabilities.docs) {\n const found = unauthored(await requirementsOf(scope, doc.spec));\n if (found.size === 0) continue;\n issues.push({\n severity: \"warn\",\n gates: true,\n code: \"scaffold.placeholder\",\n subject: doc.id,\n message: `capability ${doc.id}: spec.md still carries scaffold placeholder(s) nobody authored — ${quote(found)}. The merge would write them into the living capabilities/${doc.id}/spec.md as promises somebody meant — and a capability document outlives every service that realizes it. Replace them, or archive with --approve.`,\n });\n }\n return issues;\n}\n\n/** One delta document's requirements, through the invocation's read index when there is one. */\nasync function requirementsOf(scope: AuthoringScope, path: string): Promise<Requirement[]> {\n if (scope.context === undefined) return parseRequirements(await readFile(path, \"utf8\"));\n return scope.context.readRequirements(path);\n}\n\n/**\n * The scaffold strings a delta's MERGING requirements still carry.\n *\n * ADDED/MODIFIED only: those are the texts this MERGE would write. A BASE\n * requirement is a quote of the living state — if a sentinel sits there, some\n * earlier archive published it and this feature is not the author to gate on it\n * — and a REMOVED one is leaving the document anyway.\n */\nfunction unauthored(reqs: Requirement[]): Set<string> {\n const found = new Set<string>();\n for (const r of reqs) {\n if (r.kind === \"ADDED\" || r.kind === \"MODIFIED\") sentinelsIn(r, found);\n }\n return found;\n}\n\n/** The found sentinels as a message fragment — one spelling, so the two corpora read alike. */\nfunction quote(found: ReadonlySet<string>): string {\n return [...found].map((s) => `'${s}'`).join(\", \");\n}\n\n/** The exact template strings this requirement still carries, if any. */\nfunction sentinelsIn(r: Requirement, found: Set<string>): void {\n for (const heading of HEADING_SENTINELS) {\n if (r.name === heading) found.add(heading);\n for (const s of r.scenarios) {\n if (s.name === heading) found.add(heading);\n }\n }\n const body = [r.text.join(\"\\n\"), ...r.scenarios.map((s) => s.lines.join(\"\\n\"))].join(\"\\n\");\n for (const marker of BODY_SENTINELS) {\n if (body.includes(marker)) found.add(marker);\n }\n}\n\n/**\n * Does intent.md SAY anything? The scaffold's Why/Scope bodies are HTML\n * comments, so \"strip frontmatter, comments and headings, look for prose\" is\n * exactly the question \"did a person write the sentence the reviewer reads\n * first\". A missing file gates the same way: a feature with no stated intent\n * is not less anonymous for not having the file.\n */\nasync function intentIssue(scope: AuthoringScope, issues: Issue[]): Promise<void> {\n const path = featurePaths(scope.featureDir).intent;\n const empty = { severity: \"warn\", gates: true, code: \"intent.empty\" } as const;\n if (!existsSync(path)) {\n issues.push({\n ...empty,\n message: `intent.md is missing — nothing says why ${scope.featureId} exists. Write the Why before archiving it into the living docs, or archive with --approve.`,\n });\n return;\n }\n if (hasProse(await readFile(path, \"utf8\"))) return;\n issues.push({\n ...empty,\n message: `intent.md says nothing yet — its Why and Scope are still the scaffold's comments (questions under \\`## Open questions\\` are not a Why). Write why ${scope.featureId} exists before archiving it into the living docs, or archive with --approve.`,\n });\n}\n\n/**\n * The scaffold's `## Open questions` section, up to the next heading of its\n * level or above. loam-feature's analyze step WRITES there, so prose in it is\n * an agent's questions about the feature, never the reason it exists — and\n * counting it would let a feature whose Why nobody wrote clear the one gate\n * that asks for it, as soon as its author did the step that asks questions.\n */\nconst OPEN_QUESTIONS_SECTION = /^##[ \\t]+open questions[ \\t]*\\n[\\s\\S]*?(?=^#{1,2}[ \\t]|(?![\\s\\S]))/gim;\n\n/** Is there any authored text outside frontmatter, HTML comments, headings and the open questions? */\nfunction hasProse(text: string): boolean {\n // Normalized BEFORE the fence regex, the discipline document/parse.ts\n // applies to every other markdown read: `---\\r` is not `---`, so a CRLF\n // rewrite (or a BOM some Windows editor prefixed) kept the frontmatter,\n // which then counted as prose — and the untouched scaffold archived at\n // exit 0 through the exact gate built to refuse it.\n // The BOM is spelled as an escape, never as the raw character — a raw one is\n // invisible in review and to grep, which is how it would survive a rewrite.\n let body = text.replace(/^\\uFEFF/, \"\").replace(/\\r\\n/g, \"\\n\");\n // Frontmatter: the leading `---` block only, exactly where the scaffold puts it.\n const fm = /^---\\n[\\s\\S]*?\\n---\\n/.exec(body);\n if (fm !== null) body = body.slice(fm[0].length);\n body = body.replace(/<!--[\\s\\S]*?-->/g, \"\").replace(OPEN_QUESTIONS_SECTION, \"\");\n return body\n .split(\"\\n\")\n .some((line) => line.trim().length > 0 && !line.trimStart().startsWith(\"#\"));\n}\n"]}
|
|
@@ -2,7 +2,8 @@
|
|
|
2
2
|
* The placeholder strings `loam new` scaffolds, spelled ONCE.
|
|
3
3
|
*
|
|
4
4
|
* Both sides of the placeholder gate read this module: the templates in
|
|
5
|
-
* `commands/new/templates.ts`
|
|
5
|
+
* `commands/new/templates.ts` and `commands/new/capability-template.ts` write
|
|
6
|
+
* these exact strings, and the coherence
|
|
6
7
|
* check in `./scaffold.ts` refuses to archive them. Spelling a sentinel in two
|
|
7
8
|
* places is how the gate stops matching the scaffold — a one-character rewording
|
|
8
9
|
* of a template would silently retire the check that guards it.
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"sentinels.js","sourceRoot":"","sources":["../../../../src/core/coherence/authoring/sentinels.ts"],"names":[],"mappings":"AAAA
|
|
1
|
+
{"version":3,"file":"sentinels.js","sourceRoot":"","sources":["../../../../src/core/coherence/authoring/sentinels.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;GAaG;AAEH,2EAA2E;AAC3E,MAAM,CAAC,MAAM,oBAAoB,GAAG,2BAA2B,CAAC;AAEhE,oFAAoF;AACpF,MAAM,CAAC,MAAM,yBAAyB,GAAG,0CAA0C,CAAC;AAEpF;;;;;;;;;;GAUG;AACH,MAAM,CAAC,MAAM,+BAA+B,GAAG,yBAAyB,CAAC;AAEzE,iEAAiE;AACjE,MAAM,CAAC,MAAM,iBAAiB,GAAG,sBAAsB,CAAC;AAExD,+EAA+E;AAC/E,MAAM,CAAC,MAAM,4BAA4B,GAAG,+BAA+B,CAAC;AAE5E,yDAAyD;AACzD,MAAM,CAAC,MAAM,cAAc,GAAG,2DAA2D,CAAC;AAE1F,6DAA6D;AAC7D,MAAM,CAAC,MAAM,mBAAmB,GAAG,6DAA6D,CAAC;AAEjG,sFAAsF;AACtF,MAAM,CAAC,MAAM,yBAAyB,GAAG,yEAAyE,CAAC;AAEnH;;;uBAGuB;AACvB,MAAM,CAAC,MAAM,cAAc,GAAG,sBAAsB,CAAC;AACrD,MAAM,CAAC,MAAM,aAAa,GAAG,eAAe,CAAC;AAC7C,MAAM,CAAC,MAAM,aAAa,GAAG,0BAA0B,CAAC;AAExD;;;;GAIG;AACH,MAAM,CAAC,MAAM,cAAc,GAAsB;IAC/C,cAAc;IACd,mBAAmB;IACnB,yBAAyB;IACzB,cAAc;IACd,aAAa;IACb,aAAa;CACd,CAAC;AAEF;;;;;;;GAOG;AACH,MAAM,CAAC,MAAM,iBAAiB,GAAsB;IAClD,oBAAoB;IACpB,yBAAyB;IACzB,+BAA+B;IAC/B,iBAAiB;CAClB,CAAC","sourcesContent":["/**\n * The placeholder strings `loam new` scaffolds, spelled ONCE.\n *\n * Both sides of the placeholder gate read this module: the templates in\n * `commands/new/templates.ts` and `commands/new/capability-template.ts` write\n * these exact strings, and the coherence\n * check in `./scaffold.ts` refuses to archive them. Spelling a sentinel in two\n * places is how the gate stops matching the scaffold — a one-character rewording\n * of a template would silently retire the check that guards it.\n *\n * Detection is EXACT on purpose. A requirement whose author genuinely writes\n * \"TODO\" in prose is that author's call to make; only the strings loam itself\n * scaffolded are ones nobody wrote.\n */\n\n/** The scaffolded business-requirement heading (`specs/<svc>/spec.md`). */\nexport const REQUIREMENT_SENTINEL = \"TODO — name the behaviour\";\n\n/** The scaffolded architecture-requirement heading (`specs/<svc>/arch.spec.md`). */\nexport const ARCH_REQUIREMENT_SENTINEL = \"TODO — name the architectural obligation\";\n\n/**\n * The scaffolded capability-requirement heading\n * (`features/<FEAT>/capabilities/<id>/spec.md`).\n *\n * Its own string rather than a reuse of `REQUIREMENT_SENTINEL`, because the two\n * documents ask for different things and the heading is where the template says\n * so: a service requirement names a BEHAVIOUR the service performs, a capability\n * requirement names a PROMISE the fleet keeps. A shared \"TODO — name the\n * behaviour\" would scaffold the business document with the altitude\n * `capability.requirement-service-scoped` exists to refuse.\n */\nexport const CAPABILITY_REQUIREMENT_SENTINEL = \"TODO — name the promise\";\n\n/** The scaffolded scenario heading, shared by both spec axes. */\nexport const SCENARIO_SENTINEL = \"TODO — name the case\";\n\n/** The scaffolded description on a `--new-service` element in delta.likec4. */\nexport const SERVICE_DESCRIPTION_SENTINEL = \"TODO — what this service owns\";\n\n/** The scaffolded SHALL fill-in on the business axis. */\nexport const SHALL_SENTINEL = \"<observable behaviour, testable without reading the code>\";\n\n/** The scaffolded SHALL fill-in on the architecture axis. */\nexport const ARCH_SHALL_SENTINEL = \"<the operational/integration behaviour, observable in test>\";\n\n/** The scaffolded SHALL fill-in on the business axis — a promise, not a mechanism. */\nexport const CAPABILITY_SHALL_SENTINEL = \"<the promise, observable outside the fleet — a customer could check it>\";\n\n/** The scaffolded Given/When/Then fill-ins, shared by both spec axes. Three\n * constants rather than one keyed object because a `then` property makes an\n * object thenable — one accidental `await` away from a hang the linter\n * refuses outright. */\nexport const GIVEN_SENTINEL = \"<the starting state>\";\nexport const WHEN_SENTINEL = \"<the trigger>\";\nexport const THEN_SENTINEL = \"<the observable outcome>\";\n\n/**\n * The angle-bracket fill-ins inside scaffolded requirement bodies and steps.\n * Any one of them surviving to archive means the line around it was never\n * authored — the brackets are the template telling the author what to replace.\n */\nexport const BODY_SENTINELS: readonly string[] = [\n SHALL_SENTINEL,\n ARCH_SHALL_SENTINEL,\n CAPABILITY_SHALL_SENTINEL,\n GIVEN_SENTINEL,\n WHEN_SENTINEL,\n THEN_SENTINEL,\n];\n\n/**\n * Every scaffolded heading a requirement or scenario may still carry.\n *\n * The list is not partitioned by which document scaffolded which string, and\n * that is deliberate: the gate asks \"did anybody author this line\", and a\n * capability heading pasted into a service delta was authored exactly as little\n * as it was in the document it came from.\n */\nexport const HEADING_SENTINELS: readonly string[] = [\n REQUIREMENT_SENTINEL,\n ARCH_REQUIREMENT_SENTINEL,\n CAPABILITY_REQUIREMENT_SENTINEL,\n SCENARIO_SENTINEL,\n];\n"]}
|
|
@@ -9,7 +9,11 @@ import { featureSpecServices } from "../repo/repo.js";
|
|
|
9
9
|
import { enumeratedServiceIds, enumeratedServiceIndex } from "../repo/service-target.js";
|
|
10
10
|
import { serviceOperationIds } from "../openapi/doc.js";
|
|
11
11
|
import { declaredByService } from "./declared.js";
|
|
12
|
+
import { retiredLookup } from "./retired/archived.js";
|
|
13
|
+
import { revivedOperations } from "./retired/revival/revived.js";
|
|
12
14
|
import { coherenceLookups } from "./lookups.js";
|
|
15
|
+
import { consumerIndex } from "../consumers/consumers.js";
|
|
16
|
+
import { consumedOperationIssues } from "./consumed/operations.js";
|
|
13
17
|
import { eventCoherence } from "./events/events.js";
|
|
14
18
|
import { authoringIssues } from "./authoring/scaffold.js";
|
|
15
19
|
import { glossaryDeltaIssues } from "../glossary/delta.js";
|
|
@@ -135,21 +139,27 @@ export async function featureCoherence(request) {
|
|
|
135
139
|
// documents whose merge would publish content nobody authored — the exact
|
|
136
140
|
// template strings `loam new` wrote (authoring/scaffold.ts owns the list).
|
|
137
141
|
issues.push(...(await authoringIssues({ featureDir, featureId, splicedEls: elements.filter(ridesWithTag), svcNames, context })));
|
|
138
|
-
const { reqOps, removingOps, featureApiOps, unreadableApis, } = await declaredByService(scope, svcNames, issues, context);
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
142
|
+
const { reqOps, removingOps, featureApiOps, unreadableApis, revivalCandidates, displacedOps, } = await declaredByService(scope, svcNames, issues, context);
|
|
143
|
+
// A revival: an operation the merged contract would answer that the living
|
|
144
|
+
// contract no longer has because an ARCHIVED feature retired it — or would
|
|
145
|
+
// answer again at an address an archived feature took it out of, beside
|
|
146
|
+
// where it lives now — and the merge would read it as an addition and put
|
|
147
|
+
// it back (./retired/ says why the gate could not see this before). One
|
|
148
|
+
// lazy handle for both contract axes, so the walk of features/archive/ is
|
|
149
|
+
// paid once per call at most, and only when the merged contract answers
|
|
150
|
+
// something at an address the living one does not.
|
|
151
|
+
const retired = retiredLookup(docsDir, context);
|
|
152
|
+
for (const [svc, arrived] of revivalCandidates) {
|
|
153
|
+
if (arrived.length === 0)
|
|
154
|
+
continue;
|
|
155
|
+
issues.push(...revivedOperations(svc, arrived, await retired()));
|
|
152
156
|
}
|
|
157
|
+
const { governedByLivingSpec, definedElsewhere, deprecatedInLiving, undeprecatedByFeature } = coherenceLookups(scope, context);
|
|
158
|
+
// --- taking an operation out of the contract while the fleet still calls it ---
|
|
159
|
+
// ./consumed/operations.ts owns the gate; the index is built here because
|
|
160
|
+
// the event axis below asks the same one, so the fleet is read once.
|
|
161
|
+
const consumers = consumerIndex({ docsDir, context, afterFeature: featureDir });
|
|
162
|
+
issues.push(...(await consumedOperationIssues({ consumers, removingOps, displacedOps })));
|
|
153
163
|
// E1: Spec -> API — every operation a requirement governs must exist in that service's OpenAPI.
|
|
154
164
|
for (const [svc, ops] of reqOps) {
|
|
155
165
|
// The union below reads the feature's own contract; unreadable means the
|
|
@@ -203,7 +213,11 @@ export async function featureCoherence(request) {
|
|
|
203
213
|
if (contractUnreadable) {
|
|
204
214
|
// openapi.invalid (declared.ts) already gates; one breach, one finding.
|
|
205
215
|
}
|
|
206
|
-
else if (pathable !== undefined &&
|
|
216
|
+
else if (pathable !== undefined &&
|
|
217
|
+
// An operation replaced at its address leaves as surely as a retired
|
|
218
|
+
// one; `available` below still lists it, because serviceOperationIds
|
|
219
|
+
// subtracts markers only, so without this a new edge onto it passed.
|
|
220
|
+
(removingOps.get(pathable)?.has(r.op) === true || displacedOps.get(pathable)?.some((d) => d.id === r.op) === true)) {
|
|
207
221
|
issues.push({ severity: "error", code: "c4-api.op-removing", message: `${svcOf(r.source)} builds new consumption on '${r.op}', which this feature removes from ${target}` });
|
|
208
222
|
}
|
|
209
223
|
else if (!available.includes(r.op)) {
|
|
@@ -288,7 +302,7 @@ export async function featureCoherence(request) {
|
|
|
288
302
|
// feature's asyncapi deltas (pins, markers, justification), the tagged
|
|
289
303
|
// edges' publishes/consumes metadata, and the requirement deltas'
|
|
290
304
|
// Publishes:/Consumes: lines (./events/events.ts owns the walk).
|
|
291
|
-
issues.push(...(await eventCoherence({ scope, svcNames, taggedRels, svcOf, context })));
|
|
305
|
+
issues.push(...(await eventCoherence({ scope, svcNames, taggedRels, svcOf, context, retired, consumers })));
|
|
292
306
|
return issues;
|
|
293
307
|
}
|
|
294
308
|
//# sourceMappingURL=coherence.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"coherence.js","sourceRoot":"","sources":["../../../src/core/coherence/coherence.ts"],"names":[],"mappings":"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","sourcesContent":["import { existsSync } from \"node:fs\";\nimport { loadFile, type Elem, type LoadedDoc, type Rel } from \"../c4/likec4.js\";\nimport { elementService, serviceResolver } from \"../c4/resolve/service.js\";\nimport { extendingModels } from \"../c4/service-model/fleet/extending.js\";\nimport { serviceIdProblem } from \"../kernel/ids/service.js\";\nimport { deltaShapeIssues } from \"../delta/delta.js\";\nimport type { Issue } from \"../vocabulary/issue.js\";\nimport { featurePaths } from \"../repo/paths.js\";\nimport { featureSpecServices } from \"../repo/repo.js\";\nimport { enumeratedServiceIds, enumeratedServiceIndex } from \"../repo/service-target.js\";\nimport { serviceOperationIds } from \"../openapi/doc.js\";\nimport type { FleetContext } from \"../fleet-context.js\";\nimport { declaredByService, type DeltaScope } from \"./declared.js\";\nimport { coherenceLookups } from \"./lookups.js\";\nimport { eventCoherence } from \"./events/events.js\";\nimport { authoringIssues } from \"./authoring/scaffold.js\";\nimport { glossaryDeltaIssues } from \"../glossary/delta.js\";\nimport { flowDeltaIssues } from \"../usecases/delta/flows.js\";\nimport { deploymentDeltaIssues, readFeatureDeployments } from \"../deployment/delta.js\";\nimport type { DocsDir, FeatureDir } from \"../kernel/ids/dirs.js\";\n\n\n/**\n * Cross-axis consistency for a feature: do C4 (architecture), requirements (behaviour),\n * and OpenAPI (contract) agree? Errors are hard (would corrupt the living docs on archive);\n * warnings surface softer misalignments.\n *\n * `preloadedDelta` is the feature's already-parsed delta.likec4, when the caller\n * has it — loading a LikeC4 document spins up a fresh Langium workspace, the\n * dominant per-feature cost, and validate/archive have both parsed the same file\n * moments earlier. Standalone calls omit it and nothing changes: the file is\n * loaded here, exactly as before.\n */\n\n/** How a caller names the feature being graded, and what it already has in hand. */\nexport interface CoherenceRequest {\n docsDir: DocsDir;\n featureDir: FeatureDir;\n featureId: string;\n /** The delta LikeC4 document, when the caller already parsed it. */\n preloadedDelta?: LoadedDoc;\n context?: FleetContext;\n}\n\nexport async function featureCoherence(request: CoherenceRequest): Promise<Issue[]> {\n const { docsDir, featureDir, featureId, preloadedDelta, context } = request;\n const scope: DeltaScope = { docsDir, featureDir, featureId };\n // Delta shape first: a diff that does not apply to the living spec explains\n // everything downstream, and it is the one breach that is silent without a check.\n const issues: Issue[] = await deltaShapeIssues(docsDir, featureDir, featureId, context);\n\n // The vocabulary axis, and it is asked first because it is cheap and\n // independent: one `existsSync` for a feature with no glossary/, and nothing\n // downstream reads its answer. A term this feature introduces that the living\n // glossary already defines is the one thing that can go wrong on a create-only\n // merge, and it must be caught before the copy, not after.\n issues.push(...(await glossaryDeltaIssues(docsDir, featureDir)));\n\n // The use-case axis, and it is asked here for the vocabulary axis's reasons\n // exactly: one walk over a directory that is usually absent, nothing\n // downstream reads its answer, and a create-only merge has one thing that can\n // go wrong. A flow this feature introduces that the living `architecture/`\n // already holds must be caught before the copy, not after it has replaced an\n // authored hop sequence.\n issues.push(...(await flowDeltaIssues(docsDir, featureDir)));\n issues.push(...(await deploymentDeltaIssues(docsDir, featureDir)));\n\n // And the topology's second question, which the collision check above cannot\n // ask: does the document READ against the map this feature's own merge would\n // leave behind. It matters more literally here than one axis over — a feature\n // that stands a service up in a new cluster declares that service in its\n // `delta.likec4` and instances it in the topology, so the element the document\n // names exists only post-merge — and without it the archive copies an\n // unreadable document into `architecture/` for the next reader's\n // `loam validate --all` to fail on, against a file they did not write.\n const topology = await readFeatureDeployments({\n docsDir,\n featureDir,\n featureId,\n // A function rather than the context itself, for the reason the use-case\n // overlay states: one memoised read does not justify an edge from\n // `core/deployment/` to `fleet-context`.\n ...(context === undefined ? {} : { load: (path: string) => context.loadLikeC4(path) }),\n // The fleet's extending models, so the preview is the map the archive would\n // really leave: a nested addition is routed into the service's own model,\n // never onto the map. Read here for the same reason `load` is passed here —\n // this module holds the context and `core/deployment/` does not — and as a\n // thunk, so a feature with no `deployment/` directory pays nothing for it.\n models: () => extendingModels(docsDir, context),\n });\n if (topology.kind === \"unreadable\") {\n issues.push({\n severity: \"error\",\n code: \"deployment.doc-invalid\",\n subject: featureId,\n // The opening sentence does not say whose file is at fault, on the\n // `usecase.flow-invalid` message's reasoning: the corpus is the feature's\n // `deployment/` PLUS the living `architecture/` documents it extends, so\n // the failing path can be a living one — a landscape that no longer\n // parses, a topology somebody else wrote. Every message carries its own\n // path, so a reader can place it.\n message:\n `this feature's deployment document(s) could not be read against the map its own merge would leave behind, ` +\n `so the archive would land an architecture/ nothing has checked. The corpus is the feature's deployment/ AND ` +\n `the living architecture/ documents it extends, so the fault may be in either — each message names its file. ` +\n `${topology.errors.length} problem(s): ${topology.errors.join(\"; \")}`,\n });\n }\n\n // --- C4 delta ---\n let elements: Elem[] = [];\n let taggedEls: Elem[] = [];\n let taggedRels: Rel[] = [];\n const deltaPath = featurePaths(featureDir).delta;\n if (existsSync(deltaPath)) {\n const res = preloadedDelta ?? (context === undefined ? await loadFile(deltaPath) : await context.loadLikeC4(deltaPath));\n if (res.errors.length > 0) {\n // An unreadable architecture axis can prove nothing — it must never count as coherent.\n issues.push({\n severity: \"error\",\n code: \"delta.invalid\",\n message: `delta.likec4 has ${res.errors.length} parse error(s) — architecture axis unreadable (run \\`loam validate --feature ${featureId}\\`)`,\n });\n } else {\n elements = res.elements;\n taggedEls = res.elements.filter((e) => e.tags.includes(featureId));\n taggedRels = res.relationships.filter((r) => r.tags.includes(featureId));\n // Every rule below filters by the feature tag, so a delta whose author\n // forgot the tags entirely passes every check and archives while merging\n // nothing. Whole-file only: once ANYTHING is tagged, deciding that an\n // untagged edge \"looks intended\" would be guessing.\n if (\n (res.elements.length > 0 || res.relationships.length > 0) &&\n taggedEls.length === 0 &&\n taggedRels.length === 0\n ) {\n issues.push({\n severity: \"error\",\n code: \"delta.nothing-tagged\",\n message: `delta.likec4 declares elements/relationships but none are tagged #${featureId}; loam cannot see untagged changes`,\n });\n }\n }\n }\n // --- per-service specs (requirement operations) + openapi deltas ---\n const svcNames = await featureSpecServices(featureDir, context);\n\n // Every axis below joins on the service id, so endpoints resolve through the\n // element's `metadata { service }` binding — never through what the box is\n // called. The enumerated fleet — `services/` plus this feature's own\n // `specs/` directories, the same union `enumeratedTarget` probes below —\n // rides into the resolver so a delta that models CONTAINERS joins correctly:\n // without it, an edge into `payment.api` resolves to a service called \"api\"\n // that has never existed, and every check below grades the edge against an\n // absent service instead of the one that owns the container. An unenumerable\n // services/ falls back to the feature's own names — every living probe would\n // have found nothing there anyway.\n const fleetIds = await enumeratedServiceIds(docsDir, context);\n const svcOf = serviceResolver(elements, new Set<string>([...fleetIds, ...svcNames]));\n\n // Which elements the landscape merge would SPLICE: the tagged ones and\n // everything nested inside their blocks. Defined here because two checks\n // share it — the scaffold gate just below, and the binding-grammar check\n // near the end of this function — and the merge carries a tagged element's\n // authored block over byte for byte, children included, so a scope narrower\n // than the splice lets a nested child's text ride through unread.\n const taggedIds = taggedEls.map((el) => el.id);\n const ridesWithTag = (el: Elem): boolean =>\n el.tags.includes(featureId) || taggedIds.some((id) => el.id.startsWith(`${id}.`));\n\n // The scaffold gate: placeholder text and an unwritten intent are legal\n // documents whose merge would publish content nobody authored — the exact\n // template strings `loam new` wrote (authoring/scaffold.ts owns the list).\n issues.push(\n ...(await authoringIssues({ featureDir, featureId, splicedEls: elements.filter(ridesWithTag), svcNames, context })),\n );\n const {\n reqOps, removingOps, featureApiOps, unreadableApis,\n } = await declaredByService(scope, svcNames, issues, context);\n const {\n governedByLivingSpec, edgeConsumers, requirementConsumers, definedElsewhere,\n deprecatedInLiving, undeprecatedByFeature,\n } = coherenceLookups(scope, context);\n\n for (const [svc, retiring] of removingOps) {\n for (const op of retiring) {\n const consumers = [...(await edgeConsumers(svc, op)), ...(await requirementConsumers(svc, op))];\n if (consumers.length === 0) continue;\n issues.push({\n severity: \"error\",\n code: \"openapi.remove-op-consumed\",\n subject: svc,\n message: `${svc}: this feature retires '${op}', but the living fleet still consumes it — ${consumers.join(\"; \")}. Retire the consumer in the same feature (drop the edge from architecture/landscape.likec4, or the requirement that names it), or archive with --approve to break it deliberately.`,\n });\n }\n }\n\n // E1: Spec -> API — every operation a requirement governs must exist in that service's OpenAPI.\n for (const [svc, ops] of reqOps) {\n // The union below reads the feature's own contract; unreadable means the\n // op may well be defined in the file nobody could open, and `op-undefined`\n // would point the author at the requirement when the truth is the YAML.\n // `openapi.invalid` (declared.ts) already gates; one breach, one finding.\n if (unreadableApis.has(svc)) continue;\n const available = await serviceOperationIds(docsDir, svc, featureDir, context);\n for (const op of ops) {\n if (available.includes(op)) continue;\n const other = await definedElsewhere(svc, op);\n if (other !== undefined) {\n issues.push({ severity: \"warn\", code: \"spec-api.op-pending\", message: `requirement in ${svc} governs '${op}', defined by in-flight ${other} — archive it first` });\n continue;\n }\n issues.push({ severity: \"error\", code: \"spec-api.op-undefined\", message: `requirement in ${svc} governs '${op}', not defined in ${svc}'s OpenAPI` });\n }\n }\n\n // The declared→enumerated bridge for edge TARGETS. `svcOf` answers with\n // whatever the document wrote — `metadata { service '../../x' }` parses in\n // LikeC4 without one error — and every living lookup below joins its answer\n // into `services/<target>/` or `specs/<target>/`. So a target resolves\n // through the enumerations first (the fleet's `services/` plus this\n // feature's own `specs/`, where a service it introduces lives), and a name\n // neither knows skips the lookups — no ops, not governed, not deprecated —\n // grading exactly as an absent service, spelled with the declared name.\n const enumeratedTarget = enumeratedServiceIndex(docsDir, svcNames, context);\n\n // E2 / W1 / W4: C4 edges vs API + requirements.\n for (const r of taggedRels) {\n if (r.op === undefined) {\n if ((r.title ?? \"\").toLowerCase().startsWith(\"call\")) {\n issues.push({ severity: \"warn\", code: \"c4.op-link-missing\", message: `edge ${svcOf(r.source)} → ${svcOf(r.target)} (\"${r.title}\") has no operation link (metadata { op })` });\n }\n continue;\n }\n const target = svcOf(r.target);\n const pathable = await enumeratedTarget(target);\n // Same suspension as E1, but only for the verdicts that READ a contract:\n // does the op exist, is it being removed, is it deprecated (whose\n // un-deprecation escape reads the feature contract too). Each of those\n // would be an opinion about a file nobody could open. The governance\n // question below is deliberately NOT suspended — it joins the edge against\n // requirement documents alone, so the broken YAML could never change its\n // answer, and silencing it deferred a real warning for no reason.\n const contractUnreadable = pathable !== undefined && unreadableApis.has(pathable);\n const available =\n pathable === undefined || contractUnreadable\n ? []\n : await serviceOperationIds(docsDir, pathable, featureDir, context);\n if (contractUnreadable) {\n // openapi.invalid (declared.ts) already gates; one breach, one finding.\n } else if (pathable !== undefined && removingOps.get(pathable)?.has(r.op) === true) {\n issues.push({ severity: \"error\", code: \"c4-api.op-removing\", message: `${svcOf(r.source)} builds new consumption on '${r.op}', which this feature removes from ${target}` });\n } else if (!available.includes(r.op)) {\n const other = await definedElsewhere(target, r.op);\n if (other !== undefined) {\n issues.push({ severity: \"warn\", code: \"c4-api.op-pending\", message: `${svcOf(r.source)} calls '${r.op}' on ${target}, defined by in-flight ${other} — archive it first` });\n } else {\n issues.push({ severity: \"error\", code: \"c4-api.op-undefined\", message: `${svcOf(r.source)} calls '${r.op}' on ${target}, but ${target}'s OpenAPI does not define it (contract broken)` });\n }\n }\n // Governed by THIS target's requirements, not by anybody's. An operationId\n // is unique only inside one contract, so the fleet-wide set this used to\n // ask meant a `getStatus` governed in service A silenced the question for\n // service B's entirely unrelated `getStatus` — the warning went quiet for\n // the edge that needed it, and only in repos where some other service\n // happened to reuse the name. The living half on the same line has always\n // joined per service (`governedByLivingSpec(target, …)`); this is the delta\n // half asking it the same way.\n if (pathable === undefined || (!(reqOps.get(pathable) ?? []).includes(r.op) && !(await governedByLivingSpec(pathable, r.op)))) {\n issues.push({ severity: \"warn\", code: \"c4.op-ungoverned\", message: `'${r.op}' is called by ${svcOf(r.source)} but no requirement governs it` });\n }\n // Lifecycle: this NEW tagged edge builds consumption on an operation the\n // living provider contract already marks deprecated. Advisory, never an\n // archive gate — the edge is legal and the contract holds — but new\n // consumption of a dying op deserves an eye before it ships. Quiet when\n // the feature's own openapi delta drops the flag: that state is the fix\n // in progress, not new consumption of a dying op.\n if (!contractUnreadable && pathable !== undefined && (await deprecatedInLiving(pathable, r.op)) && !(await undeprecatedByFeature(pathable, r.op))) {\n issues.push({ severity: \"warn\", code: \"c4-api.op-deprecated\", message: `${svcOf(r.source)} builds new consumption on '${r.op}', which ${target}'s living OpenAPI marks deprecated — prefer the replacement operation` });\n }\n }\n\n // W2: API -> C4 — every operation the feature adds should be consumed by some edge.\n const consumed = new Set(taggedRels.map((r) => r.op).filter((o): o is string => o !== undefined));\n for (const op of featureApiOps) {\n if (!consumed.has(op)) {\n issues.push({ severity: \"warn\", code: \"api.op-unconsumed\", message: `operation '${op}' is added but no architecture edge consumes it (provider-only or unmodeled)` });\n }\n }\n\n // W3: a new service should carry a requirement delta.\n for (const e of taggedEls) {\n const svc: string = elementService(e);\n if (e.kind === \"softwareSystem\" && !svcNames.some((n) => n === svc)) {\n issues.push({ severity: \"warn\", code: \"service.no-requirement-delta\", message: `new service ${svc} has no requirement delta under specs/` });\n }\n }\n\n // An explicit `metadata { service '<id>' }` binding, held to the same grammar\n // every `--service` flag is — on the tagged elements AND everything nested\n // inside their blocks. The scope is the splice's, not the tag's: the\n // landscape merge carries a tagged element's authored block over byte for\n // byte, children included (landscape-merge.ts's rides() exists exactly so a\n // child travels inside its parent's text), so an untagged container's\n // binding reaches the living map as surely as its tagged parent's — and\n // checking only the tagged elements themselves let it ride through unread.\n // The binding is the one name the archive TRUSTS: the merge splices it into\n // the living map verbatim, and the new-service scan probes\n // `services/<binding>/` with it — where `metadata { service '../outside-svc' }`,\n // which LikeC4 parses without a murmur, collapses the probe right out of\n // services/, and the spliced map fails the very next `validate --all`\n // (`landscape.binding-unknown`). Explicit bindings only, on purpose: a prose\n // title with no binding is legal C4, and a title becomes a path only through\n // `specs/<svc>/`, whose own grammar check (`delta.service-id-invalid`)\n // guards that route. `ridesWithTag` itself is defined up beside the scaffold\n // gate, which shares the splice's scope for the same reason.\n for (const e of elements.filter(ridesWithTag)) {\n if (e.service === undefined) continue;\n const problem = serviceIdProblem(e.service, \"metadata { service } binding\");\n if (problem === null) continue;\n issues.push({\n severity: \"error\",\n code: \"c4.service-binding-invalid\",\n subject: e.service,\n message:\n `'${e.title}' (${e.id}) — ${problem} ` +\n `\\`loam archive\\` would splice '${e.service}' into the living landscape.likec4 exactly as written, ` +\n `and probe services/${e.service}/ with it. Bind the element to the services/<id>/ directory it means.`,\n });\n }\n\n // The event axis — the same three-way agreement the operation checks above\n // grade, joined on the message name instead of the operationId: the\n // feature's asyncapi deltas (pins, markers, justification), the tagged\n // edges' publishes/consumes metadata, and the requirement deltas'\n // Publishes:/Consumes: lines (./events/events.ts owns the walk).\n issues.push(...(await eventCoherence({ scope, svcNames, taggedRels, svcOf, context })));\n\n return issues;\n}\n"]}
|
|
1
|
+
{"version":3,"file":"coherence.js","sourceRoot":"","sources":["../../../src/core/coherence/coherence.ts"],"names":[],"mappings":"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","sourcesContent":["import { existsSync } from \"node:fs\";\nimport { loadFile, type Elem, type LoadedDoc, type Rel } from \"../c4/likec4.js\";\nimport { elementService, serviceResolver } from \"../c4/resolve/service.js\";\nimport { extendingModels } from \"../c4/service-model/fleet/extending.js\";\nimport { serviceIdProblem } from \"../kernel/ids/service.js\";\nimport { deltaShapeIssues } from \"../delta/delta.js\";\nimport type { Issue } from \"../vocabulary/issue.js\";\nimport { featurePaths } from \"../repo/paths.js\";\nimport { featureSpecServices } from \"../repo/repo.js\";\nimport { enumeratedServiceIds, enumeratedServiceIndex } from \"../repo/service-target.js\";\nimport { serviceOperationIds } from \"../openapi/doc.js\";\nimport type { FleetContext } from \"../fleet-context.js\";\nimport { declaredByService, type DeltaScope } from \"./declared.js\";\nimport { retiredLookup } from \"./retired/archived.js\";\nimport { revivedOperations } from \"./retired/revival/revived.js\";\nimport { coherenceLookups } from \"./lookups.js\";\nimport { consumerIndex } from \"../consumers/consumers.js\";\nimport { consumedOperationIssues } from \"./consumed/operations.js\";\nimport { eventCoherence } from \"./events/events.js\";\nimport { authoringIssues } from \"./authoring/scaffold.js\";\nimport { glossaryDeltaIssues } from \"../glossary/delta.js\";\nimport { flowDeltaIssues } from \"../usecases/delta/flows.js\";\nimport { deploymentDeltaIssues, readFeatureDeployments } from \"../deployment/delta.js\";\nimport type { DocsDir, FeatureDir } from \"../kernel/ids/dirs.js\";\n\n/**\n * Cross-axis consistency for a feature: do C4 (architecture), requirements (behaviour),\n * and OpenAPI (contract) agree? Errors are hard (would corrupt the living docs on archive);\n * warnings surface softer misalignments.\n *\n * `preloadedDelta` is the feature's already-parsed delta.likec4, when the caller\n * has it — loading a LikeC4 document spins up a fresh Langium workspace, the\n * dominant per-feature cost, and validate/archive have both parsed the same file\n * moments earlier. Standalone calls omit it and nothing changes: the file is\n * loaded here, exactly as before.\n */\n\n/** How a caller names the feature being graded, and what it already has in hand. */\nexport interface CoherenceRequest {\n docsDir: DocsDir;\n featureDir: FeatureDir;\n featureId: string;\n /** The delta LikeC4 document, when the caller already parsed it. */\n preloadedDelta?: LoadedDoc;\n context?: FleetContext;\n}\n\nexport async function featureCoherence(request: CoherenceRequest): Promise<Issue[]> {\n const { docsDir, featureDir, featureId, preloadedDelta, context } = request;\n const scope: DeltaScope = { docsDir, featureDir, featureId };\n // Delta shape first: a diff that does not apply to the living spec explains\n // everything downstream, and it is the one breach that is silent without a check.\n const issues: Issue[] = await deltaShapeIssues(docsDir, featureDir, featureId, context);\n\n // The vocabulary axis, and it is asked first because it is cheap and\n // independent: one `existsSync` for a feature with no glossary/, and nothing\n // downstream reads its answer. A term this feature introduces that the living\n // glossary already defines is the one thing that can go wrong on a create-only\n // merge, and it must be caught before the copy, not after.\n issues.push(...(await glossaryDeltaIssues(docsDir, featureDir)));\n\n // The use-case axis, and it is asked here for the vocabulary axis's reasons\n // exactly: one walk over a directory that is usually absent, nothing\n // downstream reads its answer, and a create-only merge has one thing that can\n // go wrong. A flow this feature introduces that the living `architecture/`\n // already holds must be caught before the copy, not after it has replaced an\n // authored hop sequence.\n issues.push(...(await flowDeltaIssues(docsDir, featureDir)));\n issues.push(...(await deploymentDeltaIssues(docsDir, featureDir)));\n\n // And the topology's second question, which the collision check above cannot\n // ask: does the document READ against the map this feature's own merge would\n // leave behind. It matters more literally here than one axis over — a feature\n // that stands a service up in a new cluster declares that service in its\n // `delta.likec4` and instances it in the topology, so the element the document\n // names exists only post-merge — and without it the archive copies an\n // unreadable document into `architecture/` for the next reader's\n // `loam validate --all` to fail on, against a file they did not write.\n const topology = await readFeatureDeployments({\n docsDir,\n featureDir,\n featureId,\n // A function rather than the context itself, for the reason the use-case\n // overlay states: one memoised read does not justify an edge from\n // `core/deployment/` to `fleet-context`.\n ...(context === undefined ? {} : { load: (path: string) => context.loadLikeC4(path) }),\n // The fleet's extending models, so the preview is the map the archive would\n // really leave: a nested addition is routed into the service's own model,\n // never onto the map. Read here for the same reason `load` is passed here —\n // this module holds the context and `core/deployment/` does not — and as a\n // thunk, so a feature with no `deployment/` directory pays nothing for it.\n models: () => extendingModels(docsDir, context),\n });\n if (topology.kind === \"unreadable\") {\n issues.push({\n severity: \"error\",\n code: \"deployment.doc-invalid\",\n subject: featureId,\n // The opening sentence does not say whose file is at fault, on the\n // `usecase.flow-invalid` message's reasoning: the corpus is the feature's\n // `deployment/` PLUS the living `architecture/` documents it extends, so\n // the failing path can be a living one — a landscape that no longer\n // parses, a topology somebody else wrote. Every message carries its own\n // path, so a reader can place it.\n message:\n `this feature's deployment document(s) could not be read against the map its own merge would leave behind, ` +\n `so the archive would land an architecture/ nothing has checked. The corpus is the feature's deployment/ AND ` +\n `the living architecture/ documents it extends, so the fault may be in either — each message names its file. ` +\n `${topology.errors.length} problem(s): ${topology.errors.join(\"; \")}`,\n });\n }\n\n // --- C4 delta ---\n let elements: Elem[] = [];\n let taggedEls: Elem[] = [];\n let taggedRels: Rel[] = [];\n const deltaPath = featurePaths(featureDir).delta;\n if (existsSync(deltaPath)) {\n const res = preloadedDelta ?? (context === undefined ? await loadFile(deltaPath) : await context.loadLikeC4(deltaPath));\n if (res.errors.length > 0) {\n // An unreadable architecture axis can prove nothing — it must never count as coherent.\n issues.push({\n severity: \"error\",\n code: \"delta.invalid\",\n message: `delta.likec4 has ${res.errors.length} parse error(s) — architecture axis unreadable (run \\`loam validate --feature ${featureId}\\`)`,\n });\n } else {\n elements = res.elements;\n taggedEls = res.elements.filter((e) => e.tags.includes(featureId));\n taggedRels = res.relationships.filter((r) => r.tags.includes(featureId));\n // Every rule below filters by the feature tag, so a delta whose author\n // forgot the tags entirely passes every check and archives while merging\n // nothing. Whole-file only: once ANYTHING is tagged, deciding that an\n // untagged edge \"looks intended\" would be guessing.\n if (\n (res.elements.length > 0 || res.relationships.length > 0) &&\n taggedEls.length === 0 &&\n taggedRels.length === 0\n ) {\n issues.push({\n severity: \"error\",\n code: \"delta.nothing-tagged\",\n message: `delta.likec4 declares elements/relationships but none are tagged #${featureId}; loam cannot see untagged changes`,\n });\n }\n }\n }\n // --- per-service specs (requirement operations) + openapi deltas ---\n const svcNames = await featureSpecServices(featureDir, context);\n\n // Every axis below joins on the service id, so endpoints resolve through the\n // element's `metadata { service }` binding — never through what the box is\n // called. The enumerated fleet — `services/` plus this feature's own\n // `specs/` directories, the same union `enumeratedTarget` probes below —\n // rides into the resolver so a delta that models CONTAINERS joins correctly:\n // without it, an edge into `payment.api` resolves to a service called \"api\"\n // that has never existed, and every check below grades the edge against an\n // absent service instead of the one that owns the container. An unenumerable\n // services/ falls back to the feature's own names — every living probe would\n // have found nothing there anyway.\n const fleetIds = await enumeratedServiceIds(docsDir, context);\n const svcOf = serviceResolver(elements, new Set<string>([...fleetIds, ...svcNames]));\n\n // Which elements the landscape merge would SPLICE: the tagged ones and\n // everything nested inside their blocks. Defined here because two checks\n // share it — the scaffold gate just below, and the binding-grammar check\n // near the end of this function — and the merge carries a tagged element's\n // authored block over byte for byte, children included, so a scope narrower\n // than the splice lets a nested child's text ride through unread.\n const taggedIds = taggedEls.map((el) => el.id);\n const ridesWithTag = (el: Elem): boolean =>\n el.tags.includes(featureId) || taggedIds.some((id) => el.id.startsWith(`${id}.`));\n\n // The scaffold gate: placeholder text and an unwritten intent are legal\n // documents whose merge would publish content nobody authored — the exact\n // template strings `loam new` wrote (authoring/scaffold.ts owns the list).\n issues.push(\n ...(await authoringIssues({ featureDir, featureId, splicedEls: elements.filter(ridesWithTag), svcNames, context })),\n );\n const {\n reqOps, removingOps, featureApiOps, unreadableApis, revivalCandidates, displacedOps,\n } = await declaredByService(scope, svcNames, issues, context);\n\n // A revival: an operation the merged contract would answer that the living\n // contract no longer has because an ARCHIVED feature retired it — or would\n // answer again at an address an archived feature took it out of, beside\n // where it lives now — and the merge would read it as an addition and put\n // it back (./retired/ says why the gate could not see this before). One\n // lazy handle for both contract axes, so the walk of features/archive/ is\n // paid once per call at most, and only when the merged contract answers\n // something at an address the living one does not.\n const retired = retiredLookup(docsDir, context);\n for (const [svc, arrived] of revivalCandidates) {\n if (arrived.length === 0) continue;\n issues.push(...revivedOperations(svc, arrived, await retired()));\n }\n const { governedByLivingSpec, definedElsewhere, deprecatedInLiving, undeprecatedByFeature } = coherenceLookups(scope, context);\n\n // --- taking an operation out of the contract while the fleet still calls it ---\n // ./consumed/operations.ts owns the gate; the index is built here because\n // the event axis below asks the same one, so the fleet is read once.\n const consumers = consumerIndex({ docsDir, context, afterFeature: featureDir });\n issues.push(...(await consumedOperationIssues({ consumers, removingOps, displacedOps })));\n\n // E1: Spec -> API — every operation a requirement governs must exist in that service's OpenAPI.\n for (const [svc, ops] of reqOps) {\n // The union below reads the feature's own contract; unreadable means the\n // op may well be defined in the file nobody could open, and `op-undefined`\n // would point the author at the requirement when the truth is the YAML.\n // `openapi.invalid` (declared.ts) already gates; one breach, one finding.\n if (unreadableApis.has(svc)) continue;\n const available = await serviceOperationIds(docsDir, svc, featureDir, context);\n for (const op of ops) {\n if (available.includes(op)) continue;\n const other = await definedElsewhere(svc, op);\n if (other !== undefined) {\n issues.push({ severity: \"warn\", code: \"spec-api.op-pending\", message: `requirement in ${svc} governs '${op}', defined by in-flight ${other} — archive it first` });\n continue;\n }\n issues.push({ severity: \"error\", code: \"spec-api.op-undefined\", message: `requirement in ${svc} governs '${op}', not defined in ${svc}'s OpenAPI` });\n }\n }\n\n // The declared→enumerated bridge for edge TARGETS. `svcOf` answers with\n // whatever the document wrote — `metadata { service '../../x' }` parses in\n // LikeC4 without one error — and every living lookup below joins its answer\n // into `services/<target>/` or `specs/<target>/`. So a target resolves\n // through the enumerations first (the fleet's `services/` plus this\n // feature's own `specs/`, where a service it introduces lives), and a name\n // neither knows skips the lookups — no ops, not governed, not deprecated —\n // grading exactly as an absent service, spelled with the declared name.\n const enumeratedTarget = enumeratedServiceIndex(docsDir, svcNames, context);\n\n // E2 / W1 / W4: C4 edges vs API + requirements.\n for (const r of taggedRels) {\n if (r.op === undefined) {\n if ((r.title ?? \"\").toLowerCase().startsWith(\"call\")) {\n issues.push({ severity: \"warn\", code: \"c4.op-link-missing\", message: `edge ${svcOf(r.source)} → ${svcOf(r.target)} (\"${r.title}\") has no operation link (metadata { op })` });\n }\n continue;\n }\n const target = svcOf(r.target);\n const pathable = await enumeratedTarget(target);\n // Same suspension as E1, but only for the verdicts that READ a contract:\n // does the op exist, is it being removed, is it deprecated (whose\n // un-deprecation escape reads the feature contract too). Each of those\n // would be an opinion about a file nobody could open. The governance\n // question below is deliberately NOT suspended — it joins the edge against\n // requirement documents alone, so the broken YAML could never change its\n // answer, and silencing it deferred a real warning for no reason.\n const contractUnreadable = pathable !== undefined && unreadableApis.has(pathable);\n const available =\n pathable === undefined || contractUnreadable\n ? []\n : await serviceOperationIds(docsDir, pathable, featureDir, context);\n if (contractUnreadable) {\n // openapi.invalid (declared.ts) already gates; one breach, one finding.\n } else if (\n pathable !== undefined &&\n // An operation replaced at its address leaves as surely as a retired\n // one; `available` below still lists it, because serviceOperationIds\n // subtracts markers only, so without this a new edge onto it passed.\n (removingOps.get(pathable)?.has(r.op) === true || displacedOps.get(pathable)?.some((d) => d.id === r.op) === true)\n ) {\n issues.push({ severity: \"error\", code: \"c4-api.op-removing\", message: `${svcOf(r.source)} builds new consumption on '${r.op}', which this feature removes from ${target}` });\n } else if (!available.includes(r.op)) {\n const other = await definedElsewhere(target, r.op);\n if (other !== undefined) {\n issues.push({ severity: \"warn\", code: \"c4-api.op-pending\", message: `${svcOf(r.source)} calls '${r.op}' on ${target}, defined by in-flight ${other} — archive it first` });\n } else {\n issues.push({ severity: \"error\", code: \"c4-api.op-undefined\", message: `${svcOf(r.source)} calls '${r.op}' on ${target}, but ${target}'s OpenAPI does not define it (contract broken)` });\n }\n }\n // Governed by THIS target's requirements, not by anybody's. An operationId\n // is unique only inside one contract, so the fleet-wide set this used to\n // ask meant a `getStatus` governed in service A silenced the question for\n // service B's entirely unrelated `getStatus` — the warning went quiet for\n // the edge that needed it, and only in repos where some other service\n // happened to reuse the name. The living half on the same line has always\n // joined per service (`governedByLivingSpec(target, …)`); this is the delta\n // half asking it the same way.\n if (pathable === undefined || (!(reqOps.get(pathable) ?? []).includes(r.op) && !(await governedByLivingSpec(pathable, r.op)))) {\n issues.push({ severity: \"warn\", code: \"c4.op-ungoverned\", message: `'${r.op}' is called by ${svcOf(r.source)} but no requirement governs it` });\n }\n // Lifecycle: this NEW tagged edge builds consumption on an operation the\n // living provider contract already marks deprecated. Advisory, never an\n // archive gate — the edge is legal and the contract holds — but new\n // consumption of a dying op deserves an eye before it ships. Quiet when\n // the feature's own openapi delta drops the flag: that state is the fix\n // in progress, not new consumption of a dying op.\n if (!contractUnreadable && pathable !== undefined && (await deprecatedInLiving(pathable, r.op)) && !(await undeprecatedByFeature(pathable, r.op))) {\n issues.push({ severity: \"warn\", code: \"c4-api.op-deprecated\", message: `${svcOf(r.source)} builds new consumption on '${r.op}', which ${target}'s living OpenAPI marks deprecated — prefer the replacement operation` });\n }\n }\n\n // W2: API -> C4 — every operation the feature adds should be consumed by some edge.\n const consumed = new Set(taggedRels.map((r) => r.op).filter((o): o is string => o !== undefined));\n for (const op of featureApiOps) {\n if (!consumed.has(op)) {\n issues.push({ severity: \"warn\", code: \"api.op-unconsumed\", message: `operation '${op}' is added but no architecture edge consumes it (provider-only or unmodeled)` });\n }\n }\n\n // W3: a new service should carry a requirement delta.\n for (const e of taggedEls) {\n const svc: string = elementService(e);\n if (e.kind === \"softwareSystem\" && !svcNames.some((n) => n === svc)) {\n issues.push({ severity: \"warn\", code: \"service.no-requirement-delta\", message: `new service ${svc} has no requirement delta under specs/` });\n }\n }\n\n // An explicit `metadata { service '<id>' }` binding, held to the same grammar\n // every `--service` flag is — on the tagged elements AND everything nested\n // inside their blocks. The scope is the splice's, not the tag's: the\n // landscape merge carries a tagged element's authored block over byte for\n // byte, children included (landscape-merge.ts's rides() exists exactly so a\n // child travels inside its parent's text), so an untagged container's\n // binding reaches the living map as surely as its tagged parent's — and\n // checking only the tagged elements themselves let it ride through unread.\n // The binding is the one name the archive TRUSTS: the merge splices it into\n // the living map verbatim, and the new-service scan probes\n // `services/<binding>/` with it — where `metadata { service '../outside-svc' }`,\n // which LikeC4 parses without a murmur, collapses the probe right out of\n // services/, and the spliced map fails the very next `validate --all`\n // (`landscape.binding-unknown`). Explicit bindings only, on purpose: a prose\n // title with no binding is legal C4, and a title becomes a path only through\n // `specs/<svc>/`, whose own grammar check (`delta.service-id-invalid`)\n // guards that route. `ridesWithTag` itself is defined up beside the scaffold\n // gate, which shares the splice's scope for the same reason.\n for (const e of elements.filter(ridesWithTag)) {\n if (e.service === undefined) continue;\n const problem = serviceIdProblem(e.service, \"metadata { service } binding\");\n if (problem === null) continue;\n issues.push({\n severity: \"error\",\n code: \"c4.service-binding-invalid\",\n subject: e.service,\n message:\n `'${e.title}' (${e.id}) — ${problem} ` +\n `\\`loam archive\\` would splice '${e.service}' into the living landscape.likec4 exactly as written, ` +\n `and probe services/${e.service}/ with it. Bind the element to the services/<id>/ directory it means.`,\n });\n }\n\n // The event axis — the same three-way agreement the operation checks above\n // grade, joined on the message name instead of the operationId: the\n // feature's asyncapi deltas (pins, markers, justification), the tagged\n // edges' publishes/consumes metadata, and the requirement deltas'\n // Publishes:/Consumes: lines (./events/events.ts owns the walk).\n issues.push(...(await eventCoherence({ scope, svcNames, taggedRels, svcOf, context, retired, consumers })));\n\n return issues;\n}\n"]}
|