@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/CHANGELOG.md
CHANGED
|
@@ -10,6 +10,3454 @@ case for a change — the alternative that was rejected, the defect it came from
|
|
|
10
10
|
generalises — lives where it is maintained: [SCHEMA.md](SCHEMA.md) for the rule,
|
|
11
11
|
[ROADMAP.md](ROADMAP.md) for the priority and its exit criteria, and the commit that landed it._
|
|
12
12
|
|
|
13
|
+
## [0.2.0-alpha.13] - 2026-10-22
|
|
14
|
+
|
|
15
|
+
Everything on `main` since `0.2.0-alpha.12`. Still a prerelease on the `alpha` line. When this
|
|
16
|
+
version was cut, `0.2.0-alpha.10`, `0.2.0-alpha.11` and `0.2.0-alpha.12` were tagged but none of
|
|
17
|
+
their publishes had run (`alpha` and `latest` both named `0.2.0-alpha.9`), so a repository
|
|
18
|
+
upgrading from alpha.9 takes alpha.10's, alpha.11's and alpha.12's sections below as well as this
|
|
19
|
+
one; `npm view @ybotok/loam dist-tags` is the record of what installs.
|
|
20
|
+
|
|
21
|
+
Almost all of it changes what an agent reads before it writes, not what a check grades. A fifth
|
|
22
|
+
reference page, `loam instructions loam-drawing`, teaches seven relationship kinds with one arrow
|
|
23
|
+
direction each, a direction only from evidence (`connects` otherwise), a `legend` view drawn from
|
|
24
|
+
real edges, and overview views that answer one question at one altitude, measured at the LikeC4
|
|
25
|
+
1.59.2 pin. `loam instructions loam-authoring` gains "Writing for a person": an intent.md that says
|
|
26
|
+
how to read the feature, a plain-language `## Purpose` for each spec delta, and requirement bodies a
|
|
27
|
+
person can read. The workflow pages point at both, and `loam explain` defines `kind`, `legend`,
|
|
28
|
+
`overview` and `purpose`. The map `loam init --create` scaffolds and the one `loam seed` writes now
|
|
29
|
+
declare the seven kinds in their `specification` block. A map an earlier loam scaffolded and nobody
|
|
30
|
+
touched still reads as the stub, but it declares no kind: add the block from the drawing page before
|
|
31
|
+
a feature draws a kinded edge, because `loam archive` refuses `merge-failed` on a kind the living
|
|
32
|
+
map lacks and `validate --feature` and `loam status` do not predict that refusal yet.
|
|
33
|
+
|
|
34
|
+
No document changes its exit code. The only runs that do are lookups of the new names:
|
|
35
|
+
`loam instructions loam-drawing`, and `loam explain` on `kind`, `legend`, `overview`, `purpose` and
|
|
36
|
+
their aliases (`publishes`, `delivers`, `arrow` and `direction` among them), exit 0 where they
|
|
37
|
+
refused `unknown-target`.
|
|
38
|
+
|
|
39
|
+
No stable code was added, renamed or retired — 360 in all, as at alpha.12, counted with
|
|
40
|
+
`loam explain --codes --json` — and no flag, `--json` key or frontmatter field was added. The new
|
|
41
|
+
page is a fifth entry of `loam instructions --json`'s `references[]` and the MCP resource
|
|
42
|
+
`loam://instructions/loam-drawing`, and the four terms are new entries of `loam explain --json`'s
|
|
43
|
+
`terms[]`.
|
|
44
|
+
|
|
45
|
+
### Added — `loam instructions loam-drawing`, and agent pages that teach a picture and a feature a person can read
|
|
46
|
+
|
|
47
|
+
No code, flag or exit code was added or changed. The new page holds the convention.
|
|
48
|
+
SCHEMA.md, "Which way an edge points", states the one part of it loam itself depends on.
|
|
49
|
+
|
|
50
|
+
- **A fifth reference page, `loam instructions loam-drawing`**, also served over MCP as
|
|
51
|
+
`loam://instructions/loam-drawing` and listed as a fifth entry of `loam instructions --json`'s
|
|
52
|
+
`references[]`. It teaches how to draw the C4 model so a person can read it. It names seven
|
|
53
|
+
relationship kinds, each with one direction: `publishes` (producer to topic), `delivers` (topic
|
|
54
|
+
to consumer), `reads`, `writes` and `readsWrites` (always written service `->` store, with the
|
|
55
|
+
kind's style placing the arrowhead), `calls`, and `connects` for a connection whose direction
|
|
56
|
+
no evidence establishes. Kinds are declared once in the map's `specification` and again in a
|
|
57
|
+
delta that draws one, never carry a tag, and never use the dotted `a .kind b` form. A
|
|
58
|
+
`legend` view is drawn from real edges. The page also covers overview views, measured at the
|
|
59
|
+
LikeC4 1.59.2 pin: one question at one altitude; a single positive `where kind is …` filter,
|
|
60
|
+
because relationships of different kinds merged into one line render default gray; the
|
|
61
|
+
`include <parent>.* -> * where …` form, which renders an empty view at subsystem altitude;
|
|
62
|
+
`rank` rows; `tail odot` on a direction-less edge; numbered titles; and no authored
|
|
63
|
+
`view … of` a subsystem or service element. `AGENTS.md`'s "Reference pages" section lists the
|
|
64
|
+
page, and loam-adopt, loam-feature, loam-implement, loam-check and loam-ship point at it
|
|
65
|
+
wherever an edge is drawn or read.
|
|
66
|
+
- **Adoption resolves directions, and a feature draws messages through the broker.** Step 3 of
|
|
67
|
+
loam-adopt gives every edge a kind, and a direction only from evidence: the code, a
|
|
68
|
+
`*_PRODUCER_*` or `*_CONSUMER_*` configuration name, or the service's own `asyncapi.yaml`.
|
|
69
|
+
Configuration that only names an address is drawn `connects`, and the adoption turns the
|
|
70
|
+
service's `connects` edges into real kinds. Its hand-back now says eight things; the new one
|
|
71
|
+
is every edge still drawn `connects`. loam-feature's delta step draws producer
|
|
72
|
+
`-[publishes]->` topic `-[delivers]->` consumer, and draws a producer outside the fleet as an
|
|
73
|
+
`#external` element. loam-implement redraws a `connects` edge once the code shows its kind.
|
|
74
|
+
- **`loam instructions loam-authoring` gains "Writing for a person".** It covers intent.md's
|
|
75
|
+
`## How to read this feature`, a business-requirement coverage table, and `## Open questions`
|
|
76
|
+
as a table (ID, question, blocks, owner, current default). Each spec delta gets a
|
|
77
|
+
plain-language `## Purpose`, which is copied into a document the archive creates and stays
|
|
78
|
+
with the archived delta otherwise. A long requirement body becomes a one-line lead plus one
|
|
79
|
+
bullet per SHALL sentence, and abbreviations are expanded on first use. Wording such as
|
|
80
|
+
"this delta" or "this feature" stays out of what the archive copies. It also covers feature
|
|
81
|
+
glossary terms, feature ADRs, use cases tagged `#cap-`, and `Covers:` edges spelled by
|
|
82
|
+
service name. loam-feature's step 4 and loam-ship's dry-run step say the same in brief, and
|
|
83
|
+
loam-feature's analyze step writes open questions as table rows.
|
|
84
|
+
- **`loam explain` knows the new words.** A fifth listing heading, "the picture — drawing the
|
|
85
|
+
map and writing for a person", holds `kind` (which also answers `publishes`, `delivers`,
|
|
86
|
+
`reads`, `writes`, `readsWrites`, `calls`, `connects`, `arrow` and `direction`), `legend`,
|
|
87
|
+
`overview` and `purpose`. `loam explain --json`'s `terms[]` gains the four.
|
|
88
|
+
- **loam-report** rebuilds a reproduction on neutral stand-ins when the fleet's own names are
|
|
89
|
+
confidential, and its `Sanitization` section says when `Actual` quotes the fleet.
|
|
90
|
+
|
|
91
|
+
### Changed — a new fleet map declares the seven relationship kinds
|
|
92
|
+
|
|
93
|
+
- The `architecture/landscape.likec4` that `loam init --create` scaffolds, and the map
|
|
94
|
+
`loam seed` writes, now declare `publishes`, `delivers`, `reads`, `writes`, `readsWrites`,
|
|
95
|
+
`calls` and `connects` in their `specification` block, with the styles the drawing page
|
|
96
|
+
teaches. The stub's comment shows each kind in use. Seed's edges stay plain: `fleet.yaml`
|
|
97
|
+
says who calls whom and nothing about what an edge carries.
|
|
98
|
+
- No `legend` view is scaffolded. A view names only elements the model declares, and a new map
|
|
99
|
+
declares none.
|
|
100
|
+
- A map an earlier loam scaffolded and nobody touched still reads as the untouched stub:
|
|
101
|
+
`loam status` keeps its `next.author-landscape` step and `loam seed` still replaces it. Such a
|
|
102
|
+
map declares no kind, so add the block from `loam instructions loam-drawing` before a feature
|
|
103
|
+
draws a kinded edge. `loam archive` copies an edge and never a declaration, and refuses with
|
|
104
|
+
`merge-failed` on a kind the living map lacks. `loam validate --feature` and `loam status` do
|
|
105
|
+
not predict that refusal yet.
|
|
106
|
+
|
|
107
|
+
## [0.2.0-alpha.12] - 2026-10-22
|
|
108
|
+
|
|
109
|
+
Everything on `main` since `0.2.0-alpha.11`. Still a prerelease on the `alpha` line. When this
|
|
110
|
+
version was cut, `0.2.0-alpha.10` and `0.2.0-alpha.11` were tagged but neither publish had run
|
|
111
|
+
(`alpha` and `latest` both named `0.2.0-alpha.9`), so a repository upgrading from alpha.9 takes
|
|
112
|
+
alpha.10's and alpha.11's sections below as well as this one; `npm view @ybotok/loam dist-tags` is
|
|
113
|
+
the record of what installs.
|
|
114
|
+
|
|
115
|
+
Both entries close a gap between what a feature's checks say and what the archive then does. A
|
|
116
|
+
feature that REMOVES a requirement a living flow's `#req-` tag claims, or MODIFIES one and adds an
|
|
117
|
+
id that flattens to the same tag, is refused at feature scope with
|
|
118
|
+
`usecase.requirement-unresolved` — the error `validate --all` used to report only after the
|
|
119
|
+
archive — and that walk reads a service-local tag against both of the service's documents, as the
|
|
120
|
+
fleet does. `loam rebase` writes a contract's `x-loam-based-on` pin and `x-loam-baselines` record
|
|
121
|
+
without re-printing the rest of the feature's `openapi.yaml` or `asyncapi.yaml`.
|
|
122
|
+
|
|
123
|
+
One entry changes an exit code. The refusal is new: `loam validate --feature`, `validate --all` for
|
|
124
|
+
an active feature and `loam archive` (`not-coherent`, which `--approve` overrides) exit 1 for a
|
|
125
|
+
feature whose merge breaks a living flow's `#req-` tag where they exited 0, and `loam status` no
|
|
126
|
+
longer calls that feature ready. Under `--strict`, a `usecase.requirement-modified` warning that
|
|
127
|
+
reading both of a service's documents takes away can let a run pass that failed. `loam rebase`
|
|
128
|
+
changes no exit code.
|
|
129
|
+
|
|
130
|
+
No stable code was added, renamed or retired — 360 in all, as at alpha.11, counted with
|
|
131
|
+
`loam explain --codes --json` — and no flag, `--json` key or frontmatter field was added.
|
|
132
|
+
|
|
133
|
+
### Fixed — a feature that removes a promise a living flow claims is refused before the archive
|
|
134
|
+
|
|
135
|
+
No code, flag or payload key was added. SCHEMA.md "A promise a living flow keeps, removed" states
|
|
136
|
+
the rule.
|
|
137
|
+
|
|
138
|
+
- **`usecase.requirement-unresolved` is raised at feature scope for a living flow.** A feature that
|
|
139
|
+
REMOVES a requirement a living flow's `#req-` tag claims passed `validate --feature`, `loam status`
|
|
140
|
+
and the archive gate, archived at exit 0, and the next `validate --all` reported
|
|
141
|
+
`usecase.requirement-unresolved` against the flow. The walk that raises
|
|
142
|
+
`usecase.requirement-modified` now raises that error too, at both of its altitudes: an
|
|
143
|
+
`architecture/` flow scoped by its `#cap-` tag to a capability requirement the feature's capability
|
|
144
|
+
delta removes, and a flow beside a service's model naming one of that service's requirements the
|
|
145
|
+
feature's `specs/` delta removes. The message names the living flow file and the requirement, and
|
|
146
|
+
the subject is the view, as `validate --all` names it. `--approve` overrides it. The tag is graded
|
|
147
|
+
against what the merge leaves: a removal the same delta re-spells under an id that flattens to the
|
|
148
|
+
same tag raises nothing, and a tag that does not resolve today is left to `validate --all`, which
|
|
149
|
+
already reports it. **Exit code:** `loam validate --feature`, `validate --all` (per active feature)
|
|
150
|
+
and `loam archive` (`not-coherent`) now exit 1 for such a feature where they exited 0, and
|
|
151
|
+
`loam status` no longer calls it ready.
|
|
152
|
+
- **A MODIFIED that leaves a living flow's tag ambiguous is the same error, not the warning.** A
|
|
153
|
+
feature that MODIFIES a requirement a living flow claims and ADDS an id that flattens to the same
|
|
154
|
+
tag (`PAY-AUTH` beside `PAY.AUTH`) got only `usecase.requirement-modified`, whose message said the
|
|
155
|
+
tag would still resolve; after the archive `validate --all` reported `usecase.requirement-unresolved`.
|
|
156
|
+
It now gets that error, naming the colliding ids, with the same exit codes as above.
|
|
157
|
+
- **A service-local `#req-` tag is read against both of the service's documents in this walk, as the
|
|
158
|
+
fleet reads it.** When the feature carried a delta for only one of spec.md and arch.spec.md, the
|
|
159
|
+
other was not read, so an id it declared did not count; and a requirement under a living document's
|
|
160
|
+
`## REMOVED Requirements` heading counted, though the fleet skips it. Both now match
|
|
161
|
+
`validate --all`. For `usecase.requirement-modified` this can only remove a warning, from a tag the
|
|
162
|
+
two documents already make ambiguous.
|
|
163
|
+
|
|
164
|
+
### Fixed — `loam rebase` pins a feature's contract without re-printing it
|
|
165
|
+
|
|
166
|
+
No code, flag, exit code or `--json` key was added or removed.
|
|
167
|
+
|
|
168
|
+
- **A rebased `openapi.yaml` or `asyncapi.yaml` changes only where loam pins it.** Writing an
|
|
169
|
+
`x-loam-based-on` pin, or writing or removing the root `x-loam-baselines` record, re-printed the
|
|
170
|
+
feature's whole contract: every long scalar re-folded at 80 columns, every flow sequence
|
|
171
|
+
re-padded (`[a, b]` became `[ a, b ]`) and CRLF turned into LF. Now only the pin lines and the
|
|
172
|
+
record change and the rest of the file stays byte-for-byte as written, with the same fallback to
|
|
173
|
+
the whole-file write that the archive's merges use for a shape they cannot splice safely. A
|
|
174
|
+
contract an earlier rebase already re-printed stays as it is: rebasing it again does not bring
|
|
175
|
+
back the author's layout.
|
|
176
|
+
|
|
177
|
+
## [0.2.0-alpha.11] - 2026-10-22
|
|
178
|
+
|
|
179
|
+
Everything on `main` since `0.2.0-alpha.10`. Still a prerelease on the `alpha` line. When this
|
|
180
|
+
version was cut, `0.2.0-alpha.10` was tagged but its publish had not run (`alpha` and `latest` both
|
|
181
|
+
named `0.2.0-alpha.9`), so a repository upgrading from alpha.9 takes alpha.10's section below as
|
|
182
|
+
well as this one; `npm view @ybotok/loam dist-tags` is the record of what installs.
|
|
183
|
+
|
|
184
|
+
Most of it makes a grade or an archive say what it used to decide in silence. An event key on an
|
|
185
|
+
edge drawn the wrong way round is named instead of binding nothing, the consumer census counts a
|
|
186
|
+
service at either end of an edge into a store or a topic, and a `Covers:` line may name a kinded
|
|
187
|
+
edge. `loam archive` merges an edge whose relationship kind carries a tag, reads an extending
|
|
188
|
+
model's dotted edges and kind-first declarations, keeps a comment with the statement under it, and
|
|
189
|
+
gives a service it creates its own `services/<svc>/model.likec4`. A feature's new services are the
|
|
190
|
+
ones the archived fleet will call services, and `loam status` owes an `openapi.yaml` only where
|
|
191
|
+
something calls the service. The archive names every scenario and join-line entry a MODIFIED or
|
|
192
|
+
REMOVED requirement takes out of the living docs, marks a first spec.md it writes into a
|
|
193
|
+
never-adopted service `adoption: partial`, and writes only the entries a contract merge changes. A
|
|
194
|
+
use-case step's title can say which of several relationships its hop means.
|
|
195
|
+
|
|
196
|
+
Several entries change an exit code, listed in each. Refusals that are gone: `loam archive` and
|
|
197
|
+
`archive --dry-run` on an edge whose relationship kind carries a tag or on a child of a kind-first
|
|
198
|
+
container in an extending model, and the `usecase.flow-invalid` / `deployment.doc-invalid` that
|
|
199
|
+
`validate --feature`, `validate --all` and `loam status` reported for the same shapes. Refusals that
|
|
200
|
+
are new: a feature's use-case hop or deployment document naming a container the delta draws inside
|
|
201
|
+
a service the feature introduces is `usecase.flow-invalid` / `deployment.doc-invalid` from
|
|
202
|
+
`validate --feature`, `validate --all` for an active feature and `loam archive` (`not-coherent`,
|
|
203
|
+
which `--approve` does not override); on the tagged-kind shape `validate --feature` can now report
|
|
204
|
+
`spine.message-unproduced`; and `loam gate --service <id>` exits 1 for a service whose first
|
|
205
|
+
spec.md an archive wrote as `adoption: partial` (`gate.service-undocumented`). Under `--strict` the
|
|
206
|
+
new warnings can fail a run that passed — `spine.message-unbound`, `usecase.requirement-modified`,
|
|
207
|
+
`openapi.info-not-merged`, `asyncapi.info-not-merged`, `gate.partner-undocumented`, and
|
|
208
|
+
`landscape.datastore-shared` / `landscape.platform-candidate` now that the census counts both ends
|
|
209
|
+
— and the warnings that are gone (`service.no-requirement-delta` on an `#external` producer or a
|
|
210
|
+
living element's stub, `c4.uncovered` on a grouping, a `usecase.step-contested` hop its title
|
|
211
|
+
resolves) can let one pass that failed.
|
|
212
|
+
|
|
213
|
+
Four stable codes were added, all warnings that never gate — `spine.message-unbound`,
|
|
214
|
+
`usecase.requirement-modified`, `openapi.info-not-merged` and `asyncapi.info-not-merged` — 360 in
|
|
215
|
+
all, 356 at alpha.10; none was renamed or retired, and no flag was added. New `--json` keys, all
|
|
216
|
+
additive: `dropped` on `loam archive` and on `loam delta`, and `partial: true` on the spec.md entry
|
|
217
|
+
of `loam adopt`'s `targets[]`. `loam archive --json`'s `plan` gains a `create` row for a service
|
|
218
|
+
model it writes, and `loam status` can read a new service's `openapi` as `done` with
|
|
219
|
+
`required: false`. One frontmatter field is new, written by loam: `adoption: partial`, which stays
|
|
220
|
+
until the service's baseline is written and the line is deleted.
|
|
221
|
+
|
|
222
|
+
### Fixed — which way an edge points no longer decides a grade in silence
|
|
223
|
+
|
|
224
|
+
One finding code was added: `spine.message-unbound` (warn). It is a new member of the frozen
|
|
225
|
+
`IssueCode` union. No flag or payload key changed, and no exit code changed except under `--strict`,
|
|
226
|
+
where every new warning below fails the run. SCHEMA.md has a new section, *Which way an edge
|
|
227
|
+
points*, that lists every reader of a relationship's direction and the drawing each one expects.
|
|
228
|
+
Until now the event rule was written only in the examples README.
|
|
229
|
+
|
|
230
|
+
- **An event key bound to a topic is reported.** `metadata { publishes }` binds the edge's SOURCE
|
|
231
|
+
and `metadata { consumes }` binds its TARGET. A consume edge drawn from the consumer to the topic,
|
|
232
|
+
or a publish edge drawn from the topic to the producer, bound nothing: the document validated, and
|
|
233
|
+
the event spine graded one link fewer without a word. `loam validate` (`--service`, and `--all`
|
|
234
|
+
per service) now raises `spine.message-unbound` on the service at the other end when the bound end
|
|
235
|
+
is infrastructure: a `topic`, `queue`, `database`, `cache` or `bucket`, by its own kind or an
|
|
236
|
+
ancestor's. `validate --feature`, `loam status <FEAT>` and the archive gate raise it for a delta's
|
|
237
|
+
tagged edges. The message spells the edge turned round. For an edge that carries both keys it says
|
|
238
|
+
to move the key at fault onto an edge of its own, since turning the arrow round would only move
|
|
239
|
+
the warning. The warning does not gate. While it stands, the service's `event.covered` line does
|
|
240
|
+
not appear. An `#external` system at the bound end does not raise it: `crm -> billingService`
|
|
241
|
+
carrying `publishes` is an outside producer, and a `consumes` on an outside system is an outside
|
|
242
|
+
consumer, both drawn correctly. Neither does an edge with no fleet service at either end.
|
|
243
|
+
- **The consumer census counts a service at either end of an edge into infrastructure.** For an
|
|
244
|
+
infrastructure element that stands for no service, `store -> service` now counts that service as
|
|
245
|
+
a consumer, as `service -> store` already did, and so does an extending model's inbound call from
|
|
246
|
+
such an element. Two drawings now warn that did not. A store a second service reads, drawn
|
|
247
|
+
`store -> service`, raises `landscape.datastore-shared`. An `#external` broker not tagged
|
|
248
|
+
`#platform` raises `landscape.platform-candidate` once three services reach its topics, counting
|
|
249
|
+
services that only consume, drawn `topic -> service`. Before, only publishers were counted. **This
|
|
250
|
+
can add a warning to a fleet that had none, and fail `--strict` with it.** While either warning
|
|
251
|
+
stands, `validate --all` does not print `landscape.matched`. For any other element the arrow is
|
|
252
|
+
still read, so a system calling into the fleet is not consumed by the services it calls.
|
|
253
|
+
- **A `Covers:` edge entry may carry the relationship's kind**, in either of LikeC4's spellings.
|
|
254
|
+
`a -[kind]-> b` used to split at the `->` after the `]`, and `a .kind b` was read as one element
|
|
255
|
+
id, so both were always `covers.unknown` while `a -> b` matched the same kinded edge. Both now
|
|
256
|
+
resolve exactly as `a -> b` does, at service and feature scope, and satisfy `c4.uncovered` the
|
|
257
|
+
same way. `node:a -[kind]-> node:b` reads as a deployment edge. The kind is read past, not
|
|
258
|
+
compared, because loam identifies an edge by its two endpoints.
|
|
259
|
+
|
|
260
|
+
### Fixed — `loam archive` splices what LikeC4 reads, where a reader expects it, and gives a service it creates its model
|
|
261
|
+
|
|
262
|
+
No code, flag or payload key was added. SCHEMA.md "How archive writes, and how unarchive undoes it"
|
|
263
|
+
states the rules.
|
|
264
|
+
|
|
265
|
+
- **An edge whose relationship kind carries a tag archives.** With `relationship publishes { #async }`
|
|
266
|
+
in the specification, every `-[publishes]->` (or `.publishes`) edge the delta added was refused as
|
|
267
|
+
`merge-failed` ("cannot locate the '…' relationship in delta.likec4") by an archive
|
|
268
|
+
`validate --feature` and `loam status` had called ready, and a feature with flows got
|
|
269
|
+
`usecase.flow-invalid` from `validate --feature` instead. The splice now matches a statement with
|
|
270
|
+
its kind's tags. **Exit code:** `loam archive` and `archive --dry-run` exit 0 where they exited 1
|
|
271
|
+
on that shape; `validate --feature`, `validate --all` (per active feature) and `loam status` no
|
|
272
|
+
longer carry the `usecase.flow-invalid` — or, for a feature with `deployment/`, the
|
|
273
|
+
`deployment.doc-invalid` — and exit 0 where they exited 1. With the merge preview readable again,
|
|
274
|
+
`validate --feature` also checks a consumed message against the map the archive leaves, so on that
|
|
275
|
+
shape it can now report `spine.message-unproduced` where it said nothing.
|
|
276
|
+
- **An extending model is read with the kinds it borrows from the map.** In a service's extending
|
|
277
|
+
`model.likec4`, a dotted edge (`a .delivers b`) and a kind-first declaration (`container outbox …`)
|
|
278
|
+
were invisible to the splice: an edge the archive routed into the model landed above the dotted
|
|
279
|
+
edge and its comment, a delta restating a dotted edge wrote a second copy at exit 0, and a child of
|
|
280
|
+
a kind-first container was refused (`merge-failed`, "nests under … which is neither in the living
|
|
281
|
+
landscape nor added by this delta"). **Exit code:** that last shape exits 0 where it exited 1 —
|
|
282
|
+
`loam archive`, and `validate --feature` and `loam status` for a feature with flows or
|
|
283
|
+
`deployment/`, which reported the same refusal as `usecase.flow-invalid` / `deployment.doc-invalid`.
|
|
284
|
+
- **An addition with no anchor lands above the comment written over the statement after it, and
|
|
285
|
+
where it lands no longer depends on unrelated edges.** On a hand-written map, an edge from a
|
|
286
|
+
service the same archive adds was spliced between a section comment and the edge it explains, and
|
|
287
|
+
turning one unrelated edge round moved it to another section. The relationship walk now sorts only
|
|
288
|
+
within the region's ascending tail, and a comment directly above a statement or the model's
|
|
289
|
+
closing brace stays with what follows it — in the landscape and in a service model alike. The bytes
|
|
290
|
+
an archive writes change only for those shapes; placement stays order-independent.
|
|
291
|
+
- **A service the archive creates gets `services/<svc>/model.likec4`.** For a service the feature
|
|
292
|
+
carries `specs/<svc>/` for, the fleet does not enumerate and the merged map draws (the living map,
|
|
293
|
+
or an element the delta adds, bound to it or titled with its name), the archive writes an empty extending
|
|
294
|
+
model (a header comment and `model { }`) and routes whatever the delta draws inside the service
|
|
295
|
+
into it, where those containers used to land on the map. `loam archive --json`'s `plan` gains a
|
|
296
|
+
`create` row for that path, the run prints `architecture: created services/<svc>/model.likec4 …`,
|
|
297
|
+
and `warnings[]` no longer carries `service.no-model` for that service — so the closing "complete
|
|
298
|
+
+ current" line is no longer withheld for it, and `validate --all` reports no `service.no-model`
|
|
299
|
+
after it. `service.no-model` is still filed (error, `gates: false`) for a service the map gains with
|
|
300
|
+
no `specs/<svc>/`, which `validate --all` then reports as `landscape.binding-unknown`, and for a
|
|
301
|
+
`specs/<svc>/` the merged map never draws (an unknown service only `--approve` merges), when the
|
|
302
|
+
merge creates its directory anyway. The archive
|
|
303
|
+
never picks a subsystem — the service lands unfiled at `services/<svc>/` unless its directory
|
|
304
|
+
already exists — and when the tree has a subsystem it prints
|
|
305
|
+
`loam subsystem move <svc> --into <subsystem>`. `unarchive` deletes the created model. The
|
|
306
|
+
shipped example's FEAT-101 now archives with no warning and a fleet that is clean after
|
|
307
|
+
`loam subsystem sync`.
|
|
308
|
+
- **The feature gate previews that routing.** A `features/<FEAT>/usecases/` hop into a container
|
|
309
|
+
the delta draws inside a service the feature introduces resolved against the preview's map, passed,
|
|
310
|
+
and was copied into `architecture/usecases/`, naming a container that now lives in the service's
|
|
311
|
+
model. **Exit code:** `loam validate --feature`, `validate --all` (per active feature) and
|
|
312
|
+
`loam archive` (`not-coherent`; `--approve` does not override it) now refuse that shape with
|
|
313
|
+
`usecase.flow-invalid` where they passed, and `loam status` no longer calls that feature ready. A
|
|
314
|
+
`features/<FEAT>/deployment/` document naming such a container is refused the same way, with
|
|
315
|
+
`deployment.doc-invalid`.
|
|
316
|
+
|
|
317
|
+
### Fixed — what a feature introduces is graded the way the archived fleet grades it
|
|
318
|
+
|
|
319
|
+
No code, flag or `--json` key was added. A feature's delta was read with a narrower idea of "a
|
|
320
|
+
service" than the fleet census `validate --all` applies once the archive lands, and `loam status`
|
|
321
|
+
owed a contract to every service a feature introduces whether or not anything could ever call it.
|
|
322
|
+
|
|
323
|
+
- **`service.no-requirement-delta` (warn) fires only for a new service of ours**: a tagged
|
|
324
|
+
`softwareSystem` at service level that is not `#external`, not a grouping (an element standing
|
|
325
|
+
for no service that contains one), has no `services/<id>/` and is not already drawn by the living
|
|
326
|
+
map. A delta's `#external` producer, and the stub of a living element — read with the living
|
|
327
|
+
element's tags, so a re-declared `#external` system stays external — no longer raise it.
|
|
328
|
+
- **The verification checklist's `service.exists` claims follow the same reading**, so
|
|
329
|
+
`loam verify` and `loam tasks` lose the claim and unit for an `#external` element, a grouping, and the
|
|
330
|
+
stub of a living `#external` system — and GAIN one for a new service drawn inside a group the
|
|
331
|
+
delta declares (the shape the shipped `FEAT-101` uses for `payment-split-service`), which a
|
|
332
|
+
dotted id used to hide. Only a grouping is looked through: what a delta draws inside any other
|
|
333
|
+
element (a container of a system with no directory yet, an endpoint inside an `#external`
|
|
334
|
+
vendor) is still not a service, and the stub of a service that already has a directory is asked
|
|
335
|
+
about only when tagged at top level, as before. A record answering the old checklist of such a
|
|
336
|
+
feature reads stale and is answered again.
|
|
337
|
+
- **`c4.uncovered` (warn) no longer asks a grouping for a `Covers:` line**; the service inside it
|
|
338
|
+
is graded as before.
|
|
339
|
+
- **`loam status` owes an `openapi.yaml` on evidence, in both forms.** A service with no living
|
|
340
|
+
contract, that no other feature in flight carries one for, owes it when the feature writes an
|
|
341
|
+
`Operations:` line for it or a tagged delta edge calling an operation on it — the links
|
|
342
|
+
`spec-api.op-undefined` and `c4-api.op-undefined` grade. A new service nothing calls now reads
|
|
343
|
+
`openapi` `done` (`required: false`), drops `next.author-openapi`, and leaves the fleet form's
|
|
344
|
+
`missing[]`, so the feature's stage can move from `missing` to `ready`. An adopted service with
|
|
345
|
+
no contract that a tagged edge calls with an op now owes one (`required: true`, `missing`,
|
|
346
|
+
`next.author-openapi`, `<svc>/openapi` in `missing[]`) where status said "none owed" beside the
|
|
347
|
+
gate's error. When the delta does not parse, the old answer stands. The evidence is the
|
|
348
|
+
feature's own: another feature's edge into a service this one introduces is that feature's
|
|
349
|
+
`c4-api.op-undefined` / `c4-api.op-pending`, not a contract this one owes.
|
|
350
|
+
- **`loam archive` does not create a living `openapi.yaml` from a contract that describes nothing**
|
|
351
|
+
(no path, webhook or component): the plan prints `openapi: <svc> — nothing to merge …, no living
|
|
352
|
+
openapi.yaml created` instead of `created ()`, and `archive --json`'s plan carries no write for it.
|
|
353
|
+
|
|
354
|
+
Exit codes: `validate --strict`, which escalates warnings, can exit 0 where one of the warnings
|
|
355
|
+
above was the only one.
|
|
356
|
+
|
|
357
|
+
### Fixed — the archive says what it replaces, creates and rewrites
|
|
358
|
+
|
|
359
|
+
Three new stable codes, all warnings that never gate: `usecase.requirement-modified`,
|
|
360
|
+
`openapi.info-not-merged`, `asyncapi.info-not-merged` (360 in all). No flag and no refusal changed;
|
|
361
|
+
the exit codes that move are `loam gate`'s and, under `--strict`, `validate`'s, both below. New
|
|
362
|
+
`--json` keys, all additive: `dropped` on `loam archive` and on `loam delta`, and `partial: true` on
|
|
363
|
+
the spec.md entry of `loam adopt`'s `targets[]`. One new frontmatter
|
|
364
|
+
field, written by loam: `adoption: partial`.
|
|
365
|
+
|
|
366
|
+
- **A MODIFIED or REMOVED requirement names what it takes away.** A MODIFIED replaces the living
|
|
367
|
+
requirement whole, so every scenario and join-line entry (`Operations:`, `Covers:`, `Requires:`,
|
|
368
|
+
`Capability:`, `Realizes:`, `Publishes:`, `Consumes:`) it did not restate vanished at archive
|
|
369
|
+
under a bare `~1`. The plan now prints one line per replaced or removed requirement naming the
|
|
370
|
+
dropped scenarios and entries, `loam archive --json` carries them as `dropped`
|
|
371
|
+
(`[{ path, kind, requirement, id?, scenarios, directives }]`), and `loam delta` carries the same
|
|
372
|
+
rows as `dropped` and prints them under *Taken out of the living docs by the merge* — on the
|
|
373
|
+
service's spec.md and arch.spec.md and on every capability document the feature changes. `path`
|
|
374
|
+
is the living document; `kind` is `MODIFIED`, `REMOVED` or `ADDED` (an ADDED that lands on a living
|
|
375
|
+
requirement of the same id or name); `id` is absent for a requirement with no `Requirement-ID:`;
|
|
376
|
+
`directives` is keyed `operations`, `covers`, `requires`, `capabilities`, `realizes`, `publishes`,
|
|
377
|
+
`consumes`, each present only for a line that loses an entry (a `Realizes:` entry is compared by
|
|
378
|
+
its target, so a pin is never a drop).
|
|
379
|
+
- **A living flow claiming a requirement the feature MODIFIES is named.** Its `#req-` tag still
|
|
380
|
+
resolves after the archive and every grade stays green, whatever the new wording says.
|
|
381
|
+
`usecase.requirement-modified` (warn) now fires wherever the feature is graded — `validate
|
|
382
|
+
--feature`, `validate --all` per active feature, `loam status <FEAT>`, and the archive's
|
|
383
|
+
`warnings[]` (it never refuses the archive) — for an `architecture/` flow whose `#cap-`-scoped `#req-` tag claims a capability
|
|
384
|
+
requirement the feature modifies, and for a flow beside a service's model whose `#req-` tag
|
|
385
|
+
names one of that service's requirements the feature's `specs/` delta modifies. **Exit code:**
|
|
386
|
+
unchanged, except under `--strict`, which fails on any warning and so now exits 1 on this shape.
|
|
387
|
+
- **A first spec.md archived into a never-adopted service is marked `adoption: partial`.** When
|
|
388
|
+
`loam archive` creates `services/<id>/spec.md` for a service that already existed and that the
|
|
389
|
+
feature does not introduce, the file carries `adoption: partial` and the plan says why.
|
|
390
|
+
`loam list`, `explore`, the context pack and `validate --all`'s scorecard keep the service at
|
|
391
|
+
`partial` (`missing` names `loam adopt --service <id>`) instead of `documented`, and `loam status`
|
|
392
|
+
lists it under `next.adopt`, with a sentence saying the spec.md holds one feature's slice, instead
|
|
393
|
+
of `next.complete-service` even when it has no model.likec4. **Exit code: `loam gate --service
|
|
394
|
+
<id>` now exits 1 for such a service** — it sits at `partial`, so `gate.service-undocumented`
|
|
395
|
+
fires where the same archive used to leave it `documented` and the gate passing; every service
|
|
396
|
+
joined to it gets `gate.partner-undocumented` (warn: exit unchanged, except under `--strict`).
|
|
397
|
+
`loam adopt --service <id>` — the command all of those name — flags that spec.md `PARTIAL` and
|
|
398
|
+
counts it as owed (`targets[]` carries `partial: true`) where it said every required artifact was
|
|
399
|
+
already there. Delete the line once the service's baseline is written — the brief and the
|
|
400
|
+
`loam-adopt` protocol now say so — and all of it clears.
|
|
401
|
+
- **The contract merges keep the bytes they did not change.** Archiving into a living
|
|
402
|
+
`openapi.yaml` or `asyncapi.yaml` re-folded every long scalar at 80 columns, re-padded flow
|
|
403
|
+
sequences and turned CRLF into LF across the whole file; only the entries the merge adds, replaces
|
|
404
|
+
or removes are now written, and the rest of the file is byte-identical (falling back to the old
|
|
405
|
+
whole-file write for a shape it cannot splice safely). A living contract the archive CREATES from
|
|
406
|
+
the feature's file keeps that file's bytes too, minus the `x-loam-*` keys it strips.
|
|
407
|
+
- **The delta's `info` is not merged, and the plan now says so.** `openapi.info-not-merged` /
|
|
408
|
+
`asyncapi.info-not-merged` (warn) name every `info` key the feature's contract spells differently
|
|
409
|
+
from the living one — including for a delta whose only change is its `info` — since the merge
|
|
410
|
+
keeps the living `info`. Edit the living contract in the same change if it should move.
|
|
411
|
+
|
|
412
|
+
### Fixed — a use-case hop can say which of several relationships it means
|
|
413
|
+
|
|
414
|
+
No code, flag or `--json` key was added or removed. Before this, a hop between two elements the map
|
|
415
|
+
joins by edges naming different operations was `usecase.step-contested` (warn) whatever the flow
|
|
416
|
+
said. On a fleet flow (`architecture/usecases/*.likec4`) between two services the map draws as
|
|
417
|
+
single elements, no edit to the flow could clear it: the repair the warning offered — draw the hop
|
|
418
|
+
between the elements that name exactly one candidate — needs containers, and a container an
|
|
419
|
+
extending model declares does not resolve on a fleet flow.
|
|
420
|
+
|
|
421
|
+
- **A step's title now picks the candidate it opens with.** Where the candidates disagree, a title
|
|
422
|
+
that STARTS with one candidate's `metadata { op }` — or with the message it `publishes` or
|
|
423
|
+
`consumes` — and goes on only after punctuation, or not at all, attributes the hop to that
|
|
424
|
+
relationship: `orders -> payments 'authorizePayment — authorizes the payment'`,
|
|
425
|
+
`'getOrder(orderId)'`, `'refundPayment'`. The name is matched exactly, case included, as a whole
|
|
426
|
+
name: a letter, digit, `_`, `.` or `-` after it means it is part of a longer name and picks
|
|
427
|
+
nothing. A name that is the first word of a sentence (`'read the order'`), a name later in the
|
|
428
|
+
title, a relationship's own title, a name the pair's candidates do not declare, or one name
|
|
429
|
+
declared by candidates that still disagree picks nothing, and the hop stays contested over every
|
|
430
|
+
candidate. A title never backs an unbacked hop and never overrules candidates that agree.
|
|
431
|
+
- **Which findings change.** A hop the title resolves is no longer `usecase.step-contested`.
|
|
432
|
+
Picked by an operation, it is attributed to that operation. Picked by a message, it is an event
|
|
433
|
+
hop, which `usecase.step-unlinked` exempts. This holds on the fleet target, on a service's own
|
|
434
|
+
flows (`loam validate --service`) and on a feature's `usecases/` flows. **Titles written before
|
|
435
|
+
this release are read the same way**: a step title that already opens with a candidate's
|
|
436
|
+
operation id and punctuation (`'createOrder: places the order'`) now picks it with no edit, its
|
|
437
|
+
next `loam validate` stops warning on that hop, and `--strict` can exit 0 where it exited 1.
|
|
438
|
+
- **Contract removals read the same pick.** `loam diff --base` and the contract-removal gate list a
|
|
439
|
+
picked hop among the flows that break when its operation is removed, and drop the "could not be
|
|
440
|
+
answered" line they printed for the other candidates' operations. A hop picked by an operation is
|
|
441
|
+
no longer listed among the flows a removed message breaks, even where another candidate consumes
|
|
442
|
+
that message; that edge itself still is. Which code fires, and the exit code, do not change: the
|
|
443
|
+
picked edge was already named as a consumer.
|
|
444
|
+
- **The `usecase.step-contested` message** now leads with how to pick, giving as the example the
|
|
445
|
+
step's own title behind a candidate's name, and only a title that would clear the warning. Drawing
|
|
446
|
+
the hop between other elements comes last, for a project that declares those elements. Its
|
|
447
|
+
`details[]` lines print every name an edge declares — `(op: <id>)`,
|
|
448
|
+
`(publishes: <message>)`, `(consumes: <message>)`, or `(no op)` when it declares none — where a
|
|
449
|
+
message edge used to print `(no op)`, and an edge declaring both an operation and a message
|
|
450
|
+
prints both, `(op: <id>, publishes: <message>)`, where it used to print only `(op: <id>)`.
|
|
451
|
+
|
|
452
|
+
## [0.2.0-alpha.10] - 2026-10-21
|
|
453
|
+
|
|
454
|
+
Everything on `main` since `0.2.0-alpha.9`. Still a prerelease on the `alpha` line.
|
|
455
|
+
|
|
456
|
+
Most of it closes the gap between a feature called clean and the fleet its archive leaves:
|
|
457
|
+
`validate --feature`, `loam status <FEAT>` and the archive gate now raise, before the archive, what
|
|
458
|
+
`validate --all` raises after it. `loam archive` keeps a created document's title, owner and
|
|
459
|
+
`## Purpose`, re-expresses every link it moves — out of the archived feature, and into it from any
|
|
460
|
+
other document — and its undo snapshot no longer breaks a feature's LikeC4 project;
|
|
461
|
+
`loam subsystem move` and `rename` rewrite the links their renames would break. A repository bound
|
|
462
|
+
to a service a feature introduces is pointed at that feature instead of being told to adopt it, by
|
|
463
|
+
`loam status`, `loam doctor`, `loam validate`, `loam show`, `loam context` and `loam steps` alike.
|
|
464
|
+
`loam status`, `loam tasks` and `loam validate` say in words where work stands, `loam explain`
|
|
465
|
+
defines the words they print, and on Windows a rolled-back write takes back the directories it
|
|
466
|
+
created.
|
|
467
|
+
|
|
468
|
+
Several entries change an exit code, listed in each. Refusals that are new: four feature-scope
|
|
469
|
+
errors (`spine.message-unproduced`, `usecase.step-unbacked`, `usecase.requirement-unresolved`,
|
|
470
|
+
`usecase.capability-unresolved`) fail `validate --feature`, `validate --all` for an active feature
|
|
471
|
+
carrying one, and `loam archive` (`not-coherent`, unless `--approve`); `loam unarchive` refuses
|
|
472
|
+
`snapshot-stale` over a relinked document edited since the archive; `move-uncommitted` also covers
|
|
473
|
+
an uncommitted document outside the moved directories that the move would relink. Refusals that are
|
|
474
|
+
gone: `loam explain` on the seventeen new terms, and `show`, `context`, `steps` and a bare
|
|
475
|
+
`validate` for a service a feature introduces. `loam archive` files `service.no-model` at
|
|
476
|
+
error severity, the severity `validate --all` gives that directory next, and it still does not gate
|
|
477
|
+
(`gates: false`): count what blocks by `gates`, never by `severity`. `loam context` with two
|
|
478
|
+
introducing features refuses `invalid-option` where it said `unknown-service`, and
|
|
479
|
+
`loam tasks <FEAT>` in a repository bound to a service the feature touches lists that service's
|
|
480
|
+
slice. The undo snapshot is now version 4, and an older loam cannot unarchive a feature this version
|
|
481
|
+
archived: upgrade every checkout that may run `loam unarchive`.
|
|
482
|
+
|
|
483
|
+
No stable code was added or renamed — 356 in all, as at alpha.9 — and no flag.
|
|
484
|
+
`loam archive --json`'s `issues[]` may now carry seven codes `validate` already had
|
|
485
|
+
(`spine.message-unproduced`, `health.uncovered` and five `usecase.*`). New `--json` keys, all
|
|
486
|
+
additive: `staleViews` on `loam unarchive`, `relinked` on `subsystem move`/`rename`, `title`,
|
|
487
|
+
`checklist` and `serviceSteps[]` on `loam status`, `title` on `show` and `list`,
|
|
488
|
+
`currentService.introducedBy` on `loam doctor`, `boundService` on `loam validate`, and `living` /
|
|
489
|
+
`introducedBy[]` on `show`, `context` and `steps` for a service a feature introduces.
|
|
490
|
+
|
|
491
|
+
### Fixed — the feature gate predicts the fleet its archive leaves
|
|
492
|
+
|
|
493
|
+
No code or flag was added. A feature `validate --feature` called clean — `loam status` "ready", the
|
|
494
|
+
archive gate agreeing — could archive at exit 0 under "living spec + landscape are now complete +
|
|
495
|
+
current." into a fleet `validate --all` then reported red. The feature scope (`validate --feature`,
|
|
496
|
+
`loam status <FEAT>`, the archive gate and `archive --dry-run`) now raises, before the archive,
|
|
497
|
+
what `validate --all` raises after it, under the same codes — except the two `Covers:` findings
|
|
498
|
+
below, which are `validate`'s alone (`--feature`, and `--all` per active feature); `loam status`
|
|
499
|
+
and the archive do not raise them:
|
|
500
|
+
|
|
501
|
+
- **A consumer with no producer is refused.** A delta `Consumes:` line or tagged `consumes` edge
|
|
502
|
+
naming a message nothing would send after the merge — no service's contract as the merge leaves
|
|
503
|
+
it, no `#external` publisher on the living map or in the delta — is `spine.message-unproduced`
|
|
504
|
+
(error) at feature scope. When only another in-flight feature's delta sends the message it is
|
|
505
|
+
`spec-event.message-pending` / `c4-event.message-pending` (warn) instead.
|
|
506
|
+
- **A feature's own flows are graded as the fleet grades them.** A hop of a
|
|
507
|
+
`features/<FEAT>/usecases/*.likec4` view that no relationship on the post-merge map backs is
|
|
508
|
+
`usecase.step-unbacked`; a `#req-` tag no `#cap-` tag scopes, or one naming no requirement as the
|
|
509
|
+
merge leaves the capability document, is `usecase.requirement-unresolved`; an unresolving `#cap-`
|
|
510
|
+
tag is `usecase.capability-unresolved` — errors. `usecase.step-contested` and
|
|
511
|
+
`usecase.step-unlinked` arrive as warnings. The messages open with the feature file's path.
|
|
512
|
+
- **A MODIFIED arch requirement that drops a `Covers: sli:` / `alert:` line** the living
|
|
513
|
+
`arch.spec.md` had is `health.uncovered` (warn) at feature scope, naming the signal.
|
|
514
|
+
- **A `Covers:` entry spelled in the delta's own ids** — a stub the delta declares where the living
|
|
515
|
+
map nests it, or a child under one — is `covers.unknown` (warn) at feature scope, offering the
|
|
516
|
+
spelling that resolves in both. An entry spelled the way the merged map will spell an element
|
|
517
|
+
this feature adds now resolves at feature scope too.
|
|
518
|
+
- **`c4.uncovered`'s hint** offers that same surviving spelling: an edge by its endpoints' service
|
|
519
|
+
names (`checkout-web -> payment-split-service`), an element by its service or the id the merge
|
|
520
|
+
gives it — the delta's own id only when the merge preview cannot be computed.
|
|
521
|
+
- **`loam archive` files `service.no-model` at error severity**, because `validate --all` reports an
|
|
522
|
+
error next — `service.no-model` for the directory the archive creates, `landscape.binding-unknown`
|
|
523
|
+
for a binding it lands with no directory, which the message now names; it still does not gate (`gates: false` in `--json` `warnings[]`). The text
|
|
524
|
+
`--dry-run` now prints it, as `--json` and the real run did, before "dry run — nothing was
|
|
525
|
+
written.", and both runs print in place of the closing line when this archive lands ahead of an
|
|
526
|
+
in-flight change it waits on (a `spec-api.op-pending`, `c4-api.op-pending`,
|
|
527
|
+
`spec-event.message-pending` or `c4-event.message-pending` warning).
|
|
528
|
+
- **A `spine.message-unproduced` producer may be an `#external` system the delta draws**, including
|
|
529
|
+
a living one it re-declares as an untagged stub: the question is asked of the map the merge leaves.
|
|
530
|
+
- **Exit code:** on any of the four errors above, `loam validate --feature`, `loam validate <FEAT>`
|
|
531
|
+
and `loam validate --all` — for every active feature carrying one, before anything is archived —
|
|
532
|
+
now exit 1 where they exited 0; `loam archive` and `archive --dry-run` exit 1 (`not-coherent`)
|
|
533
|
+
without `--approve`, which overrides all four; `loam status` counts them as gating, and
|
|
534
|
+
`loam show <FEAT> --json` reports `review.readyToArchive: false`. The warnings above
|
|
535
|
+
(`health.uncovered`, `covers.unknown`, `usecase.step-contested`, `usecase.step-unlinked`, the two
|
|
536
|
+
`*-pending`) leave the exit code alone except under `--strict`, which fails on any warning and so
|
|
537
|
+
now exits 1 on these shapes.
|
|
538
|
+
- **`service.no-model` rides at error severity in a `not-coherent` refusal's `error.issues[]`** too,
|
|
539
|
+
beside the issues that refused — count what blocks by `gates`, never by `severity`.
|
|
540
|
+
|
|
541
|
+
`loam archive --json`'s `issues[]` may carry the seven codes above (`spine.message-unproduced`,
|
|
542
|
+
`health.uncovered`, the five `usecase.*`), which the `IssueCode` union now admits; each was already
|
|
543
|
+
a stable `validate` code.
|
|
544
|
+
|
|
545
|
+
### Fixed — `loam archive` keeps what makes the living docs readable, and a link it moves still resolves
|
|
546
|
+
|
|
547
|
+
No code or flag was added. Two additive `--json` keys (`unarchive`'s `staleViews`, `subsystem
|
|
548
|
+
move`/`rename`'s `relinked`), one new snapshot layout (version 4) and one refusal that is new
|
|
549
|
+
(`move-uncommitted` over a document outside the move). Archive's `plan` gains `update` rows for the
|
|
550
|
+
feature's own documents it relinks and for every document outside the feature whose links into it
|
|
551
|
+
it rewrites, its `revouchOwed` also names a vouched spec that rewrite moves, and `unarchive`'s
|
|
552
|
+
existing `snapshot-stale` refusal now covers all of those documents too. SCHEMA.md "Conventions"
|
|
553
|
+
states the rules.
|
|
554
|
+
|
|
555
|
+
- **A living document archive CREATES carries what the delta marked as its own.** A new
|
|
556
|
+
`services/<svc>/spec.md` or `arch.spec.md` used to be a fixed header — `service`,
|
|
557
|
+
`status: draft`, `# <svc>` — and the requirements; a new `capabilities/<cap>/spec.md` was
|
|
558
|
+
`# <cap>` and the requirements. The delta's title, its narrative, its frontmatter `owner` and
|
|
559
|
+
every section around the delta sections were dropped in silence. Now a created document gets the
|
|
560
|
+
delta's `owner` (service specs), its H1 unless that title is about the change (it names the
|
|
561
|
+
feature, or calls itself a delta — the scaffold's own title does both, so a scaffolded delta keeps
|
|
562
|
+
the heading it always had), its `## Purpose` section verbatim, and one sentence linking back to
|
|
563
|
+
the archived delta ("Created when FEAT-1 was archived; its delta keeps the prose written for that
|
|
564
|
+
change", with "its delta" the link). The plan prints one note per created document, under that
|
|
565
|
+
file's line, naming what it carried and where the rest stays. A `## Purpose` in a delta against a
|
|
566
|
+
document that already exists is not merged, and the plan now says so under that file's line.
|
|
567
|
+
- **A link into the feature lands on the file's new home.** A delta link to the feature's own
|
|
568
|
+
`glossary/<term>.md`, `adrs/…` or any other file was re-expressed against the file's old place,
|
|
569
|
+
which the same archive then moved — `link.unresolved` on the first `validate --all` after a clean
|
|
570
|
+
archive. A link to a file the archive copies — a glossary term, a flow, a topology document — now
|
|
571
|
+
lands on its living copy (`glossary/<term>.md`, `architecture/usecases/…`, `architecture/…`); a
|
|
572
|
+
link to any other file under the feature lands in `features/archive/<FEAT>/`. A feature ADR is
|
|
573
|
+
never copied into the living tree: it stays with the archived feature, and living documents that
|
|
574
|
+
cite it point there. A glossary term the archive copies has its own relative links re-expressed
|
|
575
|
+
for `glossary/` too, so a term citing a service spec or the feature's ADR no longer lands broken;
|
|
576
|
+
the copy is otherwise byte-for-byte, as before. A link whose text still reaches its target from
|
|
577
|
+
where it lands keeps its exact spelling.
|
|
578
|
+
- **The documents that stay in the feature keep links that resolve.** The archive moves
|
|
579
|
+
`features/<FEAT>/` to `features/archive/<FEAT>/` as a plain directory rename, so every relative
|
|
580
|
+
link out of the feature in the intent, the feature's ADRs, its deltas — any markdown document the
|
|
581
|
+
rename carried — was one directory short afterwards, and a reader who followed a living
|
|
582
|
+
document's link into an archived ADR landed on a decision whose own links led nowhere (nothing
|
|
583
|
+
grades archived features, so nothing said so). The archive now re-expresses those links for the
|
|
584
|
+
move, in the same commit; a link to another file of the same feature, and any link whose text
|
|
585
|
+
still reaches its target, is left exactly as written. Each relinked document is an `update` row
|
|
586
|
+
in `plan`, at its path before the move (`features/<FEAT>/…`), ahead of the `move` row
|
|
587
|
+
(`--dry-run` included); the text output adds `feature documents: N relinked for
|
|
588
|
+
features/archive/<FEAT>/` with one line per document, and the snapshot parks each pre-image where
|
|
589
|
+
the move leaves it (`features/archive/<FEAT>/…`), so `loam unarchive` puts the authored bytes
|
|
590
|
+
back — archive then unarchive is still a byte-for-byte round trip — and lists them in `restored`
|
|
591
|
+
(and `discarded`, under `--force`) where they land (`features/<FEAT>/…`). A retried archive and the crash repair find those
|
|
592
|
+
pre-images across the move. No code, flag or payload key was added, and the snapshot stays
|
|
593
|
+
version 4: the new rows are literal paths every version-4 reader already restores. **Exit
|
|
594
|
+
code:** `loam unarchive` now also refuses with `snapshot-stale` (exit 1) when a document the
|
|
595
|
+
archive relinked was edited under `features/archive/<FEAT>/` since; `--force` puts the authored
|
|
596
|
+
text back over that edit. Those documents used to move back as they stood.
|
|
597
|
+
- **A document outside the feature that links into it still resolves once the feature is
|
|
598
|
+
archived.** Another change in flight citing the feature's ADR (`../<FEAT>/adrs/0001-x.md` from its
|
|
599
|
+
intent), or a living service or fleet ADR citing a decision taken for a change still open, was
|
|
600
|
+
correct the moment before the archive and `link.unresolved` (an error) on the first
|
|
601
|
+
`validate --all` after it: the rename took the directory away from under the link, and the
|
|
602
|
+
archived path the author would have needed did not exist until the archive did. The archive now
|
|
603
|
+
rewrites those links in the same commit, across the whole link corpus — the fleet's own documents,
|
|
604
|
+
every service's, every other in-flight feature's, the documents `subsystem move` rewrites for its
|
|
605
|
+
renames: a term the archive copies into `glossary/` is cited at its living copy, everything else
|
|
606
|
+
in the feature where it now lives (`features/archive/<FEAT>/…`), and every other link keeps its
|
|
607
|
+
spelling. A living spec the merge rewrites is relinked from its merged text, so it is still one
|
|
608
|
+
`plan` row. Each relinked document is an `update` row in `plan` (`--dry-run` included); the text
|
|
609
|
+
output adds `links into the feature: N document(s) relinked for features/archive/<FEAT>/`, one
|
|
610
|
+
line per document. The costs are said, not softened: a vouched `spec.md` or `arch.spec.md` the
|
|
611
|
+
rewrite moves off its stamp is on `revouchOwed` like one the merge moves (`content.stale` follows
|
|
612
|
+
until its owner re-vouches), and a rewritten route inside a living requirement adds a `note:` line
|
|
613
|
+
saying a feature that pins it with `Based-On:` reports `delta.baseline-stale` until `loam rebase`.
|
|
614
|
+
No code, flag or payload key was added. The undo is the snapshot's — each relinked document is an
|
|
615
|
+
ordinary row, so `loam unarchive` puts the authored bytes back and archive-then-unarchive stays a
|
|
616
|
+
byte-for-byte round trip. **Exit code:** `loam unarchive` now also refuses with `snapshot-stale`
|
|
617
|
+
(exit 1) when such a document — another in-flight feature's intent among them — was edited since
|
|
618
|
+
the archive; `--force` puts the pre-archive text back over that edit. A document whose own feature
|
|
619
|
+
has been archived or removed since is not drift: `unarchive` leaves it as it is, with a `left:`
|
|
620
|
+
line in the text output (no `--json` key), rather than recreating a one-file `features/<other>/`.
|
|
621
|
+
- **The undo snapshot no longer breaks a feature's LikeC4 project.** `.loam-before/` stored each
|
|
622
|
+
pre-image under its living name, so the archived feature held a second `landscape.likec4`: a
|
|
623
|
+
feature with its own `likec4.config.json`, or rendered alone, failed on duplicate elements, and so
|
|
624
|
+
did a fleet project that did not exclude `features/`. Snapshot manifests are now **version 4** and
|
|
625
|
+
store each pre-image as `files/<path>.before`. Version-2 and version-3 snapshots are still read
|
|
626
|
+
and restored, each under its own layout, and are never rewritten. **An older loam cannot unarchive
|
|
627
|
+
a feature this version archived** — it refuses with `snapshot-missing`, as it does any layout it
|
|
628
|
+
does not know — and it cannot repair an archive, unarchive or subsystem move this version was
|
|
629
|
+
killed in the middle of; finish those with this version, and upgrade every checkout that may run
|
|
630
|
+
`loam unarchive`.
|
|
631
|
+
- **`loam unarchive` names the generated views it left behind.** It restores what the archive
|
|
632
|
+
wrote, and `architecture/subsystems.likec4` / `architecture/services.likec4` are not among it, so
|
|
633
|
+
after `loam subsystem sync` had run on the merged map the undo closed on "the living docs are back
|
|
634
|
+
to what they said before the archive" over a fleet the next `validate --all` failed
|
|
635
|
+
(`subsystem.views-stale`). It now prints one `⚠ <path> no longer matches the tree and this map`
|
|
636
|
+
line per stale file, naming `loam subsystem sync`, under a narrower closing line, and `--json`
|
|
637
|
+
carries the list as `staleViews` — the key `archive --json` already has, same meaning, absent when
|
|
638
|
+
nothing is stale.
|
|
639
|
+
- **`loam subsystem move` and `rename` rewrite the relative links the renames would break** — out of
|
|
640
|
+
every markdown document under a moved directory, and into a moved directory from every other
|
|
641
|
+
authored document (fleet documents, services, in-flight features; archived features are left as
|
|
642
|
+
history) — in the same transaction as the renames, and stage them in git with the renames. Each
|
|
643
|
+
rewritten document is printed as `relinked <path>` and listed in `--json` as `relinked` (always
|
|
644
|
+
present; `[]` when no link needed rewriting). A rewritten route is still a changed byte, and the
|
|
645
|
+
move says so in its `warnings` (text: `note:` lines): a `verified` document it relinks no longer
|
|
646
|
+
hashes to its `content_digest`, so `content.stale` follows (exit 1 under `--strict`) until its
|
|
647
|
+
owner re-vouches, and a living requirement it relinks moves the digest a feature in flight may
|
|
648
|
+
have pinned with `Based-On:`, so that feature reports `delta.baseline-stale` until `loam rebase`.
|
|
649
|
+
Before this release the same move left those links `link.unresolved` instead. **Exit code:**
|
|
650
|
+
`move-uncommitted` now also refuses (exit 1) when git reports an uncommitted or untracked
|
|
651
|
+
document OUTSIDE the moved directories that the move would relink — an in-flight feature's work
|
|
652
|
+
in progress included; it used to proceed and leave the link broken. A commit killed after its
|
|
653
|
+
renames now recovers a relinked document from where the rename took it instead of refusing it as
|
|
654
|
+
edited.
|
|
655
|
+
|
|
656
|
+
### Changed — `loam status` says where a feature stands in words, with the claims counted
|
|
657
|
+
|
|
658
|
+
No code was added or renamed, and no exit code changed. The `stage` values (`missing`, `blocked`,
|
|
659
|
+
`draft`, `ready`, `done`) are untouched; what changed is that the human view no longer prints them
|
|
660
|
+
bare. `ready` means the documents are written and pass the shared checks — nothing about code or
|
|
661
|
+
evidence — and `loam status FEAT-101 — ready` over a feature nobody had built or verified read as
|
|
662
|
+
"ready to ship"; `done (archived)` over a feature that was never verified read as "shipped and
|
|
663
|
+
checked", when archive checks documents and nothing else.
|
|
664
|
+
|
|
665
|
+
- **The header is a sentence:** `loam status FEAT-101 (Payment splitting): docs ready — nothing
|
|
666
|
+
verified yet (0 of 19 claim(s) answered)`; for an archived feature `archived — never verified (no
|
|
667
|
+
verification record)`, `archived — verified (…)` or `archived — attested, not verified: …`. The
|
|
668
|
+
machine value moves to its own `stage` row, which names `loam explain <stage>`.
|
|
669
|
+
- **The artifact table says what each row means:** `written`, `not owed`, `missing`, `has errors`,
|
|
670
|
+
`blocked`; the verification row `none yet`, `open`, `attested`, `verified`, `stale`,
|
|
671
|
+
`unreadable`. The `verification` row with no record says how many claims there are to answer.
|
|
672
|
+
`coherence` ends with `N stop it (validate --feature or archive refuses on each)` — every error
|
|
673
|
+
and every gating warning — instead of `N gate archive`, which counted only findings carrying
|
|
674
|
+
`gates` and so read 0 over a feature archive refuses for an unknown service. The `checks.gating`
|
|
675
|
+
payload value is unchanged.
|
|
676
|
+
- **The fleet form says less than the feature form, on purpose:** it grades nothing, so a verified
|
|
677
|
+
feature reads "its own status says whether archive will take it", not "ready to archive"; and a
|
|
678
|
+
feature behind an interrupted commit reads as blocked by the commit even when it also waits on
|
|
679
|
+
another feature.
|
|
680
|
+
- **`next[]` statements (prose, not codes):** `next.archive` no longer says a feature "ships once
|
|
681
|
+
everything above is done" — it says archive checks only the documents, not the code and not the
|
|
682
|
+
verification record; its clean form says "archive it" instead of "ship it". `next.archived` no
|
|
683
|
+
longer says the feature "shipped": it says what archiving did and whether it was verified. The
|
|
684
|
+
fleet form's `next.feature` says the stage in the same words (`FEAT-101 (Payment splitting): docs
|
|
685
|
+
ready — nothing verified yet (no verification record).`) instead of `FEAT-101 is at 'ready'.`, and
|
|
686
|
+
`next.elided` counts what it left out by code.
|
|
687
|
+
- **Additive payload keys:** `feature.title` and `checklist` (`{claims}` — how many claims the
|
|
688
|
+
feature's checklist asks now; `null` for an archived feature) on `loam status <FEAT> --json`;
|
|
689
|
+
`features[].title` and `serviceSteps[]` on `loam status --json`; `title` on
|
|
690
|
+
`loam show <FEAT> --json` and on `loam list --json`'s `features[]`. The title is intent.md's
|
|
691
|
+
`title:`, else its first `# ` heading without the feature id, else `null`.
|
|
692
|
+
- **Feature rows name the change:** `loam status`, `loam list` and `loam show <FEAT>` print the
|
|
693
|
+
intent title beside the id. An intent.md loam cannot read is no title, never a refusal.
|
|
694
|
+
- **Help text:** `loam verify` is described as "Check a feature against its own promises: derive the
|
|
695
|
+
claims, record the answers with evidence" and `loam archive` as "Merge a finished feature's deltas
|
|
696
|
+
into the living specs + model, then archive it (checks the documents, not the code)" — neither
|
|
697
|
+
says "shipped" any more. The `validate --all` scorecard's features row glosses `ready` and `done`.
|
|
698
|
+
|
|
699
|
+
### Changed — `loam status` lists the features in flight before any service's adoption work
|
|
700
|
+
|
|
701
|
+
No code, flag or exit code changed; the ORDER of the fleet form's `next[]` did, and one payload
|
|
702
|
+
key was added. The per-service rungs (`next.adopt`, `next.complete-service`, `next.revouch`) used
|
|
703
|
+
to lead the list, so on a fleet mid-adoption the cap of ten filled with them and elided the one
|
|
704
|
+
feature in flight as "more step(s) of the same kinds".
|
|
705
|
+
|
|
706
|
+
- **Order:** `next.recover-commit` (outside the cap), then `next.adopt-bound` or the first-hour
|
|
707
|
+
ladder, then every feature (`next.feature`, `next.archive`), then the per-service rungs — the
|
|
708
|
+
bound service's own still first among them — then `next.elided` and `next.fleet-gate`. An agent
|
|
709
|
+
reading `next[0]` in a docs repo with a feature in flight now gets that feature, not an adopt.
|
|
710
|
+
- **`serviceSteps[]`** (additive, `loam status --json`): every per-service rung, uncapped, grouped
|
|
711
|
+
by code — `{code, services, command}`, `command` being the first service's.
|
|
712
|
+
- **The human view folds each per-service code into one counted line** under the features —
|
|
713
|
+
`next.adopt ×12: 12 services with no spec.md — …: a, b, c, …, +6 more.` — and only says
|
|
714
|
+
something was left out when features were.
|
|
715
|
+
|
|
716
|
+
### Changed — a repository bound to a service its feature introduces is not told to adopt it
|
|
717
|
+
|
|
718
|
+
No code was added. In a service repository whose `loam.json` names a service that exists only in
|
|
719
|
+
an active feature's `specs/<service>/`, `loam status` led with `next.adopt-bound` →
|
|
720
|
+
`loam adopt --service <id>` — adopting code that does not exist yet. It now leads with that
|
|
721
|
+
feature's `next.feature` (`loam status <FEAT> --json`) — one per feature that introduces it —
|
|
722
|
+
saying the service joins the living docs when one of them archives, and does not list those
|
|
723
|
+
features again. A bound service no feature
|
|
724
|
+
introduces gets `next.adopt-bound` as before.
|
|
725
|
+
|
|
726
|
+
### Changed — `loam doctor` agrees: a service a feature introduces is not something to adopt
|
|
727
|
+
|
|
728
|
+
No code was added and no exit code changed. In the same state — a service repository whose
|
|
729
|
+
`loam.json` binds a service that exists only in an active feature's `specs/<service>/` — `loam
|
|
730
|
+
doctor` still filed `doctor.service-unknown` with "Run `loam adopt --service <id>` to onboard it".
|
|
731
|
+
The finding keeps its code and its `warning` severity (the binding still names nothing under
|
|
732
|
+
`services/`, and `healthy` does not move), and now says which feature introduces the service and
|
|
733
|
+
that it is not in the living docs until that feature archives. Its `fix` names `loam status <FEAT>`
|
|
734
|
+
and no longer tells anyone to adopt.
|
|
735
|
+
|
|
736
|
+
- **Human view:** the `binding` row reads `payment-split-service · introduced by FEAT-101 (in
|
|
737
|
+
flight)` instead of `· unknown`, listing every introducer when more than one feature introduces
|
|
738
|
+
the service.
|
|
739
|
+
- **`--json` (additive):** `currentService.introducedBy` — the introducing features' ids, ordered
|
|
740
|
+
by id — present only when an active feature introduces the configured service.
|
|
741
|
+
`currentService.status` stays `unknown`.
|
|
742
|
+
- `loam explain doctor.service-unknown` and the `loam-codes` page of `loam instructions` describe
|
|
743
|
+
both states and both fixes.
|
|
744
|
+
- A bound service no feature introduces gets the adopt advice, byte for byte, as before.
|
|
745
|
+
|
|
746
|
+
### Changed — `loam validate` in that repository validates the feature that introduces its service
|
|
747
|
+
|
|
748
|
+
No code was added. In the same state, a bare `loam validate` refused with `✗ No service directory
|
|
749
|
+
at services/payment-split-service. Did you mean: payment-service? (service.unknown)` and exited 1.
|
|
750
|
+
It treated the newest service in the fleet as a typo of an older one, so that repository's CI could
|
|
751
|
+
not pass until the feature archived. A service a feature introduces has no `services/<id>/` yet.
|
|
752
|
+
Its documents are the feature's `specs/<id>/`, so the run now validates each introducing feature
|
|
753
|
+
instead. That is the same grade `loam validate --feature <FEAT>` gives, target for target, and the
|
|
754
|
+
exit code follows it: `0` when the feature is valid, `1` when it has errors. `--strict` and
|
|
755
|
+
`--errors-only` work as before.
|
|
756
|
+
|
|
757
|
+
- **Human view:** two lines above the report say what was validated and why —
|
|
758
|
+
`payment-split-service introduced by FEAT-101 (in flight) — not in the living docs until it
|
|
759
|
+
archives` (the header `loam show payment-split-service` prints), then `validating FEAT-101
|
|
760
|
+
instead: its specs/payment-split-service/ are this service's documents until then`.
|
|
761
|
+
- **`--json` (additive):** `boundService` — `{ id, living: false, introducedBy }`, the same keys
|
|
762
|
+
`show` and `steps` use for this state — present only on such a run. `targets` then holds the
|
|
763
|
+
features and no service, and the `summary` counts no services.
|
|
764
|
+
- **Naming the service** with `--service <id>` or the positional still targets a service, so it is
|
|
765
|
+
still `service.unknown` at error and exits 1. The message now names the feature in place of a
|
|
766
|
+
near-miss: ``No service directory at services/payment-split-service: FEAT-101 introduces it, and
|
|
767
|
+
it is not in the living docs until FEAT-101 archives. Until then its documents are FEAT-101's
|
|
768
|
+
specs/payment-split-service/ — run `loam validate --feature FEAT-101` to validate them.``
|
|
769
|
+
- `loam explain service.unknown`, its row in the `loam-check` fix table (the text `--json`'s
|
|
770
|
+
`fixes` carries) and the `loam-codes` page describe both states.
|
|
771
|
+
- A name no active feature introduces keeps today's `service.unknown` message and near-miss,
|
|
772
|
+
unchanged.
|
|
773
|
+
|
|
774
|
+
### Added — `loam explain` defines the words loam prints, and a finding page says which command reports it
|
|
775
|
+
|
|
776
|
+
No code was added. `loam explain` knew six concept terms and answered `unknown-target` for the
|
|
777
|
+
words a person meets first in loam's own output.
|
|
778
|
+
|
|
779
|
+
- **Seventeen new concept terms** (23 in all), with aliases: the stages — `stage` (`missing`,
|
|
780
|
+
`blocked`, `draft`), `ready`, `done`, each saying what it does NOT mean (`ready` is documents
|
|
781
|
+
only; an archived feature is `done` whether or not anybody verified it); `claim` and each claim
|
|
782
|
+
kind — `service.exists`, `api.exposes`, `event.declares`, `c4.calls`, `scenario.tested`; the
|
|
783
|
+
verdicts — `verified` (`unverified`), beside the existing `attested`; the `loam tasks` words —
|
|
784
|
+
`unit`, `frontier`, `describe`, `generate`, `implement`; `coherence` (`gates`, `gating`); and the
|
|
785
|
+
maturity ladder — `maturity` (`empty`, `partial`, `documented`, `sourced`). An alias names the entry
|
|
786
|
+
that answered it. **Exit code:** 0 where it was 1 (`unknown-target`) for each new term and alias.
|
|
787
|
+
- **The bare `loam explain` listing groups the terms** under four headings (feature stages, the
|
|
788
|
+
done-check, the work packet, the documents). `--json` `terms[]` keeps its row shape; it has 23
|
|
789
|
+
rows instead of 6, in the listing's grouped order, so the six existing terms are at new positions.
|
|
790
|
+
`loam explain --help` and the MCP `explain` tool's `subject` description list the new terms.
|
|
791
|
+
- **A finding page's header says which command reports the code** — "when `loam validate --feature
|
|
792
|
+
<FEAT-id>` reports it — warn" — instead of `[--feature <FEAT-id>] (warn)`, which read as an
|
|
793
|
+
optional flag. `--json` `entries[].scope` is unchanged. The `next.*` entries say what the steps
|
|
794
|
+
now say: archive checks documents only, and the fleet list leads with the features.
|
|
795
|
+
|
|
796
|
+
### Changed — `loam show`, `loam context` and `loam steps` answer for a service a feature introduces
|
|
797
|
+
|
|
798
|
+
No code was added. A service an active feature introduces (`features/<FEAT>/specs/<svc>/` exists,
|
|
799
|
+
`services/<svc>/` does not yet) was refused by all three as unknown, while `loam tasks` listed work
|
|
800
|
+
for it on the same tree.
|
|
801
|
+
|
|
802
|
+
- **`loam show <svc>`** prints the introduced-service view: which feature introduces it, its delta
|
|
803
|
+
documents, requirement counts, operations, and the `loam show`/`delta`/`tasks` commands to run.
|
|
804
|
+
`--json` is `{type: "service", id, living: false, introducedBy[], introductions[]}`, each
|
|
805
|
+
introduction carrying the keys of `show <FEAT>`'s `services[]` plus `requirements`, `archSpec` and
|
|
806
|
+
`next[]`. **Exit code:** 0 where it was 1 (`unknown-target`).
|
|
807
|
+
- **`loam context <svc>`** briefs from the introducing feature's delta, saying the service is not in
|
|
808
|
+
the living docs yet. `--json` adds `living: false` and `introducedBy[]`, and `path` is the
|
|
809
|
+
feature's `specs/<svc>/`. `--feature` naming a feature that does not introduce it refuses
|
|
810
|
+
`unknown-service`. **Exit code:** 0 where it was 1 (`unknown-service`, or `invalid-option` when the
|
|
811
|
+
feature's `specs/` directory name is not a legal service id). **Code change:** with two
|
|
812
|
+
introducing features and no `--feature`, the refusal is now `invalid-option` naming both, where it
|
|
813
|
+
was `unknown-service` — a script that read `unknown-service` as "no such service" sees the new
|
|
814
|
+
code for this one case. Its "did you mean" hint now includes introduced services.
|
|
815
|
+
- **`loam steps --service <svc>`** (or the bound service) inventories the step phrases of the
|
|
816
|
+
feature's spec deltas. `--json` adds `living: false`, `introducedBy[]` and `specs[]`. **Exit
|
|
817
|
+
code:** 0 where it was 1 (`unknown-target`, or `invalid-option` when the feature's `specs/`
|
|
818
|
+
directory name is not a legal service id).
|
|
819
|
+
- Payloads for living services, and the refusals for a name nothing introduces, are unchanged.
|
|
820
|
+
|
|
821
|
+
### Changed — `loam tasks` reads as a to-do list, and a service repository sees its own slice
|
|
822
|
+
|
|
823
|
+
No code or flag was added.
|
|
824
|
+
|
|
825
|
+
- **The human view** groups units by service and then by requirement (its file printed once), and
|
|
826
|
+
says each unit as an action — "make scenario '…' pass, then record the run", "generate the test
|
|
827
|
+
for scenario '…' (loam gherkin <FEAT>)", "expose operation '…'", "call '…' on <svc>" — keeping the
|
|
828
|
+
unit id on every row. The header says in words what `record` and `stamps` mean, and the frontier
|
|
829
|
+
line leads with the next unit's action. `--next` leads with the action and warns when this
|
|
830
|
+
repository cannot see whether the scenario's test was generated; the Given/When/Then lines are
|
|
831
|
+
unchanged.
|
|
832
|
+
- **`--service` lists every unit**; the whole-feature view still caps each service at 12, and now
|
|
833
|
+
points at `loam tasks <FEAT> --service <id>` instead of `--json`.
|
|
834
|
+
- **Behaviour change:** in a repository whose `loam.json` names a `service`, `loam tasks <FEAT>`
|
|
835
|
+
without `--service` narrows to that service when the feature touches it. Under `--json`, `service`
|
|
836
|
+
is the bound id; `units`, `counts`, `frontier`/`frontierElided`, `contracts` and `size.calls`
|
|
837
|
+
cover that service only; and `waitingOn` appears. The generated-suite stamps that tell `generate`
|
|
838
|
+
from `implement` are read from this repository alone, so other services' steps in a whole-feature
|
|
839
|
+
view were never about their repositories. Pass `--service <id>` for another service; a docs repo,
|
|
840
|
+
and a feature that does not touch the bound service, get the whole feature as before.
|
|
841
|
+
|
|
842
|
+
### Changed — `loam validate`'s human report marks every error, names files relatively, and leads with what matters
|
|
843
|
+
|
|
844
|
+
No code, severity, subject, payload key or exit code changed; `--strict` and the summary counts are
|
|
845
|
+
the same.
|
|
846
|
+
|
|
847
|
+
- **`service.no-model` and `service.unknown` print with ✗** — they printed with no marker.
|
|
848
|
+
- **Messages name files by repo-relative `/` paths** instead of the absolute path of the machine
|
|
849
|
+
that ran the check: `service.unknown`, no-model, no-spec, no-openapi, no-asyncapi, an unreadable artifact
|
|
850
|
+
(`service.unreadable` / `feature.unreadable`) and `health.dependency-unmodelled`. The same
|
|
851
|
+
`message` strings in `--json` change with them. A path outside the docs repo keeps its absolute
|
|
852
|
+
spelling.
|
|
853
|
+
- **`sources.unverifiable-from-here` prints ○, not ✓** — that check did not run here. It stays
|
|
854
|
+
severity `ok`.
|
|
855
|
+
- **On `--all`, a warning or error more than three services raise once each prints once**,
|
|
856
|
+
followed by one line counting them (`same code ×N on M services: …`, capped, `+K more`). A
|
|
857
|
+
service raising the code more than once, and a finding carrying details, still print in full;
|
|
858
|
+
every finding is still in `--json`.
|
|
859
|
+
- **`--all` opens with the counts and an "errors by code" index, then the feature blocks**, then the
|
|
860
|
+
landscape and per-service findings; an interrupted-commit finding still leads and the footer still
|
|
861
|
+
closes the report.
|
|
862
|
+
|
|
863
|
+
### Fixed — on Windows, a rolled-back write takes back the directories it created
|
|
864
|
+
|
|
865
|
+
No code, flag or payload key was added. On Windows under Node 24, a command that failed and rolled
|
|
866
|
+
back — `loam archive` most visibly — answered `merge-failed` ("rolled back, nothing was merged")
|
|
867
|
+
and still left the directories it had made standing: an empty `features/archive/`, or an empty
|
|
868
|
+
`services/<new-service>/` that names a service the merge never added. Node reports the directory
|
|
869
|
+
`mkdir` created in the `\\?\C:\…` spelling, and the rollback, comparing that with the plain
|
|
870
|
+
spelling, found nothing inside it to remove. Those directories are now removed on Windows as they
|
|
871
|
+
already were on Linux and macOS; a directory something else has put a file in is still left alone.
|
|
872
|
+
|
|
873
|
+
## [0.2.0-alpha.9] - 2026-10-20
|
|
874
|
+
|
|
875
|
+
The first publish since `0.2.0-alpha.7`. `0.2.0-alpha.8` was tagged but never reached npm (its
|
|
876
|
+
release run failed at `npm ci` on a stripped lockfile), so a repository upgrading from alpha.7 takes
|
|
877
|
+
alpha.8's section below as well as this one. Still a prerelease on the `alpha` line.
|
|
878
|
+
|
|
879
|
+
Most of it is fixes. `loam archive` merges into a landscape written in any form LikeC4 parses —
|
|
880
|
+
several model blocks, markdown strings, split heads, hyphenated ids — and no longer writes a wrong
|
|
881
|
+
merge at exit 0 for the shapes listed under "Fixed — `loam archive` merges into a landscape…",
|
|
882
|
+
which alpha.7 did. Consumers are counted once, by one index, so `loam archive`, `loam validate` and
|
|
883
|
+
`loam diff` agree; the PR gate sees a changed schema, an edited `op` on an edge and a caller drawn
|
|
884
|
+
with no `op`; `loam gate --since` keeps an archived feature in view; the trust readouts show stale
|
|
885
|
+
vouches; a waiting writer no longer deletes the next writer's lock.
|
|
886
|
+
|
|
887
|
+
Several entries change an exit code, listed in each: refusals that are new (an overwrite or
|
|
888
|
+
retirement a consumer still names, a delta reviving a retired operation, an unreadable consumer
|
|
889
|
+
document) and refusals that were false and are gone. A federated verification record is now one
|
|
890
|
+
file per service, and an older loam does not read it: upgrade every repository that records or
|
|
891
|
+
gates a feature together. New: `loam list suites`, `loam new --modifies`, `loam gate --since`,
|
|
892
|
+
`loam_adopt` on MCP; six new stable codes — `openapi.op-revived`, `asyncapi.slot-revived`,
|
|
893
|
+
`next.revouch`, `frontmatter.owner-mismatch`, `diff.op-modified` and `diff.message-modified`.
|
|
894
|
+
|
|
895
|
+
### Changed — one consumer index: `loam archive`, `loam validate` and `loam diff` agree about who still consumes an operation or a message
|
|
896
|
+
|
|
897
|
+
No code or flag was added; one additive `--json` key on two existing codes. Who still consumes what
|
|
898
|
+
a change takes out was answered three ways — the coherence gate (`openapi.remove-op-consumed`,
|
|
899
|
+
`asyncapi.remove-message-consumed`), `loam diff`'s victims (`diff.op-removed-consumed`,
|
|
900
|
+
`diff.message-removed-consumed`, the `-deprecated`/`-modified` details, `diff.consumer-added` /
|
|
901
|
+
`diff.consumer-removed`) and `loam archive`'s `openapi.op-modified` / `asyncapi.message-modified`
|
|
902
|
+
warnings — and the three copies read different documents. They are one index now; SCHEMA.md's
|
|
903
|
+
"Who still consumes it" states the rules.
|
|
904
|
+
|
|
905
|
+
- **More consumers are found, so more removals are refused:**
|
|
906
|
+
- **Edges come from the whole `architecture/` project**, not `architecture/landscape.likec4`
|
|
907
|
+
alone, for the gate as for `loam diff` and the archive's overwrite warnings: an edge with `metadata { op }` (or `consumes`) drawn in
|
|
908
|
+
`architecture/calls.likec4` or in a use-case document counts. The gate passed such a removal
|
|
909
|
+
and the next `validate --all` reported `spine.op-undefined` on the edge. Where only a sibling
|
|
910
|
+
document fails to parse, the map file's own edges still count.
|
|
911
|
+
- **The gate names use-case hops** attributed to the operation (or backed by a `consumes` edge),
|
|
912
|
+
as `loam diff` already did.
|
|
913
|
+
- **An `arch.spec.md` `Operations:` line** of another service counts, as a spec.md one does —
|
|
914
|
+
for the gate, `loam diff`'s victims, `diff.consumer-added` / `diff.consumer-removed` and the
|
|
915
|
+
archive's overwrite warnings. So an
|
|
916
|
+
operation's `diff.consumer-added` / `-removed` is not reported while the consumer's
|
|
917
|
+
`arch.spec.md` cannot be read on the other side, as it never was over an unreadable `spec.md`.
|
|
918
|
+
The consumer's own `openapi.yaml` does not suppress it: an unreadable contract proves no
|
|
919
|
+
ownership, so the line counts on that side too. The exception is a line whose join exists only
|
|
920
|
+
because that contract did not parse on one side, while on the other side it reads and defines
|
|
921
|
+
the id: there the line names the consumer's own operation, and only the contract's readability
|
|
922
|
+
changed. No `diff.consumer-added` / `diff.consumer-removed` is reported for it — before, one per
|
|
923
|
+
provider of the id, each counted in the `--json` summary's `added` / `removed`. The contract's own
|
|
924
|
+
`unreadable[]` entry is still reported, and `loam diff` still exits 1 on it.
|
|
925
|
+
- **The provider's own `spec.md` requirements count for a marker retirement too**, as they do for
|
|
926
|
+
the overwrite of the entry below: one still naming the retired operation after the merge (a
|
|
927
|
+
second one beside the REMOVED requirement) archived at exit 0 and left `spec-api.op-undefined`.
|
|
928
|
+
Its `arch.spec.md` `Operations:` lines count for neither shape: `validate` grades such a line in
|
|
929
|
+
spec.md only, so one in arch.spec.md strands nothing. An edge the provider draws to itself
|
|
930
|
+
counted already, and still does — the spine grades it like any other.
|
|
931
|
+
`loam diff` and the archive's overwrite warnings still leave the provider's own documents out:
|
|
932
|
+
they name who ELSE is affected.
|
|
933
|
+
- **On the event axis the provider's own `Publishes:` lines count**, as the merge would leave
|
|
934
|
+
them. A message slot re-keyed to another name, or a send operation turned to receive, while the
|
|
935
|
+
provider's requirement still publishes the message, archived at exit 0 and the next
|
|
936
|
+
`validate --all` reported `spec-event.message-undefined`; it is `asyncapi.remove-message-consumed`
|
|
937
|
+
now. So do its own `publishes` edges, which the spine grades on their source: a full retirement
|
|
938
|
+
over a living `paymentService -> billingService { metadata { publishes '…' } }` archived at exit
|
|
939
|
+
0 and left `spine.message-undefined` on the provider. The remedy names "drop or redraw its
|
|
940
|
+
publishes edge".
|
|
941
|
+
- **Fail closed**: a spec the merge refuses to apply — a malformed `Requirement-ID:` in the
|
|
942
|
+
living spec or the delta — no longer answers "no consumers". The living requirements count,
|
|
943
|
+
plus every requirement the delta ADDS or MODIFIES.
|
|
944
|
+
- **A consumer document loam cannot read refuses the removal**, as the gate did before it asked
|
|
945
|
+
one index: another service's `spec.md` or `arch.spec.md` that does not decode as UTF-8, the
|
|
946
|
+
provider's own `spec.md` (either spec file, for a message), or a service id two directories
|
|
947
|
+
claim. `openapi.remove-op-consumed` /
|
|
948
|
+
`asyncapi.remove-message-consumed` name each in `details[]` and say the gate could not check —
|
|
949
|
+
never that the fleet still consumes it. An edge drawn with no `metadata { op }` and a contested
|
|
950
|
+
use-case hop still never refuse; `loam diff` lists them as doubts, as before.
|
|
951
|
+
- **`loam diff` counts only a service that SENDS a message as still producing it.** A removed
|
|
952
|
+
message another service declares only with `action: receive` — a consumer's own declaration,
|
|
953
|
+
which validate requires — was `diff.message-removed` (warn), "still declared by" that consumer,
|
|
954
|
+
while the gate refused the same removal and the next `validate --all` reported
|
|
955
|
+
`spine.message-unproduced` there. It is `diff.message-removed-consumed` now, naming the gate's
|
|
956
|
+
consumers; the warning for a message another service does send reads "still sent by".
|
|
957
|
+
- **Fewer refusals where the consumer was false or already retired:**
|
|
958
|
+
- **Another service's `Operations:` line naming an id its own contract defines** names its own
|
|
959
|
+
operation, and is no consumer (gate, `loam diff`, `diff.consumer-added` / `-removed`, the
|
|
960
|
+
archive's overwrite warnings): shared
|
|
961
|
+
ids such as `healthCheck` no longer refuse a rename, and `--approve` is no longer the only way
|
|
962
|
+
past a fleet `validate --all` calls clean.
|
|
963
|
+
- **Read after the feature's merge**: a consumer requirement the same feature MODIFIES to drop
|
|
964
|
+
the id, or REMOVES, no longer counts — another service's included, which is the remedy the
|
|
965
|
+
refusal itself names.
|
|
966
|
+
- **A message another service would still send after the merge strands no consumer.**
|
|
967
|
+
`asyncapi.remove-message-consumed` counted every `consumes` edge, `Consumes:` line and use-case
|
|
968
|
+
hop of a message whoever sent it, so retiring it from one of two senders was refused over the
|
|
969
|
+
other sender's consumers. Its remedy said to drop that sender's own `consumes` edge. Meanwhile
|
|
970
|
+
`loam diff` reported the same removal as `diff.message-removed` (warn), "still sent by …", and
|
|
971
|
+
the next `validate --all` was clean. Now a consumer refuses only while no service would send the
|
|
972
|
+
message after this feature's merge. The senders are read as the merge leaves them: a service
|
|
973
|
+
the feature carries an event delta for sends what its merged contract would (the delta itself
|
|
974
|
+
where it creates that contract), so moving production to another service in the same feature
|
|
975
|
+
passes, and a feature that retires the message from every sender is still refused. An unreadable contract never counts as
|
|
976
|
+
a sender. Beside another sender, the provider's own `Publishes:` lines, its own `publishes`
|
|
977
|
+
edges and its own unreadable spec files still refuse; another service's unreadable spec file,
|
|
978
|
+
or an id two directories claim, does not. This predates the one index: the gate behaved the
|
|
979
|
+
same before it. The message is reworded to say which case applies. With no sender left it reads
|
|
980
|
+
"no readable contract would still send it after this merge" and adds the route "keep another
|
|
981
|
+
service sending it". With a sender left it names the senders, lists only the provider's own
|
|
982
|
+
documents, and gives only their remedy.
|
|
983
|
+
- **An `#external` element that publishes the message keeps its consumers produced too**, for
|
|
984
|
+
the gate and for `loam diff` as for `validate`. Both counted only senders inside the fleet, so
|
|
985
|
+
retiring a message from its only fleet sender was refused while an `#external` element still
|
|
986
|
+
published it (an edge from it with `metadata { publishes }`), and after `--approve` `loam diff`
|
|
987
|
+
reported `diff.message-removed-consumed` (error) — while `spine.message-unproduced` accepts such
|
|
988
|
+
a producer and the next `validate --all` was clean. Now the outside producer counts as a
|
|
989
|
+
sender, read off the living `architecture/` project with validate's own rule: only while the
|
|
990
|
+
project parses, and not from an edge the feature's own C4 delta draws. The gate asks the rule
|
|
991
|
+
over the service directories as the merge leaves them, so an element the same feature makes a
|
|
992
|
+
service — a `specs/<id>/` directory for the id the element, or the element it is nested in,
|
|
993
|
+
resolves to, which the archive turns into `services/<id>/` — no longer counts, and the
|
|
994
|
+
retirement is refused, as the next `loam diff` and `validate --all` would read it; `loam diff`
|
|
995
|
+
asks over the directories as they stand. `loam diff` reports
|
|
996
|
+
`diff.message-removed` (warn), "still sent by `<title>` (#external)", and a refusal for the
|
|
997
|
+
provider's own documents, which it still does not excuse, names it the same way. With no sender
|
|
998
|
+
left, the refusal now also says "nor does an #external element publish it", and its route
|
|
999
|
+
reads "keep another service sending it (…) or an #external element publishing it (a publishes
|
|
1000
|
+
edge from it in the living architecture/ project; one whose element this feature's
|
|
1001
|
+
specs/<service>/ makes a service no longer counts)". This predates the one index too.
|
|
1002
|
+
- **`openapi.remove-marker-missing` is owed only while there is a living operation to retire**:
|
|
1003
|
+
not for an id the service's living contract does not define (a consumer's REMOVED requirement
|
|
1004
|
+
naming another service's operation), and not for one the delta already replaces at its method
|
|
1005
|
+
and path (no marker could be written there). A living `openapi.yaml` that does not parse proves
|
|
1006
|
+
neither, so the marker is still owed there, as before. The overwrite's "REMOVE the requirement"
|
|
1007
|
+
route can now be followed; its message says a REMOVED requirement owes no marker there.
|
|
1008
|
+
`asyncapi.remove-marker-missing` likewise skips a message the merge already takes out.
|
|
1009
|
+
- **`details[]`** (additive) on `openapi.remove-op-consumed` and `asyncapi.remove-message-consumed`:
|
|
1010
|
+
the consumers, one per line, in the order `loam diff` lists the victims of the same removal.
|
|
1011
|
+
- **Messages reworded**: the remedies name the `architecture/` project instead of
|
|
1012
|
+
`architecture/landscape.likec4`, and say a requirement is retired in the feature's
|
|
1013
|
+
`specs/<service>/` delta; `diff.op-removed`'s no-consumer sentence reads "no current edge,
|
|
1014
|
+
requirement or use-case hop names it".
|
|
1015
|
+
- **Exit code:** where a consumer is newly found, `loam archive` exits 1 (`not-coherent`) without
|
|
1016
|
+
`--approve`; `loam validate --feature`, `loam validate <FEAT>` and `loam validate --all` (for every
|
|
1017
|
+
active feature in that shape) exit 1; `loam status` counts the finding as gating; `loam show
|
|
1018
|
+
<FEAT> --json` reports `review.readyToArchive: false`; and `loam diff` exits 1
|
|
1019
|
+
(`diff.op-removed-consumed` / `diff.message-removed-consumed`, `breaking: true`) where a removal
|
|
1020
|
+
now has a victim — a removed message whose only other declarer receives it included, which exited
|
|
1021
|
+
0. Where another service's spec file does not decode, `loam validate --feature`
|
|
1022
|
+
reports `openapi.remove-op-consumed` / `asyncapi.remove-message-consumed` naming the file where it
|
|
1023
|
+
reported `feature.unreadable`, at exit 1 as before, and `loam archive` exits 1 as before. Where
|
|
1024
|
+
two directories claim a service id, or the provider's own `spec.md` does not decode (either spec
|
|
1025
|
+
file, for a message), `loam
|
|
1026
|
+
archive` and `loam validate --feature` exit 1 for the removal whatever those documents say —
|
|
1027
|
+
before, 0 unless a copy the gate could read named it — until the document reads or `--approve` is
|
|
1028
|
+
passed. Where a refusal was false or the consumer is retired in the same feature, each
|
|
1029
|
+
of those exits 0 for that shape unless something else gates. That includes a message retired from
|
|
1030
|
+
one sender while another service would still send it after the merge, or while an `#external`
|
|
1031
|
+
element still publishes it and the feature does not make that element a service, where only
|
|
1032
|
+
consumers were named: `loam archive`, `loam validate --feature`, `loam validate <FEAT>` and `loam validate
|
|
1033
|
+
--all` exited 1 and now exit 0, `loam status` no longer counts it as gating, and `loam show
|
|
1034
|
+
<FEAT> --json` no longer reports `review.readyToArchive: false` for it. `loam diff` reports
|
|
1035
|
+
`diff.op-removed` (warn) where its only victim was a same-id requirement, and
|
|
1036
|
+
`diff.message-removed` (warn) where an `#external` element still publishes the removed message —
|
|
1037
|
+
it exited 1 (`diff.message-removed-consumed`, `breaking: true`) and now exits 0 unless something
|
|
1038
|
+
else fails. `--approve` overrides
|
|
1039
|
+
every one of these refusals as before. `--strict` changes nothing here beyond that: every finding
|
|
1040
|
+
in this entry is an error, or is unchanged.
|
|
1041
|
+
|
|
1042
|
+
### Fixed — `loam instructions loam-spine` says what `loam rebase` pins for a revived quote
|
|
1043
|
+
|
|
1044
|
+
The page said `loam rebase` "pins neither shape". That has been false since rebase began grading
|
|
1045
|
+
the delta it will write. Rebase never pins a retired operation as new, but it does repin a quote
|
|
1046
|
+
of an address a move vacated over the operation that lives there now, and it reports the revival
|
|
1047
|
+
that edit makes. The page now says so, in line with the `loam-codes` command map, WORKFLOW.md
|
|
1048
|
+
and SCHEMA.md.
|
|
1049
|
+
|
|
1050
|
+
### Fixed — an operation with no `operationId` is a slot like any other
|
|
1051
|
+
|
|
1052
|
+
No code, flag or payload key was added. The OpenAPI reader used to drop an operation written
|
|
1053
|
+
without an `operationId`. It now keeps it by its method and path, never by an id: requirements,
|
|
1054
|
+
edges and removal markers still cannot name it, and it gets no finding of its own. The checks that
|
|
1055
|
+
work by slot now see it:
|
|
1056
|
+
|
|
1057
|
+
- **`openapi.op-revived` names it.** When a quote lands where such an operation lives, the message
|
|
1058
|
+
says `<METHOD /path> now holds an operation with no operationId, and archiving this delta
|
|
1059
|
+
replaces it there`. Its move-back remedy adds that, if that operation is to stay, the same delta
|
|
1060
|
+
must define it at another address. The message no longer calls that address one "the living
|
|
1061
|
+
contract no longer has". Followed as it used to read, the remedy led to an archive that deleted
|
|
1062
|
+
the operation with nothing naming it.
|
|
1063
|
+
- **`loam rebase` and `loam validate` agree about a pin over it.** Rebase always pinned against
|
|
1064
|
+
whatever stood at the slot, but validate refused that pin with `openapi.baseline-invalid` ("no
|
|
1065
|
+
operation at that slot").
|
|
1066
|
+
- **Pins are now required at these slots.** An unpinned quote over such an operation counts into
|
|
1067
|
+
`openapi.baseline-missing` (warn, gates archive), and so does an unpinned delta operation without
|
|
1068
|
+
an `operationId` over a living one. Their pins are graded like any other (`-stale`, `-invalid`).
|
|
1069
|
+
**Exit code:** `loam archive` now exits 1 (`not-coherent`) on either shape until `loam rebase`
|
|
1070
|
+
pins it, or `--approve` merges unpinned. `openapi.baseline-missing` is a warning that gates:
|
|
1071
|
+
`loam validate` exits as before on it, except `--strict` (and `--all --strict`), which fails on
|
|
1072
|
+
any warning and so now exits 1; `loam status` counts it as gating, and `loam show <FEAT> --json`
|
|
1073
|
+
reports `review.readyToArchive: false` until the pin is written.
|
|
1074
|
+
- **A pin on such an operation where nothing lives is `openapi.baseline-invalid` (error)**, as it is
|
|
1075
|
+
for a named operation (rebase never writes one; a malformed pin on such an operation is refused
|
|
1076
|
+
the same way). **Exit code:** `loam validate --feature`, `loam validate <FEAT>` and `loam validate
|
|
1077
|
+
--all` (for every active feature carrying one) now exit 1 on it, where they exited 0 with no
|
|
1078
|
+
finding; `loam archive` exits 1 (`not-coherent`) without `--approve`; `loam status` counts it as
|
|
1079
|
+
gating; `loam show <FEAT> --json` reports `review.readyToArchive: false`.
|
|
1080
|
+
- **`openapi.remove-op-consumed`** also fires when the delta defines an operation with no
|
|
1081
|
+
`operationId` where a consumed one lives. Before, a delta holding only such an operation upserted
|
|
1082
|
+
over the consumed one with no finding of any kind.
|
|
1083
|
+
|
|
1084
|
+
### Changed — `loam archive` refuses an overwrite that takes a consumed operation out of the contract
|
|
1085
|
+
|
|
1086
|
+
No code, flag or payload key was added: an existing code covers a second shape.
|
|
1087
|
+
|
|
1088
|
+
- **`openapi.remove-op-consumed` (error, `--approve` overrides) now also fires for an overwrite.**
|
|
1089
|
+
A feature's `openapi.yaml` defines another operationId at the method and path where a living
|
|
1090
|
+
operation lives, and the contract the archive would write then defines the living one nowhere.
|
|
1091
|
+
The merge replaces it there with no removal marker naming it, so before this only the advisory
|
|
1092
|
+
`api.op-unconsumed` fired, the archive passed without `--approve`, and the next `validate`
|
|
1093
|
+
reported `spine.op-undefined` / `spec-api.op-undefined`. The consumers are a removal's — the one
|
|
1094
|
+
consumer index of the entry above — including the provider's own `spec.md` requirements as the
|
|
1095
|
+
merge would leave them: one the feature MODIFIES to name the new operation, or REMOVES, no longer counts. The
|
|
1096
|
+
message names what replaces the operation and where, and says to define it at another address in
|
|
1097
|
+
the same delta if it is to stay.
|
|
1098
|
+
- **Exit code:** on this shape `loam archive` now exits 1 (`not-coherent`) without `--approve`,
|
|
1099
|
+
and `loam validate --feature`, `loam validate <FEAT>` and `loam validate --all` (for every active
|
|
1100
|
+
feature in this shape) exit 1. `loam status` counts it as gating, and `loam show <FEAT> --json`
|
|
1101
|
+
reports `review.readyToArchive: false` until it is fixed or archived past with `--approve`. With
|
|
1102
|
+
`--approve` the archive merges as before and lists the finding in `overridden[]`.
|
|
1103
|
+
- **`c4-api.op-removing`** now also fires for a new tagged edge onto an operation the delta
|
|
1104
|
+
replaces that way.
|
|
1105
|
+
- An overwrite of an operation nothing names is unchanged: `api.op-unconsumed` stays advisory. On
|
|
1106
|
+
the event axis `asyncapi.remove-message-consumed` already graded the simulated merge's sent
|
|
1107
|
+
messages, so a channel, operation or message slot redefined under another identity was caught
|
|
1108
|
+
like a retirement for its OTHER consumers — but not for the provider's own `Publishes:` line,
|
|
1109
|
+
which the consumer-index entry above adds.
|
|
1110
|
+
|
|
1111
|
+
### Fixed — `loam archive` merges into a landscape written in any form LikeC4 parses
|
|
1112
|
+
|
|
1113
|
+
No code, flag or `--json` key was added or changed. Each case below is a landscape (or a delta)
|
|
1114
|
+
that `loam validate --all` called valid and that `loam archive` refused with `merge-failed`, nothing
|
|
1115
|
+
written — or, where the case says "exit 0", merged wrong and wrote; each now merges as LikeC4
|
|
1116
|
+
reads it.
|
|
1117
|
+
|
|
1118
|
+
- **A landscape with more than one top-level `model { }` block.** The splice map read only the
|
|
1119
|
+
first block, so every declaration in a later one was invisible and archive refused with
|
|
1120
|
+
"declares '<id>' in a form placement cannot locate — most often two declarations sharing one
|
|
1121
|
+
line" about a declaration on a line of its own. Every block is read now. An addition anchored to
|
|
1122
|
+
a living element — nested in it, joining its service's neighborhood, or joining its source's
|
|
1123
|
+
edges — lands in that element's block; an unanchored one lands in the first block's trailing
|
|
1124
|
+
region, where it landed before. A one-block map is placed byte for byte as before, and archive
|
|
1125
|
+
order still does not change the bytes. The same reading fixes three neighbours: a delta written
|
|
1126
|
+
in two `model { }` blocks (its second-block additions were "cannot locate … in delta.likec4"), a
|
|
1127
|
+
service `model.likec4` whose `extend <fqn> { }` frame sits in a later block (the merge minted a
|
|
1128
|
+
second frame in the first block, or re-added a container the model already declares there), and
|
|
1129
|
+
`loam validate --all --base`, which did not grade a call edge the branch edited in a later block.
|
|
1130
|
+
- **A declaration split across lines.** LikeC4's newlines are hidden tokens, so `name =` with its
|
|
1131
|
+
kind on the next line, a bare name above `= kind`, a kind above the name it declares, a title on
|
|
1132
|
+
the next line, a `/* … */` comment spanning a line break, an edge broken around its arrow, and an
|
|
1133
|
+
edge's `#tag` on the next line are all one statement — and the splice map, reading one line per
|
|
1134
|
+
statement, could not see them. Worse, `name = kind 'title'` with its `{` on the NEXT line read as
|
|
1135
|
+
a bodyless element: an addition anchored after it landed between the head and its body and took
|
|
1136
|
+
that body — description, children, `metadata { service }` binding — for its own, in a merge that
|
|
1137
|
+
parsed and was written (exit 0). The body stays with its element now.
|
|
1138
|
+
- **An `extend <fqn> { }` frame written in the landscape itself, with an edge inside it.** An
|
|
1139
|
+
edge the feature added, anchored to the frame's edge, was spliced INSIDE the frame — a scope, so
|
|
1140
|
+
its names re-resolved there: the delta's `zulu -> beta` landed in `extend alpha { beta = … }` as
|
|
1141
|
+
an edge into alpha's own `beta` container, in a merge that parsed and was written (exit 0). It
|
|
1142
|
+
lands outside the frame now. Elements inside such a frame were already read correctly.
|
|
1143
|
+
- **A hyphenated id (`entity-service = …`) and a dot-kind edge (`a .async b`).** LikeC4's
|
|
1144
|
+
identifiers may carry hyphens and a declared relationship kind may be written as `.kind`; the
|
|
1145
|
+
splice map read neither, so such an element was "in a form placement cannot locate" and such an
|
|
1146
|
+
edge made the relationship count disagree.
|
|
1147
|
+
- **A `link` (or `icon`) URI followed by more code on its line.** LikeC4 reads `https://…` and a
|
|
1148
|
+
relative `./…` path as one token; the splice map read its `//` as a comment that ran to the end
|
|
1149
|
+
of the line, so `x = kind 'X' { link https://… }` lost its closing brace and archive refused with
|
|
1150
|
+
`landscape.likec4 has no model block`. The URI is a token wherever it sits: a `link` or `icon`
|
|
1151
|
+
alone on its line ends there, and the child or edge declared on the line below is its own.
|
|
1152
|
+
- **A markdown string (`'''…'''` or `"""…"""`) whose prose holds an apostrophe or an odd number
|
|
1153
|
+
of quotes.** The splice map read the triple as three ordinary quotes, so the first `it's` in a
|
|
1154
|
+
long `description` ended the string and the rest of the file was read inside out — a map with
|
|
1155
|
+
one such description above its model had no model block at all, and archive refused with
|
|
1156
|
+
`landscape.likec4 has no model block`.
|
|
1157
|
+
- **A delta edge whose body holds a key or tag containing `op`, `publishes` or `consumes`.** The
|
|
1158
|
+
splice map read those keys anywhere in the edge's body, and took a hyphen for the end of a word,
|
|
1159
|
+
so `op-version 'v2'` above `op 'cancelOrder'` read as op `v2`, and a `#op-critical` or `#op` tag
|
|
1160
|
+
as an op naming the description below it. The edge then matched none of the delta's statements
|
|
1161
|
+
and archive refused with "cannot locate the '<a> -> <b>' relationship in delta.likec4". A key is
|
|
1162
|
+
read whole now, and `op`, `publishes` and `consumes` only from `metadata { }`, which is where
|
|
1163
|
+
LikeC4 reads them. The same misreading hid such an edge from `validate --all --base` (below).
|
|
1164
|
+
- **A bare carriage return (`\r` with no `\n`) between two statements.** LikeC4 breaks a line at
|
|
1165
|
+
`\r` as well as `\n`, and the splice map broke only at `\n`. A statement therefore ran on over
|
|
1166
|
+
a bare `\r` into the next one, and a `// comment` ran on over it and hid the next declaration.
|
|
1167
|
+
Two elements, two edges or a property and a child on either side of a bare `\r` were refused as
|
|
1168
|
+
"in a form placement cannot locate". A bare `\r` ends the statement and the comment now. A CRLF
|
|
1169
|
+
map reads exactly as before.
|
|
1170
|
+
- **An addition anchored to a declaration whose line goes on with more code.** Placement put an
|
|
1171
|
+
addition on the line after its anchor. When the next declaration opened a multi-line body on the
|
|
1172
|
+
anchor's own line (`alpha = … { … } beta = … {`), that next line was inside beta. An element
|
|
1173
|
+
bound to alpha's service was then written inside beta as its child, in a merge that parsed and
|
|
1174
|
+
was written (exit 0). A block comment opened on the anchor's line and closed on a later one
|
|
1175
|
+
swallowed the addition the same way. The addition now lands right after its anchor whenever
|
|
1176
|
+
code, or a block comment the line does not close, follows the anchor on its line. Two archives
|
|
1177
|
+
anchored to the two declarations on such a line also write the same bytes in either order now;
|
|
1178
|
+
before, whichever ran second landed above the other. A comment trailing the anchor changes
|
|
1179
|
+
nothing: the addition lands on the next line, as before — unless a bare `\r` ends that `// comment`
|
|
1180
|
+
and the next declaration follows it on the same line, which counts as code following the anchor.
|
|
1181
|
+
- **A bodyless declaration with a comment trailing it on its line.** A parent such as
|
|
1182
|
+
`api = container 'API' // the api` that gained its first child got its `{` inside the comment,
|
|
1183
|
+
and archive refused: the merged landscape "would not parse". The body now opens before the
|
|
1184
|
+
comment, which then trails the new closing brace. A block comment that closes on a LATER line
|
|
1185
|
+
was worse: the splice map ended the declaration inside it, so a child it gained, or an element
|
|
1186
|
+
or edge anchored after it, was written inside the comment, in a merge that parsed and was
|
|
1187
|
+
written (exit 0). The declaration now ends before such a comment. A comment that closes on the
|
|
1188
|
+
declaration's own line keeps its place, and the bytes come out as before.
|
|
1189
|
+
- **A delta whose feature tag sits in a tag list with a separator.** LikeC4 reads a tag list as
|
|
1190
|
+
`#a #b, #c;`: one or more tags, then comma groups, then an optional `;`. Stripping the feature's
|
|
1191
|
+
own tag left `#FEAT-1, #x` as `, #x` and `#FEAT-1;` as a bare `;`, and archive refused because
|
|
1192
|
+
the merged landscape "would not parse". The tag now takes the comma after it with it (or else
|
|
1193
|
+
the comma before it), and takes a `;` when no tag is left in front of it. A tag after a comma no
|
|
1194
|
+
longer leaves that comma behind either: `#x, #FEAT-1` lands as `#x`, where it used to land as
|
|
1195
|
+
`#x,`.
|
|
1196
|
+
|
|
1197
|
+
What the splice map still cannot locate is still refused (`merge-failed`, nothing written), and
|
|
1198
|
+
the message no longer guesses. It used to say "most often two declarations sharing one line"
|
|
1199
|
+
whatever the map held — over the reported declaration, which sat on a line of its own. It names the
|
|
1200
|
+
declaration (a relationship by its endpoints, where it used to give only a count) and the line
|
|
1201
|
+
LikeC4 parsed it on, then the cause the text shows: "it shares that line with '<other>'" only when
|
|
1202
|
+
other code precedes it on that line (its own kind keyword, or an edge's own source, is not other
|
|
1203
|
+
code); "the splice map read it as part of '<other>', which starts at line
|
|
1204
|
+
<n>" when the scan ran a statement on past a line break LikeC4 makes (a bare carriage return was
|
|
1205
|
+
the one such break until the splice map learnt to break there too, above); and otherwise that the
|
|
1206
|
+
splice map read nothing there, with the rewrite to make and a request to report the shape. Wording
|
|
1207
|
+
only; the code is unchanged.
|
|
1208
|
+
|
|
1209
|
+
### Changed — `loam archive` refuses a delta that would bring back an operation another feature retired
|
|
1210
|
+
|
|
1211
|
+
Two codes, both added to the `IssueCode` union, and one additive `--json` key were added.
|
|
1212
|
+
|
|
1213
|
+
- **New `openapi.op-revived` (warn, gates archive).** A feature's `openapi.yaml` spells an
|
|
1214
|
+
operation (by operationId) that its service's living contract no longer holds, and an ARCHIVED
|
|
1215
|
+
feature retired it with an `x-loam-remove: true` marker — any archived feature, including one
|
|
1216
|
+
whose directory carries the same feature id as the one being graded. Before this, the
|
|
1217
|
+
quote read as an addition: a delta that had only quoted `createOrderV1` archived after the
|
|
1218
|
+
feature retiring it and put it back into the living contract at exit 0, with the advisory
|
|
1219
|
+
`api.op-unconsumed` the only finding. The message names the operation, the retiring feature(s)
|
|
1220
|
+
and both remedies — delete the quote if it only restated the operation; archive with `--approve`
|
|
1221
|
+
if the feature brings it back on purpose.
|
|
1222
|
+
- **New `asyncapi.slot-revived` (warn, gates archive)** — the same on the event axis: a `channels`
|
|
1223
|
+
or `operations` slot by its key, or a message by its name, that an archived feature retired. A
|
|
1224
|
+
channel-slot removal's inline messages are named from that feature's `.loam-before/` snapshot
|
|
1225
|
+
pre-image, read only while it still hashes to the digest its manifest recorded.
|
|
1226
|
+
- **Both codes also cover an address an archived feature VACATED.** An archived feature that
|
|
1227
|
+
relocated an operation (a marker on the old slot, the same operationId defined at a new one)
|
|
1228
|
+
vacated the old address — and so did one that retired it there, once a later feature re-added it
|
|
1229
|
+
somewhere else. A later delta quoting the operation at that address — unpinned — archived at
|
|
1230
|
+
exit 0 and put the id back beside where it lives, one operationId at two slots (only the
|
|
1231
|
+
advisory `openapi.duplicate-operationid` said so afterwards): the slot-keyed baseline gate saw
|
|
1232
|
+
nothing living at the slot, and the id-keyed check saw the id living. That quote is now
|
|
1233
|
+
`openapi.op-revived` too. Both codes are judged on the contract the archive would write: the
|
|
1234
|
+
check runs the archive's own merge of the delta over the living contract and compares where each
|
|
1235
|
+
operationId (each message name) stands there with where it stands now. One the merged contract
|
|
1236
|
+
answers at two or more addresses — one of them new, and vacated by an archived feature — is the
|
|
1237
|
+
finding; one the living contract still holds that it answers at exactly one address is not,
|
|
1238
|
+
whether the delta moves it back with its own removal marker where it lives now or swaps another
|
|
1239
|
+
operation in there (with or without an operationId — the merge replaces what stands at that
|
|
1240
|
+
method and path). A removal marker standing where the id does not live deletes nothing, so it
|
|
1241
|
+
no longer excuses the quote. The message says which archived feature moved the operation and
|
|
1242
|
+
where to, or retired it there, and — apart from
|
|
1243
|
+
that, since anything landed after can have moved it again — where it lives now; its remedy names
|
|
1244
|
+
the address to drop: delete the quote, or, to move it back on purpose, add a removal marker
|
|
1245
|
+
where it lives now — and where the delta already spells that address, so no marker can be
|
|
1246
|
+
written there, archive with `--approve` instead. Where another operation lives at the quoted
|
|
1247
|
+
address now (on the event axis, another message is declared at the quoted key), the message
|
|
1248
|
+
names it and says archiving replaces it there — the merge writes one entry per address —
|
|
1249
|
+
instead of calling the address one the living contract no longer has, and the move-back remedy
|
|
1250
|
+
says to define it at another address (key) in the same delta if it is to stay: followed as it
|
|
1251
|
+
read before, the remedy reached a clean gate and an archive that deleted the other operation
|
|
1252
|
+
with nothing naming it. The retired-shape message names such an entry too. A quote of an
|
|
1253
|
+
address an archived feature moved the operation away from is the finding even where the living
|
|
1254
|
+
contract holds the operation nowhere (taken out of its new address by a hand edit, which leaves
|
|
1255
|
+
no marker): the move is on the record, and the message reads as a retirement brought back,
|
|
1256
|
+
naming the move. On the event axis the vacated addresses are `components.messages` keys (a
|
|
1257
|
+
marker there) and the inline declarations a channel removal took; a delta that restates the
|
|
1258
|
+
channel a name stands inline in with a `$ref`, or retires it, takes the name out of there and
|
|
1259
|
+
is not graded. For an inline declaration the remedy names the channel (restate it with a
|
|
1260
|
+
`$ref`, or retire it), never a marker nested on the inline message, which retires nothing. For
|
|
1261
|
+
a `components.messages` key the remedy also names every channel (or operation) of the merged
|
|
1262
|
+
contract still `$ref`-ing that key, to restate with the `$ref` pointed at the quoted key: told
|
|
1263
|
+
only to retire the key, an author left the channel pointing at nothing, and its send stopped
|
|
1264
|
+
sending the message — `asyncapi.remove-message-consumed`, which gates. `asyncapi.baseline-invalid`'s
|
|
1265
|
+
move-back remedy says the same. A channel or operation slot is its key, so a "moved" one is a
|
|
1266
|
+
retirement, as before. A message inline under a channel the delta brings back is reported by
|
|
1267
|
+
the channel's finding alone only while the merged contract declares the name nowhere else; where
|
|
1268
|
+
an archived feature re-declared it under `components` as it retired the channel, the inline
|
|
1269
|
+
declaration is a finding of its own (one message name in two places) — it was skipped, and
|
|
1270
|
+
`--approve` archived `asyncapi.duplicate-message`.
|
|
1271
|
+
- **Exit code: `loam archive` now exits 1 (`not-coherent`) on this shape** without `--approve`;
|
|
1272
|
+
with `--approve` it merges as before, and the operation lands in `overridden[]` and the text
|
|
1273
|
+
output's "archiving despite" list, naming what came back and whose retirement it reverses.
|
|
1274
|
+
`loam validate --feature` and `--all` report both codes as warnings: exit unchanged, except
|
|
1275
|
+
under `--strict`, which fails on any warning and so now exits 1 on this shape. `loam status`
|
|
1276
|
+
counts them as gating like every warning that gates, and `loam show <FEAT> --json` reports
|
|
1277
|
+
`review.readyToArchive: false` for such a feature until the quote is deleted (or the feature is
|
|
1278
|
+
archived past it with `--approve`).
|
|
1279
|
+
- **`loam rebase` no longer calls a retired operation one "this feature is adding".** It pins
|
|
1280
|
+
nothing for it either way, and now prints the same finding after its pin lines; the additive
|
|
1281
|
+
`--json` key `revived[]` carries the findings (`severity`, `code`, `gates`, `subject`,
|
|
1282
|
+
`message`). The operation's `pins[]` row still reads `unresolved`. The check runs over the
|
|
1283
|
+
delta as rebase will write it, pins included, under `--dry-run` too: a quote pinned to its
|
|
1284
|
+
own content is skipped by the merge, and repinned over another operation that now lives at an
|
|
1285
|
+
address an archived feature moved it away from it becomes an edit that would put it back
|
|
1286
|
+
there — that finding is in `revived[]` as well, and its `pins[]` row reads `repinned`. The
|
|
1287
|
+
text output's revival banner no longer says "nothing was pinned" for such items.
|
|
1288
|
+
- **`openapi.baseline-invalid` and `asyncapi.baseline-invalid` reworded** for a pin whose living
|
|
1289
|
+
operation or slot is gone: they used to say "drop the marker", which turned the quote into the
|
|
1290
|
+
revival above. They now say to re-read the living contract and delete the quote unless the
|
|
1291
|
+
feature deliberately brings it back. When the living contract still holds the operationId (or,
|
|
1292
|
+
for a `components.messages` key, the message name) elsewhere, the message instead says a
|
|
1293
|
+
parallel feature MOVED it there (or retired it here, and another re-added it there) and where it
|
|
1294
|
+
lives now, and to make any change there — never to archive past the revival finding, which would
|
|
1295
|
+
answer it at both addresses. The `/loam-check` rows and `loam explain` say the same. `loam
|
|
1296
|
+
rebase`'s text says such items were retired by an archived feature "or moved to another address
|
|
1297
|
+
by one".
|
|
1298
|
+
- **What it reads, and what it cannot see.** Only files under `features/archive/` — the archived
|
|
1299
|
+
deltas' removal markers and each feature's `.loam-before/` snapshot; no git. A marker counts only
|
|
1300
|
+
if the merge carried it out: the snapshot's pre-image must hold the marker's operationId at its
|
|
1301
|
+
method and path (an event marker: its channel or operation key, or its message name at its
|
|
1302
|
+
`components.messages` key), and a contract the archive created or never wrote lost nothing. A
|
|
1303
|
+
marker archived past `openapi.remove-target-missing` / `-mismatch` (or the `asyncapi.*` twins)
|
|
1304
|
+
under `--approve` deleted nothing, so a genuinely new operation it happened to name is not a
|
|
1305
|
+
revival. A feature with no intact snapshot cannot be checked, and its markers count as written.
|
|
1306
|
+
A removal made by hand-editing a living contract, or an archived feature deleted from disk,
|
|
1307
|
+
leaves nothing to read, and a quote of such an operation still grades as an addition. A marker
|
|
1308
|
+
with no `operationId` or one written at path level retired nothing when its feature archived, so
|
|
1309
|
+
it is never named as the retirer.
|
|
1310
|
+
|
|
1311
|
+
### Added — `#suite-<name>` tags and `loam list suites`: which services a test suite needs up
|
|
1312
|
+
|
|
1313
|
+
No code was added. One reserved tag prefix, one list section and one positional argument were.
|
|
1314
|
+
|
|
1315
|
+
- **A `dynamic view` may carry `#suite-<name>` tags** to say which test suites the journey runs in —
|
|
1316
|
+
a separate prefix from `#cap-`, many-to-many (one view may carry several), each declared as
|
|
1317
|
+
`tag suite-<name>` in the landscape's `specification` block like every LikeC4 tag, so a mistyped
|
|
1318
|
+
suite is a parse error.
|
|
1319
|
+
- **A view carrying only `#suite-` tags is now a use case.** The one opt-in predicate takes three
|
|
1320
|
+
prefixes (`cap-`, `req-`, `suite-`), and so does the byte scan that lets a fleet with no use cases
|
|
1321
|
+
skip the LikeC4 load. Such a view's hops are graded by `loam validate` like any other use case's
|
|
1322
|
+
(`usecase.step-unbacked`, `usecase.step-contested`, `usecase.step-unlinked` — at fleet scope, and
|
|
1323
|
+
per service for a flow beside a service's model), and every reader that names use cases sees it:
|
|
1324
|
+
`loam diff`'s removed-operation victims, `loam delta` and `loam status`'s use-case blast radius,
|
|
1325
|
+
and the context packs. A view that already carried `#cap-` or `#req-` is unaffected, and an
|
|
1326
|
+
untagged view is still never graded. On a fleet that had drawn `#suite-`-tagged views before this
|
|
1327
|
+
release, those views can now earn findings.
|
|
1328
|
+
- **New section `loam list suites [<name>] [--json]`** — the answer to "which services must be up
|
|
1329
|
+
for suite X". `[section]` accepts `suites`, and a new optional positional `[name]` narrows the
|
|
1330
|
+
listing to one suite. Additive `--json` keys, emitted only with this section: `suites[]` (`name`,
|
|
1331
|
+
`views[]` of `{id, file}`, `services[]` — enumerated service ids, sorted — and `unresolved[]` of
|
|
1332
|
+
`{element, external}`: the participants that resolve to no service, `external` true when the
|
|
1333
|
+
element or anything it is drawn inside carries `#external`, the walk stopping at the first
|
|
1334
|
+
element bound to or titled for a service as `loam gate`'s partner reading does), `ungrouped[]` (`{id, file}` of every
|
|
1335
|
+
use case carrying no `#suite-` tag, never narrowed by `[name]`), and `useCases` (`state`:
|
|
1336
|
+
`absent` | `ok` | `unreadable`, beside `unreadable` and, when unreadable, `error`, spelled as
|
|
1337
|
+
`loam list capabilities` spells them). Over an `architecture/` that does not parse the section
|
|
1338
|
+
degrades instead of refusing, and `suites` / `ungrouped` are absent rather than `[]`. Only the
|
|
1339
|
+
fleet map's use cases are listed. The text view prints one block per suite — its services, the
|
|
1340
|
+
unresolved participants marked `!`, its use cases — then the use cases in no suite.
|
|
1341
|
+
- **Refusals use existing codes.** An unknown suite name is `unknown-target` with close names; a
|
|
1342
|
+
`[name]` after any other section, and `--needs-work` with `suites`, are `invalid-option` (a name
|
|
1343
|
+
after another section used to be commander's "too many arguments", also `invalid-option` under
|
|
1344
|
+
`--json`). The campaign flags (`--subsystem`, `--owners`) refuse `suites` as they refuse every
|
|
1345
|
+
section but `services`.
|
|
1346
|
+
- The MCP tool `loam_list` gains the matching optional `name` argument, and its `section`
|
|
1347
|
+
description now lists `glossary` and `suites`. `name` without `section` is refused as invalid
|
|
1348
|
+
params (`-32602`) rather than read as a section.
|
|
1349
|
+
- `examples/docs` tags its one use case `#suite-smoke` and `#suite-notifications`, so
|
|
1350
|
+
`loam list suites` there answers checkout-web, notification-service and order-service, with
|
|
1351
|
+
`customer` and the `#external` topic `kafka.orderEvents` named as unresolved.
|
|
1352
|
+
|
|
1353
|
+
### Changed — the use-case byte scan opens on a `#` tag reference, not on a bare prefix
|
|
1354
|
+
|
|
1355
|
+
No code, flag or payload key was added, changed or retired.
|
|
1356
|
+
|
|
1357
|
+
- **loam starts LikeC4 for use cases only when a document references a reserved tag.** The scan in
|
|
1358
|
+
front of every reader of the fleet's use cases, and of a service's own flows, used to open on the
|
|
1359
|
+
bare text `cap-`, `req-` or `suite-` anywhere in a file. It now opens on `#cap-`, `#req-` or
|
|
1360
|
+
`#suite-`, with only whitespace, line breaks or comments between the `#` and the name, as LikeC4
|
|
1361
|
+
allows. That is the only way a view carries a tag, so no use case is missed.
|
|
1362
|
+
- **A broken `.likec4` beside a service's model that names a prefix only in prose no longer fails
|
|
1363
|
+
the service.** In `0.2.0-alpha.8`, a file with no tagged view that mentioned `cap-` or `req-`
|
|
1364
|
+
anywhere — a title, a comment — was loaded, and if it did not parse, `validate --service` (and
|
|
1365
|
+
`--all`, per service) reported `usecase.flow-invalid`, an error, exit 1. This release's `suite-`
|
|
1366
|
+
prefix would have added a third such substring (an id like `test-suite-runner`). Now the file is
|
|
1367
|
+
skipped, like any untagged diagram.
|
|
1368
|
+
- **The same holds at fleet scope.** Suppose `architecture/` does not parse, carries no tagged view,
|
|
1369
|
+
and only declares `tag cap-…` / `tag req-…` or names a prefix in prose. It now reads as declaring
|
|
1370
|
+
no use case. `useCases.unreadable` is then `false` where it used to be `true` in
|
|
1371
|
+
`loam status --json`, `loam delta --json` and `loam list capabilities --json`, and so is
|
|
1372
|
+
`useCaseScan.unreadable` in `loam context --json`. `validate --all` still reports that map as
|
|
1373
|
+
`landscape.invalid`.
|
|
1374
|
+
- A `#` followed by a reserved prefix still opens the scan wherever it is written, a comment
|
|
1375
|
+
included.
|
|
1376
|
+
|
|
1377
|
+
### Changed — `loam-feature` asks whether a feature is owed, and analyzes before it pins
|
|
1378
|
+
|
|
1379
|
+
No code, flag or payload key was added, changed or retired. What changes is the protocol text an
|
|
1380
|
+
agent is handed, the `intent.md` a new feature starts from (and which of its text `intent.empty`
|
|
1381
|
+
counts), and what the documentation says a feature's contract delta holds.
|
|
1382
|
+
|
|
1383
|
+
- **`loam instructions loam-feature` opens with a step 0.** A change that touches no requirement,
|
|
1384
|
+
operation or fleet edge — a refactor, a dependency bump, a fix back to behaviour the living spec
|
|
1385
|
+
already documents — needs no feature: a code PR, then `loam validate --service <id>` and
|
|
1386
|
+
`loam vouch --pack --service <id>` in the service repo, and a person's vouch. WORKFLOW.md's "The
|
|
1387
|
+
honestly-small change" now opens with the same lane.
|
|
1388
|
+
- **A new, ungraded step 4b, "Analyze before you pin", sits between authoring and `loam rebase`.**
|
|
1389
|
+
The agent reads `loam show <FEAT> --json`, `loam context <service> --feature <FEAT> --json` and the
|
|
1390
|
+
`explore` neighbours, then writes questions under `## Open questions` in `intent.md`: ambiguous
|
|
1391
|
+
SHALLs, values, limits and windows two services state differently (and which side refuses),
|
|
1392
|
+
`Realizes:` lines that do not deliver their capability, each edge's timeout, retry, partial
|
|
1393
|
+
failure and compensation, and neighbours left out of `--touches`. A person answers by editing the
|
|
1394
|
+
file. `loam-implement` sends an ambiguity it meets to the same section.
|
|
1395
|
+
- **`loam new` scaffolds an optional `## Open questions` heading in `intent.md`**, over a comment
|
|
1396
|
+
only. **`intent.empty` now reads past that section:** questions an agent writes there are not the
|
|
1397
|
+
feature's Why, so an intent whose only prose is open questions still gates the archive. The generated `loam-feature` command and skill pointers
|
|
1398
|
+
list both new steps in their spine; an unedited pointer refreshes on the next `loam init`.
|
|
1399
|
+
- **A feature's `openapi.yaml` / `asyncapi.yaml` is documented as holding only what the feature
|
|
1400
|
+
adds, changes or removes.** SCHEMA.md, WORKFLOW.md, the `loam-spine` and `loam-feature` pages,
|
|
1401
|
+
the `spec.md` scaffold's comment and the example deltas taught "a complete document, not a
|
|
1402
|
+
patch". The merge never needed one: it walks only the paths, methods and slots a delta spells, a
|
|
1403
|
+
local `$ref` resolves into the living contract, and an add-only delta needs no `loam rebase`.
|
|
1404
|
+
Each needless quote is one more pin and, once a parallel feature removes the quoted operation,
|
|
1405
|
+
either a blocking `openapi.baseline-invalid` (pinned) or an archive that puts the retired operation
|
|
1406
|
+
back (unpinned); `loam-check`'s fix for the first now says to delete the quote. The
|
|
1407
|
+
`loam-spine` page also no longer says path-level keys and components go unpinned —
|
|
1408
|
+
`x-loam-baselines` covers them.
|
|
1409
|
+
- **WORKFLOW.md gains "Build on the envelope":** a tracker export from `loam tasks <FEAT> --json`
|
|
1410
|
+
keyed on unit ids (and what rewording re-keys one), a rollup of the per-service CI summaries
|
|
1411
|
+
that reads a missing or old report as unknown, and `npx likec4 mcp` beside `loam mcp` on the same
|
|
1412
|
+
docs repo.
|
|
1413
|
+
- **The `loam-codes` page points at AGENTS.md's "Reference pages — fetch when needed" section** —
|
|
1414
|
+
it named a "The reference pages" section that does not exist — and lists `loam-report` among the
|
|
1415
|
+
pages `loam instructions` prints.
|
|
1416
|
+
|
|
1417
|
+
### Added — `loam new --modifies <Req-ID>`: a small change is new → edit → archive
|
|
1418
|
+
|
|
1419
|
+
- **New repeatable flag `--modifies <Req-ID>` on `loam new`.** Each value names a living
|
|
1420
|
+
requirement, by its `Requirement-ID:`, in a service the feature `--touches`. The scaffold copies
|
|
1421
|
+
that requirement into the service's delta, uncommented, under `## MODIFIED Requirements`, with
|
|
1422
|
+
its `Based-On:` pin already set. It is the same pin `loam rebase` writes, so a rebase right after
|
|
1423
|
+
reports it `unchanged`, and the living text moving before archive is still `delta.baseline-stale`.
|
|
1424
|
+
An `arch.spec.md` requirement lands in a scaffolded `specs/<svc>/arch.spec.md`. Before this, the
|
|
1425
|
+
author copied the requirement by hand and ran `loam rebase` to pin it.
|
|
1426
|
+
- **When every touched service has a `--modifies` and no `--new-service` or `--capability` is
|
|
1427
|
+
given, no `delta.likec4` is written.** A feature that only rewrites requirements changes no
|
|
1428
|
+
architecture, and the scaffold already told authors to delete the file in that case. `created`
|
|
1429
|
+
in `--json` simply omits it. Any other combination scaffolds `delta.likec4` as before.
|
|
1430
|
+
- **Refusals use existing codes.** An id that no touched service's living spec carries is
|
|
1431
|
+
`unknown-target`. The message names the close ids in those specs, or the untouched service that
|
|
1432
|
+
does carry the id (`add --touches <svc>`). A value that is not a Requirement-ID, or an id two
|
|
1433
|
+
touched documents both carry, is `invalid-option`. A living spec loam cannot decode is
|
|
1434
|
+
`merge-failed`, as `loam rebase` refuses it. Every refusal writes nothing.
|
|
1435
|
+
- The opt-in MCP author tool `loam_new` gains the matching `modifies` argument. No code or payload
|
|
1436
|
+
key was added.
|
|
1437
|
+
|
|
1438
|
+
### Added — `loam_adopt`: the adopt brief on the MCP server
|
|
1439
|
+
|
|
1440
|
+
- **`loam mcp` serves a sixteenth read tool, `loam_adopt`**, mirroring `loam adopt [--service <id>]
|
|
1441
|
+
[--subsystem <name>] [--targets] --json` (JSON arguments `service`, `subsystem`, `targets`). Its
|
|
1442
|
+
result is the CLI's `adopt` envelope, verbatim. It is advertised `readOnlyHint: true` like every
|
|
1443
|
+
base tool, because `adopt` writes nothing: it hands the agent the brief, and the agent writes the
|
|
1444
|
+
baseline. Until now an MCP-only host could fetch the loam-adopt protocol through
|
|
1445
|
+
`loam_instructions` and could not run its first step, `loam adopt --service <id> --json`. No
|
|
1446
|
+
code, flag or payload key was added to the CLI; `tools/list` returns one more tool.
|
|
1447
|
+
|
|
1448
|
+
### Changed — `loam init --create` with no service prints the docs repo's first hour
|
|
1449
|
+
|
|
1450
|
+
- **A run that creates a docs repo and binds no service now ends with `next — the docs repo's first
|
|
1451
|
+
hour:`**: `loam seed --from fleet.yaml` (or draw the map by hand), `loam init --docs <docs-repo>
|
|
1452
|
+
--service <id> --agent-profile service` in each service's own repo, `loam adopt --service <id>
|
|
1453
|
+
--json` there, and `loam status`. It used to print nothing after the scaffold list. These are
|
|
1454
|
+
WORKFLOW.md's Day zero steps and the rungs `loam status` already names on an empty fleet. A run
|
|
1455
|
+
that binds a service still prints the single-repo trial's hour, and a join still prints neither.
|
|
1456
|
+
Human output only: no code, flag or payload key was added, and the `--json` envelope is unchanged
|
|
1457
|
+
for both variants.
|
|
1458
|
+
|
|
1459
|
+
### Changed — every service bind loam prints names `--agent-profile service`
|
|
1460
|
+
|
|
1461
|
+
No code, flag or payload key was added, changed or retired. What changes is printed text and one
|
|
1462
|
+
`--json` value.
|
|
1463
|
+
|
|
1464
|
+
- **The bind command now reads `loam init --docs <path> --service <id> --agent-profile service`**
|
|
1465
|
+
in `loam status`'s `next.bind-service` step, its `loam explain` entry, the docs repo's first hour,
|
|
1466
|
+
the generated AGENTS.md (`## The cycle`, step 0), and the `loam-adopt` and `loam-implement`
|
|
1467
|
+
protocols. README, QUICKSTART and docs/SERVICE-TEAMS.md already told a fleet to bind each service
|
|
1468
|
+
checkout this way, because `--agent-profile` defaults to `full`, and `full` writes `loam-feature`
|
|
1469
|
+
and `loam-ship` into the service repo. But every bind command loam printed left the flag out, and
|
|
1470
|
+
an agent runs the command loam prints. WORKFLOW.md's Day zero step 3 now spells it too.
|
|
1471
|
+
- **`next.bind-service`'s `command` in `status --json` `next[]` carries the new text.** Its
|
|
1472
|
+
`execution` block still names the same two placeholders.
|
|
1473
|
+
- **Two binds keep their old form on purpose.**
|
|
1474
|
+
- `loam-adopt` names the single-repo trial (`--create`, used only when the docs repo does not
|
|
1475
|
+
exist yet) as the one bind that keeps the full profile.
|
|
1476
|
+
- `doctor.service-unbound`'s fix is a re-run in a checkout that is already wired. An omitted
|
|
1477
|
+
`--agent-profile` there keeps the profile that checkout recorded.
|
|
1478
|
+
|
|
1479
|
+
### Changed — the OpenSpec compatibility pin moves to v1.14.1, and a weekly canary watches the newest release
|
|
1480
|
+
|
|
1481
|
+
No code, CLI flag or payload key was added, changed or retired. The `audit-openspec` /
|
|
1482
|
+
`migrate-openspec` payload shape is unchanged, but the value it reports as the certified release moves.
|
|
1483
|
+
|
|
1484
|
+
- **`audit-openspec` and `migrate-openspec` now report OpenSpec v1.14.1 as the certified release.**
|
|
1485
|
+
The banner line and `baselines.release` under `--json` carry `1.14.1` / `v1.14.1` /
|
|
1486
|
+
`87c3595ace6a2e22957f39ebe5b74c2f2316e0cb` (released 2026-10-05) instead of `1.9.0` / `2826b88`.
|
|
1487
|
+
`baselines.mainCanary` is unchanged. The corpus gate's `--baseline release` is now the v1.14.1
|
|
1488
|
+
corpus: 223 Markdown files, 765 requirements and 2475 scenarios, every file round-tripped. v1.9.0
|
|
1489
|
+
stays as the new `--baseline previous` regression pin beside `legacy` (v1.7.0) and `canary`, and
|
|
1490
|
+
the scheduled `ci.yml` matrix runs all four. The sweep found no parser gap. The grammar OpenSpec
|
|
1491
|
+
1.13 added — a closing `#` run, `*`/`+` bullets on `REMOVED` and `RENAMED`, repeated delta
|
|
1492
|
+
sections — was already read by the migration, and a new test pins the repeated sections.
|
|
1493
|
+
`audit-openspec` and `migrate-openspec --apply`, run on OpenSpec's own v1.14.1 workspace, staged
|
|
1494
|
+
all 251 living requirements and 747 scenarios across 36 services. The seven vendored fixtures stay
|
|
1495
|
+
at v1.9.0, since v1.14.1 has no shape they lack. MIGRATING-from-OpenSpec.md's version matrix
|
|
1496
|
+
records all of this.
|
|
1497
|
+
- **The corpus sweep reads each file through the migration's own reader.** That reader is the one
|
|
1498
|
+
`audit-openspec` and `--apply` use: loam's parser plus the grammar only OpenSpec accepts. The
|
|
1499
|
+
sweep used to read with loam's parser alone. Every pinned corpus gives the same totals either way.
|
|
1500
|
+
- **A new scheduled workflow, `.github/workflows/openspec-canary.yml`, watches the newest OpenSpec
|
|
1501
|
+
release.** It runs weekly and on manual dispatch. It resolves the newest `vX.Y.Z` tag with
|
|
1502
|
+
`git ls-remote` and sweeps it with the new `--baseline latest` mode of `npm run
|
|
1503
|
+
test:openspec-corpus`. That mode reports how the totals moved from the certified pin instead of
|
|
1504
|
+
refusing an unpinned commit, and still fails on a file that does not round-trip. The workflow also
|
|
1505
|
+
audits OpenSpec's own workspace and diffs upstream's parser sources against the pin, in its step
|
|
1506
|
+
summary. It has no push or pull-request trigger and blocks nothing; CONTRIBUTING.md's "The
|
|
1507
|
+
OpenSpec canary" is the runbook. The README's note that `--baseline` defaults to `canary` was
|
|
1508
|
+
wrong — it defaults to `release` — and now says so.
|
|
1509
|
+
|
|
1510
|
+
### Added — documentation: running loam where service work runs, a CLI-only first ten minutes, and what loam costs an agent
|
|
1511
|
+
|
|
1512
|
+
No code, flag, command or payload key was added, changed or retired; the binary is unchanged. The
|
|
1513
|
+
new paths are `docs/SERVICE-TEAMS.md` and `scripts/payload-sizes.ts`.
|
|
1514
|
+
|
|
1515
|
+
- **README has a service-repo CI recipe** beside the docs repo's fleet-edges one. The workflow
|
|
1516
|
+
checks out the service and the docs repo as sibling subdirectories of the workspace, so a
|
|
1517
|
+
committed relative `docsDir` such as `../docs` resolves. It does not use `path: ../docs`, which
|
|
1518
|
+
`actions/checkout` refuses. It runs `loam validate --service <id> --json` at the same pinned
|
|
1519
|
+
version, and two `jq` programs turn the findings, or a refusal, into `::error` / `::warning`
|
|
1520
|
+
annotations and a `$GITHUB_STEP_SUMMARY` table. A deploy pipeline can add
|
|
1521
|
+
`loam gate --service <id> --since <ref>`. It is followed by setup for cloud agents in a service
|
|
1522
|
+
repo: a `copilot-setup-steps.yml` and a Codex environment setup script, each cloning the docs
|
|
1523
|
+
repo into the committed `docsDir` with a read token from a secret.
|
|
1524
|
+
- **README's day-zero binding passes `--agent-profile service`**, and says why: the profile
|
|
1525
|
+
defaults to `full`, which puts `loam-feature` and `loam-ship` into every service checkout.
|
|
1526
|
+
QUICKSTART's command for joining a shared docs repo passes it too.
|
|
1527
|
+
- **QUICKSTART opens with ten minutes of CLI only.** A two-service `fleet.yaml`, `loam seed`, one
|
|
1528
|
+
provider `openapi.yaml` and one misspelled `op` on an edge give `spine.op-undefined`, with no
|
|
1529
|
+
agent and no adoption. A requirements-only `loam new --touches … --touches …` then validates and
|
|
1530
|
+
plans its archive before either service is adopted. Every output shown was produced by running
|
|
1531
|
+
the commands. The single-project path moves to sections 3–6.
|
|
1532
|
+
- **`docs/SERVICE-TEAMS.md`, one screen for a team whose repository is one service in a fleet:**
|
|
1533
|
+
four commands, five slash commands, what a person signs when they vouch, and the fifteen codes
|
|
1534
|
+
they will actually meet. README, QUICKSTART and the Docs list link to it.
|
|
1535
|
+
- **COMPARISON.md names OpenSpec, Spec Kit and Kiro again, in one dated section.** "Spec-driven
|
|
1536
|
+
change tools, as of October 2026" sets out what each offers for a change that crosses
|
|
1537
|
+
repositories: OpenSpec Stores and worksets, `show --diff` and the Cloud Agent; Spec Kit's
|
|
1538
|
+
contract-driven guide, `/speckit.converge` and MCP server; Kiro's Design-First specs and Kiro
|
|
1539
|
+
Web. It records what is still open upstream (OpenSpec #725, Spec Kit #4583) and where loam
|
|
1540
|
+
differs, including the two gaps it has against them. Each claim links its source. The rest of
|
|
1541
|
+
the page stays an ecosystem map, and `test/docs-drift.test.ts` now allows the three names only
|
|
1542
|
+
inside that dated section.
|
|
1543
|
+
- **`docs/BENCHMARKS.md` has a table of agent-facing payload sizes**, in bytes and ~tokens:
|
|
1544
|
+
the generated `AGENTS.md`, `loam adopt --service <id> --json` with and without `--targets`,
|
|
1545
|
+
every `loam instructions` page with and without `--no-fix-tables`, and `loam explain <code>
|
|
1546
|
+
--json` across the whole vocabulary. `npx tsx scripts/payload-sizes.ts --write` generates it from
|
|
1547
|
+
the tree, and `test/docs-facts.test.ts` regenerates it and fails when the committed table
|
|
1548
|
+
differs. README's adopt row now points at that table instead of quoting a dated figure.
|
|
1549
|
+
`test/docs-drift.test.ts` now also requires every exact `@ybotok/loam@<version>` in README to be
|
|
1550
|
+
this checkout's version.
|
|
1551
|
+
|
|
1552
|
+
### Changed — `loam archive`'s overwrite warnings name the current consumers
|
|
1553
|
+
|
|
1554
|
+
No code or flag was added. Existing warnings now fill a payload key that was always empty.
|
|
1555
|
+
|
|
1556
|
+
- **`openapi.op-modified` and `asyncapi.message-modified` now say who the overwrite affects.**
|
|
1557
|
+
Each message ends with one of three phrases: `N current consumer(s) still name it`,
|
|
1558
|
+
`whether current consumers name it could not be fully answered`, or
|
|
1559
|
+
`no current consumer names it`. The warning's `details` (until now always `[]`) list the
|
|
1560
|
+
consumers, in `archive --json` `warnings[]` and, when the contract merge is refused as
|
|
1561
|
+
`not-coherent`, in the same warning's entry in `issues[]`:
|
|
1562
|
+
- edges of the `architecture/` project;
|
|
1563
|
+
- other services' living requirements, in either spec file;
|
|
1564
|
+
- use-case hops;
|
|
1565
|
+
- and last, any consumer whose documents could not be read, or any caller drawn without an
|
|
1566
|
+
`op`.
|
|
1567
|
+
|
|
1568
|
+
The human plan prints the same lines under the warning. This is the scan `loam diff` runs for
|
|
1569
|
+
`diff.op-modified` and `diff.message-modified`. Here it runs over the living fleet as it stands
|
|
1570
|
+
before the merge, and only when one of these warnings fires. On the 120-service benchmark fleet
|
|
1571
|
+
that costs about 130 ms. Severity is unchanged, and neither warning gates the archive. Whether a
|
|
1572
|
+
change breaks a consumer is still not graded (`oasdiff breaking` / `asyncapi diff` do that).
|
|
1573
|
+
- `openapi.component-modified` and `asyncapi.component-modified` do not name consumers. A
|
|
1574
|
+
component's consumers are the operations or messages whose `$ref` closure reaches it, and loam
|
|
1575
|
+
has no index of those.
|
|
1576
|
+
|
|
1577
|
+
### Fixed — an MCP tool argument is never read in another argument's slot
|
|
1578
|
+
|
|
1579
|
+
No code, flag or payload key was added, changed or retired. Two MCP input schemas each gained one
|
|
1580
|
+
JSON Schema keyword, named below.
|
|
1581
|
+
|
|
1582
|
+
- **A `tools/call` that supplies an optional positional argument while leaving out an earlier one is
|
|
1583
|
+
refused as invalid params (`-32602`)**, naming the argument left out, as a missing required
|
|
1584
|
+
argument already is. Positionals reach the command by position, so the later value used to be read
|
|
1585
|
+
as the earlier one: `loam_instructions` with `args` and no `workflow` took the first arg as the
|
|
1586
|
+
workflow name. An empty `args` still passes. The defect is present in `0.2.0-alpha.8`.
|
|
1587
|
+
- **The MCP tools' advertised input schemas now say that `name` needs `section` and a non-empty
|
|
1588
|
+
`args` needs `workflow`**, the rule a tool call already enforces, so a client that validates a
|
|
1589
|
+
call against `tools/list` is no longer refused `-32602` for a call the schema allowed.
|
|
1590
|
+
`loam_list`'s schema gains `dependentRequired` (`name` → `section`); `loam_instructions`' gains
|
|
1591
|
+
`dependentSchemas`, requiring `workflow` once `args` has a member, because an empty `args` still
|
|
1592
|
+
passes. Both are standard JSON Schema keywords; no tool, argument or other schema key changed.
|
|
1593
|
+
|
|
1594
|
+
### Fixed — `loam validate` reports an unreadable verification record from the docs repo
|
|
1595
|
+
|
|
1596
|
+
No code, flag or payload key was added. An existing code fires in one more place.
|
|
1597
|
+
|
|
1598
|
+
- **`evidence.record-unreadable` (warn) now fires on the feature target too**, under
|
|
1599
|
+
`validate --feature <FEAT>`, `validate <FEAT>` and every active feature under `validate --all`, run
|
|
1600
|
+
from anywhere. A `features/<FEAT>/verification.yaml` or `verification/<svc>.yaml` with git
|
|
1601
|
+
conflict markers, broken YAML, a shape that is not a record, or a summary that contradicts its
|
|
1602
|
+
claims used to pass `validate --all` in the docs repo with no finding. `verify` refused the file,
|
|
1603
|
+
and `status`, `gate` and `tasks` reported it. Until now only the service target raised this code,
|
|
1604
|
+
and only from the service's own repository. The finding names the file and gives the same repair
|
|
1605
|
+
as the service-target finding. Its `locations` hold that file as `primary`. It is a warning, as
|
|
1606
|
+
it is on the service target. `valid`, the exit code and `loam archive` are unchanged, and
|
|
1607
|
+
`--strict` fails on it. In a service repository's `validate --all` the same file is reported
|
|
1608
|
+
twice: once by the service target and once by the feature target.
|
|
1609
|
+
|
|
1610
|
+
### Fixed — `loam diff` reads a spec saved as UTF-16 without a byte-order mark the same way on both sides
|
|
1611
|
+
|
|
1612
|
+
No code, flag, path or payload key was added, changed or retired.
|
|
1613
|
+
|
|
1614
|
+
- On disk, a `spec.md` or `arch.spec.md` saved as UTF-16 without a byte-order mark was refused as
|
|
1615
|
+
unreadable. At the base ref, the same bytes were read as a readable spec with no requirements.
|
|
1616
|
+
That happened because only the working-tree side checked for NUL characters. So an untouched file
|
|
1617
|
+
was suspended on one side only, and a branch that re-saved the file as UTF-8 reported every
|
|
1618
|
+
requirement in it as `diff.requirement-added`. The base side now applies the same decoding rule as
|
|
1619
|
+
the working tree. Both sides report the file as unreadable, and the spec axis is suspended. The
|
|
1620
|
+
contract axes are unchanged: their readers on both sides already applied the same rule.
|
|
1621
|
+
|
|
1622
|
+
### Fixed — a writer waiting on the lock no longer deletes the next writer's live lock
|
|
1623
|
+
|
|
1624
|
+
No code, flag, path or payload key was added, changed or retired. What changes is that concurrent
|
|
1625
|
+
writers no longer fail, or share one commit window, when a lock holder exits while another writer
|
|
1626
|
+
waits for it.
|
|
1627
|
+
|
|
1628
|
+
- **A stale `.loam-lock` is broken only while the holder judged dead still holds it.** A writer
|
|
1629
|
+
that finds the lock held reads the holder, and if that pid no longer exists on this host, it
|
|
1630
|
+
breaks the lock. Under CPU load the holder could release normally and exit between that read and
|
|
1631
|
+
the pid check, and a third writer could take the lock in the same gap. The waiter then deleted
|
|
1632
|
+
the third writer's live lock and took one of its own, so two writers were inside one commit window.
|
|
1633
|
+
One of four concurrent `loam gherkin --json` runs failed as `merge-failed` (`ENOENT … rename
|
|
1634
|
+
'….feature.loam-<pid>-…tmp'`): the second writer's recovery read the first's in-flight journal as
|
|
1635
|
+
an interrupted commit and swept its staged files. The code is shared by every writer that takes
|
|
1636
|
+
the lock (`archive`, `unarchive`, `new`, `seed`, `rebase`, `subsystem`, `verify --record`, `vouch`,
|
|
1637
|
+
`gherkin`), and the defect is present in `0.2.0-alpha.8`. The waiter now re-reads the lock after
|
|
1638
|
+
its verdict, and moves it to a private name before removing it. Anything it moved that is not the
|
|
1639
|
+
lock it judged goes back in place. A crashed holder's lock is still reclaimed as before.
|
|
1640
|
+
|
|
1641
|
+
### Fixed — `validate --all --base` grades the landscape by what the branch changed in it
|
|
1642
|
+
|
|
1643
|
+
No code, flag, path or payload key was added, changed or retired; the unscoped `validate --all` is
|
|
1644
|
+
unchanged.
|
|
1645
|
+
|
|
1646
|
+
- **A call edge the branch adds or edits in `architecture/` is graded against its provider's
|
|
1647
|
+
contract**, even when the provider is not in scope. README's own example — `authorisePayment` on
|
|
1648
|
+
an edge into a contract spelling it `authorizePayment` — used to pass both PR gates on the example
|
|
1649
|
+
fleet and, even under `--strict`, on a freshly seeded spine-first fleet. It now fails as
|
|
1650
|
+
`spine.op-undefined` (and `spine.op-deprecated`, `spine.op-link-missing` and `spine.resolved` read
|
|
1651
|
+
the same way), filed on the `landscape` target. Only those edges are graded: the provider is not
|
|
1652
|
+
pulled into scope, so its own documents — on a seeded fleet, its `service.no-model` — stay out of
|
|
1653
|
+
the PR's verdict. An edge is new when its (source, target, op) is; a reformat grades nothing.
|
|
1654
|
+
- **An edge whose metadata or tags hold a word containing `op` is graded on the op it names.**
|
|
1655
|
+
`--base` finds the branch's edges by reading the map's text, and that reading took
|
|
1656
|
+
`op-version 'v2'` above `op 'cancelOrdr'` for op `v2`, and a `#op-critical` tag for an op naming
|
|
1657
|
+
the description below it. The edge it read matched no edge in the parsed map, so the branch's
|
|
1658
|
+
misspelt `cancelOrdr` was graded nowhere: exit 0 and `valid: true`, where the unscoped run
|
|
1659
|
+
reports `spine.op-undefined`. A key is read whole now, and the op only from `metadata { }`.
|
|
1660
|
+
- **An edge written after a bare carriage return is graded on its own source.** After a `link`,
|
|
1661
|
+
`description` or `technology` property ending in a bare `\r`, the parent's sourceless
|
|
1662
|
+
`-> target` edge was read as an edge FROM `link` (or `description`, or `technology`), and an edge
|
|
1663
|
+
after a `// comment` ending in a bare `\r` was not read at all. Either way the branch's op on that
|
|
1664
|
+
edge was graded nowhere, with the same exit 0. LikeC4 breaks a line at a bare `\r`, and the
|
|
1665
|
+
reading does too now.
|
|
1666
|
+
- **The landscape re-grades a fleet-wide vocabulary only when the file that carries it changed.**
|
|
1667
|
+
`permissions.*` rides a scoped run only when `architecture/permissions.yaml` changed,
|
|
1668
|
+
`capability.*` only when `architecture/capabilities.yaml` or `capabilities/` did,
|
|
1669
|
+
`obligation.invalid` and `obligation.unapplied` only when `architecture/obligations.yaml` did,
|
|
1670
|
+
`glossary.*` only when `glossary/` did, and `agents.stale` / `docs.binary-behind` only when
|
|
1671
|
+
`AGENTS.md` did. A one-line map edit on the example fleet is no longer red under `--strict` on four
|
|
1672
|
+
warnings that were all there at the base.
|
|
1673
|
+
- README's spine-first section now says that under the `--base` gate a PR adding a service's
|
|
1674
|
+
`openapi.yaml` stays red until that service has a `model.likec4` — `service.no-model` is the
|
|
1675
|
+
ratchet working as designed.
|
|
1676
|
+
- **Each service target in scope reports a committed map that does not parse, even when the branch
|
|
1677
|
+
left `architecture/` alone.** On such a branch the scoped run emits no `landscape` target, yet
|
|
1678
|
+
each service in scope still said `spine.landscape-invalid … the parser output is reported once,
|
|
1679
|
+
on the landscape target`, with empty `details`. So the parser output was reported nowhere. Each
|
|
1680
|
+
service's `spine.landscape-invalid` now carries the parser output in `details` whenever the run
|
|
1681
|
+
has no `landscape` target, as `validate --service` always has. A feature target does not report
|
|
1682
|
+
the map, so a run whose only targets are features still says nothing about it. The defect
|
|
1683
|
+
predates this release's `--base` changes and is present in `0.2.0-alpha.8`.
|
|
1684
|
+
|
|
1685
|
+
### Fixed — one LikeC4 typo no longer floods agent-facing output
|
|
1686
|
+
|
|
1687
|
+
No code, flag, path or payload key was added, changed or removed; what moves is the TEXT of the
|
|
1688
|
+
diagnostics LikeC4 hands loam, and one warning that stops cascading.
|
|
1689
|
+
|
|
1690
|
+
- **A parser's "Expecting: one of these possible Token sequences" message is cut to its first four
|
|
1691
|
+
alternatives plus `… (+N more)`**, keeping the `but found: '…'` line — on every surface that
|
|
1692
|
+
prints a LikeC4 diagnostic (`validate`, `context`, `show`, `tasks`, `doctor`, `gate`,
|
|
1693
|
+
`archive`). One dropped quote in the example fleet's order-service model made `loam validate
|
|
1694
|
+
--service order-service` print 18,503 lines (434 KB; 458 KB as `--json`); it now prints 27 lines
|
|
1695
|
+
(1.7 KB; 5.5 KB as `--json`).
|
|
1696
|
+
- **Each LikeC4 diagnostic is reported once, in line order.** The same message at the same line of
|
|
1697
|
+
the same file used to be listed twice, and the lexer's error led the parser's even when it sat a
|
|
1698
|
+
dozen lines below the fault. The `N error(s)` count in `c4.invalid`, `delta.invalid` and the other
|
|
1699
|
+
parse findings now counts distinct diagnostics.
|
|
1700
|
+
- **`covers.unknown` is no longer raised for a service whose own `model.likec4` does not parse.**
|
|
1701
|
+
`c4.invalid` is the one finding; the six `covers.unknown` warnings the example fleet showed for one
|
|
1702
|
+
dropped quote named ids that were spelled right. The same suspension already applied to a model
|
|
1703
|
+
extending a map that does not parse.
|
|
1704
|
+
- **A broken document no longer bloats anybody else's briefing.** A dropped quote in a feature's
|
|
1705
|
+
`delta.likec4` made `loam context order-service --json` 626 KB instead of 24 KB (now 27 KB). The
|
|
1706
|
+
fleet map's parse error reached `useCaseScan.error` on every service's `context --json` and the
|
|
1707
|
+
`loam gate` "landscape cannot be used" message raw — 173 KB and 152 KB — and now goes through the
|
|
1708
|
+
same spelling as every other diagnostic, which adds its 1-based `L<n>:` line (23 KB and 2 KB).
|
|
1709
|
+
|
|
1710
|
+
### Fixed — `doctor.agent-files-stale` fires on a changed body, not on every release
|
|
1711
|
+
|
|
1712
|
+
No code, flag, path or payload key was added, changed or retired; what a repo NOTICES is that
|
|
1713
|
+
`loam doctor` stops warning about generated command and skill files whose only difference is the
|
|
1714
|
+
version stamp.
|
|
1715
|
+
|
|
1716
|
+
- The stamp sits inside every generated command and skill file, so every release used to turn every
|
|
1717
|
+
one of them stale in every repo, even when the release had not changed a word of the pointer — a
|
|
1718
|
+
warning that is always on, which then hid the release where a pointer really did change. A file is
|
|
1719
|
+
now stale only when its body, the stamp line set aside, is not what this loam writes, AND its stamp
|
|
1720
|
+
is missing or older. Line endings do not count (a Windows checkout that converts them to CRLF is
|
|
1721
|
+
not a change). A customized body under a current stamp stays quiet, as before. Same code, same severity;
|
|
1722
|
+
the `agents.staleFiles` list in `loam doctor --json` simply stops naming stamp-only files.
|
|
1723
|
+
|
|
1724
|
+
### Fixed — relationship order is the same on every run
|
|
1725
|
+
|
|
1726
|
+
No code, flag, path or payload key was added, changed or retired; lists built from relationships
|
|
1727
|
+
come back in one stable order.
|
|
1728
|
+
|
|
1729
|
+
- LikeC4 indexes relationships by a hash of the document's path, and loam stages documents under a
|
|
1730
|
+
fresh random folder per run, so an edge whose hash happened to come out all digits (about one in
|
|
1731
|
+
2,200) jumped to the front of the list on that run only; inside a project of several documents the
|
|
1732
|
+
base order was whichever file finished reading first. A scheduled CI run failed on it. Every loader
|
|
1733
|
+
now returns relationships sorted by source, target, title, `op`, `publishes`, `consumes` and tags,
|
|
1734
|
+
so anything listed from edges — `loam explore`'s neighbours, the candidates a
|
|
1735
|
+
`usecase.step-contested` finding names, findings emitted per edge — keeps one order between runs
|
|
1736
|
+
and machines. That order is no longer the declaration order.
|
|
1737
|
+
|
|
1738
|
+
### Fixed — the repository no longer claims a version is published
|
|
1739
|
+
|
|
1740
|
+
No code, flag, path or payload key was added, changed or retired; only documentation moved.
|
|
1741
|
+
|
|
1742
|
+
- README called the version in `package.json` "the newest published version", by construction of
|
|
1743
|
+
the test that pinned it — and kept saying so for `0.2.0-alpha.8`, whose publish failed. It now
|
|
1744
|
+
names that version as the version of the checkout and points at the npm dist-tags as the source of
|
|
1745
|
+
truth; the drift test forbids the publication claim in README and in the CHANGELOG section of the
|
|
1746
|
+
checkout's own version. The `0.2.0-alpha.8` section below carries a dated note that it was never
|
|
1747
|
+
published.
|
|
1748
|
+
- The `0.2.0-alpha.3` section promised that installing `@ybotok/loam@alpha` gets it. That release was tagged
|
|
1749
|
+
and withdrawn before its publish step, which only the `0.2.0-alpha.4` section said. It now carries
|
|
1750
|
+
the same kind of dated note.
|
|
1751
|
+
- README said Codex reads `.codex/skills/`. That is the folder the `codex` target writes, and Codex
|
|
1752
|
+
still loads it, but Codex's skills documentation names only `.agents/skills`. README now says so
|
|
1753
|
+
and points at the `agents` target for the documented root. The `codex` target's path is
|
|
1754
|
+
unchanged.
|
|
1755
|
+
- README and WORKFLOW no longer quote byte sizes and code counts that had gone stale (the
|
|
1756
|
+
`loam-check` page, the AGENTS.md orientation, the finding-code total). A finding- or refusal-code
|
|
1757
|
+
count quoted on a shipped page is now graded against `loam explain --codes`.
|
|
1758
|
+
|
|
1759
|
+
### Changed — `loam archive` moves the delta's bytes and nothing else's
|
|
1760
|
+
|
|
1761
|
+
- **The requirements merge splices instead of re-serializing.** A living requirement the delta does
|
|
1762
|
+
not touch is written back byte for byte; only ADDED requirements and the ones a MODIFIED replaces
|
|
1763
|
+
are serialized. Before, the whole `## Requirements` run was re-serialized, so a living spec
|
|
1764
|
+
written without a blank line under `### Requirement:` had one inserted under every untouched
|
|
1765
|
+
requirement by its first archive — the diff, `loam vouch --pack` and the re-vouch all widened to
|
|
1766
|
+
the whole section. `loam unarchive` is unchanged and still restores every byte. Each kept slice
|
|
1767
|
+
is matched to its requirement by `Requirement-ID:` (the heading when there is none), never by
|
|
1768
|
+
position; if the slices and the living requirements do not match one to one, the whole run is
|
|
1769
|
+
re-serialized as before rather than one requirement's text landing in another's place.
|
|
1770
|
+
- **`archive --json` gains the additive `revouchOwed` key** (`[{service, path, owner}]`, on the
|
|
1771
|
+
dry run and the real merge alike): every vouched living `spec.md` / `arch.spec.md` the merge
|
|
1772
|
+
moves under its vouch stamp, with the frontmatter's `owner` (`null` when none is recorded).
|
|
1773
|
+
`content.stale` follows on each — unchanged and still a warning; no new code, nothing `--strict`
|
|
1774
|
+
lets through. The human view prints the same list under a `re-vouch owed` line, naming
|
|
1775
|
+
`loam vouch --service <id>` and saying the re-vouches belong in the archive's own PR, which
|
|
1776
|
+
`loam validate --all --base <ref> --strict` keeps red until they land. The `/loam-ship` protocol
|
|
1777
|
+
gains that step, and README's docs-repo PR recipe says so.
|
|
1778
|
+
- The human view of `loam archive` no longer prints its `archive <id>` header twice.
|
|
1779
|
+
|
|
1780
|
+
### Fixed — the adopt brief's spec examples
|
|
1781
|
+
|
|
1782
|
+
- The `spec.md` and `arch.spec.md` examples in `loam adopt`'s brief name the service being adopted
|
|
1783
|
+
in their frontmatter `service:` and title, where they used to say `payment-service` for every
|
|
1784
|
+
service — a verbatim paste failed `frontmatter.field-mismatch`. Their requirements now carry the
|
|
1785
|
+
canonical blank line under `### Requirement:`, so a spec copied from them is not reflowed by its
|
|
1786
|
+
first archive.
|
|
1787
|
+
|
|
1788
|
+
### Fixed — fleet readouts tell the truth about trust
|
|
1789
|
+
|
|
1790
|
+
A vouched spec edited after the vouch (`content.stale`, computable from the docs repo) was reported
|
|
1791
|
+
by `loam validate` and by no fleet readout; `sources.unwalked` fired in every service repo straight
|
|
1792
|
+
after a fresh vouch; and the human `status`/`list` views printed a verification count without
|
|
1793
|
+
saying who answered it. Additive throughout — no code, flag or key changed or was removed, and the
|
|
1794
|
+
maturity rung of every service is unchanged.
|
|
1795
|
+
|
|
1796
|
+
- **New stable code `next.revouch`** in `loam status`'s fleet `next[]` (and under `--service`): one
|
|
1797
|
+
step per vouched service whose `spec.md` or `arch.spec.md` changed since the vouch, naming the
|
|
1798
|
+
documents. Its `command` is `loam vouch --service <id>` and its `execution` is
|
|
1799
|
+
`kind: "human-review"`, `runnable: false`, `cwd: "service-repo"` — a re-vouch is a person's act.
|
|
1800
|
+
While one stands, `status` no longer reports `next.fleet-clean`.
|
|
1801
|
+
- **New payload key `services.contentStale`** on `loam status --json` (fleet form): of `vouched`, the
|
|
1802
|
+
services with a document moved under its stamp — a subset, like `sampledVouched`. The human line
|
|
1803
|
+
reads `N vouched (M stale)`.
|
|
1804
|
+
- **New payload key `scorecard.provenance.contentStale`** on `loam validate --all --json`: services
|
|
1805
|
+
with a `content.stale` finding in that run. The text row reads `N vouched — M stale content ·
|
|
1806
|
+
K stale digest(s) · …` (it used to say `0 stale digest(s)` beside the warning).
|
|
1807
|
+
- **New payload key `answered` inside `verification`** on `loam status --json` (the feature form, and each
|
|
1808
|
+
`features[].verification` of the fleet form) and on each `features[]` row of `loam gate --json`,
|
|
1809
|
+
which spreads the same record: the confirmed claims per `answered_by`
|
|
1810
|
+
(`runner`, `external-runner`, `agent`), the split `list --json` already carried. The human views
|
|
1811
|
+
print it beside the verdict — `status <FEAT>` as `verified 19/19 confirmed (13 runner · 6 agent)`,
|
|
1812
|
+
`list`'s verification column as `7/7 (5 runner · 2 agent)`. The verdict and its definition are
|
|
1813
|
+
unchanged.
|
|
1814
|
+
- **`loam list`** shows such a service as `vouched (stale)` (or `vouched (sampled, stale)`) and counts
|
|
1815
|
+
it in the maturity line; its `missing` — in text and in `--json`, and likewise in `explore` and the
|
|
1816
|
+
context pack, which share it — now reads `re-vouch (<file> changed since the vouch)`, so
|
|
1817
|
+
`list --needs-work` (with `--review-order` and `--owners`) lists it instead of answering
|
|
1818
|
+
"nothing to do". The `maturity` value stays `vouched`.
|
|
1819
|
+
- **`sources.unwalked`** no longer counts loam's own files — `loam.json` and the agent files recorded
|
|
1820
|
+
in its `agentFiles` — or any `.gitignore` toward its denominator. `.gitignore` is the file that
|
|
1821
|
+
defines what git tracks, so it is the measuring instrument, never a part of the service a spec
|
|
1822
|
+
owes; `package.json`, READMEs and the like stay in. A freshly vouched service repo is no longer
|
|
1823
|
+
red under `validate --service --strict` for files it cannot honestly list in `sources`.
|
|
1824
|
+
- **`loam list` legend** names the `e` (events / AsyncAPI) column: `[e]vents`.
|
|
1825
|
+
- **SCHEMA.md**: the per-service CI summary shape gains `contentStale` (the count of `content.stale`
|
|
1826
|
+
findings), and its jq derivation counts it. **WORKFLOW.md** states that `loam gate --strict` also
|
|
1827
|
+
fails on a join partner's `content.stale`.
|
|
1828
|
+
|
|
1829
|
+
### Added
|
|
1830
|
+
|
|
1831
|
+
- **New warning `frontmatter.owner-mismatch`** from `loam validate` (`--service` and `--all`) when
|
|
1832
|
+
one service's `spec.md` and `arch.spec.md` both name an `owner` and the two differ. `loam show`
|
|
1833
|
+
and `loam context` print only `spec.md`'s owner, so the other value was a second answer nobody
|
|
1834
|
+
saw. Its `details` list each file's value (`spec.md: <owner>`, `arch.spec.md: <owner>`). Silent
|
|
1835
|
+
when either file names no owner (that stays `frontmatter.field-missing`). It compares the two
|
|
1836
|
+
files only, never CODEOWNERS. As a warning it fails `loam validate --strict`, so a service that
|
|
1837
|
+
passed `--strict` with two different owners now exits 1 there until they agree.
|
|
1838
|
+
- **`openapi.generated-stale` now says what moved.** The finding gains `details` naming each
|
|
1839
|
+
operationId that differs between the build's contract and the committed copy — `served, not in
|
|
1840
|
+
the committed copy: <id>` and `committed, no longer served: <id>` — and its message counts both
|
|
1841
|
+
sides, or says every operationId matches when the difference is inside an operation or outside
|
|
1842
|
+
`paths`. The digest still decides whether it fires; same code, same severity.
|
|
1843
|
+
- **`diff.op-modified` (warn)**: an operationId present at the base ref and now, whose operation
|
|
1844
|
+
changed — its own definition, its method or path, or anything its internal `$ref`s reach, so a
|
|
1845
|
+
field renamed in a shared `components` schema counts. **`diff.message-modified` (warn)** is the
|
|
1846
|
+
same for an AsyncAPI message: its declaration or anything its payload `$ref`s reach. Both name
|
|
1847
|
+
the authored consumers in `details[]` (the same scan `diff.op-deprecated` runs) and point at
|
|
1848
|
+
oasdiff or `asyncapi diff`; neither grades whether the change is breaking, so neither sets
|
|
1849
|
+
`breaking` or changes the exit code. A PR that renamed a required field used to print "no
|
|
1850
|
+
fleet-meaningful changes" and exit 0 with nothing else.
|
|
1851
|
+
- **The `modified` count in `loam diff --json`'s `summary` now counts both new codes** beside
|
|
1852
|
+
`diff.requirement-modified`. The key is unchanged; a diff over a changed contract reports a
|
|
1853
|
+
higher number than before.
|
|
1854
|
+
- A newly deprecated operation is still reported once, as `diff.op-deprecated`, not also as
|
|
1855
|
+
`diff.op-modified`.
|
|
1856
|
+
|
|
1857
|
+
### Changed
|
|
1858
|
+
|
|
1859
|
+
- **An AsyncAPI 1.x or 2.x document is `asyncapi.invalid`, once.** loam reads AsyncAPI 3.0 only,
|
|
1860
|
+
and a document whose `asyncapi:` field says `1.` or `2.` used to read as a 3.0 contract with no
|
|
1861
|
+
messages: one `spine.message-undefined` per landscape edge, one `spec-event.message-undefined` per
|
|
1862
|
+
requirement and a `spine.message-unproduced` on every consumer, none naming the version. It now
|
|
1863
|
+
takes the unreadable path every broken contract takes — one `asyncapi.invalid` whose `details`
|
|
1864
|
+
name the version and `asyncapi convert --target-version=3.0.0`, and nothing on the event axis
|
|
1865
|
+
graded against it. The same applies to a feature's own `asyncapi.yaml` under `--feature`. The
|
|
1866
|
+
`asyncapi.invalid` message, and the `context` / `delta` lines for an unreadable event contract,
|
|
1867
|
+
now say "does not read as AsyncAPI 3.0" rather than "does not parse". **It is an error:
|
|
1868
|
+
`loam validate` now exits 1 on such a document even where no edge or requirement names its
|
|
1869
|
+
messages** — a document that used to pass. `asyncapi convert --target-version=3.0.0` upgrades it.
|
|
1870
|
+
- **`loam diff --base <ref>` exits 1 while a CHANGED `asyncapi.yaml` says 1.x or 2.x.** The
|
|
1871
|
+
document is listed in that service's `unreadable[]` once for each side on which it says 1.x or
|
|
1872
|
+
2.x, counted in the `unreadable` total of `summary`, and the service reads
|
|
1873
|
+
`change: "changed"`; it used to read as a contract with no messages and exit 0. One the branch
|
|
1874
|
+
did not touch does not do this — see the next entry. `asyncapi convert --target-version=3.0.0`
|
|
1875
|
+
upgrades it.
|
|
1876
|
+
- **`loam diff --base <ref>` skips an unreadable `openapi.yaml` or `asyncapi.yaml` that the base
|
|
1877
|
+
ref holds byte for byte.** Such a contract is no longer listed in `unreadable[]`, counted in
|
|
1878
|
+
`summary`, or the reason a service reads `change: "changed"`, so an unconverted
|
|
1879
|
+
AsyncAPI 2.x file, or a broken OpenAPI file, that the branch never touched no longer fails every
|
|
1880
|
+
PR's diff with exit 1. A contract the branch changed, or one only one side cannot read, still
|
|
1881
|
+
suspends its axis and exits 1. `spec.md` and `arch.spec.md` are not skipped: an unreadable one
|
|
1882
|
+
stays suspended even when untouched, because another service's removal may be the one that
|
|
1883
|
+
could not be checked against it. No key is added or removed.
|
|
1884
|
+
- **Suggested element ids follow `loam seed`'s convention.** For a service the fleet map does not
|
|
1885
|
+
draw yet, the `loam adopt` brief and the `loam new` delta scaffold now suggest `svc_<id>` (dots
|
|
1886
|
+
and dashes folded to `_`, e.g. `svc_payment_service`) instead of a camel-cased name, and
|
|
1887
|
+
`loam new --touches <id>` reuses the element id the map already draws the service as. A service
|
|
1888
|
+
the map nests inside another element is quoted by its last segment with a note naming the parent,
|
|
1889
|
+
since a dotted id is no top-level declaration. The commented example in a freshly seeded
|
|
1890
|
+
`architecture/landscape.likec4` uses the seeded ids too. Element ids stay cosmetic —
|
|
1891
|
+
`metadata { service '<id>' }` is the join — so nothing already written needs to change.
|
|
1892
|
+
- **`seed-landscape-edited` gives advice that can be followed.** The refusal and `loam explain
|
|
1893
|
+
seed-landscape-edited` used to say "fold the edits into fleet.yaml and delete the file", which is
|
|
1894
|
+
impossible once an edge carries `metadata { op }` (fleet.yaml cannot). They now say seed runs
|
|
1895
|
+
once, before any operation is drawn, and that the map grows from then on through
|
|
1896
|
+
`loam adopt --service <id>` and feature C4 deltas. Same code, same exit code.
|
|
1897
|
+
- **`migrate-openspec` and `audit-openspec` read the OpenSpec 1.13 requirement grammar.** A
|
|
1898
|
+
`RENAMED` pair written with a `*` or `+` bullet, or with no bullet, is a pair (it was
|
|
1899
|
+
`openspec.renamed-malformed`); a `REMOVED` section written as a bullet list (`-`, `*` or `+`) of
|
|
1900
|
+
requirement headings is read as those removals (it was `openspec.change-empty`); and a closing
|
|
1901
|
+
`#` run on a `### Requirement:` or `#### Scenario:` heading is dropped at ingest, so
|
|
1902
|
+
`### Requirement: Invoice totals ###` stages as `Invoice totals` and a MODIFIED delta naming it
|
|
1903
|
+
no longer fails `loam validate` as `delta.modified-unknown`. loam's own parser is unchanged.
|
|
1904
|
+
- **An inbound landscape edge into the provider with no `metadata { op }`** (and no
|
|
1905
|
+
`publishes`/`consumes`) is now listed in the `details[]` of `diff.op-removed`,
|
|
1906
|
+
`diff.op-removed-consumed`, `diff.op-deprecated` and `diff.op-modified` as a consumer whose
|
|
1907
|
+
operation is unknown. A removal with such a caller and no named one now says "could NOT be fully
|
|
1908
|
+
answered" instead of "no current landscape edge or living requirement names it". It stays a
|
|
1909
|
+
warning, exit 0: a map `loam seed` drew, every edge without an op, does not fail every removal.
|
|
1910
|
+
- **Without `--json`, a commander usage error** (a missing `--base` on `loam diff`, an unknown flag
|
|
1911
|
+
or command, a missing argument) now prints a fix line naming `loam <command> --help` and the code
|
|
1912
|
+
line `invalid-option · loam explain invalid-option` under commander's own message, like every
|
|
1913
|
+
other refusal. stdout stays empty, exit 1; the `--json` envelope is unchanged.
|
|
1914
|
+
- README's `.github/workflows/fleet-edges.yml` **pins loam to an exact version** (`LOAM`) and says
|
|
1915
|
+
how to move the pin, **adds an oasdiff step** per changed `services/*openapi.yaml`, and no longer
|
|
1916
|
+
labels the `diff` step "did you break a consumer?". `test/docs-drift.test.ts` keeps the pin equal
|
|
1917
|
+
to the version README names.
|
|
1918
|
+
|
|
1919
|
+
### Contributors — release preflight and test harness
|
|
1920
|
+
|
|
1921
|
+
- `npm run release:check` checks that every dependency a `package-lock.json` entry names resolves
|
|
1922
|
+
to another entry, the way Node looks it up, and then runs `npm ci --dry-run --offline` on a
|
|
1923
|
+
scratch copy of `package.json` and `package-lock.json` (new `scripts/lockfile-check.mjs`): the
|
|
1924
|
+
check that would have refused `0.2.0-alpha.8`'s stripped lockfile before the tag instead of after
|
|
1925
|
+
it. The structural walk comes first because npm's own verdict depends on the npm asked: npm 11
|
|
1926
|
+
refuses that lockfile, npm 10 (Node 22.22.3) installs it.
|
|
1927
|
+
- The test harness creates its temporary directories under the canonical (symlink-resolved) temp
|
|
1928
|
+
root, so the suite passes on macOS without a `TMPDIR` override.
|
|
1929
|
+
|
|
1930
|
+
### Changed — a federated verification record is one file per service (a file-format event)
|
|
1931
|
+
|
|
1932
|
+
- **New path written: `features/<FEAT>/verification/<svc>.yaml`.** `loam verify <FEAT> --service
|
|
1933
|
+
<svc> --record` (and `--results` / `--contract-results` under `--service`, and the bound form that
|
|
1934
|
+
substitutes `--service` from loam.json) now writes that service's attestation to a file of its
|
|
1935
|
+
own instead of into one shared `verification.yaml`. Each file is a `schema: 2` record holding
|
|
1936
|
+
exactly one attestation and that service's answers; the directory is the record, and the verdict
|
|
1937
|
+
and counts are computed from the union of its files when anything reads it. What a repo notices:
|
|
1938
|
+
two teams recording two different services on two docs-repo branches no longer conflict when
|
|
1939
|
+
their PRs merge — the shared file conflicted every time (add/add from an empty start, content
|
|
1940
|
+
conflicts on `summary:`, `claims:` and `attestations:` once one attestation had landed).
|
|
1941
|
+
- **Old path still read, forever: `features/<FEAT>/verification.yaml`**, active and archived, in
|
|
1942
|
+
every shape loam has written. A one-file federated record reads exactly as before until a service
|
|
1943
|
+
records again; that service's new file then supersedes its share of the old one (the old file is
|
|
1944
|
+
not rewritten), and the write that leaves nothing in it counting removes it. An all-at-once
|
|
1945
|
+
(schema-1) record is removed by the first `--service` write, which lists its answers in
|
|
1946
|
+
`discarded` — the same answers the old overwrite listed.
|
|
1947
|
+
- **The all-at-once form is unchanged on disk.** `loam verify <FEAT> --record` without `--service`
|
|
1948
|
+
still writes the one file `verification.yaml` — it answers the whole checklist from one place, so
|
|
1949
|
+
there is no second writer to collide with and no service to name a file after — and is still
|
|
1950
|
+
refused `record-federated` over any record carrying attestations, now including per-service files.
|
|
1951
|
+
- **When services answered different versions of the checklist**, a record kept as per-service files
|
|
1952
|
+
counts the answers of the files that answered the version the feature asks NOW — `loam verify`
|
|
1953
|
+
(read and `--record`), `loam tasks`, and `status`, `list` and `gate` for an in-flight feature all
|
|
1954
|
+
derive it — and falls back to the newest file (by `recorded` date, a later service id breaking a
|
|
1955
|
+
same-day tie) only for an archived feature's frozen view or when no file answered the current
|
|
1956
|
+
version. The others stay in `attestations` and are named by `verify.checklist-forked` until they
|
|
1957
|
+
record again — now on the recording run's own output (`notices` under `--json`) as well as the
|
|
1958
|
+
read view, so the service whose answers a tally leaves out is named where the tally is printed. A
|
|
1959
|
+
same-day record from a service with a later id against an older checklist no longer drops the
|
|
1960
|
+
current answers and makes every reader call the record stale. A one-file record keeps its old
|
|
1961
|
+
reading. The `verify.checklist-forked` message no longer suggests accepting the split.
|
|
1962
|
+
- **`record-unreadable` names the file that failed and the repair that is safe for it.** Git conflict
|
|
1963
|
+
markers in any record file are detected before the YAML is parsed and named with their lines. A
|
|
1964
|
+
service's own `verification/<svc>.yaml` is that service's word alone, so its advice is to repair
|
|
1965
|
+
it or delete it and record that service again; the one-file `verification.yaml` may hold other
|
|
1966
|
+
services' attestations, so its advice is to repair it or restore it from version control (after a
|
|
1967
|
+
merge, the copy on the branch you merged into) — never to delete it. The old advice ("delete it
|
|
1968
|
+
and record again") erased other teams' attestations when followed on a shared record, and a
|
|
1969
|
+
`verify.record-miscounted` record's "record the answers again" looped straight back into the same
|
|
1970
|
+
refusal. The same per-file advice reaches `evidence.record-unreadable`, and the `status` and
|
|
1971
|
+
`gate` sentences for an unreadable record no longer say "delete it". A per-service file holding
|
|
1972
|
+
anything but its own service's attestation is unreadable too.
|
|
1973
|
+
- **`path` in a recording `verify --json` payload** is the file this run wrote — the service's own
|
|
1974
|
+
`verification/<svc>.yaml` under `--service`. The `path` inside `recorded` on the read and frozen views, and the
|
|
1975
|
+
`verification` artifact's `path` in `status`, name the `verification/` directory once it exists,
|
|
1976
|
+
the one file otherwise. `archive` and `unarchive` carry the directory with the feature they move.
|
|
1977
|
+
No code or flag was added, changed or retired, and no payload key was added or removed. What
|
|
1978
|
+
changes is the VALUE of four `path` keys, and each may now name a DIRECTORY: `path` on a recording
|
|
1979
|
+
`verify --json`, the `path` inside `recorded` on the read and frozen views, the `verification` artifact's
|
|
1980
|
+
`path` in `status --json`, and the `path` on a `next.verify` step in `status --json` (which follows
|
|
1981
|
+
the artifact's). A script that opens one of them as a file must now expect
|
|
1982
|
+
`features/<FEAT>/verification` — a directory — once any service has recorded under `--service`.
|
|
1983
|
+
- **A loam older than this release does not read `verification/<svc>.yaml`.** It sees only
|
|
1984
|
+
`verification.yaml`, so `status`, `gate` and `verify` under an older pin report a feature recorded
|
|
1985
|
+
this way as having no record (`gate --strict` then exits 1 on `gate.feature-unverified`), and an
|
|
1986
|
+
older all-at-once `--record` writes a `verification.yaml` that this release ignores beside the
|
|
1987
|
+
directory (its answers carry no attestation). Upgrade every repository that records or gates a
|
|
1988
|
+
feature together — the docs repo's pipelines and each service repo's pin.
|
|
1989
|
+
- **`features/<FEAT>/verification/` is loam's from this release.** Every non-dot `*.yaml` in it is
|
|
1990
|
+
read as one service's attestation file, and one that is not (a hand-made `k6.yaml`, say) is
|
|
1991
|
+
`record-unreadable`: `verify` refuses and `status` shows the record as unreadable. Keep anything
|
|
1992
|
+
else beside the feature, not inside that directory.
|
|
1993
|
+
|
|
1994
|
+
### Changed — `verified` means a run of the stamped scenario, and the runner-neutral path is findable
|
|
1995
|
+
|
|
1996
|
+
- **A cucumber run must have run the stamped scenario's step count.** `verify --results` used to
|
|
1997
|
+
confirm a `scenario.tested` claim from any report element carrying its `@loam-digest-…` tag whose
|
|
1998
|
+
steps passed — a one-step stub tagged by hand confirmed five-step scenarios. A matched cucumber
|
|
1999
|
+
element must now have run exactly as many steps as `loam gherkin` emits for the scenario (`And` /
|
|
2000
|
+
`But` included); otherwise the claim is `unconfirmed`, noted `ran N step(s) where the stamped
|
|
2001
|
+
scenario has M`. The step text is not compared (outlines fill their placeholders in, dialects
|
|
2002
|
+
differ on keyword spacing). cucumber-js's hook entries inside `steps[]` (`hidden`, keyword
|
|
2003
|
+
`Before`/`After`) are read as hooks, not steps: they no longer count toward the steps, and a
|
|
2004
|
+
failing one is reported `failed in a before/after hook`. `loamScenarioReport` entries carry no
|
|
2005
|
+
steps and are held to their status, as before. What a repo notices: a report whose elements do
|
|
2006
|
+
not run every stamped step stops confirming.
|
|
2007
|
+
- **A `--results` file that is not JSON is refused naming both accepted shapes.** A JUnit XML report
|
|
2008
|
+
died with the parser's raw "Unexpected token '<'". It is still `answers-unreadable`, and the
|
|
2009
|
+
message now says the file is XML or not JSON and names what `--results` reads: cucumber JSON
|
|
2010
|
+
(cucumber-js, cucumber-jvm, behave, SpecFlow/Reqnroll, pytest-bdd `--cucumberjson`, godog) or
|
|
2011
|
+
loam's runner-neutral `{"loamScenarioReport": 1, …}`. A `--contract-results` file that is not JSON
|
|
2012
|
+
names its one shape the same way.
|
|
2013
|
+
- **`loamScenarioReport` is named wherever cucumber is.** The `--results` description in
|
|
2014
|
+
`loam verify --help` now reads "cucumber JSON, or loam's runner-neutral loamScenarioReport JSON for
|
|
2015
|
+
any other runner"; the `check` in the `done` block `loam tasks` prints for a `scenario.tested` unit names both
|
|
2016
|
+
report shapes; the `verify.scenario-attested` message's suggested command reads
|
|
2017
|
+
`--results <report.json>` and names both; WORKFLOW, the verify, implement and done-check protocols
|
|
2018
|
+
and the cycle `loam explain` teaches say the same. So does the `next.verify-attested` step in
|
|
2019
|
+
`status --json` — its `statement` names both shapes and its `command` now reads
|
|
2020
|
+
`loam verify <FEAT> --results <report.json>` (it was `--results <cucumber-report.json>`) — and
|
|
2021
|
+
`loam explain next.verify-attested`.
|
|
2022
|
+
- **The all-at-once `--record` checks each evidence entry shaped like a `file:line`.** Without
|
|
2023
|
+
`--service` it accepted any non-empty evidence — `src/foo.py:1` for a file that does not exist —
|
|
2024
|
+
and printed `verified`, while the federated form refused the same citation. An evidence entry of
|
|
2025
|
+
an agent-confirmed answer that is shaped like a `file:line` (one token ending in `:<digits>`, not
|
|
2026
|
+
a URL) must now be a canonical relative path to a regular file inside the repository loam.json was
|
|
2027
|
+
found in, with that line in it; otherwise `answers-unevidenced`, exit 1, where it used to be
|
|
2028
|
+
recorded. Every other entry — prose ("manual QA by Alice"), a URL, a path with no line — is
|
|
2029
|
+
recorded exactly as before. The form still binds to no commit and stamps no pins. Evidence that
|
|
2030
|
+
lives in another service's repository is recorded from there, with `--service`. A missing cited
|
|
2031
|
+
file is now named in the repository's own spelling ("does not exist in this repository") in both
|
|
2032
|
+
forms, never through the machine's absolute path.
|
|
2033
|
+
- No code, flag, path or payload key was added, changed or retired.
|
|
2034
|
+
|
|
2035
|
+
### Added — `loam gate --since <ref>`: the deploy gate no longer forgets a feature at archive
|
|
2036
|
+
|
|
2037
|
+
- **`loam gate` used to grade in-flight features only**, so a feature left the verification check
|
|
2038
|
+
the moment it was archived — and `loam archive` reads no verification record, by design. On the
|
|
2039
|
+
example fleet `gate --service order-service --strict` went from exit 1 (FEAT-112 unverified) to
|
|
2040
|
+
exit 0 after `loam archive FEAT-112`; a contract-only feature escaped even a vouched service,
|
|
2041
|
+
because `content_digest` covers the spec bodies and not `openapi.yaml`/`asyncapi.yaml`. Without
|
|
2042
|
+
the new flag that is still exactly what the gate does, and `--help`, WORKFLOW.md and
|
|
2043
|
+
`loam explain gate.feature-unverified` now say so.
|
|
2044
|
+
- **New flag `--since <ref>`** on `loam gate`: a docs-repo git ref — the docs commit the pipeline's
|
|
2045
|
+
last deploy recorded. Every feature archived after it that carries a `specs/<service>/` delta is
|
|
2046
|
+
graded too, by its frozen verification record, under the existing warn-only
|
|
2047
|
+
`gate.feature-unverified` (or the reused `verify.scenario-attested`). "Archived after" means its
|
|
2048
|
+
`features/archive/<FEAT>/` directory is absent at the ref, or differs from what it held there —
|
|
2049
|
+
committed since or not — so a feature unarchived, edited and archived again is graded as the new
|
|
2050
|
+
ship it is, not skipped as one an earlier deploy answered for. When git will not say what changed
|
|
2051
|
+
under `features/archive/`, every archived feature touching the service is graded. Its message
|
|
2052
|
+
names the ref and points at `loam unarchive <FEAT>` rather than a `loam verify --record` an
|
|
2053
|
+
archived feature refuses. The ref is read from the docs repo's own git exactly as
|
|
2054
|
+
`loam diff --base` reads its base, and refused the same way: `repository-unavailable` when git
|
|
2055
|
+
cannot answer from the docs repo (or cannot list `features/archive/` at the ref), `unknown-target`
|
|
2056
|
+
when the ref names no commit. No new code; no deployment state is stored.
|
|
2057
|
+
- **An archived record is held to the checklist its feature shipped with.** A record gone stale
|
|
2058
|
+
before the archive (a scenario appended after it was written) read `stale`/`unverified` in flight
|
|
2059
|
+
and `verified` with no finding once archived, because an archived record is never judged against
|
|
2060
|
+
a checklist re-derived after the merge. `loam archive` now writes the digest of the checklist a
|
|
2061
|
+
fresh `loam verify` derives just before the merge — when the feature carries a readable record —
|
|
2062
|
+
into its snapshot manifest as a new key, **`checklist`**, in
|
|
2063
|
+
`features/archive/<FEAT>/.loam-before/manifest.json` (additive: the manifest stays version 3,
|
|
2064
|
+
`unarchive` ignores the key, and an older loam reading the manifest is unaffected). `gate --since`
|
|
2065
|
+
grades the frozen record against it: a record answering an older checklist is `state: "stale"`,
|
|
2066
|
+
`verdict: "unverified"`, with a `gate.feature-unverified` saying the feature moved after its
|
|
2067
|
+
record was written. A manifest without the key — an archive made by an older loam — is graded as
|
|
2068
|
+
before, as frozen history, and the row says so. `list`, `status` and `verify` keep reading an
|
|
2069
|
+
archived record as frozen history, never stale.
|
|
2070
|
+
- **New `--json` payload keys:** top-level `since` — `{ ref, commit }` under `--since`, `null`
|
|
2071
|
+
without it — and, only on a row `--since` added, `features[].archived` (`true`) and
|
|
2072
|
+
`features[].checklistAtArchive` (the captured digest, or `null` when the archive captured none).
|
|
2073
|
+
An in-flight row carries neither key, so a payload without the flag is unchanged apart from
|
|
2074
|
+
`since: null`. The text view titles the block "verification — in flight, and archived since
|
|
2075
|
+
<ref>", marks those rows `· archived`, and adds a line under a recorded row whose archive
|
|
2076
|
+
captured no checklist.
|
|
2077
|
+
- **The MCP `loam_gate` tool** gains the matching `since` string argument.
|
|
2078
|
+
|
|
2079
|
+
## [0.2.0-alpha.8] - 2026-09-21
|
|
2080
|
+
|
|
2081
|
+
The work packet: the verify checklist read forwards. Still a prerelease on the `alpha` line.
|
|
2082
|
+
**Not published (noted 2026-10-10):** the `v0.2.0-alpha.8` tag was cut, but its release run failed at
|
|
2083
|
+
`npm ci` — npm on Windows had stripped other platforms' optional packages from the tagged commit's
|
|
2084
|
+
`package-lock.json` during the version bump (restored on `main` the same day) — so the publish never
|
|
2085
|
+
ran. `npm i @ybotok/loam@alpha` does not install this version, and `npm view @ybotok/loam dist-tags`
|
|
2086
|
+
is the record of what does. Everything in it is additive — no stable code was added,
|
|
2087
|
+
changed or retired, no existing flag, path or payload key moved, and nothing a repository has
|
|
2088
|
+
committed needs to change. What a repo NOTICES after upgrading: one new read-only command
|
|
2089
|
+
(`loam tasks`), new keys on two `--json` payloads it may already consume, and a corrected SCHEMA
|
|
2090
|
+
sentence about how scenario digests are salted — the code did not move, the sentence did.
|
|
2091
|
+
|
|
2092
|
+
### Added — `loam tasks`, the derived answer to "what do I do next, and what closes it"
|
|
2093
|
+
|
|
2094
|
+
The question an authored task file answers is a good one; the file is the wrong place to answer it,
|
|
2095
|
+
because a second mutable state drifts from the spec it was copied out of. So this is that answer
|
|
2096
|
+
DERIVED — the `loam verify` checklist read forwards, one work unit per promise the feature makes,
|
|
2097
|
+
each carrying the step it is waiting on and the fact that retires it, recomputed on every run from
|
|
2098
|
+
the delta, the generated suite and the verification record. SCHEMA's "Deliberate limits of the
|
|
2099
|
+
workflow surface" row refusing an authored `tasks.md` as authoritative state **stands unchanged**;
|
|
2100
|
+
this is the projection that row has always pointed at, and it is a projection precisely so that
|
|
2101
|
+
nothing here can disagree with the documents it was derived from.
|
|
2102
|
+
|
|
2103
|
+
- **New read-only command `loam tasks <FEAT> [--service <id>] [--next] [--json]`.** It writes
|
|
2104
|
+
nothing, anywhere, ever, and creates no artifact. Every unit it reports is one claim of the same
|
|
2105
|
+
checklist `loam verify` derives, **under the same id** — a unit and the answer that closes it are
|
|
2106
|
+
one identity on both commands. The one unit with no claim behind it is minted for an ADDED or
|
|
2107
|
+
MODIFIED requirement carrying no scenario at all (kind requirement.described): nothing to test
|
|
2108
|
+
means nothing for the checklist to ask, and a promise with no acceptance criteria is still work.
|
|
2109
|
+
- **`units[]` is the projection**, one entry per unit, carrying `id`, `kind`, `subject`, `at`,
|
|
2110
|
+
`claim`, `dependsOn[]`, a `done` block, and — where the ledger has them — `digest`, `operation`,
|
|
2111
|
+
`token` and a `source` naming the document, the 1-based line and, for a scenario, its body
|
|
2112
|
+
verbatim.
|
|
2113
|
+
- **`at` is the step the unit is waiting at**, one of `describe`, `generate`, `implement` or
|
|
2114
|
+
`attested` — never a percentage. It is spelled `at` rather than `stage` or `verdict` on purpose:
|
|
2115
|
+
both of those already mean something else on payloads an agent reads beside this one.
|
|
2116
|
+
- **Done is a fact, not a tick.** A closed unit stops appearing, and exactly one thing closes one: a
|
|
2117
|
+
claim recorded `confirmed` in a `verification.yaml` that is **not stale** against the current
|
|
2118
|
+
checklist. A stale record closes nothing — the words moved under the answers. An agent-attested
|
|
2119
|
+
scenario never vanishes: it stays visible at `attested`, because attested is not verified and a
|
|
2120
|
+
work list that hid the difference would be the gate that teaches everyone to say yes.
|
|
2121
|
+
- **`frontier` is the ids of the first ten open units in derived order**, providers before consumers
|
|
2122
|
+
on the exact token join `c4.calls.token == api.exposes.operation` — never a reading of prose —
|
|
2123
|
+
with `frontierElided` saying how many open units it left off. **The order is advice and never a
|
|
2124
|
+
lock**: nothing is assigned, nothing is reserved, and working out of order is legal and invisible
|
|
2125
|
+
to every other command.
|
|
2126
|
+
- **`--next` is the leaf packet**: the same payload with `units` narrowed to the frontier head, and
|
|
2127
|
+
what an orchestrator hands one coding agent. Everything else is left whole on purpose, so a cheap
|
|
2128
|
+
model handed one unit cannot read it as the whole job. The text view prints that leaf in full —
|
|
2129
|
+
the scenario's Given/When/Then verbatim and never reflowed (those are the bytes the digest was
|
|
2130
|
+
taken over), its digest, the `done` block whose `check` is the fact that closes the unit, and the
|
|
2131
|
+
literal follow-up command. Literal rather than a shape to fill in, because "the appropriate verify
|
|
2132
|
+
invocation" is a guess the reader will get wrong.
|
|
2133
|
+
- **`--service <id>` is a pure lens** over the same derivation, and adds `waitingOn[]`: the open
|
|
2134
|
+
units in OTHER services that this service's units depend on.
|
|
2135
|
+
- **`stamps` is `read` or `unknown`.** `unknown` means the bound service repo's generated suite could
|
|
2136
|
+
not be read — could-not-look, and never nothing-wrong.
|
|
2137
|
+
- **The rest of the payload**: `feature`, `service` (the lens, or null), `services[]`, `path`,
|
|
2138
|
+
`intent`, `checklist` (the digest of the question set the units derive from), `record` (`absent`,
|
|
2139
|
+
`recorded`, `stale` or `unreadable`), `size{requirements,scenarios,operations,messages,calls}` —
|
|
2140
|
+
the size of the DELTA and never of the open work, so that "this feature is small" and "this
|
|
2141
|
+
feature is nearly done" cannot become one sentence — `contracts{expose[],call[]}`,
|
|
2142
|
+
`counts{open,done,attested}`, and `review[]`, the four instructions for re-deriving this answer
|
|
2143
|
+
and reviewing it.
|
|
2144
|
+
- **A broken `delta.likec4` or contract axis exits 1 with `ok: true`**, naming what it could not read
|
|
2145
|
+
in `unreadable` (`architecture`, `contracts[]`). That is `loam delta`'s own rule rather than a new
|
|
2146
|
+
one: a feeder that could not be read empties the packet, an agent consuming an empty packet reads
|
|
2147
|
+
"nothing to do", so the envelope stays informative and true while the exit code stops a pipeline
|
|
2148
|
+
building on it.
|
|
2149
|
+
- **New MCP read-only tool `loam_tasks`**, declaring `readOnlyHint` like every other read tool; the
|
|
2150
|
+
base table is fifteen tools where it was fourteen.
|
|
2151
|
+
- **No stable code was added, changed or retired, and no existing flag, path or payload key moved.**
|
|
2152
|
+
The change is purely additive: everything `loam tasks` reports is a projection of state other
|
|
2153
|
+
commands already derive and grade.
|
|
2154
|
+
|
|
2155
|
+
### Added — `loam delta --json` carries the joins and the lines it had been dropping
|
|
2156
|
+
|
|
2157
|
+
- **New additive keys per requirement**: `line` (where it sits in the document it was parsed from, so
|
|
2158
|
+
a consumer can send an author to the line rather than to the file), `basedOn`, `requires`,
|
|
2159
|
+
`capabilities`, `realizes`, `publishes` and `consumes`. Every one of them is already written on the
|
|
2160
|
+
requirement and every one names work — which permission gates it, which capability it belongs to,
|
|
2161
|
+
which named promise it realizes, which messages it produces and consumes — and an implementer
|
|
2162
|
+
handed this projection had to go and read the Markdown it claimed to have projected to find them.
|
|
2163
|
+
- **New additive keys per scenario**: `line`, and `digest`, the 16-hex scenario body hash. That
|
|
2164
|
+
digest is the exact join key the rest of the toolchain already uses — the `@loam-digest-<16hex>`
|
|
2165
|
+
tag `loam gherkin` stamps and the string `loam verify --results` matches a cucumber report on — so
|
|
2166
|
+
a consumer of this brief can now name the test that answers an acceptance criterion instead of
|
|
2167
|
+
regenerating the suite to find out. It is salted by the owning service and the axis, like every
|
|
2168
|
+
other spelling of it, so an arch requirement's scenarios digest differently from identically
|
|
2169
|
+
worded business ones.
|
|
2170
|
+
- **Nothing existing moved.** Every key this payload already carried is untouched, the text output is
|
|
2171
|
+
unchanged, and no stable code was added or changed.
|
|
2172
|
+
|
|
2173
|
+
### Added — `loam gherkin --json` says which stamp belongs to which scenario
|
|
2174
|
+
|
|
2175
|
+
- **New additive key `files[].stamped`**, a list of `{scenario, digest}` pairs. `digests` was a bare
|
|
2176
|
+
array and `scenarios` a bare count, so the one thing a consumer needed — which stamp belongs to
|
|
2177
|
+
which scenario — was the one thing this payload could not say, and the only way to learn it was to
|
|
2178
|
+
read the generated file back off disk. A `kept` row reads its pairs off the file that is really on
|
|
2179
|
+
disk, because its numbers describe the in-flight emission that stayed rather than the one that was
|
|
2180
|
+
withheld.
|
|
2181
|
+
- **`digests` and `scenarios` are unchanged beside it**, and no stable code was added or changed: the
|
|
2182
|
+
envelope permits adding a key and never changing one.
|
|
2183
|
+
|
|
2184
|
+
## [0.2.0-alpha.7] - 2026-09-07
|
|
2185
|
+
|
|
2186
|
+
Infrastructure belongs on the fleet map, and the render is navigable at fleet scale. Still a
|
|
2187
|
+
prerelease on the `alpha` line: `npm i @ybotok/loam@alpha` installs it. One code is REMOVED —
|
|
2188
|
+
`landscape.datastore-private`, the first code this project has ever retired — and no flag, path or
|
|
2189
|
+
payload key is. What a repo NOTICES on its first `validate --all` after upgrading: a
|
|
2190
|
+
`subsystem.views-stale` error against the new `architecture/services.likec4`, in every fleet whose
|
|
2191
|
+
map draws a service, cleared by one `loam subsystem sync`; a second one against
|
|
2192
|
+
`architecture/subsystems.likec4` wherever subsystems exist, because those views gained boundary
|
|
2193
|
+
edges and an `of` subject; and silence where a datastore drawn beside the services used to be told
|
|
2194
|
+
to move.
|
|
2195
|
+
|
|
2196
|
+
**Upgrading: one `loam subsystem sync` per docs repo, once.** `loam validate --all` reports
|
|
2197
|
+
`subsystem.views-stale` (error) against the new `architecture/services.likec4` in EVERY existing
|
|
2198
|
+
docs repo whose fleet map draws a service — flat fleets with no subsystems included, because the
|
|
2199
|
+
file is new everywhere and an absent generated file is a state of its own. This is not a
|
|
2200
|
+
subsystems-only upgrade: a fleet that has never run `loam subsystem new` gets the finding too. A
|
|
2201
|
+
repo that DOES have subsystems gets a second finding on the same run, against
|
|
2202
|
+
`architecture/subsystems.likec4`, because the subsystem views gained boundary edges and an `of`
|
|
2203
|
+
subject and every existing copy therefore re-renders differently. One `loam subsystem sync` writes
|
|
2204
|
+
both files and clears both findings. `loam archive` names the same repair rather than claiming
|
|
2205
|
+
"complete + current". Nothing else in a repository has to change, and no document a team wrote is
|
|
2206
|
+
touched — and a document a team wrote that is already standing at one of the two generated paths is
|
|
2207
|
+
refused by name rather than overwritten (below).
|
|
2208
|
+
|
|
2209
|
+
### Removed — `landscape.datastore-private`, because where a store is drawn is the team's choice
|
|
2210
|
+
|
|
2211
|
+
- **`landscape.datastore-private` (warn) is retired.** It fired on a `database` element drawn at
|
|
2212
|
+
fleet level with exactly ONE consumer, and its whole message prescribed deleting the peer and
|
|
2213
|
+
writing the store inside its owner. The identical store nested was silent, so the check graded the
|
|
2214
|
+
DRAWING and not the architecture — a restriction rather than a fact about the fleet. A store one
|
|
2215
|
+
service owns may now be nested under that service's element or drawn beside the services, and loam
|
|
2216
|
+
grades neither. The code is not in the frozen `ErrorCode` or `IssueCode` unions, so removing it
|
|
2217
|
+
breaks no contract; `loam explain landscape.datastore-private` no longer answers, and the row is
|
|
2218
|
+
gone from the agent-facing check table.
|
|
2219
|
+
- **`landscape.datastore-shared` (warn) is unchanged**, both arms. Two or more services on one store
|
|
2220
|
+
is a fact about coupling, not a restriction, and it fires at two consumers for both placements
|
|
2221
|
+
alike. What survives from the retired code's message is the one clause that was a fact about
|
|
2222
|
+
LikeC4 rather than a preference, and SCHEMA's "Private stores" now leads with it: a second
|
|
2223
|
+
consumer's edge resolves only against a declaration the MAP holds, because a per-service project
|
|
2224
|
+
is `architecture/` plus one model.
|
|
2225
|
+
- **Infrastructure is not a service.** A new `INFRA_KINDS` set in `core/vocabulary/maturity.ts` —
|
|
2226
|
+
`database`, `queue`, `topic`, `cache`, `bucket`, compared case-insensitively — takes such an
|
|
2227
|
+
element out of four surfaces at once: the drawn-service census behind
|
|
2228
|
+
`landscape.service-undocumented`, the `loam gate` partner set (an infra endpoint is dropped from
|
|
2229
|
+
the `partners` payload, exactly as an actor already was), the `loam verify` checklist (a
|
|
2230
|
+
fleet-level store no longer mints a `service.exists` claim no repository can answer), and the
|
|
2231
|
+
scorecard's element denominator, whose `c4` row now counts services rather than services plus
|
|
2232
|
+
their stores. The hook is the kind NAME, with the same limitation `ACTOR_KINDS` lives with: a
|
|
2233
|
+
fleet spelling its kind `postgres` is not covered, and a tag can be added later as a second
|
|
2234
|
+
spelling. A store BOUND to a directory with `metadata { service }`, or titled like one, stays an
|
|
2235
|
+
adopted service on every one of the four surfaces — the census predicate wins over the kind.
|
|
2236
|
+
`landscape.service-undocumented`'s message gained a THIRD remedy beside "bind it" and "tag it
|
|
2237
|
+
`#external`", spelled from the same set: neither of the old two was true of a store the fleet
|
|
2238
|
+
owns, and that message is the only place an author learns which kind spellings loam reads.
|
|
2239
|
+
- `c4.uncovered` was left alone, and the measurement is the reason: a delta drawing
|
|
2240
|
+
`splitLedger = database 'split-ledger'` earns it, and adding `Covers: splitLedger` to the
|
|
2241
|
+
feature's `arch.spec.md` delta silences it exactly, with no `covers.unknown`. The remedy the
|
|
2242
|
+
message names is true, so this is not a residual restriction — and the code fires only on elements
|
|
2243
|
+
a feature DELTA adds, never from `validate --all` over a living tree.
|
|
2244
|
+
- The acceptance criterion, run for real: a `database` drawn at fleet level, owned by the fleet, with
|
|
2245
|
+
one consumer, no `#external` tag and no binding, now earns ZERO findings from `loam validate --all`
|
|
2246
|
+
and from `loam gate`.
|
|
2247
|
+
|
|
2248
|
+
### Added — `architecture/services.likec4`: one generated overview view per service
|
|
2249
|
+
|
|
2250
|
+
- **New generated path `architecture/services.likec4`**, written by `loam subsystem sync` beside
|
|
2251
|
+
`architecture/subsystems.likec4`, removed by it when the fleet map draws no service, and owned
|
|
2252
|
+
outright by loam: views only, byte-reproducible, one view per service the map resolves, sorted by
|
|
2253
|
+
service directory id. Each view is declared `of` its service's element, which is what makes the
|
|
2254
|
+
fleet map, every subsystem view and every other service view click-through into it with no
|
|
2255
|
+
authored link — measured at likec4 1.59.2. Peers are NAMED one by one rather than wildcarded,
|
|
2256
|
+
because on a map that groups its services the wildcard form collapses every peer to its group box
|
|
2257
|
+
and yields no drill-down at all (measured on a 120-service fleet: 0 of 10 nodes navigable). Median
|
|
2258
|
+
at 58 services in 11 groups: 10 nodes, 8 edges per view, about 7 ms of layout each.
|
|
2259
|
+
- **Titles are folders.** A per-service view is titled `<subsystem path> / <service directory id>`,
|
|
2260
|
+
built from the same path helper the subsystem views use, so both generated files fold into one
|
|
2261
|
+
browser folder tree mirroring `services/`. LikeC4 has no view-folder syntax — `views { group … }`
|
|
2262
|
+
is a parse error at the pin — and splitting the title on `/` is the whole mechanism.
|
|
2263
|
+
- **An authored view of the same element keeps precedence.** Two views `of` one element are legal;
|
|
2264
|
+
the first document alphabetically wins the default navigation target and both stay reachable,
|
|
2265
|
+
because the subject's own node cycles to the other. `architecture/landscape.likec4` sorts before
|
|
2266
|
+
`architecture/services.likec4`. loam does not detect the authored view and never will:
|
|
2267
|
+
docs/DESIGN.md rule 26 forbids reading what a view SHOWS.
|
|
2268
|
+
- **Subsystem views gained two things** in the same generator: `include <member> -> *, * -> <member>`
|
|
2269
|
+
per member, so a subsystem view answers "what does this group touch" and not only "who is in it",
|
|
2270
|
+
and `view subsystem_<name> of <ancestor>` when the map holds an element owning exactly that
|
|
2271
|
+
subsystem's members. Measured at 58/11: ~14 nodes, ~38 edges, 12 of 14 nodes navigable. Every
|
|
2272
|
+
existing `architecture/subsystems.likec4` therefore re-renders differently and is stale once.
|
|
2273
|
+
- **`subsystem.views-stale` now grades two files**, one finding per file, each naming its own path in
|
|
2274
|
+
`subject`, `locations[0]` and the message. No new code was minted: both files are written by one
|
|
2275
|
+
command in one commit and repaired by one run, which is what makes one code honest.
|
|
2276
|
+
- **`subsystem.view-id-collision` reserves `service_` as well as `subsystem_`.** A `service_` id is
|
|
2277
|
+
reserved only for a service the fleet map actually DRAWS, so an unmodelled `services/<id>/`
|
|
2278
|
+
reserves nothing; the message names which generated file and which prefix.
|
|
2279
|
+
- `deployment.doc-reserved` refuses a feature deployment document named `services.likec4` for the
|
|
2280
|
+
same reason it already refused `subsystems.likec4`: both names are generated, overwritten
|
|
2281
|
+
wholesale, and parsed by nothing.
|
|
2282
|
+
- **No writer overwrites a generated views file it did not write, and refusing is now an exit 1.**
|
|
2283
|
+
Both generated paths are filenames loam RESERVES inside `architecture/`, a directory the team
|
|
2284
|
+
owns, and every writer of them rewrites the file whole — so a repository that already kept a
|
|
2285
|
+
hand-authored diagram at one of those paths would have lost it, silently, on the first run after
|
|
2286
|
+
the release that reserved the name. Reproduced before the guard existed: with a hand-authored
|
|
2287
|
+
`architecture/services.likec4` in the tree, `loam subsystem new platform` printed
|
|
2288
|
+
`architecture/services.likec4 updated` and destroyed it, first line and all.
|
|
2289
|
+
- **What makes a file loam's is its FIRST LINE**, `// generated by loam`, with a leading byte-order
|
|
2290
|
+
mark stripped before the test — a Windows editor adds one without changing a fact, and without the
|
|
2291
|
+
strip loam would call its own output authored and refuse to regenerate it for good. A hand EDIT of
|
|
2292
|
+
a generated file keeps that line, so it is still loam's to repair and one `loam subsystem sync`
|
|
2293
|
+
puts it back: that has always been what `subsystem.views-stale` says. Only a file that never
|
|
2294
|
+
carried the mark is somebody else's. **A 0-byte file at either path counts as somebody else's** —
|
|
2295
|
+
it carries no mark — so an empty `architecture/subsystems.likec4` or
|
|
2296
|
+
`architecture/services.likec4` is no longer filled in; delete it and re-run.
|
|
2297
|
+
- **`loam subsystem sync` declines the one file and finishes the rest.** `--json` reports the new
|
|
2298
|
+
`action` value **`refused`** — on `action` for `architecture/subsystems.likec4`, on
|
|
2299
|
+
`serviceViews.action` for `architecture/services.likec4`, one predicate behind both — while the
|
|
2300
|
+
other generated file, the per-service project files and the root `exclude` are still done and
|
|
2301
|
+
still reported, and the **exit code is 1**. That is `loam validate`'s shape rather than a refusal
|
|
2302
|
+
envelope, and the non-zero exit is the point: `loam validate --all` goes on reporting
|
|
2303
|
+
`subsystem.views-stale` for that path with `loam subsystem sync` named as the repair, so a run in
|
|
2304
|
+
a hook or a CI step must not report success over a file it can never fix.
|
|
2305
|
+
- **`loam subsystem new` / `rm` / `move` / `rename` and `loam seed` refuse WHOLE**, before a byte is
|
|
2306
|
+
staged: `error.code` `already-exists`, exit 1, nothing written and — for `move` and `rename` —
|
|
2307
|
+
nothing renamed. No new code was minted, because that is what `already-exists` has always meant on
|
|
2308
|
+
these verbs; `loam seed` can answer it for the first time. They refuse whole rather than per-file
|
|
2309
|
+
because their entire contract is that the generated files land in the SAME commit as the change,
|
|
2310
|
+
so completing the structural half would manufacture the very `subsystem.views-stale` state that
|
|
2311
|
+
commit window exists never to leave behind — irreversibly, for a `move` that also stages renames.
|
|
2312
|
+
- **The refusal sentence is one sentence, verbatim from all five writers.** It names the mark loam
|
|
2313
|
+
looked for, says that not one byte changed, and gives the way out: rename the file to a name loam
|
|
2314
|
+
does not reserve — every other `.likec4` under `architecture/` renders in the same project, so the
|
|
2315
|
+
diagram is not lost — or delete it, then re-run.
|
|
2316
|
+
- **Payload keys, all additive.** `loam subsystem sync --json` keeps `path` and `action` meaning the
|
|
2317
|
+
subsystem file and adds `serviceViews: { path, action }`; `loam subsystem new` / `move` / `rm` /
|
|
2318
|
+
`rename --json` keep `views` and add `serviceViews`; `loam archive --json` keeps
|
|
2319
|
+
`subsystemViewsStale` and adds `staleViews: string[]`, the generated files it left behind. Nothing
|
|
2320
|
+
existing changed meaning.
|
|
2321
|
+
- The generated-file SET is now one table (`GENERATED_ARCHITECTURE_FILES`), read by the path
|
|
2322
|
+
builders, the name reservation, both project loads that must EXCLUDE these files (the fleet map's
|
|
2323
|
+
and `c4.fleet-project-invalid`'s), archive's staging and `loam seed`'s. Four readers that must
|
|
2324
|
+
never disagree about which files are generated, and a name in one but not another failed silently
|
|
2325
|
+
both ways while there was only one.
|
|
2326
|
+
|
|
2327
|
+
### Added — the intra-service use-case slot, published rather than graded
|
|
2328
|
+
|
|
2329
|
+
- **`loam adopt` briefs `services/<…>/<id>/usecases/<name>.likec4` as an OPTIONAL target**, with the
|
|
2330
|
+
tag grammar and a worked example: a `dynamic view` tagged `#req-<Requirement-ID>` as the first
|
|
2331
|
+
thing in the view body, the tag declared once (in the landscape's `specification` for an extending
|
|
2332
|
+
model, in the model's own for a standalone one), every hop backed by a relationship the model
|
|
2333
|
+
declares, and the view id namespaced by the service because an extending model's views share the
|
|
2334
|
+
root project's flat id namespace. Optional-and-absent prints lowercase `missing` and is not a
|
|
2335
|
+
finding — the existing mechanism for an optional artifact, not a new one.
|
|
2336
|
+
- **The generated AGENTS.md names the slot and its title convention**,
|
|
2337
|
+
`'<subsystem path> / <service id> / <use case>'`, so an authored use-case view lands in the same
|
|
2338
|
+
browser folder as its service's generated overview.
|
|
2339
|
+
- **No new finding code, and no check on absence.** docs/DESIGN.md rule 26 — presence is never owed,
|
|
2340
|
+
and no check may grade a view's absence — is enforced by `scripts/arch-check.mjs`, so there is no
|
|
2341
|
+
service.usecases-missing and there never will be. The grader (`c4.valid`'s
|
|
2342
|
+
` · K tagged flow(s) graded`) has existed since 2026-09-03; all that was missing was anyone being
|
|
2343
|
+
told. A usecase.flow-untagged check, which would grade a view that EXISTS, is recorded on
|
|
2344
|
+
[ROADMAP.md](ROADMAP.md) with its promotion bar rather than built.
|
|
2345
|
+
|
|
2346
|
+
## [0.2.0-alpha.6] - 2026-09-04
|
|
2347
|
+
|
|
2348
|
+
A service model may extend the fleet map, and the nine problem reports one fleet filed against the
|
|
2349
|
+
previous release are closed. Still a prerelease on the `alpha` line: `npm i @ybotok/loam@alpha`
|
|
2350
|
+
installs it. Nothing below removes a code, a flag or a payload key; two things a script may have
|
|
2351
|
+
read do move: every LikeC4 line number loam prints is 1-based now, and seven feature-delta codes
|
|
2352
|
+
plus the feature arm of `usecase.flow-invalid` answer `--feature <FEAT-id>` in
|
|
2353
|
+
`loam explain --json` where they answered `--service <id>`. What a repo NOTICES on its first
|
|
2354
|
+
`validate --all` after upgrading: a model written in the extending shape earns the renderer-wiring
|
|
2355
|
+
warnings until one `loam subsystem sync` — which now also deletes a stray per-service
|
|
2356
|
+
`likec4.config.json` beside such a model — a root `exclude` that hides the map is
|
|
2357
|
+
`landscape.excluded`, and a standalone model whose copies of the map's elements disagree with it
|
|
2358
|
+
is `c4.declaration-diverged`; a repository whose models all stand alone is asked to migrate
|
|
2359
|
+
nothing, and its root `exclude` is not touched.
|
|
2360
|
+
|
|
2361
|
+
### Fixed — one rule for a service's own edge, and the document that actually failed, on every surface
|
|
2362
|
+
|
|
2363
|
+
- `loam context` and `loam explore` name the `architecture/` document that ACTUALLY failed in their
|
|
2364
|
+
printed warning, off the `landscape.broken` key their `--json` payloads already carried. `parses`
|
|
2365
|
+
answers for the PROJECT, so with a broken sibling the landscape file has no errors at all — and
|
|
2366
|
+
both printers still said `architecture/landscape.likec4 does not parse`, a claim about bytes loam
|
|
2367
|
+
had read fine. `explore`'s absent arm keeps the file name: `present` is about the FILE, which is
|
|
2368
|
+
the write target every brief asks for edits to.
|
|
2369
|
+
- An edge whose two endpoints resolve to the SAME service is that service's own internal wiring,
|
|
2370
|
+
and no surface files it as an inbound call any more. `loam adopt --json` already dropped it; the
|
|
2371
|
+
one shared derivation does now, so `loam context`, `loam explore`, `loam show` and
|
|
2372
|
+
`loam vouch --pack` stopped reporting a service as its own caller (`← <id> "reads"`), and
|
|
2373
|
+
`loam list`'s fleet-wide spelling of the same rule stopped reading an `op`-carrying self-edge as
|
|
2374
|
+
proof that somebody calls the service — a service nobody calls was told it owed an OpenAPI
|
|
2375
|
+
contract, grading `partial` in `list` while `context` said `documented`. `landscape.touched` and
|
|
2376
|
+
`landscape.service-isolated` are unchanged: they read the relationships, so the map still draws
|
|
2377
|
+
an edge on the service.
|
|
2378
|
+
- The adopt brief's `service.model-excluded` row describes both entry shapes: one covering the
|
|
2379
|
+
DIRECTORY, which `loam subsystem sync` rewrites, and one shaped like a FILE
|
|
2380
|
+
(`services/**/model.likec4`), which sync cannot repair and has to be removed or narrowed by hand.
|
|
2381
|
+
The finding's own message already said which one you have; the brief promised sync for both.
|
|
2382
|
+
- `loam diff` classifies a base ref's tree with the same reserved-directory rule the live walk uses
|
|
2383
|
+
(`adrs/`, `usecases/`, `ui/` — shared as one set, not respelled). A service directory holding only
|
|
2384
|
+
the `services/<id>/usecases/` slot used to be absent from the base AND to contribute a phantom
|
|
2385
|
+
service called `usecases`, so committing it and diffing the unchanged tree reported one addition
|
|
2386
|
+
and one removal.
|
|
2387
|
+
|
|
2388
|
+
### Added — one model, many documents: a service model may extend the fleet map
|
|
2389
|
+
|
|
2390
|
+
- `services/<…>/<svc>/model.likec4` now comes in two shapes, and the FILE's own grammar decides
|
|
2391
|
+
which — never a config key, never a flag. A model declaring no `element` kind inside a top-level
|
|
2392
|
+
`specification { }` block is an **extending model**: loam parses it inside the `architecture/`
|
|
2393
|
+
project and it says what is inside the service with `extend <fully-qualified id> { … }` on the
|
|
2394
|
+
element the map already binds to the directory, re-declaring no kind, no tag and no partner. A
|
|
2395
|
+
model that declares one is a **standalone model**, parsed alone exactly as every model was
|
|
2396
|
+
before — legal forever, and not what `loam adopt` briefs any more. SCHEMA.md's new
|
|
2397
|
+
"Two shapes of a service model" carries both, the grades below, and the ordered migration recipe.
|
|
2398
|
+
- New `c4.declaration-diverged` (warn, service target, subject the service, one finding per element):
|
|
2399
|
+
a STANDALONE model and the fleet map (`architecture/`) both declare one element, and the
|
|
2400
|
+
two copies disagree about `kind`, `title`, the tag SET or the `metadata { service }` binding. The
|
|
2401
|
+
message prints both declarations, names which fields differ, and offers the two repairs — copy the
|
|
2402
|
+
map's declaration verbatim, or migrate to the extending shape. `description` is deliberately not
|
|
2403
|
+
compared: it is prose nothing grades, while the other four each change what a view shows or what
|
|
2404
|
+
loam resolves. Silent while the map does not parse. This is the check that had no code: a fleet's
|
|
2405
|
+
copies of `kafka` and its identity provider had already lost `#platform` on every copy but the
|
|
2406
|
+
map's, and nothing in loam read two documents together.
|
|
2407
|
+
- New `c4.element-unowned` (warn, service target, subject the service, one per element): an
|
|
2408
|
+
EXTENDING model declares an element outside the element that resolves to its own service. A system
|
|
2409
|
+
the service reaches belongs in `architecture/landscape.likec4`, declared once; another service's
|
|
2410
|
+
internals belong in that service's model.
|
|
2411
|
+
- New `c4.fleet-project-invalid` (warn, `landscape` target, subject the service whose directory
|
|
2412
|
+
holds the file): `--all` only, only when at least one extending model exists, the map itself
|
|
2413
|
+
parses, and no root `exclude` entry hides the map (`landscape.excluded` below is then the whole
|
|
2414
|
+
answer). loam loads every `.likec4` the root LikeC4 project reads except the generated
|
|
2415
|
+
`architecture/subsystems.likec4` — the map, its siblings, every extending model and every
|
|
2416
|
+
`.likec4` beside one — and reports each error that is NOT already present in the map alone, in a
|
|
2417
|
+
per-service project, or in a service-flow project the same run made. That difference is the class
|
|
2418
|
+
no per-service grade can see: two services each declaring `tag req-AUTH` locally, two models
|
|
2419
|
+
adding the same child id under one element, a broken untagged sibling nobody opted in. All three
|
|
2420
|
+
are renderer-fatal and all three were silent.
|
|
2421
|
+
- Three new renderer-wiring warnings on the `landscape` target, root-gated like
|
|
2422
|
+
`service.likec4-config-missing` and repaired by the same `loam subsystem sync`:
|
|
2423
|
+
`service.likec4-config-stray` (a `likec4.config.json` beside an EXTENDING model — the renderer
|
|
2424
|
+
registers a project holding nothing, or holding the model alone where it cannot parse),
|
|
2425
|
+
`service.model-excluded` (the root `likec4.config.json`'s `exclude` covers an extending model's
|
|
2426
|
+
directory, naming the entry that does it, so the renderer never loads the model) and
|
|
2427
|
+
`service.model-unexcluded` (it does NOT cover a STANDALONE model's directory, so the renderer
|
|
2428
|
+
merges that model into the map and reports every kind and element it declares as a duplicate —
|
|
2429
|
+
blanking the whole root project, not one service).
|
|
2430
|
+
- New `landscape.excluded` (warn, `landscape` target, `loam validate --all`) — the root
|
|
2431
|
+
`likec4.config.json`'s `exclude` covers `architecture/landscape.likec4` itself. loam keeps
|
|
2432
|
+
grading the map whatever that list says, so every finding in the run is right; the renderer does
|
|
2433
|
+
not, and until now nothing said so. A repo notices it as one line at the top of `validate --all`
|
|
2434
|
+
naming the entry verbatim — typically an `architecture/*.likec4` written for a palette — INSTEAD OF
|
|
2435
|
+
the `c4.fleet-project-invalid` cascade the same entry used to produce, and it is the only line in
|
|
2436
|
+
the run that names the cause. `c4.fleet-project-invalid` is not graded while the entry stands: that
|
|
2437
|
+
check loads the fleet project the way the renderer does, with no floor under the map, so the one
|
|
2438
|
+
line came back as an unresolved `extend` in every extending model — 161 warnings on the packaged
|
|
2439
|
+
five-service example. Its message says the fleet project is unread rather than clean, and grading
|
|
2440
|
+
resumes the moment the entry goes. Never gating: with the map kept by the loader, every fact loam grades holds
|
|
2441
|
+
identically either way. The repair is the team's own edit — `loam subsystem sync` maintains only
|
|
2442
|
+
the `services/` entries and will not touch it, and the message says so rather than sending its
|
|
2443
|
+
reader to a command that reports success and changes nothing.
|
|
2444
|
+
- `loam subsystem sync` now maintains the root `likec4.config.json`'s `exclude` as well as the
|
|
2445
|
+
per-service files. It rewrites the list to hold `services/<tree>/**` for exactly the models that
|
|
2446
|
+
stand alone: the wholesale `services/**` and every `services/<tree>/**` naming a directory that IS
|
|
2447
|
+
a service are removed, every other entry is kept in the order the team wrote it (a
|
|
2448
|
+
`services/legacy/**` for a directory that is no service survives), and the standalone entries are
|
|
2449
|
+
appended sorted. It runs when at least one extending model exists or when a standalone model's
|
|
2450
|
+
directory is not covered, so a repository whose models all stand alone behind `services/**` is
|
|
2451
|
+
never touched. Same staged transaction as the rest of `sync`; `--json` gains the additive
|
|
2452
|
+
`projects.exclude` (whether it rewrote, the resulting entries, and what was added and removed) and
|
|
2453
|
+
the text view prints one line when it did. A root config that is not JSON is left alone with a
|
|
2454
|
+
printed note.
|
|
2455
|
+
- `loam init --docs <path> --create` now scaffolds `likec4.config.json` with
|
|
2456
|
+
`exclude: ["**/node_modules/**", "features/**"]`. `services/` is deliberately not excluded — the
|
|
2457
|
+
models a fresh repository writes extend the map, and the root project is the only place they
|
|
2458
|
+
parse. A feature's `delta.likec4` keeps its own `specification` block and stays excluded.
|
|
2459
|
+
- The landscape the scaffold writes teaches the extending shape. Its `views` paragraph told a fresh
|
|
2460
|
+
repository that a service model "renders from its OWN directory" and that "each declares its own
|
|
2461
|
+
`specification` block, so the renderer can only be given one of them at a time" — written before
|
|
2462
|
+
an extending model belonged to the root project, and printed in the same `loam init --docs .
|
|
2463
|
+
--create` run that now excludes `features/**` precisely because it does. And the check that says
|
|
2464
|
+
a map is still untouched scaffolding now accepts EVERY generation loam has shipped, not just the
|
|
2465
|
+
current text: it is a byte-suffix match, so rewording the comment reclassified every older
|
|
2466
|
+
untouched scaffold as hand-authored — `loam seed` refused one its own scaffold had written
|
|
2467
|
+
(`seed-landscape-edited`, nothing written) and `loam status` dropped the teaching rung from the
|
|
2468
|
+
empty-fleet ladder. The two `v0.1.0-beta` stubs (no `views` block, no `tag platform`) and the
|
|
2469
|
+
`v0.2.0-alpha.1`-through-`.5` stub are all recognised, frozen beside the writer.
|
|
2470
|
+
- `loam adopt --service <id>` briefs the extending shape: all four `shape` rules of the
|
|
2471
|
+
`model.likec4` target are rewritten, and the `example` is a `model { extend <id> { … } }` document
|
|
2472
|
+
with the fleet element's real id substituted in where the brief resolves exactly one. The brief's
|
|
2473
|
+
`checks[]` gains `c4.declaration-diverged`, `c4.element-unowned`, `service.likec4-config-stray`,
|
|
2474
|
+
`service.model-excluded`, `service.model-unexcluded` and `c4.fleet-project-invalid`, and its
|
|
2475
|
+
renderer statement in `unchecked[]` says the root project now holds the map and every extending
|
|
2476
|
+
model. `/loam-adopt` steps 3, 4 and 5 and `/loam-check`'s three fix tables carry the same.
|
|
2477
|
+
- The packaged example fleet (`examples/docs`, what `loam init --example` copies out) is migrated:
|
|
2478
|
+
all five models extend the map, the five `services/**/likec4.config.json` are gone, the root
|
|
2479
|
+
`exclude` is the scaffold's two entries, `architecture/landscape.likec4` gains `element database`
|
|
2480
|
+
and the `notification-provider` system that notification-service's model used to declare for
|
|
2481
|
+
itself, and every `Covers:` entry and `views` block names the map's fully-qualified ids. Its
|
|
2482
|
+
`loam validate --all` result is unchanged: 0 errors and the same ten deliberate warnings.
|
|
2483
|
+
- `loam archive` now writes a service's OWN `model.likec4`, not just the fleet map. A delta's
|
|
2484
|
+
additions nested under a service whose model EXTENDS the map — a container, a store, a
|
|
2485
|
+
container-level edge — are spliced into that model's `extend <fqn> { … }` block instead of into
|
|
2486
|
+
`architecture/landscape.likec4`, and a model with no `extend` block for that fqn gains one. A
|
|
2487
|
+
relationship goes to the model of the service owning its SOURCE, else its TARGET, else stays on
|
|
2488
|
+
the map: a service-level edge is where every fleet check reads it. A service-level element, a
|
|
2489
|
+
STANDALONE model and a service with no model.likec4 keep the previous behaviour byte for byte.
|
|
2490
|
+
What a repo notices: `loam archive` prints a second
|
|
2491
|
+
`architecture: merged into services/<…>/model.likec4 — +N element(s), +M relationship(s)` line
|
|
2492
|
+
beside the landscape one, the model path appears in the `--json` plan (and in `--dry-run`) as an
|
|
2493
|
+
ordinary write, `loam unarchive` restores it from the same `.loam-before` snapshot as any other
|
|
2494
|
+
overwritten file, and the service is no longer `c4.invalid` (duplicate declaration) the moment its
|
|
2495
|
+
model declares the container the adopt brief told it to declare. No new payload key and no new
|
|
2496
|
+
error code. Re-archiving adds nothing a second time — existence is checked against the models too
|
|
2497
|
+
— and two archives touching one model's `extend` block land adjacently.
|
|
2498
|
+
- The archive's existence check now reads the extending models by TITLE as well as by id, so a
|
|
2499
|
+
container a service already draws under a different identifier is recognised instead of being
|
|
2500
|
+
spliced in beside itself: a delta adding `cache = container 'Order cache'` under a service whose
|
|
2501
|
+
model already declares `orderCache = container 'Order cache'` adds nothing and leaves the model
|
|
2502
|
+
byte for byte, which is what the same delta against the same title on the fleet map has always
|
|
2503
|
+
done. Nothing is routed to that model, so the plan names only the landscape — the second
|
|
2504
|
+
`merged into services/<…>/model.likec4` line appears when a model actually receives something.
|
|
2505
|
+
The models' title join is scoped to one service — it fires only when both the model's
|
|
2506
|
+
declaration and the addition resolve to the SAME explicitly bound service — because every
|
|
2507
|
+
extending model in a fleet names a container `api`, and the map's global title index is a
|
|
2508
|
+
pre-binding fallback, not a rule to spread.
|
|
2509
|
+
- An extending model with no `model { }` block — an empty file, a `views { }`-only file, a tags-only
|
|
2510
|
+
`specification { }`, all three shapes loam calls extending on purpose — gains one when a feature's
|
|
2511
|
+
addition is routed into it, instead of refusing the whole archive with `merge-failed`. It is the
|
|
2512
|
+
same call the merge already makes one level down, where a model that has never named a container's
|
|
2513
|
+
parent gains the `extend <fqn> { }` block rather than refusing.
|
|
2514
|
+
- `loam archive` proves the model it would write. The merged model is loaded as the project the
|
|
2515
|
+
per-service grade reads it in — the `architecture/` documents with this archive's own merged
|
|
2516
|
+
landscape in place, plus the model — and an archive whose model would not parse is refused at plan
|
|
2517
|
+
time (`merge-failed`) with nothing written, naming the model path and the first three errors. Two
|
|
2518
|
+
causes actually occur, and the refusal names both: a kind or tag the map's `specification` block
|
|
2519
|
+
does not declare, and a reference to ANOTHER service's interior, which lives in that service's own
|
|
2520
|
+
model and is in no project this one can see. A container-level edge whose two ends belong to two
|
|
2521
|
+
different services is the case that changes: it used to merge, because both containers landed on
|
|
2522
|
+
the map beside it, and it is now refused — there is no legal placement for it, and the answer is
|
|
2523
|
+
to draw that call service to service.
|
|
2524
|
+
- What a repo notices: nothing, until a model is written in the new shape or `loam subsystem sync`
|
|
2525
|
+
is run. A repository whose models all stand alone keeps every finding it had, and its root
|
|
2526
|
+
`exclude` is not touched. Write one extending model — or upgrade a repo whose scaffold predates
|
|
2527
|
+
`services/**` being excluded — and the next `loam validate --all` reports the renderer-wiring
|
|
2528
|
+
warnings above until one `sync`; that one sync rewrites the root `exclude`, writes the project
|
|
2529
|
+
file a standalone model is owed and DELETES the stray one beside every extending model, so after
|
|
2530
|
+
it the renderer's project picker holds one entry instead
|
|
2531
|
+
of one per adopted service, and a fleet-wide duplicate that used to blank the whole picture is
|
|
2532
|
+
reported as `c4.fleet-project-invalid` before anybody opens the renderer.
|
|
2533
|
+
|
|
2534
|
+
### Changed — a model's grades follow its shape, and `--service` reads the whole map project
|
|
2535
|
+
|
|
2536
|
+
- `service.likec4-config-missing` is narrowed to STANDALONE models. A model that extends the map
|
|
2537
|
+
belongs to the root project, so it is owed no per-service `likec4.config.json` and never earns
|
|
2538
|
+
this warning; the same directories earn `service.likec4-config-stray` instead while a file left
|
|
2539
|
+
over from before the migration is still sitting there, and `loam subsystem sync` — which now also
|
|
2540
|
+
stops writing the file for extending models — is the repair for both. The message is reworded to
|
|
2541
|
+
say which shape it is about.
|
|
2542
|
+
- `loam validate --service <id>` now reads the whole `architecture/` project, exactly as `--all`
|
|
2543
|
+
does, instead of `architecture/landscape.likec4` alone. A use-case file under `architecture/` that
|
|
2544
|
+
does not parse therefore reports `spine.landscape-invalid` on a single-service run too, where it
|
|
2545
|
+
used to be visible only under `--all`. `c4.declaration-diverged` compares against that same
|
|
2546
|
+
project.
|
|
2547
|
+
- `c4.invalid` and `c4.valid` keep their codes and change scope with the shape. For an extending
|
|
2548
|
+
model every error the per-service project raises is the MODEL's — including one the parser blames
|
|
2549
|
+
on `architecture/landscape.likec4`, because the map parses clean on its own — and `c4.valid`'s
|
|
2550
|
+
message reads `extends the fleet map — N elements · M relationships`. When the map itself does not
|
|
2551
|
+
parse, an extending model cannot be read at all: neither code is emitted for it, and
|
|
2552
|
+
`spine.landscape-invalid` carries one clause more saying the model extends the map and so cannot
|
|
2553
|
+
be read either.
|
|
2554
|
+
- `landscape.service-unmodelled` is raised on the `--service` target too, and stays an error. An
|
|
2555
|
+
EXTENDING `model.likec4` says what is inside an element the fleet map binds, so a service whose
|
|
2556
|
+
directory no map element resolves to has nothing to be inside and no count in the model means
|
|
2557
|
+
anything: `loam validate --service <id>` reports it and exits 1 where the same tree used to come
|
|
2558
|
+
back `c4.invalid` for a model read alone. `/loam-check`'s `--service` table carries a row of its
|
|
2559
|
+
own for it and `loam explain landscape.service-unmodelled` answers for both scopes. A standalone
|
|
2560
|
+
model is unaffected — the fleet run still owns that state.
|
|
2561
|
+
- `subsystem.view-id-collision`'s census covers every extending model's authored view ids under
|
|
2562
|
+
`--all`, not the landscape's alone: those documents are in the same root project now, and a view
|
|
2563
|
+
id claimed twice there makes the renderer refuse the whole project.
|
|
2564
|
+
- `spine.landscape-invalid` names the document that actually broke. The finding is raised against
|
|
2565
|
+
the whole `architecture/` PROJECT and the message hardcoded `architecture/landscape.likec4`, so an
|
|
2566
|
+
`architecture/usecases/x.likec4` with a typo sent every service's reader to a file that parses and
|
|
2567
|
+
the real path appeared nowhere in the payload. The message now lists the broken documents
|
|
2568
|
+
repo-relative and comma-separated — the spelling `landscape.invalid` already used under `--all` —
|
|
2569
|
+
each `details` line carries its own file once two or more are broken, and `locations` carries the
|
|
2570
|
+
first broken file as `primary` beside the service directory as `scope`. `loam explain`'s "fix the
|
|
2571
|
+
file the message names" is true now.
|
|
2572
|
+
- `c4.declaration-diverged` finds the copies it was written for. It joined on the literal element
|
|
2573
|
+
id, so in a GROUPED map — one nesting its services under a parent, as loam's own example does — a
|
|
2574
|
+
standalone model's `paymentService` never met the map's `marketplace.paymentService` and the check
|
|
2575
|
+
was silent on the shape it exists for. The join is now the id; then the `metadata { service }`
|
|
2576
|
+
binding when both documents wrote one; then the map element whose id ends with the model's as a
|
|
2577
|
+
dotted tail. A tail two map elements answer to joins nothing — a divergence reported against the
|
|
2578
|
+
wrong peer is worse than the silence. The message names the other side as `the fleet map
|
|
2579
|
+
(architecture/)` rather than a file it never read, and names the map's own id when it differs.
|
|
2580
|
+
- `c4.declaration-diverged` no longer joins a standalone model's `db`, `api` or `agent` to a
|
|
2581
|
+
container the map draws INSIDE another service. The dotted-tail rung stands, and a tail that
|
|
2582
|
+
carries a dot (`paymentService.api` ↔ `marketplace.paymentService.api`) is joined as before; the
|
|
2583
|
+
BARE last segment is now a key only for a FLEET-LEVEL map element — one no ancestor of which
|
|
2584
|
+
stands for a service, which is a top-level element and equally a box the map draws inside a plain
|
|
2585
|
+
grouping element, by the same fleet-census predicate every other fleet check asks. Grouping is a
|
|
2586
|
+
drawing convenience, not ownership: a `cache` the map draws inside an `infra` group is the FLEET's
|
|
2587
|
+
cache, so a standalone model that declares its own top-level `cache` gives one name two meanings
|
|
2588
|
+
and is reported. Only a SERVICE owns an interior. What a repo notices: a private cache called
|
|
2589
|
+
`db` beside another service's `db` was warned about and told to copy that service's title
|
|
2590
|
+
verbatim; it is now silent. loam's own `meta/docs` carried the workaround for it, and no longer
|
|
2591
|
+
does — its `coding agent` element is spelled `agent` again.
|
|
2592
|
+
- `c4.declaration-diverged` stops asking for a copy that is already there — in both directions.
|
|
2593
|
+
Since LikeC4 1.59.0 a kind can carry tags and every element of that kind arrives with them, so a
|
|
2594
|
+
tag difference can be one no element declaration writes and no copy can fix. loam now decides that
|
|
2595
|
+
by SIMULATING the copy rather than by matching one shape of it: when the tag sets are all that
|
|
2596
|
+
differ and copying the map's literal tags across would leave them still differing, the message
|
|
2597
|
+
says so, names `specification { element <kind> { … } }` as where they come apart, and says WHICH
|
|
2598
|
+
document declares tags on that kind, so an author knows which file to open. Both shapes reach it
|
|
2599
|
+
now: the model writing `#external` on an element whose map twin inherits `#external #platform`
|
|
2600
|
+
from `element topic { … }` used to be told to copy the map's bare line, and doing exactly that
|
|
2601
|
+
produced the other arm of the same message (measured on `examples/docs`' order-service). Grammar:
|
|
2602
|
+
"the kind differs" / "the title differs" / "the tags differ".
|
|
2603
|
+
- Three messages say what they could not say before. `c4.invalid` on a model written in BOTH shapes
|
|
2604
|
+
at once — an element kind declared in `specification { }` beside an `extend` — gains one `details`
|
|
2605
|
+
line saying the kind is what makes the file standalone, so the `extend` resolves nothing, and
|
|
2606
|
+
which of the two lines to drop. `usecase.flow-invalid` whose project refuses an undeclared
|
|
2607
|
+
`#req-`/`#cap-` tag gains one line naming the `tag <name>` declaration that is missing and the
|
|
2608
|
+
`specification { }` block it belongs in. And `c4.valid` gains ` · K tagged flow(s) graded` when
|
|
2609
|
+
the service's own project declared any, so a HEALTHY intra-service flow is visible on one surface
|
|
2610
|
+
instead of none. Both hints are the FIRST `details` line rather than the last: the cap is ten
|
|
2611
|
+
lines in text and in `--json`, and a model written in both shapes at once produces dozens of
|
|
2612
|
+
cascading errors, so the one line that diagnosed it was truncated away in exactly the case it
|
|
2613
|
+
exists for.
|
|
2614
|
+
- A `.likec4` beside `model.likec4` that the service's own `likec4.config.json` `exclude` covers is
|
|
2615
|
+
no longer read, graded or reported. loam staged every sibling regardless, so a file the renderer
|
|
2616
|
+
never loads earned `usecase.flow-invalid` under a message claiming the project is read the way the
|
|
2617
|
+
renderer reads it. Standalone models only — that file IS the project config there; a model that
|
|
2618
|
+
extends the map is read in the root project, and a per-service config beside it is
|
|
2619
|
+
`service.likec4-config-stray`. The RECURSIVE spelling counts too: a leading `**/` compiled to a
|
|
2620
|
+
required `/`, so `**/usecases/**`, `**/views.likec4` and `**/*.likec4` matched nothing while the
|
|
2621
|
+
renderer (measured, likec4 1.59.2) hides exactly what they name — `usecase.flow-invalid` gated the
|
|
2622
|
+
run at exit 1 over a file in no project loam was reading.
|
|
2623
|
+
- `spine.landscape-invalid` says "a, b have N error(s)" when it names more than one document; it
|
|
2624
|
+
read "a, b has N error(s)".
|
|
2625
|
+
- `c4.element-unowned` leads with the remedy that fits a private store. The message listed the two
|
|
2626
|
+
remedies that move an element OUT of the service — the map, or another service's model — and
|
|
2627
|
+
omitted the one an author usually wants: a store or component this service owns goes INSIDE the
|
|
2628
|
+
`extend <fqn> { }` block, where its id becomes `<fqn>.<name>`. Following what the tool printed
|
|
2629
|
+
moved a private store to fleet level and earned `landscape.service-undocumented`.
|
|
2630
|
+
|
|
2631
|
+
### Added — problem reports carry an ordinal and a status, and `loam doctor` counts them
|
|
2632
|
+
|
|
2633
|
+
- The `loam-report` protocol (`loam instructions loam-report`) names its file
|
|
2634
|
+
`loam-reports/NNN-YYYY-MM-DD-<short-slug>.md`: `NNN` is an ordinal of at least three digits,
|
|
2635
|
+
zero-padded, one
|
|
2636
|
+
more than the highest already in the directory, `001` over an empty or absent one, widening past
|
|
2637
|
+
three digits rather than reusing a number, and the
|
|
2638
|
+
protocol tells the agent to read it from `loam doctor --json` rather than count the directory
|
|
2639
|
+
itself. A path that already exists is a concurrent report claiming that ordinal — take the next
|
|
2640
|
+
one; the collision suffix is gone. That check is on the PATH while the promise is about the
|
|
2641
|
+
ordinal, so the protocol also tells the agent to re-run `doctor` after writing: two entries
|
|
2642
|
+
sharing an ordinal means the later `Recorded` renames to the next free one.
|
|
2643
|
+
The directory is `loam-reports/` beside the `loam.json` that resolved
|
|
2644
|
+
(else beside the directory the agent runs in) — `reports.dir` carries it WITH that segment on the
|
|
2645
|
+
end, so it is written into rather than joined onto — the ordinal is unique within it, and a
|
|
2646
|
+
report is cited by
|
|
2647
|
+
ordinal — in a commit message, an issue or another report's cross-references — never by filename.
|
|
2648
|
+
The document template gains `- Status: open` directly after `- Classification:`, with the
|
|
2649
|
+
vocabulary `open` / `sent` (handed to the author — the person forwarding a report sets it, never
|
|
2650
|
+
the agent writing it) / `fixed in <version>` / `superseded by <NNN>`; a report is closed by
|
|
2651
|
+
editing its own `Status` line, and there is no index to keep in step with it. Every safety
|
|
2652
|
+
sentence is unchanged: `<redacted>`, "Never re-run" a writer, "do not upload", and the section
|
|
2653
|
+
headings.
|
|
2654
|
+
- `loam doctor --json` gains one additive key, `reports`: `reports.dir` (the absolute path of the
|
|
2655
|
+
`loam-reports` directory beside the `loam.json` that resolved — and, when a `loam.json` was FOUND
|
|
2656
|
+
but could not be parsed, beside that file, which is the same root `agents` is inspected against;
|
|
2657
|
+
only where no config file exists at or above the run directory does it fall back to the run
|
|
2658
|
+
directory. The last segment is ON it, so the protocol writes inside the path rather than
|
|
2659
|
+
joining the name a second time), `reports.present`, `reports.total`, `reports.next` (the ordinal
|
|
2660
|
+
the next report takes, padded
|
|
2661
|
+
to at least three digits and widening past them, `001` when there is none), and `entries[]` —
|
|
2662
|
+
every `*.md` in the directory
|
|
2663
|
+
sorted by the ordinal its name claims, numerically, with unnumbered legacy names last (byte order
|
|
2664
|
+
stopped being report order at exactly the width the ordinal widens to: `1000-…` sorts before
|
|
2665
|
+
`998-…`), each carrying `file`, `ordinal` (the leading three-or-more-digit ordinal,
|
|
2666
|
+
`null` for a name written before the numbering, including the `YYYY-MM-DD-…` shape) and
|
|
2667
|
+
`status` (`open` | `sent` | `fixed` | `superseded` | `unstated`, read as the first word of the
|
|
2668
|
+
`- Status:` line in the report's HEADER FIELD BLOCK — the lines above the first `##` heading,
|
|
2669
|
+
quoted code skipped in BOTH markdown spellings, fenced and indented, so a report quoting the
|
|
2670
|
+
protocol's own template counts as whatever its own header says — and `fixed in 0.2.0-alpha.5`
|
|
2671
|
+
counts as `fixed`). The
|
|
2672
|
+
human view prints one row after `agents`:
|
|
2673
|
+
`reports loam-reports/ 12 · open 3 · sent 5 · fixed 3 · superseded 1 · next 013` — zero
|
|
2674
|
+
counts omitted, the total always shown, `reports (none)` when the directory is absent or
|
|
2675
|
+
empty.
|
|
2676
|
+
- Reading it is best-effort and emits no finding: an unreadable file is an entry with status
|
|
2677
|
+
`unstated`, an unreadable directory is `present: true` with `total: 0`, and a status word this
|
|
2678
|
+
vocabulary does not know is `unstated` rather than a sixth bucket. No new code is emitted and
|
|
2679
|
+
`doctor`'s `healthy` does not move for anything in the directory — an open report is not a
|
|
2680
|
+
defect in the repository holding it. Reading stays local: nothing is transmitted, which is the
|
|
2681
|
+
constraint the protocol is built around.
|
|
2682
|
+
- AGENTS.md's support paragraph now says reports are numbered and that `loam doctor` counts the
|
|
2683
|
+
directory and says which ordinal is next.
|
|
2684
|
+
- What a repo notices: `loam doctor` prints a `reports` row it never printed before — `(none)`
|
|
2685
|
+
on a repository that has never written one — and `loam doctor --json` carries a `reports` object
|
|
2686
|
+
beside `agents`. Existing reports keep their names and are counted as `unstated` with no
|
|
2687
|
+
ordinal until someone renames them and adds a `Status:` line; the next report an agent writes
|
|
2688
|
+
lands as `loam-reports/001-…` in a directory of legacy names, because `next` is one past the
|
|
2689
|
+
highest ordinal present and a `YYYY-MM-DD-…` name has none.
|
|
2690
|
+
|
|
2691
|
+
### Added — the adopt brief on an edgeless landscape element
|
|
2692
|
+
|
|
2693
|
+
- `loam adopt --service <id> --json` gains two additive keys under `landscape`: `landscape.touched`
|
|
2694
|
+
— `true` when at least one edge in the `architecture/` project has an endpoint resolving to
|
|
2695
|
+
the service (an intra-service edge counts: the predicate `loam context` prints as "(modelled, no
|
|
2696
|
+
edges touch it)"), `null` whenever `landscape.modelled` is `null` or `false` — and
|
|
2697
|
+
`landscape.attested`, the calls the service's own `model.likec4` declares across its boundary
|
|
2698
|
+
(`direction`, `counterpartId`, `counterpart`, and `title` / `op` / `publishes` / `consumes` where
|
|
2699
|
+
the edge carries them), sorted and de-duplicated, `[]` when the model is absent, does not parse,
|
|
2700
|
+
or declares none, and read only in the edgeless state. `landscape.modelled` and
|
|
2701
|
+
`landscape.elements` are now decided by the same element→service resolver `validate --all` grades
|
|
2702
|
+
`landscape.service-unmodelled` on: a container titled like a sibling service, nested inside an
|
|
2703
|
+
element bound to another service, resolves to its ancestor — so `loam adopt` reports such a
|
|
2704
|
+
service `modelled: false` where it reported `true`, which is the answer the fleet run was already
|
|
2705
|
+
giving.
|
|
2706
|
+
`landscape.instruction` is no longer `null` on element existence alone: while an element resolves
|
|
2707
|
+
to the service and no edge touches it, the brief names each attested call and asks for it as ONE
|
|
2708
|
+
edge on the existing element, collapsed to the service and naming the other party as the map
|
|
2709
|
+
already spells it — or, with nothing attested, points at the walk's outbound and message stops and
|
|
2710
|
+
says draw NOTHING rather than invent an edge — and names the element's tags loam does not read (a
|
|
2711
|
+
placeholder such as `#provisional`, and the view written `exclude element.tag = #<that>` that
|
|
2712
|
+
keeps an adopted service hidden). The `architecture/landscape.likec4` target is back in that state
|
|
2713
|
+
with `action: "edit"`, `required: true`, a shape refusing a second element and an edge-only
|
|
2714
|
+
example. The text view prints the instruction under "what the fleet already says about this
|
|
2715
|
+
service" and flags the row `EDGELESS` ("an element exists, no edge does") instead of `UNDRAWN`.
|
|
2716
|
+
Until now `loam seed --from fleet.yaml` left every service absent from `calls:` as a bound,
|
|
2717
|
+
edgeless element the brief then declared finished.
|
|
2718
|
+
- New `landscape.service-isolated` (warn, never gating, `--all` only, on the landscape target,
|
|
2719
|
+
`subject` the service id): an element resolves to `services/<…>/<svc>/`, no edge anywhere in the
|
|
2720
|
+
`architecture/` project touches it, and that service's `model.likec4` parses and declares at least
|
|
2721
|
+
one call across its
|
|
2722
|
+
boundary — the message names the first three. Evidence-gated on purpose: silent for a service with
|
|
2723
|
+
no model (a seeded fleet), an unparseable model (`c4.invalid` owns it), a model reaching nothing,
|
|
2724
|
+
an unmodelled service (`landscape.service-unmodelled` owns it), and a service the map already
|
|
2725
|
+
touches with ONE edge while its model attests five — the check is touched/untouched, not a set
|
|
2726
|
+
difference; `#external` elements are never
|
|
2727
|
+
subjects. It suppresses `landscape.matched` like every finding. The brief's `checks[]`, the
|
|
2728
|
+
`/loam-check` `--all` table and AGENTS.md's adopt step carry it; `/loam-adopt` step 5 states
|
|
2729
|
+
the silent cases and points at `landscape.touched` in step 1.
|
|
2730
|
+
- One more statement in the adoption brief's unchecked list, now seventeen: whether an adopted
|
|
2731
|
+
service is VISIBLE on the fleet map — a placeholder tag that survives adoption, a curated view
|
|
2732
|
+
that lists its members by id, and `include *` collapsing to a boundary element
|
|
2733
|
+
(`include <boundary>.**`). `/loam-adopt` step 3 names that housekeeping as part of the landscape
|
|
2734
|
+
edit.
|
|
2735
|
+
- `loam adopt --service <id>` reads the fleet map as the `architecture/` PROJECT — the landscape
|
|
2736
|
+
plus every other `.likec4` under `architecture/`, minus the generated `subsystems.likec4` — where
|
|
2737
|
+
it read `architecture/landscape.likec4` alone. It is the document set `landscape.service-isolated`
|
|
2738
|
+
already grades `touched` over, so `landscape.modelled`, `landscape.elements`, `landscape.inbound`,
|
|
2739
|
+
`landscape.outbound`, `landscape.expects`, `landscape.touched`, `landscape.attested` and
|
|
2740
|
+
`landscape.instruction` now answer about the same map `loam validate --all` does. What a repo
|
|
2741
|
+
notices: a fleet that keeps a second `model { }` block, a use case or a palette in a sibling
|
|
2742
|
+
`architecture/*.likec4` no longer gets a brief demanding edges it has already drawn —
|
|
2743
|
+
`touched: true`, `instruction: null`, and no eighth `architecture/landscape.likec4` target — where
|
|
2744
|
+
the fleet run on the same tree reported `landscape.matched`. The same widening makes
|
|
2745
|
+
`landscape.parses` false, and `landscape.modelled` `null`, when any document under `architecture/`
|
|
2746
|
+
fails to parse: the landscape file parsing on its own does not rescue a map the renderer cannot
|
|
2747
|
+
load. No key added, no key removed. The edgeless instruction names the element's home as "the
|
|
2748
|
+
fleet map (architecture/)" rather than a file it did not read — the WRITE still lands in
|
|
2749
|
+
`architecture/landscape.likec4`, and the target's `path` says so.
|
|
2750
|
+
- The adopt brief's `model.likec4` example is headed with the service being adopted
|
|
2751
|
+
(`// order-service's own C4 …`) instead of the literal `payment-service` it carried for every
|
|
2752
|
+
service — one line above an `extend <fqn>` the brief already substituted, so the personalised half
|
|
2753
|
+
taught an agent the rest of the header had been personalised too. `<svc>` joins `<fqn>` as a
|
|
2754
|
+
placeholder the shape rules leave literal and `serviceBrief` fills in; the example never leaves
|
|
2755
|
+
either in the payload.
|
|
2756
|
+
- What a repo notices: a drawn-but-edgeless service whose model reaches other systems gains one
|
|
2757
|
+
`landscape.service-isolated` warning per service on its next `loam validate --all` — under
|
|
2758
|
+
`--strict`, CI is red until an edge lands or the element is genuinely edgeless and stays so (a
|
|
2759
|
+
model attesting no cross-boundary call is silent); a seeded service nobody listed under `calls:`
|
|
2760
|
+
is briefed the edges instead of nothing.
|
|
2761
|
+
|
|
2762
|
+
### Added — a home for an intra-service use case
|
|
2763
|
+
|
|
2764
|
+
- A `dynamic view` over one service's own containers now has a slot loam reads and grades: any
|
|
2765
|
+
`.likec4` beside `services/<…>/<svc>/model.likec4` (`usecases/<name>.likec4` by convention,
|
|
2766
|
+
`views.likec4` read the same) or a tagged view inside model.likec4 itself — exactly the document
|
|
2767
|
+
set the per-service renderer project registers, read whether or not `likec4.config.json` exists.
|
|
2768
|
+
`loam validate --service <id>`, and `--all` per service, grade every view tagged
|
|
2769
|
+
`#req-<Requirement-ID>` with the codes the fleet target already uses: `usecase.step-unbacked`
|
|
2770
|
+
(error), `usecase.step-contested` (warn), `usecase.requirement-unresolved` (error — resolved
|
|
2771
|
+
against THIS service's spec.md and arch.spec.md ids, four arms), `usecase.capability-unresolved`
|
|
2772
|
+
(error — a `#cap-` tag beside a model is refused by placement, vocabulary or not) and
|
|
2773
|
+
`usecase.flow-invalid` (error — a sibling that breaks the service project; model.likec4 keeps
|
|
2774
|
+
grading alone, never `c4.invalid`). Untagged views are never graded, and an untagged sibling is
|
|
2775
|
+
not even loaded while no view in the project opts in and no sibling mentions either reserved
|
|
2776
|
+
prefix anywhere in its bytes; once loaded — a tagged view inside model.likec4 itself, or `req-` /
|
|
2777
|
+
`cap-` in a sibling's comment or title — a project that does not read is one
|
|
2778
|
+
`usecase.flow-invalid` whether or not any view in it opted in. A resolved service-local `#req-`
|
|
2779
|
+
feeds nothing else yet. The feature slot for such a flow is deferred (ROADMAP).
|
|
2780
|
+
- Every surface that names a code for a `#req-` tag declared TWICE — once in the map's
|
|
2781
|
+
`specification` and once in a tags-only block beside the `extend` — now names the one loam
|
|
2782
|
+
actually emits. SCHEMA.md, `/loam-adopt` step 3 and `loam instructions loam-codes` all said
|
|
2783
|
+
`c4.fleet-project-invalid` (a warning); measured, the finding lands where the second declaration's
|
|
2784
|
+
FILE is: `c4.invalid` — an error that gates, suspends the model's grading and cascades six
|
|
2785
|
+
`covers.unknown` — when the tags-only block is inside model.likec4, and `usecase.flow-invalid`
|
|
2786
|
+
when it is in a sibling. `c4.fleet-project-invalid` is the fleet-scope class one step out: two
|
|
2787
|
+
SERVICES each declaring the same tag locally, which no per-service project can see. No behaviour
|
|
2788
|
+
changed; an agent routing on the printed code was being sent to the wrong severity and the wrong
|
|
2789
|
+
repair.
|
|
2790
|
+
- Two corrections to the shared step graders, at fleet altitude too: a hop whose two endpoints
|
|
2791
|
+
resolve to ONE service is no longer backed by that service's internal edges on the service tier
|
|
2792
|
+
(measured: every internal relationship was a candidate, so an unbacked hop between two containers
|
|
2793
|
+
of one service graded attributed and could never earn `usecase.step-unbacked`); and
|
|
2794
|
+
`usecase.step-unlinked` no longer fires when the caller resolves to the provider — a service owes
|
|
2795
|
+
no operationId to itself, and that is the guard's whole scope: a hop from a service-local flow
|
|
2796
|
+
into another service's element still warns, exactly as it does on the fleet map. What a repo
|
|
2797
|
+
notices: a landscape that draws a service as containers with a tagged hop between two of them
|
|
2798
|
+
loses a wrong `usecase.step-unlinked` warning and may gain a right `usecase.step-unbacked` error;
|
|
2799
|
+
a service directory holding a tagged `dynamic view` beside its model starts being graded.
|
|
2800
|
+
- `loam diff` no longer counts a service as a consumer of its own operation, on either half of the
|
|
2801
|
+
question. A landscape edge whose SOURCE and TARGET both resolve to the provider is the provider's
|
|
2802
|
+
own internal call — the rule `usecase.step-unlinked` already states — and so is a use-case hop
|
|
2803
|
+
whose two endpoints resolve to it; the edge half excluded nothing while the message half already
|
|
2804
|
+
did, so the downgrade this list promised could not be observed in the configuration it described.
|
|
2805
|
+
Both are excluded now, and it is: a removal whose only trace in the fleet is the provider calling
|
|
2806
|
+
itself reports `diff.op-removed` (warn) and the run exits 0, where it reported
|
|
2807
|
+
`diff.op-removed-consumed` (error, `breaking: true`, exit 1). A second service's edge naming the
|
|
2808
|
+
operation is still `diff.op-removed-consumed`.
|
|
2809
|
+
- The adopt brief's `checks[]` gains six `--service` rows: the five above and
|
|
2810
|
+
`usecase.step-unlinked`, which the service target reports for a hop into a sibling's stand-in, and
|
|
2811
|
+
which `/loam-check`'s `--service` table now carries a row of its own for. The arch.spec.md shape
|
|
2812
|
+
names the hop-sequence slot; AGENTS.md's layout paragraph names the slot itself
|
|
2813
|
+
(`services/<…>/<svc>/usecases/*.likec4`) and the styling opt-in beside it.
|
|
2814
|
+
- `loam explain` answers the right scope for the eight codes only a feature run raises. A markdown
|
|
2815
|
+
fix table ends at the first line that is not a `|` row, and nothing separated `/loam-check`'s
|
|
2816
|
+
`--feature <FEAT-id>` rows from the `--service <id>` ones sitting above them, so
|
|
2817
|
+
`capability.uncovered`, `capability.remove-requirement-realized`, `glossary.term-exists`,
|
|
2818
|
+
`usecase.flow-exists`, `deployment.doc-exists`, `deployment.doc-invalid`,
|
|
2819
|
+
`deployment.doc-reserved` and the feature/archive arm of `usecase.flow-invalid` all came back
|
|
2820
|
+
`scope: "--service <id>"` — a service run can raise none of them. They now read
|
|
2821
|
+
`--feature <FEAT-id>`. What a consumer notices: `entries[].scope` changes value for those eight
|
|
2822
|
+
in `loam explain <code> --json` and in `loam explain --codes --json`; the meanings and the fixes
|
|
2823
|
+
are unchanged, and no code was added or removed.
|
|
2824
|
+
|
|
2825
|
+
### Changed — the generated subsystem views join the repository's styling
|
|
2826
|
+
|
|
2827
|
+
Every view in the generated `architecture/subsystems.likec4` now carries `global style subsystems`
|
|
2828
|
+
— written after its `title` and `description`, before its `include`/`group` — exactly when the
|
|
2829
|
+
`architecture/` project (the landscape merged with every other `.likec4` under `architecture/` the
|
|
2830
|
+
root `exclude` does not cover — a `palette.likec4` beside the map counts, and one under an excluded
|
|
2831
|
+
path is loaded by nobody; the one
|
|
2832
|
+
load `loam subsystem sync`, `validate --all` and `archive` share) declares a global style with the
|
|
2833
|
+
id `subsystems`, in either form: `global { styleGroup subsystems { … } }` or
|
|
2834
|
+
`global { style subsystems … }`. loam reads only that the id is declared — a census of the keys of
|
|
2835
|
+
`$data.globals.styles` beside the view-id census in `core/c4/parsed/view-ids.ts` — and never what
|
|
2836
|
+
the group says (docs/DESIGN.md rule 26). Nothing is written otherwise, and the file is
|
|
2837
|
+
byte-identical to before. When the project declares style groups under other names only, one
|
|
2838
|
+
comment line between the header and `views {` names them and says none is `subsystems`; there is
|
|
2839
|
+
no single-group fallback, because a second group declared for a hand-drawn view would otherwise
|
|
2840
|
+
switch the generated views' styling off on a change that touched no subsystem. Styling therefore
|
|
2841
|
+
restales the generated file on two triggers, and `subsystem.views-stale`, `loam explain` and the
|
|
2842
|
+
fix tables name both: declaring or removing the group named `subsystems` changes whether the views
|
|
2843
|
+
carry the `global style subsystems` line, and declaring or removing a group under ANY OTHER name
|
|
2844
|
+
changes the disclosure comment. What a repo notices:
|
|
2845
|
+
a fleet that already declares a group named `subsystems` (or any group, for the disclosure line)
|
|
2846
|
+
sees one `subsystem.views-stale` on its next `validate --all`, cleared by one `loam subsystem sync`.
|
|
2847
|
+
`loam seed` renders the file from that same project — the landscape it is about to write, staged
|
|
2848
|
+
beside every other `architecture/` document — so a palette kept in a sibling such as
|
|
2849
|
+
`architecture/usecases/palette.likec4` is referenced by the seeded views and never a stale file
|
|
2850
|
+
straight after a seed. No new code, flag or `--json` key. `LoadedDoc` gains the additive field
|
|
2851
|
+
`globalStyles` (the sorted declared ids), produced alike by every loader and pinned by the three
|
|
2852
|
+
parity suites; seven tripwire assertions in `test/likec4-view-shape.test.ts` pin the parsed shape
|
|
2853
|
+
at likec4@1.59.2.
|
|
2854
|
+
|
|
2855
|
+
### Changed — the fleet's consumer census reads the models, not the map alone
|
|
2856
|
+
|
|
2857
|
+
- `landscape.datastore-private`, `landscape.datastore-shared` and `landscape.platform-candidate`
|
|
2858
|
+
count a consumer that a service's own EXTENDING `model.likec4` attests — an outbound call at the
|
|
2859
|
+
element or a descendant — beside the edges `architecture/` draws. Under the extending shape the
|
|
2860
|
+
container-level call lives in the model and only the service-level twin reaches the map, so a
|
|
2861
|
+
store whose consuming edges were all drawn in models had zero consumers and earned nothing at all,
|
|
2862
|
+
and a store reached by three models plus one map edge was reported `landscape.datastore-private`
|
|
2863
|
+
naming the single service the map happened to draw. Extending models only: their ids are the map's
|
|
2864
|
+
fully-qualified ids, so the join is exact, while a standalone model's ids are its own file's and
|
|
2865
|
+
are never matched to a landscape element. A model that does not parse, or cannot be read,
|
|
2866
|
+
contributes nothing.
|
|
2867
|
+
- `landscape.datastore-shared` now fires for a NESTED store. A store sitting inside the element that
|
|
2868
|
+
resolves to its owner — declared there on the map, or added by the owner's extending model — is
|
|
2869
|
+
private by construction and silent at one consumer, and is graded at two: `subject` is the store's
|
|
2870
|
+
title as before, and the message adds ` — <owner>'s own store, written as '<fqn>'` beside the
|
|
2871
|
+
consuming services it names — the store's OWN id, since `<fqn>` IS the store and the earlier
|
|
2872
|
+
"nested under" sent a reader looking for a containing element of that name. The message then
|
|
2873
|
+
states the reversibility directionally: the two placements give the store the same id, but with a
|
|
2874
|
+
second consumer the move is one-way — out of the `extend` block onto the map is always safe, and
|
|
2875
|
+
back into it breaks every other consumer's edge.
|
|
2876
|
+
Until now the
|
|
2877
|
+
strongest coupling two services can have rendered perfectly and was graded nowhere, because the
|
|
2878
|
+
census only ever looked at elements drawn at fleet level.
|
|
2879
|
+
- `landscape.datastore-private`'s remedy names the placement instead of a directory, and names it
|
|
2880
|
+
for the consumer's own model SHAPE rather than assuming one. For an EXTENDING consumer it says to
|
|
2881
|
+
write the store in that model's `extend <fqn> { … }` block, or inside `<fqn>` on the map when the
|
|
2882
|
+
map draws that service's containers. For a consumer whose model STANDS ALONE — or that loam could
|
|
2883
|
+
not read as extending — it says the map placement and offers the migration to the extending shape
|
|
2884
|
+
as the other arm, because `extend <fqn>` in a standalone model resolves nothing (such a model is
|
|
2885
|
+
parsed alone) and following the old sentence there turned a run with 0 errors into `c4.invalid`.
|
|
2886
|
+
Either way it states what happens next: a second service
|
|
2887
|
+
reaching the same data draws a container-level edge to `<fqn>.<name>` in its own model plus a
|
|
2888
|
+
service-level edge on the map, and loam counts that consumer — so the state is
|
|
2889
|
+
`landscape.datastore-shared`, never silence. It also states which of the two placements keeps that
|
|
2890
|
+
door open: the edge resolves only against a store the MAP declares, because a per-service project
|
|
2891
|
+
is `architecture/` plus one model. The old "or add the second consumer's edge" is gone as an equal
|
|
2892
|
+
remedy and survives as "if another service reaches the same data today, draw its edge and this
|
|
2893
|
+
becomes that warning now". It also states the measured reversibility with its condition — the two
|
|
2894
|
+
placements give the store the same id, `<fqn>.<store>`, and with one consumer either move is free:
|
|
2895
|
+
out of the `extend` block onto the map always is, and back into it while no other service's model
|
|
2896
|
+
or map edge names the store (measured on `examples/docs`: moving `db` out of order-service's `extend`
|
|
2897
|
+
block into `orderService` on the map left the model at 12 elements · 8 relationships, left
|
|
2898
|
+
`Covers: marketplace.orderService.db` in its `arch.spec.md` resolving, and left checkout-web's
|
|
2899
|
+
container-level edge to it resolving) — and it qualifies the "never silence" promise: a consumer
|
|
2900
|
+
whose model stands alone is not counted, because its ids are its own file's, so its edge belongs
|
|
2901
|
+
on the map.
|
|
2902
|
+
- `landscape.datastore-private` and `landscape.datastore-shared` never take an element that stands
|
|
2903
|
+
for a `services/<id>/` as their subject, at ANY depth. The fleet-level half has always skipped
|
|
2904
|
+
one (a datastore bound to a directory is the binding checks' subject); the NESTED half did not,
|
|
2905
|
+
so a `database` carrying `metadata { service '<id>' }` and drawn inside another service's
|
|
2906
|
+
element became a store "owned" by its own binding, and one more reader made it
|
|
2907
|
+
`landscape.datastore-shared` — advising the team to give each service a private copy of a
|
|
2908
|
+
service. Both halves and the model-declared store census now share one `standsForService`
|
|
2909
|
+
predicate, handed down on the input record because a `fleet/map/` module may not import `fleet/`.
|
|
2910
|
+
- `landscape.invalid`'s message generalises its hint. It spoke about use-case flows alone, so an
|
|
2911
|
+
author whose MODEL EDGE named a container was told to move a flow that does not exist. It now says
|
|
2912
|
+
that an unresolved name an extending model declares — a use-case hop or an edge naming
|
|
2913
|
+
`<service-fqn>.<container>` — renders for the renderer and resolves for nobody here, and that the
|
|
2914
|
+
repair is to name the SERVICE on the map and the container in that service's own model.
|
|
2915
|
+
- `landscape.service-isolated` never takes an `#external` element as its subject. The claim rested
|
|
2916
|
+
on "an `#external` element is not an enumerated service", which is false for an element carrying
|
|
2917
|
+
the tag AND a `metadata { service '<id>' }` binding to a directory that exists: the input set had
|
|
2918
|
+
no tag filter, so a foreign box answered for one of our services. The isolation input SET now
|
|
2919
|
+
skips `#external`, matching `landscape.binding-duplicate` and
|
|
2920
|
+
`landscape.binding-unknown`. `landscape.service-unmodelled` keeps the unfiltered set — the
|
|
2921
|
+
directory IS drawn. The `touched` walk applies NO tag filter, deliberately: it answers a different
|
|
2922
|
+
question — does the map draw any edge on this service — and LikeC4 puts a kind tag on every
|
|
2923
|
+
element of the kind, so `element topic { #external }` (the idiom
|
|
2924
|
+
`examples/docs/architecture/landscape.likec4` documents) made an edge drawn from a service's OWN
|
|
2925
|
+
topic stop touching that service. The warning fired on a service the map demonstrably reaches
|
|
2926
|
+
while `loam adopt` reported `touched: true` on the same tree; the walk is the brief's
|
|
2927
|
+
`landscape.touched` predicate exactly, tag and all.
|
|
2928
|
+
|
|
2929
|
+
### Changed — the renderer wiring, measured rather than assumed
|
|
2930
|
+
|
|
2931
|
+
- `loam subsystem sync` now DELETES a `likec4.config.json` that sits beside a model which EXTENDS
|
|
2932
|
+
the fleet map, in the same journaled commit as everything else it writes, and `--json` `projects`
|
|
2933
|
+
gains `projects.removed` (POSIX paths relative to the docs root, sorted by path (not by service
|
|
2934
|
+
id — the survey's own order); `[]` on a run that removed none)
|
|
2935
|
+
beside `projects.created`. The text view prints one `removed <path>` line per file and one
|
|
2936
|
+
sentence saying why. The note this corrects said such a nested project was registered EMPTY and
|
|
2937
|
+
therefore harmless; measured on likec4 1.59.2 it CLAIMS the model — the workspace loads two
|
|
2938
|
+
projects, the model is Invalid inside its own one (the map's kinds are not in there with it) and
|
|
2939
|
+
`export json --project fleet` loses the service's containers wherever that nested project claims
|
|
2940
|
+
the model (measured both ways at the 1.59.2 pin: it did on a seeded fleet, and did not on
|
|
2941
|
+
`examples/docs`, where the root project kept the model — what happens in every state is that the
|
|
2942
|
+
docs root stops being a one-project workspace and `likec4 validate .` refuses without
|
|
2943
|
+
`--project`). A repo that migrated 56 models to
|
|
2944
|
+
`extend` and ran one `sync` kept 56 stray files and 57 projects; it now keeps one. A leftover
|
|
2945
|
+
beside a model that is GONE still stays — that one really is the team's, and a nested project
|
|
2946
|
+
holding no `.likec4` is silently absent from the renderer's list. `service.likec4-config-stray`'s
|
|
2947
|
+
message is rewritten to the measured consequence and now names a repair `sync` actually performs.
|
|
2948
|
+
`projects.current` narrows with it: it counts the services owed a project file that already had
|
|
2949
|
+
one, and a model that extends the map is owed none — so on the run that deletes the strays it
|
|
2950
|
+
drops by exactly the length of `projects.removed`, and every run after that reports the same
|
|
2951
|
+
number both spellings would have.
|
|
2952
|
+
- `loam subsystem sync` no longer rewrites the generated views file when the map cannot be read.
|
|
2953
|
+
`action` gains the value `blocked`: with `architecture/` unreadable and
|
|
2954
|
+
`architecture/subsystems.likec4` already present, the run writes nothing about that file, prints
|
|
2955
|
+
the reason, and `--json` carries that action plus `blocked.reason` reading `landscape-invalid` —
|
|
2956
|
+
exit 0, because `loam validate --all`'s `landscape.invalid` is the gating surface. Before this,
|
|
2957
|
+
any load error rendered the tolerant shape: the file was rewritten down to title and description,
|
|
2958
|
+
losing the `global style subsystems` line, every `include` and the boundary comment, while `sync`
|
|
2959
|
+
reported `updated … 1 subsystem view(s)` and answered `current` ever after. The render also now
|
|
2960
|
+
falls back to the landscape FILE when only a SIBLING document breaks the project, so a duplicate
|
|
2961
|
+
`subsystems` group declared in two `architecture/` documents costs nothing. With no generated file
|
|
2962
|
+
yet, and inside a tree-changing verb's commit window (`new`/`move`/`rm`/`rename`, whose contract is
|
|
2963
|
+
that the file lands with the change), the tolerant shape is still written and the same one-line
|
|
2964
|
+
note is printed.
|
|
2965
|
+
- The `architecture/` document set every reader shares now honours the root `likec4.config.json`'s
|
|
2966
|
+
`exclude`. A `.likec4` under `architecture/` the renderer never loads is no longer loaded by loam
|
|
2967
|
+
either: a palette under an excluded path used to yield a `global style subsystems` reference the
|
|
2968
|
+
fleet project could not resolve — `likec4 validate` Invalid on every run while `loam validate
|
|
2969
|
+
--all` reported 0 errors, `doctor` was healthy and `sync` said "current" — and a broken excluded
|
|
2970
|
+
document blanked the whole map. A root config loam cannot read an `exclude` list out of filters
|
|
2971
|
+
nothing.
|
|
2972
|
+
- An `exclude` entry is now read the way the renderer reads it, measured rather than reasoned about.
|
|
2973
|
+
loam interpreted only a literal `/**` suffix, so a root config written `services/**/*`,
|
|
2974
|
+
`services/*`, `services/` or the bare `services/<tree>` hid every extending model with zero
|
|
2975
|
+
findings; the first repair of that read the entry as a walk over path SEGMENTS, which is not what
|
|
2976
|
+
LikeC4 does either. Measured at the 1.59.2 pin on a scratch fleet of five documents, by putting
|
|
2977
|
+
one entry in the root config and counting the "N files" `npx likec4 validate .` reports: a bare
|
|
2978
|
+
entry is a raw PREFIX with no end anchor (`services/pay` -> 3 files, hiding BOTH
|
|
2979
|
+
`services/payment-service` and `services/payments/payment-service`; `architect` -> 3, hiding all of
|
|
2980
|
+
`architecture/`), it must start on a `/` boundary (`ervices` and `ces/payment-service` -> 5, that
|
|
2981
|
+
is, nothing), `*` stays inside one segment while `**` crosses (`architecture/*.likec4` -> 4, so
|
|
2982
|
+
the nested use case survives; `*.likec4` -> 0), and a glob form does NOT prefix-match
|
|
2983
|
+
(`services/pay/**` -> 5). LikeC4 prefixes an entry that does not begin with a doubled star with
|
|
2984
|
+
one, joins it onto the project folder and matches with picomatch `contains: true` — the missing
|
|
2985
|
+
end anchor is that option. So `service.model-excluded` fires now, naming the entry verbatim, on
|
|
2986
|
+
every spelling including a starred middle segment; `service.model-unexcluded` no longer
|
|
2987
|
+
false-positives on a standalone model whose directory such an entry already covers; and
|
|
2988
|
+
`subsystem sync` takes back an entry that hides an extending model whatever its spelling — the
|
|
2989
|
+
write side asked "which directory does this entry name", which answers nothing for a starred
|
|
2990
|
+
segment, so `services/*/svc-b` hid a model forever while `service.model-excluded` named that same
|
|
2991
|
+
command as the repair — and appends nothing for a directory the surviving list already covers.
|
|
2992
|
+
- The root `exclude` filter on the `architecture/` document set now has a floor:
|
|
2993
|
+
`architecture/landscape.likec4` stays in the set whatever the list says. Without one, an entry a
|
|
2994
|
+
team writes for a palette (`architecture/*.likec4` covers both) left the architecture project
|
|
2995
|
+
holding ZERO documents — and an empty project parses with zero errors, so every reader took it for
|
|
2996
|
+
"the map declares nothing". On a seeded two-service fleet that produced four errors, all false:
|
|
2997
|
+
`landscape.service-unmodelled` twice, naming the very file that models both services and telling
|
|
2998
|
+
the reader to add an element already in it, plus `c4.invalid` on the extending model; and
|
|
2999
|
+
`loam subsystem sync` answered `updated` and rewrote a good generated file down to a title, with
|
|
3000
|
+
no line in either run mentioning the `exclude` entry that caused it. loam now grades one document
|
|
3001
|
+
the renderer may skip rather than asserting a fleet's whole architecture says nothing.
|
|
3002
|
+
- `loam subsystem sync` answers `blocked` whenever the `architecture/` PROJECT does not parse, not
|
|
3003
|
+
only when `architecture/landscape.likec4` itself is unreadable. The landscape-file fallback that
|
|
3004
|
+
saves the element join says nothing about the STYLE CENSUS — a palette may live in any `.likec4`
|
|
3005
|
+
under `architecture/` — so a run made while an unrelated sibling was broken reported `updated`,
|
|
3006
|
+
deleted the `global style subsystems` line from a good generated file, answered `current` ever
|
|
3007
|
+
after, and never earned `subsystem.views-stale` while the map stayed invalid. The file is now left
|
|
3008
|
+
byte-identical in that state and the note names what was unavailable.
|
|
3009
|
+
- `loam subsystem sync --json` carries `projects.exclude.unreadable` (boolean, emitted on every
|
|
3010
|
+
run). `entries: []` alone collapsed the two answers loam insists must stay apart: "the root
|
|
3011
|
+
project excludes nothing" and "loam could not read an `exclude` list out of the root file". The
|
|
3012
|
+
text view has always had a note for the second; a machine consumer reading `entries: []` got the
|
|
3013
|
+
opposite of the truth about what the renderer will load. Additive, beside `projects.exclude.removed`.
|
|
3014
|
+
- `loam subsystem sync`'s "— nothing to write." tail is dropped when the run rewrote the root
|
|
3015
|
+
`likec4.config.json`'s `exclude`. It counted only the per-service project files created and
|
|
3016
|
+
removed, so a run with a current views file printed "nothing to write." immediately above the line
|
|
3017
|
+
reporting the rewrite.
|
|
3018
|
+
- `c4.fleet-project-invalid` now enumerates what the renderer actually loads. The document set is a
|
|
3019
|
+
walk of the docs root — everything the root `exclude` leaves, minus the generated
|
|
3020
|
+
`architecture/subsystems.likec4`, minus every standalone service directory (owned by
|
|
3021
|
+
`service.model-unexcluded`), minus `features/` (a delta carries its own `specification` and the
|
|
3022
|
+
directory is transient). A `.likec4` in a SUBSYSTEM directory — the parent of service directories
|
|
3023
|
+
— was in none of the three roots the check used to merge: `validate --all` came back
|
|
3024
|
+
byte-identical to baseline and never named the file while `npx likec4 validate --project fleet .`
|
|
3025
|
+
reported Invalid over the same tree. The finding also gains `locations` (the file the error is IN,
|
|
3026
|
+
not `architecture/landscape.likec4` on every one of them) and `details`, and its tail now names
|
|
3027
|
+
the failure it actually is — a double declaration, an unresolved reference, or neither — instead
|
|
3028
|
+
of always blaming a double declaration.
|
|
3029
|
+
- Every LikeC4 line number loam prints is 1-based, the way an editor counts — and deliberately NOT
|
|
3030
|
+
the way `likec4 validate` prints it, which passes the LSP range's start line through and so says
|
|
3031
|
+
`Line 8` for a fault on line 9.
|
|
3032
|
+
`c4.invalid`'s details said `L3` for an error on line 4; `c4.fleet-project-invalid` printed
|
|
3033
|
+
`model.likec4:0` for a declaration on line 1; and the feature arm of `usecase.flow-invalid`
|
|
3034
|
+
printed no line at all and an ABSOLUTE Windows path into loam's own staging tree — it now spells
|
|
3035
|
+
the authored file docs-relative with forward slashes and an `L<n>`, exactly as the service arm of
|
|
3036
|
+
the same code does, in the text view, in `--json` and in the `loam archive` block message.
|
|
3037
|
+
|
|
3038
|
+
### Fixed — the adoption brief answers off the document it actually read
|
|
3039
|
+
|
|
3040
|
+
- `loam adopt` no longer briefs a write to the shared fleet map when the map could not be READ.
|
|
3041
|
+
The eighth target (`architecture/landscape.likec4`) is emitted on `landscape.modelled === false`
|
|
3042
|
+
— a read that succeeded and found nothing — and on the edgeless state; `modelled: null` ("loam
|
|
3043
|
+
could not read the map", which since the `architecture/`-project widening includes a broken
|
|
3044
|
+
palette or use case beside a landscape that parses perfectly) now emits no target at all.
|
|
3045
|
+
It used to fall through the same gate as "the map draws nothing for this service", so the brief
|
|
3046
|
+
handed an agent the UNDRAWN block — "add one top-level element bound to its directory" — for a
|
|
3047
|
+
service the map already declares and binds, and splicing it earned `landscape.binding-duplicate`.
|
|
3048
|
+
`landscape.instruction` carries the whole answer instead and names the documents that failed.
|
|
3049
|
+
- `loam adopt`'s fleet-map instruction and the text view's landscape headline no longer name
|
|
3050
|
+
`architecture/landscape.likec4` as the broken file when the document that failed is a SIBLING
|
|
3051
|
+
under `architecture/`. The instruction now names the failing documents docs-relative with their
|
|
3052
|
+
error count — the same spelling `validate --all`'s `landscape.invalid` prints — and the headline
|
|
3053
|
+
says `The fleet map (architecture/) does not parse — fix that first; the files are named below.`
|
|
3054
|
+
The brief and the fleet run on the same tree used to blame two different files.
|
|
3055
|
+
- `loam adopt` no longer marks `openapi.yaml` required because a document beside the fleet map
|
|
3056
|
+
broke. The API question is answered off the landscape FILE (`landscape.callersKnown`, added to
|
|
3057
|
+
`--json`: true when the `architecture/` project parses, and true when it does not but the
|
|
3058
|
+
landscape file does), which is the evidence `loam list` and `loam context` already read: a broken
|
|
3059
|
+
`architecture/palette.likec4` used to flip `openapi.yaml` from `create missing` to
|
|
3060
|
+
`create MISSING` for a browser UI the map proves nobody calls, and told the agent to write an API
|
|
3061
|
+
contract for it. With the landscape file itself unreadable the contract is still owed.
|
|
3062
|
+
- `loam adopt`'s model.likec4 example prints a short `<your-element-id>` where loam has no id to
|
|
3063
|
+
substitute, and carries the reason once as a `//` comment above the block. The placeholder used
|
|
3064
|
+
to be a 103-character English sentence spliced into all three code positions `<fqn>` occupies —
|
|
3065
|
+
the `extend` line, the source of a container edge and `view of` — on the command's most common
|
|
3066
|
+
invocation, a brand-new adoption.
|
|
3067
|
+
- `loam adopt`'s arch.spec.md shape rule named `c4.fleet-project-invalid` (warn, `--all`) for a
|
|
3068
|
+
`#req-` tag declared both in the map and beside the model. Measured, that is `c4.invalid` (error,
|
|
3069
|
+
and the model's grading is suspended behind it) when the tags-only block is in model.likec4 and
|
|
3070
|
+
`usecase.flow-invalid` when it is in a sibling; `c4.fleet-project-invalid` is the fleet-scope
|
|
3071
|
+
class one step out, two SERVICES each declaring the same tag locally. The rule now says so, and
|
|
3072
|
+
matches the same statement in SCHEMA.md.
|
|
3073
|
+
- `loam adopt`'s model.likec4 shape rule for `c4.element-unowned` leads with the nested remedy —
|
|
3074
|
+
a store or component this service OWNS goes INSIDE the `extend <fqn> { }` block, where its id
|
|
3075
|
+
becomes `<fqn>.<name>` — before the two existing arms, matching the `--service` fix table and
|
|
3076
|
+
SCHEMA's grades table.
|
|
3077
|
+
- `loam adopt`'s `landscape.invalid` check description said "architecture/landscape.likec4 exists
|
|
3078
|
+
but does not parse". It now says the `architecture/` PROJECT does not parse — the landscape or
|
|
3079
|
+
any `.likec4` beside it — and that the message names the document that broke.
|
|
3080
|
+
|
|
3081
|
+
### Fixed — one tree, one answer: the gate's preview, and the map's other readers
|
|
3082
|
+
|
|
3083
|
+
- The merge preview every feature-side gate reads is now routed exactly as `loam archive` routes:
|
|
3084
|
+
the fleet's extending models reach it, so an addition the delta nests inside a service is
|
|
3085
|
+
previewed in that service's `model.likec4` and not on the map. Without them the preview held
|
|
3086
|
+
interior the archive puts somewhere else, and `usecase.flow-invalid` — the gate `--approve`
|
|
3087
|
+
cannot override — failed open on the one shape it exists for. Measured on a copy of
|
|
3088
|
+
`examples/docs`: a feature nesting `cache` under `marketplace.orderService` and hopping
|
|
3089
|
+
`checkoutWeb -> marketplace.orderService.cache` from `features/<FEAT>/usecases/` passed
|
|
3090
|
+
`validate --feature`, archived at exit 0, and left a fleet whose next `validate --all` reported 7
|
|
3091
|
+
errors — one `landscape.invalid` plus one `spine.landscape-invalid` per service — against a file
|
|
3092
|
+
its author never wrote. The gate now refuses before anything is written, `validate --feature`
|
|
3093
|
+
gives the same verdict, and a flow hopping between elements the map itself draws still archives.
|
|
3094
|
+
`deployment.doc-invalid` is wired the same way and had the same hole.
|
|
3095
|
+
- `loam context <service>` reads the fleet map as the `architecture/` PROJECT — the landscape
|
|
3096
|
+
merged with every use case, palette and second `model { }` block beside it — never
|
|
3097
|
+
`architecture/landscape.likec4` alone. Two commands answered opposite things about one tree:
|
|
3098
|
+
measured on a two-service fleet with a broken `architecture/palette.likec4` beside a landscape
|
|
3099
|
+
that parses, `loam adopt --service order-service` reported `parses: false` and refused to say
|
|
3100
|
+
anything about the map, while `loam context order-service` reported `parses: true`,
|
|
3101
|
+
`modelled: true` and listed the map's edges — and an agent handed both has no way to tell which is
|
|
3102
|
+
right. `landscape.present` is still a fact about the FILE; `landscape.parses` is now a fact about
|
|
3103
|
+
the project the renderer loads, so a broken sibling reads here as "nobody could look", the answer
|
|
3104
|
+
`validate --all` and `adopt` already gave. The second load of that same project the deployment
|
|
3105
|
+
slice used to make when the landscape file declared no `deployment { }` block is gone with it: one
|
|
3106
|
+
parse, one answer.
|
|
3107
|
+
- `loam seed` computes the generated views file's style census from the `architecture/` documents
|
|
3108
|
+
the root `likec4.config.json`'s `exclude` leaves, not from a raw walk of the directory. A
|
|
3109
|
+
`palette.likec4` under an excluded path is loaded by neither the renderer nor `validate --all`,
|
|
3110
|
+
yet seed read it and wrote `global style subsystems` into `architecture/subsystems.likec4` — a
|
|
3111
|
+
name the fleet project cannot resolve, written by the command whose own `next` line is
|
|
3112
|
+
`loam validate --all`. The same defect one command over from the one the exclude filter fixed in
|
|
3113
|
+
the loader. The fleet use-case scan's cheap tag gate was corrected with it: it walked
|
|
3114
|
+
`architecture/` for itself, which could only over-scan (it is a prefilter before a load that does
|
|
3115
|
+
apply the exclude), but two lists that disagree about what the project is are two answers waiting
|
|
3116
|
+
to drift.
|
|
3117
|
+
- `landscape.invalid`'s head reads `<documents> have N error(s)` once more than one document broke,
|
|
3118
|
+
matching `spine.landscape-invalid`. The named list is comma-joined when the `architecture/`
|
|
3119
|
+
project has errors in two files, and the fleet line said "a, b has 4 error(s)" directly above the
|
|
3120
|
+
service line saying "a, b have 4 error(s)" about the same two files — one report, two spellings of
|
|
3121
|
+
one fact.
|
|
3122
|
+
|
|
3123
|
+
### Fixed — a second verification round: what the archive routes, and what every message names
|
|
3124
|
+
|
|
3125
|
+
- `loam archive` no longer refuses a delta written in the legacy title-join spelling inside a group
|
|
3126
|
+
the map declares. Routing an addition into a service's extending model now requires the delta to
|
|
3127
|
+
spell the whole parent chain in the MAP's own ids — a living element, a container an extending
|
|
3128
|
+
model already declares, or a declaration this same delta adds — instead of only requiring that
|
|
3129
|
+
*some* ancestor be living. A grouped map (`marketplace = group { … }`, the shape
|
|
3130
|
+
`loam init --example` ships) made the old guard pass on `marketplace.orderSvc`, the addition was
|
|
3131
|
+
written as `extend marketplace.orderSvc` — an id the map has never declared — and the model's
|
|
3132
|
+
parse net refused the whole archive with `merge-failed` and nothing written. What a repo notices:
|
|
3133
|
+
such a delta merges onto `architecture/landscape.likec4` under the map's own id and exits 0 again,
|
|
3134
|
+
exactly as it does when the service's model is standalone or absent. The documented remainder is
|
|
3135
|
+
unchanged: a title-joined parent keeps the map path.
|
|
3136
|
+
- A service a feature INTRODUCES now keeps its interior out of the fleet map when its directory
|
|
3137
|
+
already holds an extending model. The new element's containers arrive inside its authored bytes and
|
|
3138
|
+
carry no feature tag of their own — LikeC4 does not inherit tags, so nothing in the addition set
|
|
3139
|
+
could see them — and they rode onto the map: `loam adopt --service <svc>` then told the team to
|
|
3140
|
+
write the same container into `extend <fqn> { }`, and the renderer reported a duplicate element
|
|
3141
|
+
blamed on both files while the service graded `c4.invalid`. Now the box lands on the map without
|
|
3142
|
+
its interior, the children are spliced into the model's `extend <fqn> { }` block (created if
|
|
3143
|
+
absent), and `loam archive` prints the second
|
|
3144
|
+
`architecture: merged into services/<…>/model.likec4` line for it. A service arriving with no
|
|
3145
|
+
directory at all, and one whose block also draws an internal edge between its own containers, keep
|
|
3146
|
+
the previous behaviour byte for byte — an edge written inside the block names its endpoints by
|
|
3147
|
+
their local names and there is no legal split, so the block rides whole. That is ONE decision and it covers the whole block: a child or an edge
|
|
3148
|
+
the delta tags with the feature rides with a block that rides and is spliced with a block that
|
|
3149
|
+
splits, never placed twice. While the two were decided separately the tagged child landed in both
|
|
3150
|
+
documents and the merge refused itself, `merge-failed`, on a delta neither half was wrong about.
|
|
3151
|
+
- Both C4 merges now splice with the target document's own line endings. A CRLF
|
|
3152
|
+
`architecture/landscape.likec4` or `services/<…>/model.likec4` came back with a handful of bare-LF
|
|
3153
|
+
lines in the spliced region, and a CRLF delta's blank lines arrived carrying the target indent as
|
|
3154
|
+
trailing whitespace. A document with one convention keeps it; a document that is already mixed is
|
|
3155
|
+
left alone, because there is no convention to match.
|
|
3156
|
+
- `landscape.datastore-shared` and `landscape.datastore-private` now state the reversibility of a
|
|
3157
|
+
private store's placement **directionally**, because the unconditional clause was false in every
|
|
3158
|
+
state `datastore-shared` prints in. The two placements do give the store the same id
|
|
3159
|
+
(`<owner-fqn>.<store>`), but the move is only free one way: out of the owner's `extend` block onto
|
|
3160
|
+
the owner's element on the map is always safe, and back INTO the `extend` block breaks every other
|
|
3161
|
+
consumer's edge — `c4.invalid` on that service's model, or `landscape.invalid` when the edge is
|
|
3162
|
+
drawn on the map — because a per-service project is `architecture/` plus one model, so an `extend`
|
|
3163
|
+
block is visible to its own service's project alone. `datastore-shared` needs a second consumer to
|
|
3164
|
+
fire, so the state it prints in is exactly the state in which map→extend breaks; `datastore-private`
|
|
3165
|
+
has one consumer, so it says both moves are free *today* and names the condition that ends that.
|
|
3166
|
+
- `landscape.datastore-private`'s no-model arm offers a model to WRITE, not one to migrate: with no
|
|
3167
|
+
`model.likec4` at all it now says "write an extending model at `<path>`" one sentence after saying
|
|
3168
|
+
loam read no extending model there. The STANDALONE arm still says "migrate the model", which is
|
|
3169
|
+
correct for it.
|
|
3170
|
+
- `landscape.service-isolated`'s message names the fleet map rather than one file: "no edge in the
|
|
3171
|
+
fleet map (architecture/) touches it". The check's input is the `architecture/` PROJECT — an edge
|
|
3172
|
+
added to `architecture/extra.likec4` and never to `landscape.likec4` silences it, measured — so the
|
|
3173
|
+
message now agrees with SCHEMA, with `loam explain landscape.service-isolated` and with the adopt
|
|
3174
|
+
brief, all three of which already said "anywhere in `architecture/`".
|
|
3175
|
+
- `loam doctor` prints LikeC4 line numbers 1-based, through the same `errorText` helper every other
|
|
3176
|
+
surface uses. `doctor.landscape-invalid` hand-rolled its own `(line <n>)` spelling and passed the
|
|
3177
|
+
LSP range through raw, so it named the line above the fault and disagreed with `validate --all`
|
|
3178
|
+
about one error on one tree. The message now reads
|
|
3179
|
+
`architecture/landscape.likec4 does not parse: L<n>: <message> — and N more`.
|
|
3180
|
+
- `loam context --json` and `loam explore --json` gain `landscape.broken`: the `architecture/`
|
|
3181
|
+
documents that failed, docs-relative POSIX, deduped and sorted, `[]` while `landscape.parses` is
|
|
3182
|
+
true. Spelled exactly as `landscape.invalid` and the adopt brief spell them, so the three surfaces
|
|
3183
|
+
name one file one way. Both commands read the `architecture/` PROJECT, so the document at fault is
|
|
3184
|
+
very often a sibling beside a landscape that reads perfectly.
|
|
3185
|
+
- The adopt brief no longer promises a warning that cannot fire. On an element carrying `#external`,
|
|
3186
|
+
the edgeless instruction and the `landscape.likec4` target's shape rule say that
|
|
3187
|
+
`landscape.service-isolated` will NOT fire (the check removes every `#external` element from its
|
|
3188
|
+
subject set), that this brief is the only place the state is named, and that dropping `#external`
|
|
3189
|
+
makes the check apply again. Every other tree keeps the previous sentence.
|
|
3190
|
+
- The adopt brief's `checks[]` row for `c4.declaration-diverged` names "the fleet map
|
|
3191
|
+
(`architecture/`)" instead of `architecture/landscape.likec4`. The map is a project; LikeC4 hands
|
|
3192
|
+
back no source document for an element, and the finding's own message and `loam explain` already
|
|
3193
|
+
refused to name a file.
|
|
3194
|
+
- `loam adopt --json`'s `landscape.inbound`/`landscape.outbound` drop an edge whose two endpoints
|
|
3195
|
+
resolve to the SAME service — a service's own internal wiring drawn on the map made it its own
|
|
3196
|
+
caller. `landscape.touched` is unchanged: it is now read off the map's relationships rather than
|
|
3197
|
+
off those two lists, so it still counts an intra-service edge and still matches
|
|
3198
|
+
`landscape.service-isolated`'s predicate exactly.
|
|
3199
|
+
- The service arm's use-case findings (`usecase.step-unbacked`, `usecase.step-unlinked`,
|
|
3200
|
+
`usecase.requirement-unresolved`, `usecase.capability-unresolved`) carry the flow FILE as
|
|
3201
|
+
`locations[0]` with role `primary` and the service directory beside it as `scope`. They carried the
|
|
3202
|
+
service directory alone, so a `--json` consumer had to parse the prose to learn which `.likec4` to
|
|
3203
|
+
open. `usecase.flow-invalid` on the same arm already named its file.
|
|
3204
|
+
- `usecase.flow-invalid`'s undeclared-tag hint tells a `cap-` author the placement rule instead of a
|
|
3205
|
+
declaration that cannot help: a capability is claimed at fleet altitude, so declaring `tag cap-…`
|
|
3206
|
+
beside a model makes the file parse and earns `usecase.capability-unresolved` on the next run. The
|
|
3207
|
+
`req-` hint is unchanged.
|
|
3208
|
+
- The services tree walk never classifies a service's own reserved subdirectory (`adrs`, `usecases`,
|
|
3209
|
+
`ui` — SCHEMA's service layout) as a service or a subsystem. Writing the documented
|
|
3210
|
+
`services/<id>/usecases/<name>.likec4` slot into a directory holding no artifact FILE used to make
|
|
3211
|
+
the walk read `usecases` as a service and its parent as an unmarked group: three errors
|
|
3212
|
+
(`subsystem.unmarked`, `landscape.service-unmodelled` for `services/<id>/usecases/`,
|
|
3213
|
+
`landscape.binding-unknown` for the real service) earned by following the instruction `loam init`
|
|
3214
|
+
had just scaffolded. Such a directory is also not counted when deciding whether a directory is a
|
|
3215
|
+
group, so a real service filed beside a reserved slot still refuses with `subsystem.unmarked` and
|
|
3216
|
+
is still named.
|
|
3217
|
+
- `loam` now strips a leading UTF-8 byte-order mark before parsing the root `likec4.config.json`.
|
|
3218
|
+
PowerShell's `Out-File` writes one by default, so a Windows shell saving the file loam itself wrote
|
|
3219
|
+
left every `exclude` grade silent while the renderer went on applying the list (measured:
|
|
3220
|
+
`npx likec4 validate .` at 3 source files of 8 with `services/**` in force, `loam validate --all`
|
|
3221
|
+
at 0 errors and `doctor` healthy). A rewrite through `loam subsystem sync` drops the mark.
|
|
3222
|
+
- `service.model-excluded` asks the FILE question too. An `exclude` entry shaped like a file
|
|
3223
|
+
(`services/**/model.likec4`, `**/model.likec4`) hides a model as thoroughly as one naming its
|
|
3224
|
+
directory, and nothing said so: on `examples/docs` such a line took the renderer to 3 source files
|
|
3225
|
+
of 8 while `loam validate --all` printed the string "exclude" not once. It is reported now, with
|
|
3226
|
+
the repair that fits it — `loam subsystem sync` maintains only the `services/` directory entries,
|
|
3227
|
+
so the file shape has to be narrowed or deleted by hand, and the message says which shape you have.
|
|
3228
|
+
- `loam seed` no longer overwrites a hand edit written ABOVE the scaffold's landscape stub.
|
|
3229
|
+
`isLandscapeStub` matched with `endsWith`, so anything a person put before the stub was invisible
|
|
3230
|
+
to the guard: a two-line "OWNED BY THE ARCHITECTURE GUILD — do not regenerate" header was replaced
|
|
3231
|
+
without a word while seed printed "replaced the scaffold's untouched stub", against its own refusal
|
|
3232
|
+
text ("Seed never overwrites human work"). The suffix match existed for exactly one prefix — the
|
|
3233
|
+
comment preamble `migrate-openspec` stages above the identical stub, closed by a bare `//` line —
|
|
3234
|
+
so that is now the only prefix accepted, at every scaffold generation. What a repo notices:
|
|
3235
|
+
`loam seed` refuses with `seed-landscape-edited` and writes nothing, and `loam status` calls such a
|
|
3236
|
+
map `authored` rather than `stub`. A repo staged by `migrate-openspec` still seeds.
|
|
3237
|
+
- `loam doctor` counts the report corpus that is there when the config is the thing that is broken.
|
|
3238
|
+
Read from a SUBDIRECTORY of a repo whose `loam.json` does not parse, it used to report
|
|
3239
|
+
`total 0, next 001` while naming the broken config's own path in the same envelope — handing out an
|
|
3240
|
+
ordinal five existing reports already used. `doctor` is the preflight that runs precisely when the
|
|
3241
|
+
config may not resolve, so `reports.dir` now resolves beside the `loam.json` it FOUND.
|
|
3242
|
+
- `/loam-check`'s `c4.fleet-project-invalid` row spells the `details` format the finding emits:
|
|
3243
|
+
`<path>: L<n>: <error>`, with the colon after the path that the message's own head has always
|
|
3244
|
+
carried. The row promised `<path> L<n>: <error>`, so an agent splitting on the documented separator
|
|
3245
|
+
split in the wrong place.
|
|
3246
|
+
|
|
3247
|
+
## [0.2.0-alpha.5] - 2026-09-03
|
|
3248
|
+
|
|
3249
|
+
An adopted service's model renders from the docs root. Still a prerelease on the `alpha` line:
|
|
3250
|
+
`npm i @ybotok/loam@alpha` installs it. Nothing below is breaking. What a repo with adopted services
|
|
3251
|
+
NOTICES: one `service.likec4-config-missing` warning per service on its first `validate --all`
|
|
3252
|
+
until one `loam subsystem sync`, and from that sync on `--project` for `likec4 validate` at the
|
|
3253
|
+
docs root.
|
|
3254
|
+
|
|
3255
|
+
### Added — an adopted service's model renders from the docs root
|
|
3256
|
+
|
|
3257
|
+
The root `likec4.config.json` excludes `services/**`, and has to; what that cost was never stated:
|
|
3258
|
+
an adopted service's `model.likec4` — the artifact `loam adopt` spends the most effort on — was a
|
|
3259
|
+
box on the fleet map with nothing renderable inside it in the renderer opened at the docs root, and
|
|
3260
|
+
the only documented way to see its containers was one `npx likec4 start services/<id>` per service.
|
|
3261
|
+
One problem report; one file per service closes it.
|
|
3262
|
+
|
|
3263
|
+
- `loam subsystem sync` now writes `services/<…>/<svc>/likec4.config.json` beside every service
|
|
3264
|
+
`model.likec4` that has none, whenever the root `likec4.config.json` exists — **create-only**: a
|
|
3265
|
+
file that exists is yours and is never compared, rewritten or removed. Measured at the pinned
|
|
3266
|
+
likec4, a project file inside a service directory registers the model as a project of its own
|
|
3267
|
+
regardless of the root's exclusion, so `npx likec4 start` at the docs root shows `fleet` plus one
|
|
3268
|
+
project per service. Without the root file nothing is written and the text says `loam doctor`
|
|
3269
|
+
prints it. `subsystem sync --json` carries an additive `projects` key, `{ root, created, current }`
|
|
3270
|
+
— whether the root file exists, the repo-relative paths written this run (sorted by id), and how
|
|
3271
|
+
many model-bearing services already had one. `action` still describes
|
|
3272
|
+
`architecture/subsystems.likec4` alone.
|
|
3273
|
+
- `loam validate --all` gains one warning per service, `service.likec4-config-missing`, on the
|
|
3274
|
+
`landscape` target with the service's tree path as its `scope`: `model.likec4` exists and the
|
|
3275
|
+
project file does not. Never an error, never gating, never in `--service`, `status` or `doctor`;
|
|
3276
|
+
the fix is `loam subsystem sync`, never a hand-written file. `doctor.likec4-config-missing`'s fix
|
|
3277
|
+
text now hands over to `sync` once the root file is written.
|
|
3278
|
+
- The project name: the id with every `.` folded to `-` (LikeC4's name grammar refuses the byte),
|
|
3279
|
+
prefixed `service-` when that would equal the root project's `fleet` or LikeC4's reserved
|
|
3280
|
+
`default`; `title` is the id verbatim, and the title is what the renderer's picker shows. Not
|
|
3281
|
+
injective — an id spelled billing.core beside one spelled billing-core, or `service-fleet` beside
|
|
3282
|
+
`fleet` — and LikeC4 suffixes the later one `-1`; disclosed, never graded.
|
|
3283
|
+
|
|
3284
|
+
What a repo NOTICES on upgrade: one warning per adopted service on the first `validate --all` until
|
|
3285
|
+
one `loam subsystem sync` — under `--strict`, CI is red until then; after that `sync`, N untracked
|
|
3286
|
+
files (`sync` does not `git add` them) and `move-uncommitted` on the next `loam subsystem move` of
|
|
3287
|
+
such a service until they are committed; from the first `sync` on, `likec4 validate` at the docs
|
|
3288
|
+
root needs `--project <name>` (measured: with more than one project it refuses bare, while
|
|
3289
|
+
`likec4 build` and `likec4 export` take every project and need no flag); and the renderer's project
|
|
3290
|
+
list grows by one per adopted service, beside `fleet`. No command, flag, exit code or envelope key
|
|
3291
|
+
changes; `projects` is additive.
|
|
3292
|
+
|
|
3293
|
+
## [0.2.0-alpha.4] - 2026-09-03
|
|
3294
|
+
|
|
3295
|
+
The view title becomes the subsystem's path. `0.2.0-alpha.3` was tagged and **never published** — it
|
|
3296
|
+
was withdrawn before its publish step for a reason unrelated to the code — so this is the release
|
|
3297
|
+
that carries both it and the entry below, and `npm i @ybotok/loam@alpha` moves from `0.2.0-alpha.2`
|
|
3298
|
+
straight to this one. A repo upgrading from either reports one `subsystem.views-stale` until one
|
|
3299
|
+
`loam subsystem sync`.
|
|
3300
|
+
|
|
3301
|
+
### Changed — a generated subsystem view is titled by its path
|
|
3302
|
+
|
|
3303
|
+
`0.2.0-alpha.3` gave each view the subsystem's own label. It now carries the whole path — every
|
|
3304
|
+
marked ancestor's label and its own, joined with ` / `, so `services/platform/order-flow` reads
|
|
3305
|
+
`Platform / Order flow`. Measured against the pinned likec4: the renderer reads that as a folder
|
|
3306
|
+
path, so the view list mirrors the directory tree a human already navigates — one folder per
|
|
3307
|
+
ancestor, the leaf's own label as the title — and two subsystems whose labels are equal are told
|
|
3308
|
+
apart by their parents instead of appearing twice under one name. The box a view draws keeps the
|
|
3309
|
+
subsystem's own label, unprefixed.
|
|
3310
|
+
|
|
3311
|
+
Because loam is now the one composing with `/`, it owns what a `/` inside an authored label would
|
|
3312
|
+
do: measured, LikeC4 would show only the last segment and add a folder level the tree does not have,
|
|
3313
|
+
so a slash in a marker's `title` becomes `∕` (U+2215) and the whole authored label stays visible as
|
|
3314
|
+
one folder name. The alternative — a fixed `loam subsystems /` prefix — bought the same folder
|
|
3315
|
+
without the churn and put loam's own name in a reader's UI; the tree's own words won.
|
|
3316
|
+
|
|
3317
|
+
Two costs, both real: the generated bytes move again, so a repo upgrading from `0.2.0-alpha.3`
|
|
3318
|
+
reports one `subsystem.views-stale` until one `loam subsystem sync`; and renaming a PARENT's title
|
|
3319
|
+
now rewrites the bytes of every view beneath it.
|
|
3320
|
+
|
|
3321
|
+
## [0.2.0-alpha.3] - 2026-09-03
|
|
3322
|
+
|
|
3323
|
+
The generated subsystem views, made readable — and the defect that hunt found underneath them.
|
|
3324
|
+
Still a prerelease on the `alpha` line. **Not published (noted 2026-10-10):** the `v0.2.0-alpha.3`
|
|
3325
|
+
tag was cut, but it was withdrawn before its publish step — the `0.2.0-alpha.4` section above says
|
|
3326
|
+
so, and that release carries everything here — so `npm i @ybotok/loam@alpha` never installed this
|
|
3327
|
+
version, and `npm view @ybotok/loam dist-tags` is the record of what does. Nothing below is
|
|
3328
|
+
breaking, and one entry is a change every repo NOTICES: the generated file's bytes move, so the
|
|
3329
|
+
first `loam validate --all` after upgrading asks for one `loam subsystem sync`.
|
|
3330
|
+
|
|
3331
|
+
### Fixed — a `subsystem.views-stale` no command could clear
|
|
3332
|
+
|
|
3333
|
+
`loam validate --all` grades `architecture/subsystems.likec4` against the whole `architecture/`
|
|
3334
|
+
project — the landscape merged with every `architecture/usecases/*.likec4` — while `loam subsystem
|
|
3335
|
+
sync` and the write verbs rendered it from `landscape.likec4` alone. One use-case document declaring
|
|
3336
|
+
an element bound to a filed service was therefore enough to make `sync` answer `current` while
|
|
3337
|
+
`validate --all` exited 1 with `subsystem.views-stale`, whose message names `sync` as the repair.
|
|
3338
|
+
Reproduced on `examples/docs`; the two now read the same documents. No code, flag or payload key
|
|
3339
|
+
changes.
|
|
3340
|
+
|
|
3341
|
+
### Changed — the generated subsystem views carry a label and a boundary
|
|
3342
|
+
|
|
3343
|
+
`architecture/subsystems.likec4` exists only to be rendered, and in the renderer its views were the
|
|
3344
|
+
least readable objects in a docs repo: no title, so LikeC4 showed the hex-escaped id
|
|
3345
|
+
(`subsystem_platform__order_2dflow`), and leaf includes only, so a fleet drawn as ordinary
|
|
3346
|
+
grouped C4 rendered flat. Every view now carries `title` (the marker's, or the directory name) and
|
|
3347
|
+
`description` when the marker authors one, and draws a boundary: the model's own containing element
|
|
3348
|
+
when one holds exactly the subsystem's members, a `group '<title>'` mirroring the directory subtree
|
|
3349
|
+
when the landscape nests nothing, and — when the members are nested but no element owns exactly them
|
|
3350
|
+
— no box plus one comment line, `// model: no boundary — …`, which is loam's whole answer to a tree
|
|
3351
|
+
and a map that disagree. Nothing grades that comment: placement still carries no policy.
|
|
3352
|
+
|
|
3353
|
+
Marker text is normalized to one line and escaped before it is written (measured: a raw `'` in a
|
|
3354
|
+
title is two parse errors and blanks the fleet's whole map in the renderer; `Ops' } view evil {` is
|
|
3355
|
+
zero errors and injects a view). [SCHEMA.md](SCHEMA.md) states the rules.
|
|
3356
|
+
|
|
3357
|
+
What a repo notices, and what it costs:
|
|
3358
|
+
|
|
3359
|
+
- the first `loam validate --all` after upgrading reports exactly one `subsystem.views-stale` error
|
|
3360
|
+
(exit 1) until one `loam subsystem sync` runs. `sync` does not `git add`, so the repair leaves one
|
|
3361
|
+
unstaged one-file diff to commit;
|
|
3362
|
+
- the boundary depends on which OTHER services sit under an element, so an unrelated adoption or
|
|
3363
|
+
archive can flip a view from boxed to flat and restale the file. `loam archive` now withholds its
|
|
3364
|
+
"living spec + landscape are now complete + current" line, prints one warning naming
|
|
3365
|
+
`loam subsystem sync`, and adds `subsystemViewsStale: true` to its `--json` payload when it left
|
|
3366
|
+
the file behind;
|
|
3367
|
+
- `subsystem.views-stale` and `subsystem.view-id-collision` now carry `locations`: the generated
|
|
3368
|
+
file (or `architecture/` when it does not exist yet) and the claimant document respectively. Both
|
|
3369
|
+
used to fall back to the landscape, sending an editor to a file with nothing to fix in it.
|
|
3370
|
+
|
|
3371
|
+
No command, flag, exit code, envelope key or stable code string changes; `subsystem.views-stale` is
|
|
3372
|
+
not in the `IssueCode` union and carries no `gates` key, so `loam archive` remains unaffected by it.
|
|
3373
|
+
|
|
3374
|
+
## [0.2.0-alpha.2] - 2026-09-02
|
|
3375
|
+
|
|
3376
|
+
The deployment axis, and the two defects the axis surfaced on its way in. Still a prerelease on the
|
|
3377
|
+
`alpha` line: `npm i @ybotok/loam@alpha` installs it. Nothing below is breaking, but two entries are
|
|
3378
|
+
changes a repo can NOTICE — a fleet that had already placed a `#obl-` tag on a `deploymentNode`
|
|
3379
|
+
starts seeing findings it was never shown, and `loam status` stops asking an architecture-only
|
|
3380
|
+
feature for a business spec.
|
|
3381
|
+
|
|
3382
|
+
### Added — the deployment axis: topology joined to requirements
|
|
3383
|
+
|
|
3384
|
+
A LikeC4 `deployment { }` block has always been legal in a docs repo and was completely unread. The
|
|
3385
|
+
parser resolved every `instanceOf`, and after that no requirement could name a node, no `#obl-` tag
|
|
3386
|
+
on a datacenter was graded, and the context pack an agent implements from did not mention topology
|
|
3387
|
+
at all. Five things change, and a fleet that draws no topology is unaffected by every one of them —
|
|
3388
|
+
the axis opts in on the block's existence, the way the obligation vocabulary opts in on its file.
|
|
3389
|
+
|
|
3390
|
+
- **`Covers:` gains a fourth entry form.** `node:<id>` names a deployment node or an instance, and
|
|
3391
|
+
`node:a -> node:b` names an edge between them; both match literally. This is the form that lets an
|
|
3392
|
+
architecture requirement about replication name the two clusters it is written about, and a
|
|
3393
|
+
topology change that renames one now reports `covers.unknown` with the real ids offered — spelled
|
|
3394
|
+
with the prefix, because that is the line the author has to type. Both sides of an edge carry the
|
|
3395
|
+
prefix or neither; a bare id stays an element entry.
|
|
3396
|
+
- **An obligation on a deployment object is graded.** `obligation.unknown`, `obligation.unapplied`
|
|
3397
|
+
and `obligation.uncovered` now walk the topology as well as the logical model. This is a
|
|
3398
|
+
behaviour change a repo can notice: a `#obl-` tag on a `deploymentNode` used to be invisible to
|
|
3399
|
+
every check, so a fleet that had placed one starts seeing findings it was never shown. A
|
|
3400
|
+
datacenter is owned by no service, so `obligation.uncovered` drops `subject` there and names the
|
|
3401
|
+
requirement's home in general terms rather than picking a team.
|
|
3402
|
+
- **A feature can bring topology.** `features/<FEAT>/deployment/<name>.likec4` is a create-only
|
|
3403
|
+
document `loam archive` copies into `architecture/` and `loam unarchive` takes back — the third
|
|
3404
|
+
whole-file slot, beside `usecases/` and `glossary/`. Say `extend` in it to add to a region the
|
|
3405
|
+
living map already declares; two features may extend one region from documents of their own and
|
|
3406
|
+
both archive. Two new codes, both errors and neither overridable: **`deployment.doc-exists`** when
|
|
3407
|
+
the feature's document names a file the living tree already holds, and **`deployment.doc-invalid`**
|
|
3408
|
+
when it cannot be read against the map the feature's own merge would leave behind. The second is
|
|
3409
|
+
what makes the ordinary case legal — a document instancing a service only this feature's
|
|
3410
|
+
`delta.likec4` adds resolves, because the corpus inside a feature window is feature ∪ living — and
|
|
3411
|
+
refuses the case where the merge does not land what the document names. `delta.likec4` still
|
|
3412
|
+
refuses a `deployment { }` block and now names this slot instead of the living landscape.
|
|
3413
|
+
A third, **`deployment.doc-reserved`**, refuses a name loam owns inside `architecture/` —
|
|
3414
|
+
`subsystems.likec4`, `landscape.likec4`, or a path under `usecases/` — whether or not the target
|
|
3415
|
+
exists, because two of the three fail silently when it does not: nothing parses the generated
|
|
3416
|
+
subsystem views and `loam subsystem sync` overwrites them wholesale.
|
|
3417
|
+
- **`loam context` carries the topology.** `--json` gains `living.deployment` — the service's
|
|
3418
|
+
instances, and the deployment edges touching them from either end.
|
|
3419
|
+
- **One more statement in the adoption brief's unchecked list**, now sixteen: nothing here knows
|
|
3420
|
+
whether the second cluster exists, is reachable, or holds the data a requirement claims, and no
|
|
3421
|
+
check counts datacenters — how many a service needs is a decision the fleet writes down as a
|
|
3422
|
+
requirement, never one loam evaluates.
|
|
3423
|
+
|
|
3424
|
+
### Fixed — the never-overridable archive refusal names what actually refused
|
|
3425
|
+
|
|
3426
|
+
It said `N element binding(s) name an illegal service id` for every code in that register, and had
|
|
3427
|
+
done since the use-case slot put a second one there — about features that bind nothing. The headline
|
|
3428
|
+
now counts the refusals and the closing line names their codes; each issue keeps its own sentence.
|
|
3429
|
+
The `--json` envelope is unchanged: consumers branch on `issues[].code` and `overridable`, which is
|
|
3430
|
+
what that split exists for.
|
|
3431
|
+
|
|
3432
|
+
### Fixed — `loam status` no longer invents work for an architecture-only feature
|
|
3433
|
+
|
|
3434
|
+
A feature whose only requirement delta is `specs/<svc>/arch.spec.md` reported two things that were
|
|
3435
|
+
not true, and both are corrected. The `spec` row read `missing` and `next.author-spec` told the
|
|
3436
|
+
author to write business requirements the change does not have — while `loam validate --feature`
|
|
3437
|
+
accepted the same tree at exit 0 and `coherence`'s `service.no-requirement-delta` stayed silent,
|
|
3438
|
+
because every other reader counts the service DIRECTORY and not which corpus is in it. And the
|
|
3439
|
+
`verification` row read `blocked` with `blockedBy: ["delta", "spec", "openapi", "asyncapi"]`,
|
|
3440
|
+
telling the reader there was nothing to derive a checklist from — while `featureChecklist` reads
|
|
3441
|
+
`arch.spec.md` and `loam verify` on that feature returns a real checklist.
|
|
3442
|
+
|
|
3443
|
+
The fleet form carried the same defect independently, and it broke the rule stated one line below
|
|
3444
|
+
it in its own source: `loam status` with no feature reported `missing <svc>/spec` and staged the
|
|
3445
|
+
feature `missing`, over a feature `loam status <FEAT>` called `ready`. Both forms now accept either
|
|
3446
|
+
corpus as the service's requirement delta, and `<svc>/spec` is named only when neither exists.
|
|
3447
|
+
|
|
3448
|
+
Three payload-visible consequences, all in `loam status --json`: `artifacts[]`'s `spec` row is
|
|
3449
|
+
`required: false` when the service's `arch.spec.md` delta exists (it was unconditionally `true`),
|
|
3450
|
+
`arch-spec` now counts as a verification feeder, so `blockedBy` gained `"arch-spec"` in the
|
|
3451
|
+
one case that is still genuinely blocked — a feature with no requirement delta of either kind —
|
|
3452
|
+
and `features[]`'s `missing` and `stage` no longer report an architecture-only feature as
|
|
3453
|
+
incomplete. No finding code, flag or exit code changes. This is the change to read if a script
|
|
3454
|
+
branched on `artifacts[].required` or on `features[].missing`.
|
|
3455
|
+
|
|
3456
|
+
The case is the one `arch.spec.md` exists for: an outbox, a retry policy, an alert — obligations no
|
|
3457
|
+
business scenario was ever going to carry, so the feature that adds one has no business requirement
|
|
3458
|
+
to write. Found by authoring loam's own architecture change through loam's own lifecycle, which is
|
|
3459
|
+
the trigger ROADMAP's self-hosting section records for that axis.
|
|
3460
|
+
|
|
13
3461
|
## [0.2.0-alpha.1] - 2026-09-01
|
|
14
3462
|
|
|
15
3463
|
The first release of the 0.2 line, and the whole accumulated body since `0.1.0-beta.3` — 116
|