@ybotok/loam 0.2.0-alpha.6 → 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 +1477 -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 +108 -11
- package/SCHEMA.md +721 -130
- 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 +82 -33
- 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 +55 -26
- package/dist/commands/seed/commit.js.map +1 -1
- package/dist/commands/seed/plan.js +64 -13
- package/dist/commands/seed/plan.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 +7 -7
- package/dist/commands/subsystem/edit.js.map +1 -1
- package/dist/commands/subsystem/move.js +60 -25
- 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 +141 -71
- package/dist/commands/subsystem/sync.js.map +1 -1
- package/dist/commands/subsystem/txn/report.js +55 -5
- package/dist/commands/subsystem/txn/report.js.map +1 -1
- package/dist/commands/subsystem/txn/txn.js +121 -45
- package/dist/commands/subsystem/txn/txn.js.map +1 -1
- package/dist/commands/subsystem/txn/views.js +82 -12
- 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/health-deps.js +9 -6
- package/dist/commands/validate/checks/health-deps.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/census.js +26 -3
- package/dist/commands/validate/fleet/census.js.map +1 -1
- package/dist/commands/validate/fleet/landscape.js +35 -32
- package/dist/commands/validate/fleet/landscape.js.map +1 -1
- package/dist/commands/validate/fleet/map/attest.js +7 -12
- package/dist/commands/validate/fleet/map/attest.js.map +1 -1
- package/dist/commands/validate/fleet/map/bindings.js +16 -5
- package/dist/commands/validate/fleet/map/bindings.js.map +1 -1
- package/dist/commands/validate/fleet/map/consumers.js +43 -122
- package/dist/commands/validate/fleet/map/consumers.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 +1 -1
- package/dist/commands/validate/fleet/views/fleet-project.js.map +1 -1
- package/dist/commands/validate/fleet/views/ids.js +33 -7
- package/dist/commands/validate/fleet/views/ids.js.map +1 -1
- package/dist/commands/validate/fleet/views/root-documents.js +17 -35
- package/dist/commands/validate/fleet/views/root-documents.js.map +1 -1
- package/dist/commands/validate/fleet/views/stale.js +31 -14
- package/dist/commands/validate/fleet/views/stale.js.map +1 -1
- package/dist/commands/validate/links/corpus.js +1 -4
- package/dist/commands/validate/links/corpus.js.map +1 -1
- package/dist/commands/validate/links/glossary.js.map +1 -1
- package/dist/commands/validate/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 +57 -102
- 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 +31 -17
- 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 +37 -21
- package/dist/core/agent/agents-md/refusals.js.map +1 -1
- package/dist/core/agent/agents-md.js +25 -10
- 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 +25 -22
- 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 +62 -24
- 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 +30 -25
- 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 +79 -5
- package/dist/core/brief/targets.js.map +1 -1
- package/dist/core/brief/unchecked.js +1 -1
- package/dist/core/brief/unchecked.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/architecture.js +31 -9
- package/dist/core/c4/project/architecture.js.map +1 -1
- package/dist/core/c4/project/documents.js +36 -10
- package/dist/core/c4/project/documents.js.map +1 -1
- package/dist/core/c4/project/load.js +14 -21
- package/dist/core/c4/project/load.js.map +1 -1
- package/dist/core/c4/project/staged.js +7 -3
- package/dist/core/c4/project/staged.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 +15 -16
- 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 +10 -3
- package/dist/core/gate/checks.js.map +1 -1
- package/dist/core/gate/partners.js +64 -19
- 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/paths.js +34 -29
- package/dist/core/repo/paths.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/render/body.js +45 -6
- package/dist/core/repo/tree/render/body.js.map +1 -1
- package/dist/core/repo/tree/render/service/header.js +125 -0
- package/dist/core/repo/tree/render/service/header.js.map +1 -0
- package/dist/core/repo/tree/render/service/peers.js +28 -0
- package/dist/core/repo/tree/render/service/peers.js.map +1 -0
- package/dist/core/repo/tree/render/service/view-id.js +67 -0
- package/dist/core/repo/tree/render/service/view-id.js.map +1 -0
- package/dist/core/repo/tree/render/service/views.js +78 -0
- package/dist/core/repo/tree/render/service/views.js.map +1 -0
- package/dist/core/repo/tree/render/stale.js +19 -2
- package/dist/core/repo/tree/render/stale.js.map +1 -1
- package/dist/core/repo/tree/render/views.js +102 -33
- package/dist/core/repo/tree/render/views.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 +45 -10
- 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 +53 -3
- package/dist/core/vocabulary/maturity.js.map +1 -1
- package/examples/README.md +28 -9
- package/examples/docs/architecture/landscape.likec4 +21 -7
- package/examples/docs/architecture/services.likec4 +70 -0
- package/examples/docs/architecture/subsystems.likec4 +3 -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/order-service/model.likec4 +10 -8
- package/examples/docs/services/payment-service/model.likec4 +6 -4
- package/examples/docs/services/platform/identity-service/model.likec4 +9 -7
- package/examples/docs/services/platform/notification-service/asyncapi.yaml +2 -1
- package/examples/docs/services/platform/notification-service/model.likec4 +4 -5
- 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,134 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The generated-contract check: the build's own OpenAPI document against the
|
|
3
|
+
* copy the docs repo committed.
|
|
4
|
+
*
|
|
5
|
+
* It sits beside the HTTP axis (`../api.ts`) rather than inside it because it
|
|
6
|
+
* reads a different document for a different question. The axis grades the
|
|
7
|
+
* committed contract against the living spec; this asks whether that committed
|
|
8
|
+
* contract is still the one the service serves, and it runs only in a service
|
|
9
|
+
* repo. They used to share one file, and naming the operationIds that moved
|
|
10
|
+
* pushed it over the line limit.
|
|
11
|
+
*/
|
|
12
|
+
import { existsSync } from "node:fs";
|
|
13
|
+
import { readFile } from "node:fs/promises";
|
|
14
|
+
import { createHash } from "node:crypto";
|
|
15
|
+
import { parse as parseYaml } from "yaml";
|
|
16
|
+
import { resolveInside } from "../../../../core/kernel/path-safety.js";
|
|
17
|
+
import { canonicalJson } from "../../../../core/openapi/digest.js";
|
|
18
|
+
import { openapiFromText } from "../../../../core/openapi/doc.js";
|
|
19
|
+
import {} from "../../../../core/vocabulary/report.js";
|
|
20
|
+
/**
|
|
21
|
+
* The build's own contract document, compared with the copy the docs repo
|
|
22
|
+
* committed — `openapi.generated-stale`, `contracts.source-missing`,
|
|
23
|
+
* `contracts.source-invalid`.
|
|
24
|
+
*
|
|
25
|
+
* WHY THIS IS NOT AN EXTRACTOR. It reads no line of service code and derives no
|
|
26
|
+
* meaning: it parses a standard document the team's own build emitted, at a
|
|
27
|
+
* path a human wrote into `loam.json`, and compares a digest. That is the same
|
|
28
|
+
* category as `verify --results` ingesting a cucumber report, and it is the
|
|
29
|
+
* only thing in the product that can notice the assumption SCHEMA quietly makes
|
|
30
|
+
* — that the committed `services/<id>/openapi.yaml` is the contract the service
|
|
31
|
+
* actually serves. Most fleets generate that document; a fleet that copies it
|
|
32
|
+
* by hand in month one is a fleet whose spine checks grade last quarter's
|
|
33
|
+
* endpoints by month six, and until now nothing could say so.
|
|
34
|
+
*
|
|
35
|
+
* IT NEVER WRITES. The copy stays a human `cp`, reviewed in a pull request,
|
|
36
|
+
* because that is what makes the docs-repo document a contract somebody agreed
|
|
37
|
+
* to rather than a cache of whatever the build last produced. loam reports the
|
|
38
|
+
* divergence and stops.
|
|
39
|
+
*
|
|
40
|
+
* THE DIGEST IS OVER CANONICAL JSON, never raw bytes. Two generator versions
|
|
41
|
+
* that order keys differently, or a YAML dumper that re-wraps a description,
|
|
42
|
+
* produce identical documents and must not produce a permanent warning nobody
|
|
43
|
+
* can clear — the lesson `requirementDigest` (core/document/spec.ts) already records.
|
|
44
|
+
*
|
|
45
|
+
* THE DIGEST DECIDES, THE operationId SETS EXPLAIN. A reworded description and
|
|
46
|
+
* 29 vanished endpoints were one identical warning, each costing a manual diff
|
|
47
|
+
* to tell apart, so a fired warning names the set difference both ways (or says
|
|
48
|
+
* the sets agree). The sets never fire it on their own: the digest still does.
|
|
49
|
+
*
|
|
50
|
+
* Runs only in the SERVICE repo. From the docs repo there is no build output to
|
|
51
|
+
* read and the whole family stays silent, exactly as `sources` provenance does.
|
|
52
|
+
*/
|
|
53
|
+
export async function generatedContractFindings(service, repoDir, spelled, committedPath) {
|
|
54
|
+
if (spelled === undefined)
|
|
55
|
+
return [];
|
|
56
|
+
let generatedPath;
|
|
57
|
+
try {
|
|
58
|
+
// Containment is checked HERE rather than at config load, with the service
|
|
59
|
+
// in hand so the refusal can name it — and a path that escapes the repo is
|
|
60
|
+
// a finding, never a thrown config error that would take down every other
|
|
61
|
+
// command in the repo.
|
|
62
|
+
generatedPath = resolveInside(repoDir, spelled, `"contracts.openapi"`);
|
|
63
|
+
}
|
|
64
|
+
catch (err) {
|
|
65
|
+
return [{
|
|
66
|
+
severity: "error",
|
|
67
|
+
code: "contracts.source-invalid",
|
|
68
|
+
subject: service,
|
|
69
|
+
message: `${service}: loam.json names contracts.openapi '${spelled}', which does not resolve inside this repository — ` +
|
|
70
|
+
`${err instanceof Error ? err.message : String(err)}`,
|
|
71
|
+
}];
|
|
72
|
+
}
|
|
73
|
+
if (!existsSync(generatedPath)) {
|
|
74
|
+
return [{
|
|
75
|
+
severity: "error",
|
|
76
|
+
code: "contracts.source-missing",
|
|
77
|
+
subject: service,
|
|
78
|
+
message: `${service}: loam.json names contracts.openapi '${spelled}', and nothing is there. ` +
|
|
79
|
+
"Either the build has not run in this checkout, or the path is wrong — CI that validates before it builds " +
|
|
80
|
+
"sees exactly this. Run the build first, or correct the path",
|
|
81
|
+
}];
|
|
82
|
+
}
|
|
83
|
+
const digests = [];
|
|
84
|
+
const idSets = [];
|
|
85
|
+
for (const [label, path] of [["generated", generatedPath], ["committed", committedPath]]) {
|
|
86
|
+
if (label === "committed" && !existsSync(path))
|
|
87
|
+
return [];
|
|
88
|
+
let parsed, text;
|
|
89
|
+
try {
|
|
90
|
+
text = await readFile(path, "utf8");
|
|
91
|
+
parsed = parseYaml(text);
|
|
92
|
+
}
|
|
93
|
+
catch (err) {
|
|
94
|
+
// An unreadable GENERATED document is this check's business; an
|
|
95
|
+
// unreadable committed one is already `openapi.invalid` and must not be
|
|
96
|
+
// reported twice under two codes.
|
|
97
|
+
if (label === "committed")
|
|
98
|
+
return [];
|
|
99
|
+
return [{
|
|
100
|
+
severity: "error",
|
|
101
|
+
code: "contracts.source-invalid",
|
|
102
|
+
subject: service,
|
|
103
|
+
message: `${service}: contracts.openapi '${spelled}' does not parse as YAML or JSON — ` +
|
|
104
|
+
`${err instanceof Error ? err.message : String(err)}`,
|
|
105
|
+
}];
|
|
106
|
+
}
|
|
107
|
+
digests.push(createHash("sha256").update(canonicalJson(parsed), "utf8").digest("hex"));
|
|
108
|
+
// The walk every operation join uses: "served" means what a spine check sees.
|
|
109
|
+
idSets.push(new Set(openapiFromText(text).ops.map((op) => op.id)));
|
|
110
|
+
}
|
|
111
|
+
if (digests[0] === digests[1])
|
|
112
|
+
return [];
|
|
113
|
+
const [served, committed] = idSets;
|
|
114
|
+
const added = [...served].filter((id) => !committed.has(id)).sort();
|
|
115
|
+
const removed = [...committed].filter((id) => !served.has(id)).sort();
|
|
116
|
+
const details = [
|
|
117
|
+
...added.map((id) => `served, not in the committed copy: ${id}`),
|
|
118
|
+
...removed.map((id) => `committed, no longer served: ${id}`),
|
|
119
|
+
];
|
|
120
|
+
const moved = details.length === 0
|
|
121
|
+
? "Every operationId matches, so what moved is inside an operation or outside `paths`. "
|
|
122
|
+
: `${added.length} operation(s) served but not committed, ${removed.length} committed but no longer served (details). `;
|
|
123
|
+
return [{
|
|
124
|
+
severity: "warn",
|
|
125
|
+
code: "openapi.generated-stale",
|
|
126
|
+
subject: service,
|
|
127
|
+
message: `${service}: the committed openapi.yaml differs from what this repository's build produces (${spelled}). ${moved}` +
|
|
128
|
+
"The docs-repo copy is what every operation join is graded against, so the fleet is being checked against a " +
|
|
129
|
+
"contract this service no longer serves. Review the difference and copy the generated document across — " +
|
|
130
|
+
"loam never does that for you, because the committed contract is one somebody agreed to, not a cache of the build",
|
|
131
|
+
...(details.length === 0 ? {} : { details }),
|
|
132
|
+
}];
|
|
133
|
+
}
|
|
134
|
+
//# sourceMappingURL=generated.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"generated.js","sourceRoot":"","sources":["../../../../../src/commands/validate/service/contracts/generated.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * The generated-contract check: the build's own OpenAPI document against the\n * copy the docs repo committed.\n *\n * It sits beside the HTTP axis (`../api.ts`) rather than inside it because it\n * reads a different document for a different question. The axis grades the\n * committed contract against the living spec; this asks whether that committed\n * contract is still the one the service serves, and it runs only in a service\n * repo. They used to share one file, and naming the operationIds that moved\n * pushed it over the line limit.\n */\nimport { existsSync } from \"node:fs\";\nimport { readFile } from \"node:fs/promises\";\nimport { createHash } from \"node:crypto\";\nimport { parse as parseYaml } from \"yaml\";\nimport { resolveInside } from \"../../../../core/kernel/path-safety.js\";\nimport { canonicalJson } from \"../../../../core/openapi/digest.js\";\nimport { openapiFromText } from \"../../../../core/openapi/doc.js\";\nimport { type Finding } from \"../../../../core/vocabulary/report.js\";\n\n/**\n * The build's own contract document, compared with the copy the docs repo\n * committed — `openapi.generated-stale`, `contracts.source-missing`,\n * `contracts.source-invalid`.\n *\n * WHY THIS IS NOT AN EXTRACTOR. It reads no line of service code and derives no\n * meaning: it parses a standard document the team's own build emitted, at a\n * path a human wrote into `loam.json`, and compares a digest. That is the same\n * category as `verify --results` ingesting a cucumber report, and it is the\n * only thing in the product that can notice the assumption SCHEMA quietly makes\n * — that the committed `services/<id>/openapi.yaml` is the contract the service\n * actually serves. Most fleets generate that document; a fleet that copies it\n * by hand in month one is a fleet whose spine checks grade last quarter's\n * endpoints by month six, and until now nothing could say so.\n *\n * IT NEVER WRITES. The copy stays a human `cp`, reviewed in a pull request,\n * because that is what makes the docs-repo document a contract somebody agreed\n * to rather than a cache of whatever the build last produced. loam reports the\n * divergence and stops.\n *\n * THE DIGEST IS OVER CANONICAL JSON, never raw bytes. Two generator versions\n * that order keys differently, or a YAML dumper that re-wraps a description,\n * produce identical documents and must not produce a permanent warning nobody\n * can clear — the lesson `requirementDigest` (core/document/spec.ts) already records.\n *\n * THE DIGEST DECIDES, THE operationId SETS EXPLAIN. A reworded description and\n * 29 vanished endpoints were one identical warning, each costing a manual diff\n * to tell apart, so a fired warning names the set difference both ways (or says\n * the sets agree). The sets never fire it on their own: the digest still does.\n *\n * Runs only in the SERVICE repo. From the docs repo there is no build output to\n * read and the whole family stays silent, exactly as `sources` provenance does.\n */\nexport async function generatedContractFindings(\n service: string,\n repoDir: string,\n spelled: string | undefined,\n committedPath: string,\n): Promise<Finding[]> {\n if (spelled === undefined) return [];\n let generatedPath: string;\n try {\n // Containment is checked HERE rather than at config load, with the service\n // in hand so the refusal can name it — and a path that escapes the repo is\n // a finding, never a thrown config error that would take down every other\n // command in the repo.\n generatedPath = resolveInside(repoDir, spelled, `\"contracts.openapi\"`);\n } catch (err) {\n return [{\n severity: \"error\",\n code: \"contracts.source-invalid\",\n subject: service,\n message:\n `${service}: loam.json names contracts.openapi '${spelled}', which does not resolve inside this repository — ` +\n `${err instanceof Error ? err.message : String(err)}`,\n }];\n }\n if (!existsSync(generatedPath)) {\n return [{\n severity: \"error\",\n code: \"contracts.source-missing\",\n subject: service,\n message:\n `${service}: loam.json names contracts.openapi '${spelled}', and nothing is there. ` +\n \"Either the build has not run in this checkout, or the path is wrong — CI that validates before it builds \" +\n \"sees exactly this. Run the build first, or correct the path\",\n }];\n }\n const digests: string[] = [];\n const idSets: Set<string>[] = [];\n for (const [label, path] of [[\"generated\", generatedPath], [\"committed\", committedPath]] as const) {\n if (label === \"committed\" && !existsSync(path)) return [];\n let parsed: unknown, text: string;\n try {\n text = await readFile(path, \"utf8\");\n parsed = parseYaml(text);\n } catch (err) {\n // An unreadable GENERATED document is this check's business; an\n // unreadable committed one is already `openapi.invalid` and must not be\n // reported twice under two codes.\n if (label === \"committed\") return [];\n return [{\n severity: \"error\",\n code: \"contracts.source-invalid\",\n subject: service,\n message:\n `${service}: contracts.openapi '${spelled}' does not parse as YAML or JSON — ` +\n `${err instanceof Error ? err.message : String(err)}`,\n }];\n }\n digests.push(createHash(\"sha256\").update(canonicalJson(parsed), \"utf8\").digest(\"hex\"));\n // The walk every operation join uses: \"served\" means what a spine check sees.\n idSets.push(new Set(openapiFromText(text).ops.map((op) => op.id)));\n }\n if (digests[0] === digests[1]) return [];\n const [served, committed] = idSets as [Set<string>, Set<string>];\n const added = [...served].filter((id) => !committed.has(id)).sort();\n const removed = [...committed].filter((id) => !served.has(id)).sort();\n const details = [\n ...added.map((id) => `served, not in the committed copy: ${id}`),\n ...removed.map((id) => `committed, no longer served: ${id}`),\n ];\n const moved = details.length === 0\n ? \"Every operationId matches, so what moved is inside an operation or outside `paths`. \"\n : `${added.length} operation(s) served but not committed, ${removed.length} committed but no longer served (details). `;\n return [{\n severity: \"warn\",\n code: \"openapi.generated-stale\",\n subject: service,\n message:\n `${service}: the committed openapi.yaml differs from what this repository's build produces (${spelled}). ${moved}` +\n \"The docs-repo copy is what every operation join is graded against, so the fleet is being checked against a \" +\n \"contract this service no longer serves. Review the difference and copy the generated document across — \" +\n \"loam never does that for you, because the committed contract is one somebody agreed to, not a cache of the build\",\n ...(details.length === 0 ? {} : { details }),\n }];\n}\n"]}
|
|
@@ -86,7 +86,7 @@ export async function eventAxisFindings(axis) {
|
|
|
86
86
|
severity: "error",
|
|
87
87
|
code: "asyncapi.invalid",
|
|
88
88
|
subject: service,
|
|
89
|
-
message: `${service}: asyncapi.yaml does not parse — the event spine is unchecked for this service`,
|
|
89
|
+
message: `${service}: asyncapi.yaml does not read as AsyncAPI 3.0 (it does not parse, or it declares an older version) — the event spine is unchecked for this service`,
|
|
90
90
|
...(events.error === undefined ? {} : { details: [events.error] }),
|
|
91
91
|
});
|
|
92
92
|
}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"events.js","sourceRoot":"","sources":["../../../../../src/commands/validate/service/events/events.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;GAaG;AACH,OAAO,EAAE,UAAU,EAAE,MAAM,SAAS,CAAC;AACrC,OAAO,EAIN,MAAM,wCAAwC,CAAC;AAChD,OAAO,EAAkB,MAAM,+BAA+B,CAAC;AAC/D,OAAO,EAAqB,MAAM,gCAAgC,CAAC;AACnE,OAAO,EAAgB,MAAM,uCAAuC,CAAC;AACrE,OAAO,EAAoB,MAAM,mCAAmC,CAAC;AACrE,OAAO,EAAE,YAAY,EAAE,OAAO,EAAE,MAAM,mCAAmC,CAAC;AAC1E,OAAO,EAAE,YAAY,EAAE,MAAM,mCAAmC,CAAC;AACjE,OAAO,EAAE,gBAAgB,EAAE,MAAM,wBAAwB,CAAC;AAyB1D,MAAM,CAAC,KAAK,UAAU,iBAAiB,CAAC,IAAe;IACrD,MAAM,EAAE,OAAO,EAAE,OAAO,EAAE,EAAE,EAAE,KAAK,EAAE,IAAI,EAAE,UAAU,EAAE,QAAQ,EAAE,IAAI,EAAE,SAAS,EAAE,KAAK,EAAE,KAAK,EAAE,GAC9F,IAAI,CAAC;IACP,MAAM,QAAQ,GAAc,EAAE,CAAC;IAE/B,0EAA0E;IAC1E,2EAA2E;IAC3E,gFAAgF;IAChF,MAAM,eAAe,GAAG,QAAQ,CAAC,MAAM,CAAC;IACxC,MAAM,MAAM,GAAG,MAAM,YAAY,CAAC,KAAK,CAAC,QAAQ,EAAE,KAAK,CAAC,CAAC;IACzD,MAAM,QAAQ,GAAG,IAAI,GAAG,CAAC,MAAM,CAAC,IAAI,CAAC,CAAC;IACtC,MAAM,YAAY,GAAG,IAAI,GAAG,CAAC,MAAM,CAAC,QAAQ,CAAC,CAAC;IAE9C,yEAAyE;IACzE,6EAA6E;IAC7E,8EAA8E;IAC9E,gFAAgF;IAChF,MAAM,UAAU,GACd,IAAI,KAAK,IAAI,IAAI,IAAI,CAAC,MAAM,CAAC,MAAM,GAAG,CAAC;QACrC,CAAC,CAAC,EAAE;QACJ,CAAC,CAAC,IAAI,CAAC,aAAa,CAAC,OAAO,CAAC,CAAC,CAAC,EAAE,EAAE;YAC/B,MAAM,KAAK,GAA8E,EAAE,CAAC;YAC5F,IAAI,CAAC,CAAC,SAAS,KAAK,SAAS,IAAI,SAAU,CAAC,CAAC,CAAC,MAAM,CAAC,KAAK,EAAE,EAAE,CAAC;gBAC7D,KAAK,CAAC,IAAI,CAAC,EAAE,SAAS,EAAE,WAAW,EAAE,OAAO,EAAE,CAAC,CAAC,SAAS,EAAE,KAAK,EAAE,SAAU,CAAC,CAAC,CAAC,MAAM,CAAC,EAAE,CAAC,CAAC;YAC5F,CAAC;YACD,IAAI,CAAC,CAAC,QAAQ,KAAK,SAAS,IAAI,SAAU,CAAC,CAAC,CAAC,MAAM,CAAC,KAAK,EAAE,EAAE,CAAC;gBAC5D,KAAK,CAAC,IAAI,CAAC,EAAE,SAAS,EAAE,UAAU,EAAE,OAAO,EAAE,CAAC,CAAC,QAAQ,EAAE,KAAK,EAAE,SAAU,CAAC,CAAC,CAAC,MAAM,CAAC,EAAE,CAAC,CAAC;YAC1F,CAAC;YACD,OAAO,KAAK,CAAC;QACf,CAAC,CAAC,CAAC;IACT,yEAAyE;IACzE,2EAA2E;IAC3E,MAAM,UAAU,GAAG,CAAC,GAAG,UAAU,EAAE,GAAG,QAAQ,CAAC,MAAM,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,IAAI,KAAK,SAAS,CAAC,CAAC,CAAC,OAAO,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC;QACjG,GAAG,CAAC,CAAC,SAAS,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,EAAE,SAAS,EAAE,WAAoB,EAAE,OAAO,EAAE,CAAC,EAAE,WAAW,EAAE,CAAC,CAAC,IAAI,EAAE,CAAC,CAAC;QACjG,GAAG,CAAC,CAAC,QAAQ,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,EAAE,SAAS,EAAE,UAAmB,EAAE,OAAO,EAAE,CAAC,EAAE,WAAW,EAAE,CAAC,CAAC,IAAI,EAAE,CAAC,CAAC;KAChG,CAAC,CAAC;IAEH,IAAI,CAAC,UAAU,CAAC,KAAK,CAAC,QAAQ,CAAC,EAAE,CAAC;QAChC,wEAAwE;QACxE,wEAAwE;QACxE,0EAA0E;QAC1E,qEAAqE;QACrE,2EAA2E;QAC3E,sEAAsE;QACtE,4DAA4D;QAC5D,EAAE;QACF,gEAAgE;QAChE,MAAM,QAAQ,GAAG;YACf,GAAG,IAAI,GAAG,CAAC,CAAC,GAAG,UAAU,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,OAAO,CAAC,EAAE,GAAG,UAAU,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,OAAO,CAAC,CAAC,CAAC;SACvF,CAAC,IAAI,EAAE,CAAC;QACT,IAAI,QAAQ,CAAC,MAAM,GAAG,CAAC,EAAE,CAAC;YACxB,QAAQ,CAAC,IAAI,CAAC;gBACZ,QAAQ,EAAE,OAAO;gBACjB,IAAI,EAAE,qBAAqB;gBAC3B,OAAO,EAAE,OAAO;gBAChB,OAAO,EACL,wBAAwB,KAAK,CAAC,QAAQ,SAAS,QAAQ,CAAC,MAAM,2CAA2C;oBACzG,oGAAoG;gBACtG,OAAO,EAAE,QAAQ;aAClB,CAAC,CAAC;QACL,CAAC;IACH,CAAC;SAAM,IAAI,MAAM,CAAC,UAAU,EAAE,CAAC;QAC7B,2EAA2E;QAC3E,2EAA2E;QAC3E,8EAA8E;QAC9E,yDAAyD;QACzD,QAAQ,CAAC,IAAI,CAAC;YACZ,QAAQ,EAAE,OAAO;YACjB,IAAI,EAAE,kBAAkB;YACxB,OAAO,EAAE,OAAO;YAChB,OAAO,EAAE,GAAG,OAAO,gFAAgF;YACnG,GAAG,CAAC,MAAM,CAAC,KAAK,KAAK,SAAS,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,EAAE,OAAO,EAAE,CAAC,MAAM,CAAC,KAAK,CAAC,EAAE,CAAC;SACnE,CAAC,CAAC;IACL,CAAC;SAAM,CAAC;QACN,uEAAuE;QACvE,0EAA0E;QAC1E,wEAAwE;QACxE,IAAI,MAAM,CAAC,OAAO,CAAC,MAAM,GAAG,CAAC,EAAE,CAAC;YAC9B,QAAQ,CAAC,IAAI,CAAC;gBACZ,QAAQ,EAAE,OAAO;gBACjB,IAAI,EAAE,+BAA+B;gBACrC,OAAO,EAAE,OAAO;gBAChB,OAAO,EAAE,GAAG,OAAO,yDAAyD,MAAM,CAAC,OAAO,CAAC,IAAI,CAAC,IAAI,CAAC,8GAA8G;aACpN,CAAC,CAAC;QACL,CAAC;QACD,KAAK,MAAM,IAAI,IAAI,MAAM,CAAC,cAAc,EAAE,CAAC;YACzC,QAAQ,CAAC,IAAI,CAAC;gBACZ,QAAQ,EAAE,MAAM;gBAChB,IAAI,EAAE,4BAA4B;gBAClC,OAAO,EAAE,OAAO;gBAChB,OAAO,EAAE,GAAG,OAAO,qCAAqC,IAAI,QAAQ,OAAO,CAAC,MAAM,EAAE,IAAI,CAAC,CAAC,IAAI,CAAC,OAAO,CAAC,oIAAoI;aAC5O,CAAC,CAAC;QACL,CAAC;QACD,yEAAyE;QACzE,yEAAyE;QACzE,0EAA0E;QAC1E,iEAAiE;QACjE,+CAA+C;QAC/C,MAAM,OAAO,GAAG,MAAM,CAAC,QAAQ,CAAC,MAAM,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,YAAY,KAAK,IAAI,CAAC,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,IAAI,CAAC,CAAC;QAC1F,IAAI,OAAO,CAAC,MAAM,GAAG,CAAC,EAAE,CAAC;YACvB,QAAQ,CAAC,IAAI,CAAC;gBACZ,QAAQ,EAAE,MAAM;gBAChB,IAAI,EAAE,8BAA8B;gBACpC,OAAO,EAAE,OAAO;gBAChB,OAAO,EACL,GAAG,OAAO,KAAK,OAAO,CAAC,MAAM,yCAAyC,OAAO,CAAC,IAAI,CAAC,IAAI,CAAC,MAAM;oBAC9F,4EAA4E;oBAC5E,6DAA6D;aAChE,CAAC,CAAC;QACL,CAAC;QACD,IAAI,MAAM,CAAC,YAAY,CAAC,MAAM,GAAG,CAAC,EAAE,CAAC;YACnC,QAAQ,CAAC,IAAI,CAAC;gBACZ,QAAQ,EAAE,MAAM;gBAChB,IAAI,EAAE,yBAAyB;gBAC/B,OAAO,EAAE,OAAO;gBAChB,OAAO,EACL,GAAG,OAAO,4BAA4B,MAAM,CAAC,YAAY,CAAC,MAAM,iCAAiC;oBACjG,6FAA6F;oBAC7F,+BAA+B;gBACjC,OAAO,EAAE,MAAM,CAAC,YAAY;aAC7B,CAAC,CAAC;QACL,CAAC;QACD,6EAA6E;QAC7E,yEAAyE;QACzE,yDAAyD;QACzD,sEAAsE;QACtE,6EAA6E;QAC7E,MAAM,QAAQ,GAAG,CAAC,SAAmC,EAAE,OAAe,EAAW,EAAE,CACjF,SAAS,KAAK,WAAW,CAAC,CAAC,CAAC,QAAQ,CAAC,GAAG,CAAC,OAAO,CAAC,CAAC,CAAC,CAAC,YAAY,CAAC,GAAG,CAAC,OAAO,CAAC,CAAC;QAChF,MAAM,MAAM,GAAG,CAAC,SAAmC,EAAU,EAAE,CAC7D,SAAS,KAAK,WAAW,CAAC,CAAC,CAAC,MAAM,CAAC,CAAC,CAAC,SAAS,CAAC;QACjD,KAAK,MAAM,CAAC,IAAI,UAAU,EAAE,CAAC;YAC3B,IAAI,QAAQ,CAAC,CAAC,CAAC,SAAS,EAAE,CAAC,CAAC,OAAO,CAAC;gBAAE,SAAS;YAC/C,QAAQ,CAAC,IAAI,CAAC;gBACZ,QAAQ,EAAE,OAAO;gBACjB,IAAI,EAAE,yBAAyB;gBAC/B,OAAO,EAAE,OAAO;gBAChB,OAAO,EACL,GAAG,OAAO,oBAAoB,CAAC,CAAC,SAAS,KAAK,WAAW,CAAC,CAAC,CAAC,GAAG,OAAO,MAAM,CAAC,CAAC,KAAK,EAAE,CAAC,CAAC,CAAC,GAAG,CAAC,CAAC,KAAK,MAAM,OAAO,EAAE,GAAG;oBACpH,GAAG,CAAC,CAAC,SAAS,KAAK,CAAC,CAAC,OAAO,UAAU,OAAO,uDAAuD,MAAM,CAAC,CAAC,CAAC,SAAS,CAAC,SAAS;aACnI,CAAC,CAAC;QACL,CAAC;QACD,KAAK,MAAM,CAAC,IAAI,UAAU,EAAE,CAAC;YAC3B,IAAI,QAAQ,CAAC,CAAC,CAAC,SAAS,EAAE,CAAC,CAAC,OAAO,CAAC;gBAAE,SAAS;YAC/C,QAAQ,CAAC,IAAI,CAAC;gBACZ,QAAQ,EAAE,OAAO;gBACjB,IAAI,EAAE,8BAA8B;gBACpC,OAAO,EAAE,OAAO;gBAChB,OAAO,EAAE,GAAG,OAAO,kBAAkB,CAAC,CAAC,WAAW,KAAK,CAAC,CAAC,SAAS,KAAK,CAAC,CAAC,OAAO,UAAU,OAAO,uDAAuD,MAAM,CAAC,CAAC,CAAC,SAAS,CAAC,SAAS;aACrL,CAAC,CAAC;QACL,CAAC;QACD,qEAAqE;QACrE,2EAA2E;QAC3E,IAAI,MAAM,CAAC,QAAQ,CAAC,MAAM,GAAG,CAAC,IAAI,IAAI,CAAC,MAAM,GAAG,QAAQ,CAAC,MAAM,GAAG,CAAC,IAAI,UAAU,CAAC,MAAM,KAAK,CAAC,EAAE,CAAC;YAC/F,QAAQ,CAAC,IAAI,CAAC;gBACZ,QAAQ,EAAE,MAAM;gBAChB,IAAI,EAAE,yBAAyB;gBAC/B,OAAO,EAAE,OAAO;gBAChB,OAAO,EAAE,GAAG,OAAO,4BAA4B,MAAM,CAAC,QAAQ,CAAC,MAAM,yFAAyF;aAC/J,CAAC,CAAC;QACL,CAAC;QACD,0EAA0E;QAC1E,0EAA0E;QAC1E,oEAAoE;QACpE,wEAAwE;QACxE,yEAAyE;QACzE,uEAAuE;QACvE,+CAA+C;QAC/C,MAAM,QAAQ,GAAG,IAAI,GAAG,CAAC,UAAU,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,OAAO,CAAC,CAAC,CAAC;QAC3D,MAAM,OAAO,GAAG,CAAC,GAAG,IAAI,GAAG,CAAC,MAAM,CAAC,QAAQ,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC,MAAM,CAAC,CAAC,IAAI,EAAE,EAAE,CAAC,CAAC,QAAQ,CAAC,GAAG,CAAC,IAAI,CAAC,CAAC,CAAC;QACvG,IAAI,OAAO,CAAC,MAAM,GAAG,CAAC,EAAE,CAAC;YACvB,QAAQ,CAAC,IAAI,CAAC;gBACZ,QAAQ,EAAE,MAAM;gBAChB,IAAI,EAAE,kBAAkB;gBACxB,OAAO,EAAE,OAAO;gBAChB,OAAO,EAAE,GAAG,OAAO,KAAK,OAAO,CAAC,MAAM,6EAA6E,OAAO,CAAC,IAAI,CAAC,IAAI,CAAC,EAAE;aACxI,CAAC,CAAC;QACL,CAAC;IACH,CAAC;IAED,qEAAqE;IACrE,yEAAyE;IACzE,MAAM,QAAQ,GAAG;QACf,GAAG,IAAI,GAAG,CAAC;YACT,GAAG,UAAU,CAAC,MAAM,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,SAAS,KAAK,UAAU,CAAC,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,OAAO,CAAC;YAC7E,GAAG,UAAU,CAAC,MAAM,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,SAAS,KAAK,UAAU,CAAC,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,OAAO,CAAC;SAC9E,CAAC;KACH,CAAC,IAAI,EAAE,CAAC;IACT,QAAQ,CAAC,IAAI,CAAC,GAAG,CAAC,MAAM,gBAAgB,CAAC,EAAE,OAAO,EAAE,OAAO,EAAE,IAAI,EAAE,SAAS,EAAE,KAAK,EAAE,MAAM,EAAE,QAAQ,EAAE,KAAK,EAAE,CAAC,CAAC,CAAC,CAAC;IAElH,4EAA4E;IAC5E,4EAA4E;IAC5E,+EAA+E;IAC/E,6CAA6C;IAC7C,MAAM,WAAW,GAAG,UAAU,CAAC,MAAM,GAAG,UAAU,CAAC,MAAM,CAAC;IAC1D,IAAI,WAAW,GAAG,CAAC,IAAI,QAAQ,CAAC,MAAM,KAAK,eAAe,EAAE,CAAC;QAC3D,QAAQ,CAAC,IAAI,CAAC;YACZ,QAAQ,EAAE,IAAI;YACd,IAAI,EAAE,eAAe;YACrB,OAAO,EAAE,GAAG,OAAO,kBAAkB,WAAW,4CAA4C;SAC7F,CAAC,CAAC;IACL,CAAC;IAED,OAAO,QAAQ,CAAC;AAClB,CAAC","sourcesContent":["/**\n * The async contract axis — AsyncAPI messages on the landscape spine.\n *\n * The HTTP spine (`./spine.js`) and this one are mirror images, and the mirror\n * is the whole reason this is a second axis rather than a second `metadata`\n * key. On the HTTP axis the PROVIDER owns the contract: an edge names an\n * operation and the target's openapi.yaml settles it, locally, in one file.\n * Here the PRODUCER owns the message and the consumer joins to it from another\n * repository — so \"is this message declared\" is local, while \"does anybody\n * publish it\" is a fleet question with no local answer at all. That second\n * question is the only check on either spine that reads other services'\n * contracts; it lives in `./message-producers.js`, and this axis only decides\n * when it is worth paying for — exactly when something actually consumes.\n */\nimport { existsSync } from \"node:fs\";\nimport {\n type DeclaredService,\n type PathableService,\n type RawServiceId,\n} from \"../../../../core/kernel/ids/service.js\";\nimport { type LoadedDoc } from \"../../../../core/c4/likec4.js\";\nimport { type ServicePaths } from \"../../../../core/repo/paths.js\";\nimport { type Finding } from \"../../../../core/vocabulary/report.js\";\nimport { type Requirement } from \"../../../../core/document/spec.js\";\nimport { readAsyncapi, slotsOf } from \"../../../../core/asyncapi/read.js\";\nimport { FleetContext } from \"../../../../core/fleet-context.js\";\nimport { producerFindings } from \"./message-producers.js\";\nimport type { DocsDir } from \"../../../../core/kernel/ids/dirs.js\";\n\n/** What the service target hands the event axis. */\nexport interface EventAxis {\n docsDir: DocsDir;\n service: PathableService;\n /** `service` widened for `===` against the resolver's DOCUMENT text — see service.ts. */\n me: string;\n paths: ServicePaths;\n /** Every requirement the living spec declares; `event.messages-unlinked` counts what is written. */\n reqs: Requirement[];\n /** The ones that still govern something — a REMOVED requirement links nothing. */\n livingReqs: Requirement[];\n /** arch.spec.md's requirements, which carry as many event links as spec.md's do. */\n archReqs: Requirement[];\n /** The living landscape, or null when the repo has none. */\n land: LoadedDoc | null;\n /** Its element→service resolver; null exactly when `land` is. */\n landSvcOf: ((id: string) => DeclaredService) | null;\n /** Service directories that exist — the fleet the producer question is asked over. */\n known: ReadonlySet<RawServiceId>;\n fleet?: FleetContext;\n}\n\nexport async function eventAxisFindings(axis: EventAxis): Promise<Finding[]> {\n const { docsDir, service, me, paths, reqs, livingReqs, archReqs, land, landSvcOf, known, fleet } =\n axis;\n const findings: Finding[] = [];\n\n // Where this axis started, so the closing ok-finding can be \"nothing here\n // broke\" rather than a second, independently-computed claim about the same\n // facts. A positive finding derived from the same walk cannot disagree with it.\n const eventFindingsAt = findings.length;\n const events = await readAsyncapi(paths.asyncapi, fleet);\n const sentHere = new Set(events.sent);\n const receivedHere = new Set(events.received);\n\n // Every edge that binds THIS service to a message: `publishes` binds the\n // edge's source (a service produces what it declares an `action: send` for),\n // `consumes` binds its target. One edge may legitimately carry both — a relay\n // that reads one topic and writes another is a single arrow in most fleet maps.\n const eventEdges =\n land === null || land.errors.length > 0\n ? []\n : land.relationships.flatMap((r) => {\n const bound: { direction: \"publishes\" | \"consumes\"; message: string; other: string }[] = [];\n if (r.publishes !== undefined && landSvcOf!(r.source) === me) {\n bound.push({ direction: \"publishes\", message: r.publishes, other: landSvcOf!(r.target) });\n }\n if (r.consumes !== undefined && landSvcOf!(r.target) === me) {\n bound.push({ direction: \"consumes\", message: r.consumes, other: landSvcOf!(r.source) });\n }\n return bound;\n });\n // Both requirement namespaces, and a REMOVED requirement links nothing —\n // the `livingReqs` rule the API axis already follows, applied to the pair.\n const eventLinks = [...livingReqs, ...archReqs.filter((r) => r.kind !== \"REMOVED\")].flatMap((r) => [\n ...r.publishes.map((m) => ({ direction: \"publishes\" as const, message: m, requirement: r.name })),\n ...r.consumes.map((m) => ({ direction: \"consumes\" as const, message: m, requirement: r.name })),\n ]);\n\n if (!existsSync(paths.asyncapi)) {\n // Deliberately TWO grades, not the three `service.no-openapi` uses. Its\n // middle grade — warn when the landscape cannot prove nobody calls this\n // service — rests on HTTP being the default: most services expose one, so\n // \"I could not look\" is worth saying. An event contract is genuinely\n // optional; most services in a legacy fleet touch no topic at all. Warning\n // every one of them the moment a landscape fails to parse would put a\n // finding on the whole fleet that names a file nobody owes.\n //\n // So: something already joins into the absent file, or silence.\n const dangling = [\n ...new Set([...eventEdges.map((e) => e.message), ...eventLinks.map((e) => e.message)]),\n ].sort();\n if (dangling.length > 0) {\n findings.push({\n severity: \"error\",\n code: \"service.no-asyncapi\",\n subject: service,\n message:\n `No async contract at ${paths.asyncapi}, and ${dangling.length} message link(s) already point into it — ` +\n `every landscape edge and requirement naming one of them resolves to nothing until the file is back`,\n details: dangling,\n });\n }\n } else if (events.unreadable) {\n // The openapi.invalid discipline: a contract that EXISTS but does not read\n // is a broken source of truth, not an empty one. Grading the links against\n // an empty message set would turn one file into one `spine.message-undefined`\n // per edge, every one of them pointing at the landscape.\n findings.push({\n severity: \"error\",\n code: \"asyncapi.invalid\",\n subject: service,\n message: `${service}: asyncapi.yaml does not parse — the event spine is unchecked for this service`,\n ...(events.error === undefined ? {} : { details: [events.error] }),\n });\n } else {\n // Feature-only bookkeeping published to the fleet — the openapi axis's\n // `openapi.remove-marker-living` discipline, one code for every depth the\n // reader collects markers at (slots and inline channel messages alike).\n if (events.markers.length > 0) {\n findings.push({\n severity: \"error\",\n code: \"asyncapi.remove-marker-living\",\n subject: service,\n message: `${service}: living asyncapi.yaml carries x-loam-remove: true at ${events.markers.join(\", \")} — removal markers are valid only in feature deltas; retire a slot through a feature's asyncapi.yaml instead`,\n });\n }\n for (const name of events.duplicateNames) {\n findings.push({\n severity: \"warn\",\n code: \"asyncapi.duplicate-message\",\n subject: service,\n message: `${service}: asyncapi.yaml declares message '${name}' in ${slotsOf(events, name).join(\" and \")} — every join on the name (an edge's metadata { publishes }, a requirement's Publishes: line) picks one of those slots arbitrarily`,\n });\n }\n // The contract-depth probes. Form validated and depth did not: a payload\n // of bare `type: object` and a `$ref` to nothing both read as cleanly as\n // a full schema, while a reader still cannot rebuild the payload from the\n // green contract. Presence probes only — a non-JSON schemaFormat\n // is skipped, so Avro stays a document change.\n const empties = events.messages.filter((m) => m.payloadEmpty === true).map((m) => m.name);\n if (empties.length > 0) {\n findings.push({\n severity: \"warn\",\n code: \"asyncapi.payload-undescribed\",\n subject: service,\n message:\n `${service}: ${empties.length} message(s) declare no payload shape (${empties.join(\", \")}) — ` +\n `the name joins the spine, but nothing defines what a consumer would read; ` +\n `declare properties, or a schemaFormat for a non-JSON schema`,\n });\n }\n if (events.danglingRefs.length > 0) {\n findings.push({\n severity: \"warn\",\n code: \"asyncapi.ref-unresolved\",\n subject: service,\n message:\n `${service}: asyncapi.yaml contains ${events.danglingRefs.length} internal $ref(s) that resolve ` +\n `to nothing in the document — whatever they were meant to carry is silently absent from the ` +\n `spine's view of this contract`,\n details: events.danglingRefs,\n });\n }\n // Local resolution, both directions and both sources of a claim. An edge and\n // a requirement making the same broken claim get the same sentence under\n // different codes, matching how `spine.op-undefined` and\n // `spec-api.op-undefined` split the HTTP axis: one is the fleet map's\n // mistake, the other is this document's, and the fix is in a different file.\n const declares = (direction: \"publishes\" | \"consumes\", message: string): boolean =>\n direction === \"publishes\" ? sentHere.has(message) : receivedHere.has(message);\n const action = (direction: \"publishes\" | \"consumes\"): string =>\n direction === \"publishes\" ? \"send\" : \"receive\";\n for (const e of eventEdges) {\n if (declares(e.direction, e.message)) continue;\n findings.push({\n severity: \"error\",\n code: \"spine.message-undefined\",\n subject: service,\n message:\n `${service}: landscape edge ${e.direction === \"publishes\" ? `${service} → ${e.other}` : `${e.other} → ${service}`} ` +\n `${e.direction} '${e.message}', but ${service}'s asyncapi.yaml declares no operation with action: ${action(e.direction)} for it`,\n });\n }\n for (const e of eventLinks) {\n if (declares(e.direction, e.message)) continue;\n findings.push({\n severity: \"error\",\n code: \"spec-event.message-undefined\",\n subject: service,\n message: `${service}: requirement '${e.requirement}' ${e.direction} '${e.message}', but ${service}'s asyncapi.yaml declares no operation with action: ${action(e.direction)} for it`,\n });\n }\n // The migration-debt case, `api.ops-unlinked`'s twin: a contract and\n // requirements that never name each other leave this axis vacuously green.\n if (events.messages.length > 0 && reqs.length + archReqs.length > 0 && eventLinks.length === 0) {\n findings.push({\n severity: \"warn\",\n code: \"event.messages-unlinked\",\n subject: service,\n message: `${service}: asyncapi.yaml declares ${events.messages.length} message(s) but no requirement links any — the event axis is unchecked for this service`,\n });\n }\n // `api.ungoverned`'s event twin, and the per-message half the check above\n // cannot see: `event.messages-unlinked` fires only when ZERO links exist,\n // so a contract with five declared messages and three governed ones\n // reported nothing about the other two. The join is against requirement\n // lines ONLY, deliberately ignoring landscape edges — an edge is a claim\n // about traffic, a requirement is governance — exactly as the API axis\n // ignores inbound edges when grading coverage.\n const governed = new Set(eventLinks.map((e) => e.message));\n const orphans = [...new Set(events.messages.map((m) => m.name))].filter((name) => !governed.has(name));\n if (orphans.length > 0) {\n findings.push({\n severity: \"warn\",\n code: \"event.ungoverned\",\n subject: service,\n message: `${service}: ${orphans.length} message(s) not governed by any requirement's Publishes:/Consumes: line — ${orphans.join(\", \")}`,\n });\n }\n }\n\n // The consumed-message list is the handover to the fleet question in\n // ./message-producers.js — a fleet where nothing consumes walks nothing.\n const consumed = [\n ...new Set([\n ...eventEdges.filter((e) => e.direction === \"consumes\").map((e) => e.message),\n ...eventLinks.filter((e) => e.direction === \"consumes\").map((e) => e.message),\n ]),\n ].sort();\n findings.push(...(await producerFindings({ docsDir, service, land, landSvcOf, known, events, consumed, fleet })));\n\n // Positive confirmation, on the same rule `spine.resolved` follows: claimed\n // only where the axis actually checked something. A service that touches no\n // topic says nothing here — an \"event axis clean\" on every worker in the fleet\n // would be a green tick for work nobody did.\n const eventChecks = eventEdges.length + eventLinks.length;\n if (eventChecks > 0 && findings.length === eventFindingsAt) {\n findings.push({\n severity: \"ok\",\n code: \"event.covered\",\n message: `${service}: event spine (${eventChecks} message link(s) resolve to asyncapi.yaml)`,\n });\n }\n\n return findings;\n}\n"]}
|
|
1
|
+
{"version":3,"file":"events.js","sourceRoot":"","sources":["../../../../../src/commands/validate/service/events/events.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * The async contract axis — AsyncAPI messages on the landscape spine.\n *\n * The HTTP spine (`./spine.js`) and this one are mirror images, and the mirror\n * is the whole reason this is a second axis rather than a second `metadata`\n * key. On the HTTP axis the PROVIDER owns the contract: an edge names an\n * operation and the target's openapi.yaml settles it, locally, in one file.\n * Here the PRODUCER owns the message and the consumer joins to it from another\n * repository — so \"is this message declared\" is local, while \"does anybody\n * publish it\" is a fleet question with no local answer at all. That second\n * question is the only check on either spine that reads other services'\n * contracts; it lives in `./message-producers.js`, and this axis only decides\n * when it is worth paying for — exactly when something actually consumes.\n */\nimport { existsSync } from \"node:fs\";\nimport {\n type DeclaredService,\n type PathableService,\n type RawServiceId,\n} from \"../../../../core/kernel/ids/service.js\";\nimport { type LoadedDoc } from \"../../../../core/c4/likec4.js\";\nimport { type ServicePaths } from \"../../../../core/repo/paths.js\";\nimport { type Finding } from \"../../../../core/vocabulary/report.js\";\nimport { type Requirement } from \"../../../../core/document/spec.js\";\nimport { readAsyncapi, slotsOf } from \"../../../../core/asyncapi/read.js\";\nimport { FleetContext } from \"../../../../core/fleet-context.js\";\nimport { producerFindings } from \"./message-producers.js\";\nimport type { DocsDir } from \"../../../../core/kernel/ids/dirs.js\";\n\n/** What the service target hands the event axis. */\nexport interface EventAxis {\n docsDir: DocsDir;\n service: PathableService;\n /** `service` widened for `===` against the resolver's DOCUMENT text — see service.ts. */\n me: string;\n paths: ServicePaths;\n /** Every requirement the living spec declares; `event.messages-unlinked` counts what is written. */\n reqs: Requirement[];\n /** The ones that still govern something — a REMOVED requirement links nothing. */\n livingReqs: Requirement[];\n /** arch.spec.md's requirements, which carry as many event links as spec.md's do. */\n archReqs: Requirement[];\n /** The living landscape, or null when the repo has none. */\n land: LoadedDoc | null;\n /** Its element→service resolver; null exactly when `land` is. */\n landSvcOf: ((id: string) => DeclaredService) | null;\n /** Service directories that exist — the fleet the producer question is asked over. */\n known: ReadonlySet<RawServiceId>;\n fleet: FleetContext;\n}\n\nexport async function eventAxisFindings(axis: EventAxis): Promise<Finding[]> {\n const { docsDir, service, me, paths, reqs, livingReqs, archReqs, land, landSvcOf, known, fleet } =\n axis;\n const findings: Finding[] = [];\n\n // Where this axis started, so the closing ok-finding can be \"nothing here\n // broke\" rather than a second, independently-computed claim about the same\n // facts. A positive finding derived from the same walk cannot disagree with it.\n const eventFindingsAt = findings.length;\n const events = await readAsyncapi(paths.asyncapi, fleet);\n const sentHere = new Set(events.sent);\n const receivedHere = new Set(events.received);\n\n // Every edge that binds THIS service to a message: `publishes` binds the\n // edge's source (a service produces what it declares an `action: send` for),\n // `consumes` binds its target. One edge may legitimately carry both — a relay\n // that reads one topic and writes another is a single arrow in most fleet maps.\n const eventEdges =\n land === null || land.errors.length > 0\n ? []\n : land.relationships.flatMap((r) => {\n const bound: { direction: \"publishes\" | \"consumes\"; message: string; other: string }[] = [];\n if (r.publishes !== undefined && landSvcOf!(r.source) === me) {\n bound.push({ direction: \"publishes\", message: r.publishes, other: landSvcOf!(r.target) });\n }\n if (r.consumes !== undefined && landSvcOf!(r.target) === me) {\n bound.push({ direction: \"consumes\", message: r.consumes, other: landSvcOf!(r.source) });\n }\n return bound;\n });\n // Both requirement namespaces, and a REMOVED requirement links nothing —\n // the `livingReqs` rule the API axis already follows, applied to the pair.\n const eventLinks = [...livingReqs, ...archReqs.filter((r) => r.kind !== \"REMOVED\")].flatMap((r) => [\n ...r.publishes.map((m) => ({ direction: \"publishes\" as const, message: m, requirement: r.name })),\n ...r.consumes.map((m) => ({ direction: \"consumes\" as const, message: m, requirement: r.name })),\n ]);\n\n if (!existsSync(paths.asyncapi)) {\n // Deliberately TWO grades, not the three `service.no-openapi` uses. Its\n // middle grade — warn when the landscape cannot prove nobody calls this\n // service — rests on HTTP being the default: most services expose one, so\n // \"I could not look\" is worth saying. An event contract is genuinely\n // optional; most services in a legacy fleet touch no topic at all. Warning\n // every one of them the moment a landscape fails to parse would put a\n // finding on the whole fleet that names a file nobody owes.\n //\n // So: something already joins into the absent file, or silence.\n const dangling = [\n ...new Set([...eventEdges.map((e) => e.message), ...eventLinks.map((e) => e.message)]),\n ].sort();\n if (dangling.length > 0) {\n findings.push({\n severity: \"error\",\n code: \"service.no-asyncapi\",\n subject: service,\n message:\n `No async contract at ${paths.asyncapi}, and ${dangling.length} message link(s) already point into it — ` +\n `every landscape edge and requirement naming one of them resolves to nothing until the file is back`,\n details: dangling,\n });\n }\n } else if (events.unreadable) {\n // The openapi.invalid discipline: a contract that EXISTS but does not read\n // is a broken source of truth, not an empty one. Grading the links against\n // an empty message set would turn one file into one `spine.message-undefined`\n // per edge, every one of them pointing at the landscape.\n findings.push({\n severity: \"error\",\n code: \"asyncapi.invalid\",\n subject: service,\n message: `${service}: asyncapi.yaml does not read as AsyncAPI 3.0 (it does not parse, or it declares an older version) — the event spine is unchecked for this service`,\n ...(events.error === undefined ? {} : { details: [events.error] }),\n });\n } else {\n // Feature-only bookkeeping published to the fleet — the openapi axis's\n // `openapi.remove-marker-living` discipline, one code for every depth the\n // reader collects markers at (slots and inline channel messages alike).\n if (events.markers.length > 0) {\n findings.push({\n severity: \"error\",\n code: \"asyncapi.remove-marker-living\",\n subject: service,\n message: `${service}: living asyncapi.yaml carries x-loam-remove: true at ${events.markers.join(\", \")} — removal markers are valid only in feature deltas; retire a slot through a feature's asyncapi.yaml instead`,\n });\n }\n for (const name of events.duplicateNames) {\n findings.push({\n severity: \"warn\",\n code: \"asyncapi.duplicate-message\",\n subject: service,\n message: `${service}: asyncapi.yaml declares message '${name}' in ${slotsOf(events, name).join(\" and \")} — every join on the name (an edge's metadata { publishes }, a requirement's Publishes: line) picks one of those slots arbitrarily`,\n });\n }\n // The contract-depth probes. Form validated and depth did not: a payload\n // of bare `type: object` and a `$ref` to nothing both read as cleanly as\n // a full schema, while a reader still cannot rebuild the payload from the\n // green contract. Presence probes only — a non-JSON schemaFormat\n // is skipped, so Avro stays a document change.\n const empties = events.messages.filter((m) => m.payloadEmpty === true).map((m) => m.name);\n if (empties.length > 0) {\n findings.push({\n severity: \"warn\",\n code: \"asyncapi.payload-undescribed\",\n subject: service,\n message:\n `${service}: ${empties.length} message(s) declare no payload shape (${empties.join(\", \")}) — ` +\n `the name joins the spine, but nothing defines what a consumer would read; ` +\n `declare properties, or a schemaFormat for a non-JSON schema`,\n });\n }\n if (events.danglingRefs.length > 0) {\n findings.push({\n severity: \"warn\",\n code: \"asyncapi.ref-unresolved\",\n subject: service,\n message:\n `${service}: asyncapi.yaml contains ${events.danglingRefs.length} internal $ref(s) that resolve ` +\n `to nothing in the document — whatever they were meant to carry is silently absent from the ` +\n `spine's view of this contract`,\n details: events.danglingRefs,\n });\n }\n // Local resolution, both directions and both sources of a claim. An edge and\n // a requirement making the same broken claim get the same sentence under\n // different codes, matching how `spine.op-undefined` and\n // `spec-api.op-undefined` split the HTTP axis: one is the fleet map's\n // mistake, the other is this document's, and the fix is in a different file.\n const declares = (direction: \"publishes\" | \"consumes\", message: string): boolean =>\n direction === \"publishes\" ? sentHere.has(message) : receivedHere.has(message);\n const action = (direction: \"publishes\" | \"consumes\"): string =>\n direction === \"publishes\" ? \"send\" : \"receive\";\n for (const e of eventEdges) {\n if (declares(e.direction, e.message)) continue;\n findings.push({\n severity: \"error\",\n code: \"spine.message-undefined\",\n subject: service,\n message:\n `${service}: landscape edge ${e.direction === \"publishes\" ? `${service} → ${e.other}` : `${e.other} → ${service}`} ` +\n `${e.direction} '${e.message}', but ${service}'s asyncapi.yaml declares no operation with action: ${action(e.direction)} for it`,\n });\n }\n for (const e of eventLinks) {\n if (declares(e.direction, e.message)) continue;\n findings.push({\n severity: \"error\",\n code: \"spec-event.message-undefined\",\n subject: service,\n message: `${service}: requirement '${e.requirement}' ${e.direction} '${e.message}', but ${service}'s asyncapi.yaml declares no operation with action: ${action(e.direction)} for it`,\n });\n }\n // The migration-debt case, `api.ops-unlinked`'s twin: a contract and\n // requirements that never name each other leave this axis vacuously green.\n if (events.messages.length > 0 && reqs.length + archReqs.length > 0 && eventLinks.length === 0) {\n findings.push({\n severity: \"warn\",\n code: \"event.messages-unlinked\",\n subject: service,\n message: `${service}: asyncapi.yaml declares ${events.messages.length} message(s) but no requirement links any — the event axis is unchecked for this service`,\n });\n }\n // `api.ungoverned`'s event twin, and the per-message half the check above\n // cannot see: `event.messages-unlinked` fires only when ZERO links exist,\n // so a contract with five declared messages and three governed ones\n // reported nothing about the other two. The join is against requirement\n // lines ONLY, deliberately ignoring landscape edges — an edge is a claim\n // about traffic, a requirement is governance — exactly as the API axis\n // ignores inbound edges when grading coverage.\n const governed = new Set(eventLinks.map((e) => e.message));\n const orphans = [...new Set(events.messages.map((m) => m.name))].filter((name) => !governed.has(name));\n if (orphans.length > 0) {\n findings.push({\n severity: \"warn\",\n code: \"event.ungoverned\",\n subject: service,\n message: `${service}: ${orphans.length} message(s) not governed by any requirement's Publishes:/Consumes: line — ${orphans.join(\", \")}`,\n });\n }\n }\n\n // The consumed-message list is the handover to the fleet question in\n // ./message-producers.js — a fleet where nothing consumes walks nothing.\n const consumed = [\n ...new Set([\n ...eventEdges.filter((e) => e.direction === \"consumes\").map((e) => e.message),\n ...eventLinks.filter((e) => e.direction === \"consumes\").map((e) => e.message),\n ]),\n ].sort();\n findings.push(...(await producerFindings({ docsDir, service, land, landSvcOf, known, events, consumed, fleet })));\n\n // Positive confirmation, on the same rule `spine.resolved` follows: claimed\n // only where the axis actually checked something. A service that touches no\n // topic says nothing here — an \"event axis clean\" on every worker in the fleet\n // would be a green tick for work nobody did.\n const eventChecks = eventEdges.length + eventLinks.length;\n if (eventChecks > 0 && findings.length === eventFindingsAt) {\n findings.push({\n severity: \"ok\",\n code: \"event.covered\",\n message: `${service}: event spine (${eventChecks} message link(s) resolve to asyncapi.yaml)`,\n });\n }\n\n return findings;\n}\n"]}
|
|
@@ -11,7 +11,7 @@ import {} from "../../../../core/vocabulary/report.js";
|
|
|
11
11
|
import { producersByMessage } from "../../../../core/asyncapi/producers.js";
|
|
12
12
|
import {} from "../../../../core/asyncapi/model.js";
|
|
13
13
|
import { FleetContext } from "../../../../core/fleet-context.js";
|
|
14
|
-
import { externalProducerOf } from "
|
|
14
|
+
import { externalProducerOf } from "../../../../core/c4/resolve/external.js";
|
|
15
15
|
// The fleet question, and the one check on either spine that a single
|
|
16
16
|
// repository cannot answer: a consumer joins to a message whose schema lives
|
|
17
17
|
// in the producer's contract, in the producer's repo. Paid only when something
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"message-producers.js","sourceRoot":"","sources":["../../../../../src/commands/validate/service/events/message-producers.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AACH,OAAO,EAAiE,MAAM,wCAAwC,CAAC;AACvH,OAAO,EAAkB,MAAM,+BAA+B,CAAC;AAC/D,OAAO,EAAgB,MAAM,uCAAuC,CAAC;AACrE,OAAO,EAAE,kBAAkB,EAAE,MAAM,wCAAwC,CAAC;AAC5E,OAAO,EAAoB,MAAM,oCAAoC,CAAC;AACtE,OAAO,EAAE,YAAY,EAAE,MAAM,mCAAmC,CAAC;AACjE,OAAO,EAAE,kBAAkB,EAAE,MAAM,
|
|
1
|
+
{"version":3,"file":"message-producers.js","sourceRoot":"","sources":["../../../../../src/commands/validate/service/events/message-producers.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * The fleet question the event axis asks: who produces what this service\n * consumes. Split out of `./events.js` along the seam its own header always\n * named — the local axis grades one service's files, while this is the only\n * check on either spine that reads OTHER services' contracts, and the two\n * subjects share nothing but the consumed-message list the axis hands over.\n */\nimport { type DeclaredService, type PathableService, type RawServiceId } from \"../../../../core/kernel/ids/service.js\";\nimport { type LoadedDoc } from \"../../../../core/c4/likec4.js\";\nimport { type Finding } from \"../../../../core/vocabulary/report.js\";\nimport { producersByMessage } from \"../../../../core/asyncapi/producers.js\";\nimport { type AsyncapiDoc } from \"../../../../core/asyncapi/model.js\";\nimport { FleetContext } from \"../../../../core/fleet-context.js\";\nimport { externalProducerOf } from \"../../../../core/c4/resolve/external.js\";\nimport type { DocsDir } from \"../../../../core/kernel/ids/dirs.js\";\n\n/**\n * What the producer question is asked over. One record rather than a parameter\n * list: everything here is already bound together as one service's view of the\n * fleet, and the 4-parameter limit says so too.\n */\nexport interface ProducerQuestion {\n docsDir: DocsDir;\n service: PathableService;\n /** The living landscape, or null when the repo has none. */\n land: LoadedDoc | null;\n /** Its element→service resolver; null exactly when `land` is. */\n landSvcOf: ((id: string) => DeclaredService) | null;\n /** Service directories that exist — the fleet the producer question is asked over. */\n known: ReadonlySet<RawServiceId>;\n /** This service's own parsed contract — the local-shape half of the external answer. */\n events: AsyncapiDoc;\n /** The deduped message names this service consumes, from edges and requirement lines alike. */\n consumed: readonly string[];\n fleet: FleetContext;\n}\n\n// The fleet question, and the one check on either spine that a single\n// repository cannot answer: a consumer joins to a message whose schema lives\n// in the producer's contract, in the producer's repo. Paid only when something\n// actually consumes — a fleet where nobody has adopted the axis walks nothing.\nexport async function producerFindings(input: ProducerQuestion): Promise<Finding[]> {\n const { docsDir, service, land, landSvcOf, known, events, consumed, fleet } = input;\n const findings: Finding[] = [];\n if (consumed.length === 0) return findings;\n const producers = await producersByMessage(docsDir, [...known], fleet);\n for (const message of consumed) {\n const who = producers.byMessage.get(message) ?? [];\n // \"Nobody produces this\" is an argument from absence, and it is only\n // sound over a fleet loam could actually read. One unreadable contract\n // anywhere suspends it — that service may be the producer, and the\n // alternative is blaming every consumer for somebody else's broken YAML.\n // The external answer and the contested answer below rest on positive\n // evidence and need no such guard.\n if (who.length === 0) {\n // A message produced OUTSIDE the fleet used to be inexpressible: the\n // landscape said the producer was #external, and this error fired\n // anyway — so honesty cost a red build, and the exit ramp people took\n // was deleting the link. The landscape already carries the answer; an\n // external producer shifts the contract question to the one file that\n // can settle it, this service's own asyncapi.yaml.\n const ext = externalProducerOf(message, land, landSvcOf, known);\n if (ext !== null) {\n const local = events.messages.find((m) => m.name === message);\n if (local === undefined || local.payloadEmpty === true) {\n findings.push({\n severity: \"warn\",\n code: \"spine.message-external\",\n subject: service,\n message:\n `${service}: consumes '${message}' from '${ext}', a system outside the fleet — the ` +\n `producer declares no contract here, so the only payload definition is this service's ` +\n `own asyncapi.yaml, and it currently defines no shape for it; declare the message with ` +\n `its payload`,\n });\n }\n // A carried contract closes the question: the landscape states the\n // external dependency, the consumer owns the schema — nothing to say.\n } else if (producers.unreadable.length === 0) {\n findings.push({\n severity: \"error\",\n code: \"spine.message-unproduced\",\n subject: service,\n message: `${service}: consumes '${message}', but no service in the fleet and no #external element declares it is sent — the message has no producer anywhere, so nothing defines its payload`,\n });\n }\n } else if (who.length > 1) {\n findings.push({\n severity: \"warn\",\n code: \"asyncapi.message-contested\",\n subject: service,\n message: `${service}: consumes '${message}', which ${who.length} services declare they send (${who.join(\", \")}) — every consumer's join picks one of them arbitrarily, so the payload this service reads is whichever one happens to win`,\n });\n }\n }\n return findings;\n}\n"]}
|
|
@@ -3,11 +3,19 @@
|
|
|
3
3
|
* this service has attested, its pins graded against THIS working tree.
|
|
4
4
|
*
|
|
5
5
|
* Service-repo-scoped like `sources.*` and the gherkin chain — the citations
|
|
6
|
-
* resolve only inside the service's own repository. From the docs repo
|
|
7
|
-
*
|
|
8
|
-
*
|
|
6
|
+
* resolve only inside the service's own repository. From the docs repo this
|
|
7
|
+
* TARGET is silent by design: every pin would grade `unresolved` there, which
|
|
8
|
+
* would be a lie about the code rather than about the record, and
|
|
9
9
|
* `sources.unverifiable-from-here` already names the blind spot once per
|
|
10
|
-
* service.
|
|
10
|
+
* service. One code of the family is not about the code at all —
|
|
11
|
+
* `evidence.record-unreadable` is about the record, which lives in the docs
|
|
12
|
+
* repo — so the FEATURE target (`../feature.ts`) raises it too, from anywhere,
|
|
13
|
+
* through `featureRecordFindings` below:
|
|
14
|
+
* with only this target raising it, `validate --all` in the docs repo stayed
|
|
15
|
+
* green over a conflict-marked record that `verify` refused and `status`,
|
|
16
|
+
* `gate` and `tasks` all reported. In a service repo's `--all` the same file
|
|
17
|
+
* is therefore named twice, once per target: here as this service's pins
|
|
18
|
+
* going unchecked, there as the feature's record itself.
|
|
11
19
|
*
|
|
12
20
|
* ARCHIVED records are deliberately not linted (a v1 non-goal, recorded here):
|
|
13
21
|
* a frozen record is history, post-ship code drift is normal evolution, and
|
|
@@ -24,9 +32,9 @@
|
|
|
24
32
|
import { repoPath } from "../../../core/envelope/json.js";
|
|
25
33
|
import { inOrder } from "../../../core/kernel/concurrency.js";
|
|
26
34
|
import {} from "../../../core/kernel/ids/service.js";
|
|
35
|
+
import {} from "../../../core/repo/entries.js";
|
|
27
36
|
import { listFeatures } from "../../../core/repo/repo.js";
|
|
28
|
-
import { readVerificationState } from "../../../core/verify/file.js";
|
|
29
|
-
import { verificationPath } from "../../../core/verify/record.js";
|
|
37
|
+
import { readVerificationState } from "../../../core/verify/read/file.js";
|
|
30
38
|
import { lintEvidencePins } from "../../../core/verify/pins/lint.js";
|
|
31
39
|
import {} from "../../../core/vocabulary/report.js";
|
|
32
40
|
export async function evidencePinFindings(docsDir, service, repoDir, fleet) {
|
|
@@ -49,7 +57,7 @@ export async function evidencePinFindings(docsDir, service, repoDir, fleet) {
|
|
|
49
57
|
// this service, so the finding fires regardless of attestation.
|
|
50
58
|
const read = await readVerificationState(feature.dir);
|
|
51
59
|
if (read.state === "unreadable") {
|
|
52
|
-
return { feature: feature.id, path: repoPath(docsDir,
|
|
60
|
+
return { feature: feature.id, path: repoPath(docsDir, read.path), unreadable: read.reason, advice: read.advice };
|
|
53
61
|
}
|
|
54
62
|
if (read.state === "absent" || read.verification.schema !== 2)
|
|
55
63
|
return null;
|
|
@@ -69,9 +77,7 @@ export async function evidencePinFindings(docsDir, service, repoDir, fleet) {
|
|
|
69
77
|
severity: "warn",
|
|
70
78
|
code: "evidence.record-unreadable",
|
|
71
79
|
subject: service,
|
|
72
|
-
message: `${me}: ${entry.feature}
|
|
73
|
-
"none of its evidence was checked. It is plain YAML: repair it by hand, or re-record " +
|
|
74
|
-
`(\`loam verify ${entry.feature} --service ${me} --record answers.json\`); loam never overwrites a record it could not read`,
|
|
80
|
+
message: recordUnreadableMessage(`${me}: ${entry.feature}`, entry),
|
|
75
81
|
});
|
|
76
82
|
continue;
|
|
77
83
|
}
|
|
@@ -124,6 +130,46 @@ export async function evidencePinFindings(docsDir, service, repoDir, fleet) {
|
|
|
124
130
|
}
|
|
125
131
|
return findings;
|
|
126
132
|
}
|
|
133
|
+
/**
|
|
134
|
+
* `evidence.record-unreadable` on the FEATURE target (`../feature.ts`), from
|
|
135
|
+
* anywhere — the docs repo included, which is where the record lives. Kept
|
|
136
|
+
* here, beside the service target's emission, so one module owns the code and
|
|
137
|
+
* its words: the record read is the same `readVerificationState`, the path is
|
|
138
|
+
* made docs-relative the same way, and the message is the same sentence.
|
|
139
|
+
* Readability only — an `ok` record contributes nothing here, its verdict is
|
|
140
|
+
* `verify`'s and `status`'s to state — and a warn for the reasons the header
|
|
141
|
+
* gives: `valid` and `loam archive` are untouched, `--strict` is the lever.
|
|
142
|
+
*/
|
|
143
|
+
export async function featureRecordFindings(docsDir, feature) {
|
|
144
|
+
const read = await readVerificationState(feature.dir);
|
|
145
|
+
if (read.state !== "unreadable")
|
|
146
|
+
return [];
|
|
147
|
+
const path = repoPath(docsDir, read.path);
|
|
148
|
+
return [
|
|
149
|
+
{
|
|
150
|
+
severity: "warn",
|
|
151
|
+
code: "evidence.record-unreadable",
|
|
152
|
+
message: recordUnreadableMessage(feature.id, { path, unreadable: read.reason, advice: read.advice }),
|
|
153
|
+
locations: [{ path, role: "primary" }],
|
|
154
|
+
},
|
|
155
|
+
];
|
|
156
|
+
}
|
|
157
|
+
/**
|
|
158
|
+
* The sentence `evidence.record-unreadable` carries, for both targets that
|
|
159
|
+
* raise it. Each emission spells the code literally (the codes-drift
|
|
160
|
+
* collector resolves the `code` slot, as `drift` above says); only the words
|
|
161
|
+
* are shared, so the two cannot drift into two descriptions of one file.
|
|
162
|
+
*
|
|
163
|
+
* The reader's own recipe for the one file that failed, never a fixed one:
|
|
164
|
+
* "re-record" alone sent this finding's reader into `--record`'s refusal of
|
|
165
|
+
* the very same file, and the safe repair differs between a service's own
|
|
166
|
+
* attestation file and the one-file record other services' evidence may sit
|
|
167
|
+
* in. `path` is docs-relative.
|
|
168
|
+
*/
|
|
169
|
+
function recordUnreadableMessage(label, file) {
|
|
170
|
+
return (`${label}: ${file.path} exists but cannot be read as a verification record (${file.unreadable}) — ` +
|
|
171
|
+
`none of its evidence was checked. ${file.advice} loam never overwrites a record it could not read`);
|
|
172
|
+
}
|
|
127
173
|
/** One drift as a detail line: which claim, which citation, what changed. */
|
|
128
174
|
function detailLine(d) {
|
|
129
175
|
return `${d.claim} ${d.evidence} — ${d.what}`;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"evidence-pins.js","sourceRoot":"","sources":["../../../../src/commands/validate/service/evidence-pins.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * The evidence-pin re-check as validate findings: every ACTIVE feature record\n * this service has attested, its pins graded against THIS working tree.\n *\n * Service-repo-scoped like `sources.*` and the gherkin chain — the citations\n * resolve only inside the service's own repository. From the docs repo the\n * whole family is silent by design: every pin would grade `unresolved` there,\n * which would be a lie about the code rather than about the record, and\n * `sources.unverifiable-from-here` already names the blind spot once per\n * service.\n *\n * ARCHIVED records are deliberately not linted (a v1 non-goal, recorded here):\n * a frozen record is history, post-ship code drift is normal evolution, and\n * grading it would turn every service's CI permanently yellow the week after\n * every ship. If audit-time re-checks of shipped records are ever wanted, the\n * same `lintEvidencePins` call over `listFeatures` include-archived is the\n * whole change — behind an explicit flag, so the default stays quiet.\n *\n * Everything here demotes reviewer confidence only: warns never flip `valid`,\n * `--strict` may gate on them as it gates on any warning, and no grade ever\n * moves a verdict — `attested` never silently becomes `verified` and a drifted\n * pin never un-confirms a claim.\n */\nimport { repoPath } from \"../../../core/envelope/json.js\";\nimport { inOrder } from \"../../../core/kernel/concurrency.js\";\nimport { type PathableService } from \"../../../core/kernel/ids/service.js\";\nimport { listFeatures } from \"../../../core/repo/repo.js\";\nimport { readVerificationState } from \"../../../core/verify/file.js\";\nimport { verificationPath } from \"../../../core/verify/record.js\";\nimport { lintEvidencePins, type PinDrift, type PinLint } from \"../../../core/verify/pins/lint.js\";\nimport { type Finding } from \"../../../core/vocabulary/report.js\";\nimport type { FleetContext } from \"../../../core/fleet-context.js\";\nimport type { DocsDir } from \"../../../core/kernel/ids/dirs.js\";\n\nexport async function evidencePinFindings(\n docsDir: DocsDir,\n service: PathableService,\n repoDir: string | undefined,\n fleet?: FleetContext,\n): Promise<Finding[]> {\n if (repoDir === undefined) return [];\n // Membership questions cross the provenance line through plain strings by\n // design (kernel/ids/service.ts) — record subjects are document text.\n const me: string = service;\n // ACTIVE features only — see the header for why archived records stay frozen.\n const features = await listFeatures(docsDir, {}, fleet);\n type Linted =\n | null\n | { feature: string; path: string; unreadable: string }\n | { feature: string; lint: PinLint };\n const lints = await inOrder(features, async (feature): Promise<Linted> => {\n // Three states, graded apart on purpose. ABSENT is a feature nobody has\n // verified — legitimate silence. UNREADABLE is somebody's record this run\n // could not use, and it must be a finding here rather than a skip: the\n // damage that most plausibly breaks a record in a service repo's CI is a\n // hand edit to the very pins this family grades, and a lint that went\n // silent over it would turn \"someone broke the record\" into a green run —\n // `verify` names the same file `record-unreadable`, but this CI job never\n // runs verify. An unreadable record also cannot say whether it attests\n // this service, so the finding fires regardless of attestation.\n const read = await readVerificationState(feature.dir);\n if (read.state === \"unreadable\") {\n return { feature: feature.id, path: repoPath(docsDir, verificationPath(feature.dir)), unreadable: read.reason };\n }\n if (read.state === \"absent\" || read.verification.schema !== 2) return null;\n if (!(read.verification.attestations ?? []).some((a) => a.service === me)) return null;\n return { feature: feature.id, lint: await lintEvidencePins(read.verification.claims, me, repoDir) };\n });\n\n const findings: Finding[] = [];\n let checked = 0;\n let pinnedRecords = 0;\n let drifted = false;\n for (const entry of lints) {\n if (entry === null) continue;\n if (\"unreadable\" in entry) {\n findings.push({\n severity: \"warn\",\n code: \"evidence.record-unreadable\",\n subject: service,\n message:\n `${me}: ${entry.feature}: ${entry.path} exists but cannot be read as a verification record (${entry.unreadable}) — ` +\n \"none of its evidence was checked. It is plain YAML: repair it by hand, or re-record \" +\n `(\\`loam verify ${entry.feature} --service ${me} --record answers.json\\`); loam never overwrites a record it could not read`,\n });\n continue;\n }\n const { feature, lint } = entry;\n if (lint.checked > 0 || lint.drifts.length > 0) pinnedRecords += 1;\n checked += lint.checked;\n // The code is spelled as a literal at each call below, and the grade rides\n // beside it rather than being sliced back out of the code string — the\n // codes-drift collector resolves the `code` slot, and a derivation would\n // be a second definition of the pairing free to drift from the first.\n const drift = (grade: PinDrift[\"grade\"], code: string, sentence: string): void => {\n const drifts = lint.drifts.filter((d) => d.grade === grade);\n if (drifts.length === 0) return;\n drifted = true;\n findings.push({\n severity: \"warn\",\n code,\n subject: service,\n message:\n `${me}: ${feature}: ${drifts.length} recorded evidence pin(s) ${sentence} — ` +\n \"a reading priority for the reviewer, never a verdict change; re-answer and re-record \" +\n `(\\`loam verify ${feature} --service ${me} --record answers.json\\`) once the evidence is re-read`,\n details: drifts.map(detailLine),\n });\n };\n drift(\n \"unresolved\",\n \"evidence.unresolved\",\n \"cannot be re-checked in this working tree: the cited file is gone, unreadable, not a regular file, or the cited line is past its end\",\n );\n drift(\"moved\", \"evidence.moved\", \"cite a file that changed since the record, though each cited line survives\");\n drift(\"line-changed\", \"evidence.line-changed\", \"cite line(s) that no longer say what was recorded\");\n drift(\n \"token-missing\",\n \"evidence.token-missing\",\n \"cite file(s) that no longer contain the literal string the claim asserts\",\n );\n if (lint.unpinned > 0) {\n findings.push({\n severity: \"ok\",\n // The pre-content_digest-vouch precedent: a record from before the\n // capability existed is not wrong, it is unfindable-by-this-check.\n code: \"evidence.unpinned\",\n subject: service,\n message:\n `${me}: ${feature}: ${lint.unpinned} agent-confirmed citation(s) carry no evidence pin — ` +\n `recorded before pins existed; the next \\`loam verify ${feature} --service ${me} --record\\` makes them drift-checkable`,\n });\n }\n }\n if (checked > 0 && !drifted) {\n findings.push({\n severity: \"ok\",\n code: \"evidence.checked\",\n subject: service,\n message: `${me}: ${checked} evidence pin(s) across ${pinnedRecords} feature record(s) re-checked against this working tree`,\n });\n }\n return findings;\n}\n\n/** One drift as a detail line: which claim, which citation, what changed. */\nfunction detailLine(d: PinDrift): string {\n return `${d.claim} ${d.evidence} — ${d.what}`;\n}\n"]}
|
|
1
|
+
{"version":3,"file":"evidence-pins.js","sourceRoot":"","sources":["../../../../src/commands/validate/service/evidence-pins.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * The evidence-pin re-check as validate findings: every ACTIVE feature record\n * this service has attested, its pins graded against THIS working tree.\n *\n * Service-repo-scoped like `sources.*` and the gherkin chain — the citations\n * resolve only inside the service's own repository. From the docs repo this\n * TARGET is silent by design: every pin would grade `unresolved` there, which\n * would be a lie about the code rather than about the record, and\n * `sources.unverifiable-from-here` already names the blind spot once per\n * service. One code of the family is not about the code at all —\n * `evidence.record-unreadable` is about the record, which lives in the docs\n * repo — so the FEATURE target (`../feature.ts`) raises it too, from anywhere,\n * through `featureRecordFindings` below:\n * with only this target raising it, `validate --all` in the docs repo stayed\n * green over a conflict-marked record that `verify` refused and `status`,\n * `gate` and `tasks` all reported. In a service repo's `--all` the same file\n * is therefore named twice, once per target: here as this service's pins\n * going unchecked, there as the feature's record itself.\n *\n * ARCHIVED records are deliberately not linted (a v1 non-goal, recorded here):\n * a frozen record is history, post-ship code drift is normal evolution, and\n * grading it would turn every service's CI permanently yellow the week after\n * every ship. If audit-time re-checks of shipped records are ever wanted, the\n * same `lintEvidencePins` call over `listFeatures` include-archived is the\n * whole change — behind an explicit flag, so the default stays quiet.\n *\n * Everything here demotes reviewer confidence only: warns never flip `valid`,\n * `--strict` may gate on them as it gates on any warning, and no grade ever\n * moves a verdict — `attested` never silently becomes `verified` and a drifted\n * pin never un-confirms a claim.\n */\nimport { repoPath } from \"../../../core/envelope/json.js\";\nimport { inOrder } from \"../../../core/kernel/concurrency.js\";\nimport { type PathableService } from \"../../../core/kernel/ids/service.js\";\nimport { type FeatureEntry } from \"../../../core/repo/entries.js\";\nimport { listFeatures } from \"../../../core/repo/repo.js\";\nimport { readVerificationState } from \"../../../core/verify/read/file.js\";\nimport { lintEvidencePins, type PinDrift, type PinLint } from \"../../../core/verify/pins/lint.js\";\nimport { type Finding } from \"../../../core/vocabulary/report.js\";\nimport type { FleetContext } from \"../../../core/fleet-context.js\";\nimport type { DocsDir } from \"../../../core/kernel/ids/dirs.js\";\n\nexport async function evidencePinFindings(\n docsDir: DocsDir,\n service: PathableService,\n repoDir: string | undefined,\n fleet: FleetContext,\n): Promise<Finding[]> {\n if (repoDir === undefined) return [];\n // Membership questions cross the provenance line through plain strings by\n // design (kernel/ids/service.ts) — record subjects are document text.\n const me: string = service;\n // ACTIVE features only — see the header for why archived records stay frozen.\n const features = await listFeatures(docsDir, {}, fleet);\n type Linted =\n | null\n | { feature: string; path: string; unreadable: string; advice: string }\n | { feature: string; lint: PinLint };\n const lints = await inOrder(features, async (feature): Promise<Linted> => {\n // Three states, graded apart on purpose. ABSENT is a feature nobody has\n // verified — legitimate silence. UNREADABLE is somebody's record this run\n // could not use, and it must be a finding here rather than a skip: the\n // damage that most plausibly breaks a record in a service repo's CI is a\n // hand edit to the very pins this family grades, and a lint that went\n // silent over it would turn \"someone broke the record\" into a green run —\n // `verify` names the same file `record-unreadable`, but this CI job never\n // runs verify. An unreadable record also cannot say whether it attests\n // this service, so the finding fires regardless of attestation.\n const read = await readVerificationState(feature.dir);\n if (read.state === \"unreadable\") {\n return { feature: feature.id, path: repoPath(docsDir, read.path), unreadable: read.reason, advice: read.advice };\n }\n if (read.state === \"absent\" || read.verification.schema !== 2) return null;\n if (!(read.verification.attestations ?? []).some((a) => a.service === me)) return null;\n return { feature: feature.id, lint: await lintEvidencePins(read.verification.claims, me, repoDir) };\n });\n\n const findings: Finding[] = [];\n let checked = 0;\n let pinnedRecords = 0;\n let drifted = false;\n for (const entry of lints) {\n if (entry === null) continue;\n if (\"unreadable\" in entry) {\n findings.push({\n severity: \"warn\",\n code: \"evidence.record-unreadable\",\n subject: service,\n message: recordUnreadableMessage(`${me}: ${entry.feature}`, entry),\n });\n continue;\n }\n const { feature, lint } = entry;\n if (lint.checked > 0 || lint.drifts.length > 0) pinnedRecords += 1;\n checked += lint.checked;\n // The code is spelled as a literal at each call below, and the grade rides\n // beside it rather than being sliced back out of the code string — the\n // codes-drift collector resolves the `code` slot, and a derivation would\n // be a second definition of the pairing free to drift from the first.\n const drift = (grade: PinDrift[\"grade\"], code: string, sentence: string): void => {\n const drifts = lint.drifts.filter((d) => d.grade === grade);\n if (drifts.length === 0) return;\n drifted = true;\n findings.push({\n severity: \"warn\",\n code,\n subject: service,\n message:\n `${me}: ${feature}: ${drifts.length} recorded evidence pin(s) ${sentence} — ` +\n \"a reading priority for the reviewer, never a verdict change; re-answer and re-record \" +\n `(\\`loam verify ${feature} --service ${me} --record answers.json\\`) once the evidence is re-read`,\n details: drifts.map(detailLine),\n });\n };\n drift(\n \"unresolved\",\n \"evidence.unresolved\",\n \"cannot be re-checked in this working tree: the cited file is gone, unreadable, not a regular file, or the cited line is past its end\",\n );\n drift(\"moved\", \"evidence.moved\", \"cite a file that changed since the record, though each cited line survives\");\n drift(\"line-changed\", \"evidence.line-changed\", \"cite line(s) that no longer say what was recorded\");\n drift(\n \"token-missing\",\n \"evidence.token-missing\",\n \"cite file(s) that no longer contain the literal string the claim asserts\",\n );\n if (lint.unpinned > 0) {\n findings.push({\n severity: \"ok\",\n // The pre-content_digest-vouch precedent: a record from before the\n // capability existed is not wrong, it is unfindable-by-this-check.\n code: \"evidence.unpinned\",\n subject: service,\n message:\n `${me}: ${feature}: ${lint.unpinned} agent-confirmed citation(s) carry no evidence pin — ` +\n `recorded before pins existed; the next \\`loam verify ${feature} --service ${me} --record\\` makes them drift-checkable`,\n });\n }\n }\n if (checked > 0 && !drifted) {\n findings.push({\n severity: \"ok\",\n code: \"evidence.checked\",\n subject: service,\n message: `${me}: ${checked} evidence pin(s) across ${pinnedRecords} feature record(s) re-checked against this working tree`,\n });\n }\n return findings;\n}\n\n/**\n * `evidence.record-unreadable` on the FEATURE target (`../feature.ts`), from\n * anywhere — the docs repo included, which is where the record lives. Kept\n * here, beside the service target's emission, so one module owns the code and\n * its words: the record read is the same `readVerificationState`, the path is\n * made docs-relative the same way, and the message is the same sentence.\n * Readability only — an `ok` record contributes nothing here, its verdict is\n * `verify`'s and `status`'s to state — and a warn for the reasons the header\n * gives: `valid` and `loam archive` are untouched, `--strict` is the lever.\n */\nexport async function featureRecordFindings(docsDir: DocsDir, feature: FeatureEntry): Promise<Finding[]> {\n const read = await readVerificationState(feature.dir);\n if (read.state !== \"unreadable\") return [];\n const path = repoPath(docsDir, read.path);\n return [\n {\n severity: \"warn\",\n code: \"evidence.record-unreadable\",\n message: recordUnreadableMessage(feature.id, { path, unreadable: read.reason, advice: read.advice }),\n locations: [{ path, role: \"primary\" }],\n },\n ];\n}\n\n/**\n * The sentence `evidence.record-unreadable` carries, for both targets that\n * raise it. Each emission spells the code literally (the codes-drift\n * collector resolves the `code` slot, as `drift` above says); only the words\n * are shared, so the two cannot drift into two descriptions of one file.\n *\n * The reader's own recipe for the one file that failed, never a fixed one:\n * \"re-record\" alone sent this finding's reader into `--record`'s refusal of\n * the very same file, and the safe repair differs between a service's own\n * attestation file and the one-file record other services' evidence may sit\n * in. `path` is docs-relative.\n */\nfunction recordUnreadableMessage(\n label: string,\n file: { path: string; unreadable: string; advice: string },\n): string {\n return (\n `${label}: ${file.path} exists but cannot be read as a verification record (${file.unreadable}) — ` +\n `none of its evidence was checked. ${file.advice} loam never overwrites a record it could not read`\n );\n}\n\n/** One drift as a detail line: which claim, which citation, what changed. */\nfunction detailLine(d: PinDrift): string {\n return `${d.claim} ${d.evidence} — ${d.what}`;\n}\n"]}
|
|
@@ -18,13 +18,10 @@ import {} from "../../../core/kernel/ids/service.js";
|
|
|
18
18
|
import { landscapePath as landscapeFile, permissionsPath } from "../../../core/repo/paths.js";
|
|
19
19
|
import { locateServicePaths } from "../../../core/repo/service-target.js";
|
|
20
20
|
import { readVocabulary } from "../../../core/permissions/permissions.js";
|
|
21
|
-
import {
|
|
22
|
-
import {
|
|
23
|
-
import { realizesStaleFindings, realizesUnknownFindings } from "../../../core/capabilities/realizes/findings.js";
|
|
24
|
-
import { parseRequirements, readRequirementsDocument } from "../../../core/document/parse.js";
|
|
21
|
+
import { capabilityRequirementIndex, capabilityUnknownIssues } from "../../../core/capabilities/findings.js";
|
|
22
|
+
import { realizesStaleFindings, realizesUnknownIssues } from "../../../core/capabilities/realizes/findings.js";
|
|
25
23
|
import { requiresUnknownFindings } from "../checks/requirements.js";
|
|
26
24
|
import { listServices } from "../../../core/repo/repo.js";
|
|
27
|
-
import {} from "../../../core/c4/likec4.js";
|
|
28
25
|
import {} from "../../../core/vocabulary/report.js";
|
|
29
26
|
import { closeIds } from "../../../core/c4/arch.js";
|
|
30
27
|
import { serviceProvenance } from "../../../core/provenance/findings.js";
|
|
@@ -35,7 +32,8 @@ import { interruptedCommitFinding } from "../../../core/staging/recovery/finding
|
|
|
35
32
|
import { FleetContext } from "../../../core/fleet-context.js";
|
|
36
33
|
import { serviceLinkFindings } from "../links/corpus.js";
|
|
37
34
|
import { sourceScopeFindings } from "../checks/sources.js";
|
|
38
|
-
import { apiAxisFindings
|
|
35
|
+
import { apiAxisFindings } from "./api.js";
|
|
36
|
+
import { generatedContractFindings } from "./contracts/generated.js";
|
|
39
37
|
import { evidencePinFindings } from "./evidence-pins.js";
|
|
40
38
|
import { spineFindings } from "./spine.js";
|
|
41
39
|
import { eventAxisFindings } from "./events/events.js";
|
|
@@ -91,15 +89,6 @@ export async function validateService(check) {
|
|
|
91
89
|
// through the same resolver: an extending model's own slice is exactly the
|
|
92
90
|
// elements this set makes resolvable.
|
|
93
91
|
const known = new Set((await listServices(docsDir, fleet)).map((s) => s.id));
|
|
94
|
-
/**
|
|
95
|
-
* The read index this target reads the fleet map and the model through.
|
|
96
|
-
*
|
|
97
|
-
* A single-service run may arrive without one, and it gets its own rather
|
|
98
|
-
* than a second reading rule: `serviceModel` needs the enumeration and a
|
|
99
|
-
* document memo to answer at all, and both live here. Request-scoped like
|
|
100
|
-
* every other `FleetContext` — created for this call, dropped with it.
|
|
101
|
-
*/
|
|
102
|
-
const context = fleet ?? new FleetContext();
|
|
103
92
|
// C4 model. Its absence is an error — this is where `adopt` comes in — but it
|
|
104
93
|
// must NOT silence the rest of the gate stack: `loam archive` of a feature
|
|
105
94
|
// introducing a new service creates exactly this state (spec.md, arch.spec.md,
|
|
@@ -133,7 +122,7 @@ export async function validateService(check) {
|
|
|
133
122
|
});
|
|
134
123
|
}
|
|
135
124
|
else {
|
|
136
|
-
model = await
|
|
125
|
+
model = await fleet.serviceModel(docsDir, paths);
|
|
137
126
|
elements = model.doc.elements;
|
|
138
127
|
relationships = model.doc.relationships;
|
|
139
128
|
if (!model.mapUnreadable && model.doc.errors.length === 0) {
|
|
@@ -174,7 +163,7 @@ export async function validateService(check) {
|
|
|
174
163
|
// Still gated on the landscape FILE existing: `land === null` means "this repo
|
|
175
164
|
// draws no fleet", and a repo with an `architecture/usecases/` and no map is
|
|
176
165
|
// that state, not a map that loaded empty.
|
|
177
|
-
const land = preloaded ?? (existsSync(landscapeFile(docsDir)) ? await
|
|
166
|
+
const land = preloaded ?? (existsSync(landscapeFile(docsDir)) ? await fleet.architecture(docsDir) : null);
|
|
178
167
|
// Where a standalone model's copies of the map's elements have drifted. After
|
|
179
168
|
// `c4.valid` because it is a claim about a model that parsed, and silent for
|
|
180
169
|
// an extending model, which has no copy to diverge.
|
|
@@ -233,13 +222,14 @@ export async function validateService(check) {
|
|
|
233
222
|
elements,
|
|
234
223
|
relationships,
|
|
235
224
|
// The one fact that tells "this service models nothing" apart from "this
|
|
236
|
-
// service's model could not be read"
|
|
225
|
+
// service's model could not be read" — its map does not parse, or it does
|
|
226
|
+
// not itself (`c4.invalid` above): both arrive here as empty lists, and
|
|
237
227
|
// only the second one must silence `covers.unknown`.
|
|
238
|
-
|
|
228
|
+
modelUnreadable: model !== null && (model.mapUnreadable || model.doc.errors.length > 0),
|
|
239
229
|
land,
|
|
240
230
|
known,
|
|
241
231
|
docsDir,
|
|
242
|
-
fleet
|
|
232
|
+
fleet,
|
|
243
233
|
})));
|
|
244
234
|
// The service's own use cases, GRADED — scanned beside the model above. Here
|
|
245
235
|
// rather than on the fleet target because the hops name this service's
|
|
@@ -261,20 +251,20 @@ export async function validateService(check) {
|
|
|
261
251
|
// rides the same loop against ITS fleet vocabulary; under --all the fleet
|
|
262
252
|
// memo makes that one parse for the whole run.
|
|
263
253
|
const vocabulary = await readVocabulary(permissionsPath(docsDir));
|
|
264
|
-
const capabilities =
|
|
254
|
+
const capabilities = await fleet.capabilities(docsDir);
|
|
265
255
|
// The requirement-level index behind `Realizes:`. Built once for both axes,
|
|
266
|
-
// and behind the same ladder `
|
|
256
|
+
// and behind the same ladder `capabilityUnknownIssues` applies — an absent
|
|
267
257
|
// or unreadable vocabulary means no reads and no grades at all, so a fleet
|
|
268
258
|
// that has not adopted the axis pays nothing for a line it never wrote.
|
|
269
|
-
const capabilityReqs = await capabilityRequirementIndex(capabilities,
|
|
259
|
+
const capabilityReqs = await capabilityRequirementIndex(capabilities, (p) => fleet.readRequirements(p));
|
|
270
260
|
for (const [label, docReqs] of [
|
|
271
261
|
["spec.md", reqs],
|
|
272
262
|
["arch.spec.md", archReqs],
|
|
273
263
|
]) {
|
|
274
264
|
const target = { where: `${service}: ${label}`, subject: service };
|
|
275
265
|
findings.push(...requiresUnknownFindings(docReqs, target, vocabulary));
|
|
276
|
-
findings.push(...
|
|
277
|
-
findings.push(...
|
|
266
|
+
findings.push(...capabilityUnknownIssues(docReqs, target, capabilities));
|
|
267
|
+
findings.push(...realizesUnknownIssues(docReqs, target, capabilityReqs));
|
|
278
268
|
findings.push(...realizesStaleFindings(docReqs, target, capabilityReqs));
|
|
279
269
|
// The grammar guard, last in the loop: it grades the KEYS the three checks
|
|
280
270
|
// above resolve values from, so a line none of them saw is reported here
|
|
@@ -287,7 +277,7 @@ export async function validateService(check) {
|
|
|
287
277
|
// document checks and adds no input any of them depend on.
|
|
288
278
|
findings.push(...(await serviceLinkFindings({ docsDir, subject: service, fleet }, paths)));
|
|
289
279
|
// Provenance last: who vouched for this, and what code it was written from.
|
|
290
|
-
findings.push(...(await serviceProvenance(docsDir, service, { repoDir, fleet })));
|
|
280
|
+
findings.push(...(await serviceProvenance(docsDir, service, { repoDir, loamOwned: check.loamOwned, fleet })));
|
|
291
281
|
findings.push(...(await sourceScopeFindings(docsDir, service, repoDir, fleet)));
|
|
292
282
|
// The build's own contract against the committed copy. Service-repo only:
|
|
293
283
|
// from the docs repo there is no build output to read, and the family stays
|