@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
package/SCHEMA.md
CHANGED
|
@@ -62,6 +62,7 @@ docs/
|
|
|
62
62
|
architecture/
|
|
63
63
|
landscape.likec4 global C4 (system landscape)
|
|
64
64
|
subsystems.likec4 one view per subsystem, views only [GENERATED: loam subsystem sync]
|
|
65
|
+
services.likec4 one overview view per service, views only [GENERATED: loam subsystem sync]
|
|
65
66
|
permissions.yaml system-wide authorization vocabulary [authored, opt-in]
|
|
66
67
|
capabilities.yaml system-wide capability vocabulary [authored, opt-in]
|
|
67
68
|
obligations.yaml system-wide architectural obligations [authored, opt-in]
|
|
@@ -71,11 +72,13 @@ docs/
|
|
|
71
72
|
spec.md narrative, then `## Requirements` — `Requirement-ID:` REQUIRED
|
|
72
73
|
glossary/<term>.md the system's domain vocabulary, one file per term; nesting allowed [authored, opt-in]
|
|
73
74
|
services/<svc>/ at the root (unfiled), or inside subsystem directories at any depth
|
|
74
|
-
model.likec4 boundary C4 (containers/components) [adopt]
|
|
75
|
+
model.likec4 boundary C4 (containers/components); extends the map, or stands alone [adopt]
|
|
76
|
+
likec4.config.json one LikeC4 project per STANDALONE service model, create-only [GENERATED: loam subsystem sync]
|
|
75
77
|
spec.md living requirements (Requirement/Scenario) [adopt]
|
|
76
78
|
arch.spec.md living ARCHITECTURE requirements (outbox, retries, alerts; Covers:) [authored]
|
|
77
79
|
openapi.yaml API contract [adopt / authored]
|
|
78
80
|
asyncapi.yaml async contract (AsyncAPI 3) [adopt / authored]
|
|
81
|
+
usecases/<name>.likec4 optional flows INSIDE this boundary — dynamic views over its containers, graded when tagged #req- (any .likec4 beside model.likec4 is read the same way) [authored, opt-in]
|
|
79
82
|
adrs/NNNN-*.md MADR decisions [adopt-seed / authored]
|
|
80
83
|
runbook.md operational runbook [adopt-draft / authored]
|
|
81
84
|
health.yaml SLI/SLO, checks, critical deps [authored]
|
|
@@ -86,24 +89,57 @@ docs/
|
|
|
86
89
|
delta.likec4 C4 delta (architecture) [authored]
|
|
87
90
|
specs/<svc>/spec.md requirement delta (ADDED/MODIFIED/REMOVED + scenarios) [authored]
|
|
88
91
|
specs/<svc>/arch.spec.md architectural requirement delta, same algebra [authored]
|
|
89
|
-
specs/<svc>/openapi.yaml the endpoints
|
|
90
|
-
specs/<svc>/asyncapi.yaml the event
|
|
92
|
+
specs/<svc>/openapi.yaml only the endpoints it adds/changes/removes [authored]
|
|
93
|
+
specs/<svc>/asyncapi.yaml only the event slots it adds/changes/removes [authored]
|
|
91
94
|
capabilities/<cap>/spec.md capability delta, same requirement grammar [authored]
|
|
92
95
|
glossary/<term>.md a word this feature brings with it; create-only [authored]
|
|
93
96
|
usecases/<name>.likec4 a FLOW this feature brings; views-only, create-only [authored]
|
|
94
|
-
adrs/NNNN-*.md feature-level decisions
|
|
95
|
-
verification
|
|
97
|
+
adrs/NNNN-*.md feature-level decisions; they stay with the feature when it archives [authored]
|
|
98
|
+
verification/<svc>.yaml the done-check, one file per attesting service: claims + verdicts + evidence [loam verify --service <id> --record]
|
|
99
|
+
verification.yaml the same answered all at once, and every record from before the directory — read forever [loam verify --record]
|
|
96
100
|
features/archive/<FEAT>/ a shipped change — the same files, plus:
|
|
97
|
-
.loam-before/ the bytes the merge overwrote
|
|
101
|
+
.loam-before/ the bytes the merge overwrote, each as files/<path>.before, and the checklist it shipped with [written by loam]
|
|
98
102
|
.loam-lock transient: one writer at a time [archive/unarchive only]
|
|
99
103
|
.loam-commit transient: the commit intent journal [archive/unarchive only]
|
|
100
104
|
```
|
|
101
105
|
|
|
102
106
|
There is no `flows/` tree, and there will not be one. A cross-service **interaction flow** is a
|
|
103
107
|
`dynamic view` under `architecture/` — the one place that already holds every cross-service edge —
|
|
104
|
-
tagged with the capability it realizes
|
|
105
|
-
service's directory, and the view's steps join to the
|
|
106
|
-
makes the flow checkable rather than merely drawn.
|
|
108
|
+
tagged with the capability it realizes, or with the suites it runs in (below). A flow that spans
|
|
109
|
+
services does not belong under one service's directory, and the view's steps join to the
|
|
110
|
+
landscape's own relationships, which is what makes the flow checkable rather than merely drawn.
|
|
111
|
+
|
|
112
|
+
A flow that stays INSIDE one service — the hop sequence behind an arch.spec.md requirement whose
|
|
113
|
+
`Covers:` names containers — cannot live under `architecture/`: loam parses that directory as its
|
|
114
|
+
OWN document set — the map plus its siblings, and nothing under `services/` — so a step naming a
|
|
115
|
+
container does not resolve there, and one such file takes the whole fleet map down
|
|
116
|
+
(`landscape.invalid` plus one `spine.landscape-invalid` per service). That holds however the models
|
|
117
|
+
are shaped: the renderer's root project may also hold every extending model and draw such a step
|
|
118
|
+
perfectly, and loam still grades the map alone, because the map is the document every cross-service
|
|
119
|
+
check reads. It lives beside
|
|
120
|
+
the model instead: every `.likec4` under `services/<…>/<svc>/` other than model.likec4, plus any
|
|
121
|
+
tagged `dynamic view` inside model.likec4 itself, is that service's own LikeC4 project — for an
|
|
122
|
+
extending model, the map plus those documents; for a standalone one, the set
|
|
123
|
+
`services/<svc>/likec4.config.json` registers for the renderer — and `loam validate --service <id>`
|
|
124
|
+
(and `--all`, per service) reads exactly that set, whether or not the project file exists.
|
|
125
|
+
`usecases/<name>.likec4` is the convention; `views.likec4` reads the same. Tag the view
|
|
126
|
+
`#req-<Requirement-ID>` of a requirement in that service's spec.md or arch.spec.md. The tag has to
|
|
127
|
+
be DECLARED somewhere the project can see: for a standalone model that is model.likec4's own
|
|
128
|
+
`specification` — the one block that project has; for an extending model it is
|
|
129
|
+
`architecture/landscape.likec4`'s block, or a tags-only `specification { tag req-… }` beside the
|
|
130
|
+
`extend` (legal, and it lands the tag in the project's specification). Declaring it in BOTH is a
|
|
131
|
+
duplicate, and the code depends on which document carries the second declaration — the finding
|
|
132
|
+
lands where the file is, not at fleet altitude: `c4.invalid` (an ERROR, and the model's grading is
|
|
133
|
+
suspended behind it) when the tags-only block is in model.likec4, `usecase.flow-invalid` when it is
|
|
134
|
+
in a sibling. `c4.fleet-project-invalid` is the fleet-scope class one step out: TWO SERVICES each
|
|
135
|
+
declaring the same tag locally, where no per-service project sees the collision.
|
|
136
|
+
model.likec4 stays THE model:
|
|
137
|
+
`c4.valid`, `Covers:` and every other model check read it alone, and a sibling that does not parse
|
|
138
|
+
is one `usecase.flow-invalid` while the model keeps grading. Files loam does not read: a `.likec4`
|
|
139
|
+
in a subsystem directory, in a feature directory outside `delta.likec4`/`usecases/`/`deployment/`,
|
|
140
|
+
the generated `subsystems.likec4` and `services.likec4`, a sibling the service's own `likec4.config.json` `exclude`
|
|
141
|
+
covers (it is in no project the renderer loads either), and every UNTAGGED `dynamic view`
|
|
142
|
+
anywhere — a diagram, never a use case.
|
|
107
143
|
|
|
108
144
|
A feature **brings** one the way it brings every other axis: `features/<FEAT>/usecases/<name>.likec4`
|
|
109
145
|
is a views-only document that `loam archive` copies into `architecture/usecases/` and
|
|
@@ -112,6 +148,276 @@ would leave behind — so a flow may draw a hop into a service the feature's own
|
|
|
112
148
|
and a hop into an element the merge does NOT land is refused at plan time rather than archived into
|
|
113
149
|
a map the next reader's `loam validate --all` fails on.
|
|
114
150
|
|
|
151
|
+
**A fleet flow can also say which SUITE it runs with: `#suite-<name>`.** A suite is a set of
|
|
152
|
+
journeys a test run exercises together — a smoke suite, a payments suite — and a journey joins one
|
|
153
|
+
by carrying the tag on its `dynamic view`, declared like every LikeC4 tag in the landscape's one
|
|
154
|
+
`specification` block (`tag suite-smoke`), so a mistyped suite is a parse error rather than a second,
|
|
155
|
+
empty suite. It is a SEPARATE prefix, not `#cap-`: a capability says what a flow is FOR, a suite says
|
|
156
|
+
what it is RUN WITH, and membership is **many-to-many** — one view may carry several `#suite-` tags,
|
|
157
|
+
and a suite holds as many views as carry its tag, which is why it is a tag and not a directory.
|
|
158
|
+
|
|
159
|
+
**Tagging is the opt-in, and it grades.** A view carrying only `#suite-` tags — no `#cap-`, no
|
|
160
|
+
`#req-` — IS a use case: every reader takes it (the predicate and the byte gate below now read three
|
|
161
|
+
prefixes), and `loam validate --all` grades its hops like any other's (`usecase.step-unbacked`,
|
|
162
|
+
`usecase.step-contested`, `usecase.step-unlinked`). A journey a test stand is provisioned from must
|
|
163
|
+
be held to the model it is drawn over. The suite tag claims no capability and no requirement, so it
|
|
164
|
+
earns nothing from the capability axis. An UNTAGGED view stays ungraded, exactly as before.
|
|
165
|
+
|
|
166
|
+
`loam list suites [<name>] [--json]` is the answer the tag exists for: **which services must be up
|
|
167
|
+
for suite X.** For each suite it lists the views carrying the tag, the enumerated services their
|
|
168
|
+
steps resolve to — through the same element→service resolver the step grades use, so a step into
|
|
169
|
+
one of order-service's containers counts order-service — and every participant that resolves to NO
|
|
170
|
+
service, by element id, with whether it (or anything it is drawn inside) carries `#external`: the
|
|
171
|
+
customer who drives the flow, a topic, a partner's API. The walk up stops at the first element
|
|
172
|
+
bound to or titled for a service, as `loam gate`'s partner reading does, so a service drawn inside
|
|
173
|
+
an `#external` grouping is still ours. Those are named and never dropped, because an environment
|
|
174
|
+
list that is silently short comes up, and the journey then fails against something nobody was
|
|
175
|
+
told to start. The payload is `suites[]` (`name`, `views[]` of `{id, file}`, `services[]`, `unresolved[]`
|
|
176
|
+
of `{element, external}`), `ungrouped[]` — the use cases carrying no `#suite-` tag, which every
|
|
177
|
+
per-suite line would otherwise leave out — and `useCases` (`state`: `absent` when the fleet
|
|
178
|
+
declares no use case, `ok`, or `unreadable` with `error`, beside the `unreadable` boolean
|
|
179
|
+
`list capabilities` spells). Over an unreadable `architecture/` the section degrades rather than
|
|
180
|
+
refusing, and `suites` and `ungrouped` are ABSENT rather than `[]`. A `<name>` narrows `suites[]`
|
|
181
|
+
only; an unknown one refuses `unknown-target` with the close names. The listing reads the FLEET
|
|
182
|
+
map's use cases; a `#suite-` tag on a flow inside one service's own project is graded there like any
|
|
183
|
+
other use-case tag, and is not listed — that flow runs inside one service, whose environment is the
|
|
184
|
+
service itself.
|
|
185
|
+
|
|
186
|
+
### Two shapes of a service model
|
|
187
|
+
|
|
188
|
+
`services/<…>/<svc>/model.likec4` comes in two shapes, and **the file's own grammar decides which**
|
|
189
|
+
— never a config key, never a flag. loam scans the bytes (comments and strings masked) for a
|
|
190
|
+
top-level `specification { }` block containing an `element` declaration:
|
|
191
|
+
|
|
192
|
+
- an **extending model** declares no element kind. It is parsed INSIDE the fleet's `architecture/`
|
|
193
|
+
project — the landscape plus every other `architecture/**/*.likec4` except the generated views
|
|
194
|
+
files (`subsystems.likec4`, `services.likec4`) — and says what is inside the service with `extend <fqn> { … }` on the element
|
|
195
|
+
the map already binds to the directory. `<fqn>` is the FULLY-QUALIFIED id: `extend orderService`
|
|
196
|
+
resolves to nothing when the element is `marketplace.orderService`. No `specification` is needed,
|
|
197
|
+
no partner is re-declared, and `model {}` or no model at all are both fine. `extend` accepts
|
|
198
|
+
`#tags` and a `metadata { }` block on the element it opens (merged into it — the map's `service`
|
|
199
|
+
binding survives) and REFUSES `description`, which belongs on the map's declaration. This is the
|
|
200
|
+
shape `loam adopt` briefs, and the shape `loam init --create` scaffolds a repository for.
|
|
201
|
+
- a **standalone model** declares at least one `element <kind>` inside a `specification { }` block.
|
|
202
|
+
It is parsed ALONE, exactly as every model was before this shape existed; it may re-declare the
|
|
203
|
+
partners it names; and it renders only as a LikeC4 project of its own. Legal forever — a service
|
|
204
|
+
whose docs are reviewed from its own repository, a model adopted before the fleet had a map — and
|
|
205
|
+
not what a new model is written as.
|
|
206
|
+
|
|
207
|
+
**Who declares kinds and tags.** In the extending shape, `architecture/landscape.likec4` is the one
|
|
208
|
+
`specification` block: a kind a model needs and the map does not declare (`element database`,
|
|
209
|
+
`element queue`) is added THERE, once, and a missing one is a single parse error on the model. A
|
|
210
|
+
tags-only `specification { tag req-CANCEL }` beside an `extend` is legal and lands the tag in the
|
|
211
|
+
project's specification — but a tag the map already declares, declared again there, is a duplicate
|
|
212
|
+
blamed on both files. In the standalone shape the model declares everything it uses, which is
|
|
213
|
+
exactly why its copies of the map's elements can drift.
|
|
214
|
+
|
|
215
|
+
**Seven grades come with the pair**, and one existing grade is narrowed:
|
|
216
|
+
|
|
217
|
+
| code | shape | what it says |
|
|
218
|
+
|---|---|---|
|
|
219
|
+
| `c4.declaration-diverged` (warn, service) | standalone | an element this model and the fleet map (`architecture/`) both declare, whose `kind`, `title`, tag SET or `metadata { service }` binding differ. The two are joined by id, then by the `metadata { service }` binding both wrote, then by the map element whose id ends with the model's as a dotted tail (`paymentService` ↔ `marketplace.paymentService`) — a tail two map elements answer to joins nothing, and a tail that is a BARE last segment (`db`, `api`) is a key only for a FLEET-LEVEL map element: one no ancestor of which stands for a service, so a top-level element or a box the map draws inside a plain grouping element, never an element drawn inside a service's own. `description` is deliberately not compared — it is prose, nothing grades it, and the other four each change what a view shows or what loam resolves. When the tag sets are all that differ and copying the map's declaration would not clear them — because the difference comes off `specification { element <kind> { … } }` on one side or the other — the message says so and names the document whose specification declares them, rather than asking for a copy. One finding per divergent element; silent while the map does not parse |
|
|
220
|
+
| `c4.element-unowned` (warn, service) | extending | the model declares an element OUTSIDE the element that resolves to this service. Three remedies, first first: a store or component this service owns goes INSIDE the `extend <fqn> { }` block, where its id becomes `<fqn>.<name>`; a system the service reaches belongs in the map, declared once; another service's internals belong in that service's model |
|
|
221
|
+
| `c4.fleet-project-invalid` (warn, landscape) | extending | `--all` only, and not while `landscape.excluded` holds — this load reads what the renderer reads and so has no floor under the map, and one `exclude` entry covering the map turns every extending model's `extend` into an unresolved reference (measured on `examples/docs`: 161 warnings from one line). loam loads the fleet project the way the renderer does — a walk of the docs root, minus everything the root `exclude` covers, minus the generated views files (`subsystems.likec4`, `services.likec4`), minus every STANDALONE service directory (`service.model-unexcluded` owns that state), minus `features/` (a delta carries its own `specification` and the directory is transient) — and reports each error NOT already present in the map alone or in any per-service project the same run made. A `.likec4` in a SUBSYSTEM directory, above every service, is in that set. This is the class no per-service grade can see: two services declaring the same tag locally, two models adding the same child id, a broken untagged sibling nobody opted in. All of them are renderer-fatal. `locations[0]` is the file the error is in and the message tail names the failure it actually is |
|
|
222
|
+
| `service.likec4-config-stray` (warn, landscape) | extending | a `likec4.config.json` beside an extending model. The renderer registers it as a project of its own rooted at that directory, so the docs root stops being a one-project workspace (`likec4 validate .` then refuses without `--project`), and wherever that nested project claims the model it parses alone, out of the root project where the map's kinds are: the model does not resolve and `export json --project fleet` loses the service's containers (measured, likec4 1.59.2 — which of the two happens depends on the workspace; on `examples/docs` the fleet export was byte-identical with and without the file, on a seeded fleet the containers went). Run `loam subsystem sync`: it deletes the file. Deleting it by hand works too. loam never writes one beside an extending model |
|
|
223
|
+
| `service.model-excluded` (warn, landscape) | extending | the root `likec4.config.json`'s `exclude` covers this model's directory, so the renderer never loads it and the service is a box with nothing inside it. An entry is read the way the renderer reads it (measured, likec4 1.59.2): a bare entry is a raw path PREFIX starting on a `/` boundary, so `services`, `services/`, `services/*`, `services/**`, `services/**/*`, a wildcard segment in the middle and the bare `services/pay` all cover `services/payment-service` — while the glob form `services/pay/**` wants the literal separator and covers nothing there. An entry shaped like a FILE rather than a directory (`services/**/model.likec4`, `**/model.likec4`) is reported too, with a different repair: `loam subsystem sync` maintains only the `services/` directory entries, so that shape has to be narrowed or deleted by hand. The finding quotes the entry verbatim, because the repair is to edit that line; `loam subsystem sync` is the repair for the directory shape |
|
|
224
|
+
| `service.model-unexcluded` (warn, landscape) | standalone | the mirror, and the one that costs most: the root `exclude` does NOT cover a standalone model's directory, so the renderer merges it into the root project and reports every kind and element it declares as a duplicate — blanking the whole project, not just this service. An entry already covering the directory in any of the spellings above satisfies it, and `sync` appends no second glob for one. Same repair |
|
|
225
|
+
| `service.likec4-config-missing` (warn, landscape) | standalone **only** | a standalone model with no project file beside it. It renders nowhere from the docs root until `loam subsystem sync` writes one. An extending model never earns this |
|
|
226
|
+
| `landscape.excluded` (warn, landscape) | either | `--all` only. The root `likec4.config.json`'s `exclude` covers `architecture/landscape.likec4` itself — an `architecture/*.likec4` written for a palette is the spelling that does it. The renderer then loads NO fleet map: it draws nothing, and every extending model in the root project resolves against nothing. loam is unaffected — the architecture loader keeps the map whatever the list says — which is exactly why the state was invisible. `c4.fleet-project-invalid` is suspended behind it, on the cascade rule `permissions.invalid` states: that check reads the renderer's project, so the same entry would come back once per unresolved reference instead of once as the cause, and the message says the fleet project is not graded until the entry goes. The finding quotes the entry verbatim, and `loam subsystem sync` is NOT the repair: it maintains only the `services/` entries and leaves every other line as the team wrote it |
|
|
227
|
+
|
|
228
|
+
`c4.invalid` and `c4.valid` are unchanged codes with adjusted scope: for an extending model every
|
|
229
|
+
error the per-service project raises is THIS model's, including one the parser blames on the
|
|
230
|
+
landscape, because the map parsed clean on its own; and `c4.valid`'s message reads
|
|
231
|
+
`extends the fleet map — N elements · M relationships`, followed by ` · K tagged flow(s) graded`
|
|
232
|
+
when the service's own project declared any (the one surface a healthy intra-service flow appears
|
|
233
|
+
on). When the map itself does not parse, an
|
|
234
|
+
extending model cannot be read at all: neither code is emitted, and `spine.landscape-invalid` says
|
|
235
|
+
so with the clause ` — and model.likec4 extends it, so the model cannot be read either`.
|
|
236
|
+
|
|
237
|
+
Every other model-dependent check reads the same slice it always read — the service's OWN elements
|
|
238
|
+
and relationships. For an extending model that is what the project holds minus what the map alone
|
|
239
|
+
holds, plus the **partners** its own edges point at and their ancestors, so `health.yaml`'s
|
|
240
|
+
`kafka` still resolves when the only edge names `kafka.orderEvents`. Nothing reads the map's edges
|
|
241
|
+
as the model's.
|
|
242
|
+
|
|
243
|
+
**What loam grades is still the `architecture/` project alone.** The renderer's root project is
|
|
244
|
+
WIDER than the fleet map: it also holds every extending model. loam's fleet checks — the spine, the
|
|
245
|
+
bindings, the fleet-shape advisories, and the steps of every `architecture/usecases/` flow — read
|
|
246
|
+
the landscape plus its `architecture/` siblings and nothing else, whatever shape the models take.
|
|
247
|
+
So a cross-service flow names SERVICE elements: a hop naming a container an extending model
|
|
248
|
+
declares renders perfectly and is `landscape.invalid` for loam. That is deliberate, and the reason
|
|
249
|
+
is blast radius — a service's model is graded on its own merits, and one broken model must never
|
|
250
|
+
blank the fleet's own map. A container-level flow has its own home beside the model that declares
|
|
251
|
+
the container. The same holds for a feature's `features/<FEAT>/usecases/` flow, and the gate says
|
|
252
|
+
so before the merge rather than after it: the preview stages no service model, and it ROUTES a
|
|
253
|
+
nested addition into the model that owns that service's interior exactly as `loam archive` would.
|
|
254
|
+
So a hop naming `<service-fqn>.<container>` resolves in neither the preview nor the tree the archive
|
|
255
|
+
leaves, and `usecase.flow-invalid` refuses — where the un-routed preview resolved it, passed the
|
|
256
|
+
gate `--approve` cannot override, and landed a flow the next `loam validate --all` failed on.
|
|
257
|
+
|
|
258
|
+
**Which relationship a hop exercises.** A hop is backed by the relationships declared between its
|
|
259
|
+
two endpoints, or — only when there are none — by those whose endpoints resolve to the same two
|
|
260
|
+
services, so a hop between two services matches an edge drawn into one of their containers. A reply
|
|
261
|
+
written `a <- b` is looked up under the call it answers. Nothing behind the hop is
|
|
262
|
+
`usecase.step-unbacked` (error). Candidates that all name one operation attribute the hop to it.
|
|
263
|
+
Candidates that name different operations — an edge with no `metadata { op }` counts as naming
|
|
264
|
+
none, which differs from naming one — are `usecase.step-contested` (warn), unless **the step's
|
|
265
|
+
title opens with a name one candidate declares**: its `op`, or the message it `publishes` or
|
|
266
|
+
`consumes`, followed by the end of the title or by punctuation. Then the hop is attributed to that
|
|
267
|
+
candidate:
|
|
268
|
+
|
|
269
|
+
```
|
|
270
|
+
model {
|
|
271
|
+
web -> payments 'Calls authorizePayment' { metadata { op 'authorizePayment' } }
|
|
272
|
+
web -> payments 'Calls refundPayment' { metadata { op 'refundPayment' } }
|
|
273
|
+
}
|
|
274
|
+
views {
|
|
275
|
+
dynamic view uc_refund {
|
|
276
|
+
#cap-payments
|
|
277
|
+
web -> payments 'authorizePayment — authorizes the payment'
|
|
278
|
+
web -> payments 'refundPayment(orderId)'
|
|
279
|
+
web -> payments 'takes the payment back'
|
|
280
|
+
}
|
|
281
|
+
}
|
|
282
|
+
```
|
|
283
|
+
|
|
284
|
+
The first two hops are attributed and the third is contested. The rule is deliberately narrow,
|
|
285
|
+
because a guessed operation reads exactly like a right one in every rollup built over a flow. The
|
|
286
|
+
name must OPEN the title, matched exactly with case included. It must end there or at a character
|
|
287
|
+
that cannot continue a name — anything but a letter, digit, `_`, `.` or `-` — so
|
|
288
|
+
`authorizePaymentAsync` picks nothing. Any words after it must follow punctuation (`:`, `(`, `,`, a
|
|
289
|
+
dash): as the first word of a sentence it is prose, so against a `read` edge `read: the order`
|
|
290
|
+
picks and `read the order` does not, and neither does `reads the order before release` against a
|
|
291
|
+
`release` edge. A relationship's own title is prose and picks nothing. The pick runs only among the
|
|
292
|
+
candidates the endpoints found, and only when they disagree: a title never backs an unbacked hop,
|
|
293
|
+
never reaches the service tier when the exact one answered, and never overrules candidates that
|
|
294
|
+
agree. A pick that still disagrees — one name declared as one edge's `op` and another's `publishes`
|
|
295
|
+
— leaves the hop contested over every candidate. A hop picked by a message is an event hop, which
|
|
296
|
+
`usecase.step-unlinked` exempts. `loam diff --base` and the contract-removal gate read the same
|
|
297
|
+
attribution, so a picked hop is a victim of the removal of its own operation and of no other.
|
|
298
|
+
|
|
299
|
+
It is the title because nothing else on a step can carry the choice, as measured at the LikeC4
|
|
300
|
+
1.59.2 pin. `metadata` and tags on a step are parse errors. A step's kind exists only as
|
|
301
|
+
`-[kind]->`; it needs a declared `relationship` kind, and it names a transport, so a caller's
|
|
302
|
+
reserve, read and release calls to one provider are one kind. And drawing the hop between elements
|
|
303
|
+
that each carry exactly one candidate works only where the graded project declares those elements —
|
|
304
|
+
a service's own flow over its containers, or a map that draws a service's containers itself. It is
|
|
305
|
+
closed on a fleet flow whose two services are single elements on the map, because a container an
|
|
306
|
+
extending model declares does not resolve there (above). A sequence diagram labels a message with
|
|
307
|
+
the operation it invokes anyway.
|
|
308
|
+
|
|
309
|
+
**Private stores.** **Where a store is drawn is the team's choice.** A store one service owns may be
|
|
310
|
+
nested UNDER that service's element — written in the service's `extend <fqn> { store = database … }`
|
|
311
|
+
block, or inside the service's element on the map when the map draws that service's containers — or
|
|
312
|
+
drawn beside the services as an element of its own. Both are first-class and loam grades neither as
|
|
313
|
+
wrong. Until 2026-09-07 the second earned `landscape.datastore-private` at one consumer, a warning
|
|
314
|
+
whose whole message prescribed deleting the peer while the identical store nested was silent: it
|
|
315
|
+
graded the drawing rather than the architecture, and it is retired. Infrastructure is not a service
|
|
316
|
+
either — an element whose kind is `database`, `queue`, `topic`, `cache` or `bucket` is out of the
|
|
317
|
+
service census, so an owned store drawn at fleet level is not `landscape.service-undocumented`, is
|
|
318
|
+
not a `loam gate` partner, and mints no `service.exists` verification claim. The `extend` placement
|
|
319
|
+
needs an EXTENDING model: a model that declares its own `specification` is parsed alone, so
|
|
320
|
+
`extend <fqn>` resolves nothing there, and for such a service the map is the only nesting site (or
|
|
321
|
+
migrate the model — see "Two shapes of a service model" above).
|
|
322
|
+
|
|
323
|
+
What the choice DOES decide is one thing, and it is a fact about LikeC4 rather than a preference: **a
|
|
324
|
+
second consumer's edge resolves only against a declaration the MAP holds**, because a per-service
|
|
325
|
+
project is `architecture/` plus one model. Draw a container-level edge to `<owner-fqn>.<store>` in
|
|
326
|
+
another service's model while the store lives in the owner's `extend` block and that model is
|
|
327
|
+
`c4.invalid` ("Could not resolve reference to Referenceable named '<store>'"). So a store a second
|
|
328
|
+
consumer will name has to be on the map — nested inside the owner's element there, or beside the
|
|
329
|
+
services. That was always the load-bearing half; the prescription around it was not.
|
|
330
|
+
|
|
331
|
+
Nesting closes no door in the other direction: loam counts a
|
|
332
|
+
consumer a service's EXTENDING model attests exactly as it counts one the map draws, so a nested
|
|
333
|
+
store two services reach is `landscape.datastore-shared` — the store's owner plus every service
|
|
334
|
+
whose model draws a call at it or below it, outbound or, since a store never initiates, inbound
|
|
335
|
+
from it (*Which way an edge points*). The second consumer draws that call as a
|
|
336
|
+
container-level edge to `<owner-fqn>.<store>` in its OWN model, and the service-level twin on the
|
|
337
|
+
map as usual. The two placements give the store the same id,
|
|
338
|
+
`<owner-fqn>.<store>`, so moving the declaration OUT of the owner's `extend` block into the owner's
|
|
339
|
+
element on the map rewrites no edge and no `Covers:` line — measured on `examples/docs`, where
|
|
340
|
+
moving `db` out of order-service's `extend` block into `orderService` on the map left the model at
|
|
341
|
+
12 elements · 8 relationships, left `Covers: marketplace.orderService.db` in its `arch.spec.md`
|
|
342
|
+
resolving, and left checkout-web's container-level edge to it resolving. The move BACK is free only
|
|
343
|
+
while no other service names the store: with checkout-web's edge drawn, putting `db` back into
|
|
344
|
+
order-service's `extend` block is `c4.invalid` on checkout-web's model ("Could not resolve
|
|
345
|
+
reference to Referenceable named 'db'"), or `landscape.invalid` plus one `spine.landscape-invalid`
|
|
346
|
+
per service when that second edge is drawn on the map. `landscape.datastore-shared` needs a second
|
|
347
|
+
consumer to fire, so it always prints in the state where map→extend breaks, and its message says
|
|
348
|
+
so. It is the one datastore grade left, it fires at two consumers for both placements alike, and it
|
|
349
|
+
states a coupling rather than prescribing a move: shared means the same DATA, and if two services
|
|
350
|
+
only share a host or a cluster the honest model is one private store each. The consumer census is
|
|
351
|
+
fleet-scope: it reads
|
|
352
|
+
EXTENDING models only, because their ids are the map's fully-qualified ids and the join is exact,
|
|
353
|
+
while a standalone model's ids are its own file's and are never matched to a landscape element.
|
|
354
|
+
|
|
355
|
+
**Renderer consequences.** The root project holds the map **and** every extending model, so one
|
|
356
|
+
`npx likec4 start` at the docs root draws the fleet and the inside of every adopted service, and
|
|
357
|
+
the project picker holds one entry. A standalone model is the exception, and it costs two files:
|
|
358
|
+
ONE entry in the root `likec4.config.json`'s `exclude` covering its directory (its kinds would be
|
|
359
|
+
duplicates of the map's inside one project), plus its own `likec4.config.json`, which is what makes
|
|
360
|
+
it a project of its own.
|
|
361
|
+
`loam subsystem sync` maintains both halves — it writes the per-service file for standalone models
|
|
362
|
+
only, and rewrites the root `exclude` so that it holds `services/<tree>/**` for exactly the models
|
|
363
|
+
that stand alone. The rewrite removes the wholesale `services/**` and every `services/<tree>/**`
|
|
364
|
+
naming a directory that IS a service, keeps every other entry in the order the team wrote it (a
|
|
365
|
+
`services/legacy/**` for a directory that is no service survives untouched), and appends the
|
|
366
|
+
standalone entries sorted. It runs when at least one extending model exists, or when a standalone
|
|
367
|
+
model's directory is not covered — so a repository whose models all stand alone behind `services/**`
|
|
368
|
+
is never touched, and a fresh scaffold that gains its first standalone model earns the entry on the
|
|
369
|
+
next sync. `--json` reports it as `projects.exclude`, with what was added and removed; the text view
|
|
370
|
+
prints one line when it rewrote. An entry covers a directory when it names that directory —
|
|
371
|
+
`<dir>`, `<dir>/`, `<dir>/*`, `<dir>/**` and `<dir>/**/*` all do, and a wildcard segment in the
|
|
372
|
+
middle (`services/*/legacy`) matches one level — or when it is a raw path prefix of it from a `/`
|
|
373
|
+
boundary, which is what the renderer measures: `services/pay` covers `services/payment-service`,
|
|
374
|
+
while `services/pay/**` covers nothing there. An entry is normalised the way the renderer
|
|
375
|
+
normalises it before anything is matched: a leading `./`, an inner `/./` and an inner `//` are
|
|
376
|
+
resolved away, a TRAILING `/` is not (bare `architecture` also hides `architecture-old/`,
|
|
377
|
+
`architecture/` does not), `.//` leaves a literal `.` directory and hides nothing, and a BACKSLASH
|
|
378
|
+
is a literal character to the matcher, never a separator — `services\order-service\**` hides
|
|
379
|
+
nothing at all, so loam neither grades on it nor rewrites it. Measured at the 1.59.2 pin against
|
|
380
|
+
`npx likec4 validate .` file counts; the table is in `src/core/c4/root-project/exclude.ts` and
|
|
381
|
+
pinned in `test/root-exclude.test.ts`. A directory the list already covers in
|
|
382
|
+
any of those spellings earns no second entry. `projects.exclude` carries `updated`, `entries`,
|
|
383
|
+
`added`, `removed` and `unreadable` — the last is `true` when the root file exists and loam could
|
|
384
|
+
not read an `exclude` list out of it, which is never the same answer as `entries: []`, and the list
|
|
385
|
+
is then reported untouched. Beside it, `--json` carries
|
|
386
|
+
`projects.created` (the per-service project files written) and `projects.removed` (POSIX paths
|
|
387
|
+
relative to the DOCS ROOT, deleted this run, sorted by path; `[]` otherwise) — and
|
|
388
|
+
`projects.created` is spelled the same way. Both are relative to the docs root, not to the
|
|
389
|
+
repository root, which differ whenever `docsDir` is a subdirectory. A fresh repository is
|
|
390
|
+
scaffolded with
|
|
391
|
+
`exclude: ["**/node_modules/**", "features/**"]`.
|
|
392
|
+
|
|
393
|
+
**Migrating a standalone model to the extending shape** is a documented recipe an agent performs;
|
|
394
|
+
loam does not rewrite a model, because the comments in a real one are somebody's and not loam's to
|
|
395
|
+
reflow. In order:
|
|
396
|
+
|
|
397
|
+
1. Write into `architecture/landscape.likec4` any element kind the model uses that the map does not
|
|
398
|
+
declare (`element database`, `element queue`), and any element the model reaches that the map
|
|
399
|
+
does not draw — an external system, a broker's topic — as the fleet's own declaration.
|
|
400
|
+
2. Delete the model's `specification` block.
|
|
401
|
+
3. Delete every element the map declares: the service's own element, and every re-declared partner.
|
|
402
|
+
4. Wrap what is left in `model { extend <fqn> { … } }`, `<fqn>` being the fully-qualified id of the
|
|
403
|
+
map's element for this service. Child ids inside the block stay relative; internal edges may be
|
|
404
|
+
written inside it.
|
|
405
|
+
5. Move the cross-boundary edges outside the `extend` block and spell both endpoints in full —
|
|
406
|
+
`marketplace.orderService.outbox -> kafka.orderEvents`.
|
|
407
|
+
6. Requalify every id that names one of those elements from outside the file: the `Covers:` lines in
|
|
408
|
+
`arch.spec.md`, the `of <element>` and `include` lines of any `views` block, and the hops of any
|
|
409
|
+
`#req-`-tagged flow beside the model. There is no suffix matching anywhere — `orderService.db`
|
|
410
|
+
does not answer for `marketplace.orderService.db` — so every model-local nested id has to be
|
|
411
|
+
rewritten. `health.yaml`'s `dependencies:` are the one place that usually needs no edit, because
|
|
412
|
+
a dependency resolves when an element of the own slice answers to it **by id, `metadata { service }`
|
|
413
|
+
binding or title**, and the slice holds the service's own elements plus every endpoint of the
|
|
414
|
+
model's own edges and each endpoint's ANCESTORS: `kafka` still answers through an edge naming
|
|
415
|
+
`kafka.orderEvents`, and a peer service still answers through the binding on the map's element.
|
|
416
|
+
Prefer the service id or a top-level system id there — those two spellings resolve under either
|
|
417
|
+
model shape, and a model-local nested id does not.
|
|
418
|
+
7. Delete `services/<…>/<svc>/likec4.config.json`.
|
|
419
|
+
8. Run `loam subsystem sync` (it rewrites the root `exclude`), then `loam validate --all`.
|
|
420
|
+
|
|
115
421
|
## Subsystems — a navigable tree under `services/`
|
|
116
422
|
|
|
117
423
|
`services/` may be one flat level (the compatibility case, and forever legal) or an unbounded tree
|
|
@@ -134,6 +440,13 @@ A directory under `services/` is classified **three ways, never two**:
|
|
|
134
440
|
subsystem's marker while another moves a service in, and the group directory would otherwise be
|
|
135
441
|
read as one empty service. The fleet is never reported smaller than it is.
|
|
136
442
|
|
|
443
|
+
A directory named for one of the service's own subdirectories (`adrs/`, `usecases/`, `ui/`) is
|
|
444
|
+
service interior wherever it sits: the walk never reads one as a service or a subsystem, and never
|
|
445
|
+
counts one as the subdirectory that makes its parent a group. Writing the documented
|
|
446
|
+
`services/<id>/usecases/<name>.likec4` slot into a directory that holds no artifact FILE yet used
|
|
447
|
+
to make the walk read `usecases` as a service and its parent as an unmarked group — three errors
|
|
448
|
+
earned by following the instruction `loam init` had just scaffolded.
|
|
449
|
+
|
|
137
450
|
`subsystem.yaml` is a **marker, not a manifest**:
|
|
138
451
|
|
|
139
452
|
```yaml
|
|
@@ -154,7 +467,9 @@ claimant). Subsystem names take the service-id grammar (`subsystem.name-invalid`
|
|
|
154
467
|
is unbounded and is the author's problem, not the tool's. A service directly under `services/` is
|
|
155
468
|
**unfiled** — permanent, normal, and never a finding; `loam list` carries the count
|
|
156
469
|
(`unfiledServices`, plus `services[].subsystem` and `subsystems[]`), and an empty subsystem is
|
|
157
|
-
legal, since `loam subsystem new` must be usable before anything moves in.
|
|
470
|
+
legal, since `loam subsystem new` must be usable before anything moves in. A service `loam archive`
|
|
471
|
+
creates lands unfiled too: the archive takes no placement input but the tree itself, never the map's
|
|
472
|
+
nesting ("How archive writes, and how unarchive undoes it" has the decision).
|
|
158
473
|
|
|
159
474
|
The tree is mirrored into **one generated file, never into the authored landscape**:
|
|
160
475
|
`architecture/subsystems.likec4` holds LikeC4 **views only** — no model, no tags, no `specification`
|
|
@@ -164,21 +479,263 @@ check uses; a member nothing models is omitted, which `landscape.service-unmodel
|
|
|
164
479
|
reports). The output is deterministic and line-oriented — subsystems sorted by path, members by id,
|
|
165
480
|
one `include` per line, no timestamps, no absolute paths — so it is byte-reproducible on any machine
|
|
166
481
|
and two concurrent moves into different groups merge in git without intervention.
|
|
482
|
+
|
|
483
|
+
**Styling.** Each generated view carries `global style subsystems` — after its `title` and
|
|
484
|
+
`description`, before its `include`/`group` — exactly when the `architecture/` project declares a
|
|
485
|
+
global style with that id: `global { styleGroup subsystems { style element.tag = #external { color gray } } }`
|
|
486
|
+
(or the single-rule `global { style subsystems … }`), in the landscape or in ANY `.likec4` under
|
|
487
|
+
`architecture/` the root `likec4.config.json`'s `exclude` does not cover — a `palette.likec4` beside
|
|
488
|
+
the map works, and one under an excluded path is loaded by neither the renderer nor loam.
|
|
489
|
+
`architecture/landscape.likec4` is the one exception: loam keeps the map in that set however the
|
|
490
|
+
list is written, because an architecture project with no documents in it reads as a fleet that
|
|
491
|
+
models nothing. With the targeted tag declared in the `specification` block.
|
|
492
|
+
Declaring it is the whole opt-in. loam reads only that the id is declared, never what the group
|
|
493
|
+
says, and writes nothing when it is not — a reference to an undeclared style is a parse error that
|
|
494
|
+
blanks the whole `architecture/` project in the renderer. When the project declares style groups
|
|
495
|
+
under other names only, the file opens with one comment line naming them and saying none is
|
|
496
|
+
`subsystems`, and the views carry the renderer's defaults; there is no single-group fallback.
|
|
497
|
+
Both halves restale the file once (`subsystem.views-stale`) until the next `loam subsystem sync`:
|
|
498
|
+
declaring or removing the group named `subsystems` changes whether the views carry the
|
|
499
|
+
`global style subsystems` line, and declaring or removing a group under ANY OTHER name changes the
|
|
500
|
+
disclosure comment that lists what is available. Whether a service's own views can borrow that group depends on the shape of
|
|
501
|
+
its model: a model that EXTENDS the map is in the root project, so a `global styleGroup` declared
|
|
502
|
+
under `architecture/` reaches the views beside it and referencing the id there is legal; a model
|
|
503
|
+
that STANDS ALONE is a project of its own, and a group declared under `architecture/` does not reach
|
|
504
|
+
it — those views need a declaration of their own, or the reference is the parse error that blanks
|
|
505
|
+
that project.
|
|
506
|
+
|
|
507
|
+
Each view carries the **label a human reads**: `title`, and `description` when the marker authors
|
|
508
|
+
one. The title is the subsystem's **path** — every marked ancestor's label and its own, joined with
|
|
509
|
+
` / `, so `services/platform/order-flow` reads `Platform / Order flow` — and each label is the
|
|
510
|
+
marker's `title`, falling back to the directory name (an unlabelled view is shown by its id, which
|
|
511
|
+
is hex-escaped for injectivity and reads as `subsystem_platform__order_2dflow`). LikeC4
|
|
512
|
+
reads that path as folders: the view list mirrors the directory tree, showing one folder per
|
|
513
|
+
ancestor and the leaf's own label as the title. A `/` **inside** a label becomes `∕` (U+2215), so an
|
|
514
|
+
authored slash can neither hide half of what somebody wrote nor invent a folder level the tree does
|
|
515
|
+
not have. Both strings are somebody's prose written into syntax, so both are normalized to one line
|
|
516
|
+
and escaped. Titles are not unique — only ids are — and renaming a parent's title rewrites every
|
|
517
|
+
view beneath it.
|
|
518
|
+
|
|
519
|
+
The **boundary** each view draws is a fact about the landscape, never a setting:
|
|
520
|
+
|
|
521
|
+
1. an element that holds *exactly* the subsystem's members and nothing else is included beside the
|
|
522
|
+
leaves, so the renderer nests them in the box the author drew, under the author's title;
|
|
523
|
+
2. a landscape that nests nothing puts the services in a `group '<title>'` of loam's own, mirroring
|
|
524
|
+
the directory subtree, so a parent subsystem shows its children as boxes rather than one pile;
|
|
525
|
+
3. members that ARE nested with no element owning exactly them get no box and one comment line —
|
|
526
|
+
`// model: no boundary — <id> also holds services outside this subsystem.` — because a synthetic
|
|
527
|
+
box would look identical whether or not the tree and the map agree. That comment is the whole of
|
|
528
|
+
what loam says about the two disagreeing: nothing grades it, nothing fails on it, and it lands in
|
|
529
|
+
the diff of the commit that caused it. **The tree and any `group` nesting in the landscape stay
|
|
530
|
+
independent**; `loam subsystem move` never rewrites the map, and no finding compares them.
|
|
531
|
+
|
|
532
|
+
Because the boundary depends on which *other* services sit under an element, an unrelated adoption
|
|
533
|
+
or archive can flip a view from boxed to flat — a new reason to see `subsystem.views-stale`, and why
|
|
534
|
+
`loam archive` withholds its "complete + current" line and names `loam subsystem sync` when it left
|
|
535
|
+
the file behind.
|
|
536
|
+
|
|
167
537
|
`loam subsystem sync` is the one regenerator (it also *removes* the file when the tree has no
|
|
168
538
|
subsystems), every `loam subsystem` write verb regenerates it inside the same transaction, and
|
|
169
|
-
`validate --all` grades staleness by **
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
539
|
+
`validate --all` grades staleness by **content comparison** (line endings normalized; a CRLF
|
|
540
|
+
checkout changes no fact) — one error,
|
|
541
|
+
`subsystem.views-stale`, per stale generated file, each naming its own path (there are two of them;
|
|
542
|
+
`architecture/services.likec4` is below); nothing in loam ever parses either: a views-only document
|
|
543
|
+
does not parse standalone (its `include` lines reference elements the landscape defines), and both
|
|
544
|
+
hold **static** views, whose contents loam never reads in any document. Only the LikeC4 renderer,
|
|
545
|
+
which merges the whole `architecture/` project, resolves their includes.
|
|
546
|
+
|
|
547
|
+
`sync` refuses to rewrite this file while the `architecture/` project cannot be read. The render
|
|
548
|
+
needs the map's elements and its global-style census, and a tolerant "no elements" answer wrote a
|
|
549
|
+
file holding title and description alone — the `global style subsystems` line, every `include` and
|
|
550
|
+
the boundary comment gone — while reporting `updated … 1 subsystem view(s)` and answering `current`
|
|
551
|
+
ever after. So: a load error falls back to the landscape FILE when that parses on its own (a
|
|
552
|
+
duplicate `subsystems` group declared in two `architecture/` documents costs nothing), and when
|
|
553
|
+
that fails too the facts are UNKNOWN. With an existing views file, `sync` then writes nothing about
|
|
554
|
+
it, prints why, and reports the `--json` action `blocked`, with `blocked.reason` reading
|
|
555
|
+
`landscape-invalid` — exit 0, because `loam validate --all`'s `landscape.invalid` is the gating
|
|
556
|
+
surface. With no file yet, and inside a tree-changing verb's commit window (`new`/`move`/`rm`/
|
|
557
|
+
`rename`, whose contract is that the file lands with the change), the tolerant shape is written as
|
|
558
|
+
before and the same note is printed.
|
|
559
|
+
|
|
560
|
+
### `architecture/services.likec4` — one overview view per service
|
|
561
|
+
|
|
562
|
+
The **second generated views file**, written by the same `loam subsystem sync`, in the same commit,
|
|
563
|
+
removed by it when the map draws no service, and graded by the same `subsystem.views-stale` on its
|
|
564
|
+
own path. Views only, one per service the fleet map draws, sorted by service directory id:
|
|
565
|
+
|
|
566
|
+
```
|
|
567
|
+
view service_payment_2dservice of marketplace.paymentService {
|
|
568
|
+
title 'Payments / payment-service'
|
|
569
|
+
global style services
|
|
570
|
+
include *
|
|
571
|
+
include kafka.orderEvents
|
|
572
|
+
include marketplace.checkoutWeb
|
|
573
|
+
include stripe
|
|
574
|
+
exclude * -> *
|
|
575
|
+
include -> marketplace.paymentService
|
|
576
|
+
include marketplace.paymentService ->
|
|
577
|
+
include marketplace.paymentService.* -> marketplace.paymentService.*
|
|
578
|
+
}
|
|
579
|
+
```
|
|
580
|
+
|
|
581
|
+
**Why the peers are named one by one, measured at the 1.59.2 pin on a 120-service and a 58-service
|
|
582
|
+
fleet.** The obvious recipe — `include <fqn> -> *` and `include * -> <fqn>` — is wrong at fleet
|
|
583
|
+
scale and looks perfect on a small flat fixture, which is how it would have shipped. LikeC4 draws a
|
|
584
|
+
counterpart at the highest ancestor that is not also an ancestor of the subject, so on a map that
|
|
585
|
+
GROUPS its services (which is exactly what a 100-service fleet needs at fleet level) every peer
|
|
586
|
+
collapses to its group box: the naive view of one service in the 120-service fleet drew six group
|
|
587
|
+
boxes and **0 of its 10 nodes had drill-down**. loam already reads elements and relationships, so it
|
|
588
|
+
names each peer itself — the far endpoint of every edge touching the service or anything inside it,
|
|
589
|
+
spelled as the map declares it, so an edge at a topic names the topic and not the broker, which is
|
|
590
|
+
the truer picture and the one the map already draws. `exclude * -> *` then drops the peer-to-peer
|
|
591
|
+
cross traffic (23 edges down to 7 on the same fixture), and the three lines after it put back
|
|
592
|
+
exactly the subject's own: the UNARY `include -> <fqn>` / `include <fqn> ->`, never the binary
|
|
593
|
+
wildcard, which re-introduces the parent group boxes as extra nodes (16 instead of 10). `include
|
|
594
|
+
<fqn>.* -> <fqn>.*` is the interior wiring, and it is legal on a childless element — it simply
|
|
595
|
+
matches nothing — which is what makes one line work for both model shapes: an extending model's
|
|
596
|
+
containers are in the root project and appear, a standalone model's are not and the line is silent.
|
|
597
|
+
|
|
598
|
+
Measured at 58 services in 11 groups: a median of **10 nodes and 8 edges** per generated view, every
|
|
599
|
+
peer that is a service carrying drill-down into its own generated view, and `npx likec4 export json`
|
|
600
|
+
— which layouts every view — costing about **7 ms per generated view** (1.3 s with 120 of them
|
|
601
|
+
against 451 ms without). `likec4 start` layouts on demand, so a reader pays for the one view they
|
|
602
|
+
open, and the generated views are all small BY CONSTRUCTION; the expensive view is the hand-authored
|
|
603
|
+
fleet one, and it is unaffected.
|
|
604
|
+
|
|
605
|
+
**Drill-down is the `of`, and it costs one keyword.** `view <id> of <element-fqn>` names the view's
|
|
606
|
+
subject, and LikeC4 then computes `navigateTo` on that element's node in **every other view that
|
|
607
|
+
draws it** — no authored links anywhere. So one generated view per service makes the fleet map,
|
|
608
|
+
every subsystem view and every other service view click-through into the service. The subsystem
|
|
609
|
+
views take the same keyword when the map holds an element owning exactly that subsystem's members,
|
|
610
|
+
which closes the mesh — `fleet → payments → payments/service-22 → identity/service-45 → identity`,
|
|
611
|
+
every hop generated, none authored.
|
|
612
|
+
|
|
613
|
+
**Two views of one element are legal, and the FIRST DOCUMENT ALPHABETICALLY wins** the default
|
|
614
|
+
navigation target — measured by renaming the files, LikeC4's load order being alphabetical by path.
|
|
615
|
+
Both stay reachable: inside a view of the subject, the subject's own node navigates to the OTHER
|
|
616
|
+
view of the same element, so the renderer cycles between siblings and nothing is orphaned.
|
|
617
|
+
`architecture/landscape.likec4` sorts before `architecture/services.likec4`, so a team's authored
|
|
618
|
+
`view of <element>` written in the map keeps precedence over the generated one, and neither is lost.
|
|
619
|
+
loam does not detect this and never will: **docs/DESIGN.md rule 26 forbids loam reading what a view
|
|
620
|
+
SHOWS**, in any document, so it may not look at an authored `view of` to decide whether to emit. It
|
|
621
|
+
emits for every service the map draws, and the alphabet settles the rest.
|
|
622
|
+
|
|
623
|
+
**The first line is the deed, and every writer checks it.** Both generated views files open with a
|
|
624
|
+
`// generated by loam` mark, and that mark is what makes the path loam's rather than the team's.
|
|
625
|
+
These are the only two filenames loam RESERVES inside `architecture/`, a directory that is otherwise
|
|
626
|
+
entirely authored, so a repository that already had a file at one of those paths must not lose it to
|
|
627
|
+
an upgrade — the archive's promise that authored bytes stay authored holds here too. Every writer of
|
|
628
|
+
these paths reads the first line first: `loam subsystem sync` reports that one file `refused`,
|
|
629
|
+
writes nothing for it, still does the rest of its work, and exits 1; `loam subsystem new`, `rm`,
|
|
630
|
+
`move` and `rename` and `loam seed` refuse the WHOLE operation with `already-exists`, writing,
|
|
631
|
+
renaming and staging nothing, because their commit window exists precisely to land the generated
|
|
632
|
+
file in the same commit as the change to the tree — completing half of that would manufacture the
|
|
633
|
+
`subsystem.views-stale` state the window exists never to leave behind. The consequence to know: a
|
|
634
|
+
file with no mark is never regenerated, and an EMPTY file carries no mark either, so a truncated one
|
|
635
|
+
must be deleted by hand before sync will write it again. loam's own staging is atomic and cannot
|
|
636
|
+
produce one.
|
|
637
|
+
|
|
638
|
+
**Titles are folders, because there is nothing else.** LikeC4 has **no view-folder syntax** —
|
|
639
|
+
`views { group 'X' { … } }` is a parse error at the pinned version and the AST carries no view-group
|
|
640
|
+
type at all. What it has is the title: a view title is split on `/` into browser folders and only
|
|
641
|
+
the last segment is shown. So a per-service view is titled `<subsystem path> / <service directory
|
|
642
|
+
id>`, built from the same path helper the subsystem views use, and the two generated files fold into
|
|
643
|
+
ONE folder tree mirroring `services/`. An unfiled service is its own id at the root. The service
|
|
644
|
+
**directory id** is used rather than the map element's title: it is the name every loam command
|
|
645
|
+
addresses the service by, it cannot drift from the map, and a directory id cannot contain `/`. A
|
|
646
|
+
`dynamic view` you author under `services/<…>/<id>/usecases/` lands in the same folder when you
|
|
647
|
+
title it `'<subsystem path> / <service id> / <use case>'` — a convention loam states and never
|
|
648
|
+
grades.
|
|
649
|
+
|
|
650
|
+
**Which services appear.** Exactly those the element→service join resolves, the same join the
|
|
651
|
+
subsystem views use. A service the map does not draw is omitted — its absence is already
|
|
652
|
+
`landscape.service-unmodelled`, and rendering only what the committed map resolves is what keeps the
|
|
653
|
+
output a function of committed bytes. **View ids** are `service_` plus the same hex escaping the
|
|
654
|
+
subsystem view ids use, so the two generated files cannot collide with each other, and an authored
|
|
655
|
+
view claiming one is `subsystem.view-id-collision` — which names the file and the reserved prefix. A
|
|
656
|
+
`service_` id is reserved only for a service the map actually draws. **Style**: `global style
|
|
657
|
+
services`, presence-detected off the parsed map's declared style ids exactly as `subsystems` is, and
|
|
658
|
+
written only when a group of that name is declared — a reference to an undeclared group is a parse
|
|
659
|
+
error that blanks the whole project.
|
|
660
|
+
|
|
661
|
+
### The per-service LikeC4 project file
|
|
662
|
+
|
|
663
|
+
A different artifact from the two generated views files above, and the tree's other renderer
|
|
664
|
+
concern: `services/<…>/<svc>/likec4.config.json`,
|
|
665
|
+
which `loam subsystem sync` also writes, beside every **standalone** `model.likec4` that has none.
|
|
666
|
+
An extending model needs no such file and never gets one: it belongs to the root project, which is
|
|
667
|
+
where it parses, and a project file beside it is `service.likec4-config-stray` — which `sync`
|
|
668
|
+
DELETES, in the same journaled commit as everything else it writes. A standalone model
|
|
669
|
+
needs one because it cannot share a project with the map — it declares its own kinds, and inside
|
|
670
|
+
one project every declaration is a duplicate blamed on the map too — so `sync` also keeps the root
|
|
671
|
+
`exclude` covering exactly the directories of the models that stand alone. Before extending models
|
|
672
|
+
existed, `services/**` was excluded wholesale and every model needed the file; what that exclusion
|
|
673
|
+
cost went unstated for a release: an
|
|
674
|
+
adopted service was a box on the fleet map with nothing renderable inside it. Measured at the
|
|
675
|
+
pinned likec4, a project file INSIDE a service directory is registered as a project of its own
|
|
676
|
+
regardless of the exclusion, so a docs root holding standalone models loads `fleet` plus one
|
|
677
|
+
project per such service, each carrying LikeC4's own `index` view of the containers beside the
|
|
678
|
+
authored one, and a model that does not parse breaks only its own project. (An extending model
|
|
679
|
+
is inside `fleet`, so a model that does not parse breaks the root project instead — the trade the
|
|
680
|
+
shape makes, and `c4.fleet-project-invalid` is what names it before a reader meets it in the
|
|
681
|
+
renderer.) The file is **create-only for a standalone model** —
|
|
682
|
+
never compared, never rewritten — under the root config's ownership rule, not the
|
|
683
|
+
views file's: there is no `-stale` grade because nothing can make it stale. It is removed in
|
|
684
|
+
exactly one state, and only there: beside a model that EXTENDS the map. That nested project claims
|
|
685
|
+
the model out of the root project — the workspace loads two projects, the model is Invalid inside
|
|
686
|
+
its own one (the map's kinds are not in there with it) and `export json --project fleet` then loses
|
|
687
|
+
the service's containers. Which of the two the workspace does was measured both ways at the 1.59.2
|
|
688
|
+
pin: on a seeded fleet whose model extends a top-level id the containers went; on `examples/docs`
|
|
689
|
+
the root project kept the model and the export was byte-identical. What is true in every state is
|
|
690
|
+
the second project itself — `likec4 validate .` at the docs root refuses without `--project` from
|
|
691
|
+
the moment that file exists. So it is not a harmless leftover, and `loam subsystem sync` deletes
|
|
692
|
+
it. One beside a model that is GONE stays: that one really is the team's — though the same second
|
|
693
|
+
project it registers is there too, so `likec4 validate .` and `likec4 build` at the docs root need
|
|
694
|
+
`--project` while it stands (ROADMAP's "Known and carried, 2026-09-04"). Its bytes are a
|
|
695
|
+
function of the id alone (two keys, `name` and `title`), so placement never changes them and a
|
|
696
|
+
`subsystem move` carries the file with no write of its own — it rides inside the rename the move's
|
|
697
|
+
transaction already journals (in a git-backed repo, commit the files `sync` wrote first: `move`
|
|
698
|
+
refuses over untracked paths, `move-uncommitted`); a compare would only fight the keys LikeC4 lets a
|
|
699
|
+
team add (`title`, styles). `name` is the id with every
|
|
700
|
+
`.` folded to `-` (LikeC4's name grammar refuses the byte; loam's id grammar admits it), prefixed
|
|
701
|
+
`service-` when the result would equal the root project's `fleet` (a nested project of that name
|
|
702
|
+
is silently registered as `fleet-1`) or LikeC4's reserved `default`; `title` is the id verbatim,
|
|
703
|
+
and the title is what the renderer's project picker shows. The name is **not injective**, and
|
|
704
|
+
that is disclosed rather than escaped away: an id spelled billing.core beside a service literally
|
|
705
|
+
named billing-core, or `service-fleet` beside `fleet`, share a name and LikeC4 suffixes the later
|
|
706
|
+
one `-1`; no loam
|
|
707
|
+
finding grades it, because the picker still reads the id. Order is a gate — the root file first
|
|
708
|
+
(`loam init --create` writes it, `loam doctor` prints it), then `sync`: without the root file
|
|
709
|
+
nothing is written and the text output says so. No model, no file: a spec-only service is owed
|
|
710
|
+
nothing and gets its file on the first `sync` after its model lands. No legal id, no file either: a
|
|
711
|
+
directory whose name breaks the service-id grammar (`service.id-invalid`) would get a `name` the
|
|
712
|
+
renderer silently drops, and a create-only file the renderer ignores would silence the grade for
|
|
713
|
+
good, so `sync` skips it and the rename that clears `service.id-invalid` earns the file.
|
|
714
|
+
`validate --all` grades presence only — one warning per service with
|
|
715
|
+
a STANDALONE model and no file, `service.likec4-config-missing`, on the fleet `landscape` target,
|
|
716
|
+
never an error, never in `--service`, `status` or `doctor`; the fix is `loam subsystem sync`, never
|
|
717
|
+
a hand-written file. Its two siblings grade the extending shape's version of the same question —
|
|
718
|
+
`service.likec4-config-stray` for a project file that should not be there, `service.model-excluded`
|
|
719
|
+
for a root `exclude` entry that hides the model from the renderer — and `sync` is the repair for
|
|
720
|
+
both: it removes the stray and rewrites the entry. A third reads the same list and is nobody's to
|
|
721
|
+
rewrite: `landscape.excluded` says an entry covers `architecture/landscape.likec4` itself, so the
|
|
722
|
+
renderer loads no map at all — `sync` maintains only the `services/` entries and will not touch that
|
|
723
|
+
line, and the finding quotes it so its author can. It also suspends
|
|
724
|
+
`c4.fleet-project-invalid` for that run, because the renderer's project is exactly what the entry
|
|
725
|
+
broke and grading it would report the one line once per extending model. A
|
|
726
|
+
feature's `delta.likec4` is deliberately out of scope: the directory is transient and
|
|
727
|
+
`loam archive` moves it where a project file would still be registered, so every shipped feature
|
|
728
|
+
would keep a dead project in the list forever.
|
|
174
729
|
|
|
175
730
|
A move is **one journaled transaction over N directory renames plus the regenerated views file**
|
|
176
731
|
(`loam subsystem move <name>... --into <sub|.>`; `rename` is a one-rename move; whole subtrees move
|
|
177
|
-
as one rename)
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
732
|
+
as one rename), **plus every relative markdown link the renames would break** — out of the moved
|
|
733
|
+
documents and into them, rewritten in the same commit (Conventions, above, has the rule). It
|
|
734
|
+
refuses only when git reports uncommitted changes under a directory being moved, or in a document
|
|
735
|
+
outside it that the move would relink (`move-uncommitted` — where git will not say, it proceeds),
|
|
736
|
+
and after landing it stages the renames, the relinked documents and the views file in git without
|
|
737
|
+
committing. Archive snapshots survive any of this: a keyed (version 3 or 4) snapshot re-keys
|
|
738
|
+
`services/` entries by `(service, artifact)` and restores wherever the service lives *today*.
|
|
182
739
|
|
|
183
740
|
## Canonical joins
|
|
184
741
|
|
|
@@ -193,6 +750,7 @@ lives *today*.
|
|
|
193
750
|
| Capability | requirement `Capability:` (list; also `Capabilities:`) | `architecture/capabilities.yaml` OR `capabilities/<cap>/spec.md` | declared capability id, `/` allowed for nesting |
|
|
194
751
|
| Capability requirement | requirement `Realizes:` (list) | a requirement in `capabilities/<cap>/spec.md` | `<capability-id>#<Requirement-ID>`, split at the LAST `#`, with an optional `@<digest>` pin |
|
|
195
752
|
| Capability requirement (flow) | a `dynamic view` tag `#req-<slug>` | a requirement of the capability the view's `#cap-` tag names | the `Requirement-ID` with every character outside `[A-Za-z0-9_-]` flattened to `-` |
|
|
753
|
+
| Service requirement (flow) | a `dynamic view` tag `#req-<slug>` in a `.likec4` beside model.likec4, or inside it | a requirement of the service whose directory holds the file, from either spec axis | the `Requirement-ID` flattened as above |
|
|
196
754
|
|
|
197
755
|
These keys are independent: an operation id is not a permission and a service id is not inferred
|
|
198
756
|
from a feature directory. A file can therefore be structurally valid while a join is broken; the
|
|
@@ -202,6 +760,71 @@ The last two exist only inside an `archive`/`unarchive` commit window and are re
|
|
|
202
760
|
belong in the docs repo's `.gitignore`. Finding one at rest means a write did not finish:
|
|
203
761
|
`loam doctor` grades it, and the next `archive`/`unarchive` repairs or refuses.
|
|
204
762
|
|
|
763
|
+
### Which way an edge points
|
|
764
|
+
|
|
765
|
+
LikeC4 accepts an arrow in either direction, and some of loam's readers draw meaning from it. This
|
|
766
|
+
section lists every reader and the drawing each one expects. A reader not listed here ignores
|
|
767
|
+
direction.
|
|
768
|
+
|
|
769
|
+
| Reader | Expects | Drawn the other way round |
|
|
770
|
+
|---|---|---|
|
|
771
|
+
| The HTTP spine, `metadata { op }` | caller `->` provider: the `op` resolves against the TARGET's `openapi.yaml` | graded against the wrong service: `spine.op-undefined` there |
|
|
772
|
+
| The event spine, `metadata { publishes }` | producer `->` topic: `publishes` binds the edge's SOURCE | when the source is infrastructure and the target is a service, the key binds nothing: `spine.message-unbound` (warn) on the target |
|
|
773
|
+
| The event spine, `metadata { consumes }` | topic `->` consumer: `consumes` binds the edge's TARGET | when the target is infrastructure and the source is a service, the key binds nothing: `spine.message-unbound` (warn) on the source |
|
|
774
|
+
| The consumer census behind `landscape.datastore-shared` and `landscape.platform-candidate` | a service at the SOURCE of an edge into the element, or an outbound call in the service's extending model | for infrastructure, either direction counts; for any other element the arrow is read, so a system calling INTO the fleet is not consumed by the services it calls |
|
|
775
|
+
| Review order (`loam list --review-order`) | a plain edge is a call, so its source depends on its target; a `consumes` edge is a delivery, so its target depends on its source | the dependency is counted backwards |
|
|
776
|
+
| Use-case hops (`dynamic view`) | each hop backed by a relationship drawn the same way | `usecase.step-unbacked`; a reply hop is written `<target> <- <source>` and is looked up under the call it answers |
|
|
777
|
+
| `Covers:` edge entries | `a -> b` names the edges from `a` to `b` | `covers.unknown` |
|
|
778
|
+
|
|
779
|
+
**Infrastructure** here is an element whose kind, or an ancestor's, is one of `database`, `queue`,
|
|
780
|
+
`topic`, `cache` or `bucket`, and which stands for no service: a table nested in a `database` is
|
|
781
|
+
part of it. These are the kinds that exempt an owned store from `landscape.service-undocumented`,
|
|
782
|
+
and the limitation is the same: the hook is a kind NAME, so a kind spelled `postgres`, or a broker
|
|
783
|
+
drawn as a plain `softwareSystem`, is not infrastructure. Infrastructure never initiates and never
|
|
784
|
+
decides: a store, a topic or a bucket neither calls nor publishes nor consumes. Both rules below
|
|
785
|
+
follow from that one fact.
|
|
786
|
+
|
|
787
|
+
**THE ARROW FOLLOWS THE MESSAGE on the event spine**, producer to topic and topic to consumer. A
|
|
788
|
+
key bound to a topic is therefore on the wrong end of its edge, because a topic is what sits
|
|
789
|
+
between a producer and a consumer and is never either of them. Through 0.2.0-alpha.10 such a key
|
|
790
|
+
was skipped without a word: the document validated, the metadata parsed, and the spine graded one
|
|
791
|
+
link fewer. `spine.message-unbound` now says so, at service scope (the living map) and at feature
|
|
792
|
+
scope (a delta's tagged edges), and it spells the edge turned round. When the edge carries both
|
|
793
|
+
keys, turning it round would only move the warning to the other key, so the message says to put
|
|
794
|
+
the key at fault on an edge of its own.
|
|
795
|
+
|
|
796
|
+
loam reports the reversed edge and does not quietly bind it to the other end. Review order and
|
|
797
|
+
use-case hops read the same arrow, and re-binding the spine alone would make it agree with a
|
|
798
|
+
drawing those readers still take the opposite way. An `#external` system at the bound end is not
|
|
799
|
+
this case. `crm -> billingService` carrying `publishes` says that an outside party produces the
|
|
800
|
+
message, which `spine.message-unproduced` accepts and `spine.message-external` grades, and a
|
|
801
|
+
`consumes` on an outside system says that an outside party consumes it. Both drawings are correct
|
|
802
|
+
and stay silent. So does an edge with no fleet service at either end, such as an `#external`
|
|
803
|
+
producer publishing to the broker. A key bound to a service is graded against that service's
|
|
804
|
+
contract, so a wrong drawing there already surfaces as `spine.message-undefined`.
|
|
805
|
+
|
|
806
|
+
**For the census, infrastructure's direction carries no meaning.** Fleets draw a read as a call
|
|
807
|
+
(`service -> store`) or as the data's flow (`store -> service`), and both are common. The census
|
|
808
|
+
counted only the first, so a second consumer drawn as data flow vanished, and so did the
|
|
809
|
+
`landscape.datastore-shared` it should have caused. The event doctrine above draws every consumer
|
|
810
|
+
as `topic -> service`, so no broker's consumers were ever counted toward
|
|
811
|
+
`landscape.platform-candidate`. Both directions now count, from the map's edges and from an
|
|
812
|
+
extending model's attested calls alike.
|
|
813
|
+
|
|
814
|
+
**A relationship's KIND is drawing, never a join.** No reader in the table above reads a kind:
|
|
815
|
+
`metadata { op }`, `publishes` and `consumes` are what every grade joins on, and `-[calls]->`
|
|
816
|
+
changes what the renderer draws and nothing loam decides. The convention loam teaches is in
|
|
817
|
+
`loam instructions loam-drawing`: one kind per kind of traffic — `publishes` from producer to topic,
|
|
818
|
+
`delivers` from topic to consumer, `reads`, `writes` and `readsWrites` with a store edge always
|
|
819
|
+
written service `->` store and the kind's style placing the arrowhead, `calls` for a request, and
|
|
820
|
+
`connects` wherever no evidence gives a direction yet. The fleet map `loam init --create`
|
|
821
|
+
scaffolds and the one `loam seed` templates both declare those seven kinds, and declare them only:
|
|
822
|
+
seed's edges stay plain, because `fleet.yaml` says who calls whom and nothing about what an edge
|
|
823
|
+
carries. One consequence is mechanical rather than a convention. `loam archive` splices a delta's
|
|
824
|
+
edge into the map verbatim and never copies a `specification` declaration, so a kind the living
|
|
825
|
+
map does not declare leaves the merged map unparseable and the archive refuses with
|
|
826
|
+
`merge-failed`. Declare a kind in the map before a feature that draws one archives.
|
|
827
|
+
|
|
205
828
|
## `loam.json` — the wiring, one file per repository
|
|
206
829
|
|
|
207
830
|
The docs repo is one repository; each governed implementation boundary is represented by a code
|
|
@@ -242,7 +865,10 @@ so running loam from a subdirectory works; `loam init` writes the file where it
|
|
|
242
865
|
this repo, digests that document and compares it with the committed
|
|
243
866
|
`services/<id>/openapi.yaml`: `openapi.generated-stale` (warn) when they differ,
|
|
244
867
|
`contracts.source-missing` (error) when nothing is there, `contracts.source-invalid` (error) when
|
|
245
|
-
the path escapes the repository or the file does not parse.
|
|
868
|
+
the path escapes the repository or the file does not parse. The stale warning's `details` name
|
|
869
|
+
the operationIds that differ (served but not in the committed copy, committed but no longer
|
|
870
|
+
served), and its message says when the two sets agree, so a reworded description no longer reads
|
|
871
|
+
the same as twenty vanished endpoints.
|
|
246
872
|
|
|
247
873
|
**loam reads it and never writes it.** The copy into the docs repo stays a human `cp` reviewed in
|
|
248
874
|
a pull request — that is what keeps the committed contract a document somebody agreed to rather
|
|
@@ -404,10 +1030,119 @@ the target is unchanged, only the route to it — which is what lets an author w
|
|
|
404
1030
|
resolves where they wrote it, in the pull request that reviews it, and keeps it resolving after it
|
|
405
1031
|
ships. This applies to the service and capability spec merges alike, and predates the glossary: a
|
|
406
1032
|
delta requirement citing an ADR always had it. A link inside a fenced block or a code span, an
|
|
407
|
-
absolute URL, a site-root path and a bare `#section` are left exactly as written
|
|
1033
|
+
absolute URL, a site-root path and a bare `#section` are left exactly as written, and so is a link
|
|
1034
|
+
whose text still reaches its target from where it lands.
|
|
1035
|
+
|
|
1036
|
+
**A link into the feature itself follows the file to where the archive puts it.** The same archive
|
|
1037
|
+
that moves a requirement two directories up moves the rest of the feature too, so the rule for
|
|
1038
|
+
writing a link is one sentence — write the link that resolves where you write it — and archive owns
|
|
1039
|
+
the rest:
|
|
1040
|
+
|
|
1041
|
+
- a file the archive COPIES into the living tree is cited at its living copy: a term the feature
|
|
1042
|
+
introduces (`features/<FEAT>/glossary/<term>.md` → `glossary/<term>.md`), a flow
|
|
1043
|
+
(`architecture/usecases/`), a topology document (`architecture/`);
|
|
1044
|
+
- every other file under the feature — its ADRs above all, its intent, its deltas — is cited where
|
|
1045
|
+
the archived feature keeps it, `features/archive/<FEAT>/…`.
|
|
1046
|
+
|
|
1047
|
+
So `[Order](../../glossary/order.md)` written in `features/<FEAT>/specs/<svc>/spec.md` (the
|
|
1048
|
+
feature's own term) and `[Order](../../../../glossary/order.md)` (the term's future living path,
|
|
1049
|
+
which `link.unresolved` already treats as resolved while the feature is in flight) land as the same
|
|
1050
|
+
link. **A feature ADR is never copied into the living tree**: it records a decision taken for that
|
|
1051
|
+
change, it is part of the change's history exactly as the deltas are, and the living documents that
|
|
1052
|
+
cite it point into `features/archive/<FEAT>/adrs/`. A decision meant to govern beyond the change
|
|
1053
|
+
belongs in `architecture/adrs/` or the service's own `adrs/`, written there in the same pull
|
|
1054
|
+
request. A term the archive copies gets the same treatment as the requirements: its own relative
|
|
1055
|
+
links are re-expressed for `glossary/`, two directories up from where it was written — the one
|
|
1056
|
+
change to an otherwise byte-for-byte copy.
|
|
1057
|
+
|
|
1058
|
+
**The documents that stay in the feature keep links that resolve from `features/archive/<FEAT>/`.**
|
|
1059
|
+
The archive is a directory rename, and every markdown document it carries — the intent, the
|
|
1060
|
+
feature's ADRs, its deltas, the archived copy of a term, any other `.md` under the feature — was
|
|
1061
|
+
written one directory shallower than where it now sits: `../../services/x/spec.md` in
|
|
1062
|
+
`features/<FEAT>/intent.md` would name `features/services/x/spec.md` from the archive. So the archive
|
|
1063
|
+
re-expresses their links for the move, in the same commit, under the rename's own rule: a target
|
|
1064
|
+
inside the feature moves with it, so a link from the intent to its own ADR or its own term is left
|
|
1065
|
+
exactly as written (the archived record keeps citing the archived copy it was reviewed against, not a
|
|
1066
|
+
living one that goes on changing), and every other target stays put, so only the routes the rename
|
|
1067
|
+
broke change. The plan lists each one as an `update` before the `move`, and the text output says
|
|
1068
|
+
`feature documents: N relinked for features/archive/<FEAT>/`; the snapshot parks each pre-image like
|
|
1069
|
+
any other write, so `loam unarchive` puts the authored bytes back and archive-then-unarchive stays a
|
|
1070
|
+
byte-for-byte round trip — and, like any file the archive wrote, one edited under
|
|
1071
|
+
`features/archive/<FEAT>/` since is a later change `unarchive` will not discard without `--force`
|
|
1072
|
+
(`snapshot-stale`). A document that is not UTF-8 is left as written, and the text output says so. The
|
|
1073
|
+
links are true **as of the archive**, and that is the whole promise: archived features stay out of
|
|
1074
|
+
`link.unresolved` and out of `subsystem move`'s rewrite, because a later rename is a later commit and
|
|
1075
|
+
the archive is history — what the archive owes is not to break them itself.
|
|
1076
|
+
|
|
1077
|
+
**A document outside the feature that links into it keeps a link that resolves.** The same rename
|
|
1078
|
+
takes `features/<FEAT>/` away from under every document that cites something in it — another change
|
|
1079
|
+
in flight that builds on this one's decision (`../<FEAT>/adrs/0001-x.md` from its intent), a living
|
|
1080
|
+
ADR citing the decision taken for a change still open — and no edit made in advance could have
|
|
1081
|
+
helped, because the archived path does not exist until the archive does. So the archive rewrites the
|
|
1082
|
+
links INTO the feature from every other authored document — the fleet's own documents, every
|
|
1083
|
+
service's, every other in-flight feature's: the corpus `subsystem move` rewrites for its renames —
|
|
1084
|
+
in the same commit, under the rule for a link into the feature above: a file the archive copies into
|
|
1085
|
+
the living tree is cited at its living copy, everything else where the archived feature keeps it.
|
|
1086
|
+
These documents are the living corpus and the other changes in flight, not the record of this one, so
|
|
1087
|
+
they follow a term to its living copy where the feature's own documents keep citing the archived one.
|
|
1088
|
+
Every other link keeps its spelling, and a living document the merge also rewrites is relinked from
|
|
1089
|
+
its merged text, as one write. The plan lists each as an `update` before the `move`, and the text
|
|
1090
|
+
output says `links into the feature: N document(s) relinked for features/archive/<FEAT>/`. The costs
|
|
1091
|
+
are the ones `subsystem move` names, said the archive's way: a `verified` `spec.md` or `arch.spec.md`
|
|
1092
|
+
the relink moves off its `content_digest` joins `revouchOwed`, exactly like one the merge moves, and
|
|
1093
|
+
a living requirement whose route it rewrote moves the digest a feature in flight may pin with
|
|
1094
|
+
`Based-On:` (`delta.baseline-stale` until `loam rebase` — a `note:` line in the text output). There
|
|
1095
|
+
is no `move-uncommitted` refusal: archive does not stage in git, and its undo is the snapshot, where
|
|
1096
|
+
each relinked document is an ordinary row. `loam unarchive` puts the authored bytes back, and a
|
|
1097
|
+
document edited since — another feature's intent included — is a later change it will not discard
|
|
1098
|
+
without `--force` (`snapshot-stale`). A document whose own feature has been archived or removed since
|
|
1099
|
+
is neither: `unarchive` leaves it as it is and says so (`left:` in the text output; `--json` has no
|
|
1100
|
+
key for it), because putting it back would recreate a one-file `features/<other>/` that every reader
|
|
1101
|
+
lists as a change in flight — beside the archived one, or in place of the one somebody abandoned.
|
|
1102
|
+
|
|
1103
|
+
**`loam subsystem move` and `rename` keep the same promise.** A move is a directory rename and a
|
|
1104
|
+
relative link is a route between directories, so the move rewrites every relative link it would
|
|
1105
|
+
break, in the same transaction as the renames: the links out of every markdown document under a
|
|
1106
|
+
moved directory, and the links into a moved directory from every other authored document (the
|
|
1107
|
+
fleet's own documents, every service's, every in-flight feature's — archived features are history
|
|
1108
|
+
and are left as they are). A link whose text still reaches its target from the new place — a
|
|
1109
|
+
sibling moving with its document — keeps its exact spelling. Each rewritten document is listed in
|
|
1110
|
+
the text output (`relinked <path>`) and in `--json` as `relinked`, staged in git with the renames,
|
|
1111
|
+
and asked the same uncommitted-work question: a document OUTSIDE the moved directories that the
|
|
1112
|
+
move would relink, and that git reports as uncommitted or untracked, refuses the move with
|
|
1113
|
+
`move-uncommitted` exactly as a dirty file under a moved directory does. A rewritten route is
|
|
1114
|
+
still a changed byte, and the move says what that costs rather than leaving it to the next gate: a
|
|
1115
|
+
`verified` document it relinks no longer hashes to its `content_digest` (`content.stale` until
|
|
1116
|
+
its owner re-vouches — the archive merge's `revouchOwed` in another form), and a living
|
|
1117
|
+
requirement it relinks moves the digest a feature in flight may pin with `Based-On:`
|
|
1118
|
+
(`delta.baseline-stale` until `loam rebase`). Both arrive as `warnings` (`note:` lines in
|
|
1119
|
+
text); neither is softened, because the alternative was a link that resolves to nothing.
|
|
1120
|
+
|
|
1121
|
+
**When archive CREATES a living document, it carries what the delta marked as the document's own —
|
|
1122
|
+
and nothing else.** "loam merges requirements, never prose" is not an aversion to prose: a delta's
|
|
1123
|
+
prose is about the CHANGE — the scaffold's own title is `<svc> — requirement delta for <FEAT>` — and
|
|
1124
|
+
a living document that kept it would describe a pull request that merged months ago. A machine
|
|
1125
|
+
cannot tell narrative about the service from narrative about the change, so a new
|
|
1126
|
+
`services/<svc>/spec.md`, `arch.spec.md` or `capabilities/<cap>/spec.md` gets:
|
|
1127
|
+
|
|
1128
|
+
- the delta's frontmatter **`owner`** (service specs; a capability document carries no frontmatter)
|
|
1129
|
+
— the one field that describes the service rather than the change or a vouch;
|
|
1130
|
+
- the delta's **H1**, unless it is about the change — it names the feature, or calls itself a
|
|
1131
|
+
delta — in which case the title is the id (`<svc> — architecture` for an `arch.spec.md`);
|
|
1132
|
+
- the delta's **`## Purpose`** section, verbatim, with its links re-expressed like the
|
|
1133
|
+
requirements' — the explicit marker, and the one OpenSpec's living specs already use;
|
|
1134
|
+
- one sentence linking back to the archived delta — "Created when FEAT-1 was archived; its delta
|
|
1135
|
+
keeps the prose written for that change", with "its delta" the link — so nothing the merge left
|
|
1136
|
+
behind is unreachable.
|
|
1137
|
+
|
|
1138
|
+
The plan prints one note per created document saying which of these it carried and where the rest
|
|
1139
|
+
stays. Only on creation: a living document that already exists keeps its own narrative, edited in
|
|
1140
|
+
place, and a `## Purpose` in a delta against it merges as nothing — the plan says so on that file's
|
|
1141
|
+
line rather than letting it vanish.
|
|
408
1142
|
|
|
409
1143
|
**Frontmatter**: `status`, `owner`, `service` or `feature`, `last_verified`, `vouched_by`, `sources`
|
|
410
|
-
(literal files and directories), `sources_digest`, `content_digest`, `sources_files`, `vouch_scope
|
|
1144
|
+
(literal files and directories), `sources_digest`, `content_digest`, `sources_files`, `vouch_scope`,
|
|
1145
|
+
`adoption`.
|
|
411
1146
|
`loam validate` reads it in exactly three files — a service's living `spec.md`, its living
|
|
412
1147
|
`arch.spec.md` when present (same conventions, same checks, only the label differs), and a feature's
|
|
413
1148
|
`intent.md`: the documents whose identity and status everything else joins on. `adrs/` and
|
|
@@ -456,7 +1191,12 @@ two checked files:
|
|
|
456
1191
|
`--all` alike, from the docs repo included. A `verified` spec whose current body hash mismatches
|
|
457
1192
|
gets `content.stale` (**warning**, the `sources.stale` grading: the doc changed since it was
|
|
458
1193
|
vouched, and only a person can say whether verified still holds). A verified doc with no
|
|
459
|
-
`content_digest` predates the stamp and stays quiet — the fix is re-vouching.
|
|
1194
|
+
`content_digest` predates the stamp and stays quiet — the fix is re-vouching. Every fleet readout
|
|
1195
|
+
carries the same fact, because it is computable from the docs repo: `loam list` shows
|
|
1196
|
+
`vouched (stale)` and owes the row `re-vouch` in `missing` (so `--needs-work` lists it), `loam
|
|
1197
|
+
status` counts it under `services.contentStale` and emits `next.revouch` instead of
|
|
1198
|
+
`next.fleet-clean`, and `validate --all`'s scorecard counts it under `provenance.contentStale`.
|
|
1199
|
+
The rung stays `vouched` — a person did stamp the document — exactly as for `vouch_scope` below.
|
|
460
1200
|
- `vouch_scope` — written by `loam vouch --sample <n>`, never by hand, and present only when the
|
|
461
1201
|
person who vouched read a recorded SAMPLE of the document rather than all of it. One flat string,
|
|
462
1202
|
`sampled <k>/<n> seed=<16 hex>`: k sections read of n, and the seed that chose them. `status`
|
|
@@ -472,6 +1212,23 @@ two checked files:
|
|
|
472
1212
|
`loam list`, `loam show` and `loam status`, since the sample is per file. A later FULL
|
|
473
1213
|
`loam vouch` deletes the field, which is the only thing that clears the warning; deleting it by
|
|
474
1214
|
hand forges a full vouch out of a partial one, and the generated `AGENTS.md` says so.
|
|
1215
|
+
- `adoption: partial` — written by `loam archive`, never by hand, on a service's living `spec.md`
|
|
1216
|
+
only, and only when the archive CREATES it for a service that existed before the feature (its
|
|
1217
|
+
directory is enumerated) and that the feature's own `delta.likec4` does not introduce. What lands
|
|
1218
|
+
then is that feature's slice — the requirements it added — and nothing about what the service
|
|
1219
|
+
already did, so the document is not an adoption, and the decision is to say so where every
|
|
1220
|
+
fleet readout looks rather than to refuse the archive: the plan prints why the file is marked,
|
|
1221
|
+
the maturity ladder keeps the service at `partial` (a spec.md carrying the marker does not count
|
|
1222
|
+
toward `documented`; `loam list`'s `missing` names `loam adopt --service <id>`, so the scorecard
|
|
1223
|
+
and `--needs-work` see it too, and `loam gate --service <id>` refuses the deploy with
|
|
1224
|
+
`gate.service-undocumented` exactly as it did before the archive), and `loam status` keeps
|
|
1225
|
+
`next.adopt` for it — in place of `next.complete-service` — with a sentence that does not claim
|
|
1226
|
+
the file is missing. A service the feature introduces is never marked — its slice
|
|
1227
|
+
is all of it — and neither is a spec.md that already existed. `loam adopt` flags the file `PARTIAL`
|
|
1228
|
+
and counts it as owed (`targets[]` carries `partial: true`), while its action stays `diff` — never
|
|
1229
|
+
replace it: write the baseline from the code around the slice, then **delete the line**, which is
|
|
1230
|
+
the only thing that clears it. Any value other than `partial` reads
|
|
1231
|
+
as absent.
|
|
475
1232
|
- Missing fields are warnings, and `loam list` reports the fleet's draft/verified/unmarked split.
|
|
476
1233
|
|
|
477
1234
|
**Vouching** (`loam vouch --service <id>`, run in the service's own repo) is the human promotion
|
|
@@ -615,7 +1372,21 @@ There is no manifest of services: the directories under `services/` **are** the
|
|
|
615
1372
|
why the two are graded differently;
|
|
616
1373
|
- two elements resolving to the **same** `services/<svc>/` → `landscape.binding-duplicate`
|
|
617
1374
|
(**warning**): every element→service join picks one of them arbitrarily, so the other's edges are
|
|
618
|
-
quietly filed under a service that does not own them
|
|
1375
|
+
quietly filed under a service that does not own them;
|
|
1376
|
+
- an element resolving to a `services/<svc>/` that no edge in the map touches, while that service's
|
|
1377
|
+
`model.likec4` parses and declares at least one call across its own boundary →
|
|
1378
|
+
`landscape.service-isolated` (**warning**, never gating): the service is drawn and invisible to
|
|
1379
|
+
every cross-service check, and the finding names the attested calls to draw as edges on the
|
|
1380
|
+
existing element, collapsed to the service. Evidence-gated on purpose — a service with no model,
|
|
1381
|
+
an unparseable one (`c4.invalid`), or a model reaching nothing is silent here, because an edgeless
|
|
1382
|
+
element that is TRUE is a fact and an invented edge is a dependency the fleet then plans against;
|
|
1383
|
+
the adopt brief's `landscape.touched` is the only place that silent state is named. And BINARY:
|
|
1384
|
+
one edge closes it, so a map drawing one of five attested calls is silent too — the check is
|
|
1385
|
+
touched/untouched, never a set difference over the attested calls, because loam cannot tell a
|
|
1386
|
+
call the map forgot from one the fleet deliberately does not draw at its own altitude. "Edge in
|
|
1387
|
+
the map" means an edge in the `architecture/` PROJECT — the landscape plus every other `.likec4`
|
|
1388
|
+
under it, minus the generated views files (`subsystems.likec4`, `services.likec4`) — and `loam adopt` reads the same set. An
|
|
1389
|
+
`#external` element is never the subject, whatever directory it is bound to.
|
|
619
1390
|
|
|
620
1391
|
**`architecture/landscape.likec4` is required, not optional.** Its absence is a finding, never a
|
|
621
1392
|
skipped check: an **error** as soon as `services/` holds at least one service directory, and a
|
|
@@ -626,7 +1397,7 @@ it cannot be silence: with no map, the whole cross-service layer — the C4↔AP
|
|
|
626
1397
|
`landscape.service-unmodelled`, every derived view — computes nothing and *reports* nothing, which
|
|
627
1398
|
reads exactly like a clean fleet. `loam init` scaffolds the file (empty, commented, parsing clean)
|
|
628
1399
|
so a fresh docs repo starts from a real one rather than from that ambiguity, and `loam adopt` briefs
|
|
629
|
-
the
|
|
1400
|
+
the write each service still owes it — the block, or the edges on an element no edge touches.
|
|
630
1401
|
|
|
631
1402
|
Absences inside a service directory are graded too. A directory with no `spec.md` is a **warning**
|
|
632
1403
|
(`service.no-spec`) — partial adoption is a supported state, but the checks that file feeds are
|
|
@@ -649,7 +1420,11 @@ A missing `openapi.yaml` is graded on what already points into it, under one cod
|
|
|
649
1420
|
And `loam validate --service <id>` for a directory that does not exist at all is an **error**
|
|
650
1421
|
(`service.unknown`) that suggests close ids — never `loam adopt`, which would steer an agent into
|
|
651
1422
|
adopting the typo. Only a legal id earns that grading: a name that is neither an existing directory
|
|
652
|
-
nor a legal service id is refused as `invalid-option` before any path is built from it. A
|
|
1423
|
+
nor a legal service id is refused as `invalid-option` before any path is built from it. A service
|
|
1424
|
+
an active feature introduces is not a typo: its only documents are that feature's `specs/<svc>/`
|
|
1425
|
+
until it archives, so the same error names the feature and `loam validate --feature <FEAT>` instead
|
|
1426
|
+
of a near-miss, and a bare `loam validate` in a repository bound to that service validates the
|
|
1427
|
+
introducing features in its place (`--json` says so in `boundService`). A
|
|
653
1428
|
`features/<FEAT>/specs/<svc>/` directory addressing a service that neither exists as
|
|
654
1429
|
`services/<svc>/` nor is introduced by that feature's own tagged `delta.likec4` is the same class of
|
|
655
1430
|
mistake and gets its own **error**, `delta.service-unknown`, naming close ids: archiving would
|
|
@@ -717,9 +1492,64 @@ under any flag — `x-loam-remove` and `x-loam-based-on` are stripped at path le
|
|
|
717
1492
|
level alike, on every merge branch. A path item consisting of nothing but a marker publishes no
|
|
718
1493
|
phantom path. The same feature must carry a `REMOVED` requirement whose `Operations:` line names
|
|
719
1494
|
that id. Coherence gates an absent path+method, an id mismatch, an unjustified marker, a removed
|
|
720
|
-
requirement with no marker,
|
|
1495
|
+
requirement with no marker, a new tagged C4 edge consuming the operation being removed, and a
|
|
1496
|
+
removal anything would still consume after the merge (`openapi.remove-op-consumed`, error,
|
|
1497
|
+
`--approve` overrides — **Who still consumes it** below says what counts). A `REMOVED`
|
|
1498
|
+
requirement owes a marker only while there is a living operation for it to retire: not for an id
|
|
1499
|
+
the service's living contract does not define (a consumer's `REMOVED` requirement naming another
|
|
1500
|
+
service's operation), and not for one this delta already replaces at its address (the overwrite
|
|
1501
|
+
below — no marker could be written there, since the replacement holds the slot). A living
|
|
1502
|
+
contract that does not parse proves neither exemption, so the marker stays owed.
|
|
721
1503
|
`serviceOperationIds` evaluates the feature as living upserts minus removals, so other same-feature
|
|
722
|
-
checks see the post-archive contract.
|
|
1504
|
+
checks see the post-archive contract.
|
|
1505
|
+
|
|
1506
|
+
**An overwrite can remove an operation too, and is graded as one.** The merge writes per method and
|
|
1507
|
+
path, so a delta defining ANOTHER operationId where a living operation lives replaces it there — and
|
|
1508
|
+
when the contract the merge would write then defines the living one nowhere, it has left the
|
|
1509
|
+
contract with no marker for any of the checks above to read. Coherence asks the removal's own
|
|
1510
|
+
consumer question of it, on the merged contract: `openapi.remove-op-consumed` (error, `--approve`
|
|
1511
|
+
overrides, exactly as for a marker) while anything would still consume it after the merge. With no
|
|
1512
|
+
marker there is no `REMOVED` requirement either, so the provider's own requirement naming the old id
|
|
1513
|
+
stays unless the feature `MODIFIES` it to name the new operation or `REMOVES` it — and a `REMOVED`
|
|
1514
|
+
one owes no marker here. A new tagged edge onto the replaced operation is `c4-api.op-removing`. The
|
|
1515
|
+
fix is the delta's choice: define the replaced operation at another address in the same delta if it
|
|
1516
|
+
is to stay; for a rename, redraw the living edge and restate the governing requirement. An
|
|
1517
|
+
overwrite of an operation nothing names stays the advisory `api.op-unconsumed` it always was.
|
|
1518
|
+
|
|
1519
|
+
**Who still consumes it — one answer for the gate, `loam diff` and `loam archive`.** The consumers
|
|
1520
|
+
of an operation are read once, the same way for every reader (`core/consumers/`):
|
|
1521
|
+
|
|
1522
|
+
- every edge of the `architecture/` **project** whose `metadata { op }` names it and whose target
|
|
1523
|
+
resolves to the provider — any document the spine grades, never `landscape.likec4` alone (when
|
|
1524
|
+
only a sibling document fails to parse, the map file's own edges still count);
|
|
1525
|
+
- another service's requirement — in `spec.md` **or** `arch.spec.md` — whose `Operations:` line
|
|
1526
|
+
names it, **unless that service's own contract defines the same id** — then the line names that
|
|
1527
|
+
service's own operation: `validate` resolves a spec.md `Operations:` line against its own
|
|
1528
|
+
service's contract, and `loam dependencies` reads a line in either file naming an operation its
|
|
1529
|
+
own service provides as no dependency on anybody (an unreadable or absent contract proves no
|
|
1530
|
+
ownership, and the line counts);
|
|
1531
|
+
- the use-case hops the model attributes to it (a contested hop is a doubt, never a consumer);
|
|
1532
|
+
- the provider's OWN documents still naming it — its `spec.md` requirements, and edges it draws to
|
|
1533
|
+
itself. Not its `arch.spec.md` lines: `validate` grades an `Operations:` line in spec.md only, so
|
|
1534
|
+
one in arch.spec.md strands nothing (another service's does count — it is a dependency on this
|
|
1535
|
+
contract, as `loam dependencies` reads it). The gate counts these, because the merge strands them as surely; `loam diff` and the archive's
|
|
1536
|
+
`openapi.op-modified` warnings answer who ELSE is affected, and leave them out (a service owes no
|
|
1537
|
+
operationId to itself).
|
|
1538
|
+
|
|
1539
|
+
The gate reads all of it **after the feature's merge**: a requirement the same feature `MODIFIES`
|
|
1540
|
+
to drop the id, or `REMOVES`, no longer counts — another service's too, so retiring the consumer in
|
|
1541
|
+
the same feature is a fix the gate sees — and one of its own `ADDED`/`MODIFIED` requirements naming
|
|
1542
|
+
the id does. A delta the merge would refuse to apply fails closed: the living requirements count,
|
|
1543
|
+
plus every one the delta adds or modifies. Edges and hops are read as they stand: the landscape
|
|
1544
|
+
merge only ADDS edges and a feature's flows are create-only, so no feature document can take one
|
|
1545
|
+
away — drop or redraw a living edge by editing the living map in the same change. The refusal lists
|
|
1546
|
+
the consumers in its `details[]`, in the order `loam diff` lists the victims of the same removal.
|
|
1547
|
+
A consumer document nobody could read refuses too, and is named there: a spec file that does not
|
|
1548
|
+
decode (another service's, or the provider's own), or a service id two directories claim — the gate
|
|
1549
|
+
cannot prove such a document does not name what is leaving, so it says it could not check, never
|
|
1550
|
+
that the fleet still consumes it. An edge drawn with no `metadata { op }` and a contested use-case
|
|
1551
|
+
hop are doubts about a drawing: `loam diff` lists them, and they never refuse, since `loam seed`
|
|
1552
|
+
draws every edge without an op. Archive deletes only the matching method, reports removals in
|
|
723
1553
|
text and `openapiRemovals` JSON, never copies the marker into living OpenAPI, and deliberately does
|
|
724
1554
|
not garbage-collect components. A removal marker in a living `openapi.yaml` is
|
|
725
1555
|
`openapi.remove-marker-living` (error). The usual staged-write rollback and `unarchive` snapshot
|
|
@@ -731,9 +1561,11 @@ A `paymentService -> kafka 'Publishes PaymentAuthorized'` edge used to read as f
|
|
|
731
1561
|
while naming no contract anybody could check. The event axis is the API axis's mirror, in **AsyncAPI
|
|
732
1562
|
3.0** — only 3.0, because its operations are named top-level objects carrying
|
|
733
1563
|
`action: send|receive`, the exact analog of an `operationId`, where 2.x spells the same thing as
|
|
734
|
-
`publish`/`subscribe` under a channel and leaves whose perspective they take ambiguous. A
|
|
735
|
-
|
|
736
|
-
|
|
1564
|
+
`publish`/`subscribe` under a channel and leaves whose perspective they take ambiguous. A document
|
|
1565
|
+
whose `asyncapi:` field says 1.x or 2.x is refused as one `asyncapi.invalid` naming the version, and
|
|
1566
|
+
nothing on the event axis is graded against it: reading it as a 3.0 contract with no messages used
|
|
1567
|
+
to turn every edge and requirement into a misleading `spine.message-undefined`. The AsyncAPI CLI's
|
|
1568
|
+
`asyncapi convert --target-version=3.0.0` upgrades it. Like OpenAPI it is read with a shallow structural walk over the `yaml` parse, never
|
|
737
1569
|
a real AsyncAPI parser: the slice loam joins on is small enough that the parser's model would be
|
|
738
1570
|
dead weight, and adding it would be a fourth runtime dependency.
|
|
739
1571
|
|
|
@@ -796,9 +1628,14 @@ the edges point at channels it is the channels that collect three consumers each
|
|
|
796
1628
|
broker leaves a row of untagged hubs behind it.
|
|
797
1629
|
|
|
798
1630
|
**The feature lifecycle, mirrored from the OpenAPI axis.** A feature changes this contract through
|
|
799
|
-
`features/<FEAT>/specs/<svc>/asyncapi.yaml` — a
|
|
800
|
-
|
|
801
|
-
|
|
1631
|
+
`features/<FEAT>/specs/<svc>/asyncapi.yaml` — a self-contained AsyncAPI 3.0 document holding **only
|
|
1632
|
+
the slots the feature adds, changes or removes**, exactly as an openapi delta holds only its own
|
|
1633
|
+
operations: the merge walks only the slots the delta declares, so a slot it leaves out stays as the
|
|
1634
|
+
living contract has it, and an edited slot is written out whole. A slot restated without being
|
|
1635
|
+
changed is legal and the machinery tells it from an edit, but it buys nothing and costs a pin,
|
|
1636
|
+
which a parallel feature's removal turns into `asyncapi.baseline-invalid` and `asyncapi.slot-revived`
|
|
1637
|
+
— or, unpinned, into `asyncapi.slot-revived` alone, the contract axis's revival check (under *Living
|
|
1638
|
+
spec vs delta* below) on this axis's identity. Identity is the **slot** — the pair
|
|
802
1639
|
(section, key) over the three named sections `channels.<key>`, `operations.<key>` and
|
|
803
1640
|
`components.messages.<key>` — and the join token stays the message name. Three feature-only keys,
|
|
804
1641
|
spelled and shaped exactly as on the OpenAPI axis: `x-loam-based-on` inside a slot's value (written
|
|
@@ -850,11 +1687,50 @@ decision that reverses that stance: the removal family exists, a message-slot re
|
|
|
850
1687
|
justified by a `REMOVED` requirement's `Publishes:`/`Consumes:` line (channel and operation slots
|
|
851
1688
|
need exactness only — requirements join on message names and never on channel or operation keys, so
|
|
852
1689
|
demanding a justifying requirement there would invent a join the corpus cannot express), and
|
|
853
|
-
`asyncapi.remove-message-consumed` gates a removal the
|
|
1690
|
+
`asyncapi.remove-message-consumed` gates a removal the fleet would still consume after the merge —
|
|
1691
|
+
a `consumes` edge of the `architecture/` project, another service's `Consumes:` line in either spec
|
|
1692
|
+
file (no ownership rule here: a consumer declares the message in its own contract as `receive` by
|
|
1693
|
+
construction), a use-case hop backed by such an edge — or whose provider's own `Publishes:` line the
|
|
1694
|
+
merge would leave behind, or its own `publishes` edge (the spine grades that edge on its source,
|
|
1695
|
+
`spine.message-undefined`; drop or redraw it in the living map), which covers a message slot re-keyed to another name or a send operation
|
|
1696
|
+
turned around as surely as a marker: read after the merge, as on the HTTP axis, and a `REMOVED`
|
|
1697
|
+
requirement owes no marker for a message the merge already takes out. **A consumer counts only
|
|
1698
|
+
while nothing would still send the message after the merge** — no service, and no `#external`
|
|
1699
|
+
element whose edge publishes it: the join is fleet-global, and a consumer with another producer is
|
|
1700
|
+
not stranded — `spine.message-unproduced` grades a consumer by exactly that, an outside producer
|
|
1701
|
+
included (*The direction is not decoration* above), and `loam diff` reports such a removal as
|
|
1702
|
+
`diff.message-removed`, "still sent by …", naming an outside producer as `<title> (#external)`.
|
|
1703
|
+
The senders are read after the merge too: a service this feature carries an event delta for sends
|
|
1704
|
+
what the merged contract would (the delta itself, where it creates the contract), and vouches for
|
|
1705
|
+
nothing where the merge cannot be simulated, so a feature retiring the message from every sender
|
|
1706
|
+
is still refused; an unreadable contract never vouches. The outside producer is read off the
|
|
1707
|
+
living `architecture/` project with validate's own rule, and only while that project parses. The
|
|
1708
|
+
landscape merge only adds to the map, so no feature takes its edge or tag away — but the rule also
|
|
1709
|
+
asks that no `services/<id>/` directory stand for the element, and the merge creates one from each
|
|
1710
|
+
`specs/<id>/` the feature carries. So the rule is asked over the directories as the merge leaves
|
|
1711
|
+
them: a feature that onboards the outside system as a service (`specs/legacy-gateway/`, its element
|
|
1712
|
+
titled or bound `legacy-gateway`, or a topic nested inside it) takes the producer away, and a
|
|
1713
|
+
retirement in it is refused unless something else still sends the message — the new service's own
|
|
1714
|
+
contract, in its `specs/legacy-gateway/` delta, for one. A `publishes` edge
|
|
1715
|
+
from an `#external` element that only the feature's own C4 delta draws does not count either —
|
|
1716
|
+
draw it in the living map, or archive with `--approve`. Another sender does not excuse the
|
|
1717
|
+
provider's own `Publishes:` lines and `publishes` edges — those are graded against its own
|
|
1718
|
+
contract, whoever else sends the message — nor its own unreadable spec files; another service's
|
|
1719
|
+
unreadable spec file, or an id two directories claim, refuses only while nothing would send. The lag argument
|
|
854
1720
|
survives as an **operational warning**, not a veto: passing the gate proves the *documents* agree
|
|
855
1721
|
nobody consumes the message — it does not prove no consumer is still reading the topic with lag, so
|
|
856
1722
|
sequence the runtime retirement accordingly.
|
|
857
1723
|
|
|
1724
|
+
**The same senders answer the consumer this feature ADDS.** A delta `Consumes:` line or tagged
|
|
1725
|
+
`consumes` edge naming a message nothing would send after the merge is `spine.message-unproduced`
|
|
1726
|
+
at feature scope — the error the next `validate --all` would raise against the consumer, raised
|
|
1727
|
+
before the archive lands it. Here an `#external` publisher the feature's own delta draws DOES count:
|
|
1728
|
+
it is an addition, the one thing the landscape merge makes, and the map `validate --all` then reads
|
|
1729
|
+
carries it. The suspension is service scope's: any contract in the fleet that does not read leaves
|
|
1730
|
+
the question unanswered. When only another in-flight feature's delta sends the message, the finding
|
|
1731
|
+
softens to `spec-event.message-pending` (a requirement line) or `c4-event.message-pending` (an
|
|
1732
|
+
edge) — archive that feature first; archived ahead of it, the consumer is unproduced until it lands.
|
|
1733
|
+
|
|
858
1734
|
**Payload-schema correctness stays out of scope, stated so the boundary reads as a decision rather
|
|
859
1735
|
than a gap.** loam never validates that a payload schema is correct JSON Schema, correct Avro, or
|
|
860
1736
|
correct anything: digests are content identity and every check joins on names. A fleet that wants
|
|
@@ -914,7 +1790,9 @@ the source for tests.
|
|
|
914
1790
|
diff, tagged to the feature.
|
|
915
1791
|
- **`loam archive <FEAT>`** merges the delta into the living state on three axes — **requirements**
|
|
916
1792
|
(`spec.md`, and `arch.spec.md` beside it: the two files ride one merge code path, parameterized by
|
|
917
|
-
filename), **API** (`openapi.yaml`), **architecture** (`landscape.likec4
|
|
1793
|
+
filename), **API** (`openapi.yaml`), **architecture** (`landscape.likec4`, plus the
|
|
1794
|
+
`services/<…>/<svc>/model.likec4` of each service whose EXTENDING model owns an addition nested
|
|
1795
|
+
inside it) — then archives the
|
|
918
1796
|
feature, so the living state stays complete. Archived deltas are the evolution history (like
|
|
919
1797
|
`git log`). `--dry-run` prints the whole plan and writes nothing.
|
|
920
1798
|
- **Coherence gate:** `loam validate --feature` checks the three axes agree (C4 edge `op` ↔ OpenAPI
|
|
@@ -933,7 +1811,11 @@ the source for tests.
|
|
|
933
1811
|
(`spec-api.op-pending` / `c4-api.op-pending`) naming that feature — archive it first — because the
|
|
934
1812
|
fix is ordering, not authoring.
|
|
935
1813
|
- **`loam unarchive <FEAT>`** takes an archive back: it restores the living docs and re-opens the
|
|
936
|
-
feature.
|
|
1814
|
+
feature, and names any generated views file the restore left behind the tree (`staleViews`).
|
|
1815
|
+
- **A document the merge CREATES** — a new service's `spec.md` or `arch.spec.md`, a capability's
|
|
1816
|
+
`spec.md` — carries what the delta marked as its own (frontmatter `owner`, a title that is not
|
|
1817
|
+
the change's, the `## Purpose` section) and one line linking back to the archived delta;
|
|
1818
|
+
Conventions, above, has the rule.
|
|
937
1819
|
- **The requirement baseline.** A MODIFIED requirement carries its full new text rather than a diff,
|
|
938
1820
|
so the merge REPLACES the living requirement instead of folding into it — and two features
|
|
939
1821
|
rewriting one requirement destroy the loser's text entirely. `loam validate` names the other
|
|
@@ -962,19 +1844,35 @@ the source for tests.
|
|
|
962
1844
|
never the resolution itself — a pin says "I read this version", so `loam rebase` reports every
|
|
963
1845
|
requirement that moved and says plainly that restamping without re-reading merges your text over
|
|
964
1846
|
theirs. There is deliberately no three-way merge: requirement prose does not merge.
|
|
965
|
-
- **The same baseline on the contract axis
|
|
966
|
-
|
|
967
|
-
|
|
968
|
-
|
|
969
|
-
|
|
970
|
-
|
|
971
|
-
|
|
972
|
-
|
|
1847
|
+
- **The same baseline on the contract axis.** A feature's `openapi.yaml` is a **self-contained
|
|
1848
|
+
document holding only what the feature adds, changes or removes** — not a copy of the living
|
|
1849
|
+
contract. The merge walks only the paths and methods the delta spells: an operation it leaves out
|
|
1850
|
+
stays exactly as the living contract has it, a local `$ref` (`#/components/schemas/Order`)
|
|
1851
|
+
resolves into the living document, and a brand-new operation, path-level key or component has no
|
|
1852
|
+
living version to pin, so an add-only delta never trips `openapi.baseline-missing` and needs no
|
|
1853
|
+
`loam rebase` at all. What the merge does upsert is every operation the document *does* spell,
|
|
1854
|
+
so an operation quoted without being changed — copied for context, or a whole contract pasted in
|
|
1855
|
+
— is a collision waiting for somebody else's edit: two deltas over one service, each quoting the
|
|
1856
|
+
operation the other edits, destroy each other, because whichever archives second pushes its
|
|
1857
|
+
authoring-time copy back over what landed in between. A quote also costs something even pinned:
|
|
1858
|
+
it is one more pin, and a pinned quote whose living operation a parallel feature removes is
|
|
1859
|
+
`openapi.baseline-invalid` — the living slot is gone, so the pin is founded on nothing — an
|
|
1860
|
+
error that blocks the archive until the quote is deleted. Unpinned, the same quote has no living
|
|
1861
|
+
operation behind it, so the baseline axis cannot tell it from an addition — and it used to
|
|
1862
|
+
archive as one, at exit 0, putting the retired operation back. The **revival check** below is
|
|
1863
|
+
what refuses it now. So a delta quotes nothing it does not change, and every operation in it that
|
|
1864
|
+
the living contract already holds carries
|
|
1865
|
+
**`x-loam-based-on`** — the same 16-hex digest, in the
|
|
973
1866
|
vendor-extension shape this axis already uses for `x-loam-remove`, written by `loam rebase` and
|
|
974
1867
|
stripped on merge exactly like `Based-On:`. The digest is taken over the operation's *canonical*
|
|
975
1868
|
form (object keys sorted), which is the same comparison `isDeepStrictEqual` makes when the merge
|
|
976
1869
|
decides an operation "differs" — so a pin goes stale exactly when the merge would say something
|
|
977
|
-
moved, never over a reordered key. The pin is
|
|
1870
|
+
moved, never over a reordered key. The pin is keyed by the slot, method and path, on both sides,
|
|
1871
|
+
so an operation written with no `operationId` — living or in the delta — is pinned, graded and
|
|
1872
|
+
quoted exactly as a named one: `loam rebase` always pinned against whatever stood at the slot,
|
|
1873
|
+
and the gate once read named operations alone, so it refused the pin rebase had just written as
|
|
1874
|
+
one over "no operation at that slot" and never asked an unnamed quote for a pin. The pin is
|
|
1875
|
+
always the digest of the **living** operation, and
|
|
978
1876
|
that one rule yields both merge verdicts by itself: an operation the author only quoted is
|
|
979
1877
|
byte-equal to living, so its pin equals its own content and **the merge skips it entirely**
|
|
980
1878
|
(reported as `· quotes …` in the plan, so the plan never silently writes less than the delta
|
|
@@ -984,7 +1882,8 @@ the source for tests.
|
|
|
984
1882
|
decision, reverting an operation nobody edited is a bug. `openapi.baseline-missing` is a warning
|
|
985
1883
|
that gates the archive, exactly like its requirement-axis sibling — every unpinned restatement is
|
|
986
1884
|
a candidate rollback — and it is reported **once per service** rather than per operation: the fix
|
|
987
|
-
is one command, and twenty-nine identical findings
|
|
1885
|
+
is one command, and the twenty-nine identical findings a contract pasted whole from a
|
|
1886
|
+
thirty-operation service would raise teach people to filter the code out. **Covered
|
|
988
1887
|
since the path-item/component baselines landed:** the same discipline now reaches the two surfaces
|
|
989
1888
|
the operation pin cannot — path-level keys (`parameters`, `servers`, `summary`, `x-*`) and every
|
|
990
1889
|
`components/<kind>/<name>` — through a single feature-only root record **`x-loam-baselines`** (`{
|
|
@@ -999,6 +1898,93 @@ the source for tests.
|
|
|
999
1898
|
component copying is verdict-driven, with genuinely new components copied only when reachable from
|
|
1000
1899
|
content actually written. Removal markers still carry no pin, since
|
|
1001
1900
|
`openapi.remove-target-mismatch` already checks that exact slot.
|
|
1901
|
+
- **A retired operation does not come back by accident.** An operation the living contract does
|
|
1902
|
+
not hold is either new or *retired*, and the living contract alone cannot say which: both are
|
|
1903
|
+
simply absent. So the feature gate also reads what every **archived** feature took out — the
|
|
1904
|
+
`x-loam-remove: true` markers left in `features/archive/<FEAT>/specs/<svc>/openapi.yaml` and
|
|
1905
|
+
`asyncapi.yaml`, through the same relocation rule the gate applies to an in-flight delta (a
|
|
1906
|
+
marker on an id the same delta redefines elsewhere moved it, and retired nothing). A delta entry
|
|
1907
|
+
that is not itself a removal, that its service's living contract does not hold, and that an
|
|
1908
|
+
archived feature retired is **`openapi.op-revived`** (by operationId — a
|
|
1909
|
+
retired operation quoted at a new path is the same operation back) or **`asyncapi.slot-revived`**
|
|
1910
|
+
(a `channels` or `operations` slot by its key; a message by its *name*, wherever it is declared).
|
|
1911
|
+
A relocation retires nothing, but it VACATES an address — and so does a retirement, once a later
|
|
1912
|
+
feature re-adds the id somewhere else — so the index records, per id, every address an archived
|
|
1913
|
+
feature's executed marker vacated, which feature, and for a move where its delta put the id. The
|
|
1914
|
+
finding is judged on the contract the archive would WRITE: the check runs the archive's own
|
|
1915
|
+
merge of the delta over the living contract and compares where each operationId stands there with
|
|
1916
|
+
where it stands now. An (operationId, address) the merged contract has and the living one lacks
|
|
1917
|
+
is `openapi.op-revived` when the living contract holds the id nowhere and an archived feature
|
|
1918
|
+
retired it (anywhere — by identity) or moved it away from that very address, or when the merged
|
|
1919
|
+
contract answers the id at two or more addresses and an archived feature retired it at, or moved
|
|
1920
|
+
it away from, that very address: archived, it would answer the id at both. An id that the living
|
|
1921
|
+
contract still holds and the merged contract answers at exactly one address is never a revival —
|
|
1922
|
+
a delta moving it back with its own removal marker where it lives now, or swapping another
|
|
1923
|
+
operation in there (named or not: the merge writes per method and path), leaves it at one
|
|
1924
|
+
address — while a marker standing where the id does not live deletes nothing, and excuses
|
|
1925
|
+
nothing. One address is no exemption for an id the living contract holds nowhere: that is the
|
|
1926
|
+
first case, so a quote of an address a move vacated is the finding even after a hand edit took
|
|
1927
|
+
the id out of where the move put it. Reading the merged contract replaced a
|
|
1928
|
+
case-by-case re-derivation of the merge that each review found a new hole in.
|
|
1929
|
+
On the event axis the same rule runs on the message name, over the event merge: the addresses a
|
|
1930
|
+
feature vacates are `components.messages` keys (a marker there) and the inline declarations a
|
|
1931
|
+
channel removal took. A delta that restates the channel an inline declaration stands in (with
|
|
1932
|
+
the message as a `$ref`) or retires that channel takes the name out, since a channel upsert
|
|
1933
|
+
replaces the channel's interior; a marker nested on the inline message does not
|
|
1934
|
+
(`asyncapi.remove-marker-inline`), so the finding names the channel as the remedy. A channel or
|
|
1935
|
+
operation slot's identity is its key, so moving one is a retirement plus an addition, graded as
|
|
1936
|
+
such. A message declared inline under a channel the delta brings back is that channel's
|
|
1937
|
+
interior, and the channel's finding stands for it — unless the merged contract declares the
|
|
1938
|
+
name somewhere else too (an archived feature re-declared it under `components` when it retired
|
|
1939
|
+
the channel): then the inline declaration is a second place of its own, and gets its own
|
|
1940
|
+
finding, since the channel's says nothing of the duplicate `--approve` would archive.
|
|
1941
|
+
Both are warnings that **gate** the archive, `openapi.baseline-missing`'s doctrine: the document
|
|
1942
|
+
is legal, and re-adding a retired operation can be exactly what a feature means — a retirement
|
|
1943
|
+
rolled back — so `--approve` carries the call, and the archive output then names what came back
|
|
1944
|
+
and whose retirement it reverses. The message offers both remedies: delete the quote if it only
|
|
1945
|
+
restated the operation; archive with `--approve` if the feature brings it back on purpose (a pin
|
|
1946
|
+
over nothing is `openapi.baseline-invalid`, so a deliberate re-add drops it first). At a vacated
|
|
1947
|
+
address beside a living one it names the address to drop instead: delete the quote, or, to move
|
|
1948
|
+
the id back on purpose, take it out of where it lives now in the same delta (a removal marker
|
|
1949
|
+
there; for a message declared inline, restate or retire its channel; for a `components.messages`
|
|
1950
|
+
key, also restate every channel or operation of the merged contract still `$ref`-ing it, with
|
|
1951
|
+
the `$ref` pointed at the quoted key — the message names them, since a marker alone would leave
|
|
1952
|
+
them pointing at nothing and their send would stop sending the message) — and where the delta
|
|
1953
|
+
already spells that address, so no marker can stand there, archive with `--approve`. A slot
|
|
1954
|
+
holds one entry, so where the living contract has ANOTHER operation (or message) at the address
|
|
1955
|
+
the delta spells the id at, the merge replaces it there: the message names it ("now holds
|
|
1956
|
+
'requestCancellation', and archiving this delta replaces it there"), and the move-back remedy
|
|
1957
|
+
says to define it at another address (another key, for a message) in the same delta if it is
|
|
1958
|
+
to stay — never an address
|
|
1959
|
+
"the living contract no longer has", a remedy that once led to a clean gate and an archive that
|
|
1960
|
+
deleted the other entry unnamed. `loam rebase`
|
|
1961
|
+
never pins such an entry as new: it prints the same finding (`revived[]` in `--json`) and leaves
|
|
1962
|
+
the decision to the author. It asks the question of the delta as it will WRITE it, pins
|
|
1963
|
+
included: a quote pinned to its own content is skipped by the merge, and the same quote
|
|
1964
|
+
repinned to whatever now lives at its address is an edit the merge writes — so a revival
|
|
1965
|
+
rebase's own repin creates is in that list, `--dry-run` included, and the next validate agrees. A channel-slot removal retires the messages its living channel
|
|
1966
|
+
declared inline, which the delta never spells, so those names are read from the archived
|
|
1967
|
+
feature's undo snapshot (`.loam-before/`, the pre-image of the contract the merge ran against) —
|
|
1968
|
+
only while that pre-image still hashes to what the manifest recorded. A marker is a request, not a
|
|
1969
|
+
record of what happened: the OpenAPI merge deletes a living operation only when the one at the
|
|
1970
|
+
marker's own method and path carries the marker's `operationId`, and the event merge deletes a
|
|
1971
|
+
`channels` or `operations` slot only when the living contract has that key, and a
|
|
1972
|
+
`components.messages` slot only when the message declared there carries the marker's name. A
|
|
1973
|
+
marker that missed — archived past `remove-target-missing` or `remove-target-mismatch` under
|
|
1974
|
+
`--approve` — deleted nothing, so each marker counts only if the same pre-image held its target
|
|
1975
|
+
(a contract the archive created, or never wrote, lost nothing to it); a feature with no intact
|
|
1976
|
+
snapshot cannot be checked, and its markers count as written. Two marker
|
|
1977
|
+
shapes are never sources, because neither can match anything: a marker with no `operationId`
|
|
1978
|
+
(`openapi.remove-marker-anonymous`) and one written at path level
|
|
1979
|
+
(`openapi.remove-marker-path-level`) — a feature archived past either under `--approve` left the
|
|
1980
|
+
operation living.
|
|
1981
|
+
**The honest limit:** the check knows only what the files under `features/archive/` record. A
|
|
1982
|
+
removal made by hand-editing a living contract leaves no marker anywhere, and an archived feature
|
|
1983
|
+
deleted from disk takes its markers with it; both read as "never retired", and a quote of such an
|
|
1984
|
+
operation grades as an addition exactly as it did before the check existed. A channel's inline
|
|
1985
|
+
messages drop out the same way when its feature carries no intact pre-image (archived before the
|
|
1986
|
+
snapshot existed, or the snapshot edited or removed since) — the channel slot itself is still
|
|
1987
|
+
caught. No git history is read: the archive directory is the record.
|
|
1002
1988
|
|
|
1003
1989
|
A requirement may carry a stable identity in its body: `Requirement-ID: <id>`, where the ID matches
|
|
1004
1990
|
`[A-Za-z][A-Za-z0-9._-]{0,127}`. It is optional for backward compatibility. When present in a delta,
|
|
@@ -1044,14 +2030,26 @@ scenarios exercise. Comma-separated, same keep-last-line quirk (and the same rep
|
|
|
1044
2030
|
|
|
1045
2031
|
- a **C4 element** — its id (`paymentService.db`), or the service a bound/titled element stands for;
|
|
1046
2032
|
resolved against the service's own model plus the landscape (for a feature delta: the feature's
|
|
1047
|
-
`delta.likec4`, the landscape,
|
|
2033
|
+
`delta.likec4`, the landscape, the service's model, and the map the merge would leave — and an
|
|
2034
|
+
entry that resolves ONLY through the delta's own ids is `covers.unknown` at feature scope, because
|
|
2035
|
+
the merge lands a stub's children under the living element it binds to, so the delta's spelling
|
|
2036
|
+
is not the map's: write the service name, or the id the merged map gives the element);
|
|
1048
2037
|
- an **edge** — `paymentService -> kafka`, each side resolved the same way against the declared
|
|
1049
|
-
relationships
|
|
2038
|
+
relationships. Direction counts: `kafka -> paymentService` names a different edge (*Which way an
|
|
2039
|
+
edge points*). The entry may carry the relationship's kind exactly as LikeC4 writes the edge,
|
|
2040
|
+
`paymentService -[async]-> kafka` or `paymentService .async kafka`. loam identifies an edge by
|
|
2041
|
+
its two endpoints and reads no relationship kind, so the kind is read past rather than compared:
|
|
2042
|
+
either kinded spelling covers exactly what `a -> b` covers, every kind of edge from `a` to `b`.
|
|
2043
|
+
(Through 0.2.0-alpha.10 both kinded spellings were always `covers.unknown`: the bracket form
|
|
2044
|
+
split at the `->` after the `]`, and the dotted form was read as one element id.);
|
|
1050
2045
|
- a **health signal** — `alert:<id>` / `sli:<id>`, ids the service's `health.yaml` declares.
|
|
1051
2046
|
|
|
1052
2047
|
An entry that resolves to nothing is `covers.unknown` (**warning**, with close ids offered where
|
|
1053
2048
|
they exist): the typo guard, because a mistyped entry silently costs exactly the coverage it was
|
|
1054
|
-
written for.
|
|
2049
|
+
written for. It is suspended — every form at once — for a service whose `model.likec4` could not be
|
|
2050
|
+
read, whether the model itself does not parse (`c4.invalid`) or it extends a map that does not
|
|
2051
|
+
(`spine.landscape-invalid`): an entry naming a container of an unread model is not a typo, and the
|
|
2052
|
+
one repair brings the whole grade back on the next run.
|
|
1055
2053
|
|
|
1056
2054
|
**Coverage obligations** — both warnings, deliberately: they never gate `archive`, and `--strict` is
|
|
1057
2055
|
the CI escalation.
|
|
@@ -1059,7 +2057,10 @@ the CI escalation.
|
|
|
1059
2057
|
- `c4.uncovered` (feature scope): a NEW tagged element or tagged edge in the feature's
|
|
1060
2058
|
`delta.likec4` that no requirement across the feature's `arch.spec.md` deltas covers.
|
|
1061
2059
|
Grouping-only elements follow the landscape checks' exemptions (`person`-kind elements,
|
|
1062
|
-
`#external` tags
|
|
2060
|
+
`#external` tags, and a GROUPING — an element that stands for no service and contains one, such
|
|
2061
|
+
as a new domain `group` drawn around a new service: there is no obligation a group can carry, and
|
|
2062
|
+
the service inside it is graded on its own). The `Covers:` line it offers is the spelling that survives the merge — an edge
|
|
2063
|
+
by its endpoints' service names, an element by its service or the id the merge gives it.
|
|
1063
2064
|
- `health.uncovered` (service scope): an alert or SLI declared in `services/<svc>/health.yaml` that
|
|
1064
2065
|
no requirement in the LIVING `arch.spec.md` covers. This is the moment `health.yaml` stops being
|
|
1065
2066
|
inert — and all loam reads out of it is ids: the recognized keys are top-level `slis:` and
|
|
@@ -1072,12 +2073,21 @@ the CI escalation.
|
|
|
1072
2073
|
**warning** (`health.invalid`): its ids are unknown, not empty, so `health.uncovered` stays quiet
|
|
1073
2074
|
AND the `Covers: alert:/sli:` entries of that service go ungraded — the old silence turned every
|
|
1074
2075
|
such entry into a false `covers.unknown` "typo" pointing at the spec, when the truth was the file.
|
|
2076
|
+
At FEATURE scope the same code names what the merge takes away: a signal the living
|
|
2077
|
+
`arch.spec.md` covers that this feature's arch delta would leave covered by nothing — a MODIFIED
|
|
2078
|
+
requirement restates its whole block, so a `Covers:` entry not carried over is one it deletes.
|
|
1075
2079
|
- `health.dependency-unmodelled` (service scope): a `dependencies:` id that nothing in the service's
|
|
1076
2080
|
OWN `model.likec4` answers to — by element id, `metadata { service }` binding, or title; exact
|
|
1077
|
-
match, with a did-you-mean hint. Deliberately the service model and never the landscape
|
|
2081
|
+
match, with a did-you-mean hint. Deliberately the service model and never the landscape read on
|
|
2082
|
+
its own: the
|
|
1078
2083
|
landscape carries only what crosses the service boundary, the model carries everything the service
|
|
1079
|
-
touches (a private store
|
|
1080
|
-
|
|
2084
|
+
touches (a private store nested inside the service's own element — one of the two placements
|
|
2085
|
+
"Private stores" above leaves to the team, and `landscape.datastore-shared` is what a second
|
|
2086
|
+
consumer of that nested store earns), and that is the set the on-call file must agree with. For an extending model
|
|
2087
|
+
the set is its own SLICE: the elements it adds, plus the partners its own edges point at and their
|
|
2088
|
+
ancestors — so a dependency named `kafka` resolves through an edge that names `kafka.orderEvents`,
|
|
2089
|
+
and a peer service resolves through the `metadata { service }` binding on the map's element the
|
|
2090
|
+
model points at. A partner the model never reaches still does not answer, which is the check.
|
|
1081
2091
|
|
|
1082
2092
|
The rest of the machinery treats the axis as what it is — requirements. `loam archive` merges an
|
|
1083
2093
|
`arch.spec.md` delta into the living one through the same code path as `spec.md` (same delta
|
|
@@ -1161,12 +2171,15 @@ digest:
|
|
|
1161
2171
|
- stamps: line 1 is `# generated by loam v<version> — …` (the AGENTS.md stamp pattern — the file is
|
|
1162
2172
|
never to be hand-edited), and each scenario carries the tag `@loam-digest-<16hex>` on the line
|
|
1163
2173
|
above its `Scenario:` keyword: the first 16 hex characters of `loam verify`'s scenario body hash —
|
|
1164
|
-
sha256 of the body lines joined and edge-trimmed, salted with the
|
|
1165
|
-
exact recipe `scenario.tested` claim ids fold in — so the
|
|
1166
|
-
disagree about what a scenario says
|
|
1167
|
-
|
|
1168
|
-
|
|
1169
|
-
(
|
|
2174
|
+
sha256 of the body lines joined and edge-trimmed, salted with the owning SERVICE and, on the arch
|
|
2175
|
+
axis, with that axis's file name — the exact recipe `scenario.tested` claim ids fold in — so the
|
|
2176
|
+
stamp, the claim and the spec can never disagree about what a scenario says. Both salts are there
|
|
2177
|
+
for one reason: a digest that spanned two namespaces would let a green run answer a question
|
|
2178
|
+
nobody ran a test for. Identically-worded scenarios across `spec.md` and `arch.spec.md` can never
|
|
2179
|
+
share a digest (a business-axis test run cannot answer for an arch scenario), and neither can two
|
|
2180
|
+
services wording a scenario the same way (one repository's green run cannot answer another
|
|
2181
|
+
repository's claim). A tag rather than a comment because cucumber's JSON report carries tags per
|
|
2182
|
+
scenario (`elements[].tags`): the stamp rides through the runner untouched, which is what
|
|
1170
2183
|
`loam verify --results` matches on.
|
|
1171
2184
|
|
|
1172
2185
|
**Staleness** (`loam validate --service <id>`, service-repo-scoped like the `sources.*` chain):
|
|
@@ -1239,6 +2252,12 @@ requirement also needs a current `Based-On:` pin before archive; `loam rebase <F
|
|
|
1239
2252
|
Omitting or leaving the intent as scaffold text raises the gating `intent.empty` /
|
|
1240
2253
|
`scaffold.placeholder` warning, so the old one-file shape is not archive-ready.
|
|
1241
2254
|
|
|
2255
|
+
Below that floor is no feature at all. A change that touches no requirement, no operation and
|
|
2256
|
+
no fleet edge — a refactor, a dependency bump, a fix that brings the code back to behaviour the
|
|
2257
|
+
living spec already documents — leaves the docs repo untouched: it is a code PR, then
|
|
2258
|
+
`loam validate --service <id>` in the service repo, where `sources.stale` names the moved paths, and
|
|
2259
|
+
the re-vouch they owe (`loam vouch --pack` is the reading list; the vouch is a person's).
|
|
2260
|
+
|
|
1242
2261
|
Each axis may be legitimately empty, and the checks grade absence differently from
|
|
1243
2262
|
emptiness-by-accident:
|
|
1244
2263
|
|
|
@@ -1249,11 +2268,17 @@ emptiness-by-accident:
|
|
|
1249
2268
|
declared-but-untagged is almost always a forgotten tag, while declaring nothing is a legible
|
|
1250
2269
|
statement that the architecture is unchanged.
|
|
1251
2270
|
- **openapi delta** — absent whenever the feature adds no operations; a requirement change that
|
|
1252
|
-
stays inside the existing contract has no API axis to speak on.
|
|
2271
|
+
stays inside the existing contract has no API axis to speak on. Present, it holds only the
|
|
2272
|
+
operations the feature adds, changes or removes.
|
|
1253
2273
|
- **requirement delta** — the axis that is nearly always present, because a feature that changes no
|
|
1254
2274
|
requirement changes no promised behaviour. The exception is architecture-only work: a new service
|
|
1255
2275
|
in the C4 delta with no `specs/<svc>/spec.md` gets `service.no-requirement-delta` as a warning,
|
|
1256
|
-
not an error.
|
|
2276
|
+
not an error. "New service" is the fleet census's reading of the map the merge leaves: a tagged
|
|
2277
|
+
`softwareSystem` of ours — not `#external`, not a grouping, drawn inside nothing but groupings
|
|
2278
|
+
the delta declares — that no `services/<id>/` answers for and the living map does not already draw. A
|
|
2279
|
+
delta's `#external` producer owes no requirement delta, and neither does the stub of a living
|
|
2280
|
+
element (whose living tags the merge keeps, so a re-declared `#external` system stays external
|
|
2281
|
+
whatever the stub says).
|
|
1257
2282
|
|
|
1258
2283
|
`intent.md` is where the "why" lives — the delta says what changed, and nothing else says what for —
|
|
1259
2284
|
and the archive gate holds it to that: `intent.empty` (a warning that gates, `--approve`
|
|
@@ -1262,6 +2287,16 @@ a feature cannot fold into the living docs with its reason unwritten. Its siblin
|
|
|
1262
2287
|
`scaffold.placeholder` refuses the rest of an unedited `loam new` scaffold the same way — the exact
|
|
1263
2288
|
template strings, a requirement or description nobody authored, must not become living truth.
|
|
1264
2289
|
|
|
2290
|
+
The scaffolded `intent.md` also carries an optional `## Open questions` heading over a comment.
|
|
2291
|
+
It is where `loam-feature`'s analyze step — between authoring and `loam rebase` — writes what the
|
|
2292
|
+
documents leave open: a SHALL two engineers could build two ways, a value or window two services
|
|
2293
|
+
state differently and which side refuses, a `Realizes:` that does not deliver its capability, an
|
|
2294
|
+
edge with no stated timeout, retry, partial-failure or compensation answer, a neighbour left out of
|
|
2295
|
+
`--touches`. A person answers by editing the file. Nothing grades the section: `intent.empty` reads
|
|
2296
|
+
past it, so questions written there are never mistaken for the Why, and they never gate the archive —
|
|
2297
|
+
every check loam has is a join between exact tokens, and whether two services MEAN the same thing by
|
|
2298
|
+
one is the part no join reads.
|
|
2299
|
+
|
|
1265
2300
|
### Where a capability lives (a declared name, and the document behind it)
|
|
1266
2301
|
|
|
1267
2302
|
A business capability — "payment splitting" — is spread across the living specs of every service
|
|
@@ -1428,13 +2463,19 @@ refusing it, which is why the flattening is a whitelist and not a list of substi
|
|
|
1428
2463
|
document declares no requirements yet, it declares none flattening to this slug (close ids
|
|
1429
2464
|
offered, each already spelled as the tag to write), or two of its ids flatten to one slug.
|
|
1430
2465
|
|
|
1431
|
-
**
|
|
2466
|
+
**Any reserved prefix opts a view in.** A view carrying `#req-` and no `#cap-` is an author who
|
|
1432
2467
|
asked to be graded and forgot half the claim; it earns the `unscoped` message rather than silence.
|
|
1433
|
-
|
|
1434
|
-
|
|
1435
|
-
|
|
1436
|
-
|
|
1437
|
-
|
|
2468
|
+
A view carrying only `#suite-<name>` is a use case too (see the layout section above). That opt-in
|
|
2469
|
+
is **one predicate**, and every reader takes it: `loam validate`, `loam diff`, `loam delta`,
|
|
2470
|
+
`loam status`, the context and explore packs, `loam list capabilities` and `loam list suites` all
|
|
2471
|
+
decide "is this a use case" the same way, so a flow one of them can see is a flow all of them can
|
|
2472
|
+
see. The byte scan that lets a fleet with no use cases skip the LikeC4 load looks for every prefix
|
|
2473
|
+
— `cap-`, `req-` and `suite-` — for the same reason: a gate narrower than the predicate is silence,
|
|
2474
|
+
not a smaller answer. It looks for each written as a tag REFERENCE: a `#`, then only what LikeC4
|
|
2475
|
+
skips between the `#` and a tag's name (whitespace, line breaks, comments), then the prefix — the
|
|
2476
|
+
only way a view carries a tag. A prefix in prose, a title or a `tag cap-…` declaration does not open
|
|
2477
|
+
it, so a hand-drawn `.likec4` beside a service's model that names `suite-` in its title, carries no
|
|
2478
|
+
tagged view and does not parse is not loaded and earns no `usecase.flow-invalid`.
|
|
1438
2479
|
|
|
1439
2480
|
Being SEEN is not the same as claiming something, and the difference is worth stating because it
|
|
1440
2481
|
looks like a gap and is not. A view with no resolved `#cap-` tag claims no capability, so it seeds
|
|
@@ -1465,6 +2506,21 @@ turn a mistake into a green fleet. And a run that could not READ the flows (a si
|
|
|
1465
2506
|
an `architecture/` that did not parse) suspends `capability.requirement-unrealized` entirely: loam
|
|
1466
2507
|
did not look, which is never the same answer as "there is nothing there".
|
|
1467
2508
|
|
|
2509
|
+
**A `#req-` tag inside a service's own project is scoped by the SERVICE, not by a capability.** A
|
|
2510
|
+
`dynamic view` beside model.likec4 carrying `#req-<slug>` resolves against the `Requirement-ID`s of
|
|
2511
|
+
that service's spec.md and arch.spec.md together — flattened by the same rule, an id both declare
|
|
2512
|
+
resolving once, and `many` when the two documents declare ids that flatten alike, because a
|
|
2513
|
+
Requirement-ID is unique inside ONE document and a flow beside the model has two.
|
|
2514
|
+
`usecase.requirement-unresolved` there has four arms: no document at all, no identified requirement,
|
|
2515
|
+
none flattening to the slug (close ids offered), or two that do. No `#cap-` tag belongs there
|
|
2516
|
+
(`usecase.capability-unresolved` says so, vocabulary or not); `usecase.step-unlinked` never fires
|
|
2517
|
+
on a hop whose caller and provider resolve to the same service — a service owes no operationId to
|
|
2518
|
+
itself — and still fires on a hop from such a flow into another service's element (a stand-in the
|
|
2519
|
+
model declares for a sibling, with no `metadata { op }`), exactly as on the fleet map; and a
|
|
2520
|
+
resolved service-local claim is the join being checked and nothing more today: it
|
|
2521
|
+
enters no capability `keptBy`, counts toward no `Covers:` or `c4.uncovered`, appears in no pack,
|
|
2522
|
+
`loam list capabilities`, `loam diff` or `loam delta`, and no feature carries one yet.
|
|
2523
|
+
|
|
1468
2524
|
**Both corpora are reported together, and neither is derived from the other.**
|
|
1469
2525
|
`loam list capabilities --json` carries `keptBy` (the flows keeping a promise) beside `realizedBy`
|
|
1470
2526
|
(the service requirements realizing it) on every `requirements[]` row, with the same three-state
|
|
@@ -1516,9 +2572,11 @@ requirement grammar and delta algebra every service spec already uses —
|
|
|
1516
2572
|
nesting spelled by the tree exactly as the living corpus spells it
|
|
1517
2573
|
(`features/FEAT-9/capabilities/payments/refunds/spec.md`). `loam archive` merges it through the same
|
|
1518
2574
|
transactional path as every other document and **creates `capabilities/<cap>/spec.md`, and the
|
|
1519
|
-
`capabilities/` tree itself, when the feature is the first to name that capability
|
|
1520
|
-
|
|
1521
|
-
|
|
2575
|
+
`capabilities/` tree itself, when the feature is the first to name that capability** — titled by
|
|
2576
|
+
the delta's own H1 unless that title is about the change, with the delta's `## Purpose` as its
|
|
2577
|
+
narrative and a line linking back to the archived delta (Conventions, "When archive CREATES a
|
|
2578
|
+
living document"); `loam unarchive` takes it back and removes what the archive created;
|
|
2579
|
+
`loam rebase` pins its `MODIFIED`/`REMOVED` requirements.
|
|
1522
2580
|
|
|
1523
2581
|
**Both lifecycles coexist and neither is deprecated.** A capability document may still be edited
|
|
1524
2582
|
directly under PR review, like `capabilities.yaml` and the use cases. What changes for a document a
|
|
@@ -1533,6 +2591,41 @@ they gate archive. Without that the feature-local delta becomes a second way to
|
|
|
1533
2591
|
requirements: a capability requirement carrying `Operations:` would merge into the living tree and
|
|
1534
2592
|
only then earn its error, against a document whoever reads the finding did not write.
|
|
1535
2593
|
|
|
2594
|
+
**A promise a living flow keeps, rewritten, is named.** A MODIFIED keeps the `Requirement-ID` and
|
|
2595
|
+
replaces the text, so a living flow tagged `#req-<id>` still resolves after the archive and every
|
|
2596
|
+
grade of it stays green — while it may draw exactly what the new wording forbids. Nothing in loam
|
|
2597
|
+
reads a flow's meaning, so `usecase.requirement-modified` (**warning**, never gates) names each such
|
|
2598
|
+
flow under `validate --feature` and in the archive's warnings: an `architecture/` flow whose `#cap-`
|
|
2599
|
+
tag scopes its `#req-` tag to a capability requirement this feature modifies, and — at the service
|
|
2600
|
+
altitude, where the directory is the scope — a flow beside a service's model whose `#req-` tag names
|
|
2601
|
+
one of that service's spec.md / arch.spec.md requirements the feature's `specs/` delta modifies. Read
|
|
2602
|
+
the flow against the replacement, and redraw it (or drop the tag) in the same change if it no longer
|
|
2603
|
+
keeps the promise.
|
|
2604
|
+
|
|
2605
|
+
**A promise a living flow keeps, removed, is the fleet's error one archive early.** A REMOVED
|
|
2606
|
+
requirement leaves the flow's `#req-` tag resolving to nothing, so the very next `validate --all`
|
|
2607
|
+
reports `usecase.requirement-unresolved` against the living flow. The same walk, over the same two
|
|
2608
|
+
altitudes and scopes, now raises that code at feature scope — **error**, gating the archive, with the
|
|
2609
|
+
view as its subject exactly as the fleet names it, and a message naming the living flow file and the
|
|
2610
|
+
requirement the feature removes. It is graded against what the merge leaves, not against the REMOVED
|
|
2611
|
+
list: a removal the same delta re-spells under an id that flattens to the same tag keeps the tag
|
|
2612
|
+
resolving and earns nothing; and a service-local tag reads BOTH of that service's documents, skipping
|
|
2613
|
+
a requirement under a `## REMOVED Requirements` heading as `validate --all` does, so an id the
|
|
2614
|
+
document the feature leaves alone still declares keeps it resolving too. The same verdict is asked of
|
|
2615
|
+
a MODIFIED: it keeps its id, but an id the feature adds beside it can flatten to the same tag, and
|
|
2616
|
+
then the flow earns this error (naming the colliding ids) instead of `usecase.requirement-modified`,
|
|
2617
|
+
whose "the tag will still resolve" would be false. Only a tag that resolves today is asked about —
|
|
2618
|
+
one already broken is `validate --all`'s finding now. `--approve`
|
|
2619
|
+
may override it, as it overrides `capability.remove-requirement-realized` for the same retirement
|
|
2620
|
+
seen through a `Realizes:` line: whether to retire a promise something still claims is the author's
|
|
2621
|
+
call, and the living docs then carry exactly the error named. The fix inside the feature is to drop
|
|
2622
|
+
the tag from the living flow (or point it at the promise that replaces this one) in the same change,
|
|
2623
|
+
or to MODIFY the requirement instead of removing it.
|
|
2624
|
+
|
|
2625
|
+
The cost of both is a byte scan for the tag's spelling, the living document of a touched service's
|
|
2626
|
+
other axis, and a LikeC4 load only for a project a document carrying the tag is in; a feature that
|
|
2627
|
+
modifies and removes nothing pays nothing.
|
|
2628
|
+
|
|
1536
2629
|
**The archive refuses a promise nothing keeps, in both directions.**
|
|
1537
2630
|
|
|
1538
2631
|
- `capability.uncovered` (**warning that gates**, `--approve`-overridable) — the feature ADDS a
|
|
@@ -1552,6 +2645,13 @@ only then earn its error, against a document whoever reads the finding did not w
|
|
|
1552
2645
|
does not land, or a landscape merge that itself refuses. The flows were not graded, so the archive
|
|
1553
2646
|
would copy them into `architecture/` for somebody else's `validate --all` to fail on. `--approve`
|
|
1554
2647
|
overrides loam's judgement about coherence, never its ability to read an axis.
|
|
2648
|
+
- `usecase.step-unbacked`, `usecase.requirement-unresolved`, `usecase.capability-unresolved`
|
|
2649
|
+
(**errors**) and `usecase.step-contested`, `usecase.step-unlinked` (warnings) — the flows that DO
|
|
2650
|
+
read, graded by the very grader `validate --all` runs over a living flow, against the post-merge
|
|
2651
|
+
map and the capability vocabulary and requirement ids the merge would leave (an id this feature's
|
|
2652
|
+
capability delta adds resolves; one it removes does not). Each message opens with the feature
|
|
2653
|
+
file's path. Before these ran here, a hop nothing backs and a `#req-` tag nothing scopes archived
|
|
2654
|
+
clean and turned the next `validate --all` red.
|
|
1555
2655
|
- `usecase.flow-exists` (**error**, never `--approve`-overridable) — a flow this feature introduces
|
|
1556
2656
|
that `architecture/usecases/` already holds. The merge is a whole-file copy, so it would replace
|
|
1557
2657
|
an authored hop sequence wholesale; the same shape and the same severity as `glossary.term-exists`,
|
|
@@ -1604,7 +2704,10 @@ ready. Several useful product shapes remain outside that boundary on purpose:
|
|
|
1604
2704
|
a third thing to keep truthful.
|
|
1605
2705
|
- **Authored `tasks.md` as authoritative state** — loam derives its actionable checklist (`loam
|
|
1606
2706
|
delta`, `loam verify`), so it cannot drift from the delta. Imported task lists are preserved as
|
|
1607
|
-
non-authoritative migration evidence until a human chooses their disposition.
|
|
2707
|
+
non-authoritative migration evidence until a human chooses their disposition. The refusal stands,
|
|
2708
|
+
and the derived projection now exists as `loam tasks`, which answers the same question the file
|
|
2709
|
+
would have — recomputed from the delta, the generated suite and the verification record on every
|
|
2710
|
+
run, so there is nothing to tick and nothing to go stale.
|
|
1608
2711
|
- **Per-tool workflow forks** — `AGENTS.md` is the tool-agnostic contract. Generated adapters wrap
|
|
1609
2712
|
shared bodies and are never independent process definitions.
|
|
1610
2713
|
|
|
@@ -1675,7 +2778,7 @@ the merge wrote, and `before`, of the pre-image sitting beside the manifest (nul
|
|
|
1675
2778
|
created the file). That directory travels with the feature into `features/archive/`, and
|
|
1676
2779
|
`loam unarchive` puts it back.
|
|
1677
2780
|
|
|
1678
|
-
The manifest is **version
|
|
2781
|
+
The manifest is **version 4**. Version 2 added `before`: a version-1 snapshot described only what
|
|
1679
2782
|
archive *wrote*; nothing described the bytes it would *restore*, so a pre-image altered inside the
|
|
1680
2783
|
archived feature directory — a bad merge, a rebase, a stray editor — was written back verbatim and
|
|
1681
2784
|
every loam surface certified the result as "the living docs are back to what they said before the
|
|
@@ -1688,12 +2791,42 @@ naming the pre-image under `.loam-before/files/`. At restore time the write targ
|
|
|
1688
2791
|
the **current services enumeration**: the bytes land wherever the service lives *today*, not
|
|
1689
2792
|
wherever it lived on the day of the archive, and an id the enumeration no longer answers falls back
|
|
1690
2793
|
to the literal path (an absent service restores as a create, exactly as before). Entries under
|
|
1691
|
-
`architecture/`
|
|
1692
|
-
|
|
1693
|
-
|
|
1694
|
-
|
|
1695
|
-
|
|
1696
|
-
|
|
2794
|
+
`architecture/` stay literal-path-only — they do not move. Entries for the feature's own documents
|
|
2795
|
+
(the ones the archive relinks for its move, Conventions above) are literal too, recorded where the
|
|
2796
|
+
move leaves them — `features/archive/<FEAT>/…`, which is where `unarchive` restores them before it
|
|
2797
|
+
moves the directory back — so they needed no layout version of their own; the two readers that meet
|
|
2798
|
+
a snapshot before its feature moved, the clobber guard below and the crash repair, look for them
|
|
2799
|
+
across the move. The documents outside the feature whose links into it the archive rewrites are
|
|
2800
|
+
ordinary rows: a service's keyed by `(service, artifact)` like every `services/` entry, the rest — a
|
|
2801
|
+
fleet ADR, a glossary term, another in-flight feature's intent — by their literal path. Version 4 stores each
|
|
2802
|
+
pre-image as **`files/<path>.before`** rather than under its living name. A pre-image kept as
|
|
2803
|
+
`files/architecture/landscape.likec4` was a second copy of the map inside the archived feature, and
|
|
2804
|
+
every tool that reads by extension took it for a document: a feature with its own
|
|
2805
|
+
`likec4.config.json`, or one rendered alone (`likec4 start features/archive/<FEAT>`), loaded the map
|
|
2806
|
+
twice beside the delta and failed on duplicate elements, as did a fleet project that did not
|
|
2807
|
+
exclude `features/`. With the suffix no pre-image is a `.likec4`, `.md` or `.yaml` to anything.
|
|
2808
|
+
Version-2 and version-3 manifests are still read, forever, each under its own rules — version 2 by
|
|
2809
|
+
literal path, version 3 keyed, both with pre-images under their living names: they already sit
|
|
2810
|
+
inside archived features, which carry them across loam upgrades, and a snapshot is restored from
|
|
2811
|
+
where it was written, never migrated. An older loam reads a version-4 snapshot as one it cannot
|
|
2812
|
+
honour (`snapshot-missing` from `unarchive`), which is the refusal it owes a layout it would have to
|
|
2813
|
+
guess at. Correspondingly, `archive` refuses to replace a `.loam-before/` whose pre-images no longer
|
|
2814
|
+
match the living bytes — version 2, 3 or 4 alike, with a keyed claim graded at its service's
|
|
2815
|
+
current directory: the previous archive's writes are still in the docs and those files are the only
|
|
2816
|
+
way back.
|
|
2817
|
+
|
|
2818
|
+
The manifest also carries one key that is not about bytes, added without a version bump because
|
|
2819
|
+
nothing that restores reads it: **`checklist`**, the digest of the checklist a fresh `loam verify`
|
|
2820
|
+
derived for the feature just before the merge — written when the feature carried a readable
|
|
2821
|
+
verification record then, absent otherwise and in every manifest an older loam wrote. It is the
|
|
2822
|
+
question the feature asked as it shipped. Once archived, a record is frozen history and the checklist
|
|
2823
|
+
cannot be re-derived (the merge moved the feature's operations into the living contract, so a
|
|
2824
|
+
derivation after it can only mismatch), so this is the one thing an archived record can still be
|
|
2825
|
+
held to: `loam gate --since` grades the record against it, and a record that answered an older
|
|
2826
|
+
checklist reads `stale`. `unarchive` takes the snapshot away with the rest of it, so a feature
|
|
2827
|
+
archived again gets a fresh capture. `list`, `status` and `verify` keep reading an archived record as
|
|
2828
|
+
frozen history; a manifest with no `checklist` leaves the gate reading it that way too, and saying
|
|
2829
|
+
so.
|
|
1697
2830
|
|
|
1698
2831
|
Some checks run at plan time rather than in the gate, because only the computed merge can see them.
|
|
1699
2832
|
A **living** requirement sitting outside `## Requirements` refuses the whole archive
|
|
@@ -1702,15 +2835,63 @@ requirement would land in the file twice — and `--approve` does not override i
|
|
|
1702
2835
|
duplication is mechanical, not a judgment call; re-home the requirement first. A delta that
|
|
1703
2836
|
redefines a path+method the living OpenAPI already has (`openapi.op-modified`, warning) merges
|
|
1704
2837
|
anyway, but the plan says so — the overwrite is wholesale, and the living definition it replaces
|
|
1705
|
-
deserved an eye.
|
|
2838
|
+
deserved an eye. One that defines a different operationId there, leaving a consumed operation
|
|
2839
|
+
defined nowhere, never reaches the plan without `--approve`: the gate refuses it first
|
|
2840
|
+
(`openapi.remove-op-consumed`, the overwrite shape). An unparseable `delta.likec4` fails the archive at plan time regardless of
|
|
1706
2841
|
`--approve`: the alternative was archiving with the architecture axis silently dropped, which is the
|
|
1707
2842
|
data loss this tool exists to prevent.
|
|
1708
2843
|
|
|
2844
|
+
**What a replacement takes away is named, not just counted.** A MODIFIED requirement carries its
|
|
2845
|
+
full new text and replaces the living one whole, so every living scenario and every join-line entry
|
|
2846
|
+
(`Operations:`, `Covers:`, `Requires:`, `Capability:`, `Realizes:`, `Publishes:`, `Consumes:`) it
|
|
2847
|
+
does not restate is gone after the merge. Under each `← +A ~M -R` line the plan now prints one line
|
|
2848
|
+
per replaced or removed requirement saying what goes — the scenarios by name, the entries verbatim —
|
|
2849
|
+
and `archive --json` carries the same rows as `dropped`
|
|
2850
|
+
(`[{ path, kind, requirement, id?, scenarios, directives: { covers?: […], … } }]`: `path` the living
|
|
2851
|
+
document; `kind` `MODIFIED`, `REMOVED`, or `ADDED` for an ADDED that lands on a living requirement
|
|
2852
|
+
of the same id or name; `requirement` and `id` the LIVING requirement's heading and id, `id` absent
|
|
2853
|
+
when it has none; `directives` keyed by field — `operations`, `covers`, `requires`,
|
|
2854
|
+
`capabilities`, `realizes`, `publishes`, `consumes` — each present only for a line that loses an
|
|
2855
|
+
entry, and a `Realizes:` entry compared by its target so a `@<digest>` pin is never a drop). `loam delta --json` carries the identical rows as its own `dropped` key, and its human view
|
|
2856
|
+
prints them under *Taken out of the living docs by the merge*, so the brief says before the archive
|
|
2857
|
+
what the plan says during it. Read verbatim and by name, deliberately: a renamed scenario or a
|
|
2858
|
+
`Covers:` entry respelled from an element id to the service name is listed, because what is listed
|
|
2859
|
+
is what the living text says and the merged text does not — the author decides whether it meant the
|
|
2860
|
+
same. Never a warning and never a gate: rewriting a requirement smaller is legitimate; what was
|
|
2861
|
+
missing was being told. The one drop that IS graded is the health signal's, as before
|
|
2862
|
+
(`health.uncovered` at feature scope).
|
|
2863
|
+
|
|
2864
|
+
**The contract merges write over the living text, not out of a printer.** Both the OpenAPI and the
|
|
2865
|
+
AsyncAPI merge used to hand the living contract back through the `yaml` library's printer, which
|
|
2866
|
+
re-folds every plain scalar at 80 columns, re-pads every flow sequence (`[a, b]` → `[ a, b ]`) and
|
|
2867
|
+
turns CRLF into LF — so a delta adding one channel rewrote a long `description` three sections away.
|
|
2868
|
+
`lineWidth: 0` is not the fix (measured: it joins every scalar the author folded by hand onto one
|
|
2869
|
+
line). The merge now SPLICES: the living and merged trees are walked block map by block map, every
|
|
2870
|
+
entry whose resolved value is unchanged keeps its bytes, a changed entry is descended into or
|
|
2871
|
+
replaced, a new one is printed after the entry the merge placed it behind, a removed one is cut with
|
|
2872
|
+
the comment lines directly above it, and new lines take the file's own line ending. Only those
|
|
2873
|
+
entries are printed by the library. The spliced text is parsed back and must equal the merged tree
|
|
2874
|
+
value for value; a shape the walk does not understand (a non-scalar key, a key spelled twice, a
|
|
2875
|
+
reorder, an alias it cannot print alone) or a mismatch falls back to the whole-document write, so
|
|
2876
|
+
the splice can narrow a diff and can never change what a contract says. `loam rebase` writes its
|
|
2877
|
+
`x-loam-based-on` pins and `x-loam-baselines` record into a FEATURE's contract the same way, so a
|
|
2878
|
+
rebase changes those lines and no others in the author's file.
|
|
2879
|
+
|
|
2880
|
+
**The delta's `info` is not merged, and the plan says so.** Neither merge carries `info` (nor any
|
|
2881
|
+
root key outside the sections it merges) — a delta is a diff of operations and slots, and its `info`
|
|
2882
|
+
is most often a copy. But a delta that DID move it lost the change at exit 0 with nothing in the
|
|
2883
|
+
plan. Now every `info` key the delta declares with a value different from the living contract's is
|
|
2884
|
+
reported — `openapi.info-not-merged` / `asyncapi.info-not-merged`, a warning naming the keys
|
|
2885
|
+
(`info.version`, `info.description`) — including for a delta whose only change is its `info`, which
|
|
2886
|
+
merges nothing at all. Keys the delta leaves out are not compared. If the `info` should move, edit
|
|
2887
|
+
the living contract in the same change.
|
|
2888
|
+
|
|
1709
2889
|
The landscape merge **splices authored source, it does not re-serialize**. Each new element and
|
|
1710
2890
|
relationship is copied into the living landscape byte-for-byte from `delta.likec4` — technology,
|
|
1711
2891
|
style, icons, links, metadata, nested children, every tag — with exactly one edit: the feature's own
|
|
1712
2892
|
tag is stripped (the additions are baseline now — the drop above is why unarchive restores bytes),
|
|
1713
|
-
|
|
2893
|
+
together with the `,` or `;` that set it apart in a tag list (`#FEAT-1, #x` lands as `#x`), and a
|
|
2894
|
+
construct the strip empties goes with it (`x = kind 'y' { #FEAT-1 }` lands as `x = kind 'y'`).
|
|
1714
2895
|
Placement is deterministic and **service-grouped**, and never moves an authored byte — it only
|
|
1715
2896
|
chooses where the new blocks go. A top-level element whose service the model already draws (the
|
|
1716
2897
|
`metadata { service }` binding first, the title as fallback) lands immediately after the last
|
|
@@ -1725,18 +2906,129 @@ its leading statement (an anchored join sits where its anchor is, wherever its o
|
|
|
1725
2906
|
sort, so comparing per statement would make placement depend on which archive ran first). That is
|
|
1726
2907
|
what makes the scheme **order-independent**: archiving feature A then feature B (touching different
|
|
1727
2908
|
services) yields the byte-identical landscape to B then A, so concurrent archive PRs usually splice
|
|
1728
|
-
different lines and git merges them without help. The
|
|
2909
|
+
different lines and git merges them without help. The relationship walk sorts **only within the
|
|
2910
|
+
region's ascending tail** — the run of cluster heads that already reads in key order from the end,
|
|
2911
|
+
which loam's own insertions keep that way — and stops where the existing order descends. A
|
|
2912
|
+
hand-written relationship region is sorted by nobody, and walking into it made the landing spot a
|
|
2913
|
+
function of unrelated edges: turning one hand-drawn edge round (`svc -> broker` to
|
|
2914
|
+
`broker -[delivers]-> svc`) changed the head key the walk compared and moved an addition sixty lines
|
|
2915
|
+
away. On a region loam wrote the bound changes nothing, and it keeps the scheme order-independent:
|
|
2916
|
+
the statements before the tail never change, so every archive inserts into the same sorted run. (The
|
|
2917
|
+
element walk keeps its plain insertion sort over the few top-level elements, as before.) Wherever a
|
|
2918
|
+
walk stops **before** a statement, or at the closing brace, the addition lands **above the comment
|
|
2919
|
+
written directly over it** — a comment is about what follows it, or heads the section it opens, and
|
|
2920
|
+
an addition spliced at the statement's own line sat between a six-line explanation and the edge it
|
|
2921
|
+
explained. "Directly" is literal: a blank line ends the comment run, so a comment separated from
|
|
2922
|
+
the statement below it by a blank line stands alone and stays where it is, and the tail of a block
|
|
2923
|
+
comment opened on an earlier line is never split. The same rule at both stopping points is what
|
|
2924
|
+
keeps two additions landing in one gap order-free. The insertions carry no marker comment — which
|
|
1729
2925
|
archive added what is the git history's to say. A **nested** element lands inside its parent's block
|
|
1730
2926
|
— inside the living parent when the landscape already has it, riding verbatim inside the spliced
|
|
1731
|
-
parent when the parent is new — never as a flat dotted id, which LikeC4 rejects at top level.
|
|
2927
|
+
parent when the parent is new — never as a flat dotted id, which LikeC4 rejects at top level. A
|
|
2928
|
+
landscape may be written in **several top-level `model { }` blocks**, which LikeC4 reads as one
|
|
2929
|
+
model, and the merge reads all of them: an ANCHORED addition — nested in a parent, joining its
|
|
2930
|
+
service's neighborhood, or joining its source's edges — lands in its anchor's block, and an
|
|
2931
|
+
unanchored one lands in the trailing region of the **first** block, the one every addition went to
|
|
2932
|
+
before a second block was readable. The walks never cross a block boundary, so the scheme stays
|
|
2933
|
+
order-independent, and a one-block map places exactly as before. LikeC4
|
|
1732
2934
|
reference resolution is not order-dependent, so a relationship placed before an element it names
|
|
1733
2935
|
still parses — and because splicing is text surgery, the computed landscape is **parsed in memory
|
|
1734
2936
|
before anything is written**: a result LikeC4 rejects — a splice bug, or legal inputs the living
|
|
1735
2937
|
document cannot absorb, like an element kind or tag its `specification` block never declares —
|
|
1736
2938
|
refuses the archive at plan time (`merge-failed`, nothing written), the same discipline as the
|
|
1737
|
-
unparseable delta. A living declaration
|
|
1738
|
-
|
|
1739
|
-
|
|
2939
|
+
unparseable delta. A living declaration the splice map cannot locate refuses the same way rather
|
|
2940
|
+
than be spliced around blind — most often one sharing its line with another statement, which is
|
|
2941
|
+
legal LikeC4 the splice map cannot see around, since it reads one statement per line. The refusal
|
|
2942
|
+
names the line LikeC4 parsed the declaration on and what the splice map read there instead, and
|
|
2943
|
+
says "shares that line" only when the line shows it: give each top-level declaration its own line,
|
|
2944
|
+
then re-run.
|
|
2945
|
+
|
|
2946
|
+
**Which document an addition lands in.** The C4 merge asks it per declaration, not per feature. An
|
|
2947
|
+
element whose PARENT resolves to a service whose `model.likec4` extends the map is spliced into that
|
|
2948
|
+
model — and so is the interior a delta writes inside a service element the same archive introduces,
|
|
2949
|
+
since those containers carry no feature tag and would otherwise ride onto the map inside their
|
|
2950
|
+
parent's bytes. The parent chain has to be spelled in the map's own ids for either to happen. The
|
|
2951
|
+
splice goes inside the `extend <fqn> { … }` block for the parent — in whichever `model { }` block
|
|
2952
|
+
holds it, and appended to the first one when the model has none, as
|
|
2953
|
+
the `model { }` block itself is appended to a model that has none (an empty file, a `views { }`-only
|
|
2954
|
+
file and a tags-only `specification { }` are all extending models, and all three are legal to
|
|
2955
|
+
archive into). A
|
|
2956
|
+
relationship goes to the model of the service owning its SOURCE when the source is interior to such
|
|
2957
|
+
a service, else to the model owning its TARGET, else to the map: a service-level edge stays where
|
|
2958
|
+
every fleet check reads it. A service with a STANDALONE model, or none yet, owns nothing — the map
|
|
2959
|
+
draws its containers, which is legal and is what every repository did before the extending shape.
|
|
2960
|
+
The merged model is proved the way the grader reads it (the `architecture/` documents with this
|
|
2961
|
+
archive's own merged landscape in place, plus the model) before anything is written, and it appears
|
|
2962
|
+
in the `--json` plan as an ordinary write, so `loam unarchive` restores it from the same
|
|
2963
|
+
`.loam-before` snapshot as any other overwritten file. Two limits are deliberate and each refuses at
|
|
2964
|
+
plan time with nothing written: a merged model that does not parse beside the map (`merge-failed`),
|
|
2965
|
+
and a container-level edge whose other end lives in a THIRD document — another service's interior
|
|
2966
|
+
resolves in neither this model nor the map, so that call is drawn service to service on the map. One
|
|
2967
|
+
more is silent: a delta that spells the parent under its own name rather than the map's (the legacy
|
|
2968
|
+
title-join) keeps landing on the map, because splicing its bytes into a document that shares the
|
|
2969
|
+
map's id namespace would carry ids nothing there resolves. It is the WHOLE chain that decides, not
|
|
2970
|
+
the outermost box — `marketplace.orderSvc.cache` under a map that spells the service
|
|
2971
|
+
`marketplace.orderService` is a title join and stays on the map, even though the group
|
|
2972
|
+
`marketplace` is the map's own. A block a delta writes for a NEW service is split — the box to the
|
|
2973
|
+
map, its containers to the model — unless the block also draws a relationship between those
|
|
2974
|
+
containers. Such an edge names its endpoints by their local names inside the block, so splitting it
|
|
2975
|
+
would leave the map naming children it no longer declares; the block rides whole instead, as it did
|
|
2976
|
+
before routing existed. The block decides once for everything written inside it: a container or an
|
|
2977
|
+
edge the delta tags with the feature follows its block rather than its own tag, so it is placed
|
|
2978
|
+
exactly once whichever way the block goes. Re-archiving is idempotent in the models
|
|
2979
|
+
too: an addition whose id, or whose title inside the same bound service, an extending model already
|
|
2980
|
+
declares is skipped exactly as one the map declares is. The title half is service-scoped in a model
|
|
2981
|
+
and global on the map — the map's global join is the fallback for documents written before
|
|
2982
|
+
`metadata { service }` existed, and a service's interior is where two boxes called `api` would
|
|
2983
|
+
collide.
|
|
2984
|
+
|
|
2985
|
+
**A service the archive creates gets its model, empty.** A service the feature carries
|
|
2986
|
+
`specs/<svc>/` for and the fleet does not enumerate is one whose directory the archive creates; when
|
|
2987
|
+
the map the merge leaves also DRAWS it — the living map, or an element the delta adds, bound to it or
|
|
2988
|
+
titled with its name; a context element the delta only quotes is merged nowhere — the archive writes its
|
|
2989
|
+
`model.likec4` beside its first documents: a header comment and an empty
|
|
2990
|
+
`model { }`, extending the map, with whatever the delta draws inside the service routed into it
|
|
2991
|
+
like any extending model's interior (FEAT-101's `api` container lands there, not on the map). It is
|
|
2992
|
+
a `create` in the plan, so `loam unarchive` deletes it. Two alternatives were rejected. Leaving the
|
|
2993
|
+
file out was the old behaviour: `service.no-model` on the next `validate --all` after an archive
|
|
2994
|
+
that exited 0, and the new service's containers riding onto the map because no model existed to own
|
|
2995
|
+
them. Writing a container model would be inventing one. An EMPTY extending model invents nothing —
|
|
2996
|
+
LikeC4 reads it as the map's box with nothing drawn inside, and an empty file is already a legal
|
|
2997
|
+
extending model — while it lets the service's first `validate --all` grade it rather than stop at a
|
|
2998
|
+
missing file. A team that pre-placed an empty extending model got exactly this result; nobody has to
|
|
2999
|
+
know to now. The feature gate's merge preview routes the same way, so a feature flow hopping into
|
|
3000
|
+
the new service's container is `usecase.flow-invalid` before the archive, as it would be
|
|
3001
|
+
`landscape.invalid` after it — and a feature deployment document naming it
|
|
3002
|
+
`deployment.doc-invalid`. A service whose directory already exists is enumerated and left alone.
|
|
3003
|
+
`service.no-model` is still owed — at error severity, never gating — in two shapes: a service the
|
|
3004
|
+
map gains with no `specs/<svc>/` behind it (there is no directory, `landscape.binding-unknown`
|
|
3005
|
+
follows, and a model would be the one document in a directory the archive invented), and a
|
|
3006
|
+
`specs/<svc>/` the merged map never draws, which the gate already calls an unknown service and only
|
|
3007
|
+
`--approve` brings to the merge — a model there would fabricate a service out of what is usually a
|
|
3008
|
+
typo, which is also why the spec merge creates no empty living spec.
|
|
3009
|
+
|
|
3010
|
+
**Where it lands is the tree's to say, never the map's.** The directory is always the unfiled
|
|
3011
|
+
`services/<svc>/`. The archive does not read a subsystem off the delta's group nesting — the map's
|
|
3012
|
+
grouping and the tree are independent (Subsystems, above), and a map edit must never decide a
|
|
3013
|
+
directory — and it takes no `metadata { subsystem }` hint either: that key would be spliced into the
|
|
3014
|
+
authored landscape, writing tree placement into the one document the tree is never mirrored into.
|
|
3015
|
+
The tree is placement's only input. To file a new service at archive time, create its directory
|
|
3016
|
+
first — an empty `model.likec4` at `services/<subsystem>/<svc>/` does it, and the archive then writes
|
|
3017
|
+
the specs there and routes the interior into that model — or move it afterwards with
|
|
3018
|
+
`loam subsystem move <svc> --into <subsystem>`, which the archive prints when the tree has a
|
|
3019
|
+
subsystem.
|
|
3020
|
+
|
|
3021
|
+
**What the splice reads off kinds.** An extending model declares no kinds, so the archive scans it
|
|
3022
|
+
with the map's: without them its `kind name` declarations and its `a .kind b` edges read as property
|
|
3023
|
+
lines, an edge routed into the model landed above a dotted edge, a delta restating a dotted edge
|
|
3024
|
+
added a second copy, and a child of a kind-first container was refused as nesting under nothing.
|
|
3025
|
+
Kinds declared in an `architecture/` document other than the landscape are not borrowed — the
|
|
3026
|
+
landscape is the extending shape's one `specification`. And a relationship kind may carry tags
|
|
3027
|
+
(`relationship publishes { #async }`, LikeC4 1.59.0+): the parse hands every edge of the kind those
|
|
3028
|
+
tags, the delta's statement shows only what is written in it, so the splice matches a parsed addition
|
|
3029
|
+
to its statement with the kind's tags added to the written ones. Matched as written, every edge of a
|
|
3030
|
+
tagged kind found no statement — `merge-failed` from an archive `validate --feature` had called ready,
|
|
3031
|
+
or `usecase.flow-invalid` when the feature had flows.
|
|
1740
3032
|
|
|
1741
3033
|
The OpenAPI merge carries components along with operations, and since the path-item/component
|
|
1742
3034
|
baselines it decides each one **by verdict rather than by reachability**: a component the feature
|
|
@@ -1753,6 +3045,14 @@ the written content and copied components that resolves in **neither** document
|
|
|
1753
3045
|
overrides it — unlike the mechanical refusals, this one is a judgment call. External references —
|
|
1754
3046
|
URLs, file paths, anything not starting `#/` — are out of scope: left untouched, never gated.
|
|
1755
3047
|
|
|
3048
|
+
A service with no living contract gets the feature's `openapi.yaml`, stripped of removal markers —
|
|
3049
|
+
unless that document describes nothing: no path, no webhook and no component in any section. Then no
|
|
3050
|
+
living `openapi.yaml` is created and the plan says `nothing to merge`, the spec axis's rule for a
|
|
3051
|
+
delta that leaves no requirements. A `paths: {}` stub published as a living contract says "this
|
|
3052
|
+
service exposes nothing" to every consumer that joins against it; with no file the service reads as
|
|
3053
|
+
one with no HTTP surface, which `service.no-openapi` already grades on evidence — silent while
|
|
3054
|
+
nothing calls it, an error once something does.
|
|
3055
|
+
|
|
1756
3056
|
`loam archive --json` speaks the same machine contract as everything else: on success (or
|
|
1757
3057
|
`--dry-run`) the plan (`{path, action, to?}` per file), the non-blocking warnings, and whatever
|
|
1758
3058
|
`--approve` overrode; on refusal a stable `error.code` — `not-coherent` and
|
|
@@ -1779,6 +3079,16 @@ lists the files that need a hand.
|
|
|
1779
3079
|
- `commit-interrupted` — a previous `archive`/`unarchive` was killed mid-commit and this run cannot
|
|
1780
3080
|
repair it on its own.
|
|
1781
3081
|
|
|
3082
|
+
What `unarchive` restores is what the archive WROTE, and the two generated views files were never
|
|
3083
|
+
among it: `loam subsystem sync` owns them, and a sync run between the archive and the undo rewrote
|
|
3084
|
+
them against the merged map. So after the restore `unarchive` asks the question archive asks after
|
|
3085
|
+
its merge — through `validate`'s own staleness grade — and when `architecture/subsystems.likec4` or
|
|
3086
|
+
`architecture/services.likec4` no longer matches the restored tree and map it names each one
|
|
3087
|
+
(`⚠ <path> no longer matches the tree and this map — run \`loam subsystem sync\``) instead of
|
|
3088
|
+
closing on "the living docs are back to what they said before the archive", and `--json` carries
|
|
3089
|
+
them as `staleViews` — the same key, with the same meaning, as `archive --json`'s; absent when
|
|
3090
|
+
nothing is stale.
|
|
3091
|
+
|
|
1782
3092
|
Rules (`loam validate`): every requirement has ≥1 scenario; every C4 edge `op` resolves to an
|
|
1783
3093
|
OpenAPI operation governed by a requirement; and **the diff applies** — a `MODIFIED`/`REMOVED`
|
|
1784
3094
|
requirement exists in the living spec, an `ADDED` one does not, and a section heading matches
|
|
@@ -1808,12 +3118,14 @@ design. The heading must be reachable, too — a leading UTF-8 BOM used to hide
|
|
|
1808
3118
|
|
|
1809
3119
|
## The verification record
|
|
1810
3120
|
|
|
1811
|
-
|
|
3121
|
+
The done-check is written down inside the feature: one `features/<FEAT>/verification/<svc>.yaml` per
|
|
3122
|
+
attesting service, or — for the all-at-once form, and every record written before the directory —
|
|
3123
|
+
the one file `features/<FEAT>/verification.yaml`. `loam verify <FEAT>` derives a
|
|
1812
3124
|
**checklist** from the feature's own artifacts — the same files `validate` reads, never the code:
|
|
1813
3125
|
|
|
1814
3126
|
| kind | one claim per | reads |
|
|
1815
3127
|
|---|---|---|
|
|
1816
|
-
| `service.exists` | tagged top
|
|
3128
|
+
| `service.exists` | tagged element the delta draws as a service of ours: at top level or only inside groupings the delta declares (elements that stand for no service and contain one — any other level owns what is drawn inside it), and not a person, `#external`, unbound infrastructure or a grouping itself. The stub of a living element is read with the living element's tags, and the stub of a service that already has a `services/<id>/` is asked about only when tagged at top level | `delta.likec4` |
|
|
1817
3129
|
| `api.exposes` | operationId the feature's openapi delta adds that the living one lacks | `specs/<svc>/openapi.yaml` |
|
|
1818
3130
|
| `event.declares` | (direction, message) the feature's asyncapi delta declares that the living contract's send/receive sets lack — quotes and removals are never questions | `specs/<svc>/asyncapi.yaml` |
|
|
1819
3131
|
| `c4.calls` | tagged edge carrying `metadata { op }` | `delta.likec4` |
|
|
@@ -1832,18 +3144,26 @@ nobody wrote must not carry over as if it still applied.
|
|
|
1832
3144
|
anything that does not answer the *current* checklist, each under its own `--json` `error.code`:
|
|
1833
3145
|
`answers-unreadable` (not JSON, or a verdict outside `confirmed` / `unconfirmed`),
|
|
1834
3146
|
`answers-mismatch` (an id nobody asked about, a claim with no answer, or one answered twice),
|
|
1835
|
-
`answers-unevidenced` (a `confirmed` with no `file:line` behind it
|
|
1836
|
-
|
|
3147
|
+
`answers-unevidenced` (a `confirmed` with no `file:line` behind it, or one citing a file or a line
|
|
3148
|
+
that does not exist in the repository the record is written from — the all-at-once form included,
|
|
3149
|
+
which binds to no commit but no longer takes a citation on trust). The all-at-once form checks what
|
|
3150
|
+
is shaped like a citation and only that: an evidence entry that is one token ending in `:<digits>`
|
|
3151
|
+
and is not a URL must name a regular file inside the repository with that line, while prose ("manual
|
|
3152
|
+
QA by Alice"), a URL or a path with no line is recorded as it always was. The federated form keeps
|
|
3153
|
+
its stricter contract — every agent entry a `file:line` bound to the attested commit. An unchecked
|
|
3154
|
+
claim must never be able to masquerade as checked.
|
|
1837
3155
|
|
|
1838
3156
|
**`--results <report.json>` answers the `scenario.tested` claims mechanically, from a cucumber JSON
|
|
1839
|
-
test report** — the format `cucumber-js --format json` emits, and cucumber-jvm, behave
|
|
1840
|
-
speak too. The contract is exactly this and nothing more: a top-level array of features, each
|
|
3157
|
+
test report** — the format `cucumber-js --format json` emits, and cucumber-jvm, behave,
|
|
3158
|
+
SpecFlow/Reqnroll, pytest-bdd (`--cucumberjson`) and godog's cucumber formatter speak too. The contract is exactly this and nothing more: a top-level array of features, each
|
|
1841
3159
|
`elements[]` (scenarios) with `name`, `tags[] {name}` and `steps[] {result {status}}` — plus the two
|
|
1842
3160
|
places those dialects put a failure the steps never see: cucumber-jvm's per-element
|
|
1843
3161
|
`before[]`/`after[]` hook results and behave's element-level `status`. Every other field is ignored,
|
|
1844
3162
|
tagless elements (backgrounds, hand-written scenarios) are invisible, and a file that is not that
|
|
1845
3163
|
array at all refuses under `answers-unreadable` rather than quietly answering every claim "not
|
|
1846
|
-
found".
|
|
3164
|
+
found". A file that is not JSON at all — a JUnit XML report is the usual one — refuses the same way,
|
|
3165
|
+
and the refusal names both shapes `--results` reads, because the second one exists for exactly that
|
|
3166
|
+
runner.
|
|
1847
3167
|
|
|
1848
3168
|
Matching is by digest and nothing else: a claim is answered by the report scenarios carrying its
|
|
1849
3169
|
`@loam-digest-<16hex>` tag — the tag `loam gherkin` stamped, the same 16 hex of the same body hash
|
|
@@ -1852,12 +3172,21 @@ name), so a business run can never answer an arch claim however identically the
|
|
|
1852
3172
|
worded. Names never match anything: a reworded spec scenario matches nothing until the suite is
|
|
1853
3173
|
regenerated and re-run — an agent must not be able to SAY a scenario is tested; only a green run
|
|
1854
3174
|
may. The verdicts: every matching occurrence (a digest the report holds twice is a re-run, and all
|
|
1855
|
-
occurrences count) ran at least one step, every step `passed`, every before/after hook `passed`,
|
|
1856
|
-
|
|
3175
|
+
occurrences count) ran at least one step, every step `passed`, every before/after hook `passed`, the
|
|
3176
|
+
element-level status (when the dialect carries one) is `passed`, and **it ran exactly as many steps
|
|
3177
|
+
as the stamped scenario has** — the count `loam gherkin` emits for it, `And`/`But` included →
|
|
3178
|
+
`confirmed`, with evidence
|
|
1857
3179
|
`<report-path>: <feature uri or name> › <scenario name>` per occurrence; a failed / undefined /
|
|
1858
3180
|
pending / ambiguous step (`failed at step N`), a failing hook (`failed in a before/after hook`), a
|
|
1859
3181
|
failed element status, a skipped-only run (`skipped`), or no match at all (`not found in report`) →
|
|
1860
|
-
`unconfirmed` with the reason as the note
|
|
3182
|
+
`unconfirmed` with the reason as the note — and a run whose step count differs from the stamped
|
|
3183
|
+
scenario's is `unconfirmed` too (`ran N step(s) where the stamped scenario has M`). The digest is
|
|
3184
|
+
only a tag, and a tag is text: a one-step stub carrying the right digest used to confirm five-step
|
|
3185
|
+
scenarios, because nothing compared what ran with what was stamped. The count is what every dialect
|
|
3186
|
+
can be held to; the step TEXT is not compared, because an outline's steps reach the report with
|
|
3187
|
+
their placeholders filled in and dialects disagree about keyword spacing and localisation. The hook
|
|
3188
|
+
entries cucumber-js lists among `steps[]` (marked `hidden`, keyword `Before`/`After`) are read as
|
|
3189
|
+
hooks, never counted as steps — and a failing one still sinks the run.
|
|
1861
3190
|
|
|
1862
3191
|
`--results` OWNS every `scenario.tested` claim **in the same run**: an answers-file entry for one is
|
|
1863
3192
|
refused (`answers-mismatch` — the runner owns it), `--record` alongside must answer exactly the
|
|
@@ -1889,6 +3218,8 @@ own runner-neutral shape, chosen by its marker key:
|
|
|
1889
3218
|
```
|
|
1890
3219
|
|
|
1891
3220
|
`digest` is the 16 lowercase hex of the `@loam-digest-…` tag `loam gherkin` stamped on the scenario.
|
|
3221
|
+
An entry carries no steps, so it is held to its `status` alone — the step-count check above is a
|
|
3222
|
+
property of the cucumber shape.
|
|
1892
3223
|
`status` is `passed` or `failed` — nothing else. `test` is optional free text: the runner's own name
|
|
1893
3224
|
for the run, used as the evidence string a reader follows. `loamScenarioReport: 1` is the format
|
|
1894
3225
|
version this loam reads; any other value refuses rather than being partly understood.
|
|
@@ -1914,12 +3245,18 @@ carrying the marker is always graded as this shape, even when malformed: falling
|
|
|
1914
3245
|
cucumber parser would tell an author their report is not a cucumber array, which is true, useless,
|
|
1915
3246
|
and hides the real mistake.
|
|
1916
3247
|
|
|
1917
|
-
**
|
|
1918
|
-
|
|
1919
|
-
|
|
1920
|
-
|
|
1921
|
-
|
|
1922
|
-
|
|
3248
|
+
**Scenario digests are salted by both the owning service and the axis** (business versus arch), so
|
|
3249
|
+
two services wording a scenario identically do not share one: the hash folds in the service the
|
|
3250
|
+
claim is filed under. The service salt is what makes ordinary repeated wording CORRECT rather than
|
|
3251
|
+
merely refused — "the service returns 404 for an unknown id" is a sentence a fleet writes many
|
|
3252
|
+
times, and before the salt those scenarios shared a digest, so one repository's green run could
|
|
3253
|
+
appear to confirm claims from suites that never ran each other's tests, and the only defence was to
|
|
3254
|
+
decline to answer. `verify.digest-contested` (warn) survives as the guard on that invariant rather
|
|
3255
|
+
than as a diagnosis of ordinary authoring: if a digest is ever claimed by two services it is a
|
|
3256
|
+
truncated-hash collision, a single report still cannot say whose suite ran it, and those claims are
|
|
3257
|
+
left `unconfirmed` naming both services and the affected claim ids rather than confirmed from one
|
|
3258
|
+
run. Record each service's claims from its own repository with `--service`, which narrows the
|
|
3259
|
+
checklist before matching.
|
|
1923
3260
|
|
|
1924
3261
|
### Contract-test reports answer the `api.exposes` claims
|
|
1925
3262
|
|
|
@@ -2013,7 +3350,7 @@ service's as a pure lens — no repository binding, usable from the docs repo, r
|
|
|
2013
3350
|
`unknown-service` when the checklist has no claims owned by that id — and both agents must have
|
|
2014
3351
|
answered the same scope. Combining the flag with `--record`, `--results` or `--contract-results`
|
|
2015
3352
|
refuses `invalid-option`, as does any arity other than exactly two files and an archived feature
|
|
2016
|
-
(frozen history has no current checklist to answer). A
|
|
3353
|
+
(frozen history has no current checklist to answer). A verification record too damaged to read does
|
|
2017
3354
|
**not** block the diff — the lens never reads the record, and the moment a cross-examination is most
|
|
2018
3355
|
needed is before re-recording over a broken one.
|
|
2019
3356
|
|
|
@@ -2033,7 +3370,7 @@ block pins each file's path and the sha256 of the exact bytes read.
|
|
|
2033
3370
|
confirmed/unconfirmed/unanswered. The payload also deliberately carries **no `verified`, `verdict`
|
|
2034
3371
|
or `attested` key at all**: two answer sets agreeing — usually two contexts of the same model, so
|
|
2035
3372
|
correlated — ranks where a reviewer reads first; it is not verification, it upgrades nothing, and it
|
|
2036
|
-
leaves
|
|
3373
|
+
leaves the verification record byte-identical (no docs lock is taken, and the diff runs while a writer
|
|
2037
3374
|
holds one).
|
|
2038
3375
|
|
|
2039
3376
|
### Attested is not verified
|
|
@@ -2068,28 +3405,79 @@ the decision:
|
|
|
2068
3405
|
So "a green run is the only thing that confirms a scenario" is a claim about the **record**, not
|
|
2069
3406
|
about the CLI's flags: the short path still works, and it is legible afterwards as the short path.
|
|
2070
3407
|
|
|
2071
|
-
### The record is federated: one
|
|
3408
|
+
### The record is federated: one attestation per service, one file each
|
|
2072
3409
|
|
|
2073
3410
|
A cross-service feature's claims belong to several services, and each service's code lives in its
|
|
2074
3411
|
own repository — so no single place can honestly answer all of them.
|
|
2075
3412
|
`loam verify <FEAT> --service <id> --record answers.json`, run **in that service's own repo**,
|
|
2076
3413
|
narrows the checklist to that service's claims and binds the answers to this repository's commit.
|
|
2077
|
-
The result is
|
|
2078
|
-
|
|
2079
|
-
|
|
2080
|
-
|
|
2081
|
-
|
|
2082
|
-
|
|
2083
|
-
|
|
2084
|
-
|
|
2085
|
-
|
|
2086
|
-
|
|
2087
|
-
|
|
2088
|
-
|
|
2089
|
-
|
|
2090
|
-
|
|
2091
|
-
|
|
2092
|
-
|
|
3414
|
+
The result is that service's **attestation**, written to a file of its own:
|
|
3415
|
+
`features/<FEAT>/verification/<id>.yaml` — a `schema: 2` record holding exactly that service's
|
|
3416
|
+
attestation (the service id, the commit the answers were bound to, the checklist digest it answered,
|
|
3417
|
+
and its answered claims) and nothing of anybody else's. **The directory is the record**, the rule
|
|
3418
|
+
`glossary/` and `capabilities/` already follow: loam reads every file in it and adds them up, and
|
|
3419
|
+
the verdict and the counts are computed from the union at read time — exactly as they always were,
|
|
3420
|
+
since nothing ever trusted a stored `summary`. Each file's own `summary` counts the whole checklist,
|
|
3421
|
+
with the claims other services own honestly `unanswered` in it.
|
|
3422
|
+
|
|
3423
|
+
**Why one file per service.** The record used to be ONE `verification.yaml` for every service, and
|
|
3424
|
+
every attestation rewrote its `summary`, its `recorded` date and its checklist-ordered `claims`. Two
|
|
3425
|
+
teams recording two different services on two docs-repo branches therefore always conflicted when
|
|
3426
|
+
the PRs merged — add/add from an empty start, content conflicts on the summary, the claims and the
|
|
3427
|
+
attestations once one had landed — and sorting the attestations did not help, because two
|
|
3428
|
+
insertions at one anchor are a conflict whatever the order. The advisory lock below serializes
|
|
3429
|
+
writers on one checkout only; a fleet records from many. With a file per service, two teams' records
|
|
3430
|
+
touch two different paths and merge cleanly. Recording a service again replaces that service's file
|
|
3431
|
+
and nothing else; another service's file is never rewritten, and its pins never touched.
|
|
3432
|
+
|
|
3433
|
+
The record answers **one version of the checklist**: the one the feature asks now — `loam verify`
|
|
3434
|
+
(read and record alike), `tasks`, and `status`, `list` and `gate` for an in-flight feature all
|
|
3435
|
+
derive it, and the files that answered it are the ones that count. Only where no reader knows the
|
|
3436
|
+
current version (the frozen view of an archived feature) or no file answered it does the newest
|
|
3437
|
+
file decide (by `recorded`, a later service id breaking a same-day tie). Newest-by-date used to
|
|
3438
|
+
decide always, and `recorded` is a date: a service that recorded the current checklist lost a
|
|
3439
|
+
same-day tie to a service with a later id that had recorded an older one, so the current answers
|
|
3440
|
+
were dropped and every reader called the record stale. A file that answered a different version
|
|
3441
|
+
keeps its attestation in the list — `verify.checklist-forked` names it, on the recording run's
|
|
3442
|
+
output as well as the read view — but its answers do not count until that service records again.
|
|
3443
|
+
The one shared file used to settle this by rewriting: the last writer pruned everybody's answers to
|
|
3444
|
+
its own checklist. Per-service files are never rewritten by anyone else, so nothing can prune them,
|
|
3445
|
+
and counting an answer filed against another question set would let an id the feature no longer
|
|
3446
|
+
asks fill the slot of the question that replaced it.
|
|
3447
|
+
|
|
3448
|
+
**`features/<FEAT>/verification/` is loam's.** Every non-dot `*.yaml` in it is read as one
|
|
3449
|
+
service's attestation file — the file name is the claim about whose word it holds — so a file there
|
|
3450
|
+
that is not one (a hand-made `k6.yaml`, say) makes the record `record-unreadable`, exactly like a
|
|
3451
|
+
damaged attestation. Keep anything else beside the feature, not inside that directory.
|
|
3452
|
+
|
|
3453
|
+
**The one-file record is read forever.** A `verification.yaml` written before the directory existed
|
|
3454
|
+
— a federated record with several attestations in it, active or archived — reads exactly as it
|
|
3455
|
+
always did while no service has a file of its own. Once services do, it is read beside them: a
|
|
3456
|
+
service that records again supersedes its share of the one-file record (that attestation and its
|
|
3457
|
+
answers drop out of the union; the file itself is not rewritten), and the write that leaves nothing
|
|
3458
|
+
in it counting removes it. An all-at-once record (no `schema`, no attestations) never counts once a
|
|
3459
|
+
service has a file — its answers carry no commit attestation — so the first `--service` write
|
|
3460
|
+
removes it in the same commit and lists its answers in `discarded`. Answers a write takes off the
|
|
3461
|
+
disk that its new file does not carry are reported in `discarded`, tagged `unattested` (an
|
|
3462
|
+
all-at-once answer) or `off-checklist` (a claim the current checklist no longer asks). Nothing is
|
|
3463
|
+
dropped silently.
|
|
3464
|
+
|
|
3465
|
+
**The all-at-once form keeps the one file, by decision.** `--record` without `--service` answers the
|
|
3466
|
+
whole checklist from one place on one word, with no commit and no service — so there is no second
|
|
3467
|
+
writer for it to collide with and no service to name a file after, and every record it ever wrote
|
|
3468
|
+
(the example fleet's included) stays byte-for-byte what it is. There is one reader either way:
|
|
3469
|
+
`loam verify`, `status`, `list`, `tasks`, `gate` and `validate` all read the record through the same
|
|
3470
|
+
union, whichever files it is made of.
|
|
3471
|
+
|
|
3472
|
+
The write holds the same advisory lock archive does, and holds it from before the feature is
|
|
3473
|
+
resolved to after the record lands — two `--record` runs for different services serialize (the
|
|
3474
|
+
later one waits briefly, then writes its own file, so **both** attestations survive), and a record
|
|
3475
|
+
can no longer be written into a path an `archive` is renaming out from under it. The commit itself
|
|
3476
|
+
is staged beside the target and swapped in by one rename over the exact bytes the read found, so a
|
|
3477
|
+
kill mid-record leaves the previous record whole — never truncated YAML — and an edit that landed in
|
|
3478
|
+
between refuses instead of being buried. A write that also removes a superseded one-file record
|
|
3479
|
+
holds that removal to the same compare, and lands the service's own file first, so a kill between
|
|
3480
|
+
the two leaves a record that still reads whole:
|
|
2093
3481
|
|
|
2094
3482
|
- `docs-busy` — another writer held the lock longer than the record was willing to wait (5 s).
|
|
2095
3483
|
Nothing was read or written; re-run once it finishes. The contention runs both ways: `archive`,
|
|
@@ -2110,9 +3498,10 @@ Recording refuses rather than guesses, each under its own `--json` `error.code`:
|
|
|
2110
3498
|
- `repository-unavailable` — `--service` from a repo that declares none, or one whose commit cannot
|
|
2111
3499
|
be read. An attestation with nothing to bind to is not an attestation.
|
|
2112
3500
|
- **`record-federated`** — the legacy all-at-once form (`--record` with **no** `--service`) over a
|
|
2113
|
-
record that already carries attestations
|
|
2114
|
-
|
|
2115
|
-
|
|
3501
|
+
record that already carries attestations, in per-service files or in a one-file record. That form
|
|
3502
|
+
writes the whole record from one place, so it would put one unattested answer set over every
|
|
3503
|
+
other repository's evidence; loam refuses outright and names the attestors, and never silently
|
|
3504
|
+
migrates or merges. Re-run with `--service`.
|
|
2116
3505
|
- `record-unreadable` — see below.
|
|
2117
3506
|
|
|
2118
3507
|
Reading is unrestricted: `loam verify <FEAT> --service <id>` **without** `--record` is a pure lens
|
|
@@ -2121,12 +3510,26 @@ in `services`, which is how you find out which repositories still owe an attesta
|
|
|
2121
3510
|
|
|
2122
3511
|
The record is YAML, like the other data artifacts, and holds the claim text next to each verdict so
|
|
2123
3512
|
it reads without loam. Because it has to survive without loam, human edits are expected — and read
|
|
2124
|
-
back defensively. A file that exists but does not read as a record (
|
|
3513
|
+
back defensively. A record file that exists but does not read as a record (git conflict markers a
|
|
3514
|
+
merge left in it — named before the YAML parser sees them, since the parser either fails on a line
|
|
3515
|
+
that is not the problem or reads both sides as one plausible document; broken YAML; a shape missing
|
|
2125
3516
|
what the readers use — `summary` with its counts, `recorded`, `checklist`, claims each with an `id`
|
|
2126
|
-
and a known `verdict
|
|
3517
|
+
and a known `verdict`; or a `verification/<svc>.yaml` holding anything but that one service's
|
|
3518
|
+
attestation and answers) is **`record-unreadable`**: reported with the file and the line, and **never
|
|
2127
3519
|
overwritten**. It used to count as *no record at all*, which was the wrong direction on the one
|
|
2128
3520
|
artifact loam cannot reconstruct — "no record" invites writing a fresh one over evidence somebody's
|
|
2129
|
-
edit merely broke. Only a genuinely absent file is "no verification record".
|
|
3521
|
+
edit merely broke. Only a genuinely absent file is "no verification record".
|
|
3522
|
+
|
|
3523
|
+
The refusal names the ONE file that failed and the repair that is safe for it, because the two kinds
|
|
3524
|
+
of file call for opposite advice. A service's own `verification/<svc>.yaml` holds that service's
|
|
3525
|
+
word and nobody else's — after a merge, both sides of a conflict in it are that service's own
|
|
3526
|
+
attestations — so repairing it by hand, or deleting it and recording that service again from its
|
|
3527
|
+
repository, loses no other team's evidence. The one-file `verification.yaml` may hold every other
|
|
3528
|
+
service's attestation, so its advice is to repair it or **restore it from version control** (after a
|
|
3529
|
+
merge, the copy on the branch you merged into) and then record again — **never to delete it**. The
|
|
3530
|
+
old one-size advice, "delete it and record again", erased other teams' attestations when followed on
|
|
3531
|
+
a shared record, and the miscount advice, "record the answers again", sent the reader straight back
|
|
3532
|
+
into this refusal: `--record` will not write over a file it could not read. Both now end in a record. Extra keys a human adds
|
|
2130
3533
|
still pass through untouched. Every verdict names who answered it — `answered_by: runner` (a
|
|
2131
3534
|
cucumber report's green run, mechanical), `answered_by: external-runner` (a contract-test report's
|
|
2132
3535
|
operationId-matched green entry, mechanical too), or `answered_by: agent` (somebody's word about the
|
|
@@ -2196,7 +3599,7 @@ next `--record` makes it drift-checkable), warn `evidence.unresolved` (the cited
|
|
|
2196
3599
|
unsafe, not a regular file, or the cited line is past its end), `evidence.moved` (the file's digest
|
|
2197
3600
|
differs while the cited line still reads as pinned), `evidence.line-changed` (the cited line no
|
|
2198
3601
|
longer says what was recorded), `evidence.token-missing` (the file contained the token at the
|
|
2199
|
-
attested commit and no longer does) and `evidence.record-unreadable` (a verification
|
|
3602
|
+
attested commit and no longer does) and `evidence.record-unreadable` (a verification record file that
|
|
2200
3603
|
exists but cannot be read — none of its evidence was checked, and a silent skip would read as clean,
|
|
2201
3604
|
on the very file a hand edit to the pins most plausibly breaks). Archived records are deliberately
|
|
2202
3605
|
not linted — frozen history, and post-ship code drift is normal evolution. From the docs repo the
|
|
@@ -2206,13 +3609,18 @@ verdict in either direction, `attested` never silently becomes `verified`, and t
|
|
|
2206
3609
|
fail `validate`; `--strict` remains the CI lever it always was.
|
|
2207
3610
|
|
|
2208
3611
|
It lives inside the feature, so `archive` carries it into `features/archive/<FEAT>/` with everything
|
|
2209
|
-
else
|
|
3612
|
+
else — the `verification/` directory moves with the feature it sits in, and `unarchive` brings it
|
|
3613
|
+
back the same way; the frozen view reads it through the same union. `loam verify` re-run later compares `checklist` against the current digest and reports the
|
|
2210
3614
|
record as **stale** if the feature moved under it — while the feature is active. Once archived there
|
|
2211
3615
|
is no current checklist to be stale against (the merge itself moved the feature's operations into
|
|
2212
3616
|
the living OpenAPI, so a re-derived checklist could only disagree): `loam verify` on an archived
|
|
2213
3617
|
feature renders the record verbatim as **frozen history** (`frozen: true` under `--json`, no
|
|
2214
3618
|
staleness judgment), and `--record` refuses (`invalid-option` — there is no current checklist for
|
|
2215
|
-
the answers to answer): the record is about the words that shipped, and it stays that way.
|
|
3619
|
+
the answers to answer): the record is about the words that shipped, and it stays that way. One
|
|
3620
|
+
reader holds it to something all the same: `loam gate --since <ref>` grades an archived record
|
|
3621
|
+
against the checklist digest `archive` captured as the feature merged (the snapshot manifest's
|
|
3622
|
+
`checklist`, above), because the deploy question is whether what shipped was what was checked — a
|
|
3623
|
+
record that had gone stale before the archive is `stale` there, never `verified` again.
|
|
2216
3624
|
|
|
2217
3625
|
**Verification does not gate `archive`.** Coherence gates because loam computed it from the
|
|
2218
3626
|
documents; a verdict is an agent's word about code loam never read, and a gate in front of shipping
|
|
@@ -2221,6 +3629,76 @@ comes later, and an `unconfirmed` claim with a note is worth more there than a `
|
|
|
2221
3629
|
can back up. `loam verify` exits 0 either way; branch on `verified` in the `--json` payload, or on
|
|
2222
3630
|
`verdict` when you need to tell `attested` from `unverified`.
|
|
2223
3631
|
|
|
3632
|
+
### The same checklist read forwards (`loam tasks`)
|
|
3633
|
+
|
|
3634
|
+
`loam verify` answers *answered, and by whom*. `loam tasks <FEAT> [--service <id>] [--next]
|
|
3635
|
+
[--json]` answers the other half of the same question — *still open, at which step, and what fact
|
|
3636
|
+
closes it* — and it is the identical checklist read in the other direction. It is derived on every
|
|
3637
|
+
run from the feature's deltas, the generated suite and the verification record; it writes nothing,
|
|
3638
|
+
anywhere, ever, and it adds no artifact for a later run to disagree with.
|
|
3639
|
+
|
|
3640
|
+
**One unit per promise, and a unit's id is the claim's id.** Every claim the checklist derives is
|
|
3641
|
+
one work unit, under the same identity on both commands, so the thing to do and the answer that
|
|
3642
|
+
closes it can never be two different objects. One kind of unit has no claim behind it: an ADDED or
|
|
3643
|
+
MODIFIED requirement with no scenario under it asks the checklist nothing — there is no scenario to
|
|
3644
|
+
test — so `loam tasks` mints a unit of its own for it, of the kind requirement.described. That is
|
|
3645
|
+
the promise nobody has written acceptance criteria for yet, and it is where the loop starts.
|
|
3646
|
+
|
|
3647
|
+
**Where a unit stands** is its `at` field, and the four values are the steps of the loop this
|
|
3648
|
+
document already describes rather than a percentage: `describe` (the requirement carries no scenario
|
|
3649
|
+
at all, so there is nothing to generate a test from), `generate` (the scenario exists and this
|
|
3650
|
+
repository's generated suite does not carry its stamp yet), `implement` (everything else still
|
|
3651
|
+
open), `attested` (an agent's word confirmed the claim and no run has). It is spelled `at`
|
|
3652
|
+
deliberately, and not `stage` or `verdict`: both of those already mean something else on payloads an
|
|
3653
|
+
agent reads beside this one, and a third meaning for either word is how a consumer comes to branch
|
|
3654
|
+
on the wrong field.
|
|
3655
|
+
|
|
3656
|
+
**Done is a fact, not a tick.** A closed unit simply stops appearing, and exactly one thing closes
|
|
3657
|
+
one: a claim recorded `confirmed` in a verification record that is **not stale** against the current
|
|
3658
|
+
checklist. A stale record closes nothing — the words moved under the answers, which is the state
|
|
3659
|
+
`loam verify` already reports. An agent-attested scenario never vanishes either: it stays visible at
|
|
3660
|
+
`attested`, because attested is not verified, and a work list that hid the difference would be
|
|
3661
|
+
exactly the gate that teaches everyone the cheapest way past it is to say yes.
|
|
3662
|
+
|
|
3663
|
+
**The frontier** is the first ten open units in derived order, providers before consumers — ordered
|
|
3664
|
+
on the exact token join `c4.calls.token == api.exposes.operation`, never on a reading of prose.
|
|
3665
|
+
**The order is advice and never a lock**: nothing is assigned, nothing is reserved, and working out
|
|
3666
|
+
of order is legal and invisible to every other command.
|
|
3667
|
+
|
|
3668
|
+
**`--next` is the leaf**: the same packet with `units` narrowed to the frontier head, which is what
|
|
3669
|
+
an orchestrator hands a coding agent. Everything else — the counts, the frontier, the review
|
|
3670
|
+
notes — is left whole on purpose, so a cheap model handed one unit cannot read it as the whole job.
|
|
3671
|
+
The text view prints that leaf in full: the scenario's body verbatim and never reflowed (the bytes
|
|
3672
|
+
the digest was taken over), the digest itself — the same `@loam-digest-<16hex>` join key
|
|
3673
|
+
`loam gherkin` stamps and `loam verify --results` matches on — the `done` block whose `check` is the
|
|
3674
|
+
fact that closes the unit, and the literal follow-up command to run next, spelled out rather than
|
|
3675
|
+
left as a shape to fill in.
|
|
3676
|
+
|
|
3677
|
+
**`--service <id>` is a pure lens.** It narrows the units to one service's without binding a
|
|
3678
|
+
repository, and adds `waitingOn[]`: the open units in OTHER services that this service's units
|
|
3679
|
+
depend on. A team learns what it is blocked by from the same run that tells it what to do. In a
|
|
3680
|
+
service repository (`loam.json` names a `service`) the lens defaults to that service when the
|
|
3681
|
+
feature touches it, because the generated-suite stamps that tell `generate` from `implement` are
|
|
3682
|
+
read from that repository alone; the docs repo, and a feature that does not touch the bound
|
|
3683
|
+
service, get the whole feature.
|
|
3684
|
+
|
|
3685
|
+
**What the payload says about what it could not see.** `stamps` is `read` or `unknown`, and
|
|
3686
|
+
`unknown` means the bound service repo's generated suite could not be read — could-not-look, never
|
|
3687
|
+
nothing-wrong, because a question loam was unable to ask must not read as one that came back clean.
|
|
3688
|
+
Beside it the payload carries `checklist` (the digest of the question set these units derive from),
|
|
3689
|
+
`record` (`absent`, `recorded`, `stale` or `unreadable`), `intent`, `services[]`,
|
|
3690
|
+
`size{requirements,scenarios,operations,messages,calls}` — the size of the DELTA and never of the
|
|
3691
|
+
open work, so that "this feature is small" and "this feature is nearly done" cannot become one
|
|
3692
|
+
sentence — `contracts{expose[],call[]}`, `counts{open,done,attested}` and `review[]`, the four
|
|
3693
|
+
instructions for re-deriving this answer and reviewing it, because a projection that says what to do
|
|
3694
|
+
must also say how to check it was read correctly. A broken `delta.likec4` or contract axis names
|
|
3695
|
+
what it could not read in `unreadable` and exits 1 with `ok: true` — `loam delta`'s own rule rather
|
|
3696
|
+
than a new one: a feeder nothing could read empties the packet, an empty packet reads as "nothing to
|
|
3697
|
+
do", so the envelope stays true while the exit code stops a pipeline building on it.
|
|
3698
|
+
|
|
3699
|
+
The command mints no finding or refusal code of its own, and `loam_tasks` serves the same projection
|
|
3700
|
+
read-only over MCP.
|
|
3701
|
+
|
|
2224
3702
|
## Operating at fleet scale
|
|
2225
3703
|
|
|
2226
3704
|
One docs repo, a hundred services, many teams. The repo scales the way any shared codebase does —
|
|
@@ -2263,14 +3741,28 @@ through the forge, not through loam features:
|
|
|
2263
3741
|
**parsed** model and never computes a view — it reads what a view **declares** (a `dynamic view`'s
|
|
2264
3742
|
tags and ordered steps, already materialized by the parse loam pays for) and never what a view
|
|
2265
3743
|
**shows**. It renders nothing, and view *computation* is what is superlinear in edges. (If you
|
|
2266
|
-
want diagrams, add a `views` block and render it with LikeC4's own tooling — `npx likec4 start`
|
|
2267
|
-
the docs
|
|
2268
|
-
|
|
3744
|
+
want diagrams, add a `views` block and render it with LikeC4's own tooling — `npx likec4 start`
|
|
3745
|
+
at the docs root, which renders the fleet map AND every adopted service's model: an extending
|
|
3746
|
+
model is part of the root `fleet` project, beside the map, and a standalone one is the project its
|
|
3747
|
+
own `services/<svc>/likec4.config.json` declares (written by `loam subsystem sync`; the Layout
|
|
3748
|
+
section, "Two shapes of a service model", has the rule), and `npx likec4 start features/<FEAT>`
|
|
3749
|
+
for a delta, which is
|
|
3750
|
+
transient and gets no project — and scope the views, because an `include *` over a fleet-sized
|
|
3751
|
+
landscape takes minutes. Two costs arrive with the first STANDALONE service project, and only
|
|
3752
|
+
with it. Measured at the
|
|
3753
|
+
pinned likec4: with more than one project, `likec4 validate` at the docs root refuses without
|
|
3754
|
+
`--project <name>`, so a CI step that ran it bare changes (`likec4 build` and `likec4 export` take
|
|
3755
|
+
every project and need no flag). And the renderer's project list grows by one per such service
|
|
3756
|
+
beside `fleet` — LikeC4 projects are separate models, so a box on the fleet map is not a link into
|
|
3757
|
+
that service's project. A fleet whose models all extend the map pays neither: one project, one
|
|
3758
|
+
picker entry, and the containers of every adopted service reachable from the map's own boxes.
|
|
3759
|
+
`loam init`
|
|
2269
3760
|
scaffolds the landscape with no `views` block for that reason: loam should not plant a diagram it
|
|
2270
3761
|
never draws **— and never asks for one: a fleet that draws nothing owes no views block.** It also
|
|
2271
|
-
scaffolds a `likec4.config.json`
|
|
2272
|
-
|
|
2273
|
-
|
|
3762
|
+
scaffolds a `likec4.config.json` whose `exclude` is `["**/node_modules/**", "features/**"]`: a
|
|
3763
|
+
feature's `delta.likec4` re-declares the landscape's own identifiers and carries a `specification`
|
|
3764
|
+
block of its own, so it is read alone, and `services/` is deliberately NOT excluded — the models a
|
|
3765
|
+
fresh repository writes extend the map and the root project is the only place they parse.)
|
|
2274
3766
|
`loam validate --all --json` over the same 120 services used to be the outlier at ~13–14 s — one
|
|
2275
3767
|
fresh LikeC4 workspace per service; since the batched loader it parses the run's C4 documents in
|
|
2276
3768
|
one shared workspace and answers in **~0.7 s median** on the committed 120-service benchmark
|
|
@@ -2308,8 +3800,15 @@ repairing anything:
|
|
|
2308
3800
|
`needs` and (for authored files) the grading command in `after`. It is a **projection over the gates, never
|
|
2309
3801
|
a second opinion**: `stage` is taken from the union of what `validate --feature` errors on and
|
|
2310
3802
|
what `archive` refuses to merge (severity *or* `gates`), so status may be more pessimistic than
|
|
2311
|
-
either and is never greener than both — an invariant with a test behind it. The
|
|
2312
|
-
|
|
3803
|
+
either and is never greener than both — an invariant with a test behind it. The stage values are
|
|
3804
|
+
for branching, not progress words: `ready` means the documents are written and clean, nothing
|
|
3805
|
+
about code or evidence, and an archived feature is `done` whatever its record says — so the human
|
|
3806
|
+
view prints the stage as a sentence with the claims counted, and the payload carries
|
|
3807
|
+
`feature.title` (intent.md's title) and `checklist.claims` (what the checklist asks now; null once
|
|
3808
|
+
archived). The fleet form lists every feature in flight before any service's adoption rung — and
|
|
3809
|
+
carries every rung uncapped in `serviceSteps[]` (`{code, services, command}` per code), which the
|
|
3810
|
+
human view folds into one counted line per code. It is
|
|
3811
|
+
capped at ten substantive steps plus a `next.elided` notice saying how many of which codes were left out, and
|
|
2313
3812
|
whenever anything is outstanding its **last** entry is `next.fleet-gate` →
|
|
2314
3813
|
`loam validate --all --json` (a repository with work in it must never get a list that omits the
|
|
2315
3814
|
command CI runs; a fleet whose services exist and owe nothing says so instead, with
|
|
@@ -2358,11 +3857,14 @@ repairing anything:
|
|
|
2358
3857
|
|
|
2359
3858
|
And it reports the **agent surface**: which tool and profile the repo is wired for, how many command and skill
|
|
2360
3859
|
files it holds, how many a newer loam would lay down (`doctor.agent-files-missing`), and how many
|
|
2361
|
-
have fallen behind (`doctor.agent-files-stale`).
|
|
2362
|
-
`<!-- generated by loam vX.Y.Z -->` line every generated body opens with
|
|
3860
|
+
have fallen behind (`doctor.agent-files-stale`). A file has fallen behind when BOTH hold: its
|
|
3861
|
+
bytes, the `<!-- generated by loam vX.Y.Z -->` line every generated body opens with set aside, are
|
|
3862
|
+
not what this binary writes; and that stamp is missing or older than this binary. A stamp-only
|
|
3863
|
+
difference is not staleness — the stamp moves in every file at every release, and a warning that
|
|
3864
|
+
fires on every upgrade hides the release where a pointer's body really changed. A customized body
|
|
3865
|
+
under a current stamp is not staleness either: the stamp is the record that somebody reviewed it.
|
|
2363
3866
|
The digest manifest is what authorizes `init` to refresh an unchanged pointer; a different body is
|
|
2364
|
-
treated as human-owned and never overwritten.
|
|
2365
|
-
every edit around it is legitimate. Which deliveries a repo holds is asked of the files rather
|
|
3867
|
+
treated as human-owned and never overwritten. Which deliveries a repo holds is asked of the files rather
|
|
2366
3868
|
than of a record: `loam.json` stores tools and profile, not deliveries, and a repo initialized
|
|
2367
3869
|
`--no-skills` is not reported as missing the selected skills.
|
|
2368
3870
|
|
|
@@ -2376,7 +3878,28 @@ repairing anything:
|
|
|
2376
3878
|
|
|
2377
3879
|
`loam-report` is a separate support protocol, generated as a command and skill in every profile
|
|
2378
3880
|
but never appended to the lifecycle. It writes
|
|
2379
|
-
`loam-reports/YYYY-MM-DD-<short-slug>.md`
|
|
3881
|
+
`loam-reports/NNN-YYYY-MM-DD-<short-slug>.md` — `NNN` an ORDINAL of at least three digits,
|
|
3882
|
+
zero-padded, one more
|
|
3883
|
+
than the highest already in the directory (`001` when it is empty or absent, and widening past
|
|
3884
|
+
three digits rather than reusing a number once a corpus gets there), so a report has a
|
|
3885
|
+
short handle a commit message, an issue or the next report can cite. The document carries a
|
|
3886
|
+
`- Status: open` line beside its classification, whose vocabulary is `open` / `sent` (handed to
|
|
3887
|
+
the author, set by whoever forwards it) / `fixed in <version>` / `superseded by <NNN>`, and
|
|
3888
|
+
`loam doctor` reads the directory: how many reports it holds, how many stand at each status, and
|
|
3889
|
+
which ordinal comes next. `doctor --json` carries that as the `reports` object — `reports.dir`
|
|
3890
|
+
(the ABSOLUTE path of the directory, `loam-reports/` included, so an agent writes inside it rather
|
|
3891
|
+
than joining the name on a second time), resolved beside the `loam.json` that parsed, or beside
|
|
3892
|
+
the one loam FOUND when it did not parse, and only beside the run directory when there is no
|
|
3893
|
+
config file at or above it — `doctor` is the preflight that runs precisely when the config may not
|
|
3894
|
+
resolve, so it counts the corpus that is there rather than handing out an ordinal five existing
|
|
3895
|
+
reports already used — `reports.present`, `reports.total`, `reports.next`, and
|
|
3896
|
+
`reports.entries[]` with a `file`, an `ordinal` (`null` for a name written before the numbering)
|
|
3897
|
+
and a `status`, in ordinal order, numerically, with unnumbered names last (byte order stopped
|
|
3898
|
+
being report order at exactly the width the ordinal widens to: `1000-…` sorts before `998-…`). The status is read from the report's HEADER FIELD BLOCK alone — the lines above
|
|
3899
|
+
the first `##` heading, quoted code skipped in both markdown spellings (a fence, closed only by a
|
|
3900
|
+
run of the same character at least as long as the one that opened it, and an indented block,
|
|
3901
|
+
which is how the protocol prints its own template) — so a report quoting this protocol's own
|
|
3902
|
+
template still counts as whatever its own header says. The body carries expected/actual behavior,
|
|
2380
3903
|
version, stable envelope and finding codes, relevant locations, write state, a minimal safe
|
|
2381
3904
|
reproduction, classification and missing evidence. Before writing, the agent redacts secrets and
|
|
2382
3905
|
home paths and reduces command output to the excerpt that proves the symptom. It never retries a
|
|
@@ -2444,6 +3967,7 @@ and publishes it wherever the fleet view reads:
|
|
|
2444
3967
|
"errors": 0,
|
|
2445
3968
|
"warnings": 2,
|
|
2446
3969
|
"sources": { "current": 1, "stale": 0, "unvouched": 0, "absent": 0 },
|
|
3970
|
+
"contentStale": 0,
|
|
2447
3971
|
"generatedAt": "2026-08-03T12:00:00Z"
|
|
2448
3972
|
}
|
|
2449
3973
|
```
|
|
@@ -2462,10 +3986,16 @@ loam validate --service "$SVC" --json | jq '{
|
|
|
2462
3986
|
unvouched: [.targets[0].findings[] | select(.code == "sources.unvouched")] | length,
|
|
2463
3987
|
absent: [.targets[0].findings[] | select(.code == "sources.absent")] | length
|
|
2464
3988
|
},
|
|
3989
|
+
contentStale: [.targets[0].findings[] | select(.code == "content.stale")] | length,
|
|
2465
3990
|
generatedAt: (now | todate)
|
|
2466
3991
|
}'
|
|
2467
3992
|
```
|
|
2468
3993
|
|
|
3994
|
+
`contentStale` counts the spec documents whose words moved after their vouch — the doc-side half of
|
|
3995
|
+
staleness, beside the code-side `sources.stale`. Unlike that half it is not owed to the service repo
|
|
3996
|
+
alone (the docs repo's own `validate --all` scorecard counts it too), but a summary that left it out
|
|
3997
|
+
would publish "0 stale" over a document nobody re-read.
|
|
3998
|
+
|
|
2469
3999
|
`generatedAt` is the CI's clock, never loam's — loam output carries no timestamps, by design, so the
|
|
2470
4000
|
same repo state always yields the same bytes. That is also what makes the timestamp the liveness
|
|
2471
4001
|
signal: the rule is that every service repo's CI publishes this summary on every run, so a summary
|
|
@@ -2485,20 +4015,44 @@ hand-merges the block. That resolution has a documented cost, not a surprise: a
|
|
|
2485
4015
|
landscape moves the file away from the bytes the archive's `.loam-before/` snapshot recorded, so a
|
|
2486
4016
|
later `loam unarchive` of that feature refuses with `snapshot-stale` and needs `--force` — the
|
|
2487
4017
|
resolver is trading undo precision for the merge, and should know it. The trigger for revisiting is
|
|
2488
|
-
written down so nobody relitigates it early
|
|
2489
|
-
|
|
2490
|
-
|
|
2491
|
-
map
|
|
4018
|
+
written down so nobody relitigates it early — and half of what it once asked for has already
|
|
4019
|
+
arrived. Per-service internals now live in `services/<svc>/model.likec4` **by default**, and that is
|
|
4020
|
+
where a second team adopting a service writes, so it adds nothing to this file. It is a default and
|
|
4021
|
+
not a prohibition: the map MAY hold a service's interior, and sometimes must. A store a second
|
|
4022
|
+
service will reach has to be declared where that consumer's project can see it — a per-service
|
|
4023
|
+
project is `architecture/` plus one model — so it is drawn ON THE MAP, nested inside the owner's
|
|
4024
|
+
element or beside the services, whichever the team prefers. Which of the two is nobody's business
|
|
4025
|
+
but the team's: "Private stores" above leaves the placement open and loam grades neither, so nothing
|
|
4026
|
+
here says a store must move. A delta spelling a parent in the legacy
|
|
4027
|
+
title-join lands its container on the map too. What an extending model owns is what it DECLARES,
|
|
4028
|
+
not everything below the service's element. `loam archive` routes rather than forbids — a delta's
|
|
4029
|
+
additions nested under a service whose model extends the map, with the whole parent chain spelled
|
|
4030
|
+
in the map's own ids, are spliced into that model's `extend <fqn> { … }` block instead of this
|
|
4031
|
+
file, and the merged model is proved to parse beside the merged map before anything is written. What is left of the
|
|
4032
|
+
trigger is the CROSS-SERVICE EDGE, which still exists twice — container-level in the model, service
|
|
4033
|
+
level here — because the spine, the platform census and every other fleet check read this file
|
|
4034
|
+
alone. So: when landscape merge conflicts become routine — weekly, not monthly — thin the map to
|
|
4035
|
+
elements plus cross-service edges and teach the fleet checks to read the edges out of the models
|
|
4036
|
+
that attest them. Until then, one file keeps the fleet map one diffable document, and
|
|
4037
|
+
`landscape.service-isolated` remains the nudge that carries an attested call up into it.
|
|
2492
4038
|
|
|
2493
4039
|
## Two flows
|
|
2494
4040
|
|
|
2495
4041
|
- **Bootstrap (reverse):** `loam adopt --service <id>` emits a **brief**, not an extraction — the
|
|
2496
4042
|
target paths (and which already exist, to be diffed and never overwritten), the grammar of each
|
|
2497
|
-
artifact, what the living
|
|
4043
|
+
artifact, what the living fleet map — the `architecture/` project, not the landscape file alone —
|
|
4044
|
+
already says about the service, the frontmatter to write, the
|
|
2498
4045
|
checks `loam validate --service <id>` will then run, and the ones that do not exist. One target is
|
|
2499
4046
|
not under `services/<id>/`: `architecture/landscape.likec4`, carried with `action: "edit"` and a
|
|
2500
|
-
`landscape.instruction` naming the
|
|
2501
|
-
nothing in the map resolves to the service
|
|
4047
|
+
`landscape.instruction` naming the write this service owes the fleet map — the element while
|
|
4048
|
+
nothing in the map resolves to the service, the EDGES while an element does and no edge touches it
|
|
4049
|
+
(`landscape.touched: false`, with `landscape.attested` listing the calls the service's own
|
|
4050
|
+
model.likec4 declares across its boundary); absent, and the instruction `null`, only once an
|
|
4051
|
+
element resolves and an edge touches it. `landscape.callersKnown` says whether loam could read WHO
|
|
4052
|
+
CALLS this service — true when the `architecture/` project parses, and true when it does not but
|
|
4053
|
+
`architecture/landscape.likec4` does. It is what an empty `landscape.expects` has to be read
|
|
4054
|
+
against: only a readable landscape makes "nothing calls this service" evidence, and it is the one
|
|
4055
|
+
fact `openapi.yaml`'s `required` is derived from. An agent reads the code and writes draft
|
|
2502
4056
|
`model.likec4` + `spec.md` + `openapi.yaml` + `adrs/`, `runbook.md`, `health.yaml`, and makes that
|
|
2503
4057
|
one addition to the shared map. A human promotes `draft` -> `verified` with `loam vouch`.
|
|
2504
4058
|
- **Forward (generative):** author `features/<FEAT>/delta.likec4` -> `loam delta <FEAT>` projects it
|
|
@@ -2511,7 +4065,7 @@ says what its architecture means, and two generated models of the same code disa
|
|
|
2511
4065
|
every run — a done-check built on diffing them would flap until somebody turned it off. What is
|
|
2512
4066
|
deterministic is the **question**: which files, in which grammar, bound to which existing elements;
|
|
2513
4067
|
which operations, which edges, which scenarios. loam owns the questions and the checking; an agent
|
|
2514
|
-
owns the reading. `sources` and
|
|
4068
|
+
owns the reading. `sources` and the verification record are where the answers are written down.
|
|
2515
4069
|
|
|
2516
4070
|
## Status
|
|
2517
4071
|
|
|
@@ -2524,9 +4078,9 @@ component-surface pins), `verify`,
|
|
|
2524
4078
|
transactional `archive` / `unarchive`, `vouch`, and the OpenSpec audit/migration on-ramp are
|
|
2525
4079
|
implemented, each with a `--json` contract.
|
|
2526
4080
|
|
|
2527
|
-
Known and unclosed, so nobody has to discover them: the
|
|
2528
|
-
|
|
2529
|
-
|
|
4081
|
+
Known and unclosed, so nobody has to discover them: the scheduled CI `stability` job has still to be
|
|
4082
|
+
observed green, even though behavior, coverage, the installed-package smoke and the local stress
|
|
4083
|
+
runner all pass. Both contract axes now merge a components-only delta: a feature
|
|
2530
4084
|
asyncapi.yaml declaring no channel/operation/message slot (`components.schemas`-only, say) merges
|
|
2531
4085
|
the component surfaces it declares, pinned by the root `x-loam-baselines` record, and they enter the
|
|
2532
4086
|
ref sweep like anything else the merge wrote — so a `$ref` whose target the delta itself defines
|