@ybotok/loam 0.2.0-alpha.1 → 0.2.0-alpha.13
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 +3448 -0
- package/COMPARISON.md +91 -3
- package/CONTRIBUTING.md +37 -0
- package/MIGRATING-from-OpenSpec.md +47 -23
- package/QUICKSTART.md +189 -12
- package/README.md +274 -65
- package/ROADMAP.md +572 -31
- package/SCHEMA.md +1709 -155
- package/WORKFLOW.md +298 -72
- 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/adopt/render.js +50 -9
- package/dist/commands/adopt/render.js.map +1 -1
- package/dist/commands/archive/archive.js +16 -19
- package/dist/commands/archive/archive.js.map +1 -1
- package/dist/commands/archive/closing.js +42 -0
- package/dist/commands/archive/closing.js.map +1 -0
- package/dist/commands/archive/plan/contracts/asyncapi.js +36 -42
- 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 +77 -43
- package/dist/commands/archive/plan/contracts/openapi.js.map +1 -1
- package/dist/commands/archive/plan/contracts/verdict-lines.js +103 -0
- package/dist/commands/archive/plan/contracts/verdict-lines.js.map +1 -0
- package/dist/commands/archive/plan/document/head.js +151 -0
- package/dist/commands/archive/plan/document/head.js.map +1 -0
- package/dist/commands/archive/plan/document/links.js +284 -0
- package/dist/commands/archive/plan/document/links.js.map +1 -0
- package/dist/commands/archive/plan/document/run.js +136 -0
- package/dist/commands/archive/plan/document/run.js.map +1 -0
- package/dist/commands/archive/plan/gate.js +21 -19
- package/dist/commands/archive/plan/gate.js.map +1 -1
- package/dist/commands/archive/plan/landscape.js +167 -30
- package/dist/commands/archive/plan/landscape.js.map +1 -1
- package/dist/commands/archive/plan/model/extending.js +85 -0
- package/dist/commands/archive/plan/model/extending.js.map +1 -0
- package/dist/commands/archive/plan/refusal.js +14 -19
- package/dist/commands/archive/plan/refusal.js.map +1 -1
- package/dist/commands/archive/plan/specs.js +137 -56
- package/dist/commands/archive/plan/specs.js.map +1 -1
- package/dist/commands/archive/plan/state.js +3 -0
- package/dist/commands/archive/plan/state.js.map +1 -1
- package/dist/commands/archive/run.js +134 -30
- 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 +85 -18
- package/dist/commands/context/context.js.map +1 -1
- package/dist/commands/context/print.js +29 -7
- package/dist/commands/context/print.js.map +1 -1
- package/dist/commands/delta/delta.js +102 -14
- package/dist/commands/delta/delta.js.map +1 -1
- package/dist/commands/delta/print.js +17 -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/doctor.js +60 -1
- package/dist/commands/doctor.js.map +1 -1
- package/dist/commands/explain/explain.js +89 -13
- package/dist/commands/explain/explain.js.map +1 -1
- package/dist/commands/explore.js +16 -10
- 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 +5 -7
- package/dist/commands/list/json.js.map +1 -1
- package/dist/commands/list/list.js +44 -29
- package/dist/commands/list/list.js.map +1 -1
- package/dist/commands/list/print.js +52 -9
- 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 +37 -34
- 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 +158 -13
- package/dist/commands/seed/plan.js.map +1 -1
- package/dist/commands/seed/seed.js +13 -11
- package/dist/commands/seed/seed.js.map +1 -1
- package/dist/commands/show/feature.js +9 -5
- package/dist/commands/show/feature.js.map +1 -1
- package/dist/commands/show/introduced.js +157 -0
- package/dist/commands/show/introduced.js.map +1 -0
- package/dist/commands/show/marks.js +35 -6
- package/dist/commands/show/marks.js.map +1 -1
- package/dist/commands/show/service.js +41 -33
- package/dist/commands/show/service.js.map +1 -1
- package/dist/commands/show/show.js +17 -7
- package/dist/commands/show/show.js.map +1 -1
- package/dist/commands/status/fleet-next.js +90 -0
- package/dist/commands/status/fleet-next.js.map +1 -0
- package/dist/commands/status/json.js +2 -0
- package/dist/commands/status/json.js.map +1 -1
- package/dist/commands/status/print.js +89 -37
- 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 +103 -23
- 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 +106 -46
- 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 +289 -74
- package/dist/commands/subsystem/sync.js.map +1 -1
- package/dist/commands/subsystem/txn/exclude.js +132 -0
- package/dist/commands/subsystem/txn/exclude.js.map +1 -0
- package/dist/commands/subsystem/txn/links.js +128 -0
- package/dist/commands/subsystem/txn/links.js.map +1 -0
- package/dist/commands/subsystem/txn/report.js +174 -0
- package/dist/commands/subsystem/txn/report.js.map +1 -0
- package/dist/commands/subsystem/txn/txn.js +147 -45
- package/dist/commands/subsystem/txn/txn.js.map +1 -1
- package/dist/commands/subsystem/txn/views.js +200 -21
- package/dist/commands/subsystem/txn/views.js.map +1 -1
- package/dist/commands/tasks/actions.js +107 -0
- package/dist/commands/tasks/actions.js.map +1 -0
- package/dist/commands/tasks/render.js +253 -0
- package/dist/commands/tasks/render.js.map +1 -0
- package/dist/commands/tasks/tasks.js +119 -0
- package/dist/commands/tasks/tasks.js.map +1 -0
- package/dist/commands/unarchive/manifest.js +42 -26
- package/dist/commands/unarchive/manifest.js.map +1 -1
- package/dist/commands/unarchive/restore.js +83 -33
- 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 +78 -27
- package/dist/commands/validate/arch-coverage.js.map +1 -1
- package/dist/commands/validate/checks/fleet-shape.js +34 -126
- 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/introduced.js +102 -0
- package/dist/commands/validate/checks/introduced.js.map +1 -0
- 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 +11 -2
- package/dist/commands/validate/checks/sources.js.map +1 -1
- package/dist/commands/validate/coverage/covers-after.js +105 -0
- package/dist/commands/validate/coverage/covers-after.js.map +1 -0
- package/dist/commands/validate/feature.js +26 -20
- package/dist/commands/validate/feature.js.map +1 -1
- package/dist/commands/validate/fleet/census.js +37 -13
- package/dist/commands/validate/fleet/census.js.map +1 -1
- package/dist/commands/validate/fleet/landscape.js +144 -126
- package/dist/commands/validate/fleet/landscape.js.map +1 -1
- package/dist/commands/validate/fleet/load.js +66 -0
- package/dist/commands/validate/fleet/load.js.map +1 -1
- package/dist/commands/validate/fleet/map/attest.js +98 -0
- package/dist/commands/validate/fleet/map/attest.js.map +1 -0
- package/dist/commands/validate/fleet/map/bindings.js +107 -0
- package/dist/commands/validate/fleet/map/bindings.js.map +1 -0
- package/dist/commands/validate/fleet/map/consumers.js +244 -0
- package/dist/commands/validate/fleet/map/consumers.js.map +1 -0
- package/dist/commands/validate/fleet/map/isolation.js +116 -0
- package/dist/commands/validate/fleet/map/isolation.js.map +1 -0
- package/dist/commands/validate/fleet/map/usecases.js +54 -0
- package/dist/commands/validate/fleet/map/usecases.js.map +1 -0
- package/dist/commands/validate/fleet/obligations.js +44 -14
- package/dist/commands/validate/fleet/obligations.js.map +1 -1
- package/dist/commands/validate/fleet/scorecard/print.js +10 -2
- 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/place.js.map +1 -1
- package/dist/commands/validate/fleet/views/fleet-project.js +217 -0
- package/dist/commands/validate/fleet/views/fleet-project.js.map +1 -0
- package/dist/commands/validate/fleet/views/ids.js +50 -15
- package/dist/commands/validate/fleet/views/ids.js.map +1 -1
- package/dist/commands/validate/fleet/views/projects.js +278 -0
- package/dist/commands/validate/fleet/views/projects.js.map +1 -0
- package/dist/commands/validate/fleet/views/root-documents.js +83 -0
- package/dist/commands/validate/fleet/views/root-documents.js.map +1 -0
- package/dist/commands/validate/fleet/views/stale.js +40 -47
- 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/render/repeats.js +63 -0
- package/dist/commands/validate/render/repeats.js.map +1 -0
- package/dist/commands/validate/render/rollups.js +115 -0
- package/dist/commands/validate/render/rollups.js.map +1 -0
- package/dist/commands/validate/render/text.js +199 -0
- package/dist/commands/validate/render/text.js.map +1 -0
- package/dist/commands/validate/report.js +36 -160
- 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 +21 -107
- 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 +36 -14
- 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/model/diverged.js +226 -0
- package/dist/commands/validate/service/model/diverged.js.map +1 -0
- package/dist/commands/validate/service/model/grade.js +163 -0
- package/dist/commands/validate/service/model/grade.js.map +1 -0
- package/dist/commands/validate/service/service.js +121 -61
- package/dist/commands/validate/service/service.js.map +1 -1
- package/dist/commands/validate/service/specs.js +88 -28
- package/dist/commands/validate/service/specs.js.map +1 -1
- package/dist/commands/validate/service/spine.js +38 -3
- package/dist/commands/validate/service/spine.js.map +1 -1
- package/dist/commands/validate/service/usecases/tags.js +113 -0
- package/dist/commands/validate/service/usecases/tags.js.map +1 -0
- package/dist/commands/validate/service/usecases/usecases.js +209 -0
- package/dist/commands/validate/service/usecases/usecases.js.map +1 -0
- package/dist/commands/validate/validate.js +85 -60
- 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 +55 -52
- 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 +81 -106
- package/dist/core/agent/agents-md/command-map.js.map +1 -1
- package/dist/core/agent/agents-md/map/explain.js +21 -9
- package/dist/core/agent/agents-md/map/explain.js.map +1 -1
- package/dist/core/agent/agents-md/map/lenses/context.js +17 -8
- 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 +24 -8
- package/dist/core/agent/agents-md/map/status.js.map +1 -1
- package/dist/core/agent/agents-md/map/subsystem.js +80 -8
- package/dist/core/agent/agents-md/map/subsystem.js.map +1 -1
- package/dist/core/agent/agents-md/map/usecases.js +88 -10
- package/dist/core/agent/agents-md/map/usecases.js.map +1 -1
- package/dist/core/agent/agents-md/refusals.js +49 -22
- package/dist/core/agent/agents-md/refusals.js.map +1 -1
- package/dist/core/agent/agents-md.js +39 -6
- 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/protocol.js +1 -1
- package/dist/core/agent/protocol.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/support/report.js +46 -5
- package/dist/core/agent/support/report.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 +158 -11
- package/dist/core/agent/workflows/adopt.js.map +1 -1
- package/dist/core/agent/workflows/check.js +78 -39
- package/dist/core/agent/workflows/check.js.map +1 -1
- package/dist/core/agent/workflows/closing.js +88 -27
- package/dist/core/agent/workflows/closing.js.map +1 -1
- package/dist/core/agent/workflows/feature.js +128 -13
- package/dist/core/agent/workflows/feature.js.map +1 -1
- package/dist/core/agent/workflows/implement.js +96 -13
- package/dist/core/agent/workflows/implement.js.map +1 -1
- package/dist/core/agent/workflows/reference/authoring.js +106 -4
- 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/drawing/drawing.js +253 -0
- package/dist/core/agent/workflows/reference/drawing/drawing.js.map +1 -0
- package/dist/core/agent/workflows/reference/reference.js +6 -3
- package/dist/core/agent/workflows/reference/reference.js.map +1 -1
- package/dist/core/agent/workflows/reference/spine.js +68 -27
- 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 +32 -50
- package/dist/core/asyncapi/merge/markers.js.map +1 -1
- package/dist/core/asyncapi/merge/merge.js +33 -53
- package/dist/core/asyncapi/merge/merge.js.map +1 -1
- package/dist/core/asyncapi/merge/pin.js +12 -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 +96 -36
- package/dist/core/brief/brief.js.map +1 -1
- package/dist/core/brief/checks.js +138 -3
- package/dist/core/brief/checks.js.map +1 -1
- package/dist/core/brief/landscape.js +209 -130
- package/dist/core/brief/landscape.js.map +1 -1
- package/dist/core/brief/map/extend.js +84 -0
- package/dist/core/brief/map/extend.js.map +1 -0
- package/dist/core/brief/map/owed.js +266 -0
- package/dist/core/brief/map/owed.js.map +1 -0
- package/dist/core/brief/targets.js +109 -31
- package/dist/core/brief/targets.js.map +1 -1
- package/dist/core/brief/unchecked.js +14 -3
- package/dist/core/brief/unchecked.js.map +1 -1
- package/dist/core/c4/arch.js +90 -182
- package/dist/core/c4/arch.js.map +1 -1
- package/dist/core/c4/direction/direction.js +114 -0
- package/dist/core/c4/direction/direction.js.map +1 -0
- package/dist/core/c4/likec4.js +169 -14
- 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/deployment.js +160 -0
- package/dist/core/c4/parsed/deployment.js.map +1 -0
- package/dist/core/c4/parsed/dynamic-views.js.map +1 -1
- package/dist/core/c4/parsed/values.js +54 -1
- package/dist/core/c4/parsed/values.js.map +1 -1
- package/dist/core/c4/parsed/view-ids.js +39 -1
- package/dist/core/c4/parsed/view-ids.js.map +1 -1
- package/dist/core/c4/prefetch/fleet.js +134 -0
- package/dist/core/c4/prefetch/fleet.js.map +1 -0
- package/dist/core/c4/project/architecture.js +123 -7
- package/dist/core/c4/project/architecture.js.map +1 -1
- package/dist/core/c4/project/batch.js +140 -0
- package/dist/core/c4/project/batch.js.map +1 -0
- package/dist/core/c4/project/documents.js +43 -12
- package/dist/core/c4/project/documents.js.map +1 -1
- package/dist/core/c4/project/load.js +79 -43
- package/dist/core/c4/project/load.js.map +1 -1
- package/dist/core/c4/project/staged.js +277 -0
- package/dist/core/c4/project/staged.js.map +1 -0
- package/dist/core/c4/resolve/attested.js +0 -0
- package/dist/core/c4/resolve/attested.js.map +1 -0
- 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 +78 -17
- package/dist/core/c4/resolve/service.js.map +1 -1
- package/dist/core/c4/resolve/steps.js +290 -0
- package/dist/core/c4/resolve/steps.js.map +1 -0
- package/dist/core/c4/resolve/systems.js +154 -0
- package/dist/core/c4/resolve/systems.js.map +1 -0
- package/dist/core/c4/root-project/exclude.js +352 -0
- package/dist/core/c4/root-project/exclude.js.map +1 -0
- package/dist/core/c4/seed/template.js +65 -1
- package/dist/core/c4/seed/template.js.map +1 -1
- package/dist/core/c4/service-model/created/models.js +96 -0
- package/dist/core/c4/service-model/created/models.js.map +1 -0
- package/dist/core/c4/service-model/documents.js +47 -0
- package/dist/core/c4/service-model/documents.js.map +1 -0
- package/dist/core/c4/service-model/fleet/extending.js +82 -0
- package/dist/core/c4/service-model/fleet/extending.js.map +1 -0
- package/dist/core/c4/service-model/load.js +165 -0
- package/dist/core/c4/service-model/load.js.map +1 -0
- package/dist/core/c4/service-model/renderer.js +128 -0
- package/dist/core/c4/service-model/renderer.js.map +1 -0
- package/dist/core/c4/service-model/shape.js +106 -0
- package/dist/core/c4/service-model/shape.js.map +1 -0
- package/dist/core/c4/service-model/slice.js +188 -0
- package/dist/core/c4/service-model/slice.js.map +1 -0
- package/dist/core/c4/source-mask.js +297 -10
- package/dist/core/c4/source-mask.js.map +1 -1
- package/dist/core/c4/source-scan.js +222 -141
- package/dist/core/c4/source-scan.js.map +1 -1
- package/dist/core/c4/splice/authored-source.js +79 -13
- package/dist/core/c4/splice/authored-source.js.map +1 -1
- package/dist/core/c4/splice/contract.js +20 -0
- package/dist/core/c4/splice/contract.js.map +1 -1
- package/dist/core/c4/splice/delta-blocks.js +20 -20
- package/dist/core/c4/splice/delta-blocks.js.map +1 -1
- package/dist/core/c4/splice/identity/declared.js +87 -0
- package/dist/core/c4/splice/identity/declared.js.map +1 -0
- package/dist/core/c4/splice/identity/edges.js +37 -0
- package/dist/core/c4/splice/identity/edges.js.map +1 -1
- package/dist/core/c4/splice/identity/existing.js +80 -0
- package/dist/core/c4/splice/identity/existing.js.map +1 -0
- package/dist/core/c4/splice/identity/interior.js +80 -0
- package/dist/core/c4/splice/identity/interior.js.map +1 -0
- package/dist/core/c4/splice/identity/owner.js +50 -0
- package/dist/core/c4/splice/identity/owner.js.map +1 -0
- package/dist/core/c4/splice/landscape-merge.js +182 -114
- package/dist/core/c4/splice/landscape-merge.js.map +1 -1
- package/dist/core/c4/splice/model/merge.js +151 -0
- package/dist/core/c4/splice/model/merge.js.map +1 -0
- package/dist/core/c4/splice/placement.js +160 -56
- package/dist/core/c4/splice/placement.js.map +1 -1
- package/dist/core/c4/splice/text/spot.js +131 -0
- package/dist/core/c4/splice/text/spot.js.map +1 -0
- package/dist/core/c4/workspace.js +13 -19
- 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/delta/uncovered.js +2 -2
- package/dist/core/capabilities/delta/uncovered.js.map +1 -1
- package/dist/core/capabilities/findings.js +18 -22
- 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/calls/called.js +52 -0
- package/dist/core/coherence/calls/called.js.map +1 -0
- package/dist/core/coherence/coherence.js +103 -22
- 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/covers/health.js +85 -0
- package/dist/core/coherence/covers/health.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 +147 -16
- 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 +28 -87
- package/dist/core/coherence/events/lookups.js.map +1 -1
- package/dist/core/coherence/events/produced.js +100 -0
- package/dist/core/coherence/events/produced.js.map +1 -0
- 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 +51 -6
- package/dist/core/delta/delta.js.map +1 -1
- package/dist/core/dependencies/fanin.js +2 -2
- package/dist/core/dependencies/fanin.js.map +1 -1
- package/dist/core/deployment/delta.js +191 -0
- package/dist/core/deployment/delta.js.map +1 -0
- package/dist/core/deployment/objects.js +145 -0
- package/dist/core/deployment/objects.js.map +1 -0
- 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 +49 -21
- 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/docs.js +31 -22
- package/dist/core/docs.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/config.js +7 -3
- package/dist/core/doctor/config.js.map +1 -1
- package/dist/core/doctor/doctor.js +91 -11
- package/dist/core/doctor/doctor.js.map +1 -1
- package/dist/core/doctor/report.js.map +1 -1
- package/dist/core/doctor/reports/scan.js +242 -0
- package/dist/core/doctor/reports/scan.js.map +1 -0
- package/dist/core/document/apply.js +158 -11
- 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 +26 -21
- package/dist/core/explain/families.js.map +1 -1
- package/dist/core/explain/lookup.js +14 -1
- package/dist/core/explain/lookup.js.map +1 -1
- package/dist/core/explain/refusals.js +5 -5
- package/dist/core/explain/refusals.js.map +1 -1
- package/dist/core/explain/terms/documents.js +227 -0
- package/dist/core/explain/terms/documents.js.map +1 -0
- package/dist/core/explain/terms/done-check.js +144 -0
- package/dist/core/explain/terms/done-check.js.map +1 -0
- package/dist/core/explain/terms/entry.js +14 -0
- package/dist/core/explain/terms/entry.js.map +1 -0
- package/dist/core/explain/terms/stages.js +71 -0
- package/dist/core/explain/terms/stages.js.map +1 -0
- package/dist/core/explain/terms/tasks.js +97 -0
- package/dist/core/explain/terms/tasks.js.map +1 -0
- package/dist/core/explain/terms.js +68 -109
- package/dist/core/explain/terms.js.map +1 -1
- package/dist/core/explore/explore.js +40 -10
- package/dist/core/explore/explore.js.map +1 -1
- package/dist/core/fleet-context.js +81 -95
- package/dist/core/fleet-context.js.map +1 -1
- package/dist/core/gate/checks.js +11 -4
- package/dist/core/gate/checks.js.map +1 -1
- package/dist/core/gate/partners.js +69 -28
- 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/delta.js +10 -7
- package/dist/core/glossary/delta.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/ids/fqn/ancestors.js +65 -0
- package/dist/core/kernel/ids/fqn/ancestors.js.map +1 -0
- package/dist/core/kernel/path-safety.js +38 -0
- package/dist/core/kernel/path-safety.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 +51 -37
- package/dist/core/links/corpus.js.map +1 -1
- package/dist/core/links/findings.js.map +1 -1
- package/dist/core/links/parse.js +38 -9
- package/dist/core/links/parse.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 +40 -10
- 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 +301 -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 +28 -52
- package/dist/core/openapi/merge/merge.js.map +1 -1
- package/dist/core/openapi/merge/pin.js +17 -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 +43 -4
- package/dist/core/pack/living.js.map +1 -1
- package/dist/core/pack/pack.js +50 -6
- package/dist/core/pack/pack.js.map +1 -1
- package/dist/core/projection/arch-slice.js +2 -2
- package/dist/core/projection/arch-slice.js.map +1 -1
- package/dist/core/projection/introduced.js +14 -0
- package/dist/core/projection/introduced.js.map +1 -0
- 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/authored/paths.js +18 -0
- package/dist/core/repo/authored/paths.js.map +1 -1
- package/dist/core/repo/entries.js.map +1 -1
- package/dist/core/repo/paths.js +92 -6
- package/dist/core/repo/paths.js.map +1 -1
- package/dist/core/repo/repo.js +74 -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 +258 -0
- package/dist/core/repo/tree/render/body.js.map +1 -0
- package/dist/core/repo/tree/render/projects.js +182 -0
- package/dist/core/repo/tree/render/projects.js.map +1 -0
- 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 +42 -0
- package/dist/core/repo/tree/render/stale.js.map +1 -0
- package/dist/core/repo/tree/render/text.js +84 -0
- package/dist/core/repo/tree/render/text.js.map +1 -0
- package/dist/core/repo/tree/render/views.js +293 -0
- package/dist/core/repo/tree/render/views.js.map +1 -0
- package/dist/core/repo/tree/walk.js +53 -32
- 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/scaffold/landscape.js +121 -13
- package/dist/core/scaffold/landscape.js.map +1 -1
- package/dist/core/scaffold/legacy.js +271 -0
- package/dist/core/scaffold/legacy.js.map +1 -0
- package/dist/core/staging/commit.js +26 -15
- 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 +84 -6
- package/dist/core/staging/recovery/recover.js.map +1 -1
- package/dist/core/staging/snapshot/manifest.js +188 -0
- package/dist/core/staging/snapshot/manifest.js.map +1 -0
- package/dist/core/staging/{snapshot.js → snapshot/snapshot.js} +39 -45
- package/dist/core/staging/snapshot/snapshot.js.map +1 -0
- package/dist/core/staging/txn/forward.js +24 -14
- package/dist/core/staging/txn/forward.js.map +1 -1
- package/dist/core/staging/writes.js.map +1 -1
- package/dist/core/status/actions/execution.js +11 -0
- package/dist/core/status/actions/execution.js.map +1 -1
- package/dist/core/status/contracts.js +39 -15
- package/dist/core/status/contracts.js.map +1 -1
- package/dist/core/status/feature/artifacts.js +33 -8
- package/dist/core/status/feature/artifacts.js.map +1 -1
- package/dist/core/status/feature/feature.js +24 -17
- package/dist/core/status/feature/feature.js.map +1 -1
- package/dist/core/status/feature/next.js +20 -7
- package/dist/core/status/feature/next.js.map +1 -1
- package/dist/core/status/feature/words.js +96 -0
- package/dist/core/status/feature/words.js.map +1 -0
- package/dist/core/status/fleet/fleet.js +43 -14
- package/dist/core/status/fleet/fleet.js.map +1 -1
- package/dist/core/status/fleet/next.js +153 -58
- package/dist/core/status/fleet/next.js.map +1 -1
- package/dist/core/status/report.js.map +1 -1
- package/dist/core/status/scan.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 +199 -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/claims.js +31 -7
- package/dist/core/usecases/delta/claims.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/delta/modified.js +315 -0
- package/dist/core/usecases/delta/modified.js.map +1 -0
- package/dist/core/usecases/delta/overlay.js +32 -191
- package/dist/core/usecases/delta/overlay.js.map +1 -1
- package/dist/core/usecases/fleet.js +195 -36
- package/dist/core/usecases/fleet.js.map +1 -1
- package/dist/{commands/validate/fleet/usecases → core/usecases/grade}/capability-tag.js +8 -9
- package/dist/core/usecases/grade/capability-tag.js.map +1 -0
- package/dist/core/usecases/grade/feature.js +88 -0
- package/dist/core/usecases/grade/feature.js.map +1 -0
- package/dist/{commands/validate/fleet/usecases → core/usecases/grade}/requirement-tag.js +3 -4
- package/dist/core/usecases/grade/requirement-tag.js.map +1 -0
- package/dist/{commands/validate/fleet/usecases → core/usecases/grade}/steps.js +102 -33
- package/dist/core/usecases/grade/steps.js.map +1 -0
- package/dist/{commands/validate/fleet/usecases → core/usecases/grade}/usecases.js +23 -10
- package/dist/core/usecases/grade/usecases.js.map +1 -0
- package/dist/core/usecases/operations.js +26 -14
- package/dist/core/usecases/operations.js.map +1 -1
- package/dist/core/usecases/place.js.map +1 -1
- package/dist/core/usecases/service/exclude.js +119 -0
- package/dist/core/usecases/service/exclude.js.map +1 -0
- package/dist/core/usecases/service/flows.js +164 -0
- package/dist/core/usecases/service/flows.js.map +1 -0
- 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 +70 -12
- 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 +4 -0
- package/dist/core/vocabulary/issue.js.map +1 -1
- package/dist/core/vocabulary/maturity.js +75 -4
- package/dist/core/vocabulary/maturity.js.map +1 -1
- package/dist/core/vocabulary/report.js +16 -1
- package/dist/core/vocabulary/report.js.map +1 -1
- package/examples/README.md +54 -11
- package/examples/docs/architecture/landscape.likec4 +62 -6
- package/examples/docs/architecture/services.likec4 +70 -0
- package/examples/docs/architecture/subsystems.likec4 +7 -0
- package/examples/docs/architecture/usecases/order-notification.likec4 +16 -7
- 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/FEAT-112-retire-order-v1/specs/order-service/openapi.yaml +4 -1
- package/examples/docs/features/archive/FEAT-088-refunds/specs/payment-service/openapi.yaml +13 -10
- package/examples/docs/likec4.config.json +0 -1
- package/examples/docs/services/checkout-web/model.likec4 +39 -56
- package/examples/docs/services/order-service/arch.spec.md +13 -6
- package/examples/docs/services/order-service/asyncapi.yaml +3 -2
- package/examples/docs/services/order-service/model.likec4 +58 -70
- package/examples/docs/services/payment-service/arch.spec.md +7 -4
- package/examples/docs/services/payment-service/model.likec4 +38 -37
- package/examples/docs/services/platform/identity-service/arch.spec.md +7 -4
- package/examples/docs/services/platform/identity-service/model.likec4 +35 -37
- package/examples/docs/services/platform/notification-service/asyncapi.yaml +2 -1
- package/examples/docs/services/platform/notification-service/health.yaml +8 -3
- package/examples/docs/services/platform/notification-service/model.likec4 +43 -64
- package/package.json +1 -1
- package/dist/commands/validate/checks/vocabulary.js +0 -31
- package/dist/commands/validate/checks/vocabulary.js.map +0 -1
- package/dist/commands/validate/fleet/usecases/capability-tag.js.map +0 -1
- package/dist/commands/validate/fleet/usecases/requirement-tag.js.map +0 -1
- package/dist/commands/validate/fleet/usecases/steps.js.map +0 -1
- package/dist/commands/validate/fleet/usecases/usecases.js.map +0 -1
- package/dist/core/agent/agents-md/artifacts.js +0 -220
- package/dist/core/agent/agents-md/artifacts.js.map +0 -1
- package/dist/core/agent/agents-md/cycle.js +0 -165
- 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 -247
- package/dist/core/diff/victims.js.map +0 -1
- package/dist/core/repo/tree/views.js +0 -172
- package/dist/core/repo/tree/views.js.map +0 -1
- package/dist/core/staging/snapshot.js.map +0 -1
- package/dist/core/verify/file.js.map +0 -1
|
@@ -10,11 +10,6 @@
|
|
|
10
10
|
* is what reads it.
|
|
11
11
|
*/
|
|
12
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
13
|
import { closeIds } from "../../../core/c4/arch.js";
|
|
19
14
|
import {} from "../../../core/c4/likec4.js";
|
|
20
15
|
import {} from "../../../core/kernel/ids/service.js";
|
|
@@ -24,15 +19,19 @@ import { responseGovernanceFindings } from "../checks/requirements.js";
|
|
|
24
19
|
import {} from "../../../core/document/spec.js";
|
|
25
20
|
import { readOpenapi } from "../../../core/openapi/doc.js";
|
|
26
21
|
import { FleetContext } from "../../../core/fleet-context.js";
|
|
22
|
+
import { repoPath } from "../../../core/envelope/json.js";
|
|
23
|
+
import {} from "../../../core/kernel/ids/dirs.js";
|
|
27
24
|
export async function apiAxisFindings(axis) {
|
|
28
|
-
const { service, me, paths, reqs, livingReqs, land, landSvcOf, fleet } = axis;
|
|
25
|
+
const { docsDir, service, me, paths, reqs, livingReqs, land, landSvcOf, fleet } = axis;
|
|
29
26
|
const findings = [];
|
|
30
27
|
// API coverage: every operation in openapi.yaml is governed by a requirement.
|
|
31
28
|
const api = await readOpenapi(paths.openapi, fleet);
|
|
32
29
|
const removeMarkers = api.ops.filter((o) => o.remove);
|
|
33
30
|
const liveOps = api.ops.filter((o) => !o.remove);
|
|
34
|
-
|
|
35
|
-
const
|
|
31
|
+
/** No contract to read: deleted, renamed, or never written. */
|
|
32
|
+
const contractMissing = !existsSync(paths.openapi);
|
|
33
|
+
const contract = contractOf(api, contractMissing);
|
|
34
|
+
const { ops, deprecated: deprecatedOps } = contract;
|
|
36
35
|
// Inbound calls the landscape can PROVE: op-linked edges whose target
|
|
37
36
|
// resolves to this service. Null when the landscape proves nothing at all
|
|
38
37
|
// (absent, or it did not parse) — which is not the same fact as "nobody
|
|
@@ -43,8 +42,6 @@ export async function apiAxisFindings(axis) {
|
|
|
43
42
|
// The other half of the evidence a contract is owed: the living spec's own
|
|
44
43
|
// `Operations:` lines. A requirement on its way out governs nothing.
|
|
45
44
|
const governedOps = livingReqs.flatMap((r) => r.operations);
|
|
46
|
-
/** No contract to read: deleted, renamed, or never written. */
|
|
47
|
-
const contractMissing = !existsSync(paths.openapi);
|
|
48
45
|
if (contractMissing) {
|
|
49
46
|
// A contract that is gone is not a reason to stop grading the API axis —
|
|
50
47
|
// it IS the axis's answer, and every check below used to live inside the
|
|
@@ -67,7 +64,7 @@ export async function apiAxisFindings(axis) {
|
|
|
67
64
|
severity: "error",
|
|
68
65
|
code: "service.no-openapi",
|
|
69
66
|
subject: service,
|
|
70
|
-
message: `No OpenAPI contract at ${paths.openapi}, and ${dangling.length} operation link(s) already point into it — ` +
|
|
67
|
+
message: `No OpenAPI contract at ${repoPath(docsDir, paths.openapi)}, and ${dangling.length} operation link(s) already point into it — ` +
|
|
71
68
|
`every requirement and landscape edge naming one of them resolves to nothing until the file is back`,
|
|
72
69
|
details: dangling,
|
|
73
70
|
});
|
|
@@ -76,7 +73,7 @@ export async function apiAxisFindings(axis) {
|
|
|
76
73
|
findings.push({
|
|
77
74
|
severity: "warn",
|
|
78
75
|
code: "service.no-openapi",
|
|
79
|
-
message: `No OpenAPI contract at ${paths.openapi} — API coverage and the landscape spine are unchecked`,
|
|
76
|
+
message: `No OpenAPI contract at ${repoPath(docsDir, paths.openapi)} — API coverage and the landscape spine are unchecked`,
|
|
80
77
|
});
|
|
81
78
|
}
|
|
82
79
|
}
|
|
@@ -236,103 +233,20 @@ export async function apiAxisFindings(axis) {
|
|
|
236
233
|
}
|
|
237
234
|
}
|
|
238
235
|
}
|
|
239
|
-
return {
|
|
240
|
-
findings,
|
|
241
|
-
contract: { ops, deprecated: deprecatedOps, unreadable: api.unreadable || contractMissing },
|
|
242
|
-
};
|
|
236
|
+
return { findings, contract };
|
|
243
237
|
}
|
|
244
238
|
/**
|
|
245
|
-
*
|
|
246
|
-
*
|
|
247
|
-
* `
|
|
248
|
-
*
|
|
249
|
-
* WHY THIS IS NOT AN EXTRACTOR. It reads no line of service code and derives no
|
|
250
|
-
* meaning: it parses a standard document the team's own build emitted, at a
|
|
251
|
-
* path a human wrote into `loam.json`, and compares a digest. That is the same
|
|
252
|
-
* category as `verify --results` ingesting a cucumber report, and it is the
|
|
253
|
-
* only thing in the product that can notice the assumption SCHEMA quietly makes
|
|
254
|
-
* — that the committed `services/<id>/openapi.yaml` is the contract the service
|
|
255
|
-
* actually serves. Most fleets generate that document; a fleet that copies it
|
|
256
|
-
* by hand in month one is a fleet whose spine checks grade last quarter's
|
|
257
|
-
* endpoints by month six, and until now nothing could say so.
|
|
258
|
-
*
|
|
259
|
-
* IT NEVER WRITES. The copy stays a human `cp`, reviewed in a pull request,
|
|
260
|
-
* because that is what makes the docs-repo document a contract somebody agreed
|
|
261
|
-
* to rather than a cache of whatever the build last produced. loam reports the
|
|
262
|
-
* divergence and stops.
|
|
263
|
-
*
|
|
264
|
-
* THE DIGEST IS OVER CANONICAL JSON, never raw bytes. Two generator versions
|
|
265
|
-
* that order keys differently, or a YAML dumper that re-wraps a description,
|
|
266
|
-
* produce identical documents and must not produce a permanent warning nobody
|
|
267
|
-
* can clear — the lesson `requirementDigest` already records one package over.
|
|
268
|
-
*
|
|
269
|
-
* Runs only in the SERVICE repo. From the docs repo there is no build output to
|
|
270
|
-
* read and the whole family stays silent, exactly as `sources` provenance does.
|
|
239
|
+
* What the spine may conclude from one contract read — spelled once, because a
|
|
240
|
+
* `--base` run now grades a changed edge against a provider it does not grade
|
|
241
|
+
* as a target (`../scope.ts`), and two spellings of "which operations are
|
|
242
|
+
* live" would be two chances for a removal marker to count as defined.
|
|
271
243
|
*/
|
|
272
|
-
export
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
// a finding, never a thrown config error that would take down every other
|
|
280
|
-
// command in the repo.
|
|
281
|
-
generatedPath = resolveInside(repoDir, spelled, `"contracts.openapi"`);
|
|
282
|
-
}
|
|
283
|
-
catch (err) {
|
|
284
|
-
return [{
|
|
285
|
-
severity: "error",
|
|
286
|
-
code: "contracts.source-invalid",
|
|
287
|
-
subject: service,
|
|
288
|
-
message: `${service}: loam.json names contracts.openapi '${spelled}', which does not resolve inside this repository — ` +
|
|
289
|
-
`${err instanceof Error ? err.message : String(err)}`,
|
|
290
|
-
}];
|
|
291
|
-
}
|
|
292
|
-
if (!existsSync(generatedPath)) {
|
|
293
|
-
return [{
|
|
294
|
-
severity: "error",
|
|
295
|
-
code: "contracts.source-missing",
|
|
296
|
-
subject: service,
|
|
297
|
-
message: `${service}: loam.json names contracts.openapi '${spelled}', and nothing is there. ` +
|
|
298
|
-
"Either the build has not run in this checkout, or the path is wrong — CI that validates before it builds " +
|
|
299
|
-
"sees exactly this. Run the build first, or correct the path",
|
|
300
|
-
}];
|
|
301
|
-
}
|
|
302
|
-
const digests = [];
|
|
303
|
-
for (const [label, path] of [["generated", generatedPath], ["committed", committedPath]]) {
|
|
304
|
-
if (label === "committed" && !existsSync(path))
|
|
305
|
-
return [];
|
|
306
|
-
let parsed;
|
|
307
|
-
try {
|
|
308
|
-
parsed = parseYaml(await readFile(path, "utf8"));
|
|
309
|
-
}
|
|
310
|
-
catch (err) {
|
|
311
|
-
// An unreadable GENERATED document is this check's business; an
|
|
312
|
-
// unreadable committed one is already `openapi.invalid` and must not be
|
|
313
|
-
// reported twice under two codes.
|
|
314
|
-
if (label === "committed")
|
|
315
|
-
return [];
|
|
316
|
-
return [{
|
|
317
|
-
severity: "error",
|
|
318
|
-
code: "contracts.source-invalid",
|
|
319
|
-
subject: service,
|
|
320
|
-
message: `${service}: contracts.openapi '${spelled}' does not parse as YAML or JSON — ` +
|
|
321
|
-
`${err instanceof Error ? err.message : String(err)}`,
|
|
322
|
-
}];
|
|
323
|
-
}
|
|
324
|
-
digests.push(createHash("sha256").update(canonicalJson(parsed), "utf8").digest("hex"));
|
|
325
|
-
}
|
|
326
|
-
if (digests[0] === digests[1])
|
|
327
|
-
return [];
|
|
328
|
-
return [{
|
|
329
|
-
severity: "warn",
|
|
330
|
-
code: "openapi.generated-stale",
|
|
331
|
-
subject: service,
|
|
332
|
-
message: `${service}: the committed openapi.yaml differs from what this repository's build produces (${spelled}). ` +
|
|
333
|
-
"The docs-repo copy is what every operation join is graded against, so the fleet is being checked against a " +
|
|
334
|
-
"contract this service no longer serves. Review the difference and copy the generated document across — " +
|
|
335
|
-
"loam never does that for you, because the committed contract is one somebody agreed to, not a cache of the build",
|
|
336
|
-
}];
|
|
244
|
+
export function contractOf(api, missing) {
|
|
245
|
+
const live = api.ops.filter((o) => !o.remove);
|
|
246
|
+
return {
|
|
247
|
+
ops: live.map((o) => o.id),
|
|
248
|
+
deprecated: new Set(live.filter((o) => o.deprecated).map((o) => o.id)),
|
|
249
|
+
unreadable: api.unreadable || missing,
|
|
250
|
+
};
|
|
337
251
|
}
|
|
338
252
|
//# sourceMappingURL=api.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"api.js","sourceRoot":"","sources":["../../../../src/commands/validate/service/api.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;GAUG;AACH,OAAO,EAAE,UAAU,EAAE,MAAM,SAAS,CAAC;AACrC,OAAO,EAAE,QAAQ,EAAE,MAAM,kBAAkB,CAAC;AAC5C,OAAO,EAAE,UAAU,EAAE,MAAM,aAAa,CAAC;AACzC,OAAO,EAAE,KAAK,IAAI,SAAS,EAAE,MAAM,MAAM,CAAC;AAC1C,OAAO,EAAE,aAAa,EAAE,MAAM,qCAAqC,CAAC;AACpE,OAAO,EAAE,aAAa,EAAE,MAAM,iCAAiC,CAAC;AAChE,OAAO,EAAE,QAAQ,EAAE,MAAM,0BAA0B,CAAC;AACpD,OAAO,EAAkB,MAAM,4BAA4B,CAAC;AAC5D,OAAO,EAA8C,MAAM,qCAAqC,CAAC;AACjG,OAAO,EAAqB,MAAM,6BAA6B,CAAC;AAChE,OAAO,EAAgB,MAAM,oCAAoC,CAAC;AAClE,OAAO,EAAE,0BAA0B,EAAE,MAAM,2BAA2B,CAAC;AACvE,OAAO,EAAoB,MAAM,gCAAgC,CAAC;AAClE,OAAO,EAAE,WAAW,EAAE,MAAM,8BAA8B,CAAC;AAC3D,OAAO,EAAE,YAAY,EAAE,MAAM,gCAAgC,CAAC;AAkC9D,MAAM,CAAC,KAAK,UAAU,eAAe,CACnC,IAAa;IAEb,MAAM,EAAE,OAAO,EAAE,EAAE,EAAE,KAAK,EAAE,IAAI,EAAE,UAAU,EAAE,IAAI,EAAE,SAAS,EAAE,KAAK,EAAE,GAAG,IAAI,CAAC;IAC9E,MAAM,QAAQ,GAAc,EAAE,CAAC;IAE/B,8EAA8E;IAC9E,MAAM,GAAG,GAAG,MAAM,WAAW,CAAC,KAAK,CAAC,OAAO,EAAE,KAAK,CAAC,CAAC;IACpD,MAAM,aAAa,GAAG,GAAG,CAAC,GAAG,CAAC,MAAM,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,MAAM,CAAC,CAAC;IACtD,MAAM,OAAO,GAAG,GAAG,CAAC,GAAG,CAAC,MAAM,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,CAAC,MAAM,CAAC,CAAC;IACjD,MAAM,GAAG,GAAG,OAAO,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC;IACrC,MAAM,aAAa,GAAG,IAAI,GAAG,CAAC,OAAO,CAAC,MAAM,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,UAAU,CAAC,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC;IACpF,sEAAsE;IACtE,0EAA0E;IAC1E,wEAAwE;IACxE,mDAAmD;IACnD,MAAM,UAAU,GACd,IAAI,KAAK,IAAI,IAAI,IAAI,CAAC,MAAM,CAAC,MAAM,GAAG,CAAC;QACrC,CAAC,CAAC,IAAI;QACN,CAAC,CAAC,IAAI,CAAC,aAAa,CAAC,MAAM,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,EAAE,KAAK,SAAS,IAAI,SAAU,CAAC,CAAC,CAAC,MAAM,CAAC,KAAK,EAAE,CAAC,CAAC;IAC1F,2EAA2E;IAC3E,qEAAqE;IACrE,MAAM,WAAW,GAAG,UAAU,CAAC,OAAO,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,UAAU,CAAC,CAAC;IAC5D,+DAA+D;IAC/D,MAAM,eAAe,GAAG,CAAC,UAAU,CAAC,KAAK,CAAC,OAAO,CAAC,CAAC;IACnD,IAAI,eAAe,EAAE,CAAC;QACpB,yEAAyE;QACzE,yEAAyE;QACzE,2EAA2E;QAC3E,iEAAiE;QACjE,EAAE;QACF,wEAAwE;QACxE,uEAAuE;QACvE,wEAAwE;QACxE,yEAAyE;QACzE,uEAAuE;QACvE,2EAA2E;QAC3E,oCAAoC;QACpC,EAAE;QACF,wEAAwE;QACxE,8CAA8C;QAC9C,MAAM,QAAQ,GAAG,CAAC,GAAG,IAAI,GAAG,CAAC,CAAC,GAAG,WAAW,EAAE,GAAG,CAAC,UAAU,IAAI,EAAE,CAAC,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,EAAG,CAAC,CAAC,CAAC,CAAC,CAAC,IAAI,EAAE,CAAC;QAChG,IAAI,QAAQ,CAAC,MAAM,GAAG,CAAC,EAAE,CAAC;YACxB,QAAQ,CAAC,IAAI,CAAC;gBACZ,QAAQ,EAAE,OAAO;gBACjB,IAAI,EAAE,oBAAoB;gBAC1B,OAAO,EAAE,OAAO;gBAChB,OAAO,EACL,0BAA0B,KAAK,CAAC,OAAO,SAAS,QAAQ,CAAC,MAAM,6CAA6C;oBAC5G,oGAAoG;gBACtG,OAAO,EAAE,QAAQ;aAClB,CAAC,CAAC;QACL,CAAC;aAAM,IAAI,UAAU,KAAK,IAAI,EAAE,CAAC;YAC/B,QAAQ,CAAC,IAAI,CAAC;gBACZ,QAAQ,EAAE,MAAM;gBAChB,IAAI,EAAE,oBAAoB;gBAC1B,OAAO,EAAE,0BAA0B,KAAK,CAAC,OAAO,uDAAuD;aACxG,CAAC,CAAC;QACL,CAAC;IACH,CAAC;SAAM,IAAI,GAAG,CAAC,UAAU,EAAE,CAAC;QAC1B,4EAA4E;QAC5E,uEAAuE;QACvE,2EAA2E;QAC3E,2EAA2E;QAC3E,iEAAiE;QACjE,oEAAoE;QACpE,QAAQ,CAAC,IAAI,CAAC;YACZ,QAAQ,EAAE,OAAO;YACjB,IAAI,EAAE,iBAAiB;YACvB,OAAO,EAAE,GAAG,OAAO,oFAAoF;YACvG,GAAG,CAAC,GAAG,CAAC,KAAK,KAAK,SAAS,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,EAAE,OAAO,EAAE,CAAC,GAAG,CAAC,KAAK,CAAC,EAAE,CAAC;SAC7D,CAAC,CAAC;IACL,CAAC;SAAM,CAAC;QACN,IAAI,aAAa,CAAC,MAAM,GAAG,CAAC,EAAE,CAAC;YAC7B,QAAQ,CAAC,IAAI,CAAC;gBACZ,QAAQ,EAAE,OAAO;gBACjB,IAAI,EAAE,8BAA8B;gBACpC,OAAO,EAAE,GAAG,OAAO,kCAAkC,aAAa,CAAC,MAAM,6BAA6B,aAAa,CAAC,GAAG,CAAC,CAAC,EAAE,EAAE,EAAE,CAAC,EAAE,CAAC,EAAE,CAAC,CAAC,IAAI,CAAC,IAAI,CAAC,sDAAsD;aACxM,CAAC,CAAC;QACL,CAAC;QACD,2EAA2E;QAC3E,4EAA4E;QAC5E,4EAA4E;QAC5E,yEAAyE;QACzE,iEAAiE;QACjE,0EAA0E;QAC1E,0EAA0E;QAC1E,sDAAsD;QACtD,KAAK,MAAM,OAAO,IAAI,GAAG,CAAC,iBAAiB,EAAE,CAAC;YAC5C,QAAQ,CAAC,IAAI,CAAC;gBACZ,QAAQ,EAAE,OAAO;gBACjB,IAAI,EAAE,kCAAkC;gBACxC,OAAO,EAAE,OAAO;gBAChB,OAAO,EACL,GAAG,OAAO,iEAAiE,OAAO,MAAM;oBACxF,mHAAmH;oBACnH,2JAA2J;aAC9J,CAAC,CAAC;QACL,CAAC;QACD,yEAAyE;QACzE,yEAAyE;QACzE,2EAA2E;QAC3E,wEAAwE;QACxE,0EAA0E;QAC1E,6DAA6D;QAC7D,KAAK,MAAM,EAAE,IAAI,GAAG,CAAC,YAAY,EAAE,CAAC;YAClC,MAAM,KAAK,GAAG,GAAG,CAAC,GAAG,CAAC,MAAM,CAAC,CAAC,EAAE,EAAE,EAAE,CAAC,EAAE,CAAC,EAAE,KAAK,EAAE,CAAC,CAAC,GAAG,CAAC,CAAC,EAAE,EAAE,EAAE,CAAC,GAAG,EAAE,CAAC,MAAM,IAAI,EAAE,CAAC,IAAI,EAAE,CAAC,CAAC;YAC1F,QAAQ,CAAC,IAAI,CAAC;gBACZ,QAAQ,EAAE,MAAM;gBAChB,IAAI,EAAE,+BAA+B;gBACrC,OAAO,EAAE,OAAO;gBAChB,OAAO,EAAE,GAAG,OAAO,6CAA6C,EAAE,QAAQ,KAAK,CAAC,IAAI,CAAC,OAAO,CAAC,8IAA8I;aAC5O,CAAC,CAAC;QACL,CAAC;QACD,kEAAkE;QAClE,uEAAuE;QACvE,yEAAyE;QACzE,uDAAuD;QACvD,yEAAyE;QACzE,MAAM,WAAW,GAAG,OAAO,CAAC,MAAM,CAAC,CAAC,EAAE,EAAE,EAAE,CAAC,EAAE,CAAC,WAAW,KAAK,IAAI,CAAC,CAAC,GAAG,CAAC,CAAC,EAAE,EAAE,EAAE,CAAC,EAAE,CAAC,EAAE,CAAC,CAAC;QACvF,IAAI,WAAW,CAAC,MAAM,GAAG,CAAC,EAAE,CAAC;YAC3B,QAAQ,CAAC,IAAI,CAAC;gBACZ,QAAQ,EAAE,MAAM;gBAChB,IAAI,EAAE,8BAA8B;gBACpC,OAAO,EAAE,OAAO;gBAChB,OAAO,EACL,GAAG,OAAO,KAAK,WAAW,CAAC,MAAM,6CAA6C,WAAW,CAAC,IAAI,CAAC,IAAI,CAAC,MAAM;oBAC1G,6FAA6F;oBAC7F,2FAA2F;aAC9F,CAAC,CAAC;QACL,CAAC;QACD,IAAI,GAAG,CAAC,YAAY,CAAC,MAAM,GAAG,CAAC,EAAE,CAAC;YAChC,QAAQ,CAAC,IAAI,CAAC;gBACZ,QAAQ,EAAE,MAAM;gBAChB,IAAI,EAAE,wBAAwB;gBAC9B,OAAO,EAAE,OAAO;gBAChB,OAAO,EACL,GAAG,OAAO,2BAA2B,GAAG,CAAC,YAAY,CAAC,MAAM,oCAAoC;oBAChG,wFAAwF;oBACxF,4EAA4E;gBAC9E,OAAO,EAAE,GAAG,CAAC,YAAY;aAC1B,CAAC,CAAC;QACL,CAAC;QACD,MAAM,OAAO,GAAG,IAAI,GAAG,CAAC,GAAG,CAAC,CAAC;QAC7B,6EAA6E;QAC7E,wEAAwE;QACxE,6EAA6E;QAC7E,0EAA0E;QAC1E,yEAAyE;QACzE,2EAA2E;QAC3E,4EAA4E;QAC5E,kCAAkC;QAClC,KAAK,MAAM,CAAC,IAAI,UAAU,EAAE,CAAC;YAC3B,KAAK,MAAM,EAAE,IAAI,CAAC,CAAC,UAAU,EAAE,CAAC;gBAC9B,IAAI,OAAO,CAAC,GAAG,CAAC,EAAE,CAAC;oBAAE,SAAS;gBAC9B,MAAM,KAAK,GAAG,QAAQ,CAAC,EAAE,EAAE,GAAG,CAAC,CAAC;gBAChC,QAAQ,CAAC,IAAI,CAAC;oBACZ,QAAQ,EAAE,OAAO;oBACjB,IAAI,EAAE,uBAAuB;oBAC7B,OAAO,EAAE,OAAO;oBAChB,OAAO,EACL,GAAG,OAAO,kBAAkB,CAAC,CAAC,IAAI,cAAc,EAAE,qBAAqB,OAAO,YAAY;wBAC1F,CAAC,KAAK,CAAC,MAAM,GAAG,CAAC,CAAC,CAAC,CAAC,mBAAmB,KAAK,CAAC,IAAI,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,CAAC;iBACnE,CAAC,CAAC;YACL,CAAC;QACH,CAAC;QACD,IAAI,GAAG,CAAC,MAAM,GAAG,CAAC,EAAE,CAAC;YACrB,MAAM,QAAQ,GAAG,IAAI,GAAG,CAAC,UAAU,CAAC,OAAO,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,UAAU,CAAC,CAAC,CAAC;YAClE,MAAM,OAAO,GAAG,GAAG,CAAC,MAAM,CAAC,CAAC,EAAE,EAAE,EAAE,CAAC,CAAC,QAAQ,CAAC,GAAG,CAAC,EAAE,CAAC,CAAC,CAAC;YACtD,IAAI,OAAO,CAAC,MAAM,KAAK,CAAC,EAAE,CAAC;gBACzB,QAAQ,CAAC,IAAI,CAAC;oBACZ,QAAQ,EAAE,IAAI;oBACd,IAAI,EAAE,aAAa;oBACnB,kEAAkE;oBAClE,qEAAqE;oBACrE,sEAAsE;oBACtE,gBAAgB;oBAChB,OAAO,EAAE,GAAG,OAAO,kBAAkB,GAAG,CAAC,MAAM,4CAA4C,UAAU,CAAC,MAAM,yBAAyB;iBACtI,CAAC,CAAC;YACL,CAAC;iBAAM,CAAC;gBACN,QAAQ,CAAC,IAAI,CAAC;oBACZ,QAAQ,EAAE,MAAM;oBAChB,IAAI,EAAE,gBAAgB;oBACtB,OAAO,EAAE,GAAG,OAAO,KAAK,OAAO,CAAC,MAAM,mDAAmD,OAAO,CAAC,IAAI,CAAC,IAAI,CAAC,EAAE;iBAC9G,CAAC,CAAC;YACL,CAAC;YACD,QAAQ,CAAC,IAAI,CAAC,GAAG,0BAA0B,CAAC,OAAO,EAAE,OAAO,EAAE,UAAU,CAAC,CAAC,CAAC;YAC3E,sEAAsE;YACtE,qEAAqE;YACrE,wEAAwE;YACxE,2EAA2E;YAC3E,IAAI,IAAI,CAAC,MAAM,GAAG,CAAC,IAAI,IAAI,CAAC,KAAK,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,UAAU,CAAC,MAAM,KAAK,CAAC,CAAC,EAAE,CAAC;gBACpE,QAAQ,CAAC,IAAI,CAAC;oBACZ,QAAQ,EAAE,MAAM;oBAChB,IAAI,EAAE,kBAAkB;oBACxB,OAAO,EAAE,GAAG,OAAO,0BAA0B,GAAG,CAAC,MAAM,yFAAyF;iBACjJ,CAAC,CAAC;YACL,CAAC;YACD,qEAAqE;YACrE,oEAAoE;YACpE,2EAA2E;YAC3E,4EAA4E;YAC5E,qGAAqG;YACrG,kEAAkE;YAClE,KAAK,MAAM,CAAC,IAAI,IAAI,EAAE,CAAC;gBACrB,MAAM,QAAQ,GAAG,CAAC,CAAC,UAAU,CAAC,MAAM,CAAC,CAAC,EAAE,EAAE,EAAE,CAAC,OAAO,CAAC,GAAG,CAAC,EAAE,CAAC,CAAC,CAAC;gBAC9D,IAAI,QAAQ,CAAC,MAAM,KAAK,CAAC,IAAI,CAAC,QAAQ,CAAC,KAAK,CAAC,CAAC,EAAE,EAAE,EAAE,CAAC,aAAa,CAAC,GAAG,CAAC,EAAE,CAAC,CAAC;oBAAE,SAAS;gBACtF,QAAQ,CAAC,IAAI,CAAC;oBACZ,QAAQ,EAAE,MAAM;oBAChB,IAAI,EAAE,4BAA4B;oBAClC,OAAO,EAAE,GAAG,OAAO,kBAAkB,CAAC,CAAC,IAAI,2CAA2C,QAAQ,CAAC,IAAI,CAAC,IAAI,CAAC,yGAAyG;iBACnN,CAAC,CAAC;YACL,CAAC;QACD,CAAC;IACH,CAAC;IAED,OAAO;QACL,QAAQ;QACR,QAAQ,EAAE,EAAE,GAAG,EAAE,UAAU,EAAE,aAAa,EAAE,UAAU,EAAE,GAAG,CAAC,UAAU,IAAI,eAAe,EAAE;KAC5F,CAAC;AACJ,CAAC;AAED;;;;;;;;;;;;;;;;;;;;;;;;;;;GA2BG;AACH,MAAM,CAAC,KAAK,UAAU,yBAAyB,CAC7C,OAAe,EACf,OAAe,EACf,OAA2B,EAC3B,aAAqB;IAErB,IAAI,OAAO,KAAK,SAAS;QAAE,OAAO,EAAE,CAAC;IACrC,IAAI,aAAqB,CAAC;IAC1B,IAAI,CAAC;QACH,2EAA2E;QAC3E,2EAA2E;QAC3E,0EAA0E;QAC1E,uBAAuB;QACvB,aAAa,GAAG,aAAa,CAAC,OAAO,EAAE,OAAO,EAAE,qBAAqB,CAAC,CAAC;IACzE,CAAC;IAAC,OAAO,GAAG,EAAE,CAAC;QACb,OAAO,CAAC;gBACN,QAAQ,EAAE,OAAO;gBACjB,IAAI,EAAE,0BAA0B;gBAChC,OAAO,EAAE,OAAO;gBAChB,OAAO,EACL,GAAG,OAAO,wCAAwC,OAAO,qDAAqD;oBAC9G,GAAG,GAAG,YAAY,KAAK,CAAC,CAAC,CAAC,GAAG,CAAC,OAAO,CAAC,CAAC,CAAC,MAAM,CAAC,GAAG,CAAC,EAAE;aACxD,CAAC,CAAC;IACL,CAAC;IACD,IAAI,CAAC,UAAU,CAAC,aAAa,CAAC,EAAE,CAAC;QAC/B,OAAO,CAAC;gBACN,QAAQ,EAAE,OAAO;gBACjB,IAAI,EAAE,0BAA0B;gBAChC,OAAO,EAAE,OAAO;gBAChB,OAAO,EACL,GAAG,OAAO,wCAAwC,OAAO,2BAA2B;oBACpF,2GAA2G;oBAC3G,6DAA6D;aAChE,CAAC,CAAC;IACL,CAAC;IACD,MAAM,OAAO,GAAa,EAAE,CAAC;IAC7B,KAAK,MAAM,CAAC,KAAK,EAAE,IAAI,CAAC,IAAI,CAAC,CAAC,WAAW,EAAE,aAAa,CAAC,EAAE,CAAC,WAAW,EAAE,aAAa,CAAC,CAAU,EAAE,CAAC;QAClG,IAAI,KAAK,KAAK,WAAW,IAAI,CAAC,UAAU,CAAC,IAAI,CAAC;YAAE,OAAO,EAAE,CAAC;QAC1D,IAAI,MAAe,CAAC;QACpB,IAAI,CAAC;YACH,MAAM,GAAG,SAAS,CAAC,MAAM,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC,CAAC,CAAC;QACnD,CAAC;QAAC,OAAO,GAAG,EAAE,CAAC;YACb,gEAAgE;YAChE,wEAAwE;YACxE,kCAAkC;YAClC,IAAI,KAAK,KAAK,WAAW;gBAAE,OAAO,EAAE,CAAC;YACrC,OAAO,CAAC;oBACN,QAAQ,EAAE,OAAO;oBACjB,IAAI,EAAE,0BAA0B;oBAChC,OAAO,EAAE,OAAO;oBAChB,OAAO,EACL,GAAG,OAAO,wBAAwB,OAAO,qCAAqC;wBAC9E,GAAG,GAAG,YAAY,KAAK,CAAC,CAAC,CAAC,GAAG,CAAC,OAAO,CAAC,CAAC,CAAC,MAAM,CAAC,GAAG,CAAC,EAAE;iBACxD,CAAC,CAAC;QACL,CAAC;QACD,OAAO,CAAC,IAAI,CAAC,UAAU,CAAC,QAAQ,CAAC,CAAC,MAAM,CAAC,aAAa,CAAC,MAAM,CAAC,EAAE,MAAM,CAAC,CAAC,MAAM,CAAC,KAAK,CAAC,CAAC,CAAC;IACzF,CAAC;IACD,IAAI,OAAO,CAAC,CAAC,CAAC,KAAK,OAAO,CAAC,CAAC,CAAC;QAAE,OAAO,EAAE,CAAC;IACzC,OAAO,CAAC;YACN,QAAQ,EAAE,MAAM;YAChB,IAAI,EAAE,yBAAyB;YAC/B,OAAO,EAAE,OAAO;YAChB,OAAO,EACL,GAAG,OAAO,oFAAoF,OAAO,KAAK;gBAC1G,6GAA6G;gBAC7G,yGAAyG;gBACzG,kHAAkH;SACrH,CAAC,CAAC;AACL,CAAC","sourcesContent":["/**\n * The HTTP contract axis: openapi.yaml graded against the living spec.\n *\n * What it decides for everything downstream is whether there is a contract to\n * read at all. A missing or unparseable openapi.yaml is ONE finding about ONE\n * file, and that fact has to travel: grading the fleet map's inbound edges\n * against the empty operation set it leaves behind turns one broken YAML into\n * one `spine.op-undefined` per consumer, each of them accusing the landscape of\n * a fault that is not there. `ServiceContract` is how it travels; `./spine.js`\n * is what reads it.\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 { closeIds } from \"../../../core/c4/arch.js\";\nimport { type LoadedDoc } from \"../../../core/c4/likec4.js\";\nimport { type DeclaredService, type PathableService } from \"../../../core/kernel/ids/service.js\";\nimport { type ServicePaths } from \"../../../core/repo/paths.js\";\nimport { type Finding } from \"../../../core/vocabulary/report.js\";\nimport { responseGovernanceFindings } from \"../checks/requirements.js\";\nimport { type Requirement } from \"../../../core/document/spec.js\";\nimport { readOpenapi } from \"../../../core/openapi/doc.js\";\nimport { FleetContext } from \"../../../core/fleet-context.js\";\n\n/** What the service target hands the HTTP axis. */\nexport interface ApiAxis {\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, REMOVED ones included. */\n reqs: Requirement[];\n /** The ones that still govern something. */\n livingReqs: Requirement[];\n /** The living landscape, or null when the repo has none. */\n land: LoadedDoc | null;\n /** Its element→service resolver, built once per run; null exactly when `land` is. */\n landSvcOf: ((id: string) => DeclaredService) | null;\n fleet?: FleetContext;\n}\n\n/**\n * What the spine may conclude from this service's contract.\n *\n * `unreadable` covers both shapes of \"there is nothing to read\" — absent and\n * unparseable — because they have the same consequence for an inbound edge:\n * the contract proves neither that the operation exists nor that it does not.\n */\nexport interface ServiceContract {\n /** operationIds the living contract defines, empty when there is nothing to read. */\n ops: string[];\n /** Those of them the contract marks `deprecated: true`. */\n deprecated: ReadonlySet<string>;\n unreadable: boolean;\n}\n\nexport async function apiAxisFindings(\n axis: ApiAxis,\n): Promise<{ findings: Finding[]; contract: ServiceContract }> {\n const { service, me, paths, reqs, livingReqs, land, landSvcOf, fleet } = axis;\n const findings: Finding[] = [];\n\n // API coverage: every operation in openapi.yaml is governed by a requirement.\n const api = await readOpenapi(paths.openapi, fleet);\n const removeMarkers = api.ops.filter((o) => o.remove);\n const liveOps = api.ops.filter((o) => !o.remove);\n const ops = liveOps.map((o) => o.id);\n const deprecatedOps = new Set(liveOps.filter((o) => o.deprecated).map((o) => o.id));\n // Inbound calls the landscape can PROVE: op-linked edges whose target\n // resolves to this service. Null when the landscape proves nothing at all\n // (absent, or it did not parse) — which is not the same fact as \"nobody\n // calls me\", and the two must not be graded alike.\n const inboundOps =\n land === null || land.errors.length > 0\n ? null\n : land.relationships.filter((r) => r.op !== undefined && landSvcOf!(r.target) === me);\n // The other half of the evidence a contract is owed: the living spec's own\n // `Operations:` lines. A requirement on its way out governs nothing.\n const governedOps = livingReqs.flatMap((r) => r.operations);\n /** No contract to read: deleted, renamed, or never written. */\n const contractMissing = !existsSync(paths.openapi);\n if (contractMissing) {\n // A contract that is gone is not a reason to stop grading the API axis —\n // it IS the axis's answer, and every check below used to live inside the\n // file-exists branch, so deleting or misspelling the file turned the whole\n // axis (including the documented `spec-api.op-undefined`) green.\n //\n // What decides the severity is whether anything already written down is\n // holding a join into it: a living `Operations:` line, or an op-linked\n // landscape edge. With one of those, the absence breaks a link somebody\n // authored — an error. With neither, this is a service that legitimately\n // has no HTTP surface (a worker nobody calls), and a fleet mid-rollout\n // must not go red for it — a warning, or, when the landscape PROVES nobody\n // calls it, the documented silence.\n //\n // One finding, not one per dangling link: the fix is a single file, and\n // the links it strands ride along as details.\n const dangling = [...new Set([...governedOps, ...(inboundOps ?? []).map((r) => r.op!)])].sort();\n if (dangling.length > 0) {\n findings.push({\n severity: \"error\",\n code: \"service.no-openapi\",\n subject: service,\n message:\n `No OpenAPI contract at ${paths.openapi}, and ${dangling.length} operation link(s) already point into it — ` +\n `every requirement and landscape edge naming one of them resolves to nothing until the file is back`,\n details: dangling,\n });\n } else if (inboundOps === null) {\n findings.push({\n severity: \"warn\",\n code: \"service.no-openapi\",\n message: `No OpenAPI contract at ${paths.openapi} — API coverage and the landscape spine are unchecked`,\n });\n }\n } else if (api.unreadable) {\n // A contract that EXISTS but does not read is a broken source of truth, not\n // an empty one: swallowing it into zero operations used to grade every\n // inbound landscape edge `spine.op-undefined` — a false diagnosis pointing\n // at the landscape when the truth was this file. So the file is the error,\n // and every check that reads the contract (api.*, the spine's op\n // resolution) is suspended below, the landscape.invalid discipline.\n findings.push({\n severity: \"error\",\n code: \"openapi.invalid\",\n message: `${service}: openapi.yaml does not parse — API coverage and the landscape spine are unchecked`,\n ...(api.error === undefined ? {} : { details: [api.error] }),\n });\n } else {\n if (removeMarkers.length > 0) {\n findings.push({\n severity: \"error\",\n code: \"openapi.remove-marker-living\",\n message: `${service}: living openapi.yaml contains ${removeMarkers.length} x-loam-remove marker(s) (${removeMarkers.map((op) => op.id).join(\", \")}) — removal markers are valid only in feature deltas`,\n });\n }\n // The same marker written at PATH level, beside the methods. `readOpenapi`\n // is keyed by (path, method), which is precisely why the check above cannot\n // see this one and precisely how one reached a living contract in the first\n // place: it addresses no operation, so it retired nothing, and it stayed\n // invisible afterwards. Error, like its method-level sibling — a\n // feature-only key published to every consumer of the fleet, and one that\n // keeps the empty-path cleanup from ever firing. Same code as the archive\n // plan's: one breach, one name, wherever it is found.\n for (const removed of api.pathLevelRemovals) {\n findings.push({\n severity: \"error\",\n code: \"openapi.remove-marker-path-level\",\n subject: service,\n message:\n `${service}: living openapi.yaml carries x-loam-remove at PATH level on '${removed}' — ` +\n `a removal marker names ONE operation, so beside the methods it retires nothing and is not a contract key either, ` +\n `and no id-keyed check can see it. Delete it from the living contract; retire an operation through a feature delta whose marker sits inside the operation.`,\n });\n }\n // Two slots claiming one operationId in a LIVING contract. `readOpenapi`\n // has computed this since the merge needed it, and only the feature path\n // (coherence) ever read it — so the ambiguity was reachable only when some\n // unrelated feature happened to carry a delta for this service, and the\n // fleet gate that actually runs in CI was blind to it. Same code and same\n // sentence as the feature-scope check: one breach, one name.\n for (const id of api.duplicateIds) {\n const slots = api.ops.filter((op) => op.id === id).map((op) => `${op.method} ${op.path}`);\n findings.push({\n severity: \"warn\",\n code: \"openapi.duplicate-operationid\",\n subject: service,\n message: `${service}: the living OpenAPI defines operationId '${id}' at ${slots.join(\" and \")} — every join on the id (a requirement's Operations: line, an edge's metadata { op }, a removal marker) picks one of those slots arbitrarily`,\n });\n }\n // The contract-depth probes. Form validated and depth did not: an\n // operation whose one response is `description: OK`, and a `$ref` to a\n // schema nobody wrote, both read as cleanly as a complete contract while\n // a reader still cannot rebuild its payloads. Presence\n // probes over the document's own keys; nothing reads what a schema says.\n const undescribed = liveOps.filter((op) => op.undescribed === true).map((op) => op.id);\n if (undescribed.length > 0) {\n findings.push({\n severity: \"warn\",\n code: \"openapi.response-undescribed\",\n subject: service,\n message:\n `${service}: ${undescribed.length} operation(s) declare no response schema (${undescribed.join(\", \")}) — ` +\n `the contract names the operation and says nothing about what it returns, so a reader still ` +\n `needs the code; declare content with a schema on at least one response (204/304 excepted)`,\n });\n }\n if (api.danglingRefs.length > 0) {\n findings.push({\n severity: \"warn\",\n code: \"openapi.ref-unresolved\",\n subject: service,\n message:\n `${service}: openapi.yaml contains ${api.danglingRefs.length} internal $ref(s) that resolve to ` +\n `nothing in the document — the operation validates while its schema is silently absent ` +\n `(the archive plan refuses the same breach in a feature delta, as an error)`,\n details: api.danglingRefs,\n });\n }\n const defined = new Set(ops);\n // `Operations:` on a LIVING requirement, resolved against this service's own\n // contract. Nothing did this before: the same spine is checked inside a\n // feature delta (coherence's spec-api.op-undefined) and then never again, so\n // a typo that shipped, or an operation later renamed out of openapi.yaml,\n // left a living requirement governing an operation that does not exist —\n // green forever, and every downstream join through that id silently empty.\n // Same code and severity as the feature-scope check on purpose: one breach,\n // one name, wherever it is found.\n for (const r of livingReqs) {\n for (const op of r.operations) {\n if (defined.has(op)) continue;\n const close = closeIds(op, ops);\n findings.push({\n severity: \"error\",\n code: \"spec-api.op-undefined\",\n subject: service,\n message:\n `${service}: requirement '${r.name}' governs '${op}', not defined in ${service}'s OpenAPI` +\n (close.length > 0 ? `. Did you mean: ${close.join(\", \")}?` : \"\"),\n });\n }\n }\n if (ops.length > 0) {\n const governed = new Set(livingReqs.flatMap((r) => r.operations));\n const orphans = ops.filter((op) => !governed.has(op));\n if (orphans.length === 0) {\n findings.push({\n severity: \"ok\",\n code: \"api.covered\",\n // Both counts on purpose: the ratio is the one signal that lets a\n // reviewer smell a thin baseline from the summary — forty behaviours\n // filed under one requirement passes every check, and \"covered\" alone\n // read as done.\n message: `${service}: API covered (${ops.length} operation(s) governed by requirements · ${livingReqs.length} living requirement(s))`,\n });\n } else {\n findings.push({\n severity: \"warn\",\n code: \"api.ungoverned\",\n message: `${service}: ${orphans.length} operation(s) not governed by any requirement — ${orphans.join(\", \")}`,\n });\n }\n findings.push(...responseGovernanceFindings(service, liveOps, livingReqs));\n // The migration-debt case: requirements exist, the API exists, and no\n // `Operations:` line ties them — every cross-axis check above and in\n // feature mode is vacuously green. Once per service, not per operation;\n // with zero requirements the spec (or its absence) is the finding instead.\n if (reqs.length > 0 && reqs.every((r) => r.operations.length === 0)) {\n findings.push({\n severity: \"warn\",\n code: \"api.ops-unlinked\",\n message: `${service}: openapi.yaml defines ${ops.length} operation(s) but no requirement links any — the API axis is unchecked for this service`,\n });\n }\n // Lifecycle: a requirement whose `Operations:` list resolves ONLY to\n // deprecated operations governs behaviour the contract is retiring.\n // Deprecation is the documented first step of removing an op; the explicit\n // feature marker is the final step. Until that delta archives, the op stays\n // live, so the fix is migration or a coordinated retirement. Ops the contract does not define at all\n // prove nothing here and are left to spec-api.op-undefined above.\n for (const r of reqs) {\n const resolved = r.operations.filter((op) => defined.has(op));\n if (resolved.length === 0 || !resolved.every((op) => deprecatedOps.has(op))) continue;\n findings.push({\n severity: \"warn\",\n code: \"api.requirement-deprecated\",\n message: `${service}: requirement '${r.name}' governs only deprecated operation(s) (${resolved.join(\", \")}) — the behaviour it describes is on its way out; migrate it to the replacement operation, or retire it`,\n });\n }\n }\n }\n\n return {\n findings,\n contract: { ops, deprecated: deprecatedOps, unreadable: api.unreadable || contractMissing },\n };\n}\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` already records one package over.\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 for (const [label, path] of [[\"generated\", generatedPath], [\"committed\", committedPath]] as const) {\n if (label === \"committed\" && !existsSync(path)) return [];\n let parsed: unknown;\n try {\n parsed = parseYaml(await readFile(path, \"utf8\"));\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 }\n if (digests[0] === digests[1]) return [];\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}). ` +\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 }];\n}\n"]}
|
|
1
|
+
{"version":3,"file":"api.js","sourceRoot":"","sources":["../../../../src/commands/validate/service/api.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * The HTTP contract axis: openapi.yaml graded against the living spec.\n *\n * What it decides for everything downstream is whether there is a contract to\n * read at all. A missing or unparseable openapi.yaml is ONE finding about ONE\n * file, and that fact has to travel: grading the fleet map's inbound edges\n * against the empty operation set it leaves behind turns one broken YAML into\n * one `spine.op-undefined` per consumer, each of them accusing the landscape of\n * a fault that is not there. `ServiceContract` is how it travels; `./spine.js`\n * is what reads it.\n */\nimport { existsSync } from \"node:fs\";\nimport { closeIds } from \"../../../core/c4/arch.js\";\nimport { type LoadedDoc } from \"../../../core/c4/likec4.js\";\nimport { type DeclaredService, type PathableService } from \"../../../core/kernel/ids/service.js\";\nimport { type ServicePaths } from \"../../../core/repo/paths.js\";\nimport { type Finding } from \"../../../core/vocabulary/report.js\";\nimport { responseGovernanceFindings } from \"../checks/requirements.js\";\nimport { type Requirement } from \"../../../core/document/spec.js\";\nimport { readOpenapi, type OpenapiDoc } from \"../../../core/openapi/doc.js\";\nimport { FleetContext } from \"../../../core/fleet-context.js\";\nimport { repoPath } from \"../../../core/envelope/json.js\";\nimport { type DocsDir } from \"../../../core/kernel/ids/dirs.js\";\n\n/** What the service target hands the HTTP axis. */\nexport interface ApiAxis {\n /** The docs root, so a message names the contract by its repo-relative path. */\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, REMOVED ones included. */\n reqs: Requirement[];\n /** The ones that still govern something. */\n livingReqs: Requirement[];\n /** The living landscape, or null when the repo has none. */\n land: LoadedDoc | null;\n /** Its element→service resolver, built once per run; null exactly when `land` is. */\n landSvcOf: ((id: string) => DeclaredService) | null;\n fleet: FleetContext;\n}\n\n/**\n * What the spine may conclude from this service's contract.\n *\n * `unreadable` covers both shapes of \"there is nothing to read\" — absent and\n * unparseable — because they have the same consequence for an inbound edge:\n * the contract proves neither that the operation exists nor that it does not.\n */\nexport interface ServiceContract {\n /** operationIds the living contract defines, empty when there is nothing to read. */\n ops: string[];\n /** Those of them the contract marks `deprecated: true`. */\n deprecated: ReadonlySet<string>;\n unreadable: boolean;\n}\n\nexport async function apiAxisFindings(\n axis: ApiAxis,\n): Promise<{ findings: Finding[]; contract: ServiceContract }> {\n const { docsDir, service, me, paths, reqs, livingReqs, land, landSvcOf, fleet } = axis;\n const findings: Finding[] = [];\n\n // API coverage: every operation in openapi.yaml is governed by a requirement.\n const api = await readOpenapi(paths.openapi, fleet);\n const removeMarkers = api.ops.filter((o) => o.remove);\n const liveOps = api.ops.filter((o) => !o.remove);\n /** No contract to read: deleted, renamed, or never written. */\n const contractMissing = !existsSync(paths.openapi);\n const contract = contractOf(api, contractMissing);\n const { ops, deprecated: deprecatedOps } = contract;\n // Inbound calls the landscape can PROVE: op-linked edges whose target\n // resolves to this service. Null when the landscape proves nothing at all\n // (absent, or it did not parse) — which is not the same fact as \"nobody\n // calls me\", and the two must not be graded alike.\n const inboundOps =\n land === null || land.errors.length > 0\n ? null\n : land.relationships.filter((r) => r.op !== undefined && landSvcOf!(r.target) === me);\n // The other half of the evidence a contract is owed: the living spec's own\n // `Operations:` lines. A requirement on its way out governs nothing.\n const governedOps = livingReqs.flatMap((r) => r.operations);\n if (contractMissing) {\n // A contract that is gone is not a reason to stop grading the API axis —\n // it IS the axis's answer, and every check below used to live inside the\n // file-exists branch, so deleting or misspelling the file turned the whole\n // axis (including the documented `spec-api.op-undefined`) green.\n //\n // What decides the severity is whether anything already written down is\n // holding a join into it: a living `Operations:` line, or an op-linked\n // landscape edge. With one of those, the absence breaks a link somebody\n // authored — an error. With neither, this is a service that legitimately\n // has no HTTP surface (a worker nobody calls), and a fleet mid-rollout\n // must not go red for it — a warning, or, when the landscape PROVES nobody\n // calls it, the documented silence.\n //\n // One finding, not one per dangling link: the fix is a single file, and\n // the links it strands ride along as details.\n const dangling = [...new Set([...governedOps, ...(inboundOps ?? []).map((r) => r.op!)])].sort();\n if (dangling.length > 0) {\n findings.push({\n severity: \"error\",\n code: \"service.no-openapi\",\n subject: service,\n message:\n `No OpenAPI contract at ${repoPath(docsDir, paths.openapi)}, and ${dangling.length} operation link(s) already point into it — ` +\n `every requirement and landscape edge naming one of them resolves to nothing until the file is back`,\n details: dangling,\n });\n } else if (inboundOps === null) {\n findings.push({\n severity: \"warn\",\n code: \"service.no-openapi\",\n message: `No OpenAPI contract at ${repoPath(docsDir, paths.openapi)} — API coverage and the landscape spine are unchecked`,\n });\n }\n } else if (api.unreadable) {\n // A contract that EXISTS but does not read is a broken source of truth, not\n // an empty one: swallowing it into zero operations used to grade every\n // inbound landscape edge `spine.op-undefined` — a false diagnosis pointing\n // at the landscape when the truth was this file. So the file is the error,\n // and every check that reads the contract (api.*, the spine's op\n // resolution) is suspended below, the landscape.invalid discipline.\n findings.push({\n severity: \"error\",\n code: \"openapi.invalid\",\n message: `${service}: openapi.yaml does not parse — API coverage and the landscape spine are unchecked`,\n ...(api.error === undefined ? {} : { details: [api.error] }),\n });\n } else {\n if (removeMarkers.length > 0) {\n findings.push({\n severity: \"error\",\n code: \"openapi.remove-marker-living\",\n message: `${service}: living openapi.yaml contains ${removeMarkers.length} x-loam-remove marker(s) (${removeMarkers.map((op) => op.id).join(\", \")}) — removal markers are valid only in feature deltas`,\n });\n }\n // The same marker written at PATH level, beside the methods. `readOpenapi`\n // is keyed by (path, method), which is precisely why the check above cannot\n // see this one and precisely how one reached a living contract in the first\n // place: it addresses no operation, so it retired nothing, and it stayed\n // invisible afterwards. Error, like its method-level sibling — a\n // feature-only key published to every consumer of the fleet, and one that\n // keeps the empty-path cleanup from ever firing. Same code as the archive\n // plan's: one breach, one name, wherever it is found.\n for (const removed of api.pathLevelRemovals) {\n findings.push({\n severity: \"error\",\n code: \"openapi.remove-marker-path-level\",\n subject: service,\n message:\n `${service}: living openapi.yaml carries x-loam-remove at PATH level on '${removed}' — ` +\n `a removal marker names ONE operation, so beside the methods it retires nothing and is not a contract key either, ` +\n `and no id-keyed check can see it. Delete it from the living contract; retire an operation through a feature delta whose marker sits inside the operation.`,\n });\n }\n // Two slots claiming one operationId in a LIVING contract. `readOpenapi`\n // has computed this since the merge needed it, and only the feature path\n // (coherence) ever read it — so the ambiguity was reachable only when some\n // unrelated feature happened to carry a delta for this service, and the\n // fleet gate that actually runs in CI was blind to it. Same code and same\n // sentence as the feature-scope check: one breach, one name.\n for (const id of api.duplicateIds) {\n const slots = api.ops.filter((op) => op.id === id).map((op) => `${op.method} ${op.path}`);\n findings.push({\n severity: \"warn\",\n code: \"openapi.duplicate-operationid\",\n subject: service,\n message: `${service}: the living OpenAPI defines operationId '${id}' at ${slots.join(\" and \")} — every join on the id (a requirement's Operations: line, an edge's metadata { op }, a removal marker) picks one of those slots arbitrarily`,\n });\n }\n // The contract-depth probes. Form validated and depth did not: an\n // operation whose one response is `description: OK`, and a `$ref` to a\n // schema nobody wrote, both read as cleanly as a complete contract while\n // a reader still cannot rebuild its payloads. Presence\n // probes over the document's own keys; nothing reads what a schema says.\n const undescribed = liveOps.filter((op) => op.undescribed === true).map((op) => op.id);\n if (undescribed.length > 0) {\n findings.push({\n severity: \"warn\",\n code: \"openapi.response-undescribed\",\n subject: service,\n message:\n `${service}: ${undescribed.length} operation(s) declare no response schema (${undescribed.join(\", \")}) — ` +\n `the contract names the operation and says nothing about what it returns, so a reader still ` +\n `needs the code; declare content with a schema on at least one response (204/304 excepted)`,\n });\n }\n if (api.danglingRefs.length > 0) {\n findings.push({\n severity: \"warn\",\n code: \"openapi.ref-unresolved\",\n subject: service,\n message:\n `${service}: openapi.yaml contains ${api.danglingRefs.length} internal $ref(s) that resolve to ` +\n `nothing in the document — the operation validates while its schema is silently absent ` +\n `(the archive plan refuses the same breach in a feature delta, as an error)`,\n details: api.danglingRefs,\n });\n }\n const defined = new Set(ops);\n // `Operations:` on a LIVING requirement, resolved against this service's own\n // contract. Nothing did this before: the same spine is checked inside a\n // feature delta (coherence's spec-api.op-undefined) and then never again, so\n // a typo that shipped, or an operation later renamed out of openapi.yaml,\n // left a living requirement governing an operation that does not exist —\n // green forever, and every downstream join through that id silently empty.\n // Same code and severity as the feature-scope check on purpose: one breach,\n // one name, wherever it is found.\n for (const r of livingReqs) {\n for (const op of r.operations) {\n if (defined.has(op)) continue;\n const close = closeIds(op, ops);\n findings.push({\n severity: \"error\",\n code: \"spec-api.op-undefined\",\n subject: service,\n message:\n `${service}: requirement '${r.name}' governs '${op}', not defined in ${service}'s OpenAPI` +\n (close.length > 0 ? `. Did you mean: ${close.join(\", \")}?` : \"\"),\n });\n }\n }\n if (ops.length > 0) {\n const governed = new Set(livingReqs.flatMap((r) => r.operations));\n const orphans = ops.filter((op) => !governed.has(op));\n if (orphans.length === 0) {\n findings.push({\n severity: \"ok\",\n code: \"api.covered\",\n // Both counts on purpose: the ratio is the one signal that lets a\n // reviewer smell a thin baseline from the summary — forty behaviours\n // filed under one requirement passes every check, and \"covered\" alone\n // read as done.\n message: `${service}: API covered (${ops.length} operation(s) governed by requirements · ${livingReqs.length} living requirement(s))`,\n });\n } else {\n findings.push({\n severity: \"warn\",\n code: \"api.ungoverned\",\n message: `${service}: ${orphans.length} operation(s) not governed by any requirement — ${orphans.join(\", \")}`,\n });\n }\n findings.push(...responseGovernanceFindings(service, liveOps, livingReqs));\n // The migration-debt case: requirements exist, the API exists, and no\n // `Operations:` line ties them — every cross-axis check above and in\n // feature mode is vacuously green. Once per service, not per operation;\n // with zero requirements the spec (or its absence) is the finding instead.\n if (reqs.length > 0 && reqs.every((r) => r.operations.length === 0)) {\n findings.push({\n severity: \"warn\",\n code: \"api.ops-unlinked\",\n message: `${service}: openapi.yaml defines ${ops.length} operation(s) but no requirement links any — the API axis is unchecked for this service`,\n });\n }\n // Lifecycle: a requirement whose `Operations:` list resolves ONLY to\n // deprecated operations governs behaviour the contract is retiring.\n // Deprecation is the documented first step of removing an op; the explicit\n // feature marker is the final step. Until that delta archives, the op stays\n // live, so the fix is migration or a coordinated retirement. Ops the contract does not define at all\n // prove nothing here and are left to spec-api.op-undefined above.\n for (const r of reqs) {\n const resolved = r.operations.filter((op) => defined.has(op));\n if (resolved.length === 0 || !resolved.every((op) => deprecatedOps.has(op))) continue;\n findings.push({\n severity: \"warn\",\n code: \"api.requirement-deprecated\",\n message: `${service}: requirement '${r.name}' governs only deprecated operation(s) (${resolved.join(\", \")}) — the behaviour it describes is on its way out; migrate it to the replacement operation, or retire it`,\n });\n }\n }\n }\n\n return { findings, contract };\n}\n\n/**\n * What the spine may conclude from one contract read — spelled once, because a\n * `--base` run now grades a changed edge against a provider it does not grade\n * as a target (`../scope.ts`), and two spellings of \"which operations are\n * live\" would be two chances for a removal marker to count as defined.\n */\nexport function contractOf(api: OpenapiDoc, missing: boolean): ServiceContract {\n const live = api.ops.filter((o) => !o.remove);\n return {\n ops: live.map((o) => o.id),\n deprecated: new Set(live.filter((o) => o.deprecated).map((o) => o.id)),\n unreadable: api.unreadable || missing,\n };\n}\n"]}
|
|
@@ -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"]}
|
|
@@ -15,11 +15,13 @@
|
|
|
15
15
|
import { existsSync } from "node:fs";
|
|
16
16
|
import {} from "../../../../core/kernel/ids/service.js";
|
|
17
17
|
import {} from "../../../../core/c4/likec4.js";
|
|
18
|
+
import { eventKeys, infrastructureKind, unboundSentence } from "../../../../core/c4/direction/direction.js";
|
|
18
19
|
import {} from "../../../../core/repo/paths.js";
|
|
19
20
|
import {} from "../../../../core/vocabulary/report.js";
|
|
20
21
|
import {} from "../../../../core/document/spec.js";
|
|
21
22
|
import { readAsyncapi, slotsOf } from "../../../../core/asyncapi/read.js";
|
|
22
23
|
import { FleetContext } from "../../../../core/fleet-context.js";
|
|
24
|
+
import { repoPath } from "../../../../core/envelope/json.js";
|
|
23
25
|
import { producerFindings } from "./message-producers.js";
|
|
24
26
|
export async function eventAxisFindings(axis) {
|
|
25
27
|
const { docsDir, service, me, paths, reqs, livingReqs, archReqs, land, landSvcOf, known, fleet } = axis;
|
|
@@ -33,20 +35,40 @@ export async function eventAxisFindings(axis) {
|
|
|
33
35
|
const receivedHere = new Set(events.received);
|
|
34
36
|
// Every edge that binds THIS service to a message: `publishes` binds the
|
|
35
37
|
// edge's source (a service produces what it declares an `action: send` for),
|
|
36
|
-
// `consumes` binds its target
|
|
37
|
-
//
|
|
38
|
-
|
|
38
|
+
// `consumes` binds its target — `eventKeys` is where that rule is written,
|
|
39
|
+
// once, for this scope and the feature gate alike. One edge may legitimately
|
|
40
|
+
// carry both: a producer drawn straight to its consumer is a single arrow.
|
|
41
|
+
const readable = land !== null && land.errors.length === 0 && landSvcOf !== null;
|
|
42
|
+
const eventEdges = !readable
|
|
39
43
|
? []
|
|
40
|
-
: land.relationships.flatMap((r) =>
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
+
: land.relationships.flatMap((r) => eventKeys(r)
|
|
45
|
+
.filter((k) => landSvcOf(k.bound) === me)
|
|
46
|
+
.map((k) => ({ direction: k.direction, message: k.message, other: landSvcOf(k.other) })));
|
|
47
|
+
// The edges the filter above drops that THIS service drew the wrong way
|
|
48
|
+
// round: a key bound to infrastructure (a topic, which neither publishes nor
|
|
49
|
+
// consumes) while this service sits at the other end. It used to fall out of
|
|
50
|
+
// `eventEdges` without a word, so a consume edge drawn from the consumer to
|
|
51
|
+
// the topic graded nothing and said nothing. A key bound to an `#external`
|
|
52
|
+
// system is NOT this case — an outside producer or consumer, which the
|
|
53
|
+
// drawing states correctly (core/c4/direction/direction.ts says why). Raised on
|
|
54
|
+
// the service at the unbound end, because it is the one with a drawing to fix.
|
|
55
|
+
const knownIds = known;
|
|
56
|
+
if (readable) {
|
|
57
|
+
const infraKindOf = infrastructureKind(land.elements);
|
|
58
|
+
for (const r of land.relationships) {
|
|
59
|
+
for (const key of eventKeys(r)) {
|
|
60
|
+
const kind = infraKindOf(key.bound);
|
|
61
|
+
if (kind === undefined || landSvcOf(key.other) !== me || knownIds.has(landSvcOf(key.bound)))
|
|
62
|
+
continue;
|
|
63
|
+
findings.push({
|
|
64
|
+
severity: "warn",
|
|
65
|
+
code: "spine.message-unbound",
|
|
66
|
+
subject: service,
|
|
67
|
+
message: `${service}: landscape ${unboundSentence(r, key, kind)}`,
|
|
68
|
+
});
|
|
44
69
|
}
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
}
|
|
48
|
-
return bound;
|
|
49
|
-
});
|
|
70
|
+
}
|
|
71
|
+
}
|
|
50
72
|
// Both requirement namespaces, and a REMOVED requirement links nothing —
|
|
51
73
|
// the `livingReqs` rule the API axis already follows, applied to the pair.
|
|
52
74
|
const eventLinks = [...livingReqs, ...archReqs.filter((r) => r.kind !== "REMOVED")].flatMap((r) => [
|
|
@@ -71,7 +93,7 @@ export async function eventAxisFindings(axis) {
|
|
|
71
93
|
severity: "error",
|
|
72
94
|
code: "service.no-asyncapi",
|
|
73
95
|
subject: service,
|
|
74
|
-
message: `No async contract at ${paths.asyncapi}, and ${dangling.length} message link(s) already point into it — ` +
|
|
96
|
+
message: `No async contract at ${repoPath(docsDir, paths.asyncapi)}, and ${dangling.length} message link(s) already point into it — ` +
|
|
75
97
|
`every landscape edge and requirement naming one of them resolves to nothing until the file is back`,
|
|
76
98
|
details: dangling,
|
|
77
99
|
});
|
|
@@ -86,7 +108,7 @@ export async function eventAxisFindings(axis) {
|
|
|
86
108
|
severity: "error",
|
|
87
109
|
code: "asyncapi.invalid",
|
|
88
110
|
subject: service,
|
|
89
|
-
message: `${service}: asyncapi.yaml does not parse — the event spine is unchecked for this service`,
|
|
111
|
+
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
112
|
...(events.error === undefined ? {} : { details: [events.error] }),
|
|
91
113
|
});
|
|
92
114
|
}
|