@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
|
@@ -8,7 +8,9 @@
|
|
|
8
8
|
* leaves the living docs exactly as they were.
|
|
9
9
|
*/
|
|
10
10
|
import { existsSync } from "node:fs";
|
|
11
|
-
import {
|
|
11
|
+
import { parse } from "yaml";
|
|
12
|
+
import { CodedFailure, repoPath } from "../../../../core/envelope/json.js";
|
|
13
|
+
import { isRecord } from "../../../../core/kernel/records.js";
|
|
12
14
|
import { planWrite, readUtf8 } from "../../../../core/staging/writes.js";
|
|
13
15
|
import { featureSpecPaths } from "../../../../core/repo/paths.js";
|
|
14
16
|
import { locateServicePaths } from "../../../../core/repo/service-target.js";
|
|
@@ -16,11 +18,12 @@ import { readOpenapi } from "../../../../core/openapi/doc.js";
|
|
|
16
18
|
import { OpenapiMergeError } from "../../../../core/openapi/merge/error.js";
|
|
17
19
|
import { stripOpenapiRemovalMarkers } from "../../../../core/openapi/merge/markers.js";
|
|
18
20
|
import { mergeOpenapiPaths } from "../../../../core/openapi/merge/merge.js";
|
|
19
|
-
import { ArchiveFailure } from "../refusal.js";
|
|
20
21
|
import {} from "../state.js";
|
|
22
|
+
import { consumerClause } from "./consumers.js";
|
|
23
|
+
import { infoDrift, sayQuoted, sayStale, warnInfoDrift, warnOverwrite } from "./verdict-lines.js";
|
|
21
24
|
export async function planOpenapiContracts(config, gated, plan, say) {
|
|
22
25
|
const { featureDir, deltaServices } = gated;
|
|
23
|
-
const { writes,
|
|
26
|
+
const { writes, planGates, openapiRemovals } = plan;
|
|
24
27
|
// OpenAPI merge — fold the feature's openapi deltas into the living service APIs.
|
|
25
28
|
for (const svc of deltaServices) {
|
|
26
29
|
const featOpenapi = featureSpecPaths(featureDir, svc).openapi;
|
|
@@ -40,7 +43,7 @@ export async function planOpenapiContracts(config, gated, plan, say) {
|
|
|
40
43
|
// A document loam cannot read is one it cannot merge, and that is the same
|
|
41
44
|
// answer as any other merge it could not compute: nothing is written.
|
|
42
45
|
if (featDoc.unreadable) {
|
|
43
|
-
throw new
|
|
46
|
+
throw new CodedFailure("merge-failed", `feature openapi ${repoPath(config.docsDir, featOpenapi)} for ${svc} does not read as an OpenAPI document (${featDoc.error ?? "not a YAML mapping"}) — the API axis cannot be merged; fix the file, or delete it if this feature has no contract delta`);
|
|
44
47
|
}
|
|
45
48
|
const ops = featDoc.ops.filter((op) => !op.remove).map((op) => op.id);
|
|
46
49
|
// An `x-loam-remove: true` written at PATH level retires nothing: the marker
|
|
@@ -65,20 +68,33 @@ export async function planOpenapiContracts(config, gated, plan, say) {
|
|
|
65
68
|
// "does the reader see a removal" let exactly that marker through into a
|
|
66
69
|
// living contract, published to every consumer of the fleet.
|
|
67
70
|
const content = stripOpenapiRemovalMarkers(featText, svc);
|
|
71
|
+
// A contract that describes nothing is not created, on the spec axis's
|
|
72
|
+
// rule for a delta that leaves no requirements (../specs.ts). A
|
|
73
|
+
// `paths: {}` stub is what an author wrote to quiet a status that used to
|
|
74
|
+
// demand a contract from every new service, and copying it in published
|
|
75
|
+
// "this service exposes nothing" as a living contract the fleet then
|
|
76
|
+
// joins against — announced as `created ()`. With no file, the service
|
|
77
|
+
// reads as one with no HTTP surface, which `service.no-openapi` already
|
|
78
|
+
// grades: silent while nothing calls it, an error once something does.
|
|
79
|
+
if (describesNothing(content)) {
|
|
80
|
+
say(` openapi: ${svc} — nothing to merge (delta defines no paths or components), no living openapi.yaml created`);
|
|
81
|
+
continue;
|
|
82
|
+
}
|
|
68
83
|
writes.push(planWrite(livingOpenapi, content));
|
|
69
84
|
say(` openapi: ${svc} — created (${ops.join(", ")})`);
|
|
70
85
|
}
|
|
71
86
|
else {
|
|
87
|
+
const livingText = await readUtf8(livingOpenapi);
|
|
72
88
|
let merge;
|
|
73
89
|
try {
|
|
74
|
-
merge = mergeOpenapiPaths(
|
|
90
|
+
merge = mergeOpenapiPaths(livingText, featText, svc);
|
|
75
91
|
}
|
|
76
92
|
catch (err) {
|
|
77
93
|
if (err instanceof OpenapiMergeError)
|
|
78
|
-
throw new
|
|
94
|
+
throw new CodedFailure("merge-failed", err.message);
|
|
79
95
|
throw err;
|
|
80
96
|
}
|
|
81
|
-
const { text, modified, pathItemModified, removed, quoted, pathItemQuoted, pathItemStale, componentsModified, componentsAdded, componentsQuoted, componentsStale, unresolved, } = merge;
|
|
97
|
+
const { text, modified, modifiedOperationIds, pathItemModified, removed, quoted, pathItemQuoted, pathItemStale, componentsModified, componentsAdded, componentsQuoted, componentsStale, unresolved, } = merge;
|
|
82
98
|
if (text !== null) {
|
|
83
99
|
writes.push(planWrite(livingOpenapi, text));
|
|
84
100
|
// The parenthesised list names the OPERATIONS the delta merged, and a
|
|
@@ -92,58 +108,46 @@ export async function planOpenapiContracts(config, gated, plan, say) {
|
|
|
92
108
|
const mergedNames = ops.length > 0 ? ops : componentsAdded.map((comp) => `component ${comp}`);
|
|
93
109
|
say(` openapi: ${svc} — merged${mergedNames.length > 0 ? ` (${mergedNames.join(", ")})` : ""}`);
|
|
94
110
|
}
|
|
111
|
+
// Asked whatever the merge wrote, for the event planner's reason: an
|
|
112
|
+
// `info`-only delta merges nothing, and archived in silence.
|
|
113
|
+
warnInfoDrift(plan, say, {
|
|
114
|
+
svc, code: "openapi.info-not-merged", file: "openapi.yaml", carries: "paths and components",
|
|
115
|
+
labels: infoDrift(parse(livingText), parse(featText)),
|
|
116
|
+
});
|
|
95
117
|
if (removed.length > 0) {
|
|
96
118
|
openapiRemovals.push({ service: svc, operations: removed });
|
|
97
119
|
for (const label of removed)
|
|
98
120
|
say(` - removes ${label}`);
|
|
99
121
|
}
|
|
100
|
-
//
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
say(` · quotes ${label} — unchanged since it was pinned, left as living has it`);
|
|
107
|
-
for (const label of pathItemQuoted) {
|
|
108
|
-
say(` · quotes path-level ${label} — unchanged since it was pinned, left as living has it`);
|
|
109
|
-
}
|
|
110
|
-
for (const comp of componentsQuoted) {
|
|
111
|
-
say(` · quotes component ${comp} — unchanged since it was pinned, left as living has it`);
|
|
112
|
-
}
|
|
113
|
-
// Stale surfaces reaching the merge at all means --approve pushed past
|
|
114
|
-
// the gate's openapi.baseline-stale — the plan still names what it cost.
|
|
115
|
-
for (const label of pathItemStale) {
|
|
116
|
-
say(` ⚠ stale baseline on path-level ${label} — the living value moved since this delta was pinned; the overwrite is what --approve chose`);
|
|
117
|
-
}
|
|
118
|
-
for (const comp of componentsStale) {
|
|
119
|
-
say(` ⚠ stale baseline on component ${comp} — the living value moved since this delta was pinned; the overwrite is what --approve chose`);
|
|
120
|
-
}
|
|
122
|
+
// The quote and stale lines (./verdict-lines.ts says why each is said).
|
|
123
|
+
sayQuoted(say, quoted);
|
|
124
|
+
sayQuoted(say, pathItemQuoted.map((label) => `path-level ${label}`));
|
|
125
|
+
sayQuoted(say, componentsQuoted.map((comp) => `component ${comp}`));
|
|
126
|
+
sayStale(say, pathItemStale.map((label) => `path-level ${label}`));
|
|
127
|
+
sayStale(say, componentsStale.map((comp) => `component ${comp}`));
|
|
121
128
|
for (const label of modified) {
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
129
|
+
// Who the overwrite lands on: the living fleet's consumers of the
|
|
130
|
+
// operation, through `loam diff`'s own scan (./consumers.ts). An
|
|
131
|
+
// operation with no operationId on either side has nothing anyone can
|
|
132
|
+
// name it by, so the warning stays the sentence it always was.
|
|
133
|
+
const id = modifiedOperationIds.get(label);
|
|
134
|
+
const named = id === undefined ? null : consumerClause(await config.consumers.op(svc, id));
|
|
135
|
+
warnOverwrite(plan, say, {
|
|
136
|
+
svc, code: "openapi.op-modified", label, named,
|
|
126
137
|
message: `${svc}: the delta redefines ${label}, which the living OpenAPI already has — the merge overwrites the living operation wholesale`,
|
|
127
138
|
});
|
|
128
|
-
say(` ⚠ overwrites ${label} — the living definition differs`);
|
|
129
139
|
}
|
|
130
140
|
for (const label of pathItemModified) {
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
code: "openapi.path-item-modified",
|
|
134
|
-
subject: svc,
|
|
141
|
+
warnOverwrite(plan, say, {
|
|
142
|
+
svc, code: "openapi.path-item-modified", label: `path-level ${label}`,
|
|
135
143
|
message: `${svc}: the delta redefines the path-level key ${label}, which the living OpenAPI already has — the merge overwrites it wholesale, and it applies to EVERY operation on that path, including ones this feature never mentions`,
|
|
136
144
|
});
|
|
137
|
-
say(` ⚠ overwrites path-level ${label} — the living definition differs`);
|
|
138
145
|
}
|
|
139
146
|
for (const comp of componentsModified) {
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
code: "openapi.component-modified",
|
|
143
|
-
subject: svc,
|
|
147
|
+
warnOverwrite(plan, say, {
|
|
148
|
+
svc, code: "openapi.component-modified", label: `component ${comp}`,
|
|
144
149
|
message: `${svc}: the delta declares component '${comp}', which the living OpenAPI already defines differently — the merge overwrites the living component wholesale`,
|
|
145
150
|
});
|
|
146
|
-
say(` ⚠ overwrites component ${comp} — the living definition differs`);
|
|
147
151
|
}
|
|
148
152
|
for (const u of unresolved) {
|
|
149
153
|
planGates.push({
|
|
@@ -156,4 +160,34 @@ export async function planOpenapiContracts(config, gated, plan, say) {
|
|
|
156
160
|
}
|
|
157
161
|
}
|
|
158
162
|
}
|
|
163
|
+
/**
|
|
164
|
+
* Does this contract describe no surface at all — no path, no webhook, and no
|
|
165
|
+
* component in any section? Only the document's frame is left (`openapi`,
|
|
166
|
+
* `info`, `servers`, `tags`), which is a title and nothing anyone can call.
|
|
167
|
+
*
|
|
168
|
+
* Conservative in one direction: anything that is not an empty mapping where a
|
|
169
|
+
* mapping belongs counts as content, so a document this cannot classify is
|
|
170
|
+
* created exactly as before. The reader above has already refused one that
|
|
171
|
+
* does not parse.
|
|
172
|
+
*/
|
|
173
|
+
function describesNothing(text) {
|
|
174
|
+
let doc;
|
|
175
|
+
try {
|
|
176
|
+
doc = parse(text);
|
|
177
|
+
}
|
|
178
|
+
catch {
|
|
179
|
+
// Not a document this can classify, so it is content: created as before.
|
|
180
|
+
return false;
|
|
181
|
+
}
|
|
182
|
+
if (doc === null || doc === undefined)
|
|
183
|
+
return true;
|
|
184
|
+
if (!isRecord(doc))
|
|
185
|
+
return false;
|
|
186
|
+
const components = doc["components"];
|
|
187
|
+
const sections = components === undefined || components === null ? [] : isRecord(components) ? Object.values(components) : [components];
|
|
188
|
+
return [doc["paths"], doc["webhooks"], ...sections].every(isEmptyMapping);
|
|
189
|
+
}
|
|
190
|
+
function isEmptyMapping(value) {
|
|
191
|
+
return value === undefined || value === null || (isRecord(value) && Object.keys(value).length === 0);
|
|
192
|
+
}
|
|
159
193
|
//# sourceMappingURL=openapi.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"openapi.js","sourceRoot":"","sources":["../../../../../src/commands/archive/plan/contracts/openapi.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * The OpenAPI contract merge: the feature's openapi deltas folded into each\n * living service API — the half of the archive plan that answers to the\n * contract axis, split out of ../specs.ts so the requirements merge and the\n * contract merges stop sharing one file.\n *\n * Nothing is written. Everything lands in the `Plan`, so a failure on any axis\n * leaves the living docs exactly as they were.\n */\nimport { existsSync } from \"node:fs\";\nimport { repoPath } from \"../../../../core/envelope/json.js\";\nimport { planWrite, readUtf8 } from \"../../../../core/staging/writes.js\";\nimport { featureSpecPaths } from \"../../../../core/repo/paths.js\";\nimport { locateServicePaths } from \"../../../../core/repo/service-target.js\";\nimport { readOpenapi } from \"../../../../core/openapi/doc.js\";\nimport { OpenapiMergeError } from \"../../../../core/openapi/merge/error.js\";\nimport { stripOpenapiRemovalMarkers } from \"../../../../core/openapi/merge/markers.js\";\nimport { mergeOpenapiPaths, type OpenapiMergeResult } from \"../../../../core/openapi/merge/merge.js\";\nimport { ArchiveFailure } from \"../refusal.js\";\nimport { type Gated, type Plan } from \"../state.js\";\nimport type { DocsDir } from \"../../../../core/kernel/ids/dirs.js\";\nimport type { FleetContext } from \"../../../../core/fleet-context.js\";\n\nexport async function planOpenapiContracts(\n config: { docsDir: DocsDir; fleet?: FleetContext },\n gated: Gated,\n plan: Plan,\n say: (line?: string) => void,\n): Promise<void> {\n const { featureDir, deltaServices } = gated;\n const { writes, planWarns, planGates, openapiRemovals } = plan;\n // OpenAPI merge — fold the feature's openapi deltas into the living service APIs.\n for (const svc of deltaServices) {\n const featOpenapi = featureSpecPaths(featureDir, svc).openapi;\n if (!existsSync(featOpenapi)) continue;\n const featText = await readUtf8(featOpenapi);\n // The shared context: this planner runs once per delta service, and a\n // context-less locate is a full fleet walk per service.\n const livingOpenapi = (await locateServicePaths(config.docsDir, svc, config.fleet)).openapi;\n const featDoc = await readOpenapi(featOpenapi);\n // Every other reader of this flag suspends its own judgement when it is set\n // — validate grades `openapi.invalid`, show and status print that the file\n // does not parse. Archive was the only one that never looked, and it is the\n // command that WRITES: a feature openapi.yaml holding a sequence instead of\n // a mapping was planned verbatim into services/<svc>/openapi.yaml, `ok:\n // true`, printing `created ()` because the reader saw no operations in it.\n // A document loam cannot read is one it cannot merge, and that is the same\n // answer as any other merge it could not compute: nothing is written.\n if (featDoc.unreadable) {\n throw new ArchiveFailure(\n \"merge-failed\",\n `feature openapi ${repoPath(config.docsDir, featOpenapi)} for ${svc} does not read as an OpenAPI document (${featDoc.error ?? \"not a YAML mapping\"}) — the API axis cannot be merged; fix the file, or delete it if this feature has no contract delta`,\n );\n }\n const ops = featDoc.ops.filter((op) => !op.remove).map((op) => op.id);\n // An `x-loam-remove: true` written at PATH level retires nothing: the marker\n // addresses one operation, and beside the methods there is no operation for\n // it to address. The merge now strips it on every branch, so the living\n // contract is safe either way — but the author asked for a removal that will\n // not happen, and silence there is how a retired endpoint stayed live.\n // Gated like the other plan-visible breaches, --approve and all.\n for (const path of featDoc.pathLevelRemovals) {\n planGates.push({\n severity: \"error\",\n code: \"openapi.remove-marker-path-level\",\n subject: svc,\n message: `${svc}: '${path}' carries x-loam-remove at PATH level, beside the methods — a removal marker names ONE operation, so this retires nothing and is not a contract key either. Move it inside the operation you are retiring (with its operationId), or delete it.`,\n });\n }\n if (!existsSync(livingOpenapi)) {\n // A removal against a non-existent contract is gated by coherence. Keep\n // the feature-only marker out of living docs even under --approve — and\n // ask the DOCUMENT, not the operation reader: a marker with no\n // operationId is invisible to `operations()`, so gating the strip on\n // \"does the reader see a removal\" let exactly that marker through into a\n // living contract, published to every consumer of the fleet.\n const content = stripOpenapiRemovalMarkers(featText, svc);\n writes.push(planWrite(livingOpenapi, content));\n say(` openapi: ${svc} — created (${ops.join(\", \")})`);\n } else {\n let merge: OpenapiMergeResult;\n try {\n merge = mergeOpenapiPaths(await readUtf8(livingOpenapi), featText, svc);\n } catch (err) {\n if (err instanceof OpenapiMergeError) throw new ArchiveFailure(\"merge-failed\", err.message);\n throw err;\n }\n const {\n text, modified, pathItemModified, removed, quoted, pathItemQuoted, pathItemStale,\n componentsModified, componentsAdded, componentsQuoted, componentsStale, unresolved,\n } = merge;\n if (text !== null) {\n writes.push(planWrite(livingOpenapi, text));\n // The parenthesised list names the OPERATIONS the delta merged, and a\n // delta whose whole change is a component has none — so this printed\n // `merged ()`, the same empty-parenthesis lie the create branch above\n // is commented for: it reads as \"this merge wrote nothing\" about a\n // merge that just published a shared schema to every consumer of the\n // fleet. Name the components it added instead, and drop the\n // parentheses entirely when there is nothing to put in them — a delta\n // that only EDITED existing surfaces is named by the lines below.\n const mergedNames = ops.length > 0 ? ops : componentsAdded.map((comp) => `component ${comp}`);\n say(` openapi: ${svc} — merged${mergedNames.length > 0 ? ` (${mergedNames.join(\", \")})` : \"\"}`);\n }\n if (removed.length > 0) {\n openapiRemovals.push({ service: svc, operations: removed });\n for (const label of removed) say(` - removes ${label}`);\n }\n // Said out loud, because \"the plan wrote less than my delta spells\" is\n // the one thing a reader cannot infer from a merged file. Not a warning:\n // quoting the contract around your change is correct authoring, and\n // leaving the quote alone is the correct merge. The path-level and\n // component quotes get the same sentence for the same reason.\n for (const label of quoted) say(` · quotes ${label} — unchanged since it was pinned, left as living has it`);\n for (const label of pathItemQuoted) {\n say(` · quotes path-level ${label} — unchanged since it was pinned, left as living has it`);\n }\n for (const comp of componentsQuoted) {\n say(` · quotes component ${comp} — unchanged since it was pinned, left as living has it`);\n }\n // Stale surfaces reaching the merge at all means --approve pushed past\n // the gate's openapi.baseline-stale — the plan still names what it cost.\n for (const label of pathItemStale) {\n say(` ⚠ stale baseline on path-level ${label} — the living value moved since this delta was pinned; the overwrite is what --approve chose`);\n }\n for (const comp of componentsStale) {\n say(` ⚠ stale baseline on component ${comp} — the living value moved since this delta was pinned; the overwrite is what --approve chose`);\n }\n for (const label of modified) {\n planWarns.push({\n severity: \"warn\",\n code: \"openapi.op-modified\",\n subject: svc,\n message: `${svc}: the delta redefines ${label}, which the living OpenAPI already has — the merge overwrites the living operation wholesale`,\n });\n say(` ⚠ overwrites ${label} — the living definition differs`);\n }\n for (const label of pathItemModified) {\n planWarns.push({\n severity: \"warn\",\n code: \"openapi.path-item-modified\",\n subject: svc,\n message: `${svc}: the delta redefines the path-level key ${label}, which the living OpenAPI already has — the merge overwrites it wholesale, and it applies to EVERY operation on that path, including ones this feature never mentions`,\n });\n say(` ⚠ overwrites path-level ${label} — the living definition differs`);\n }\n for (const comp of componentsModified) {\n planWarns.push({\n severity: \"warn\",\n code: \"openapi.component-modified\",\n subject: svc,\n message: `${svc}: the delta declares component '${comp}', which the living OpenAPI already defines differently — the merge overwrites the living component wholesale`,\n });\n say(` ⚠ overwrites component ${comp} — the living definition differs`);\n }\n for (const u of unresolved) {\n planGates.push({\n severity: \"error\",\n code: \"openapi.ref-unresolved\",\n subject: svc,\n message: `${svc}: $ref '${u.ref}' (referenced from ${u.from}) resolves in neither the feature's openapi.yaml nor the living one — the merged document would carry a dangling reference`,\n });\n }\n }\n }\n}\n"]}
|
|
1
|
+
{"version":3,"file":"openapi.js","sourceRoot":"","sources":["../../../../../src/commands/archive/plan/contracts/openapi.ts"],"names":[],"mappings":"AAAA;;;;;;;;GAQG;AACH,OAAO,EAAE,UAAU,EAAE,MAAM,SAAS,CAAC;AACrC,OAAO,EAAE,KAAK,EAAE,MAAM,MAAM,CAAC;AAC7B,OAAO,EAAE,YAAY,EAAE,QAAQ,EAAE,MAAM,mCAAmC,CAAC;AAC3E,OAAO,EAAE,QAAQ,EAAE,MAAM,oCAAoC,CAAC;AAC9D,OAAO,EAAE,SAAS,EAAE,QAAQ,EAAE,MAAM,oCAAoC,CAAC;AACzE,OAAO,EAAE,gBAAgB,EAAE,MAAM,gCAAgC,CAAC;AAClE,OAAO,EAAE,kBAAkB,EAAE,MAAM,yCAAyC,CAAC;AAC7E,OAAO,EAAE,WAAW,EAAE,MAAM,iCAAiC,CAAC;AAC9D,OAAO,EAAE,iBAAiB,EAAE,MAAM,yCAAyC,CAAC;AAC5E,OAAO,EAAE,0BAA0B,EAAE,MAAM,2CAA2C,CAAC;AACvF,OAAO,EAAE,iBAAiB,EAA2B,MAAM,yCAAyC,CAAC;AACrG,OAAO,EAAyB,MAAM,aAAa,CAAC;AACpD,OAAO,EAAE,cAAc,EAA0B,MAAM,gBAAgB,CAAC;AACxE,OAAO,EAAE,SAAS,EAAE,SAAS,EAAE,QAAQ,EAAE,aAAa,EAAE,aAAa,EAAE,MAAM,oBAAoB,CAAC;AAIlG,MAAM,CAAC,KAAK,UAAU,oBAAoB,CACxC,MAA+E,EAC/E,KAAY,EACZ,IAAU,EACV,GAA4B;IAE5B,MAAM,EAAE,UAAU,EAAE,aAAa,EAAE,GAAG,KAAK,CAAC;IAC5C,MAAM,EAAE,MAAM,EAAE,SAAS,EAAE,eAAe,EAAE,GAAG,IAAI,CAAC;IACpD,kFAAkF;IAClF,KAAK,MAAM,GAAG,IAAI,aAAa,EAAE,CAAC;QAChC,MAAM,WAAW,GAAG,gBAAgB,CAAC,UAAU,EAAE,GAAG,CAAC,CAAC,OAAO,CAAC;QAC9D,IAAI,CAAC,UAAU,CAAC,WAAW,CAAC;YAAE,SAAS;QACvC,MAAM,QAAQ,GAAG,MAAM,QAAQ,CAAC,WAAW,CAAC,CAAC;QAC7C,sEAAsE;QACtE,wDAAwD;QACxD,MAAM,aAAa,GAAG,CAAC,MAAM,kBAAkB,CAAC,MAAM,CAAC,OAAO,EAAE,GAAG,EAAE,MAAM,CAAC,KAAK,CAAC,CAAC,CAAC,OAAO,CAAC;QAC5F,MAAM,OAAO,GAAG,MAAM,WAAW,CAAC,WAAW,CAAC,CAAC;QAC/C,4EAA4E;QAC5E,2EAA2E;QAC3E,4EAA4E;QAC5E,4EAA4E;QAC5E,wEAAwE;QACxE,2EAA2E;QAC3E,2EAA2E;QAC3E,sEAAsE;QACtE,IAAI,OAAO,CAAC,UAAU,EAAE,CAAC;YACvB,MAAM,IAAI,YAAY,CACpB,cAAc,EACd,mBAAmB,QAAQ,CAAC,MAAM,CAAC,OAAO,EAAE,WAAW,CAAC,QAAQ,GAAG,0CAA0C,OAAO,CAAC,KAAK,IAAI,oBAAoB,qGAAqG,CACxP,CAAC;QACJ,CAAC;QACD,MAAM,GAAG,GAAG,OAAO,CAAC,GAAG,CAAC,MAAM,CAAC,CAAC,EAAE,EAAE,EAAE,CAAC,CAAC,EAAE,CAAC,MAAM,CAAC,CAAC,GAAG,CAAC,CAAC,EAAE,EAAE,EAAE,CAAC,EAAE,CAAC,EAAE,CAAC,CAAC;QACtE,6EAA6E;QAC7E,4EAA4E;QAC5E,wEAAwE;QACxE,6EAA6E;QAC7E,uEAAuE;QACvE,iEAAiE;QACjE,KAAK,MAAM,IAAI,IAAI,OAAO,CAAC,iBAAiB,EAAE,CAAC;YAC7C,SAAS,CAAC,IAAI,CAAC;gBACb,QAAQ,EAAE,OAAO;gBACjB,IAAI,EAAE,kCAAkC;gBACxC,OAAO,EAAE,GAAG;gBACZ,OAAO,EAAE,GAAG,GAAG,MAAM,IAAI,iPAAiP;aAC3Q,CAAC,CAAC;QACL,CAAC;QACD,IAAI,CAAC,UAAU,CAAC,aAAa,CAAC,EAAE,CAAC;YAC/B,wEAAwE;YACxE,wEAAwE;YACxE,+DAA+D;YAC/D,qEAAqE;YACrE,yEAAyE;YACzE,6DAA6D;YAC7D,MAAM,OAAO,GAAG,0BAA0B,CAAC,QAAQ,EAAE,GAAG,CAAC,CAAC;YAC1D,uEAAuE;YACvE,gEAAgE;YAChE,0EAA0E;YAC1E,wEAAwE;YACxE,qEAAqE;YACrE,uEAAuE;YACvE,wEAAwE;YACxE,uEAAuE;YACvE,IAAI,gBAAgB,CAAC,OAAO,CAAC,EAAE,CAAC;gBAC9B,GAAG,CAAC,cAAc,GAAG,4FAA4F,CAAC,CAAC;gBACnH,SAAS;YACX,CAAC;YACD,MAAM,CAAC,IAAI,CAAC,SAAS,CAAC,aAAa,EAAE,OAAO,CAAC,CAAC,CAAC;YAC/C,GAAG,CAAC,cAAc,GAAG,eAAe,GAAG,CAAC,IAAI,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC;QACzD,CAAC;aAAM,CAAC;YACN,MAAM,UAAU,GAAG,MAAM,QAAQ,CAAC,aAAa,CAAC,CAAC;YACjD,IAAI,KAAyB,CAAC;YAC9B,IAAI,CAAC;gBACH,KAAK,GAAG,iBAAiB,CAAC,UAAU,EAAE,QAAQ,EAAE,GAAG,CAAC,CAAC;YACvD,CAAC;YAAC,OAAO,GAAG,EAAE,CAAC;gBACb,IAAI,GAAG,YAAY,iBAAiB;oBAAE,MAAM,IAAI,YAAY,CAAC,cAAc,EAAE,GAAG,CAAC,OAAO,CAAC,CAAC;gBAC1F,MAAM,GAAG,CAAC;YACZ,CAAC;YACD,MAAM,EACJ,IAAI,EAAE,QAAQ,EAAE,oBAAoB,EAAE,gBAAgB,EAAE,OAAO,EAAE,MAAM,EAAE,cAAc,EAAE,aAAa,EACtG,kBAAkB,EAAE,eAAe,EAAE,gBAAgB,EAAE,eAAe,EAAE,UAAU,GACnF,GAAG,KAAK,CAAC;YACV,IAAI,IAAI,KAAK,IAAI,EAAE,CAAC;gBAClB,MAAM,CAAC,IAAI,CAAC,SAAS,CAAC,aAAa,EAAE,IAAI,CAAC,CAAC,CAAC;gBAC5C,sEAAsE;gBACtE,qEAAqE;gBACrE,sEAAsE;gBACtE,mEAAmE;gBACnE,qEAAqE;gBACrE,4DAA4D;gBAC5D,sEAAsE;gBACtE,kEAAkE;gBAClE,MAAM,WAAW,GAAG,GAAG,CAAC,MAAM,GAAG,CAAC,CAAC,CAAC,CAAC,GAAG,CAAC,CAAC,CAAC,eAAe,CAAC,GAAG,CAAC,CAAC,IAAI,EAAE,EAAE,CAAC,aAAa,IAAI,EAAE,CAAC,CAAC;gBAC9F,GAAG,CAAC,cAAc,GAAG,YAAY,WAAW,CAAC,MAAM,GAAG,CAAC,CAAC,CAAC,CAAC,KAAK,WAAW,CAAC,IAAI,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC;YACnG,CAAC;YACD,qEAAqE;YACrE,6DAA6D;YAC7D,aAAa,CAAC,IAAI,EAAE,GAAG,EAAE;gBACvB,GAAG,EAAE,IAAI,EAAE,yBAAyB,EAAE,IAAI,EAAE,cAAc,EAAE,OAAO,EAAE,sBAAsB;gBAC3F,MAAM,EAAE,SAAS,CAAC,KAAK,CAAC,UAAU,CAAC,EAAE,KAAK,CAAC,QAAQ,CAAC,CAAC;aACtD,CAAC,CAAC;YACH,IAAI,OAAO,CAAC,MAAM,GAAG,CAAC,EAAE,CAAC;gBACvB,eAAe,CAAC,IAAI,CAAC,EAAE,OAAO,EAAE,GAAG,EAAE,UAAU,EAAE,OAAO,EAAE,CAAC,CAAC;gBAC5D,KAAK,MAAM,KAAK,IAAI,OAAO;oBAAE,GAAG,CAAC,mBAAmB,KAAK,EAAE,CAAC,CAAC;YAC/D,CAAC;YACD,wEAAwE;YACxE,SAAS,CAAC,GAAG,EAAE,MAAM,CAAC,CAAC;YACvB,SAAS,CAAC,GAAG,EAAE,cAAc,CAAC,GAAG,CAAC,CAAC,KAAK,EAAE,EAAE,CAAC,cAAc,KAAK,EAAE,CAAC,CAAC,CAAC;YACrE,SAAS,CAAC,GAAG,EAAE,gBAAgB,CAAC,GAAG,CAAC,CAAC,IAAI,EAAE,EAAE,CAAC,aAAa,IAAI,EAAE,CAAC,CAAC,CAAC;YACpE,QAAQ,CAAC,GAAG,EAAE,aAAa,CAAC,GAAG,CAAC,CAAC,KAAK,EAAE,EAAE,CAAC,cAAc,KAAK,EAAE,CAAC,CAAC,CAAC;YACnE,QAAQ,CAAC,GAAG,EAAE,eAAe,CAAC,GAAG,CAAC,CAAC,IAAI,EAAE,EAAE,CAAC,aAAa,IAAI,EAAE,CAAC,CAAC,CAAC;YAClE,KAAK,MAAM,KAAK,IAAI,QAAQ,EAAE,CAAC;gBAC7B,kEAAkE;gBAClE,iEAAiE;gBACjE,sEAAsE;gBACtE,+DAA+D;gBAC/D,MAAM,EAAE,GAAG,oBAAoB,CAAC,GAAG,CAAC,KAAK,CAAC,CAAC;gBAC3C,MAAM,KAAK,GAAG,EAAE,KAAK,SAAS,CAAC,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC,cAAc,CAAC,MAAM,MAAM,CAAC,SAAS,CAAC,EAAE,CAAC,GAAG,EAAE,EAAE,CAAC,CAAC,CAAC;gBAC3F,aAAa,CAAC,IAAI,EAAE,GAAG,EAAE;oBACvB,GAAG,EAAE,IAAI,EAAE,qBAAqB,EAAE,KAAK,EAAE,KAAK;oBAC9C,OAAO,EAAE,GAAG,GAAG,yBAAyB,KAAK,8FAA8F;iBAC5I,CAAC,CAAC;YACL,CAAC;YACD,KAAK,MAAM,KAAK,IAAI,gBAAgB,EAAE,CAAC;gBACrC,aAAa,CAAC,IAAI,EAAE,GAAG,EAAE;oBACvB,GAAG,EAAE,IAAI,EAAE,4BAA4B,EAAE,KAAK,EAAE,cAAc,KAAK,EAAE;oBACrE,OAAO,EAAE,GAAG,GAAG,4CAA4C,KAAK,wKAAwK;iBACzO,CAAC,CAAC;YACL,CAAC;YACD,KAAK,MAAM,IAAI,IAAI,kBAAkB,EAAE,CAAC;gBACtC,aAAa,CAAC,IAAI,EAAE,GAAG,EAAE;oBACvB,GAAG,EAAE,IAAI,EAAE,4BAA4B,EAAE,KAAK,EAAE,aAAa,IAAI,EAAE;oBACnE,OAAO,EAAE,GAAG,GAAG,mCAAmC,IAAI,+GAA+G;iBACtK,CAAC,CAAC;YACL,CAAC;YACD,KAAK,MAAM,CAAC,IAAI,UAAU,EAAE,CAAC;gBAC3B,SAAS,CAAC,IAAI,CAAC;oBACb,QAAQ,EAAE,OAAO;oBACjB,IAAI,EAAE,wBAAwB;oBAC9B,OAAO,EAAE,GAAG;oBACZ,OAAO,EAAE,GAAG,GAAG,WAAW,CAAC,CAAC,GAAG,sBAAsB,CAAC,CAAC,IAAI,4HAA4H;iBACxL,CAAC,CAAC;YACL,CAAC;QACH,CAAC;IACH,CAAC;AACH,CAAC;AAED;;;;;;;;;GASG;AACH,SAAS,gBAAgB,CAAC,IAAY;IACpC,IAAI,GAAY,CAAC;IACjB,IAAI,CAAC;QACH,GAAG,GAAG,KAAK,CAAC,IAAI,CAAC,CAAC;IACpB,CAAC;IAAC,MAAM,CAAC;QACP,yEAAyE;QACzE,OAAO,KAAK,CAAC;IACf,CAAC;IACD,IAAI,GAAG,KAAK,IAAI,IAAI,GAAG,KAAK,SAAS;QAAE,OAAO,IAAI,CAAC;IACnD,IAAI,CAAC,QAAQ,CAAC,GAAG,CAAC;QAAE,OAAO,KAAK,CAAC;IACjC,MAAM,UAAU,GAAG,GAAG,CAAC,YAAY,CAAC,CAAC;IACrC,MAAM,QAAQ,GAAG,UAAU,KAAK,SAAS,IAAI,UAAU,KAAK,IAAI,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,QAAQ,CAAC,UAAU,CAAC,CAAC,CAAC,CAAC,MAAM,CAAC,MAAM,CAAC,UAAU,CAAC,CAAC,CAAC,CAAC,CAAC,UAAU,CAAC,CAAC;IACxI,OAAO,CAAC,GAAG,CAAC,OAAO,CAAC,EAAE,GAAG,CAAC,UAAU,CAAC,EAAE,GAAG,QAAQ,CAAC,CAAC,KAAK,CAAC,cAAc,CAAC,CAAC;AAC5E,CAAC;AAED,SAAS,cAAc,CAAC,KAAc;IACpC,OAAO,KAAK,KAAK,SAAS,IAAI,KAAK,KAAK,IAAI,IAAI,CAAC,QAAQ,CAAC,KAAK,CAAC,IAAI,MAAM,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC,MAAM,KAAK,CAAC,CAAC,CAAC;AACvG,CAAC","sourcesContent":["/**\n * The OpenAPI contract merge: the feature's openapi deltas folded into each\n * living service API — the half of the archive plan that answers to the\n * contract axis, split out of ../specs.ts so the requirements merge and the\n * contract merges stop sharing one file.\n *\n * Nothing is written. Everything lands in the `Plan`, so a failure on any axis\n * leaves the living docs exactly as they were.\n */\nimport { existsSync } from \"node:fs\";\nimport { parse } from \"yaml\";\nimport { CodedFailure, repoPath } from \"../../../../core/envelope/json.js\";\nimport { isRecord } from \"../../../../core/kernel/records.js\";\nimport { planWrite, readUtf8 } from \"../../../../core/staging/writes.js\";\nimport { featureSpecPaths } from \"../../../../core/repo/paths.js\";\nimport { locateServicePaths } from \"../../../../core/repo/service-target.js\";\nimport { readOpenapi } from \"../../../../core/openapi/doc.js\";\nimport { OpenapiMergeError } from \"../../../../core/openapi/merge/error.js\";\nimport { stripOpenapiRemovalMarkers } from \"../../../../core/openapi/merge/markers.js\";\nimport { mergeOpenapiPaths, type OpenapiMergeResult } from \"../../../../core/openapi/merge/merge.js\";\nimport { type Gated, type Plan } from \"../state.js\";\nimport { consumerClause, type ContractConsumers } from \"./consumers.js\";\nimport { infoDrift, sayQuoted, sayStale, warnInfoDrift, warnOverwrite } from \"./verdict-lines.js\";\nimport type { DocsDir } from \"../../../../core/kernel/ids/dirs.js\";\nimport type { FleetContext } from \"../../../../core/fleet-context.js\";\n\nexport async function planOpenapiContracts(\n config: { docsDir: DocsDir; fleet: FleetContext; consumers: ContractConsumers },\n gated: Gated,\n plan: Plan,\n say: (line?: string) => void,\n): Promise<void> {\n const { featureDir, deltaServices } = gated;\n const { writes, planGates, openapiRemovals } = plan;\n // OpenAPI merge — fold the feature's openapi deltas into the living service APIs.\n for (const svc of deltaServices) {\n const featOpenapi = featureSpecPaths(featureDir, svc).openapi;\n if (!existsSync(featOpenapi)) continue;\n const featText = await readUtf8(featOpenapi);\n // The shared context: this planner runs once per delta service, and a\n // context-less locate is a full fleet walk per service.\n const livingOpenapi = (await locateServicePaths(config.docsDir, svc, config.fleet)).openapi;\n const featDoc = await readOpenapi(featOpenapi);\n // Every other reader of this flag suspends its own judgement when it is set\n // — validate grades `openapi.invalid`, show and status print that the file\n // does not parse. Archive was the only one that never looked, and it is the\n // command that WRITES: a feature openapi.yaml holding a sequence instead of\n // a mapping was planned verbatim into services/<svc>/openapi.yaml, `ok:\n // true`, printing `created ()` because the reader saw no operations in it.\n // A document loam cannot read is one it cannot merge, and that is the same\n // answer as any other merge it could not compute: nothing is written.\n if (featDoc.unreadable) {\n throw new CodedFailure(\n \"merge-failed\",\n `feature openapi ${repoPath(config.docsDir, featOpenapi)} for ${svc} does not read as an OpenAPI document (${featDoc.error ?? \"not a YAML mapping\"}) — the API axis cannot be merged; fix the file, or delete it if this feature has no contract delta`,\n );\n }\n const ops = featDoc.ops.filter((op) => !op.remove).map((op) => op.id);\n // An `x-loam-remove: true` written at PATH level retires nothing: the marker\n // addresses one operation, and beside the methods there is no operation for\n // it to address. The merge now strips it on every branch, so the living\n // contract is safe either way — but the author asked for a removal that will\n // not happen, and silence there is how a retired endpoint stayed live.\n // Gated like the other plan-visible breaches, --approve and all.\n for (const path of featDoc.pathLevelRemovals) {\n planGates.push({\n severity: \"error\",\n code: \"openapi.remove-marker-path-level\",\n subject: svc,\n message: `${svc}: '${path}' carries x-loam-remove at PATH level, beside the methods — a removal marker names ONE operation, so this retires nothing and is not a contract key either. Move it inside the operation you are retiring (with its operationId), or delete it.`,\n });\n }\n if (!existsSync(livingOpenapi)) {\n // A removal against a non-existent contract is gated by coherence. Keep\n // the feature-only marker out of living docs even under --approve — and\n // ask the DOCUMENT, not the operation reader: a marker with no\n // operationId is invisible to `operations()`, so gating the strip on\n // \"does the reader see a removal\" let exactly that marker through into a\n // living contract, published to every consumer of the fleet.\n const content = stripOpenapiRemovalMarkers(featText, svc);\n // A contract that describes nothing is not created, on the spec axis's\n // rule for a delta that leaves no requirements (../specs.ts). A\n // `paths: {}` stub is what an author wrote to quiet a status that used to\n // demand a contract from every new service, and copying it in published\n // \"this service exposes nothing\" as a living contract the fleet then\n // joins against — announced as `created ()`. With no file, the service\n // reads as one with no HTTP surface, which `service.no-openapi` already\n // grades: silent while nothing calls it, an error once something does.\n if (describesNothing(content)) {\n say(` openapi: ${svc} — nothing to merge (delta defines no paths or components), no living openapi.yaml created`);\n continue;\n }\n writes.push(planWrite(livingOpenapi, content));\n say(` openapi: ${svc} — created (${ops.join(\", \")})`);\n } else {\n const livingText = await readUtf8(livingOpenapi);\n let merge: OpenapiMergeResult;\n try {\n merge = mergeOpenapiPaths(livingText, featText, svc);\n } catch (err) {\n if (err instanceof OpenapiMergeError) throw new CodedFailure(\"merge-failed\", err.message);\n throw err;\n }\n const {\n text, modified, modifiedOperationIds, pathItemModified, removed, quoted, pathItemQuoted, pathItemStale,\n componentsModified, componentsAdded, componentsQuoted, componentsStale, unresolved,\n } = merge;\n if (text !== null) {\n writes.push(planWrite(livingOpenapi, text));\n // The parenthesised list names the OPERATIONS the delta merged, and a\n // delta whose whole change is a component has none — so this printed\n // `merged ()`, the same empty-parenthesis lie the create branch above\n // is commented for: it reads as \"this merge wrote nothing\" about a\n // merge that just published a shared schema to every consumer of the\n // fleet. Name the components it added instead, and drop the\n // parentheses entirely when there is nothing to put in them — a delta\n // that only EDITED existing surfaces is named by the lines below.\n const mergedNames = ops.length > 0 ? ops : componentsAdded.map((comp) => `component ${comp}`);\n say(` openapi: ${svc} — merged${mergedNames.length > 0 ? ` (${mergedNames.join(\", \")})` : \"\"}`);\n }\n // Asked whatever the merge wrote, for the event planner's reason: an\n // `info`-only delta merges nothing, and archived in silence.\n warnInfoDrift(plan, say, {\n svc, code: \"openapi.info-not-merged\", file: \"openapi.yaml\", carries: \"paths and components\",\n labels: infoDrift(parse(livingText), parse(featText)),\n });\n if (removed.length > 0) {\n openapiRemovals.push({ service: svc, operations: removed });\n for (const label of removed) say(` - removes ${label}`);\n }\n // The quote and stale lines (./verdict-lines.ts says why each is said).\n sayQuoted(say, quoted);\n sayQuoted(say, pathItemQuoted.map((label) => `path-level ${label}`));\n sayQuoted(say, componentsQuoted.map((comp) => `component ${comp}`));\n sayStale(say, pathItemStale.map((label) => `path-level ${label}`));\n sayStale(say, componentsStale.map((comp) => `component ${comp}`));\n for (const label of modified) {\n // Who the overwrite lands on: the living fleet's consumers of the\n // operation, through `loam diff`'s own scan (./consumers.ts). An\n // operation with no operationId on either side has nothing anyone can\n // name it by, so the warning stays the sentence it always was.\n const id = modifiedOperationIds.get(label);\n const named = id === undefined ? null : consumerClause(await config.consumers.op(svc, id));\n warnOverwrite(plan, say, {\n svc, code: \"openapi.op-modified\", label, named,\n message: `${svc}: the delta redefines ${label}, which the living OpenAPI already has — the merge overwrites the living operation wholesale`,\n });\n }\n for (const label of pathItemModified) {\n warnOverwrite(plan, say, {\n svc, code: \"openapi.path-item-modified\", label: `path-level ${label}`,\n message: `${svc}: the delta redefines the path-level key ${label}, which the living OpenAPI already has — the merge overwrites it wholesale, and it applies to EVERY operation on that path, including ones this feature never mentions`,\n });\n }\n for (const comp of componentsModified) {\n warnOverwrite(plan, say, {\n svc, code: \"openapi.component-modified\", label: `component ${comp}`,\n message: `${svc}: the delta declares component '${comp}', which the living OpenAPI already defines differently — the merge overwrites the living component wholesale`,\n });\n }\n for (const u of unresolved) {\n planGates.push({\n severity: \"error\",\n code: \"openapi.ref-unresolved\",\n subject: svc,\n message: `${svc}: $ref '${u.ref}' (referenced from ${u.from}) resolves in neither the feature's openapi.yaml nor the living one — the merged document would carry a dangling reference`,\n });\n }\n }\n }\n}\n\n/**\n * Does this contract describe no surface at all — no path, no webhook, and no\n * component in any section? Only the document's frame is left (`openapi`,\n * `info`, `servers`, `tags`), which is a title and nothing anyone can call.\n *\n * Conservative in one direction: anything that is not an empty mapping where a\n * mapping belongs counts as content, so a document this cannot classify is\n * created exactly as before. The reader above has already refused one that\n * does not parse.\n */\nfunction describesNothing(text: string): boolean {\n let doc: unknown;\n try {\n doc = parse(text);\n } catch {\n // Not a document this can classify, so it is content: created as before.\n return false;\n }\n if (doc === null || doc === undefined) return true;\n if (!isRecord(doc)) return false;\n const components = doc[\"components\"];\n const sections = components === undefined || components === null ? [] : isRecord(components) ? Object.values(components) : [components];\n return [doc[\"paths\"], doc[\"webhooks\"], ...sections].every(isEmptyMapping);\n}\n\nfunction isEmptyMapping(value: unknown): boolean {\n return value === undefined || value === null || (isRecord(value) && Object.keys(value).length === 0);\n}\n"]}
|
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The plan lines a contract merge's verdicts print — the quote, the stale
|
|
3
|
+
* baseline, the overwrite and the `info` the merge does not carry — spelled
|
|
4
|
+
* once for both planners
|
|
5
|
+
* (./openapi.ts, ./asyncapi.ts), which used to print them in fourteen parallel
|
|
6
|
+
* loops. Every surface reaches the reader by exactly the same sentence an
|
|
7
|
+
* operation or a slot does, because it is graded by exactly the same verdict;
|
|
8
|
+
* one spelling is what keeps the two axes from describing one verdict in two
|
|
9
|
+
* sentences. The label prefix (`path-level …`, `component …`, `components.…`)
|
|
10
|
+
* stays at the call site: it is the one part that differs.
|
|
11
|
+
*/
|
|
12
|
+
import { isDeepStrictEqual } from "node:util";
|
|
13
|
+
import { withoutFeatureKeysDeep } from "../../../../core/openapi/digest.js";
|
|
14
|
+
import { isRecord } from "../../../../core/kernel/records.js";
|
|
15
|
+
import { compareIds } from "../../../../core/repo/entries.js";
|
|
16
|
+
/**
|
|
17
|
+
* Said out loud, because "the plan wrote less than my delta spells" is the one
|
|
18
|
+
* thing a reader cannot infer from a merged file. Not a warning: quoting the
|
|
19
|
+
* contract around your change is correct authoring, and leaving the quote
|
|
20
|
+
* alone is the correct merge. The path-level and component quotes get the
|
|
21
|
+
* same sentence for the same reason.
|
|
22
|
+
*/
|
|
23
|
+
export function sayQuoted(say, labels) {
|
|
24
|
+
for (const label of labels)
|
|
25
|
+
say(` · quotes ${label} — unchanged since it was pinned, left as living has it`);
|
|
26
|
+
}
|
|
27
|
+
/**
|
|
28
|
+
* Stale surfaces reaching the merge at all means --approve pushed past the
|
|
29
|
+
* gate's `openapi.baseline-stale` / `asyncapi.baseline-stale` — the plan still
|
|
30
|
+
* names what it cost.
|
|
31
|
+
*/
|
|
32
|
+
export function sayStale(say, labels) {
|
|
33
|
+
for (const label of labels) {
|
|
34
|
+
say(` ⚠ stale baseline on ${label} — the living value moved since this delta was pinned; the overwrite is what --approve chose`);
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
/**
|
|
38
|
+
* Warn about one overwrite and print it. The consumer clause, when there is
|
|
39
|
+
* one, ends both the warning's message and the printed line, and its evidence
|
|
40
|
+
* rides as the warning's `details` and as indented lines under it.
|
|
41
|
+
*/
|
|
42
|
+
export function warnOverwrite(plan, say, overwrite) {
|
|
43
|
+
const named = overwrite.named ?? null;
|
|
44
|
+
const clause = named === null ? "" : `; ${named.clause}`;
|
|
45
|
+
plan.planWarns.push({
|
|
46
|
+
severity: "warn",
|
|
47
|
+
code: overwrite.code,
|
|
48
|
+
subject: overwrite.svc,
|
|
49
|
+
message: overwrite.message + clause,
|
|
50
|
+
...(named === null ? {} : { details: named.details }),
|
|
51
|
+
});
|
|
52
|
+
say(` ⚠ overwrites ${overwrite.label} — the living definition differs${clause}`);
|
|
53
|
+
for (const line of named?.details ?? [])
|
|
54
|
+
say(` ${line}`);
|
|
55
|
+
}
|
|
56
|
+
/**
|
|
57
|
+
* The `info` keys a feature's contract spells differently from the living one,
|
|
58
|
+
* as `info.<key>` labels, sorted — empty when the delta writes no `info`, or
|
|
59
|
+
* writes exactly what living has.
|
|
60
|
+
*
|
|
61
|
+
* Neither merge carries `info` (nor any root key outside the sections it
|
|
62
|
+
* merges: `../../../../core/asyncapi/merge/markers.ts` names what those are),
|
|
63
|
+
* and that is a decision rather than a gap — a delta is a diff of operations
|
|
64
|
+
* and slots, and its `info` is most often the scaffold's or a copy. But a
|
|
65
|
+
* delta that DID move it — a new version, a rewritten description — lost the
|
|
66
|
+
* change with exit 0 and nothing in the plan, so the reader learned it from
|
|
67
|
+
* the living file weeks later. Only keys the delta declares are compared: a
|
|
68
|
+
* key living has and the delta leaves out is a key nobody asked to change.
|
|
69
|
+
* Feature bookkeeping is never compared: an `x-loam-*` key of `info` itself is
|
|
70
|
+
* skipped, and the merges' own markers are stripped from the values below it
|
|
71
|
+
* (`withoutFeatureKeysDeep`), so a pin can never read as drift.
|
|
72
|
+
*/
|
|
73
|
+
export function infoDrift(livingPlain, featurePlain) {
|
|
74
|
+
const feature = isRecord(featurePlain) ? featurePlain.info : undefined;
|
|
75
|
+
if (!isRecord(feature))
|
|
76
|
+
return [];
|
|
77
|
+
const living = isRecord(livingPlain) && isRecord(livingPlain.info) ? livingPlain.info : {};
|
|
78
|
+
return Object.keys(feature)
|
|
79
|
+
.filter((key) => !key.startsWith("x-loam-"))
|
|
80
|
+
.filter((key) => !isDeepStrictEqual(withoutFeatureKeysDeep(feature[key]), living[key]))
|
|
81
|
+
.map((key) => `info.${key}`)
|
|
82
|
+
.sort(compareIds);
|
|
83
|
+
}
|
|
84
|
+
/**
|
|
85
|
+
* Warn about one contract's `info` drift and print it. A warning, not a gate:
|
|
86
|
+
* keeping the living `info` is the merge working as designed, and the remedy —
|
|
87
|
+
* edit the living file in the same change — is the author's call. What must
|
|
88
|
+
* not happen is the silence.
|
|
89
|
+
*/
|
|
90
|
+
export function warnInfoDrift(plan, say, drift) {
|
|
91
|
+
if (drift.labels.length === 0)
|
|
92
|
+
return;
|
|
93
|
+
const which = drift.labels.join(", ");
|
|
94
|
+
plan.planWarns.push({
|
|
95
|
+
severity: "warn",
|
|
96
|
+
code: drift.code,
|
|
97
|
+
subject: drift.svc,
|
|
98
|
+
message: `${drift.svc}: the delta's ${which} differ(s) from the living ${drift.file} — the merge carries ${drift.carries} and never ` +
|
|
99
|
+
`info, so the living contract keeps its own. If it should move, edit the living ${drift.file} in this change.`,
|
|
100
|
+
});
|
|
101
|
+
say(` ⚠ info not merged — the delta's ${which} differ(s) from the living ${drift.file}, which keeps its own`);
|
|
102
|
+
}
|
|
103
|
+
//# sourceMappingURL=verdict-lines.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"verdict-lines.js","sourceRoot":"","sources":["../../../../../src/commands/archive/plan/contracts/verdict-lines.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * The plan lines a contract merge's verdicts print — the quote, the stale\n * baseline, the overwrite and the `info` the merge does not carry — spelled\n * once for both planners\n * (./openapi.ts, ./asyncapi.ts), which used to print them in fourteen parallel\n * loops. Every surface reaches the reader by exactly the same sentence an\n * operation or a slot does, because it is graded by exactly the same verdict;\n * one spelling is what keeps the two axes from describing one verdict in two\n * sentences. The label prefix (`path-level …`, `component …`, `components.…`)\n * stays at the call site: it is the one part that differs.\n */\nimport { isDeepStrictEqual } from \"node:util\";\nimport { withoutFeatureKeysDeep } from \"../../../../core/openapi/digest.js\";\nimport { isRecord } from \"../../../../core/kernel/records.js\";\nimport { compareIds } from \"../../../../core/repo/entries.js\";\nimport type { IssueCode } from \"../../../../core/vocabulary/issue.js\";\nimport type { Plan } from \"../state.js\";\n\ntype Say = (line?: string) => void;\n\n/**\n * Said out loud, because \"the plan wrote less than my delta spells\" is the one\n * thing a reader cannot infer from a merged file. Not a warning: quoting the\n * contract around your change is correct authoring, and leaving the quote\n * alone is the correct merge. The path-level and component quotes get the\n * same sentence for the same reason.\n */\nexport function sayQuoted(say: Say, labels: readonly string[]): void {\n for (const label of labels) say(` · quotes ${label} — unchanged since it was pinned, left as living has it`);\n}\n\n/**\n * Stale surfaces reaching the merge at all means --approve pushed past the\n * gate's `openapi.baseline-stale` / `asyncapi.baseline-stale` — the plan still\n * names what it cost.\n */\nexport function sayStale(say: Say, labels: readonly string[]): void {\n for (const label of labels) {\n say(` ⚠ stale baseline on ${label} — the living value moved since this delta was pinned; the overwrite is what --approve chose`);\n }\n}\n\n/** One overwrite the plan warns about. The code is spelled literally at each call site. */\nexport interface Overwrite {\n svc: string;\n code: IssueCode;\n /** What the printed line names: the label with its prefix. */\n label: string;\n /** The warning's sentence, before any consumer clause. */\n message: string;\n /** Who the overwrite lands on (./consumers.ts), or absent where nothing can name it. */\n named?: { clause: string; details: string[] } | null;\n}\n\n/**\n * Warn about one overwrite and print it. The consumer clause, when there is\n * one, ends both the warning's message and the printed line, and its evidence\n * rides as the warning's `details` and as indented lines under it.\n */\nexport function warnOverwrite(plan: Pick<Plan, \"planWarns\">, say: Say, overwrite: Overwrite): void {\n const named = overwrite.named ?? null;\n const clause = named === null ? \"\" : `; ${named.clause}`;\n plan.planWarns.push({\n severity: \"warn\",\n code: overwrite.code,\n subject: overwrite.svc,\n message: overwrite.message + clause,\n ...(named === null ? {} : { details: named.details }),\n });\n say(` ⚠ overwrites ${overwrite.label} — the living definition differs${clause}`);\n for (const line of named?.details ?? []) say(` ${line}`);\n}\n\n/**\n * The `info` keys a feature's contract spells differently from the living one,\n * as `info.<key>` labels, sorted — empty when the delta writes no `info`, or\n * writes exactly what living has.\n *\n * Neither merge carries `info` (nor any root key outside the sections it\n * merges: `../../../../core/asyncapi/merge/markers.ts` names what those are),\n * and that is a decision rather than a gap — a delta is a diff of operations\n * and slots, and its `info` is most often the scaffold's or a copy. But a\n * delta that DID move it — a new version, a rewritten description — lost the\n * change with exit 0 and nothing in the plan, so the reader learned it from\n * the living file weeks later. Only keys the delta declares are compared: a\n * key living has and the delta leaves out is a key nobody asked to change.\n * Feature bookkeeping is never compared: an `x-loam-*` key of `info` itself is\n * skipped, and the merges' own markers are stripped from the values below it\n * (`withoutFeatureKeysDeep`), so a pin can never read as drift.\n */\nexport function infoDrift(livingPlain: unknown, featurePlain: unknown): string[] {\n const feature = isRecord(featurePlain) ? featurePlain.info : undefined;\n if (!isRecord(feature)) return [];\n const living = isRecord(livingPlain) && isRecord(livingPlain.info) ? livingPlain.info : {};\n return Object.keys(feature)\n .filter((key) => !key.startsWith(\"x-loam-\"))\n .filter((key) => !isDeepStrictEqual(withoutFeatureKeysDeep(feature[key]), living[key]))\n .map((key) => `info.${key}`)\n .sort(compareIds);\n}\n\n/** Which contract the drift is about, the code it is reported under, and what the merge carries instead. */\nexport interface InfoDrift {\n svc: string;\n code: IssueCode;\n /** `openapi.yaml` or `asyncapi.yaml` — the living file the author edits instead. */\n file: string;\n /** What the merge DOES carry, in the words the warning prints. */\n carries: string;\n labels: readonly string[];\n}\n\n/**\n * Warn about one contract's `info` drift and print it. A warning, not a gate:\n * keeping the living `info` is the merge working as designed, and the remedy —\n * edit the living file in the same change — is the author's call. What must\n * not happen is the silence.\n */\nexport function warnInfoDrift(plan: Pick<Plan, \"planWarns\">, say: Say, drift: InfoDrift): void {\n if (drift.labels.length === 0) return;\n const which = drift.labels.join(\", \");\n plan.planWarns.push({\n severity: \"warn\",\n code: drift.code,\n subject: drift.svc,\n message:\n `${drift.svc}: the delta's ${which} differ(s) from the living ${drift.file} — the merge carries ${drift.carries} and never ` +\n `info, so the living contract keeps its own. If it should move, edit the living ${drift.file} in this change.`,\n });\n say(` ⚠ info not merged — the delta's ${which} differ(s) from the living ${drift.file}, which keeps its own`);\n}\n"]}
|
|
@@ -0,0 +1,151 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* What a living document that `loam archive` CREATES says above
|
|
3
|
+
* `## Requirements` — the part of a new service spec, architecture spec or
|
|
4
|
+
* capability document that the requirement algebra has nothing to say about.
|
|
5
|
+
*
|
|
6
|
+
* THE DEFECT THIS CLOSES. A created document used to be a fixed header —
|
|
7
|
+
* `service`, `status: draft`, `# <id>` — and the requirements. Everything else
|
|
8
|
+
* the delta said was dropped in silence: its title, the narrative a reader
|
|
9
|
+
* needs before the first requirement, the frontmatter `owner`, any section
|
|
10
|
+
* written after the last delta section. The requirements arrived byte for byte
|
|
11
|
+
* and the document arrived unreadable to anyone who had not read the delta.
|
|
12
|
+
*
|
|
13
|
+
* WHY NOT CARRY THE WHOLE PREAMBLE. "loam merges requirements, never prose"
|
|
14
|
+
* (SCHEMA) is not an aversion to prose; it is the observation that a delta's
|
|
15
|
+
* prose is ABOUT THE CHANGE. The scaffold's own H1 is "<svc> — requirement
|
|
16
|
+
* delta for <FEAT>" and its intro tells the reviewer what a delta is; carrying
|
|
17
|
+
* either into the living document would make it describe a pull request that
|
|
18
|
+
* merged months ago. A machine cannot tell narrative about the service from
|
|
19
|
+
* narrative about the change, so loam carries only what the author MARKED as
|
|
20
|
+
* the document's own, and says where the rest is:
|
|
21
|
+
*
|
|
22
|
+
* - `owner` from the delta's frontmatter — the one field that describes the
|
|
23
|
+
* service rather than the change or a vouch (copying `vouched_by` or a digest
|
|
24
|
+
* would forge a review nobody performed). A capability document carries no
|
|
25
|
+
* frontmatter by decision (`planCapabilities` says why), so it gets none.
|
|
26
|
+
* - the delta's H1, unless it is about the change — it names the feature or
|
|
27
|
+
* calls itself a delta, which is exactly what the scaffold writes.
|
|
28
|
+
* - the delta's `## Purpose` section, verbatim — the explicit marker, and the
|
|
29
|
+
* one OpenSpec's living specs already use for the document's own narrative.
|
|
30
|
+
* - a back-link to the archived delta, always, so nothing the merge left behind
|
|
31
|
+
* is unreachable from the document it created.
|
|
32
|
+
*
|
|
33
|
+
* Only on CREATION. A living document that already exists keeps its own
|
|
34
|
+
* narrative, edited in place; a `## Purpose` in a delta against it merges as
|
|
35
|
+
* nothing, and `planSpecs` says so rather than letting it vanish.
|
|
36
|
+
*/
|
|
37
|
+
import { dirname, relative, sep } from "node:path";
|
|
38
|
+
import { stringify as stringifyYaml } from "yaml";
|
|
39
|
+
import { parseFrontmatter, stringField } from "../../../../core/document/frontmatter.js";
|
|
40
|
+
import { serializeRequirements } from "../../../../core/document/spec.js";
|
|
41
|
+
import { repoPath } from "../../../../core/envelope/json.js";
|
|
42
|
+
import { fenceTracker } from "../../../../core/kernel/fences.js";
|
|
43
|
+
/** A level-1 heading line. */
|
|
44
|
+
const H1_RE = /^#\s+(.+?)\s*$/;
|
|
45
|
+
/** The marked section — matched on the whole trimmed line, case-insensitively, like `## Requirements`. */
|
|
46
|
+
const PURPOSE_RE = /^##\s+Purpose\s*$/i;
|
|
47
|
+
/** Where a section ends: the next H1 or H2. Deeper headings are the section's own. */
|
|
48
|
+
const SECTION_END_RE = /^#{1,2}\s/;
|
|
49
|
+
/**
|
|
50
|
+
* The created document's whole text, and the one line that tells the person
|
|
51
|
+
* reading the plan what came across from the delta and where the rest is.
|
|
52
|
+
*/
|
|
53
|
+
export function createdDocument(request) {
|
|
54
|
+
const archived = request.retarget(request.deltaPath);
|
|
55
|
+
// Percent-escaped spaces, as `rebaseLinks` writes a route it computed: a bare
|
|
56
|
+
// space would end the link target at the first word.
|
|
57
|
+
const backLink = relative(dirname(request.livingPath), archived).split(sep).join("/").split(" ").join("%20");
|
|
58
|
+
const head = createdHead({ ...request, backLink });
|
|
59
|
+
const where = repoPath(request.docsDir, archived);
|
|
60
|
+
const note = head.carried.length > 0
|
|
61
|
+
? `carried from the delta: ${head.carried.join(", ")} — the rest of its prose stays in ${where}, linked from the new document`
|
|
62
|
+
: `the delta's prose stays in ${where}, linked from the new document — mark what belongs here '## Purpose' and archive carries it`;
|
|
63
|
+
return { text: `${head.text}## Requirements\n\n${serializeRequirements(request.requirements)}`, note };
|
|
64
|
+
}
|
|
65
|
+
/** The head of a document archive creates — see the module comment for what goes in it and why. */
|
|
66
|
+
function createdHead(request) {
|
|
67
|
+
const fm = parseFrontmatter(request.delta);
|
|
68
|
+
const marked = markedParts(fm.body, request.featureId);
|
|
69
|
+
const owner = request.service === null ? undefined : stringField(fm, "owner");
|
|
70
|
+
const carried = [];
|
|
71
|
+
let text = "";
|
|
72
|
+
if (request.service !== null) {
|
|
73
|
+
const fields = {
|
|
74
|
+
service: request.service,
|
|
75
|
+
status: "draft",
|
|
76
|
+
...(request.adoption === undefined ? {} : { adoption: request.adoption }),
|
|
77
|
+
...(owner === undefined ? {} : { owner }),
|
|
78
|
+
};
|
|
79
|
+
text += `---\n${stringifyYaml(fields, { lineWidth: 0 })}---\n\n`;
|
|
80
|
+
if (owner !== undefined)
|
|
81
|
+
carried.push("owner");
|
|
82
|
+
}
|
|
83
|
+
text += `# ${marked.title ?? request.fallbackTitle}\n\n`;
|
|
84
|
+
if (marked.title !== undefined)
|
|
85
|
+
carried.push("its title");
|
|
86
|
+
text += `Created when ${request.featureId} was archived; [its delta](${request.backLink}) keeps the prose written for that change.\n\n`;
|
|
87
|
+
if (marked.purpose !== undefined) {
|
|
88
|
+
text += `## Purpose\n\n${marked.purpose}\n\n`;
|
|
89
|
+
carried.push("its ## Purpose");
|
|
90
|
+
}
|
|
91
|
+
return { text, carried };
|
|
92
|
+
}
|
|
93
|
+
/** Does the delta carry a `## Purpose` section at all — the question a merge into an EXISTING document asks. */
|
|
94
|
+
export function hasPurpose(delta) {
|
|
95
|
+
return markedParts(parseFrontmatter(delta).body, "").purpose !== undefined;
|
|
96
|
+
}
|
|
97
|
+
/**
|
|
98
|
+
* Is this H1 the CHANGE's title rather than the document's? Two mechanical
|
|
99
|
+
* signals, both in words loam owns: it names the feature, or it calls itself a
|
|
100
|
+
* delta — which no living document does, and which every delta title loam
|
|
101
|
+
* scaffolds does both of ("<svc> — requirement delta for <FEAT>"). Anything
|
|
102
|
+
* else is a title an author chose for the thing itself, the shape a domain
|
|
103
|
+
* written into a feature in one go arrives in.
|
|
104
|
+
*/
|
|
105
|
+
function namesTheChange(title, featureId) {
|
|
106
|
+
const lower = title.toLowerCase();
|
|
107
|
+
return (featureId !== "" && lower.includes(featureId.toLowerCase())) || /\bdelta\b/.test(lower);
|
|
108
|
+
}
|
|
109
|
+
/**
|
|
110
|
+
* The delta's title and `## Purpose` body, each only when the author wrote one
|
|
111
|
+
* that belongs to the living document. Fence-aware, like every heading walk in
|
|
112
|
+
* loam: a heading inside a fenced block is quoted text, and a fenced block
|
|
113
|
+
* inside the Purpose is part of it.
|
|
114
|
+
*/
|
|
115
|
+
function markedParts(body, featureId) {
|
|
116
|
+
const fenced = fenceTracker();
|
|
117
|
+
let title;
|
|
118
|
+
let seenSection = false;
|
|
119
|
+
let purpose = null;
|
|
120
|
+
let purposeEnded = false;
|
|
121
|
+
for (const line of body.split(/\r?\n/)) {
|
|
122
|
+
const quoted = fenced(line);
|
|
123
|
+
if (purpose !== null && !purposeEnded) {
|
|
124
|
+
if (quoted || !SECTION_END_RE.test(line)) {
|
|
125
|
+
purpose.push(line);
|
|
126
|
+
continue;
|
|
127
|
+
}
|
|
128
|
+
purposeEnded = true;
|
|
129
|
+
}
|
|
130
|
+
if (quoted)
|
|
131
|
+
continue;
|
|
132
|
+
const h1 = H1_RE.exec(line);
|
|
133
|
+
// The document's title is an H1 above its first section; one further down
|
|
134
|
+
// is a heading inside the text, not the name of the document.
|
|
135
|
+
if (h1 !== null && !seenSection && title === undefined)
|
|
136
|
+
title = h1[1];
|
|
137
|
+
if (/^##\s/.test(line)) {
|
|
138
|
+
seenSection = true;
|
|
139
|
+
if (purpose === null && PURPOSE_RE.test(line.trim()))
|
|
140
|
+
purpose = [];
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
// Blank LINES trimmed, never leading spaces: an indented code block opening
|
|
144
|
+
// the section is the author's text.
|
|
145
|
+
const purposeText = purpose?.join("\n").replace(/^(?:[ \t]*\n)+/, "").trimEnd();
|
|
146
|
+
return {
|
|
147
|
+
...(title === undefined || namesTheChange(title, featureId) ? {} : { title }),
|
|
148
|
+
...(purposeText === undefined || purposeText === "" ? {} : { purpose: purposeText }),
|
|
149
|
+
};
|
|
150
|
+
}
|
|
151
|
+
//# sourceMappingURL=head.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"head.js","sourceRoot":"","sources":["../../../../../src/commands/archive/plan/document/head.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * What a living document that `loam archive` CREATES says above\n * `## Requirements` — the part of a new service spec, architecture spec or\n * capability document that the requirement algebra has nothing to say about.\n *\n * THE DEFECT THIS CLOSES. A created document used to be a fixed header —\n * `service`, `status: draft`, `# <id>` — and the requirements. Everything else\n * the delta said was dropped in silence: its title, the narrative a reader\n * needs before the first requirement, the frontmatter `owner`, any section\n * written after the last delta section. The requirements arrived byte for byte\n * and the document arrived unreadable to anyone who had not read the delta.\n *\n * WHY NOT CARRY THE WHOLE PREAMBLE. \"loam merges requirements, never prose\"\n * (SCHEMA) is not an aversion to prose; it is the observation that a delta's\n * prose is ABOUT THE CHANGE. The scaffold's own H1 is \"<svc> — requirement\n * delta for <FEAT>\" and its intro tells the reviewer what a delta is; carrying\n * either into the living document would make it describe a pull request that\n * merged months ago. A machine cannot tell narrative about the service from\n * narrative about the change, so loam carries only what the author MARKED as\n * the document's own, and says where the rest is:\n *\n * - `owner` from the delta's frontmatter — the one field that describes the\n * service rather than the change or a vouch (copying `vouched_by` or a digest\n * would forge a review nobody performed). A capability document carries no\n * frontmatter by decision (`planCapabilities` says why), so it gets none.\n * - the delta's H1, unless it is about the change — it names the feature or\n * calls itself a delta, which is exactly what the scaffold writes.\n * - the delta's `## Purpose` section, verbatim — the explicit marker, and the\n * one OpenSpec's living specs already use for the document's own narrative.\n * - a back-link to the archived delta, always, so nothing the merge left behind\n * is unreachable from the document it created.\n *\n * Only on CREATION. A living document that already exists keeps its own\n * narrative, edited in place; a `## Purpose` in a delta against it merges as\n * nothing, and `planSpecs` says so rather than letting it vanish.\n */\nimport { dirname, relative, sep } from \"node:path\";\nimport { stringify as stringifyYaml } from \"yaml\";\nimport { parseFrontmatter, stringField } from \"../../../../core/document/frontmatter.js\";\nimport { serializeRequirements, type Requirement } from \"../../../../core/document/spec.js\";\nimport { repoPath } from \"../../../../core/envelope/json.js\";\nimport { fenceTracker } from \"../../../../core/kernel/fences.js\";\nimport type { DocsDir } from \"../../../../core/kernel/ids/dirs.js\";\nimport type { Retarget } from \"./links.js\";\n\n/** A level-1 heading line. */\nconst H1_RE = /^#\\s+(.+?)\\s*$/;\n/** The marked section — matched on the whole trimmed line, case-insensitively, like `## Requirements`. */\nconst PURPOSE_RE = /^##\\s+Purpose\\s*$/i;\n/** Where a section ends: the next H1 or H2. Deeper headings are the section's own. */\nconst SECTION_END_RE = /^#{1,2}\\s/;\n\n/** One living document this archive creates, and the delta it is created from. */\nexport interface CreatedRequest {\n docsDir: DocsDir;\n featureId: string;\n /** The delta's path, where it sits BEFORE the archive moves the feature. */\n deltaPath: string;\n /** The delta's text, its links already re-expressed for `livingPath`'s directory. */\n delta: string;\n livingPath: string;\n /** Where each feature file will be after the archive — the back-link's target comes from it. */\n retarget: Retarget;\n /** The heading written when the delta carries no title of its own. */\n fallbackTitle: string;\n /** The `service:` the created frontmatter names; null for a document that carries no frontmatter. */\n service: string | null;\n /**\n * `partial` when the document is a service's FIRST spec.md and the service\n * existed before this feature — so what lands is the feature's slice and\n * not the service. Written to the frontmatter as `adoption: partial`\n * (SCHEMA.md), which the maturity ladder and `loam status` read as \"not\n * adopted yet\" until a person deletes the line.\n */\n adoption?: \"partial\";\n requirements: Requirement[];\n}\n\n/**\n * The created document's whole text, and the one line that tells the person\n * reading the plan what came across from the delta and where the rest is.\n */\nexport function createdDocument(request: CreatedRequest): { text: string; note: string } {\n const archived = request.retarget(request.deltaPath);\n // Percent-escaped spaces, as `rebaseLinks` writes a route it computed: a bare\n // space would end the link target at the first word.\n const backLink = relative(dirname(request.livingPath), archived).split(sep).join(\"/\").split(\" \").join(\"%20\");\n const head = createdHead({ ...request, backLink });\n const where = repoPath(request.docsDir, archived);\n const note =\n head.carried.length > 0\n ? `carried from the delta: ${head.carried.join(\", \")} — the rest of its prose stays in ${where}, linked from the new document`\n : `the delta's prose stays in ${where}, linked from the new document — mark what belongs here '## Purpose' and archive carries it`;\n return { text: `${head.text}## Requirements\\n\\n${serializeRequirements(request.requirements)}`, note };\n}\n\ninterface HeadRequest {\n delta: string;\n featureId: string;\n fallbackTitle: string;\n service: string | null;\n adoption?: \"partial\";\n /** The created document's relative link to the archived delta. */\n backLink: string;\n}\n\ninterface CreatedHead {\n /** Everything above `## Requirements`, ending in a blank line. */\n text: string;\n /** What the delta contributed, in the words the per-file note prints. */\n carried: string[];\n}\n\n/** The head of a document archive creates — see the module comment for what goes in it and why. */\nfunction createdHead(request: HeadRequest): CreatedHead {\n const fm = parseFrontmatter(request.delta);\n const marked = markedParts(fm.body, request.featureId);\n const owner = request.service === null ? undefined : stringField(fm, \"owner\");\n const carried: string[] = [];\n let text = \"\";\n if (request.service !== null) {\n const fields = {\n service: request.service,\n status: \"draft\",\n ...(request.adoption === undefined ? {} : { adoption: request.adoption }),\n ...(owner === undefined ? {} : { owner }),\n };\n text += `---\\n${stringifyYaml(fields, { lineWidth: 0 })}---\\n\\n`;\n if (owner !== undefined) carried.push(\"owner\");\n }\n text += `# ${marked.title ?? request.fallbackTitle}\\n\\n`;\n if (marked.title !== undefined) carried.push(\"its title\");\n text += `Created when ${request.featureId} was archived; [its delta](${request.backLink}) keeps the prose written for that change.\\n\\n`;\n if (marked.purpose !== undefined) {\n text += `## Purpose\\n\\n${marked.purpose}\\n\\n`;\n carried.push(\"its ## Purpose\");\n }\n return { text, carried };\n}\n\n/** Does the delta carry a `## Purpose` section at all — the question a merge into an EXISTING document asks. */\nexport function hasPurpose(delta: string): boolean {\n return markedParts(parseFrontmatter(delta).body, \"\").purpose !== undefined;\n}\n\n/**\n * Is this H1 the CHANGE's title rather than the document's? Two mechanical\n * signals, both in words loam owns: it names the feature, or it calls itself a\n * delta — which no living document does, and which every delta title loam\n * scaffolds does both of (\"<svc> — requirement delta for <FEAT>\"). Anything\n * else is a title an author chose for the thing itself, the shape a domain\n * written into a feature in one go arrives in.\n */\nfunction namesTheChange(title: string, featureId: string): boolean {\n const lower = title.toLowerCase();\n return (featureId !== \"\" && lower.includes(featureId.toLowerCase())) || /\\bdelta\\b/.test(lower);\n}\n\n/**\n * The delta's title and `## Purpose` body, each only when the author wrote one\n * that belongs to the living document. Fence-aware, like every heading walk in\n * loam: a heading inside a fenced block is quoted text, and a fenced block\n * inside the Purpose is part of it.\n */\nfunction markedParts(body: string, featureId: string): { title?: string; purpose?: string } {\n const fenced = fenceTracker();\n let title: string | undefined;\n let seenSection = false;\n let purpose: string[] | null = null;\n let purposeEnded = false;\n for (const line of body.split(/\\r?\\n/)) {\n const quoted = fenced(line);\n if (purpose !== null && !purposeEnded) {\n if (quoted || !SECTION_END_RE.test(line)) {\n purpose.push(line);\n continue;\n }\n purposeEnded = true;\n }\n if (quoted) continue;\n const h1 = H1_RE.exec(line);\n // The document's title is an H1 above its first section; one further down\n // is a heading inside the text, not the name of the document.\n if (h1 !== null && !seenSection && title === undefined) title = h1[1]!;\n if (/^##\\s/.test(line)) {\n seenSection = true;\n if (purpose === null && PURPOSE_RE.test(line.trim())) purpose = [];\n }\n }\n // Blank LINES trimmed, never leading spaces: an indented code block opening\n // the section is the author's text.\n const purposeText = purpose?.join(\"\\n\").replace(/^(?:[ \\t]*\\n)+/, \"\").trimEnd();\n return {\n ...(title === undefined || namesTheChange(title, featureId) ? {} : { title }),\n ...(purposeText === undefined || purposeText === \"\" ? {} : { purpose: purposeText }),\n };\n}\n"]}
|