@ybotok/loam 0.2.0-alpha.7 → 0.2.0-alpha.9
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +1315 -1
- package/COMPARISON.md +91 -3
- package/CONTRIBUTING.md +37 -0
- package/MIGRATING-from-OpenSpec.md +40 -22
- package/QUICKSTART.md +181 -9
- package/README.md +256 -57
- package/ROADMAP.md +83 -7
- package/SCHEMA.md +569 -98
- package/WORKFLOW.md +272 -62
- package/dist/cli.js +26 -8
- package/dist/cli.js.map +1 -1
- package/dist/commands/adopt/adopt.js +5 -10
- package/dist/commands/adopt/adopt.js.map +1 -1
- package/dist/commands/archive/archive.js +15 -18
- package/dist/commands/archive/archive.js.map +1 -1
- package/dist/commands/archive/plan/contracts/asyncapi.js +27 -41
- package/dist/commands/archive/plan/contracts/asyncapi.js.map +1 -1
- package/dist/commands/archive/plan/contracts/consumers.js +56 -0
- package/dist/commands/archive/plan/contracts/consumers.js.map +1 -0
- package/dist/commands/archive/plan/contracts/openapi.js +25 -42
- package/dist/commands/archive/plan/contracts/openapi.js.map +1 -1
- package/dist/commands/archive/plan/contracts/verdict-lines.js +41 -0
- package/dist/commands/archive/plan/contracts/verdict-lines.js.map +1 -0
- package/dist/commands/archive/plan/gate.js +3 -7
- package/dist/commands/archive/plan/gate.js.map +1 -1
- package/dist/commands/archive/plan/landscape.js +4 -4
- package/dist/commands/archive/plan/landscape.js.map +1 -1
- package/dist/commands/archive/plan/refusal.js +8 -18
- package/dist/commands/archive/plan/refusal.js.map +1 -1
- package/dist/commands/archive/plan/specs.js +134 -25
- package/dist/commands/archive/plan/specs.js.map +1 -1
- package/dist/commands/archive/plan/state.js +1 -0
- package/dist/commands/archive/plan/state.js.map +1 -1
- package/dist/commands/archive/run.js +33 -11
- package/dist/commands/archive/run.js.map +1 -1
- package/dist/commands/archive/shipped.js +49 -0
- package/dist/commands/archive/shipped.js.map +1 -0
- package/dist/commands/context/context.js +4 -7
- package/dist/commands/context/context.js.map +1 -1
- package/dist/commands/context/print.js +2 -2
- package/dist/commands/context/print.js.map +1 -1
- package/dist/commands/delta/delta.js +44 -12
- package/dist/commands/delta/delta.js.map +1 -1
- package/dist/commands/delta/print.js +1 -1
- package/dist/commands/delta/print.js.map +1 -1
- package/dist/commands/dependencies.js +3 -7
- package/dist/commands/dependencies.js.map +1 -1
- package/dist/commands/diff/diff.js +20 -27
- package/dist/commands/diff/diff.js.map +1 -1
- package/dist/commands/explore.js +4 -8
- package/dist/commands/explore.js.map +1 -1
- package/dist/commands/gate/gate.js +45 -8
- package/dist/commands/gate/gate.js.map +1 -1
- package/dist/commands/gate/print.js +15 -3
- package/dist/commands/gate/print.js.map +1 -1
- package/dist/commands/gherkin/commit.js +13 -54
- package/dist/commands/gherkin/commit.js.map +1 -1
- package/dist/commands/gherkin/gherkin.js +36 -20
- package/dist/commands/gherkin/gherkin.js.map +1 -1
- package/dist/commands/gherkin/reconcile.js +10 -13
- package/dist/commands/gherkin/reconcile.js.map +1 -1
- package/dist/commands/init/first-hour.js +44 -0
- package/dist/commands/init/first-hour.js.map +1 -1
- package/dist/commands/init/init.js +18 -22
- package/dist/commands/init/init.js.map +1 -1
- package/dist/commands/list/campaign/campaign.js +4 -6
- package/dist/commands/list/campaign/campaign.js.map +1 -1
- package/dist/commands/list/json.js +0 -4
- package/dist/commands/list/json.js.map +1 -1
- package/dist/commands/list/list.js +38 -23
- package/dist/commands/list/list.js.map +1 -1
- package/dist/commands/list/print.js +38 -6
- package/dist/commands/list/print.js.map +1 -1
- package/dist/commands/list/suites/suites.js +140 -0
- package/dist/commands/list/suites/suites.js.map +1 -0
- package/dist/commands/list/views.js +28 -33
- package/dist/commands/list/views.js.map +1 -1
- package/dist/commands/mcp/dispatch.js +91 -36
- package/dist/commands/mcp/dispatch.js.map +1 -1
- package/dist/commands/mcp/mcp.js +56 -15
- package/dist/commands/mcp/mcp.js.map +1 -1
- package/dist/commands/mcp/serve.js.map +1 -1
- package/dist/commands/migrate-openspec/materialize/active.js +27 -22
- package/dist/commands/migrate-openspec/materialize/active.js.map +1 -1
- package/dist/commands/migrate-openspec/materialize/stage.js +4 -3
- package/dist/commands/migrate-openspec/materialize/stage.js.map +1 -1
- package/dist/commands/migrate-openspec/materialize/target.js +2 -2
- package/dist/commands/migrate-openspec/materialize/target.js.map +1 -1
- package/dist/commands/migrate-openspec/openspec/decisions.js +9 -14
- package/dist/commands/migrate-openspec/openspec/decisions.js.map +1 -1
- package/dist/commands/migrate-openspec/openspec/error.js +7 -8
- package/dist/commands/migrate-openspec/openspec/error.js.map +1 -1
- package/dist/commands/migrate-openspec/openspec/mapping.js +1 -9
- package/dist/commands/migrate-openspec/openspec/mapping.js.map +1 -1
- package/dist/commands/migrate-openspec/openspec/paths.js +3 -6
- package/dist/commands/migrate-openspec/openspec/paths.js.map +1 -1
- package/dist/commands/new/capability-template.js +118 -0
- package/dist/commands/new/capability-template.js.map +1 -0
- package/dist/commands/new/commit.js +8 -22
- package/dist/commands/new/commit.js.map +1 -1
- package/dist/commands/new/modifies.js +206 -0
- package/dist/commands/new/modifies.js.map +1 -0
- package/dist/commands/new/new.js +59 -14
- package/dist/commands/new/new.js.map +1 -1
- package/dist/commands/new/templates.js +48 -124
- package/dist/commands/new/templates.js.map +1 -1
- package/dist/commands/open.js +3 -7
- package/dist/commands/open.js.map +1 -1
- package/dist/commands/policy/gate.js +27 -0
- package/dist/commands/policy/gate.js.map +1 -1
- package/dist/commands/rebase/plan.js +2 -2
- package/dist/commands/rebase/plan.js.map +1 -1
- package/dist/commands/rebase/rebase.js +60 -35
- package/dist/commands/rebase/rebase.js.map +1 -1
- package/dist/commands/seed/commit.js +21 -25
- package/dist/commands/seed/commit.js.map +1 -1
- package/dist/commands/seed/seed.js +6 -9
- package/dist/commands/seed/seed.js.map +1 -1
- package/dist/commands/show/show.js +3 -7
- package/dist/commands/show/show.js.map +1 -1
- package/dist/commands/status/print.js +13 -4
- package/dist/commands/status/print.js.map +1 -1
- package/dist/commands/status/status.js +3 -7
- package/dist/commands/status/status.js.map +1 -1
- package/dist/commands/steps/steps.js +3 -7
- package/dist/commands/steps/steps.js.map +1 -1
- package/dist/commands/subsystem/edit.js +3 -5
- package/dist/commands/subsystem/edit.js.map +1 -1
- package/dist/commands/subsystem/move.js +5 -8
- package/dist/commands/subsystem/move.js.map +1 -1
- package/dist/commands/subsystem/query.js +2 -4
- package/dist/commands/subsystem/query.js.map +1 -1
- package/dist/commands/subsystem/subsystem.js +4 -7
- package/dist/commands/subsystem/subsystem.js.map +1 -1
- package/dist/commands/subsystem/sync.js +14 -41
- package/dist/commands/subsystem/sync.js.map +1 -1
- package/dist/commands/subsystem/txn/txn.js +17 -38
- package/dist/commands/subsystem/txn/txn.js.map +1 -1
- package/dist/commands/subsystem/txn/views.js +32 -5
- package/dist/commands/subsystem/txn/views.js.map +1 -1
- package/dist/commands/tasks/render.js +116 -0
- package/dist/commands/tasks/render.js.map +1 -0
- package/dist/commands/tasks/tasks.js +113 -0
- package/dist/commands/tasks/tasks.js.map +1 -0
- package/dist/commands/unarchive/manifest.js +6 -9
- package/dist/commands/unarchive/manifest.js.map +1 -1
- package/dist/commands/unarchive/restore.js +17 -28
- package/dist/commands/unarchive/restore.js.map +1 -1
- package/dist/commands/unarchive/unarchive.js +22 -30
- package/dist/commands/unarchive/unarchive.js.map +1 -1
- package/dist/commands/validate/arch-coverage.js +6 -15
- package/dist/commands/validate/arch-coverage.js.map +1 -1
- package/dist/commands/validate/checks/fleet-shape.js +4 -49
- package/dist/commands/validate/checks/fleet-shape.js.map +1 -1
- package/dist/commands/validate/checks/requirements.js +18 -8
- package/dist/commands/validate/checks/requirements.js.map +1 -1
- package/dist/commands/validate/checks/sources.js +6 -2
- package/dist/commands/validate/checks/sources.js.map +1 -1
- package/dist/commands/validate/feature.js +23 -19
- package/dist/commands/validate/feature.js.map +1 -1
- package/dist/commands/validate/fleet/landscape.js +23 -23
- package/dist/commands/validate/fleet/landscape.js.map +1 -1
- package/dist/commands/validate/fleet/map/attest.js +6 -10
- package/dist/commands/validate/fleet/map/attest.js.map +1 -1
- package/dist/commands/validate/fleet/map/bindings.js +1 -3
- package/dist/commands/validate/fleet/map/bindings.js.map +1 -1
- package/dist/commands/validate/fleet/map/usecases.js +5 -11
- package/dist/commands/validate/fleet/map/usecases.js.map +1 -1
- package/dist/commands/validate/fleet/obligations.js +1 -5
- package/dist/commands/validate/fleet/obligations.js.map +1 -1
- package/dist/commands/validate/fleet/scorecard/print.js +5 -1
- package/dist/commands/validate/fleet/scorecard/print.js.map +1 -1
- package/dist/commands/validate/fleet/scorecard/scorecard.js +17 -11
- package/dist/commands/validate/fleet/scorecard/scorecard.js.map +1 -1
- package/dist/commands/validate/fleet/usecases/usecases.js +6 -4
- package/dist/commands/validate/fleet/usecases/usecases.js.map +1 -1
- package/dist/commands/validate/fleet/views/fleet-project.js.map +1 -1
- package/dist/commands/validate/fleet/views/root-documents.js +8 -28
- package/dist/commands/validate/fleet/views/root-documents.js.map +1 -1
- package/dist/commands/validate/links/corpus.js +1 -4
- package/dist/commands/validate/links/corpus.js.map +1 -1
- package/dist/commands/validate/links/glossary.js.map +1 -1
- package/dist/commands/validate/report.js +5 -4
- package/dist/commands/validate/report.js.map +1 -1
- package/dist/commands/validate/scope.js +113 -0
- package/dist/commands/validate/scope.js.map +1 -0
- package/dist/commands/validate/service/api.js +16 -104
- package/dist/commands/validate/service/api.js.map +1 -1
- package/dist/commands/validate/service/contracts/generated.js +134 -0
- package/dist/commands/validate/service/contracts/generated.js.map +1 -0
- package/dist/commands/validate/service/events/events.js +1 -1
- package/dist/commands/validate/service/events/events.js.map +1 -1
- package/dist/commands/validate/service/events/message-producers.js +1 -1
- package/dist/commands/validate/service/events/message-producers.js.map +1 -1
- package/dist/commands/validate/service/evidence-pins.js +56 -10
- package/dist/commands/validate/service/evidence-pins.js.map +1 -1
- package/dist/commands/validate/service/service.js +16 -26
- package/dist/commands/validate/service/service.js.map +1 -1
- package/dist/commands/validate/service/specs.js +31 -38
- package/dist/commands/validate/service/specs.js.map +1 -1
- package/dist/commands/validate/service/spine.js +8 -11
- package/dist/commands/validate/service/spine.js.map +1 -1
- package/dist/commands/validate/service/usecases/usecases.js +3 -3
- package/dist/commands/validate/service/usecases/usecases.js.map +1 -1
- package/dist/commands/validate/validate.js +44 -26
- package/dist/commands/validate/validate.js.map +1 -1
- package/dist/commands/verify/cross/diff.js +2 -6
- package/dist/commands/verify/cross/diff.js.map +1 -1
- package/dist/commands/verify/evidence/contract.js +1 -1
- package/dist/commands/verify/evidence/contract.js.map +1 -1
- package/dist/commands/verify/evidence/read.js +12 -3
- package/dist/commands/verify/evidence/read.js.map +1 -1
- package/dist/commands/verify/frozen.js +45 -23
- package/dist/commands/verify/frozen.js.map +1 -1
- package/dist/commands/verify/record.js +69 -17
- package/dist/commands/verify/record.js.map +1 -1
- package/dist/commands/verify/report.js +27 -44
- package/dist/commands/verify/report.js.map +1 -1
- package/dist/commands/verify/results.js +112 -30
- package/dist/commands/verify/results.js.map +1 -1
- package/dist/commands/verify/verify.js +54 -51
- package/dist/commands/verify/verify.js.map +1 -1
- package/dist/commands/vouch/pack/pack.js +2 -2
- package/dist/commands/vouch/pack/pack.js.map +1 -1
- package/dist/commands/vouch/pack/print.js +1 -6
- package/dist/commands/vouch/pack/print.js.map +1 -1
- package/dist/commands/vouch/pack/sections.js +1 -2
- package/dist/commands/vouch/pack/sections.js.map +1 -1
- package/dist/commands/vouch/run.js +26 -81
- package/dist/commands/vouch/run.js.map +1 -1
- package/dist/commands/vouch/sample/plan.js +20 -34
- package/dist/commands/vouch/sample/plan.js.map +1 -1
- package/dist/commands/vouch/vet/verify.js +39 -7
- package/dist/commands/vouch/vet/verify.js.map +1 -1
- package/dist/commands/vouch/vouch.js +10 -17
- package/dist/commands/vouch/vouch.js.map +1 -1
- package/dist/core/agent/agents-md/command-map.js +56 -101
- package/dist/core/agent/agents-md/command-map.js.map +1 -1
- package/dist/core/agent/agents-md/map/explain.js +4 -3
- package/dist/core/agent/agents-md/map/explain.js.map +1 -1
- package/dist/core/agent/agents-md/map/lenses/context.js +4 -3
- package/dist/core/agent/agents-md/map/lenses/context.js.map +1 -1
- package/dist/core/agent/agents-md/map/lenses/diff.js +33 -20
- package/dist/core/agent/agents-md/map/lenses/diff.js.map +1 -1
- package/dist/core/agent/agents-md/map/lenses/gate.js +13 -5
- package/dist/core/agent/agents-md/map/lenses/gate.js.map +1 -1
- package/dist/core/agent/agents-md/map/mcp.js +10 -7
- package/dist/core/agent/agents-md/map/mcp.js.map +1 -1
- package/dist/core/agent/agents-md/map/status.js +9 -3
- package/dist/core/agent/agents-md/map/status.js.map +1 -1
- package/dist/core/agent/agents-md/map/subsystem.js +4 -3
- package/dist/core/agent/agents-md/map/subsystem.js.map +1 -1
- package/dist/core/agent/agents-md/map/usecases.js +24 -6
- package/dist/core/agent/agents-md/map/usecases.js.map +1 -1
- package/dist/core/agent/agents-md/refusals.js +32 -18
- package/dist/core/agent/agents-md/refusals.js.map +1 -1
- package/dist/core/agent/agents-md.js +9 -3
- package/dist/core/agent/agents-md.js.map +1 -1
- package/dist/core/agent/agents-stamp.js +10 -0
- package/dist/core/agent/agents-stamp.js.map +1 -1
- package/dist/core/agent/scaffold.js +10 -15
- package/dist/core/agent/scaffold.js.map +1 -1
- package/dist/core/agent/tools/dialects.js +1 -0
- package/dist/core/agent/tools/dialects.js.map +1 -1
- package/dist/core/agent/tools/registry.js +41 -76
- package/dist/core/agent/tools/registry.js.map +1 -1
- package/dist/core/agent/workflows/adopt.js +5 -3
- package/dist/core/agent/workflows/adopt.js.map +1 -1
- package/dist/core/agent/workflows/check.js +20 -16
- package/dist/core/agent/workflows/check.js.map +1 -1
- package/dist/core/agent/workflows/closing.js +54 -21
- package/dist/core/agent/workflows/closing.js.map +1 -1
- package/dist/core/agent/workflows/feature.js +79 -11
- package/dist/core/agent/workflows/feature.js.map +1 -1
- package/dist/core/agent/workflows/implement.js +91 -13
- package/dist/core/agent/workflows/implement.js.map +1 -1
- package/dist/core/agent/workflows/reference/authoring.js +3 -1
- package/dist/core/agent/workflows/reference/authoring.js.map +1 -1
- package/dist/core/agent/workflows/reference/codes.js +8 -6
- package/dist/core/agent/workflows/reference/codes.js.map +1 -1
- package/dist/core/agent/workflows/reference/done-check.js +37 -15
- package/dist/core/agent/workflows/reference/done-check.js.map +1 -1
- package/dist/core/agent/workflows/reference/reference.js.map +1 -1
- package/dist/core/agent/workflows/reference/spine.js +50 -18
- package/dist/core/agent/workflows/reference/spine.js.map +1 -1
- package/dist/core/asyncapi/baseline/plan.js +14 -81
- package/dist/core/asyncapi/baseline/plan.js.map +1 -1
- package/dist/core/asyncapi/digest.js +30 -25
- package/dist/core/asyncapi/digest.js.map +1 -1
- package/dist/core/asyncapi/merge/components.js +5 -4
- package/dist/core/asyncapi/merge/components.js.map +1 -1
- package/dist/core/asyncapi/merge/error.js +7 -2
- package/dist/core/asyncapi/merge/error.js.map +1 -1
- package/dist/core/asyncapi/merge/markers.js +30 -50
- package/dist/core/asyncapi/merge/markers.js.map +1 -1
- package/dist/core/asyncapi/merge/merge.js +31 -53
- package/dist/core/asyncapi/merge/merge.js.map +1 -1
- package/dist/core/asyncapi/merge/pin.js +10 -36
- package/dist/core/asyncapi/merge/pin.js.map +1 -1
- package/dist/core/asyncapi/model.js.map +1 -1
- package/dist/core/asyncapi/read.js +36 -12
- package/dist/core/asyncapi/read.js.map +1 -1
- package/dist/core/brief/brief.js +6 -18
- package/dist/core/brief/brief.js.map +1 -1
- package/dist/core/brief/landscape.js +8 -12
- package/dist/core/brief/landscape.js.map +1 -1
- package/dist/core/brief/map/extend.js +8 -5
- package/dist/core/brief/map/extend.js.map +1 -1
- package/dist/core/brief/map/owed.js +8 -26
- package/dist/core/brief/map/owed.js.map +1 -1
- package/dist/core/brief/targets.js +15 -5
- package/dist/core/brief/targets.js.map +1 -1
- package/dist/core/c4/likec4.js +147 -16
- package/dist/core/c4/likec4.js.map +1 -1
- package/dist/core/c4/locate/unlocated.js +150 -0
- package/dist/core/c4/locate/unlocated.js.map +1 -0
- package/dist/core/c4/parsed/values.js +54 -1
- package/dist/core/c4/parsed/values.js.map +1 -1
- package/dist/core/c4/prefetch/fleet.js +1 -9
- package/dist/core/c4/prefetch/fleet.js.map +1 -1
- package/dist/core/c4/project/documents.js +31 -6
- package/dist/core/c4/project/documents.js.map +1 -1
- package/dist/core/c4/project/load.js +6 -17
- package/dist/core/c4/project/load.js.map +1 -1
- package/dist/core/c4/resolve/external.js +51 -0
- package/dist/core/c4/resolve/external.js.map +1 -0
- package/dist/core/c4/resolve/service.js +25 -1
- package/dist/core/c4/resolve/service.js.map +1 -1
- package/dist/core/c4/seed/template.js +15 -1
- package/dist/core/c4/seed/template.js.map +1 -1
- package/dist/core/c4/service-model/shape.js +2 -22
- package/dist/core/c4/service-model/shape.js.map +1 -1
- package/dist/core/c4/source-mask.js +288 -4
- package/dist/core/c4/source-mask.js.map +1 -1
- package/dist/core/c4/source-scan.js +203 -147
- package/dist/core/c4/source-scan.js.map +1 -1
- package/dist/core/c4/splice/authored-source.js +19 -0
- package/dist/core/c4/splice/authored-source.js.map +1 -1
- package/dist/core/c4/splice/delta-blocks.js +5 -18
- package/dist/core/c4/splice/delta-blocks.js.map +1 -1
- package/dist/core/c4/splice/landscape-merge.js +13 -8
- package/dist/core/c4/splice/landscape-merge.js.map +1 -1
- package/dist/core/c4/splice/model/merge.js +10 -6
- package/dist/core/c4/splice/model/merge.js.map +1 -1
- package/dist/core/c4/splice/placement.js +108 -18
- package/dist/core/c4/splice/placement.js.map +1 -1
- package/dist/core/c4/workspace.js +13 -22
- package/dist/core/c4/workspace.js.map +1 -1
- package/dist/core/capabilities/capabilities.js +6 -6
- package/dist/core/capabilities/capabilities.js.map +1 -1
- package/dist/core/capabilities/delta/doc.js +7 -10
- package/dist/core/capabilities/delta/doc.js.map +1 -1
- package/dist/core/capabilities/findings.js +15 -20
- package/dist/core/capabilities/findings.js.map +1 -1
- package/dist/core/capabilities/realizes/findings.js +12 -19
- package/dist/core/capabilities/realizes/findings.js.map +1 -1
- package/dist/core/capabilities/realizes/join.js +0 -20
- package/dist/core/capabilities/realizes/join.js.map +1 -1
- package/dist/core/capabilities/rollup.js +6 -0
- package/dist/core/capabilities/rollup.js.map +1 -1
- package/dist/core/capabilities/tree.js +6 -28
- package/dist/core/capabilities/tree.js.map +1 -1
- package/dist/core/capabilities/usecase-join.js +3 -13
- package/dist/core/capabilities/usecase-join.js.map +1 -1
- package/dist/core/coherence/authoring/scaffold.js +11 -3
- package/dist/core/coherence/authoring/scaffold.js.map +1 -1
- package/dist/core/coherence/authoring/sentinels.js +2 -1
- package/dist/core/coherence/authoring/sentinels.js.map +1 -1
- package/dist/core/coherence/coherence.js +30 -16
- package/dist/core/coherence/coherence.js.map +1 -1
- package/dist/core/coherence/consumed/operations.js +70 -0
- package/dist/core/coherence/consumed/operations.js.map +1 -0
- package/dist/core/coherence/consumed/refusal.js +38 -0
- package/dist/core/coherence/consumed/refusal.js.map +1 -0
- package/dist/core/coherence/declared.js +60 -34
- package/dist/core/coherence/declared.js.map +1 -1
- package/dist/core/coherence/events/declared.js +44 -59
- package/dist/core/coherence/events/declared.js.map +1 -1
- package/dist/core/coherence/events/events.js +119 -8
- package/dist/core/coherence/events/events.js.map +1 -1
- package/dist/core/coherence/events/grades.js +31 -8
- package/dist/core/coherence/events/grades.js.map +1 -1
- package/dist/core/coherence/events/lookups.js +14 -82
- package/dist/core/coherence/events/lookups.js.map +1 -1
- package/dist/core/coherence/lookups.js +18 -78
- package/dist/core/coherence/lookups.js.map +1 -1
- package/dist/core/coherence/retired/archived.js +285 -0
- package/dist/core/coherence/retired/archived.js.map +1 -0
- package/dist/core/coherence/retired/event-pair.js +62 -0
- package/dist/core/coherence/retired/event-pair.js.map +1 -0
- package/dist/core/coherence/retired/merged.js +79 -0
- package/dist/core/coherence/retired/merged.js.map +1 -0
- package/dist/core/coherence/retired/retirement.js +49 -0
- package/dist/core/coherence/retired/retirement.js.map +1 -0
- package/dist/core/coherence/retired/revival/arrivals.js +131 -0
- package/dist/core/coherence/retired/revival/arrivals.js.map +1 -0
- package/dist/core/coherence/retired/revival/revived.js +297 -0
- package/dist/core/coherence/retired/revival/revived.js.map +1 -0
- package/dist/core/consumers/after.js +113 -0
- package/dist/core/consumers/after.js.map +1 -0
- package/dist/core/consumers/consumers.js +296 -0
- package/dist/core/consumers/consumers.js.map +1 -0
- package/dist/core/consumers/current.js +211 -0
- package/dist/core/consumers/current.js.map +1 -0
- package/dist/core/consumers/scan.js +46 -0
- package/dist/core/consumers/scan.js.map +1 -0
- package/dist/core/consumers/state.js +27 -0
- package/dist/core/consumers/state.js.map +1 -0
- package/dist/core/delta/delta.js +1 -1
- package/dist/core/delta/delta.js.map +1 -1
- package/dist/core/deployment/delta.js +3 -6
- package/dist/core/deployment/delta.js.map +1 -1
- package/dist/core/diff/base-git.js +94 -12
- package/dist/core/diff/base-git.js.map +1 -1
- package/dist/core/diff/base-state.js +28 -16
- package/dist/core/diff/base-state.js.map +1 -1
- package/dist/core/diff/joins.js +136 -0
- package/dist/core/diff/joins.js.map +1 -0
- package/dist/core/diff/scope/changed-paths.js +22 -6
- package/dist/core/diff/scope/changed-paths.js.map +1 -1
- package/dist/core/diff/scope/edges.js +82 -0
- package/dist/core/diff/scope/edges.js.map +1 -0
- package/dist/core/diff/semantic.js +100 -31
- package/dist/core/diff/semantic.js.map +1 -1
- package/dist/core/doctor/agents.js +34 -18
- package/dist/core/doctor/agents.js.map +1 -1
- package/dist/core/doctor/report.js.map +1 -1
- package/dist/core/document/apply.js +10 -2
- package/dist/core/document/apply.js.map +1 -1
- package/dist/core/document/frontmatter.js +55 -0
- package/dist/core/document/frontmatter.js.map +1 -1
- package/dist/core/document/parse.js +29 -6
- package/dist/core/document/parse.js.map +1 -1
- package/dist/core/document/scenarios.js +17 -0
- package/dist/core/document/scenarios.js.map +1 -1
- package/dist/core/document/spec.js +4 -11
- package/dist/core/document/spec.js.map +1 -1
- package/dist/core/envelope/config.js +10 -0
- package/dist/core/envelope/config.js.map +1 -1
- package/dist/core/envelope/json.js +34 -0
- package/dist/core/envelope/json.js.map +1 -1
- package/dist/core/explain/families.js +17 -12
- package/dist/core/explain/families.js.map +1 -1
- package/dist/core/explain/refusals.js +3 -3
- package/dist/core/explain/refusals.js.map +1 -1
- package/dist/core/explain/terms.js +18 -9
- package/dist/core/explain/terms.js.map +1 -1
- package/dist/core/explore/explore.js +2 -7
- package/dist/core/explore/explore.js.map +1 -1
- package/dist/core/fleet-context.js +34 -95
- package/dist/core/fleet-context.js.map +1 -1
- package/dist/core/gate/checks.js +5 -2
- package/dist/core/gate/checks.js.map +1 -1
- package/dist/core/gate/partners.js +4 -2
- package/dist/core/gate/partners.js.map +1 -1
- package/dist/core/gate/report.js.map +1 -1
- package/dist/core/gate/verification.js +75 -12
- package/dist/core/gate/verification.js.map +1 -1
- package/dist/core/gherkin/stale.js +13 -37
- package/dist/core/gherkin/stale.js.map +1 -1
- package/dist/core/glossary/tree.js +6 -19
- package/dist/core/glossary/tree.js.map +1 -1
- package/dist/core/kernel/concurrency.js +19 -0
- package/dist/core/kernel/concurrency.js.map +1 -1
- package/dist/core/kernel/records.js +11 -0
- package/dist/core/kernel/records.js.map +1 -1
- package/dist/core/links/backlinks.js +1 -3
- package/dist/core/links/backlinks.js.map +1 -1
- package/dist/core/links/corpus.js +5 -36
- package/dist/core/links/corpus.js.map +1 -1
- package/dist/core/mcp/argv.js +101 -0
- package/dist/core/mcp/argv.js.map +1 -1
- package/dist/core/mcp/author-tools.js +2 -1
- package/dist/core/mcp/author-tools.js.map +1 -1
- package/dist/core/mcp/protocol.js +2 -2
- package/dist/core/mcp/protocol.js.map +1 -1
- package/dist/core/mcp/tools.js +43 -33
- package/dist/core/mcp/tools.js.map +1 -1
- package/dist/core/openapi/baseline/gate.js +53 -12
- package/dist/core/openapi/baseline/gate.js.map +1 -1
- package/dist/core/openapi/baseline/plan.js +22 -8
- package/dist/core/openapi/baseline/plan.js.map +1 -1
- package/dist/core/openapi/baseline/record.js +0 -1
- package/dist/core/openapi/baseline/record.js.map +1 -1
- package/dist/core/openapi/contract-text.js +33 -0
- package/dist/core/openapi/contract-text.js.map +1 -0
- package/dist/core/openapi/contract-tree.js +56 -0
- package/dist/core/openapi/contract-tree.js.map +1 -0
- package/dist/core/openapi/depth.js +38 -24
- package/dist/core/openapi/depth.js.map +1 -1
- package/dist/core/openapi/digest.js +52 -2
- package/dist/core/openapi/digest.js.map +1 -1
- package/dist/core/openapi/doc.js +12 -4
- package/dist/core/openapi/doc.js.map +1 -1
- package/dist/core/openapi/merge/components.js +25 -60
- package/dist/core/openapi/merge/components.js.map +1 -1
- package/dist/core/openapi/merge/error.js +3 -13
- package/dist/core/openapi/merge/error.js.map +1 -1
- package/dist/core/openapi/merge/markers.js +26 -30
- package/dist/core/openapi/merge/markers.js.map +1 -1
- package/dist/core/openapi/merge/merge.js +26 -52
- package/dist/core/openapi/merge/merge.js.map +1 -1
- package/dist/core/openapi/merge/pin.js +15 -35
- package/dist/core/openapi/merge/pin.js.map +1 -1
- package/dist/core/openspec/decide/verdict.js.map +1 -1
- package/dist/core/openspec/inventory.js +4 -5
- package/dist/core/openspec/inventory.js.map +1 -1
- package/dist/core/openspec/model/mapping.js +30 -28
- package/dist/core/openspec/model/mapping.js.map +1 -1
- package/dist/core/openspec/model/model.js +12 -7
- package/dist/core/openspec/model/model.js.map +1 -1
- package/dist/core/openspec/scan/changes.js +10 -4
- package/dist/core/openspec/scan/changes.js.map +1 -1
- package/dist/core/openspec/scan/config.js +7 -16
- package/dist/core/openspec/scan/config.js.map +1 -1
- package/dist/core/openspec/scan/shape.js +63 -19
- package/dist/core/openspec/scan/shape.js.map +1 -1
- package/dist/core/openspec/scan/walk.js +21 -38
- package/dist/core/openspec/scan/walk.js.map +1 -1
- package/dist/core/pack/living.js +2 -16
- package/dist/core/pack/living.js.map +1 -1
- package/dist/core/provenance/findings.js +48 -12
- package/dist/core/provenance/findings.js.map +1 -1
- package/dist/core/provenance/git.js +33 -57
- package/dist/core/provenance/git.js.map +1 -1
- package/dist/core/provenance/gitq/moves.js +71 -114
- package/dist/core/provenance/gitq/moves.js.map +1 -1
- package/dist/core/provenance/gitq/vouched-ancestor.js +2 -2
- package/dist/core/provenance/gitq/vouched-ancestor.js.map +1 -1
- package/dist/core/provenance/sources.js +23 -6
- package/dist/core/provenance/sources.js.map +1 -1
- package/dist/core/provenance/stamp.js +5 -17
- package/dist/core/provenance/stamp.js.map +1 -1
- package/dist/core/repo/entries.js.map +1 -1
- package/dist/core/repo/repo.js +24 -14
- package/dist/core/repo/repo.js.map +1 -1
- package/dist/core/repo/tree/find.js +12 -3
- package/dist/core/repo/tree/find.js.map +1 -1
- package/dist/core/repo/tree/fs.js +27 -1
- package/dist/core/repo/tree/fs.js.map +1 -1
- package/dist/core/repo/tree/walk.js +8 -30
- package/dist/core/repo/tree/walk.js.map +1 -1
- package/dist/core/results.js +47 -12
- package/dist/core/results.js.map +1 -1
- package/dist/core/staging/commit.js +10 -12
- package/dist/core/staging/commit.js.map +1 -1
- package/dist/core/staging/lock.js +74 -12
- package/dist/core/staging/lock.js.map +1 -1
- package/dist/core/staging/recovery/intent.js +2 -5
- package/dist/core/staging/recovery/intent.js.map +1 -1
- package/dist/core/staging/recovery/recover.js +60 -1
- package/dist/core/staging/recovery/recover.js.map +1 -1
- package/dist/core/staging/snapshot/manifest.js +152 -0
- package/dist/core/staging/snapshot/manifest.js.map +1 -0
- package/dist/core/staging/{snapshot.js → snapshot/snapshot.js} +10 -34
- package/dist/core/staging/snapshot/snapshot.js.map +1 -0
- package/dist/core/status/actions/execution.js +11 -0
- package/dist/core/status/actions/execution.js.map +1 -1
- package/dist/core/status/feature/artifacts.js +2 -2
- package/dist/core/status/feature/artifacts.js.map +1 -1
- package/dist/core/status/feature/feature.js +5 -11
- package/dist/core/status/feature/feature.js.map +1 -1
- package/dist/core/status/fleet/fleet.js +6 -2
- package/dist/core/status/fleet/fleet.js.map +1 -1
- package/dist/core/status/fleet/next.js +26 -3
- package/dist/core/status/fleet/next.js.map +1 -1
- package/dist/core/status/report.js.map +1 -1
- package/dist/core/status/verification.js +30 -15
- package/dist/core/status/verification.js.map +1 -1
- package/dist/core/tasks/packet.js +174 -0
- package/dist/core/tasks/packet.js.map +1 -0
- package/dist/core/tasks/record.js +43 -0
- package/dist/core/tasks/record.js.map +1 -0
- package/dist/core/tasks/sources.js +181 -0
- package/dist/core/tasks/sources.js.map +1 -0
- package/dist/core/tasks/stamps.js +76 -0
- package/dist/core/tasks/stamps.js.map +1 -0
- package/dist/core/tasks/unit.js +0 -0
- package/dist/core/tasks/unit.js.map +1 -0
- package/dist/core/usecases/capability.js +11 -14
- package/dist/core/usecases/capability.js.map +1 -1
- package/dist/core/usecases/delta/flows.js +5 -7
- package/dist/core/usecases/delta/flows.js.map +1 -1
- package/dist/core/usecases/fleet.js +178 -31
- package/dist/core/usecases/fleet.js.map +1 -1
- package/dist/core/usecases/operations.js +5 -4
- package/dist/core/usecases/operations.js.map +1 -1
- package/dist/core/usecases/service/flows.js +9 -6
- package/dist/core/usecases/service/flows.js.map +1 -1
- package/dist/core/usecases/suites/participants.js +83 -0
- package/dist/core/usecases/suites/participants.js.map +1 -0
- package/dist/core/usecases/suites/suites.js +45 -0
- package/dist/core/usecases/suites/suites.js.map +1 -0
- package/dist/core/verify/answers.js +12 -5
- package/dist/core/verify/answers.js.map +1 -1
- package/dist/core/verify/build.js +102 -82
- package/dist/core/verify/build.js.map +1 -1
- package/dist/core/verify/checklist.js +3 -0
- package/dist/core/verify/checklist.js.map +1 -1
- package/dist/core/verify/location.js +67 -0
- package/dist/core/verify/location.js.map +1 -0
- package/dist/core/verify/pins/lint.js +3 -2
- package/dist/core/verify/pins/lint.js.map +1 -1
- package/dist/core/verify/pins/pin.js +1 -1
- package/dist/core/verify/pins/pin.js.map +1 -1
- package/dist/core/verify/read/file.js +224 -0
- package/dist/core/verify/read/file.js.map +1 -0
- package/dist/core/verify/{file.js → read/shape.js} +16 -104
- package/dist/core/verify/read/shape.js.map +1 -0
- package/dist/core/verify/read/union.js +105 -0
- package/dist/core/verify/read/union.js.map +1 -0
- package/dist/core/verify/record.js +35 -12
- package/dist/core/verify/record.js.map +1 -1
- package/dist/core/verify/store/commit.js +40 -30
- package/dist/core/verify/store/commit.js.map +1 -1
- package/dist/core/verify/store/render.js +8 -4
- package/dist/core/verify/store/render.js.map +1 -1
- package/dist/core/vocabulary/issue.js +1 -0
- package/dist/core/vocabulary/issue.js.map +1 -1
- package/dist/core/vocabulary/maturity.js +12 -2
- package/dist/core/vocabulary/maturity.js.map +1 -1
- package/examples/README.md +21 -6
- package/examples/docs/architecture/landscape.likec4 +8 -0
- package/examples/docs/architecture/usecases/order-notification.likec4 +6 -2
- package/examples/docs/features/FEAT-101-payment-splitting/specs/payment-service/asyncapi.yaml +11 -8
- package/examples/docs/features/FEAT-101-payment-splitting/specs/payment-split-service/openapi.yaml +6 -6
- package/examples/docs/features/archive/FEAT-088-refunds/specs/payment-service/openapi.yaml +13 -10
- package/examples/docs/services/order-service/asyncapi.yaml +3 -2
- package/examples/docs/services/platform/notification-service/asyncapi.yaml +2 -1
- package/package.json +1 -1
- package/dist/commands/validate/checks/vocabulary.js +0 -16
- package/dist/commands/validate/checks/vocabulary.js.map +0 -1
- package/dist/core/agent/agents-md/artifacts.js +0 -221
- package/dist/core/agent/agents-md/artifacts.js.map +0 -1
- package/dist/core/agent/agents-md/cycle.js +0 -180
- package/dist/core/agent/agents-md/cycle.js.map +0 -1
- package/dist/core/agent/agents-md/shipped/archive-gate.js +0 -98
- package/dist/core/agent/agents-md/shipped/archive-gate.js.map +0 -1
- package/dist/core/agent/agents-md/spine.js +0 -40
- package/dist/core/agent/agents-md/spine.js.map +0 -1
- package/dist/core/coherence/events/merged.js +0 -40
- package/dist/core/coherence/events/merged.js.map +0 -1
- package/dist/core/diff/current-state.js +0 -96
- package/dist/core/diff/current-state.js.map +0 -1
- package/dist/core/diff/victims.js +0 -256
- package/dist/core/diff/victims.js.map +0 -1
- package/dist/core/staging/snapshot.js.map +0 -1
- package/dist/core/verify/file.js.map +0 -1
|
@@ -0,0 +1,136 @@
|
|
|
1
|
+
import { ownsOperation, REQUIREMENT_AXES } from "../consumers/consumers.js";
|
|
2
|
+
/** The requirements one service contributes to a join — both spec files, a REMOVED one governing nothing. */
|
|
3
|
+
function reqsOf(state) {
|
|
4
|
+
return REQUIREMENT_AXES.flatMap((axis) => {
|
|
5
|
+
const a = state[axis];
|
|
6
|
+
return a.kind === "read" ? a.value.filter((r) => r.kind !== "REMOVED") : [];
|
|
7
|
+
});
|
|
8
|
+
}
|
|
9
|
+
/** The map key a tuple is joined on between the two states. */
|
|
10
|
+
function tupleKey(t) {
|
|
11
|
+
return `${t.kind}\0${t.consumer}\0${t.provider}\0${t.token}`;
|
|
12
|
+
}
|
|
13
|
+
/**
|
|
14
|
+
* Every cross-service join one state holds, keyed for the base/current join.
|
|
15
|
+
* The rules are the consumer index's, read off it rather than restated: both
|
|
16
|
+
* spec files for `Operations:` and `Consumes:` alike (`REQUIREMENT_AXES`), and
|
|
17
|
+
* an `Operations:` line naming an id the consumer's own contract defines is
|
|
18
|
+
* that consumer's own operation, no join onto anybody else's
|
|
19
|
+
* (`ownsOperation`). An unreadable axis simply contributes nothing here — no
|
|
20
|
+
* line from a spec file nobody could read, no ownership from a contract nobody
|
|
21
|
+
* could read — and `tupleVisible` below guards the GRADING, so a join is never
|
|
22
|
+
* called added or removed off a side nobody could read, and `ownershipFlicker`
|
|
23
|
+
* keeps one from being called so off a consumer's own contract that merely
|
|
24
|
+
* stopped or started parsing.
|
|
25
|
+
*/
|
|
26
|
+
export function consumerTuples(states) {
|
|
27
|
+
const providersOf = (pick) => {
|
|
28
|
+
const map = new Map();
|
|
29
|
+
for (const id of [...states.keys()].sort()) {
|
|
30
|
+
for (const token of pick(states.get(id)))
|
|
31
|
+
map.set(token, [...(map.get(token) ?? []), id]);
|
|
32
|
+
}
|
|
33
|
+
return map;
|
|
34
|
+
};
|
|
35
|
+
const opProviders = providersOf((s) => (s.openapi.kind === "read" ? s.openapi.value.map((op) => op.id) : []));
|
|
36
|
+
const msgProviders = providersOf((s) => (s.asyncapi.kind === "read" ? s.asyncapi.value.map((m) => m.name) : []));
|
|
37
|
+
const out = new Map();
|
|
38
|
+
const add = (t) => {
|
|
39
|
+
out.set(tupleKey(t), t);
|
|
40
|
+
};
|
|
41
|
+
for (const consumer of [...states.keys()].sort()) {
|
|
42
|
+
const state = states.get(consumer);
|
|
43
|
+
for (const r of reqsOf(state)) {
|
|
44
|
+
for (const op of r.operations) {
|
|
45
|
+
if (ownsOperation(state, op))
|
|
46
|
+
continue;
|
|
47
|
+
for (const p of opProviders.get(op) ?? [])
|
|
48
|
+
if (p !== consumer)
|
|
49
|
+
add({ kind: "op", consumer, provider: p, token: op });
|
|
50
|
+
}
|
|
51
|
+
for (const name of r.consumes) {
|
|
52
|
+
for (const p of msgProviders.get(name) ?? [])
|
|
53
|
+
if (p !== consumer)
|
|
54
|
+
add({ kind: "message", consumer, provider: p, token: name });
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
return out;
|
|
59
|
+
}
|
|
60
|
+
/** May a tuple be graded off this side? Absence answers; unreadable does not; an ambiguous base identity answers nothing. */
|
|
61
|
+
function tupleVisible(states, ambiguous, t) {
|
|
62
|
+
if (ambiguous.has(t.consumer) || ambiguous.has(t.provider))
|
|
63
|
+
return false;
|
|
64
|
+
const readable = (id, axes) => {
|
|
65
|
+
const state = states.get(id);
|
|
66
|
+
return state === undefined || axes.every((axis) => state[axis].kind !== "unreadable");
|
|
67
|
+
};
|
|
68
|
+
// Both spec files on the consumer's side, for either kind: a line in the file
|
|
69
|
+
// nobody could read may be the very one the join came or went by. NOT the
|
|
70
|
+
// consumer's own contract, although an operation join reads it: it is asked
|
|
71
|
+
// only whether the line names the consumer's OWN operation, and an
|
|
72
|
+
// unreadable contract proves no ownership (`ownsOperation`), so the line
|
|
73
|
+
// counts on that side — the index's own rule. A side holding it unread can
|
|
74
|
+
// only have MORE joins, never hide one, and suppressing the join there kept
|
|
75
|
+
// `diff.consumer-added` / `-removed` off a report whose victims named that
|
|
76
|
+
// very consumer (the contract review, finding 4). Those extra joins are a
|
|
77
|
+
// change of their own only when the readable side does not prove the
|
|
78
|
+
// ownership the unread one could not — `ownershipFlicker` below.
|
|
79
|
+
return readable(t.consumer, ["spec", "archSpec"]) && readable(t.provider, [t.kind === "op" ? "openapi" : "asyncapi"]);
|
|
80
|
+
}
|
|
81
|
+
/**
|
|
82
|
+
* Is this operation join held only because the consumer's own contract did not
|
|
83
|
+
* parse on the side that holds it, while on the other side it reads and
|
|
84
|
+
* defines the id? Then the line names the consumer's OWN operation — the
|
|
85
|
+
* readable side says so — and nothing about the join moved but the contract's
|
|
86
|
+
* readability: the line that never changed "went away" when the contract was
|
|
87
|
+
* fixed and "appeared" when it broke, once per provider of a shared id such as
|
|
88
|
+
* `healthCheck` (the verification of finding 4, N1). The unreadable contract
|
|
89
|
+
* is already its own `unreadable[]` entry on the consumer, which carries the
|
|
90
|
+
* run to exit 1; a second, false finding on every provider said nothing true.
|
|
91
|
+
*
|
|
92
|
+
* Only that shape is skipped. A readable side that defines no such id proves
|
|
93
|
+
* no ownership either, so the join is the line's and is graded as before; a
|
|
94
|
+
* message join asks no ownership at all (`ownsOperation` says why).
|
|
95
|
+
*/
|
|
96
|
+
function ownershipFlicker(sides, t) {
|
|
97
|
+
if (t.kind !== "op" || sides.holding.get(t.consumer)?.openapi.kind !== "unreadable")
|
|
98
|
+
return false;
|
|
99
|
+
const other = sides.other.get(t.consumer);
|
|
100
|
+
return other !== undefined && ownsOperation(other, t.token);
|
|
101
|
+
}
|
|
102
|
+
/**
|
|
103
|
+
* The joins that appeared or went away between base and current, sorted for
|
|
104
|
+
* deterministic output (provider, kind, token, consumer). The visibility
|
|
105
|
+
* guard is the containment rule: a join is never called "added" off a base
|
|
106
|
+
* side nobody could read, nor "removed" off an unreadable working file — the
|
|
107
|
+
* suspended axis already carries that fact. The flicker guard is its mirror on
|
|
108
|
+
* the side that HOLDS the join: one held only because the consumer's own
|
|
109
|
+
* contract did not read there, over an id the other side shows it owns, is no
|
|
110
|
+
* change either.
|
|
111
|
+
*/
|
|
112
|
+
export function consumerJoinChanges(input) {
|
|
113
|
+
const baseTuples = consumerTuples(input.base);
|
|
114
|
+
const currentTuples = consumerTuples(input.current);
|
|
115
|
+
const graded = [];
|
|
116
|
+
const added = { holding: input.current, other: input.base };
|
|
117
|
+
const removed = { holding: input.base, other: input.current };
|
|
118
|
+
for (const [key, t] of currentTuples) {
|
|
119
|
+
if (baseTuples.has(key) || ownershipFlicker(added, t))
|
|
120
|
+
continue;
|
|
121
|
+
if (tupleVisible(input.base, input.baseAmbiguous, t))
|
|
122
|
+
graded.push({ t, change: "added" });
|
|
123
|
+
}
|
|
124
|
+
for (const [key, t] of baseTuples) {
|
|
125
|
+
if (currentTuples.has(key) || ownershipFlicker(removed, t))
|
|
126
|
+
continue;
|
|
127
|
+
if (tupleVisible(input.current, input.currentAmbiguous, t))
|
|
128
|
+
graded.push({ t, change: "removed" });
|
|
129
|
+
}
|
|
130
|
+
return graded.sort((a, b) => {
|
|
131
|
+
const ka = `${a.t.provider}\0${a.t.kind}\0${a.t.token}\0${a.t.consumer}\0${a.change}`;
|
|
132
|
+
const kb = `${b.t.provider}\0${b.t.kind}\0${b.t.token}\0${b.t.consumer}\0${b.change}`;
|
|
133
|
+
return ka < kb ? -1 : 1;
|
|
134
|
+
});
|
|
135
|
+
}
|
|
136
|
+
//# sourceMappingURL=joins.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"joins.js","sourceRoot":"","sources":["../../../src/core/diff/joins.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * The cross-service joins `loam diff` reports appearing or going away between\n * the base ref and the working tree — `diff.consumer-added` /\n * `diff.consumer-removed`.\n *\n * These lived in `./victims.ts` beside the per-removal victim index until that\n * index moved down to `core/consumers/` to become the one consumer answer the\n * coherence gate and the archive planner read too. The joins below stayed: they\n * compare two WHOLE states, which only the diff has, while the index answers\n * about one fleet.\n */\nimport type { Requirement } from \"../document/spec.js\";\nimport type { ServiceState } from \"../consumers/state.js\";\nimport { ownsOperation, REQUIREMENT_AXES } from \"../consumers/consumers.js\";\n\n/** The requirements one service contributes to a join — both spec files, a REMOVED one governing nothing. */\nfunction reqsOf(state: ServiceState): Requirement[] {\n return REQUIREMENT_AXES.flatMap((axis) => {\n const a = state[axis];\n return a.kind === \"read\" ? a.value.filter((r) => r.kind !== \"REMOVED\") : [];\n });\n}\n\n/**\n * One cross-service join: `consumer`'s living requirements name a `token` that\n * `provider`'s contract defines. The same \"who depends on what\" question the\n * consumer index (`core/consumers/consumers.ts`) answers per removal, asked\n * here of a WHOLE state — base or current — so `./semantic.ts` can report\n * joins that appeared or went away.\n */\nexport interface ConsumerTuple {\n kind: \"op\" | \"message\";\n consumer: string;\n provider: string;\n token: string;\n}\n\n/** The map key a tuple is joined on between the two states. */\nfunction tupleKey(t: ConsumerTuple): string {\n return `${t.kind}\\0${t.consumer}\\0${t.provider}\\0${t.token}`;\n}\n\n/**\n * Every cross-service join one state holds, keyed for the base/current join.\n * The rules are the consumer index's, read off it rather than restated: both\n * spec files for `Operations:` and `Consumes:` alike (`REQUIREMENT_AXES`), and\n * an `Operations:` line naming an id the consumer's own contract defines is\n * that consumer's own operation, no join onto anybody else's\n * (`ownsOperation`). An unreadable axis simply contributes nothing here — no\n * line from a spec file nobody could read, no ownership from a contract nobody\n * could read — and `tupleVisible` below guards the GRADING, so a join is never\n * called added or removed off a side nobody could read, and `ownershipFlicker`\n * keeps one from being called so off a consumer's own contract that merely\n * stopped or started parsing.\n */\nexport function consumerTuples(states: ReadonlyMap<string, ServiceState>): Map<string, ConsumerTuple> {\n const providersOf = (pick: (s: ServiceState) => string[]): Map<string, string[]> => {\n const map = new Map<string, string[]>();\n for (const id of [...states.keys()].sort()) {\n for (const token of pick(states.get(id)!)) map.set(token, [...(map.get(token) ?? []), id]);\n }\n return map;\n };\n const opProviders = providersOf((s) => (s.openapi.kind === \"read\" ? s.openapi.value.map((op) => op.id) : []));\n const msgProviders = providersOf((s) => (s.asyncapi.kind === \"read\" ? s.asyncapi.value.map((m) => m.name) : []));\n const out = new Map<string, ConsumerTuple>();\n const add = (t: ConsumerTuple): void => {\n out.set(tupleKey(t), t);\n };\n for (const consumer of [...states.keys()].sort()) {\n const state = states.get(consumer)!;\n for (const r of reqsOf(state)) {\n for (const op of r.operations) {\n if (ownsOperation(state, op)) continue;\n for (const p of opProviders.get(op) ?? []) if (p !== consumer) add({ kind: \"op\", consumer, provider: p, token: op });\n }\n for (const name of r.consumes) {\n for (const p of msgProviders.get(name) ?? []) if (p !== consumer) add({ kind: \"message\", consumer, provider: p, token: name });\n }\n }\n }\n return out;\n}\n\n/** May a tuple be graded off this side? Absence answers; unreadable does not; an ambiguous base identity answers nothing. */\nfunction tupleVisible(states: ReadonlyMap<string, ServiceState>, ambiguous: ReadonlySet<string>, t: ConsumerTuple): boolean {\n if (ambiguous.has(t.consumer) || ambiguous.has(t.provider)) return false;\n const readable = (id: string, axes: (\"spec\" | \"archSpec\" | \"openapi\" | \"asyncapi\")[]): boolean => {\n const state = states.get(id);\n return state === undefined || axes.every((axis) => state[axis].kind !== \"unreadable\");\n };\n // Both spec files on the consumer's side, for either kind: a line in the file\n // nobody could read may be the very one the join came or went by. NOT the\n // consumer's own contract, although an operation join reads it: it is asked\n // only whether the line names the consumer's OWN operation, and an\n // unreadable contract proves no ownership (`ownsOperation`), so the line\n // counts on that side — the index's own rule. A side holding it unread can\n // only have MORE joins, never hide one, and suppressing the join there kept\n // `diff.consumer-added` / `-removed` off a report whose victims named that\n // very consumer (the contract review, finding 4). Those extra joins are a\n // change of their own only when the readable side does not prove the\n // ownership the unread one could not — `ownershipFlicker` below.\n return readable(t.consumer, [\"spec\", \"archSpec\"]) && readable(t.provider, [t.kind === \"op\" ? \"openapi\" : \"asyncapi\"]);\n}\n\n/**\n * Is this operation join held only because the consumer's own contract did not\n * parse on the side that holds it, while on the other side it reads and\n * defines the id? Then the line names the consumer's OWN operation — the\n * readable side says so — and nothing about the join moved but the contract's\n * readability: the line that never changed \"went away\" when the contract was\n * fixed and \"appeared\" when it broke, once per provider of a shared id such as\n * `healthCheck` (the verification of finding 4, N1). The unreadable contract\n * is already its own `unreadable[]` entry on the consumer, which carries the\n * run to exit 1; a second, false finding on every provider said nothing true.\n *\n * Only that shape is skipped. A readable side that defines no such id proves\n * no ownership either, so the join is the line's and is graded as before; a\n * message join asks no ownership at all (`ownsOperation` says why).\n */\nfunction ownershipFlicker(sides: { holding: ReadonlyMap<string, ServiceState>; other: ReadonlyMap<string, ServiceState> }, t: ConsumerTuple): boolean {\n if (t.kind !== \"op\" || sides.holding.get(t.consumer)?.openapi.kind !== \"unreadable\") return false;\n const other = sides.other.get(t.consumer);\n return other !== undefined && ownsOperation(other, t.token);\n}\n\n/** One graded cross-service join change between the two states. */\nexport interface ConsumerChange {\n t: ConsumerTuple;\n change: \"added\" | \"removed\";\n}\n\n/**\n * The joins that appeared or went away between base and current, sorted for\n * deterministic output (provider, kind, token, consumer). The visibility\n * guard is the containment rule: a join is never called \"added\" off a base\n * side nobody could read, nor \"removed\" off an unreadable working file — the\n * suspended axis already carries that fact. The flicker guard is its mirror on\n * the side that HOLDS the join: one held only because the consumer's own\n * contract did not read there, over an id the other side shows it owns, is no\n * change either.\n */\nexport function consumerJoinChanges(input: {\n base: ReadonlyMap<string, ServiceState>;\n baseAmbiguous: ReadonlySet<string>;\n current: ReadonlyMap<string, ServiceState>;\n currentAmbiguous: ReadonlySet<string>;\n}): ConsumerChange[] {\n const baseTuples = consumerTuples(input.base);\n const currentTuples = consumerTuples(input.current);\n const graded: ConsumerChange[] = [];\n const added = { holding: input.current, other: input.base };\n const removed = { holding: input.base, other: input.current };\n for (const [key, t] of currentTuples) {\n if (baseTuples.has(key) || ownershipFlicker(added, t)) continue;\n if (tupleVisible(input.base, input.baseAmbiguous, t)) graded.push({ t, change: \"added\" });\n }\n for (const [key, t] of baseTuples) {\n if (currentTuples.has(key) || ownershipFlicker(removed, t)) continue;\n if (tupleVisible(input.current, input.currentAmbiguous, t)) graded.push({ t, change: \"removed\" });\n }\n return graded.sort((a, b) => {\n const ka = `${a.t.provider}\\0${a.t.kind}\\0${a.t.token}\\0${a.t.consumer}\\0${a.change}`;\n const kb = `${b.t.provider}\\0${b.t.kind}\\0${b.t.token}\\0${b.t.consumer}\\0${b.change}`;\n return ka < kb ? -1 : 1;\n });\n}\n"]}
|
|
@@ -15,7 +15,9 @@
|
|
|
15
15
|
* directory is at the five-file limit and because this is a different subject
|
|
16
16
|
* from the semantic diff beside it: `diff` compares two states of the fleet and
|
|
17
17
|
* reports what moved, this one only decides WHICH SUBJECTS a run should look
|
|
18
|
-
* at. Nothing here reads a document
|
|
18
|
+
* at. Nothing here reads a document; the one question that has to — which call
|
|
19
|
+
* edges on the fleet map are this branch's — is `./edges.ts`'s, and it reads
|
|
20
|
+
* only the map documents the branch changed.
|
|
19
21
|
*
|
|
20
22
|
* What it does NOT do, and the line is not negotiable: a diff-scoped validate
|
|
21
23
|
* cannot see a victim boundary the change did not touch. Removing an operation
|
|
@@ -25,10 +27,17 @@
|
|
|
25
27
|
* the two gates answer "did you document what you touched" and "did you break a
|
|
26
28
|
* consumer", and neither substitutes for the other. Selling a green here that
|
|
27
29
|
* hides a broken join would be worse than shipping no flag at all.
|
|
30
|
+
*
|
|
31
|
+
* A join the branch WROTE is a different case, and it is graded: an edge the
|
|
32
|
+
* branch added or edited on the fleet map is checked against its provider's
|
|
33
|
+
* contract even with the provider out of scope (`edges` below), because the
|
|
34
|
+
* edge is the document this branch touched and the provider is only where its
|
|
35
|
+
* answer lives.
|
|
28
36
|
*/
|
|
29
37
|
import { declineDetail, gitText } from "../../provenance/git.js";
|
|
30
38
|
import { repoPath } from "../../envelope/json.js";
|
|
31
39
|
import { resolveBase } from "../base-git.js";
|
|
40
|
+
import { edgesSince } from "./edges.js";
|
|
32
41
|
/**
|
|
33
42
|
* Every path that differs between `ref` and the WORKING TREE, docs-relative.
|
|
34
43
|
*
|
|
@@ -88,6 +97,10 @@ async function changedPaths(docsDir, commit, prefix) {
|
|
|
88
97
|
function inside(path, dir) {
|
|
89
98
|
return dir !== "" && (path === dir || path.startsWith(`${dir}/`));
|
|
90
99
|
}
|
|
100
|
+
/** Did this branch change `rel` — a docs-relative file, or anything under a docs-relative directory? */
|
|
101
|
+
export function touched(scope, rel) {
|
|
102
|
+
return scope.changed.some((path) => inside(path, rel));
|
|
103
|
+
}
|
|
91
104
|
/** The fleet's own documents — the landscape target's subject, which owns no service directory. */
|
|
92
105
|
const ARCHITECTURE_DIR = "architecture";
|
|
93
106
|
/**
|
|
@@ -134,15 +147,18 @@ export async function scopeSince(request) {
|
|
|
134
147
|
};
|
|
135
148
|
}
|
|
136
149
|
const dirOf = (abs) => repoPath(docsDir, abs);
|
|
150
|
+
// `architecture/` is the landscape target's whole directory: the fleet map,
|
|
151
|
+
// the permission/capability/obligation vocabularies and the fleet ADRs all
|
|
152
|
+
// live there, and every one of them is graded by the landscape target
|
|
153
|
+
// rather than by any service.
|
|
154
|
+
const architecture = changed.filter((path) => inside(path, ARCHITECTURE_DIR));
|
|
137
155
|
return {
|
|
138
156
|
kind: "scope",
|
|
139
157
|
ref,
|
|
140
158
|
commit: base.commit,
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
// rather than by any service.
|
|
145
|
-
landscape: changed.some((path) => inside(path, ARCHITECTURE_DIR)),
|
|
159
|
+
landscape: architecture.length > 0,
|
|
160
|
+
changed,
|
|
161
|
+
edges: await edgesSince({ docsDir, commit: base.commit, prefix: base.prefix }, architecture.filter((path) => path.endsWith(".likec4"))),
|
|
146
162
|
services: request.services.filter((svc) => changed.some((path) => inside(path, dirOf(svc.dir)))),
|
|
147
163
|
features: request.features.filter((feat) => changed.some((path) => inside(path, dirOf(feat.dir)))),
|
|
148
164
|
totals: { services: request.services.length, features: request.features.length },
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"changed-paths.js","sourceRoot":"","sources":["../../../../src/core/diff/scope/changed-paths.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * Which targets a base git ref puts in scope — the narrowing behind\n * `loam validate --all --base <ref>`.\n *\n * The defect this exists for: a partly-adopted fleet has no CI setting that is\n * both passing today and tightening over time. `--all` is green over eleven\n * undocumented services (they warn, and warnings do not gate) and `--strict` is\n * red from the first minute of adoption until the last boundary is written, so\n * a team installs either a gate that never notices adoption stalling at 8% or\n * one it cannot turn on. `--base` is the third setting: grade what this branch\n * TOUCHED, at full `--strict` severity if the caller wants it, and let the\n * untouched majority stay untouched.\n *\n * A SUB-PACKAGE of `core/diff/` rather than a sixth file in it, because that\n * directory is at the five-file limit and because this is a different subject\n * from the semantic diff beside it: `diff` compares two states of the fleet and\n * reports what moved, this one only decides WHICH SUBJECTS a run should look\n * at. Nothing here reads a document.\n *\n * What it does NOT do, and the line is not negotiable: a diff-scoped validate\n * cannot see a victim boundary the change did not touch. Removing an operation\n * from `payment-service` puts `payment-service` in scope and leaves\n * `checkout-web` — whose living requirement still names the operation — out of\n * it. That join is `loam diff --base`'s question and it stays `loam diff`'s;\n * the two gates answer \"did you document what you touched\" and \"did you break a\n * consumer\", and neither substitutes for the other. Selling a green here that\n * hides a broken join would be worse than shipping no flag at all.\n */\nimport { declineDetail, gitText } from \"../../provenance/git.js\";\nimport { repoPath, type ErrorCode } from \"../../envelope/json.js\";\nimport { resolveBase } from \"../base-git.js\";\nimport type { DocsDir } from \"../../kernel/ids/dirs.js\";\nimport type { FeatureEntry, ServiceEntry } from \"../../repo/entries.js\";\n\n/** Everything the narrowing needs: where to ask, what to ask about, and what there is to narrow. */\nexport interface ScopeRequest {\n docsDir: DocsDir;\n /** The ref as the caller spelled it — echoed in every message and in the payload. */\n ref: string;\n /** The run's full service enumeration, at whatever depth the tree walk found each one. */\n services: readonly ServiceEntry[];\n /** The run's full ACTIVE feature enumeration. */\n features: readonly FeatureEntry[];\n}\n\n/** The narrowed run: which targets the branch touched, and what it narrowed from. */\nexport interface ValidateScope {\n kind: \"scope\";\n ref: string;\n /** The 40-hex commit `ref` resolved to — what a rerun would have to pass to reproduce this scope. */\n commit: string;\n /** Did `architecture/` change? The landscape target is the fleet's own subject and has no directory of its own. */\n landscape: boolean;\n services: ServiceEntry[];\n features: FeatureEntry[];\n /** The denominators — what the run would have graded without `--base`. */\n totals: { services: number; features: number };\n}\n\n/**\n * A refusal, already spelled. The code is one `loam diff` already emits for the\n * same two conditions and the message is in the same register, deliberately: a\n * second stable code for \"that ref does not resolve\" would be a second thing to\n * document, branch on and keep in step, for a condition that is identical.\n *\n * The prose is built here rather than in the command because it is the same\n * sentence for every caller and because `base-git.ts`'s `detail` — git's own\n * words, bounded — is what makes it diagnosable; a command that only received a\n * `kind` would have to reinvent the sentence to say why.\n */\nexport interface ScopeRefusal {\n kind: \"refused\";\n code: ErrorCode;\n message: string;\n}\n\nexport type ScopeOutcome = ValidateScope | ScopeRefusal;\n\n/**\n * Every path that differs between `ref` and the WORKING TREE, docs-relative.\n *\n * Two questions, not one, and the second is not optional: `git diff` compares\n * the tree git tracks, so a service adopted five minutes ago — a brand new\n * `services/<id>/spec.md` nobody has committed — is invisible to it. `validate`\n * grades the working tree, so the scope has to be the working tree's, and an\n * untracked new boundary that the run then skipped would be the flag's very\n * first user reporting that it graded nothing.\n *\n * `--no-renames` on purpose: with rename detection on (git's default since\n * 2.9) a service moved into a subsystem reports only its DESTINATION, and the\n * checks that grade the fleet from the old placement would be out of scope.\n * Two paths for a move is the honest answer.\n *\n * `-z` because git quotes non-ASCII and space-bearing paths in line output —\n * the same reason `gitTrackedFiles` asks that way, and the same phantom\n * directory named `\"src` if it did not.\n */\nasync function changedPaths(docsDir: DocsDir, commit: string, prefix: string): Promise<string[] | { detail: string }> {\n // Both run from docsDir. `git diff` ignores cwd and answers for the whole\n // repository with root-relative paths; `git ls-files` restricts itself to\n // cwd, and `--full-name` makes ITS paths root-relative too — so one prefix\n // strip below is correct for both, exactly as `listBaseTree` needs one.\n const tracked = await gitText(docsDir, [\"diff\", \"--name-only\", \"--no-renames\", \"-z\", commit, \"--\"]);\n if (tracked.spawnError !== undefined || tracked.code !== 0 || tracked.overflowed) {\n return { detail: tracked.overflowed ? \"the changed-path listing exceeded the output cap.\" : declineDetail(tracked) };\n }\n const untracked = await gitText(docsDir, [\"ls-files\", \"--others\", \"--exclude-standard\", \"--full-name\", \"-z\"]);\n if (untracked.spawnError !== undefined || untracked.code !== 0 || untracked.overflowed) {\n return { detail: untracked.overflowed ? \"the untracked-path listing exceeded the output cap.\" : declineDetail(untracked) };\n }\n const seen = new Set<string>();\n for (const raw of [...tracked.out.split(\"\\0\"), ...untracked.out.split(\"\\0\")]) {\n // A docs repo checked out as a subdirectory of a larger repository answers\n // in the whole repository's paths; anything outside the docs dir is\n // somebody else's change and is not a target here.\n if (raw === \"\" || !raw.startsWith(prefix)) continue;\n const rel = raw.slice(prefix.length);\n if (rel !== \"\") seen.add(rel);\n }\n return [...seen].sort();\n}\n\n/**\n * Is `path` inside `dir`? Both docs-relative, forward-slashed.\n *\n * A prefix test on the directory plus a separator, never a split on \"/\" and a\n * read of the second segment: a service filed under a subsystem lives at\n * `services/platform/payment-service/`, not at `services/<id>/`, and a\n * path-splitting implementation drops every filed service out of scope\n * SILENTLY — the run stays green because it graded nothing, which is the exact\n * failure this flag is not allowed to have. The separator is what stops\n * `services/pay` from claiming `services/payments/spec.md`.\n */\nfunction inside(path: string, dir: string): boolean {\n return dir !== \"\" && (path === dir || path.startsWith(`${dir}/`));\n}\n\n/** The fleet's own documents — the landscape target's subject, which owns no service directory. */\nconst ARCHITECTURE_DIR = \"architecture\";\n\n/**\n * Narrow a `--all` run to what changed since `ref`.\n *\n * Targets are matched through the ENUMERATIONS' own directories\n * (`ServiceEntry.dir`, `FeatureEntry.dir`), turned docs-relative by the same\n * `repoPath` the JSON contract uses. That is the whole reason the enumerations\n * are passed in rather than re-derived: they already know where each service\n * sits, at whatever depth, and asking them is what makes a filed fleet work.\n */\nexport async function scopeSince(request: ScopeRequest): Promise<ScopeOutcome> {\n const { docsDir, ref } = request;\n const base = await resolveBase(docsDir, ref);\n if (base.kind === \"no-git\") {\n return {\n kind: \"refused\",\n code: \"repository-unavailable\",\n message:\n `The docs repo at ${docsDir} is not somewhere git can answer from (${base.detail}) — ` +\n `--base narrows the run to what changed since a ref, which it reads with read-only git questions ` +\n `(rev-parse/diff/ls-files). Clone the docs repo with its history, then re-run — or drop --base to grade every target.`,\n };\n }\n if (base.kind === \"no-ref\") {\n return {\n kind: \"refused\",\n code: \"unknown-target\",\n message:\n `--base ${ref} does not resolve to a commit in the docs repo at ${docsDir}` +\n `${base.detail === \"\" ? \"\" : ` (${base.detail})`}. ` +\n `Pass a ref that exists there — main, origin/main, a tag or a commit sha — fetching first if it lives on the remote.`,\n };\n }\n\n const changed = await changedPaths(docsDir, base.commit, base.prefix);\n if (!Array.isArray(changed)) {\n // Git resolved the ref and then would not say what moved. There is no\n // partial answer to give: a scope built from half an answer grades a\n // subset nobody chose and reports it as the branch's whole footprint.\n return {\n kind: \"refused\",\n code: \"repository-unavailable\",\n message:\n `git could not list what changed since ${ref} in the docs repo at ${docsDir} (${changed.detail}) — ` +\n `without that list there is no honest scope, and a run over a guessed subset would report a green ` +\n `for targets nobody graded. Re-run once git can answer, or drop --base to grade every target.`,\n };\n }\n\n const dirOf = (abs: string): string => repoPath(docsDir, abs);\n return {\n kind: \"scope\",\n ref,\n commit: base.commit,\n // `architecture/` is the landscape target's whole directory: the fleet map,\n // the permission/capability/obligation vocabularies and the fleet ADRs all\n // live there, and every one of them is graded by the landscape target\n // rather than by any service.\n landscape: changed.some((path) => inside(path, ARCHITECTURE_DIR)),\n services: request.services.filter((svc) => changed.some((path) => inside(path, dirOf(svc.dir)))),\n features: request.features.filter((feat) => changed.some((path) => inside(path, dirOf(feat.dir)))),\n totals: { services: request.services.length, features: request.features.length },\n };\n}\n\n/**\n * The `scope` object the `--json` payload carries — the run saying out loud\n * what it looked at.\n *\n * Additive, and emitted only under `--base`, so no consumer of today's payload\n * moves. It exists because the alternative is a payload that is byte-shaped\n * like a whole-fleet run and is not one: `summary.services` would report two\n * where the fleet has twelve, `valid: true` would read as a claim about the\n * fleet, and nothing in the document would say otherwise.\n */\nexport function scopeJson(scope: ValidateScope): Record<string, unknown> {\n return {\n base: scope.ref,\n commit: scope.commit,\n landscape: scope.landscape,\n services: scope.services.map((svc) => svc.id),\n features: scope.features.map((feat) => feat.id),\n totals: scope.totals,\n };\n}\n\n/**\n * The one line both the text view and a reader of a CI log need: how much of\n * the fleet this run actually graded, and against what.\n *\n * The \"nothing was graded\" clause is the load-bearing half. A `--base` run with\n * no targets in scope has nothing to report and exits 0 — correctly, the branch\n * touched no governed boundary — and a 0 that reads like a green over the\n * system is the exact failure the docs-repo readiness gate exists to prevent.\n * So the sentence says it, rather than letting an empty report say nothing.\n */\nexport function scopeLine(scope: ValidateScope): string {\n const graded = scope.services.length + scope.features.length + (scope.landscape ? 1 : 0);\n return (\n `${scope.services.length} of ${scope.totals.services} services, ` +\n `${scope.features.length} of ${scope.totals.features} features in scope since ${scope.ref}` +\n (scope.landscape ? \", plus the landscape\" : \"\") +\n (graded === 0 ? \" — nothing was graded\" : \"\")\n );\n}\n"]}
|
|
1
|
+
{"version":3,"file":"changed-paths.js","sourceRoot":"","sources":["../../../../src/core/diff/scope/changed-paths.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * Which targets a base git ref puts in scope — the narrowing behind\n * `loam validate --all --base <ref>`.\n *\n * The defect this exists for: a partly-adopted fleet has no CI setting that is\n * both passing today and tightening over time. `--all` is green over eleven\n * undocumented services (they warn, and warnings do not gate) and `--strict` is\n * red from the first minute of adoption until the last boundary is written, so\n * a team installs either a gate that never notices adoption stalling at 8% or\n * one it cannot turn on. `--base` is the third setting: grade what this branch\n * TOUCHED, at full `--strict` severity if the caller wants it, and let the\n * untouched majority stay untouched.\n *\n * A SUB-PACKAGE of `core/diff/` rather than a sixth file in it, because that\n * directory is at the five-file limit and because this is a different subject\n * from the semantic diff beside it: `diff` compares two states of the fleet and\n * reports what moved, this one only decides WHICH SUBJECTS a run should look\n * at. Nothing here reads a document; the one question that has to — which call\n * edges on the fleet map are this branch's — is `./edges.ts`'s, and it reads\n * only the map documents the branch changed.\n *\n * What it does NOT do, and the line is not negotiable: a diff-scoped validate\n * cannot see a victim boundary the change did not touch. Removing an operation\n * from `payment-service` puts `payment-service` in scope and leaves\n * `checkout-web` — whose living requirement still names the operation — out of\n * it. That join is `loam diff --base`'s question and it stays `loam diff`'s;\n * the two gates answer \"did you document what you touched\" and \"did you break a\n * consumer\", and neither substitutes for the other. Selling a green here that\n * hides a broken join would be worse than shipping no flag at all.\n *\n * A join the branch WROTE is a different case, and it is graded: an edge the\n * branch added or edited on the fleet map is checked against its provider's\n * contract even with the provider out of scope (`edges` below), because the\n * edge is the document this branch touched and the provider is only where its\n * answer lives.\n */\nimport { declineDetail, gitText } from \"../../provenance/git.js\";\nimport { repoPath, type ErrorCode } from \"../../envelope/json.js\";\nimport { resolveBase } from \"../base-git.js\";\nimport { edgesSince } from \"./edges.js\";\nimport type { DocsDir } from \"../../kernel/ids/dirs.js\";\nimport type { FeatureEntry, ServiceEntry } from \"../../repo/entries.js\";\n\n/** Everything the narrowing needs: where to ask, what to ask about, and what there is to narrow. */\nexport interface ScopeRequest {\n docsDir: DocsDir;\n /** The ref as the caller spelled it — echoed in every message and in the payload. */\n ref: string;\n /** The run's full service enumeration, at whatever depth the tree walk found each one. */\n services: readonly ServiceEntry[];\n /** The run's full ACTIVE feature enumeration. */\n features: readonly FeatureEntry[];\n}\n\n/** The narrowed run: which targets the branch touched, and what it narrowed from. */\nexport interface ValidateScope {\n kind: \"scope\";\n ref: string;\n /** The 40-hex commit `ref` resolved to — what a rerun would have to pass to reproduce this scope. */\n commit: string;\n /** Did `architecture/` change? The landscape target is the fleet's own subject and has no directory of its own. */\n landscape: boolean;\n /**\n * Every docs-relative path that differs from the base, untracked files\n * included. Kept because the landscape target grades more files than the\n * map — the fleet vocabularies, AGENTS.md — and `touched` is how the run asks\n * which of them this branch changed.\n */\n changed: readonly string[];\n /**\n * The fleet-map call edges this branch introduced or changed, as\n * `./edges.ts`'s `edgeKey`s. Empty unless `architecture/` changed.\n */\n edges: ReadonlySet<string>;\n services: ServiceEntry[];\n features: FeatureEntry[];\n /** The denominators — what the run would have graded without `--base`. */\n totals: { services: number; features: number };\n}\n\n/**\n * A refusal, already spelled. The code is one `loam diff` already emits for the\n * same two conditions and the message is in the same register, deliberately: a\n * second stable code for \"that ref does not resolve\" would be a second thing to\n * document, branch on and keep in step, for a condition that is identical.\n *\n * The prose is built here rather than in the command because it is the same\n * sentence for every caller and because `base-git.ts`'s `detail` — git's own\n * words, bounded — is what makes it diagnosable; a command that only received a\n * `kind` would have to reinvent the sentence to say why.\n */\nexport interface ScopeRefusal {\n kind: \"refused\";\n code: ErrorCode;\n message: string;\n}\n\nexport type ScopeOutcome = ValidateScope | ScopeRefusal;\n\n/**\n * Every path that differs between `ref` and the WORKING TREE, docs-relative.\n *\n * Two questions, not one, and the second is not optional: `git diff` compares\n * the tree git tracks, so a service adopted five minutes ago — a brand new\n * `services/<id>/spec.md` nobody has committed — is invisible to it. `validate`\n * grades the working tree, so the scope has to be the working tree's, and an\n * untracked new boundary that the run then skipped would be the flag's very\n * first user reporting that it graded nothing.\n *\n * `--no-renames` on purpose: with rename detection on (git's default since\n * 2.9) a service moved into a subsystem reports only its DESTINATION, and the\n * checks that grade the fleet from the old placement would be out of scope.\n * Two paths for a move is the honest answer.\n *\n * `-z` because git quotes non-ASCII and space-bearing paths in line output —\n * the same reason `gitTrackedFiles` asks that way, and the same phantom\n * directory named `\"src` if it did not.\n */\nasync function changedPaths(docsDir: DocsDir, commit: string, prefix: string): Promise<string[] | { detail: string }> {\n // Both run from docsDir. `git diff` ignores cwd and answers for the whole\n // repository with root-relative paths; `git ls-files` restricts itself to\n // cwd, and `--full-name` makes ITS paths root-relative too — so one prefix\n // strip below is correct for both, exactly as `listBaseTree` needs one.\n const tracked = await gitText(docsDir, [\"diff\", \"--name-only\", \"--no-renames\", \"-z\", commit, \"--\"]);\n if (tracked.spawnError !== undefined || tracked.code !== 0 || tracked.overflowed) {\n return { detail: tracked.overflowed ? \"the changed-path listing exceeded the output cap.\" : declineDetail(tracked) };\n }\n const untracked = await gitText(docsDir, [\"ls-files\", \"--others\", \"--exclude-standard\", \"--full-name\", \"-z\"]);\n if (untracked.spawnError !== undefined || untracked.code !== 0 || untracked.overflowed) {\n return { detail: untracked.overflowed ? \"the untracked-path listing exceeded the output cap.\" : declineDetail(untracked) };\n }\n const seen = new Set<string>();\n for (const raw of [...tracked.out.split(\"\\0\"), ...untracked.out.split(\"\\0\")]) {\n // A docs repo checked out as a subdirectory of a larger repository answers\n // in the whole repository's paths; anything outside the docs dir is\n // somebody else's change and is not a target here.\n if (raw === \"\" || !raw.startsWith(prefix)) continue;\n const rel = raw.slice(prefix.length);\n if (rel !== \"\") seen.add(rel);\n }\n return [...seen].sort();\n}\n\n/**\n * Is `path` inside `dir`? Both docs-relative, forward-slashed.\n *\n * A prefix test on the directory plus a separator, never a split on \"/\" and a\n * read of the second segment: a service filed under a subsystem lives at\n * `services/platform/payment-service/`, not at `services/<id>/`, and a\n * path-splitting implementation drops every filed service out of scope\n * SILENTLY — the run stays green because it graded nothing, which is the exact\n * failure this flag is not allowed to have. The separator is what stops\n * `services/pay` from claiming `services/payments/spec.md`.\n */\nfunction inside(path: string, dir: string): boolean {\n return dir !== \"\" && (path === dir || path.startsWith(`${dir}/`));\n}\n\n/** Did this branch change `rel` — a docs-relative file, or anything under a docs-relative directory? */\nexport function touched(scope: ValidateScope, rel: string): boolean {\n return scope.changed.some((path) => inside(path, rel));\n}\n\n/** The fleet's own documents — the landscape target's subject, which owns no service directory. */\nconst ARCHITECTURE_DIR = \"architecture\";\n\n/**\n * Narrow a `--all` run to what changed since `ref`.\n *\n * Targets are matched through the ENUMERATIONS' own directories\n * (`ServiceEntry.dir`, `FeatureEntry.dir`), turned docs-relative by the same\n * `repoPath` the JSON contract uses. That is the whole reason the enumerations\n * are passed in rather than re-derived: they already know where each service\n * sits, at whatever depth, and asking them is what makes a filed fleet work.\n */\nexport async function scopeSince(request: ScopeRequest): Promise<ScopeOutcome> {\n const { docsDir, ref } = request;\n const base = await resolveBase(docsDir, ref);\n if (base.kind === \"no-git\") {\n return {\n kind: \"refused\",\n code: \"repository-unavailable\",\n message:\n `The docs repo at ${docsDir} is not somewhere git can answer from (${base.detail}) — ` +\n `--base narrows the run to what changed since a ref, which it reads with read-only git questions ` +\n `(rev-parse/diff/ls-files). Clone the docs repo with its history, then re-run — or drop --base to grade every target.`,\n };\n }\n if (base.kind === \"no-ref\") {\n return {\n kind: \"refused\",\n code: \"unknown-target\",\n message:\n `--base ${ref} does not resolve to a commit in the docs repo at ${docsDir}` +\n `${base.detail === \"\" ? \"\" : ` (${base.detail})`}. ` +\n `Pass a ref that exists there — main, origin/main, a tag or a commit sha — fetching first if it lives on the remote.`,\n };\n }\n\n const changed = await changedPaths(docsDir, base.commit, base.prefix);\n if (!Array.isArray(changed)) {\n // Git resolved the ref and then would not say what moved. There is no\n // partial answer to give: a scope built from half an answer grades a\n // subset nobody chose and reports it as the branch's whole footprint.\n return {\n kind: \"refused\",\n code: \"repository-unavailable\",\n message:\n `git could not list what changed since ${ref} in the docs repo at ${docsDir} (${changed.detail}) — ` +\n `without that list there is no honest scope, and a run over a guessed subset would report a green ` +\n `for targets nobody graded. Re-run once git can answer, or drop --base to grade every target.`,\n };\n }\n\n const dirOf = (abs: string): string => repoPath(docsDir, abs);\n // `architecture/` is the landscape target's whole directory: the fleet map,\n // the permission/capability/obligation vocabularies and the fleet ADRs all\n // live there, and every one of them is graded by the landscape target\n // rather than by any service.\n const architecture = changed.filter((path) => inside(path, ARCHITECTURE_DIR));\n return {\n kind: \"scope\",\n ref,\n commit: base.commit,\n landscape: architecture.length > 0,\n changed,\n edges: await edgesSince(\n { docsDir, commit: base.commit, prefix: base.prefix },\n architecture.filter((path) => path.endsWith(\".likec4\")),\n ),\n services: request.services.filter((svc) => changed.some((path) => inside(path, dirOf(svc.dir)))),\n features: request.features.filter((feat) => changed.some((path) => inside(path, dirOf(feat.dir)))),\n totals: { services: request.services.length, features: request.features.length },\n };\n}\n\n/**\n * The `scope` object the `--json` payload carries — the run saying out loud\n * what it looked at.\n *\n * Additive, and emitted only under `--base`, so no consumer of today's payload\n * moves. It exists because the alternative is a payload that is byte-shaped\n * like a whole-fleet run and is not one: `summary.services` would report two\n * where the fleet has twelve, `valid: true` would read as a claim about the\n * fleet, and nothing in the document would say otherwise.\n */\nexport function scopeJson(scope: ValidateScope): Record<string, unknown> {\n return {\n base: scope.ref,\n commit: scope.commit,\n landscape: scope.landscape,\n services: scope.services.map((svc) => svc.id),\n features: scope.features.map((feat) => feat.id),\n totals: scope.totals,\n };\n}\n\n/**\n * The one line both the text view and a reader of a CI log need: how much of\n * the fleet this run actually graded, and against what.\n *\n * The \"nothing was graded\" clause is the load-bearing half. A `--base` run with\n * no targets in scope has nothing to report and exits 0 — correctly, the branch\n * touched no governed boundary — and a 0 that reads like a green over the\n * system is the exact failure the docs-repo readiness gate exists to prevent.\n * So the sentence says it, rather than letting an empty report say nothing.\n */\nexport function scopeLine(scope: ValidateScope): string {\n const graded = scope.services.length + scope.features.length + (scope.landscape ? 1 : 0);\n return (\n `${scope.services.length} of ${scope.totals.services} services, ` +\n `${scope.features.length} of ${scope.totals.features} features in scope since ${scope.ref}` +\n (scope.landscape ? \", plus the landscape\" : \"\") +\n (graded === 0 ? \" — nothing was graded\" : \"\")\n );\n}\n"]}
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Which fleet-map call edges a branch introduced — the one half of the `--base`
|
|
3
|
+
* narrowing that reads a document.
|
|
4
|
+
*
|
|
5
|
+
* The defect this exists for: `spine.op-undefined` is graded on the PROVIDER's
|
|
6
|
+
* service target, and an `architecture/` change only ever put the landscape in
|
|
7
|
+
* scope. So the hand edit the spine-first path is built on — naming the
|
|
8
|
+
* operation on a call edge — was graded by no gate a PR runs: README's own
|
|
9
|
+
* one-letter example (`authorisePayment` against a contract spelling it
|
|
10
|
+
* `authorizePayment`) merged green through `loam diff --base` and through
|
|
11
|
+
* `validate --all --base --strict`, on the adopted example fleet and on a
|
|
12
|
+
* freshly seeded one alike. This module answers the question that grade needs
|
|
13
|
+
* — which edges are this branch's — and `commands/validate/scope.ts` grades
|
|
14
|
+
* exactly those against their providers' contracts.
|
|
15
|
+
*
|
|
16
|
+
* AN EDGE IS ITS (source, target, op) TUPLE. A tuple present now and absent at
|
|
17
|
+
* the base is new or changed — a new edge, a re-pointed one, a renamed
|
|
18
|
+
* endpoint, an op named or re-spelled — and every one of those is a join the
|
|
19
|
+
* branch wrote. A tuple present at both ends is not, wherever in the file it
|
|
20
|
+
* moved and however it was re-indented: the key is what the edge MEANS, so a
|
|
21
|
+
* reformat grades nothing.
|
|
22
|
+
*
|
|
23
|
+
* TEXT, NOT A PARSE, at both ends. The base side would otherwise need a second
|
|
24
|
+
* LikeC4 workspace over bytes read out of git — the most expensive thing loam
|
|
25
|
+
* does, paid on every map PR to answer a question about a handful of lines.
|
|
26
|
+
* `scanModel` is the archive's splice map and resolves endpoints the way LikeC4
|
|
27
|
+
* does; `test/likec4-scan-parity.test.ts` pins the two readings together, and
|
|
28
|
+
* the key below is built from the same three fields the parsed `Rel` carries, so
|
|
29
|
+
* a scanned key names the edge the parsed map holds.
|
|
30
|
+
*
|
|
31
|
+
* ONLY THE DOCUMENTS THAT CHANGED are read, at both ends: an edge in an
|
|
32
|
+
* untouched document is by construction not this branch's, and an edge moved
|
|
33
|
+
* between two documents changes both, so its base key is still read.
|
|
34
|
+
*
|
|
35
|
+
* Which way a failure falls, and why: a base document git will not show — new
|
|
36
|
+
* on this branch, or a git that declines — contributes no base keys, so every
|
|
37
|
+
* edge in it reads as new and is graded. Grading an edge the branch did not
|
|
38
|
+
* write costs a reader one finding; skipping one it did write is the green this
|
|
39
|
+
* module exists to remove.
|
|
40
|
+
*/
|
|
41
|
+
import { existsSync } from "node:fs";
|
|
42
|
+
import { readFile } from "node:fs/promises";
|
|
43
|
+
import { join } from "node:path";
|
|
44
|
+
import { scanModel } from "../../c4/source-scan.js";
|
|
45
|
+
import { inOrder } from "../../kernel/concurrency.js";
|
|
46
|
+
import { showBaseFile } from "../base-git.js";
|
|
47
|
+
/**
|
|
48
|
+
* One edge's identity as a string a `Set` can hold. JSON rather than a joined
|
|
49
|
+
* string because an element id may legally carry any separator a join would
|
|
50
|
+
* pick, and two different tuples must never spell one key.
|
|
51
|
+
*/
|
|
52
|
+
export function edgeKey(edge) {
|
|
53
|
+
return JSON.stringify([edge.source, edge.target, edge.op ?? null]);
|
|
54
|
+
}
|
|
55
|
+
function keysOf(text) {
|
|
56
|
+
return text === null ? [] : (scanModel(text)?.rels ?? []).map(edgeKey);
|
|
57
|
+
}
|
|
58
|
+
/** The document as the base commit held it, or null when git will not show it (see the banner for why that is safe). */
|
|
59
|
+
async function baseText(base, rel) {
|
|
60
|
+
const read = await showBaseFile(base, rel);
|
|
61
|
+
return read.kind === "ok" ? read.bytes.toString("utf8") : null;
|
|
62
|
+
}
|
|
63
|
+
/** The document as the working tree holds it, or null when the branch deleted it. */
|
|
64
|
+
async function currentText(base, rel) {
|
|
65
|
+
const abs = join(base.docsDir, rel);
|
|
66
|
+
return existsSync(abs) ? readFile(abs, "utf8") : null;
|
|
67
|
+
}
|
|
68
|
+
/**
|
|
69
|
+
* The `edgeKey`s of every edge `documents` hold now and did not hold at the
|
|
70
|
+
* base. `documents` are docs-relative `.likec4` paths the branch changed —
|
|
71
|
+
* the caller's filter, so this module never has to know which directory the
|
|
72
|
+
* fleet map lives in.
|
|
73
|
+
*/
|
|
74
|
+
export async function edgesSince(base, documents) {
|
|
75
|
+
const sides = await inOrder(documents, async (rel) => {
|
|
76
|
+
const [before, after] = await Promise.all([baseText(base, rel), currentText(base, rel)]);
|
|
77
|
+
return { before: keysOf(before), after: keysOf(after) };
|
|
78
|
+
});
|
|
79
|
+
const before = new Set(sides.flatMap((side) => side.before));
|
|
80
|
+
return new Set(sides.flatMap((side) => side.after).filter((key) => !before.has(key)));
|
|
81
|
+
}
|
|
82
|
+
//# sourceMappingURL=edges.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"edges.js","sourceRoot":"","sources":["../../../../src/core/diff/scope/edges.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAuCG;AACH,OAAO,EAAE,UAAU,EAAE,MAAM,SAAS,CAAC;AACrC,OAAO,EAAE,QAAQ,EAAE,MAAM,kBAAkB,CAAC;AAC5C,OAAO,EAAE,IAAI,EAAE,MAAM,WAAW,CAAC;AACjC,OAAO,EAAE,SAAS,EAAE,MAAM,yBAAyB,CAAC;AACpD,OAAO,EAAE,OAAO,EAAE,MAAM,6BAA6B,CAAC;AACtD,OAAO,EAAE,YAAY,EAAqB,MAAM,gBAAgB,CAAC;AASjE;;;;GAIG;AACH,MAAM,UAAU,OAAO,CAAC,IAAe;IACrC,OAAO,IAAI,CAAC,SAAS,CAAC,CAAC,IAAI,CAAC,MAAM,EAAE,IAAI,CAAC,MAAM,EAAE,IAAI,CAAC,EAAE,IAAI,IAAI,CAAC,CAAC,CAAC;AACrE,CAAC;AAED,SAAS,MAAM,CAAC,IAAmB;IACjC,OAAO,IAAI,KAAK,IAAI,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,CAAC,SAAS,CAAC,IAAI,CAAC,EAAE,IAAI,IAAI,EAAE,CAAC,CAAC,GAAG,CAAC,OAAO,CAAC,CAAC;AACzE,CAAC;AAED,wHAAwH;AACxH,KAAK,UAAU,QAAQ,CAAC,IAAkB,EAAE,GAAW;IACrD,MAAM,IAAI,GAAG,MAAM,YAAY,CAAC,IAAI,EAAE,GAAG,CAAC,CAAC;IAC3C,OAAO,IAAI,CAAC,IAAI,KAAK,IAAI,CAAC,CAAC,CAAC,IAAI,CAAC,KAAK,CAAC,QAAQ,CAAC,MAAM,CAAC,CAAC,CAAC,CAAC,IAAI,CAAC;AACjE,CAAC;AAED,qFAAqF;AACrF,KAAK,UAAU,WAAW,CAAC,IAAkB,EAAE,GAAW;IACxD,MAAM,GAAG,GAAG,IAAI,CAAC,IAAI,CAAC,OAAO,EAAE,GAAG,CAAC,CAAC;IACpC,OAAO,UAAU,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC,QAAQ,CAAC,GAAG,EAAE,MAAM,CAAC,CAAC,CAAC,CAAC,IAAI,CAAC;AACxD,CAAC;AAED;;;;;GAKG;AACH,MAAM,CAAC,KAAK,UAAU,UAAU,CAAC,IAAkB,EAAE,SAA4B;IAC/E,MAAM,KAAK,GAAG,MAAM,OAAO,CAAC,SAAS,EAAE,KAAK,EAAE,GAAG,EAAE,EAAE;QACnD,MAAM,CAAC,MAAM,EAAE,KAAK,CAAC,GAAG,MAAM,OAAO,CAAC,GAAG,CAAC,CAAC,QAAQ,CAAC,IAAI,EAAE,GAAG,CAAC,EAAE,WAAW,CAAC,IAAI,EAAE,GAAG,CAAC,CAAC,CAAC,CAAC;QACzF,OAAO,EAAE,MAAM,EAAE,MAAM,CAAC,MAAM,CAAC,EAAE,KAAK,EAAE,MAAM,CAAC,KAAK,CAAC,EAAE,CAAC;IAC1D,CAAC,CAAC,CAAC;IACH,MAAM,MAAM,GAAG,IAAI,GAAG,CAAC,KAAK,CAAC,OAAO,CAAC,CAAC,IAAI,EAAE,EAAE,CAAC,IAAI,CAAC,MAAM,CAAC,CAAC,CAAC;IAC7D,OAAO,IAAI,GAAG,CAAC,KAAK,CAAC,OAAO,CAAC,CAAC,IAAI,EAAE,EAAE,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC,MAAM,CAAC,CAAC,GAAG,EAAE,EAAE,CAAC,CAAC,MAAM,CAAC,GAAG,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC;AACxF,CAAC","sourcesContent":["/**\n * Which fleet-map call edges a branch introduced — the one half of the `--base`\n * narrowing that reads a document.\n *\n * The defect this exists for: `spine.op-undefined` is graded on the PROVIDER's\n * service target, and an `architecture/` change only ever put the landscape in\n * scope. So the hand edit the spine-first path is built on — naming the\n * operation on a call edge — was graded by no gate a PR runs: README's own\n * one-letter example (`authorisePayment` against a contract spelling it\n * `authorizePayment`) merged green through `loam diff --base` and through\n * `validate --all --base --strict`, on the adopted example fleet and on a\n * freshly seeded one alike. This module answers the question that grade needs\n * — which edges are this branch's — and `commands/validate/scope.ts` grades\n * exactly those against their providers' contracts.\n *\n * AN EDGE IS ITS (source, target, op) TUPLE. A tuple present now and absent at\n * the base is new or changed — a new edge, a re-pointed one, a renamed\n * endpoint, an op named or re-spelled — and every one of those is a join the\n * branch wrote. A tuple present at both ends is not, wherever in the file it\n * moved and however it was re-indented: the key is what the edge MEANS, so a\n * reformat grades nothing.\n *\n * TEXT, NOT A PARSE, at both ends. The base side would otherwise need a second\n * LikeC4 workspace over bytes read out of git — the most expensive thing loam\n * does, paid on every map PR to answer a question about a handful of lines.\n * `scanModel` is the archive's splice map and resolves endpoints the way LikeC4\n * does; `test/likec4-scan-parity.test.ts` pins the two readings together, and\n * the key below is built from the same three fields the parsed `Rel` carries, so\n * a scanned key names the edge the parsed map holds.\n *\n * ONLY THE DOCUMENTS THAT CHANGED are read, at both ends: an edge in an\n * untouched document is by construction not this branch's, and an edge moved\n * between two documents changes both, so its base key is still read.\n *\n * Which way a failure falls, and why: a base document git will not show — new\n * on this branch, or a git that declines — contributes no base keys, so every\n * edge in it reads as new and is graded. Grading an edge the branch did not\n * write costs a reader one finding; skipping one it did write is the green this\n * module exists to remove.\n */\nimport { existsSync } from \"node:fs\";\nimport { readFile } from \"node:fs/promises\";\nimport { join } from \"node:path\";\nimport { scanModel } from \"../../c4/source-scan.js\";\nimport { inOrder } from \"../../kernel/concurrency.js\";\nimport { showBaseFile, type ResolvedBase } from \"../base-git.js\";\n\n/** The fields of an edge that make it the same join — shared by a parsed `Rel` and a scanned one. */\ninterface EdgeTuple {\n source: string;\n target: string;\n op?: string;\n}\n\n/**\n * One edge's identity as a string a `Set` can hold. JSON rather than a joined\n * string because an element id may legally carry any separator a join would\n * pick, and two different tuples must never spell one key.\n */\nexport function edgeKey(edge: EdgeTuple): string {\n return JSON.stringify([edge.source, edge.target, edge.op ?? null]);\n}\n\nfunction keysOf(text: string | null): string[] {\n return text === null ? [] : (scanModel(text)?.rels ?? []).map(edgeKey);\n}\n\n/** The document as the base commit held it, or null when git will not show it (see the banner for why that is safe). */\nasync function baseText(base: ResolvedBase, rel: string): Promise<string | null> {\n const read = await showBaseFile(base, rel);\n return read.kind === \"ok\" ? read.bytes.toString(\"utf8\") : null;\n}\n\n/** The document as the working tree holds it, or null when the branch deleted it. */\nasync function currentText(base: ResolvedBase, rel: string): Promise<string | null> {\n const abs = join(base.docsDir, rel);\n return existsSync(abs) ? readFile(abs, \"utf8\") : null;\n}\n\n/**\n * The `edgeKey`s of every edge `documents` hold now and did not hold at the\n * base. `documents` are docs-relative `.likec4` paths the branch changed —\n * the caller's filter, so this module never has to know which directory the\n * fleet map lives in.\n */\nexport async function edgesSince(base: ResolvedBase, documents: readonly string[]): Promise<ReadonlySet<string>> {\n const sides = await inOrder(documents, async (rel) => {\n const [before, after] = await Promise.all([baseText(base, rel), currentText(base, rel)]);\n return { before: keysOf(before), after: keysOf(after) };\n });\n const before = new Set(sides.flatMap((side) => side.before));\n return new Set(sides.flatMap((side) => side.after).filter((key) => !before.has(key)));\n}\n"]}
|