@ybotok/loam 0.2.0-alpha.7 → 0.2.0-alpha.9
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +1315 -1
- package/COMPARISON.md +91 -3
- package/CONTRIBUTING.md +37 -0
- package/MIGRATING-from-OpenSpec.md +40 -22
- package/QUICKSTART.md +181 -9
- package/README.md +256 -57
- package/ROADMAP.md +83 -7
- package/SCHEMA.md +569 -98
- package/WORKFLOW.md +272 -62
- package/dist/cli.js +26 -8
- package/dist/cli.js.map +1 -1
- package/dist/commands/adopt/adopt.js +5 -10
- package/dist/commands/adopt/adopt.js.map +1 -1
- package/dist/commands/archive/archive.js +15 -18
- package/dist/commands/archive/archive.js.map +1 -1
- package/dist/commands/archive/plan/contracts/asyncapi.js +27 -41
- package/dist/commands/archive/plan/contracts/asyncapi.js.map +1 -1
- package/dist/commands/archive/plan/contracts/consumers.js +56 -0
- package/dist/commands/archive/plan/contracts/consumers.js.map +1 -0
- package/dist/commands/archive/plan/contracts/openapi.js +25 -42
- package/dist/commands/archive/plan/contracts/openapi.js.map +1 -1
- package/dist/commands/archive/plan/contracts/verdict-lines.js +41 -0
- package/dist/commands/archive/plan/contracts/verdict-lines.js.map +1 -0
- package/dist/commands/archive/plan/gate.js +3 -7
- package/dist/commands/archive/plan/gate.js.map +1 -1
- package/dist/commands/archive/plan/landscape.js +4 -4
- package/dist/commands/archive/plan/landscape.js.map +1 -1
- package/dist/commands/archive/plan/refusal.js +8 -18
- package/dist/commands/archive/plan/refusal.js.map +1 -1
- package/dist/commands/archive/plan/specs.js +134 -25
- package/dist/commands/archive/plan/specs.js.map +1 -1
- package/dist/commands/archive/plan/state.js +1 -0
- package/dist/commands/archive/plan/state.js.map +1 -1
- package/dist/commands/archive/run.js +33 -11
- package/dist/commands/archive/run.js.map +1 -1
- package/dist/commands/archive/shipped.js +49 -0
- package/dist/commands/archive/shipped.js.map +1 -0
- package/dist/commands/context/context.js +4 -7
- package/dist/commands/context/context.js.map +1 -1
- package/dist/commands/context/print.js +2 -2
- package/dist/commands/context/print.js.map +1 -1
- package/dist/commands/delta/delta.js +44 -12
- package/dist/commands/delta/delta.js.map +1 -1
- package/dist/commands/delta/print.js +1 -1
- package/dist/commands/delta/print.js.map +1 -1
- package/dist/commands/dependencies.js +3 -7
- package/dist/commands/dependencies.js.map +1 -1
- package/dist/commands/diff/diff.js +20 -27
- package/dist/commands/diff/diff.js.map +1 -1
- package/dist/commands/explore.js +4 -8
- package/dist/commands/explore.js.map +1 -1
- package/dist/commands/gate/gate.js +45 -8
- package/dist/commands/gate/gate.js.map +1 -1
- package/dist/commands/gate/print.js +15 -3
- package/dist/commands/gate/print.js.map +1 -1
- package/dist/commands/gherkin/commit.js +13 -54
- package/dist/commands/gherkin/commit.js.map +1 -1
- package/dist/commands/gherkin/gherkin.js +36 -20
- package/dist/commands/gherkin/gherkin.js.map +1 -1
- package/dist/commands/gherkin/reconcile.js +10 -13
- package/dist/commands/gherkin/reconcile.js.map +1 -1
- package/dist/commands/init/first-hour.js +44 -0
- package/dist/commands/init/first-hour.js.map +1 -1
- package/dist/commands/init/init.js +18 -22
- package/dist/commands/init/init.js.map +1 -1
- package/dist/commands/list/campaign/campaign.js +4 -6
- package/dist/commands/list/campaign/campaign.js.map +1 -1
- package/dist/commands/list/json.js +0 -4
- package/dist/commands/list/json.js.map +1 -1
- package/dist/commands/list/list.js +38 -23
- package/dist/commands/list/list.js.map +1 -1
- package/dist/commands/list/print.js +38 -6
- package/dist/commands/list/print.js.map +1 -1
- package/dist/commands/list/suites/suites.js +140 -0
- package/dist/commands/list/suites/suites.js.map +1 -0
- package/dist/commands/list/views.js +28 -33
- package/dist/commands/list/views.js.map +1 -1
- package/dist/commands/mcp/dispatch.js +91 -36
- package/dist/commands/mcp/dispatch.js.map +1 -1
- package/dist/commands/mcp/mcp.js +56 -15
- package/dist/commands/mcp/mcp.js.map +1 -1
- package/dist/commands/mcp/serve.js.map +1 -1
- package/dist/commands/migrate-openspec/materialize/active.js +27 -22
- package/dist/commands/migrate-openspec/materialize/active.js.map +1 -1
- package/dist/commands/migrate-openspec/materialize/stage.js +4 -3
- package/dist/commands/migrate-openspec/materialize/stage.js.map +1 -1
- package/dist/commands/migrate-openspec/materialize/target.js +2 -2
- package/dist/commands/migrate-openspec/materialize/target.js.map +1 -1
- package/dist/commands/migrate-openspec/openspec/decisions.js +9 -14
- package/dist/commands/migrate-openspec/openspec/decisions.js.map +1 -1
- package/dist/commands/migrate-openspec/openspec/error.js +7 -8
- package/dist/commands/migrate-openspec/openspec/error.js.map +1 -1
- package/dist/commands/migrate-openspec/openspec/mapping.js +1 -9
- package/dist/commands/migrate-openspec/openspec/mapping.js.map +1 -1
- package/dist/commands/migrate-openspec/openspec/paths.js +3 -6
- package/dist/commands/migrate-openspec/openspec/paths.js.map +1 -1
- package/dist/commands/new/capability-template.js +118 -0
- package/dist/commands/new/capability-template.js.map +1 -0
- package/dist/commands/new/commit.js +8 -22
- package/dist/commands/new/commit.js.map +1 -1
- package/dist/commands/new/modifies.js +206 -0
- package/dist/commands/new/modifies.js.map +1 -0
- package/dist/commands/new/new.js +59 -14
- package/dist/commands/new/new.js.map +1 -1
- package/dist/commands/new/templates.js +48 -124
- package/dist/commands/new/templates.js.map +1 -1
- package/dist/commands/open.js +3 -7
- package/dist/commands/open.js.map +1 -1
- package/dist/commands/policy/gate.js +27 -0
- package/dist/commands/policy/gate.js.map +1 -1
- package/dist/commands/rebase/plan.js +2 -2
- package/dist/commands/rebase/plan.js.map +1 -1
- package/dist/commands/rebase/rebase.js +60 -35
- package/dist/commands/rebase/rebase.js.map +1 -1
- package/dist/commands/seed/commit.js +21 -25
- package/dist/commands/seed/commit.js.map +1 -1
- package/dist/commands/seed/seed.js +6 -9
- package/dist/commands/seed/seed.js.map +1 -1
- package/dist/commands/show/show.js +3 -7
- package/dist/commands/show/show.js.map +1 -1
- package/dist/commands/status/print.js +13 -4
- package/dist/commands/status/print.js.map +1 -1
- package/dist/commands/status/status.js +3 -7
- package/dist/commands/status/status.js.map +1 -1
- package/dist/commands/steps/steps.js +3 -7
- package/dist/commands/steps/steps.js.map +1 -1
- package/dist/commands/subsystem/edit.js +3 -5
- package/dist/commands/subsystem/edit.js.map +1 -1
- package/dist/commands/subsystem/move.js +5 -8
- 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 +14 -41
- package/dist/commands/subsystem/sync.js.map +1 -1
- package/dist/commands/subsystem/txn/txn.js +17 -38
- package/dist/commands/subsystem/txn/txn.js.map +1 -1
- package/dist/commands/subsystem/txn/views.js +32 -5
- package/dist/commands/subsystem/txn/views.js.map +1 -1
- package/dist/commands/tasks/render.js +116 -0
- package/dist/commands/tasks/render.js.map +1 -0
- package/dist/commands/tasks/tasks.js +113 -0
- package/dist/commands/tasks/tasks.js.map +1 -0
- package/dist/commands/unarchive/manifest.js +6 -9
- package/dist/commands/unarchive/manifest.js.map +1 -1
- package/dist/commands/unarchive/restore.js +17 -28
- package/dist/commands/unarchive/restore.js.map +1 -1
- package/dist/commands/unarchive/unarchive.js +22 -30
- package/dist/commands/unarchive/unarchive.js.map +1 -1
- package/dist/commands/validate/arch-coverage.js +6 -15
- package/dist/commands/validate/arch-coverage.js.map +1 -1
- package/dist/commands/validate/checks/fleet-shape.js +4 -49
- package/dist/commands/validate/checks/fleet-shape.js.map +1 -1
- package/dist/commands/validate/checks/requirements.js +18 -8
- package/dist/commands/validate/checks/requirements.js.map +1 -1
- package/dist/commands/validate/checks/sources.js +6 -2
- package/dist/commands/validate/checks/sources.js.map +1 -1
- package/dist/commands/validate/feature.js +23 -19
- package/dist/commands/validate/feature.js.map +1 -1
- package/dist/commands/validate/fleet/landscape.js +23 -23
- package/dist/commands/validate/fleet/landscape.js.map +1 -1
- package/dist/commands/validate/fleet/map/attest.js +6 -10
- package/dist/commands/validate/fleet/map/attest.js.map +1 -1
- package/dist/commands/validate/fleet/map/bindings.js +1 -3
- package/dist/commands/validate/fleet/map/bindings.js.map +1 -1
- package/dist/commands/validate/fleet/map/usecases.js +5 -11
- package/dist/commands/validate/fleet/map/usecases.js.map +1 -1
- package/dist/commands/validate/fleet/obligations.js +1 -5
- package/dist/commands/validate/fleet/obligations.js.map +1 -1
- package/dist/commands/validate/fleet/scorecard/print.js +5 -1
- package/dist/commands/validate/fleet/scorecard/print.js.map +1 -1
- package/dist/commands/validate/fleet/scorecard/scorecard.js +17 -11
- package/dist/commands/validate/fleet/scorecard/scorecard.js.map +1 -1
- package/dist/commands/validate/fleet/usecases/usecases.js +6 -4
- package/dist/commands/validate/fleet/usecases/usecases.js.map +1 -1
- package/dist/commands/validate/fleet/views/fleet-project.js.map +1 -1
- package/dist/commands/validate/fleet/views/root-documents.js +8 -28
- package/dist/commands/validate/fleet/views/root-documents.js.map +1 -1
- package/dist/commands/validate/links/corpus.js +1 -4
- package/dist/commands/validate/links/corpus.js.map +1 -1
- package/dist/commands/validate/links/glossary.js.map +1 -1
- package/dist/commands/validate/report.js +5 -4
- package/dist/commands/validate/report.js.map +1 -1
- package/dist/commands/validate/scope.js +113 -0
- package/dist/commands/validate/scope.js.map +1 -0
- package/dist/commands/validate/service/api.js +16 -104
- package/dist/commands/validate/service/api.js.map +1 -1
- package/dist/commands/validate/service/contracts/generated.js +134 -0
- package/dist/commands/validate/service/contracts/generated.js.map +1 -0
- package/dist/commands/validate/service/events/events.js +1 -1
- package/dist/commands/validate/service/events/events.js.map +1 -1
- package/dist/commands/validate/service/events/message-producers.js +1 -1
- package/dist/commands/validate/service/events/message-producers.js.map +1 -1
- package/dist/commands/validate/service/evidence-pins.js +56 -10
- package/dist/commands/validate/service/evidence-pins.js.map +1 -1
- package/dist/commands/validate/service/service.js +16 -26
- package/dist/commands/validate/service/service.js.map +1 -1
- package/dist/commands/validate/service/specs.js +31 -38
- package/dist/commands/validate/service/specs.js.map +1 -1
- package/dist/commands/validate/service/spine.js +8 -11
- package/dist/commands/validate/service/spine.js.map +1 -1
- package/dist/commands/validate/service/usecases/usecases.js +3 -3
- package/dist/commands/validate/service/usecases/usecases.js.map +1 -1
- package/dist/commands/validate/validate.js +44 -26
- package/dist/commands/validate/validate.js.map +1 -1
- package/dist/commands/verify/cross/diff.js +2 -6
- package/dist/commands/verify/cross/diff.js.map +1 -1
- package/dist/commands/verify/evidence/contract.js +1 -1
- package/dist/commands/verify/evidence/contract.js.map +1 -1
- package/dist/commands/verify/evidence/read.js +12 -3
- package/dist/commands/verify/evidence/read.js.map +1 -1
- package/dist/commands/verify/frozen.js +45 -23
- package/dist/commands/verify/frozen.js.map +1 -1
- package/dist/commands/verify/record.js +69 -17
- package/dist/commands/verify/record.js.map +1 -1
- package/dist/commands/verify/report.js +27 -44
- package/dist/commands/verify/report.js.map +1 -1
- package/dist/commands/verify/results.js +112 -30
- package/dist/commands/verify/results.js.map +1 -1
- package/dist/commands/verify/verify.js +54 -51
- package/dist/commands/verify/verify.js.map +1 -1
- package/dist/commands/vouch/pack/pack.js +2 -2
- package/dist/commands/vouch/pack/pack.js.map +1 -1
- package/dist/commands/vouch/pack/print.js +1 -6
- package/dist/commands/vouch/pack/print.js.map +1 -1
- package/dist/commands/vouch/pack/sections.js +1 -2
- package/dist/commands/vouch/pack/sections.js.map +1 -1
- package/dist/commands/vouch/run.js +26 -81
- package/dist/commands/vouch/run.js.map +1 -1
- package/dist/commands/vouch/sample/plan.js +20 -34
- package/dist/commands/vouch/sample/plan.js.map +1 -1
- package/dist/commands/vouch/vet/verify.js +39 -7
- package/dist/commands/vouch/vet/verify.js.map +1 -1
- package/dist/commands/vouch/vouch.js +10 -17
- package/dist/commands/vouch/vouch.js.map +1 -1
- package/dist/core/agent/agents-md/command-map.js +56 -101
- package/dist/core/agent/agents-md/command-map.js.map +1 -1
- package/dist/core/agent/agents-md/map/explain.js +4 -3
- package/dist/core/agent/agents-md/map/explain.js.map +1 -1
- package/dist/core/agent/agents-md/map/lenses/context.js +4 -3
- package/dist/core/agent/agents-md/map/lenses/context.js.map +1 -1
- package/dist/core/agent/agents-md/map/lenses/diff.js +33 -20
- package/dist/core/agent/agents-md/map/lenses/diff.js.map +1 -1
- package/dist/core/agent/agents-md/map/lenses/gate.js +13 -5
- package/dist/core/agent/agents-md/map/lenses/gate.js.map +1 -1
- package/dist/core/agent/agents-md/map/mcp.js +10 -7
- package/dist/core/agent/agents-md/map/mcp.js.map +1 -1
- package/dist/core/agent/agents-md/map/status.js +9 -3
- package/dist/core/agent/agents-md/map/status.js.map +1 -1
- package/dist/core/agent/agents-md/map/subsystem.js +4 -3
- package/dist/core/agent/agents-md/map/subsystem.js.map +1 -1
- package/dist/core/agent/agents-md/map/usecases.js +24 -6
- package/dist/core/agent/agents-md/map/usecases.js.map +1 -1
- package/dist/core/agent/agents-md/refusals.js +32 -18
- package/dist/core/agent/agents-md/refusals.js.map +1 -1
- package/dist/core/agent/agents-md.js +9 -3
- package/dist/core/agent/agents-md.js.map +1 -1
- package/dist/core/agent/agents-stamp.js +10 -0
- package/dist/core/agent/agents-stamp.js.map +1 -1
- package/dist/core/agent/scaffold.js +10 -15
- package/dist/core/agent/scaffold.js.map +1 -1
- package/dist/core/agent/tools/dialects.js +1 -0
- package/dist/core/agent/tools/dialects.js.map +1 -1
- package/dist/core/agent/tools/registry.js +41 -76
- package/dist/core/agent/tools/registry.js.map +1 -1
- package/dist/core/agent/workflows/adopt.js +5 -3
- package/dist/core/agent/workflows/adopt.js.map +1 -1
- package/dist/core/agent/workflows/check.js +20 -16
- package/dist/core/agent/workflows/check.js.map +1 -1
- package/dist/core/agent/workflows/closing.js +54 -21
- package/dist/core/agent/workflows/closing.js.map +1 -1
- package/dist/core/agent/workflows/feature.js +79 -11
- package/dist/core/agent/workflows/feature.js.map +1 -1
- package/dist/core/agent/workflows/implement.js +91 -13
- package/dist/core/agent/workflows/implement.js.map +1 -1
- package/dist/core/agent/workflows/reference/authoring.js +3 -1
- package/dist/core/agent/workflows/reference/authoring.js.map +1 -1
- package/dist/core/agent/workflows/reference/codes.js +8 -6
- package/dist/core/agent/workflows/reference/codes.js.map +1 -1
- package/dist/core/agent/workflows/reference/done-check.js +37 -15
- package/dist/core/agent/workflows/reference/done-check.js.map +1 -1
- package/dist/core/agent/workflows/reference/reference.js.map +1 -1
- package/dist/core/agent/workflows/reference/spine.js +50 -18
- package/dist/core/agent/workflows/reference/spine.js.map +1 -1
- package/dist/core/asyncapi/baseline/plan.js +14 -81
- package/dist/core/asyncapi/baseline/plan.js.map +1 -1
- package/dist/core/asyncapi/digest.js +30 -25
- package/dist/core/asyncapi/digest.js.map +1 -1
- package/dist/core/asyncapi/merge/components.js +5 -4
- package/dist/core/asyncapi/merge/components.js.map +1 -1
- package/dist/core/asyncapi/merge/error.js +7 -2
- package/dist/core/asyncapi/merge/error.js.map +1 -1
- package/dist/core/asyncapi/merge/markers.js +30 -50
- package/dist/core/asyncapi/merge/markers.js.map +1 -1
- package/dist/core/asyncapi/merge/merge.js +31 -53
- package/dist/core/asyncapi/merge/merge.js.map +1 -1
- package/dist/core/asyncapi/merge/pin.js +10 -36
- package/dist/core/asyncapi/merge/pin.js.map +1 -1
- package/dist/core/asyncapi/model.js.map +1 -1
- package/dist/core/asyncapi/read.js +36 -12
- package/dist/core/asyncapi/read.js.map +1 -1
- package/dist/core/brief/brief.js +6 -18
- package/dist/core/brief/brief.js.map +1 -1
- package/dist/core/brief/landscape.js +8 -12
- package/dist/core/brief/landscape.js.map +1 -1
- package/dist/core/brief/map/extend.js +8 -5
- package/dist/core/brief/map/extend.js.map +1 -1
- package/dist/core/brief/map/owed.js +8 -26
- package/dist/core/brief/map/owed.js.map +1 -1
- package/dist/core/brief/targets.js +15 -5
- package/dist/core/brief/targets.js.map +1 -1
- package/dist/core/c4/likec4.js +147 -16
- package/dist/core/c4/likec4.js.map +1 -1
- package/dist/core/c4/locate/unlocated.js +150 -0
- package/dist/core/c4/locate/unlocated.js.map +1 -0
- package/dist/core/c4/parsed/values.js +54 -1
- package/dist/core/c4/parsed/values.js.map +1 -1
- package/dist/core/c4/prefetch/fleet.js +1 -9
- package/dist/core/c4/prefetch/fleet.js.map +1 -1
- package/dist/core/c4/project/documents.js +31 -6
- package/dist/core/c4/project/documents.js.map +1 -1
- package/dist/core/c4/project/load.js +6 -17
- package/dist/core/c4/project/load.js.map +1 -1
- package/dist/core/c4/resolve/external.js +51 -0
- package/dist/core/c4/resolve/external.js.map +1 -0
- package/dist/core/c4/resolve/service.js +25 -1
- package/dist/core/c4/resolve/service.js.map +1 -1
- package/dist/core/c4/seed/template.js +15 -1
- package/dist/core/c4/seed/template.js.map +1 -1
- package/dist/core/c4/service-model/shape.js +2 -22
- package/dist/core/c4/service-model/shape.js.map +1 -1
- package/dist/core/c4/source-mask.js +288 -4
- package/dist/core/c4/source-mask.js.map +1 -1
- package/dist/core/c4/source-scan.js +203 -147
- package/dist/core/c4/source-scan.js.map +1 -1
- package/dist/core/c4/splice/authored-source.js +19 -0
- package/dist/core/c4/splice/authored-source.js.map +1 -1
- package/dist/core/c4/splice/delta-blocks.js +5 -18
- package/dist/core/c4/splice/delta-blocks.js.map +1 -1
- package/dist/core/c4/splice/landscape-merge.js +13 -8
- package/dist/core/c4/splice/landscape-merge.js.map +1 -1
- package/dist/core/c4/splice/model/merge.js +10 -6
- package/dist/core/c4/splice/model/merge.js.map +1 -1
- package/dist/core/c4/splice/placement.js +108 -18
- package/dist/core/c4/splice/placement.js.map +1 -1
- package/dist/core/c4/workspace.js +13 -22
- package/dist/core/c4/workspace.js.map +1 -1
- package/dist/core/capabilities/capabilities.js +6 -6
- package/dist/core/capabilities/capabilities.js.map +1 -1
- package/dist/core/capabilities/delta/doc.js +7 -10
- package/dist/core/capabilities/delta/doc.js.map +1 -1
- package/dist/core/capabilities/findings.js +15 -20
- package/dist/core/capabilities/findings.js.map +1 -1
- package/dist/core/capabilities/realizes/findings.js +12 -19
- package/dist/core/capabilities/realizes/findings.js.map +1 -1
- package/dist/core/capabilities/realizes/join.js +0 -20
- package/dist/core/capabilities/realizes/join.js.map +1 -1
- package/dist/core/capabilities/rollup.js +6 -0
- package/dist/core/capabilities/rollup.js.map +1 -1
- package/dist/core/capabilities/tree.js +6 -28
- package/dist/core/capabilities/tree.js.map +1 -1
- package/dist/core/capabilities/usecase-join.js +3 -13
- package/dist/core/capabilities/usecase-join.js.map +1 -1
- package/dist/core/coherence/authoring/scaffold.js +11 -3
- package/dist/core/coherence/authoring/scaffold.js.map +1 -1
- package/dist/core/coherence/authoring/sentinels.js +2 -1
- package/dist/core/coherence/authoring/sentinels.js.map +1 -1
- package/dist/core/coherence/coherence.js +30 -16
- package/dist/core/coherence/coherence.js.map +1 -1
- package/dist/core/coherence/consumed/operations.js +70 -0
- package/dist/core/coherence/consumed/operations.js.map +1 -0
- package/dist/core/coherence/consumed/refusal.js +38 -0
- package/dist/core/coherence/consumed/refusal.js.map +1 -0
- package/dist/core/coherence/declared.js +60 -34
- package/dist/core/coherence/declared.js.map +1 -1
- package/dist/core/coherence/events/declared.js +44 -59
- package/dist/core/coherence/events/declared.js.map +1 -1
- package/dist/core/coherence/events/events.js +119 -8
- package/dist/core/coherence/events/events.js.map +1 -1
- package/dist/core/coherence/events/grades.js +31 -8
- package/dist/core/coherence/events/grades.js.map +1 -1
- package/dist/core/coherence/events/lookups.js +14 -82
- package/dist/core/coherence/events/lookups.js.map +1 -1
- package/dist/core/coherence/lookups.js +18 -78
- package/dist/core/coherence/lookups.js.map +1 -1
- package/dist/core/coherence/retired/archived.js +285 -0
- package/dist/core/coherence/retired/archived.js.map +1 -0
- package/dist/core/coherence/retired/event-pair.js +62 -0
- package/dist/core/coherence/retired/event-pair.js.map +1 -0
- package/dist/core/coherence/retired/merged.js +79 -0
- package/dist/core/coherence/retired/merged.js.map +1 -0
- package/dist/core/coherence/retired/retirement.js +49 -0
- package/dist/core/coherence/retired/retirement.js.map +1 -0
- package/dist/core/coherence/retired/revival/arrivals.js +131 -0
- package/dist/core/coherence/retired/revival/arrivals.js.map +1 -0
- package/dist/core/coherence/retired/revival/revived.js +297 -0
- package/dist/core/coherence/retired/revival/revived.js.map +1 -0
- package/dist/core/consumers/after.js +113 -0
- package/dist/core/consumers/after.js.map +1 -0
- package/dist/core/consumers/consumers.js +296 -0
- package/dist/core/consumers/consumers.js.map +1 -0
- package/dist/core/consumers/current.js +211 -0
- package/dist/core/consumers/current.js.map +1 -0
- package/dist/core/consumers/scan.js +46 -0
- package/dist/core/consumers/scan.js.map +1 -0
- package/dist/core/consumers/state.js +27 -0
- package/dist/core/consumers/state.js.map +1 -0
- package/dist/core/delta/delta.js +1 -1
- package/dist/core/delta/delta.js.map +1 -1
- package/dist/core/deployment/delta.js +3 -6
- package/dist/core/deployment/delta.js.map +1 -1
- package/dist/core/diff/base-git.js +94 -12
- package/dist/core/diff/base-git.js.map +1 -1
- package/dist/core/diff/base-state.js +28 -16
- package/dist/core/diff/base-state.js.map +1 -1
- package/dist/core/diff/joins.js +136 -0
- package/dist/core/diff/joins.js.map +1 -0
- package/dist/core/diff/scope/changed-paths.js +22 -6
- package/dist/core/diff/scope/changed-paths.js.map +1 -1
- package/dist/core/diff/scope/edges.js +82 -0
- package/dist/core/diff/scope/edges.js.map +1 -0
- package/dist/core/diff/semantic.js +100 -31
- package/dist/core/diff/semantic.js.map +1 -1
- package/dist/core/doctor/agents.js +34 -18
- package/dist/core/doctor/agents.js.map +1 -1
- package/dist/core/doctor/report.js.map +1 -1
- package/dist/core/document/apply.js +10 -2
- package/dist/core/document/apply.js.map +1 -1
- package/dist/core/document/frontmatter.js +55 -0
- package/dist/core/document/frontmatter.js.map +1 -1
- package/dist/core/document/parse.js +29 -6
- package/dist/core/document/parse.js.map +1 -1
- package/dist/core/document/scenarios.js +17 -0
- package/dist/core/document/scenarios.js.map +1 -1
- package/dist/core/document/spec.js +4 -11
- package/dist/core/document/spec.js.map +1 -1
- package/dist/core/envelope/config.js +10 -0
- package/dist/core/envelope/config.js.map +1 -1
- package/dist/core/envelope/json.js +34 -0
- package/dist/core/envelope/json.js.map +1 -1
- package/dist/core/explain/families.js +17 -12
- package/dist/core/explain/families.js.map +1 -1
- package/dist/core/explain/refusals.js +3 -3
- package/dist/core/explain/refusals.js.map +1 -1
- package/dist/core/explain/terms.js +18 -9
- package/dist/core/explain/terms.js.map +1 -1
- package/dist/core/explore/explore.js +2 -7
- package/dist/core/explore/explore.js.map +1 -1
- package/dist/core/fleet-context.js +34 -95
- package/dist/core/fleet-context.js.map +1 -1
- package/dist/core/gate/checks.js +5 -2
- package/dist/core/gate/checks.js.map +1 -1
- package/dist/core/gate/partners.js +4 -2
- package/dist/core/gate/partners.js.map +1 -1
- package/dist/core/gate/report.js.map +1 -1
- package/dist/core/gate/verification.js +75 -12
- package/dist/core/gate/verification.js.map +1 -1
- package/dist/core/gherkin/stale.js +13 -37
- package/dist/core/gherkin/stale.js.map +1 -1
- package/dist/core/glossary/tree.js +6 -19
- package/dist/core/glossary/tree.js.map +1 -1
- package/dist/core/kernel/concurrency.js +19 -0
- package/dist/core/kernel/concurrency.js.map +1 -1
- package/dist/core/kernel/records.js +11 -0
- package/dist/core/kernel/records.js.map +1 -1
- package/dist/core/links/backlinks.js +1 -3
- package/dist/core/links/backlinks.js.map +1 -1
- package/dist/core/links/corpus.js +5 -36
- package/dist/core/links/corpus.js.map +1 -1
- package/dist/core/mcp/argv.js +101 -0
- package/dist/core/mcp/argv.js.map +1 -1
- package/dist/core/mcp/author-tools.js +2 -1
- package/dist/core/mcp/author-tools.js.map +1 -1
- package/dist/core/mcp/protocol.js +2 -2
- package/dist/core/mcp/protocol.js.map +1 -1
- package/dist/core/mcp/tools.js +43 -33
- package/dist/core/mcp/tools.js.map +1 -1
- package/dist/core/openapi/baseline/gate.js +53 -12
- package/dist/core/openapi/baseline/gate.js.map +1 -1
- package/dist/core/openapi/baseline/plan.js +22 -8
- package/dist/core/openapi/baseline/plan.js.map +1 -1
- package/dist/core/openapi/baseline/record.js +0 -1
- package/dist/core/openapi/baseline/record.js.map +1 -1
- package/dist/core/openapi/contract-text.js +33 -0
- package/dist/core/openapi/contract-text.js.map +1 -0
- package/dist/core/openapi/contract-tree.js +56 -0
- package/dist/core/openapi/contract-tree.js.map +1 -0
- package/dist/core/openapi/depth.js +38 -24
- package/dist/core/openapi/depth.js.map +1 -1
- package/dist/core/openapi/digest.js +52 -2
- package/dist/core/openapi/digest.js.map +1 -1
- package/dist/core/openapi/doc.js +12 -4
- package/dist/core/openapi/doc.js.map +1 -1
- package/dist/core/openapi/merge/components.js +25 -60
- package/dist/core/openapi/merge/components.js.map +1 -1
- package/dist/core/openapi/merge/error.js +3 -13
- package/dist/core/openapi/merge/error.js.map +1 -1
- package/dist/core/openapi/merge/markers.js +26 -30
- package/dist/core/openapi/merge/markers.js.map +1 -1
- package/dist/core/openapi/merge/merge.js +26 -52
- package/dist/core/openapi/merge/merge.js.map +1 -1
- package/dist/core/openapi/merge/pin.js +15 -35
- package/dist/core/openapi/merge/pin.js.map +1 -1
- package/dist/core/openspec/decide/verdict.js.map +1 -1
- package/dist/core/openspec/inventory.js +4 -5
- package/dist/core/openspec/inventory.js.map +1 -1
- package/dist/core/openspec/model/mapping.js +30 -28
- package/dist/core/openspec/model/mapping.js.map +1 -1
- package/dist/core/openspec/model/model.js +12 -7
- package/dist/core/openspec/model/model.js.map +1 -1
- package/dist/core/openspec/scan/changes.js +10 -4
- package/dist/core/openspec/scan/changes.js.map +1 -1
- package/dist/core/openspec/scan/config.js +7 -16
- package/dist/core/openspec/scan/config.js.map +1 -1
- package/dist/core/openspec/scan/shape.js +63 -19
- package/dist/core/openspec/scan/shape.js.map +1 -1
- package/dist/core/openspec/scan/walk.js +21 -38
- package/dist/core/openspec/scan/walk.js.map +1 -1
- package/dist/core/pack/living.js +2 -16
- package/dist/core/pack/living.js.map +1 -1
- package/dist/core/provenance/findings.js +48 -12
- package/dist/core/provenance/findings.js.map +1 -1
- package/dist/core/provenance/git.js +33 -57
- package/dist/core/provenance/git.js.map +1 -1
- package/dist/core/provenance/gitq/moves.js +71 -114
- package/dist/core/provenance/gitq/moves.js.map +1 -1
- package/dist/core/provenance/gitq/vouched-ancestor.js +2 -2
- package/dist/core/provenance/gitq/vouched-ancestor.js.map +1 -1
- package/dist/core/provenance/sources.js +23 -6
- package/dist/core/provenance/sources.js.map +1 -1
- package/dist/core/provenance/stamp.js +5 -17
- package/dist/core/provenance/stamp.js.map +1 -1
- package/dist/core/repo/entries.js.map +1 -1
- package/dist/core/repo/repo.js +24 -14
- package/dist/core/repo/repo.js.map +1 -1
- package/dist/core/repo/tree/find.js +12 -3
- package/dist/core/repo/tree/find.js.map +1 -1
- package/dist/core/repo/tree/fs.js +27 -1
- package/dist/core/repo/tree/fs.js.map +1 -1
- package/dist/core/repo/tree/walk.js +8 -30
- package/dist/core/repo/tree/walk.js.map +1 -1
- package/dist/core/results.js +47 -12
- package/dist/core/results.js.map +1 -1
- package/dist/core/staging/commit.js +10 -12
- package/dist/core/staging/commit.js.map +1 -1
- package/dist/core/staging/lock.js +74 -12
- package/dist/core/staging/lock.js.map +1 -1
- package/dist/core/staging/recovery/intent.js +2 -5
- package/dist/core/staging/recovery/intent.js.map +1 -1
- package/dist/core/staging/recovery/recover.js +60 -1
- package/dist/core/staging/recovery/recover.js.map +1 -1
- package/dist/core/staging/snapshot/manifest.js +152 -0
- package/dist/core/staging/snapshot/manifest.js.map +1 -0
- package/dist/core/staging/{snapshot.js → snapshot/snapshot.js} +10 -34
- package/dist/core/staging/snapshot/snapshot.js.map +1 -0
- package/dist/core/status/actions/execution.js +11 -0
- package/dist/core/status/actions/execution.js.map +1 -1
- package/dist/core/status/feature/artifacts.js +2 -2
- package/dist/core/status/feature/artifacts.js.map +1 -1
- package/dist/core/status/feature/feature.js +5 -11
- package/dist/core/status/feature/feature.js.map +1 -1
- package/dist/core/status/fleet/fleet.js +6 -2
- package/dist/core/status/fleet/fleet.js.map +1 -1
- package/dist/core/status/fleet/next.js +26 -3
- package/dist/core/status/fleet/next.js.map +1 -1
- package/dist/core/status/report.js.map +1 -1
- package/dist/core/status/verification.js +30 -15
- package/dist/core/status/verification.js.map +1 -1
- package/dist/core/tasks/packet.js +174 -0
- package/dist/core/tasks/packet.js.map +1 -0
- package/dist/core/tasks/record.js +43 -0
- package/dist/core/tasks/record.js.map +1 -0
- package/dist/core/tasks/sources.js +181 -0
- package/dist/core/tasks/sources.js.map +1 -0
- package/dist/core/tasks/stamps.js +76 -0
- package/dist/core/tasks/stamps.js.map +1 -0
- package/dist/core/tasks/unit.js +0 -0
- package/dist/core/tasks/unit.js.map +1 -0
- package/dist/core/usecases/capability.js +11 -14
- package/dist/core/usecases/capability.js.map +1 -1
- package/dist/core/usecases/delta/flows.js +5 -7
- package/dist/core/usecases/delta/flows.js.map +1 -1
- package/dist/core/usecases/fleet.js +178 -31
- package/dist/core/usecases/fleet.js.map +1 -1
- package/dist/core/usecases/operations.js +5 -4
- package/dist/core/usecases/operations.js.map +1 -1
- package/dist/core/usecases/service/flows.js +9 -6
- package/dist/core/usecases/service/flows.js.map +1 -1
- package/dist/core/usecases/suites/participants.js +83 -0
- package/dist/core/usecases/suites/participants.js.map +1 -0
- package/dist/core/usecases/suites/suites.js +45 -0
- package/dist/core/usecases/suites/suites.js.map +1 -0
- package/dist/core/verify/answers.js +12 -5
- package/dist/core/verify/answers.js.map +1 -1
- package/dist/core/verify/build.js +102 -82
- package/dist/core/verify/build.js.map +1 -1
- package/dist/core/verify/checklist.js +3 -0
- package/dist/core/verify/checklist.js.map +1 -1
- package/dist/core/verify/location.js +67 -0
- package/dist/core/verify/location.js.map +1 -0
- package/dist/core/verify/pins/lint.js +3 -2
- package/dist/core/verify/pins/lint.js.map +1 -1
- package/dist/core/verify/pins/pin.js +1 -1
- package/dist/core/verify/pins/pin.js.map +1 -1
- package/dist/core/verify/read/file.js +224 -0
- package/dist/core/verify/read/file.js.map +1 -0
- package/dist/core/verify/{file.js → read/shape.js} +16 -104
- package/dist/core/verify/read/shape.js.map +1 -0
- package/dist/core/verify/read/union.js +105 -0
- package/dist/core/verify/read/union.js.map +1 -0
- package/dist/core/verify/record.js +35 -12
- package/dist/core/verify/record.js.map +1 -1
- package/dist/core/verify/store/commit.js +40 -30
- package/dist/core/verify/store/commit.js.map +1 -1
- package/dist/core/verify/store/render.js +8 -4
- package/dist/core/verify/store/render.js.map +1 -1
- package/dist/core/vocabulary/issue.js +1 -0
- package/dist/core/vocabulary/issue.js.map +1 -1
- package/dist/core/vocabulary/maturity.js +12 -2
- package/dist/core/vocabulary/maturity.js.map +1 -1
- package/examples/README.md +21 -6
- package/examples/docs/architecture/landscape.likec4 +8 -0
- package/examples/docs/architecture/usecases/order-notification.likec4 +6 -2
- package/examples/docs/features/FEAT-101-payment-splitting/specs/payment-service/asyncapi.yaml +11 -8
- package/examples/docs/features/FEAT-101-payment-splitting/specs/payment-split-service/openapi.yaml +6 -6
- package/examples/docs/features/archive/FEAT-088-refunds/specs/payment-service/openapi.yaml +13 -10
- package/examples/docs/services/order-service/asyncapi.yaml +3 -2
- package/examples/docs/services/platform/notification-service/asyncapi.yaml +2 -1
- package/package.json +1 -1
- package/dist/commands/validate/checks/vocabulary.js +0 -16
- package/dist/commands/validate/checks/vocabulary.js.map +0 -1
- package/dist/core/agent/agents-md/artifacts.js +0 -221
- package/dist/core/agent/agents-md/artifacts.js.map +0 -1
- package/dist/core/agent/agents-md/cycle.js +0 -180
- package/dist/core/agent/agents-md/cycle.js.map +0 -1
- package/dist/core/agent/agents-md/shipped/archive-gate.js +0 -98
- package/dist/core/agent/agents-md/shipped/archive-gate.js.map +0 -1
- package/dist/core/agent/agents-md/spine.js +0 -40
- package/dist/core/agent/agents-md/spine.js.map +0 -1
- package/dist/core/coherence/events/merged.js +0 -40
- package/dist/core/coherence/events/merged.js.map +0 -1
- package/dist/core/diff/current-state.js +0 -96
- package/dist/core/diff/current-state.js.map +0 -1
- package/dist/core/diff/victims.js +0 -256
- package/dist/core/diff/victims.js.map +0 -1
- package/dist/core/staging/snapshot.js.map +0 -1
- package/dist/core/verify/file.js.map +0 -1
package/CHANGELOG.md
CHANGED
|
@@ -10,6 +10,1317 @@ 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.9] - 2026-10-20
|
|
14
|
+
|
|
15
|
+
The first publish since `0.2.0-alpha.7`. `0.2.0-alpha.8` was tagged but never reached npm (its
|
|
16
|
+
release run failed at `npm ci` on a stripped lockfile), so a repository upgrading from alpha.7 takes
|
|
17
|
+
alpha.8's section below as well as this one. Still a prerelease on the `alpha` line.
|
|
18
|
+
|
|
19
|
+
Most of it is fixes. `loam archive` merges into a landscape written in any form LikeC4 parses —
|
|
20
|
+
several model blocks, markdown strings, split heads, hyphenated ids — and no longer writes a wrong
|
|
21
|
+
merge at exit 0 for the shapes listed under "Fixed — `loam archive` merges into a landscape…",
|
|
22
|
+
which alpha.7 did. Consumers are counted once, by one index, so `loam archive`, `loam validate` and
|
|
23
|
+
`loam diff` agree; the PR gate sees a changed schema, an edited `op` on an edge and a caller drawn
|
|
24
|
+
with no `op`; `loam gate --since` keeps an archived feature in view; the trust readouts show stale
|
|
25
|
+
vouches; a waiting writer no longer deletes the next writer's lock.
|
|
26
|
+
|
|
27
|
+
Several entries change an exit code, listed in each: refusals that are new (an overwrite or
|
|
28
|
+
retirement a consumer still names, a delta reviving a retired operation, an unreadable consumer
|
|
29
|
+
document) and refusals that were false and are gone. A federated verification record is now one
|
|
30
|
+
file per service, and an older loam does not read it: upgrade every repository that records or
|
|
31
|
+
gates a feature together. New: `loam list suites`, `loam new --modifies`, `loam gate --since`,
|
|
32
|
+
`loam_adopt` on MCP; six new stable codes — `openapi.op-revived`, `asyncapi.slot-revived`,
|
|
33
|
+
`next.revouch`, `frontmatter.owner-mismatch`, `diff.op-modified` and `diff.message-modified`.
|
|
34
|
+
|
|
35
|
+
### Changed — one consumer index: `loam archive`, `loam validate` and `loam diff` agree about who still consumes an operation or a message
|
|
36
|
+
|
|
37
|
+
No code or flag was added; one additive `--json` key on two existing codes. Who still consumes what
|
|
38
|
+
a change takes out was answered three ways — the coherence gate (`openapi.remove-op-consumed`,
|
|
39
|
+
`asyncapi.remove-message-consumed`), `loam diff`'s victims (`diff.op-removed-consumed`,
|
|
40
|
+
`diff.message-removed-consumed`, the `-deprecated`/`-modified` details, `diff.consumer-added` /
|
|
41
|
+
`diff.consumer-removed`) and `loam archive`'s `openapi.op-modified` / `asyncapi.message-modified`
|
|
42
|
+
warnings — and the three copies read different documents. They are one index now; SCHEMA.md's
|
|
43
|
+
"Who still consumes it" states the rules.
|
|
44
|
+
|
|
45
|
+
- **More consumers are found, so more removals are refused:**
|
|
46
|
+
- **Edges come from the whole `architecture/` project**, not `architecture/landscape.likec4`
|
|
47
|
+
alone, for the gate as for `loam diff` and the archive's overwrite warnings: an edge with `metadata { op }` (or `consumes`) drawn in
|
|
48
|
+
`architecture/calls.likec4` or in a use-case document counts. The gate passed such a removal
|
|
49
|
+
and the next `validate --all` reported `spine.op-undefined` on the edge. Where only a sibling
|
|
50
|
+
document fails to parse, the map file's own edges still count.
|
|
51
|
+
- **The gate names use-case hops** attributed to the operation (or backed by a `consumes` edge),
|
|
52
|
+
as `loam diff` already did.
|
|
53
|
+
- **An `arch.spec.md` `Operations:` line** of another service counts, as a spec.md one does —
|
|
54
|
+
for the gate, `loam diff`'s victims, `diff.consumer-added` / `diff.consumer-removed` and the
|
|
55
|
+
archive's overwrite warnings. So an
|
|
56
|
+
operation's `diff.consumer-added` / `-removed` is not reported while the consumer's
|
|
57
|
+
`arch.spec.md` cannot be read on the other side, as it never was over an unreadable `spec.md`.
|
|
58
|
+
The consumer's own `openapi.yaml` does not suppress it: an unreadable contract proves no
|
|
59
|
+
ownership, so the line counts on that side too. The exception is a line whose join exists only
|
|
60
|
+
because that contract did not parse on one side, while on the other side it reads and defines
|
|
61
|
+
the id: there the line names the consumer's own operation, and only the contract's readability
|
|
62
|
+
changed. No `diff.consumer-added` / `diff.consumer-removed` is reported for it — before, one per
|
|
63
|
+
provider of the id, each counted in the `--json` summary's `added` / `removed`. The contract's own
|
|
64
|
+
`unreadable[]` entry is still reported, and `loam diff` still exits 1 on it.
|
|
65
|
+
- **The provider's own `spec.md` requirements count for a marker retirement too**, as they do for
|
|
66
|
+
the overwrite of the entry below: one still naming the retired operation after the merge (a
|
|
67
|
+
second one beside the REMOVED requirement) archived at exit 0 and left `spec-api.op-undefined`.
|
|
68
|
+
Its `arch.spec.md` `Operations:` lines count for neither shape: `validate` grades such a line in
|
|
69
|
+
spec.md only, so one in arch.spec.md strands nothing. An edge the provider draws to itself
|
|
70
|
+
counted already, and still does — the spine grades it like any other.
|
|
71
|
+
`loam diff` and the archive's overwrite warnings still leave the provider's own documents out:
|
|
72
|
+
they name who ELSE is affected.
|
|
73
|
+
- **On the event axis the provider's own `Publishes:` lines count**, as the merge would leave
|
|
74
|
+
them. A message slot re-keyed to another name, or a send operation turned to receive, while the
|
|
75
|
+
provider's requirement still publishes the message, archived at exit 0 and the next
|
|
76
|
+
`validate --all` reported `spec-event.message-undefined`; it is `asyncapi.remove-message-consumed`
|
|
77
|
+
now. So do its own `publishes` edges, which the spine grades on their source: a full retirement
|
|
78
|
+
over a living `paymentService -> billingService { metadata { publishes '…' } }` archived at exit
|
|
79
|
+
0 and left `spine.message-undefined` on the provider. The remedy names "drop or redraw its
|
|
80
|
+
publishes edge".
|
|
81
|
+
- **Fail closed**: a spec the merge refuses to apply — a malformed `Requirement-ID:` in the
|
|
82
|
+
living spec or the delta — no longer answers "no consumers". The living requirements count,
|
|
83
|
+
plus every requirement the delta ADDS or MODIFIES.
|
|
84
|
+
- **A consumer document loam cannot read refuses the removal**, as the gate did before it asked
|
|
85
|
+
one index: another service's `spec.md` or `arch.spec.md` that does not decode as UTF-8, the
|
|
86
|
+
provider's own `spec.md` (either spec file, for a message), or a service id two directories
|
|
87
|
+
claim. `openapi.remove-op-consumed` /
|
|
88
|
+
`asyncapi.remove-message-consumed` name each in `details[]` and say the gate could not check —
|
|
89
|
+
never that the fleet still consumes it. An edge drawn with no `metadata { op }` and a contested
|
|
90
|
+
use-case hop still never refuse; `loam diff` lists them as doubts, as before.
|
|
91
|
+
- **`loam diff` counts only a service that SENDS a message as still producing it.** A removed
|
|
92
|
+
message another service declares only with `action: receive` — a consumer's own declaration,
|
|
93
|
+
which validate requires — was `diff.message-removed` (warn), "still declared by" that consumer,
|
|
94
|
+
while the gate refused the same removal and the next `validate --all` reported
|
|
95
|
+
`spine.message-unproduced` there. It is `diff.message-removed-consumed` now, naming the gate's
|
|
96
|
+
consumers; the warning for a message another service does send reads "still sent by".
|
|
97
|
+
- **Fewer refusals where the consumer was false or already retired:**
|
|
98
|
+
- **Another service's `Operations:` line naming an id its own contract defines** names its own
|
|
99
|
+
operation, and is no consumer (gate, `loam diff`, `diff.consumer-added` / `-removed`, the
|
|
100
|
+
archive's overwrite warnings): shared
|
|
101
|
+
ids such as `healthCheck` no longer refuse a rename, and `--approve` is no longer the only way
|
|
102
|
+
past a fleet `validate --all` calls clean.
|
|
103
|
+
- **Read after the feature's merge**: a consumer requirement the same feature MODIFIES to drop
|
|
104
|
+
the id, or REMOVES, no longer counts — another service's included, which is the remedy the
|
|
105
|
+
refusal itself names.
|
|
106
|
+
- **A message another service would still send after the merge strands no consumer.**
|
|
107
|
+
`asyncapi.remove-message-consumed` counted every `consumes` edge, `Consumes:` line and use-case
|
|
108
|
+
hop of a message whoever sent it, so retiring it from one of two senders was refused over the
|
|
109
|
+
other sender's consumers. Its remedy said to drop that sender's own `consumes` edge. Meanwhile
|
|
110
|
+
`loam diff` reported the same removal as `diff.message-removed` (warn), "still sent by …", and
|
|
111
|
+
the next `validate --all` was clean. Now a consumer refuses only while no service would send the
|
|
112
|
+
message after this feature's merge. The senders are read as the merge leaves them: a service
|
|
113
|
+
the feature carries an event delta for sends what its merged contract would (the delta itself
|
|
114
|
+
where it creates that contract), so moving production to another service in the same feature
|
|
115
|
+
passes, and a feature that retires the message from every sender is still refused. An unreadable contract never counts as
|
|
116
|
+
a sender. Beside another sender, the provider's own `Publishes:` lines, its own `publishes`
|
|
117
|
+
edges and its own unreadable spec files still refuse; another service's unreadable spec file,
|
|
118
|
+
or an id two directories claim, does not. This predates the one index: the gate behaved the
|
|
119
|
+
same before it. The message is reworded to say which case applies. With no sender left it reads
|
|
120
|
+
"no readable contract would still send it after this merge" and adds the route "keep another
|
|
121
|
+
service sending it". With a sender left it names the senders, lists only the provider's own
|
|
122
|
+
documents, and gives only their remedy.
|
|
123
|
+
- **An `#external` element that publishes the message keeps its consumers produced too**, for
|
|
124
|
+
the gate and for `loam diff` as for `validate`. Both counted only senders inside the fleet, so
|
|
125
|
+
retiring a message from its only fleet sender was refused while an `#external` element still
|
|
126
|
+
published it (an edge from it with `metadata { publishes }`), and after `--approve` `loam diff`
|
|
127
|
+
reported `diff.message-removed-consumed` (error) — while `spine.message-unproduced` accepts such
|
|
128
|
+
a producer and the next `validate --all` was clean. Now the outside producer counts as a
|
|
129
|
+
sender, read off the living `architecture/` project with validate's own rule: only while the
|
|
130
|
+
project parses, and not from an edge the feature's own C4 delta draws. The gate asks the rule
|
|
131
|
+
over the service directories as the merge leaves them, so an element the same feature makes a
|
|
132
|
+
service — a `specs/<id>/` directory for the id the element, or the element it is nested in,
|
|
133
|
+
resolves to, which the archive turns into `services/<id>/` — no longer counts, and the
|
|
134
|
+
retirement is refused, as the next `loam diff` and `validate --all` would read it; `loam diff`
|
|
135
|
+
asks over the directories as they stand. `loam diff` reports
|
|
136
|
+
`diff.message-removed` (warn), "still sent by `<title>` (#external)", and a refusal for the
|
|
137
|
+
provider's own documents, which it still does not excuse, names it the same way. With no sender
|
|
138
|
+
left, the refusal now also says "nor does an #external element publish it", and its route
|
|
139
|
+
reads "keep another service sending it (…) or an #external element publishing it (a publishes
|
|
140
|
+
edge from it in the living architecture/ project; one whose element this feature's
|
|
141
|
+
specs/<service>/ makes a service no longer counts)". This predates the one index too.
|
|
142
|
+
- **`openapi.remove-marker-missing` is owed only while there is a living operation to retire**:
|
|
143
|
+
not for an id the service's living contract does not define (a consumer's REMOVED requirement
|
|
144
|
+
naming another service's operation), and not for one the delta already replaces at its method
|
|
145
|
+
and path (no marker could be written there). A living `openapi.yaml` that does not parse proves
|
|
146
|
+
neither, so the marker is still owed there, as before. The overwrite's "REMOVE the requirement"
|
|
147
|
+
route can now be followed; its message says a REMOVED requirement owes no marker there.
|
|
148
|
+
`asyncapi.remove-marker-missing` likewise skips a message the merge already takes out.
|
|
149
|
+
- **`details[]`** (additive) on `openapi.remove-op-consumed` and `asyncapi.remove-message-consumed`:
|
|
150
|
+
the consumers, one per line, in the order `loam diff` lists the victims of the same removal.
|
|
151
|
+
- **Messages reworded**: the remedies name the `architecture/` project instead of
|
|
152
|
+
`architecture/landscape.likec4`, and say a requirement is retired in the feature's
|
|
153
|
+
`specs/<service>/` delta; `diff.op-removed`'s no-consumer sentence reads "no current edge,
|
|
154
|
+
requirement or use-case hop names it".
|
|
155
|
+
- **Exit code:** where a consumer is newly found, `loam archive` exits 1 (`not-coherent`) without
|
|
156
|
+
`--approve`; `loam validate --feature`, `loam validate <FEAT>` and `loam validate --all` (for every
|
|
157
|
+
active feature in that shape) exit 1; `loam status` counts the finding as gating; `loam show
|
|
158
|
+
<FEAT> --json` reports `review.readyToArchive: false`; and `loam diff` exits 1
|
|
159
|
+
(`diff.op-removed-consumed` / `diff.message-removed-consumed`, `breaking: true`) where a removal
|
|
160
|
+
now has a victim — a removed message whose only other declarer receives it included, which exited
|
|
161
|
+
0. Where another service's spec file does not decode, `loam validate --feature`
|
|
162
|
+
reports `openapi.remove-op-consumed` / `asyncapi.remove-message-consumed` naming the file where it
|
|
163
|
+
reported `feature.unreadable`, at exit 1 as before, and `loam archive` exits 1 as before. Where
|
|
164
|
+
two directories claim a service id, or the provider's own `spec.md` does not decode (either spec
|
|
165
|
+
file, for a message), `loam
|
|
166
|
+
archive` and `loam validate --feature` exit 1 for the removal whatever those documents say —
|
|
167
|
+
before, 0 unless a copy the gate could read named it — until the document reads or `--approve` is
|
|
168
|
+
passed. Where a refusal was false or the consumer is retired in the same feature, each
|
|
169
|
+
of those exits 0 for that shape unless something else gates. That includes a message retired from
|
|
170
|
+
one sender while another service would still send it after the merge, or while an `#external`
|
|
171
|
+
element still publishes it and the feature does not make that element a service, where only
|
|
172
|
+
consumers were named: `loam archive`, `loam validate --feature`, `loam validate <FEAT>` and `loam validate
|
|
173
|
+
--all` exited 1 and now exit 0, `loam status` no longer counts it as gating, and `loam show
|
|
174
|
+
<FEAT> --json` no longer reports `review.readyToArchive: false` for it. `loam diff` reports
|
|
175
|
+
`diff.op-removed` (warn) where its only victim was a same-id requirement, and
|
|
176
|
+
`diff.message-removed` (warn) where an `#external` element still publishes the removed message —
|
|
177
|
+
it exited 1 (`diff.message-removed-consumed`, `breaking: true`) and now exits 0 unless something
|
|
178
|
+
else fails. `--approve` overrides
|
|
179
|
+
every one of these refusals as before. `--strict` changes nothing here beyond that: every finding
|
|
180
|
+
in this entry is an error, or is unchanged.
|
|
181
|
+
|
|
182
|
+
### Fixed — `loam instructions loam-spine` says what `loam rebase` pins for a revived quote
|
|
183
|
+
|
|
184
|
+
The page said `loam rebase` "pins neither shape". That has been false since rebase began grading
|
|
185
|
+
the delta it will write. Rebase never pins a retired operation as new, but it does repin a quote
|
|
186
|
+
of an address a move vacated over the operation that lives there now, and it reports the revival
|
|
187
|
+
that edit makes. The page now says so, in line with the `loam-codes` command map, WORKFLOW.md
|
|
188
|
+
and SCHEMA.md.
|
|
189
|
+
|
|
190
|
+
### Fixed — an operation with no `operationId` is a slot like any other
|
|
191
|
+
|
|
192
|
+
No code, flag or payload key was added. The OpenAPI reader used to drop an operation written
|
|
193
|
+
without an `operationId`. It now keeps it by its method and path, never by an id: requirements,
|
|
194
|
+
edges and removal markers still cannot name it, and it gets no finding of its own. The checks that
|
|
195
|
+
work by slot now see it:
|
|
196
|
+
|
|
197
|
+
- **`openapi.op-revived` names it.** When a quote lands where such an operation lives, the message
|
|
198
|
+
says `<METHOD /path> now holds an operation with no operationId, and archiving this delta
|
|
199
|
+
replaces it there`. Its move-back remedy adds that, if that operation is to stay, the same delta
|
|
200
|
+
must define it at another address. The message no longer calls that address one "the living
|
|
201
|
+
contract no longer has". Followed as it used to read, the remedy led to an archive that deleted
|
|
202
|
+
the operation with nothing naming it.
|
|
203
|
+
- **`loam rebase` and `loam validate` agree about a pin over it.** Rebase always pinned against
|
|
204
|
+
whatever stood at the slot, but validate refused that pin with `openapi.baseline-invalid` ("no
|
|
205
|
+
operation at that slot").
|
|
206
|
+
- **Pins are now required at these slots.** An unpinned quote over such an operation counts into
|
|
207
|
+
`openapi.baseline-missing` (warn, gates archive), and so does an unpinned delta operation without
|
|
208
|
+
an `operationId` over a living one. Their pins are graded like any other (`-stale`, `-invalid`).
|
|
209
|
+
**Exit code:** `loam archive` now exits 1 (`not-coherent`) on either shape until `loam rebase`
|
|
210
|
+
pins it, or `--approve` merges unpinned. `openapi.baseline-missing` is a warning that gates:
|
|
211
|
+
`loam validate` exits as before on it, except `--strict` (and `--all --strict`), which fails on
|
|
212
|
+
any warning and so now exits 1; `loam status` counts it as gating, and `loam show <FEAT> --json`
|
|
213
|
+
reports `review.readyToArchive: false` until the pin is written.
|
|
214
|
+
- **A pin on such an operation where nothing lives is `openapi.baseline-invalid` (error)**, as it is
|
|
215
|
+
for a named operation (rebase never writes one; a malformed pin on such an operation is refused
|
|
216
|
+
the same way). **Exit code:** `loam validate --feature`, `loam validate <FEAT>` and `loam validate
|
|
217
|
+
--all` (for every active feature carrying one) now exit 1 on it, where they exited 0 with no
|
|
218
|
+
finding; `loam archive` exits 1 (`not-coherent`) without `--approve`; `loam status` counts it as
|
|
219
|
+
gating; `loam show <FEAT> --json` reports `review.readyToArchive: false`.
|
|
220
|
+
- **`openapi.remove-op-consumed`** also fires when the delta defines an operation with no
|
|
221
|
+
`operationId` where a consumed one lives. Before, a delta holding only such an operation upserted
|
|
222
|
+
over the consumed one with no finding of any kind.
|
|
223
|
+
|
|
224
|
+
### Changed — `loam archive` refuses an overwrite that takes a consumed operation out of the contract
|
|
225
|
+
|
|
226
|
+
No code, flag or payload key was added: an existing code covers a second shape.
|
|
227
|
+
|
|
228
|
+
- **`openapi.remove-op-consumed` (error, `--approve` overrides) now also fires for an overwrite.**
|
|
229
|
+
A feature's `openapi.yaml` defines another operationId at the method and path where a living
|
|
230
|
+
operation lives, and the contract the archive would write then defines the living one nowhere.
|
|
231
|
+
The merge replaces it there with no removal marker naming it, so before this only the advisory
|
|
232
|
+
`api.op-unconsumed` fired, the archive passed without `--approve`, and the next `validate`
|
|
233
|
+
reported `spine.op-undefined` / `spec-api.op-undefined`. The consumers are a removal's — the one
|
|
234
|
+
consumer index of the entry above — including the provider's own `spec.md` requirements as the
|
|
235
|
+
merge would leave them: one the feature MODIFIES to name the new operation, or REMOVES, no longer counts. The
|
|
236
|
+
message names what replaces the operation and where, and says to define it at another address in
|
|
237
|
+
the same delta if it is to stay.
|
|
238
|
+
- **Exit code:** on this shape `loam archive` now exits 1 (`not-coherent`) without `--approve`,
|
|
239
|
+
and `loam validate --feature`, `loam validate <FEAT>` and `loam validate --all` (for every active
|
|
240
|
+
feature in this shape) exit 1. `loam status` counts it as gating, and `loam show <FEAT> --json`
|
|
241
|
+
reports `review.readyToArchive: false` until it is fixed or archived past with `--approve`. With
|
|
242
|
+
`--approve` the archive merges as before and lists the finding in `overridden[]`.
|
|
243
|
+
- **`c4-api.op-removing`** now also fires for a new tagged edge onto an operation the delta
|
|
244
|
+
replaces that way.
|
|
245
|
+
- An overwrite of an operation nothing names is unchanged: `api.op-unconsumed` stays advisory. On
|
|
246
|
+
the event axis `asyncapi.remove-message-consumed` already graded the simulated merge's sent
|
|
247
|
+
messages, so a channel, operation or message slot redefined under another identity was caught
|
|
248
|
+
like a retirement for its OTHER consumers — but not for the provider's own `Publishes:` line,
|
|
249
|
+
which the consumer-index entry above adds.
|
|
250
|
+
|
|
251
|
+
### Fixed — `loam archive` merges into a landscape written in any form LikeC4 parses
|
|
252
|
+
|
|
253
|
+
No code, flag or `--json` key was added or changed. Each case below is a landscape (or a delta)
|
|
254
|
+
that `loam validate --all` called valid and that `loam archive` refused with `merge-failed`, nothing
|
|
255
|
+
written — or, where the case says "exit 0", merged wrong and wrote; each now merges as LikeC4
|
|
256
|
+
reads it.
|
|
257
|
+
|
|
258
|
+
- **A landscape with more than one top-level `model { }` block.** The splice map read only the
|
|
259
|
+
first block, so every declaration in a later one was invisible and archive refused with
|
|
260
|
+
"declares '<id>' in a form placement cannot locate — most often two declarations sharing one
|
|
261
|
+
line" about a declaration on a line of its own. Every block is read now. An addition anchored to
|
|
262
|
+
a living element — nested in it, joining its service's neighborhood, or joining its source's
|
|
263
|
+
edges — lands in that element's block; an unanchored one lands in the first block's trailing
|
|
264
|
+
region, where it landed before. A one-block map is placed byte for byte as before, and archive
|
|
265
|
+
order still does not change the bytes. The same reading fixes three neighbours: a delta written
|
|
266
|
+
in two `model { }` blocks (its second-block additions were "cannot locate … in delta.likec4"), a
|
|
267
|
+
service `model.likec4` whose `extend <fqn> { }` frame sits in a later block (the merge minted a
|
|
268
|
+
second frame in the first block, or re-added a container the model already declares there), and
|
|
269
|
+
`loam validate --all --base`, which did not grade a call edge the branch edited in a later block.
|
|
270
|
+
- **A declaration split across lines.** LikeC4's newlines are hidden tokens, so `name =` with its
|
|
271
|
+
kind on the next line, a bare name above `= kind`, a kind above the name it declares, a title on
|
|
272
|
+
the next line, a `/* … */` comment spanning a line break, an edge broken around its arrow, and an
|
|
273
|
+
edge's `#tag` on the next line are all one statement — and the splice map, reading one line per
|
|
274
|
+
statement, could not see them. Worse, `name = kind 'title'` with its `{` on the NEXT line read as
|
|
275
|
+
a bodyless element: an addition anchored after it landed between the head and its body and took
|
|
276
|
+
that body — description, children, `metadata { service }` binding — for its own, in a merge that
|
|
277
|
+
parsed and was written (exit 0). The body stays with its element now.
|
|
278
|
+
- **An `extend <fqn> { }` frame written in the landscape itself, with an edge inside it.** An
|
|
279
|
+
edge the feature added, anchored to the frame's edge, was spliced INSIDE the frame — a scope, so
|
|
280
|
+
its names re-resolved there: the delta's `zulu -> beta` landed in `extend alpha { beta = … }` as
|
|
281
|
+
an edge into alpha's own `beta` container, in a merge that parsed and was written (exit 0). It
|
|
282
|
+
lands outside the frame now. Elements inside such a frame were already read correctly.
|
|
283
|
+
- **A hyphenated id (`entity-service = …`) and a dot-kind edge (`a .async b`).** LikeC4's
|
|
284
|
+
identifiers may carry hyphens and a declared relationship kind may be written as `.kind`; the
|
|
285
|
+
splice map read neither, so such an element was "in a form placement cannot locate" and such an
|
|
286
|
+
edge made the relationship count disagree.
|
|
287
|
+
- **A `link` (or `icon`) URI followed by more code on its line.** LikeC4 reads `https://…` and a
|
|
288
|
+
relative `./…` path as one token; the splice map read its `//` as a comment that ran to the end
|
|
289
|
+
of the line, so `x = kind 'X' { link https://… }` lost its closing brace and archive refused with
|
|
290
|
+
`landscape.likec4 has no model block`. The URI is a token wherever it sits: a `link` or `icon`
|
|
291
|
+
alone on its line ends there, and the child or edge declared on the line below is its own.
|
|
292
|
+
- **A markdown string (`'''…'''` or `"""…"""`) whose prose holds an apostrophe or an odd number
|
|
293
|
+
of quotes.** The splice map read the triple as three ordinary quotes, so the first `it's` in a
|
|
294
|
+
long `description` ended the string and the rest of the file was read inside out — a map with
|
|
295
|
+
one such description above its model had no model block at all, and archive refused with
|
|
296
|
+
`landscape.likec4 has no model block`.
|
|
297
|
+
- **A delta edge whose body holds a key or tag containing `op`, `publishes` or `consumes`.** The
|
|
298
|
+
splice map read those keys anywhere in the edge's body, and took a hyphen for the end of a word,
|
|
299
|
+
so `op-version 'v2'` above `op 'cancelOrder'` read as op `v2`, and a `#op-critical` or `#op` tag
|
|
300
|
+
as an op naming the description below it. The edge then matched none of the delta's statements
|
|
301
|
+
and archive refused with "cannot locate the '<a> -> <b>' relationship in delta.likec4". A key is
|
|
302
|
+
read whole now, and `op`, `publishes` and `consumes` only from `metadata { }`, which is where
|
|
303
|
+
LikeC4 reads them. The same misreading hid such an edge from `validate --all --base` (below).
|
|
304
|
+
- **A bare carriage return (`\r` with no `\n`) between two statements.** LikeC4 breaks a line at
|
|
305
|
+
`\r` as well as `\n`, and the splice map broke only at `\n`. A statement therefore ran on over
|
|
306
|
+
a bare `\r` into the next one, and a `// comment` ran on over it and hid the next declaration.
|
|
307
|
+
Two elements, two edges or a property and a child on either side of a bare `\r` were refused as
|
|
308
|
+
"in a form placement cannot locate". A bare `\r` ends the statement and the comment now. A CRLF
|
|
309
|
+
map reads exactly as before.
|
|
310
|
+
- **An addition anchored to a declaration whose line goes on with more code.** Placement put an
|
|
311
|
+
addition on the line after its anchor. When the next declaration opened a multi-line body on the
|
|
312
|
+
anchor's own line (`alpha = … { … } beta = … {`), that next line was inside beta. An element
|
|
313
|
+
bound to alpha's service was then written inside beta as its child, in a merge that parsed and
|
|
314
|
+
was written (exit 0). A block comment opened on the anchor's line and closed on a later one
|
|
315
|
+
swallowed the addition the same way. The addition now lands right after its anchor whenever
|
|
316
|
+
code, or a block comment the line does not close, follows the anchor on its line. Two archives
|
|
317
|
+
anchored to the two declarations on such a line also write the same bytes in either order now;
|
|
318
|
+
before, whichever ran second landed above the other. A comment trailing the anchor changes
|
|
319
|
+
nothing: the addition lands on the next line, as before — unless a bare `\r` ends that `// comment`
|
|
320
|
+
and the next declaration follows it on the same line, which counts as code following the anchor.
|
|
321
|
+
- **A bodyless declaration with a comment trailing it on its line.** A parent such as
|
|
322
|
+
`api = container 'API' // the api` that gained its first child got its `{` inside the comment,
|
|
323
|
+
and archive refused: the merged landscape "would not parse". The body now opens before the
|
|
324
|
+
comment, which then trails the new closing brace. A block comment that closes on a LATER line
|
|
325
|
+
was worse: the splice map ended the declaration inside it, so a child it gained, or an element
|
|
326
|
+
or edge anchored after it, was written inside the comment, in a merge that parsed and was
|
|
327
|
+
written (exit 0). The declaration now ends before such a comment. A comment that closes on the
|
|
328
|
+
declaration's own line keeps its place, and the bytes come out as before.
|
|
329
|
+
- **A delta whose feature tag sits in a tag list with a separator.** LikeC4 reads a tag list as
|
|
330
|
+
`#a #b, #c;`: one or more tags, then comma groups, then an optional `;`. Stripping the feature's
|
|
331
|
+
own tag left `#FEAT-1, #x` as `, #x` and `#FEAT-1;` as a bare `;`, and archive refused because
|
|
332
|
+
the merged landscape "would not parse". The tag now takes the comma after it with it (or else
|
|
333
|
+
the comma before it), and takes a `;` when no tag is left in front of it. A tag after a comma no
|
|
334
|
+
longer leaves that comma behind either: `#x, #FEAT-1` lands as `#x`, where it used to land as
|
|
335
|
+
`#x,`.
|
|
336
|
+
|
|
337
|
+
What the splice map still cannot locate is still refused (`merge-failed`, nothing written), and
|
|
338
|
+
the message no longer guesses. It used to say "most often two declarations sharing one line"
|
|
339
|
+
whatever the map held — over the reported declaration, which sat on a line of its own. It names the
|
|
340
|
+
declaration (a relationship by its endpoints, where it used to give only a count) and the line
|
|
341
|
+
LikeC4 parsed it on, then the cause the text shows: "it shares that line with '<other>'" only when
|
|
342
|
+
other code precedes it on that line (its own kind keyword, or an edge's own source, is not other
|
|
343
|
+
code); "the splice map read it as part of '<other>', which starts at line
|
|
344
|
+
<n>" when the scan ran a statement on past a line break LikeC4 makes (a bare carriage return was
|
|
345
|
+
the one such break until the splice map learnt to break there too, above); and otherwise that the
|
|
346
|
+
splice map read nothing there, with the rewrite to make and a request to report the shape. Wording
|
|
347
|
+
only; the code is unchanged.
|
|
348
|
+
|
|
349
|
+
### Changed — `loam archive` refuses a delta that would bring back an operation another feature retired
|
|
350
|
+
|
|
351
|
+
Two codes, both added to the `IssueCode` union, and one additive `--json` key were added.
|
|
352
|
+
|
|
353
|
+
- **New `openapi.op-revived` (warn, gates archive).** A feature's `openapi.yaml` spells an
|
|
354
|
+
operation (by operationId) that its service's living contract no longer holds, and an ARCHIVED
|
|
355
|
+
feature retired it with an `x-loam-remove: true` marker — any archived feature, including one
|
|
356
|
+
whose directory carries the same feature id as the one being graded. Before this, the
|
|
357
|
+
quote read as an addition: a delta that had only quoted `createOrderV1` archived after the
|
|
358
|
+
feature retiring it and put it back into the living contract at exit 0, with the advisory
|
|
359
|
+
`api.op-unconsumed` the only finding. The message names the operation, the retiring feature(s)
|
|
360
|
+
and both remedies — delete the quote if it only restated the operation; archive with `--approve`
|
|
361
|
+
if the feature brings it back on purpose.
|
|
362
|
+
- **New `asyncapi.slot-revived` (warn, gates archive)** — the same on the event axis: a `channels`
|
|
363
|
+
or `operations` slot by its key, or a message by its name, that an archived feature retired. A
|
|
364
|
+
channel-slot removal's inline messages are named from that feature's `.loam-before/` snapshot
|
|
365
|
+
pre-image, read only while it still hashes to the digest its manifest recorded.
|
|
366
|
+
- **Both codes also cover an address an archived feature VACATED.** An archived feature that
|
|
367
|
+
relocated an operation (a marker on the old slot, the same operationId defined at a new one)
|
|
368
|
+
vacated the old address — and so did one that retired it there, once a later feature re-added it
|
|
369
|
+
somewhere else. A later delta quoting the operation at that address — unpinned — archived at
|
|
370
|
+
exit 0 and put the id back beside where it lives, one operationId at two slots (only the
|
|
371
|
+
advisory `openapi.duplicate-operationid` said so afterwards): the slot-keyed baseline gate saw
|
|
372
|
+
nothing living at the slot, and the id-keyed check saw the id living. That quote is now
|
|
373
|
+
`openapi.op-revived` too. Both codes are judged on the contract the archive would write: the
|
|
374
|
+
check runs the archive's own merge of the delta over the living contract and compares where each
|
|
375
|
+
operationId (each message name) stands there with where it stands now. One the merged contract
|
|
376
|
+
answers at two or more addresses — one of them new, and vacated by an archived feature — is the
|
|
377
|
+
finding; one the living contract still holds that it answers at exactly one address is not,
|
|
378
|
+
whether the delta moves it back with its own removal marker where it lives now or swaps another
|
|
379
|
+
operation in there (with or without an operationId — the merge replaces what stands at that
|
|
380
|
+
method and path). A removal marker standing where the id does not live deletes nothing, so it
|
|
381
|
+
no longer excuses the quote. The message says which archived feature moved the operation and
|
|
382
|
+
where to, or retired it there, and — apart from
|
|
383
|
+
that, since anything landed after can have moved it again — where it lives now; its remedy names
|
|
384
|
+
the address to drop: delete the quote, or, to move it back on purpose, add a removal marker
|
|
385
|
+
where it lives now — and where the delta already spells that address, so no marker can be
|
|
386
|
+
written there, archive with `--approve` instead. Where another operation lives at the quoted
|
|
387
|
+
address now (on the event axis, another message is declared at the quoted key), the message
|
|
388
|
+
names it and says archiving replaces it there — the merge writes one entry per address —
|
|
389
|
+
instead of calling the address one the living contract no longer has, and the move-back remedy
|
|
390
|
+
says to define it at another address (key) in the same delta if it is to stay: followed as it
|
|
391
|
+
read before, the remedy reached a clean gate and an archive that deleted the other operation
|
|
392
|
+
with nothing naming it. The retired-shape message names such an entry too. A quote of an
|
|
393
|
+
address an archived feature moved the operation away from is the finding even where the living
|
|
394
|
+
contract holds the operation nowhere (taken out of its new address by a hand edit, which leaves
|
|
395
|
+
no marker): the move is on the record, and the message reads as a retirement brought back,
|
|
396
|
+
naming the move. On the event axis the vacated addresses are `components.messages` keys (a
|
|
397
|
+
marker there) and the inline declarations a channel removal took; a delta that restates the
|
|
398
|
+
channel a name stands inline in with a `$ref`, or retires it, takes the name out of there and
|
|
399
|
+
is not graded. For an inline declaration the remedy names the channel (restate it with a
|
|
400
|
+
`$ref`, or retire it), never a marker nested on the inline message, which retires nothing. For
|
|
401
|
+
a `components.messages` key the remedy also names every channel (or operation) of the merged
|
|
402
|
+
contract still `$ref`-ing that key, to restate with the `$ref` pointed at the quoted key: told
|
|
403
|
+
only to retire the key, an author left the channel pointing at nothing, and its send stopped
|
|
404
|
+
sending the message — `asyncapi.remove-message-consumed`, which gates. `asyncapi.baseline-invalid`'s
|
|
405
|
+
move-back remedy says the same. A channel or operation slot is its key, so a "moved" one is a
|
|
406
|
+
retirement, as before. A message inline under a channel the delta brings back is reported by
|
|
407
|
+
the channel's finding alone only while the merged contract declares the name nowhere else; where
|
|
408
|
+
an archived feature re-declared it under `components` as it retired the channel, the inline
|
|
409
|
+
declaration is a finding of its own (one message name in two places) — it was skipped, and
|
|
410
|
+
`--approve` archived `asyncapi.duplicate-message`.
|
|
411
|
+
- **Exit code: `loam archive` now exits 1 (`not-coherent`) on this shape** without `--approve`;
|
|
412
|
+
with `--approve` it merges as before, and the operation lands in `overridden[]` and the text
|
|
413
|
+
output's "archiving despite" list, naming what came back and whose retirement it reverses.
|
|
414
|
+
`loam validate --feature` and `--all` report both codes as warnings: exit unchanged, except
|
|
415
|
+
under `--strict`, which fails on any warning and so now exits 1 on this shape. `loam status`
|
|
416
|
+
counts them as gating like every warning that gates, and `loam show <FEAT> --json` reports
|
|
417
|
+
`review.readyToArchive: false` for such a feature until the quote is deleted (or the feature is
|
|
418
|
+
archived past it with `--approve`).
|
|
419
|
+
- **`loam rebase` no longer calls a retired operation one "this feature is adding".** It pins
|
|
420
|
+
nothing for it either way, and now prints the same finding after its pin lines; the additive
|
|
421
|
+
`--json` key `revived[]` carries the findings (`severity`, `code`, `gates`, `subject`,
|
|
422
|
+
`message`). The operation's `pins[]` row still reads `unresolved`. The check runs over the
|
|
423
|
+
delta as rebase will write it, pins included, under `--dry-run` too: a quote pinned to its
|
|
424
|
+
own content is skipped by the merge, and repinned over another operation that now lives at an
|
|
425
|
+
address an archived feature moved it away from it becomes an edit that would put it back
|
|
426
|
+
there — that finding is in `revived[]` as well, and its `pins[]` row reads `repinned`. The
|
|
427
|
+
text output's revival banner no longer says "nothing was pinned" for such items.
|
|
428
|
+
- **`openapi.baseline-invalid` and `asyncapi.baseline-invalid` reworded** for a pin whose living
|
|
429
|
+
operation or slot is gone: they used to say "drop the marker", which turned the quote into the
|
|
430
|
+
revival above. They now say to re-read the living contract and delete the quote unless the
|
|
431
|
+
feature deliberately brings it back. When the living contract still holds the operationId (or,
|
|
432
|
+
for a `components.messages` key, the message name) elsewhere, the message instead says a
|
|
433
|
+
parallel feature MOVED it there (or retired it here, and another re-added it there) and where it
|
|
434
|
+
lives now, and to make any change there — never to archive past the revival finding, which would
|
|
435
|
+
answer it at both addresses. The `/loam-check` rows and `loam explain` say the same. `loam
|
|
436
|
+
rebase`'s text says such items were retired by an archived feature "or moved to another address
|
|
437
|
+
by one".
|
|
438
|
+
- **What it reads, and what it cannot see.** Only files under `features/archive/` — the archived
|
|
439
|
+
deltas' removal markers and each feature's `.loam-before/` snapshot; no git. A marker counts only
|
|
440
|
+
if the merge carried it out: the snapshot's pre-image must hold the marker's operationId at its
|
|
441
|
+
method and path (an event marker: its channel or operation key, or its message name at its
|
|
442
|
+
`components.messages` key), and a contract the archive created or never wrote lost nothing. A
|
|
443
|
+
marker archived past `openapi.remove-target-missing` / `-mismatch` (or the `asyncapi.*` twins)
|
|
444
|
+
under `--approve` deleted nothing, so a genuinely new operation it happened to name is not a
|
|
445
|
+
revival. A feature with no intact snapshot cannot be checked, and its markers count as written.
|
|
446
|
+
A removal made by hand-editing a living contract, or an archived feature deleted from disk,
|
|
447
|
+
leaves nothing to read, and a quote of such an operation still grades as an addition. A marker
|
|
448
|
+
with no `operationId` or one written at path level retired nothing when its feature archived, so
|
|
449
|
+
it is never named as the retirer.
|
|
450
|
+
|
|
451
|
+
### Added — `#suite-<name>` tags and `loam list suites`: which services a test suite needs up
|
|
452
|
+
|
|
453
|
+
No code was added. One reserved tag prefix, one list section and one positional argument were.
|
|
454
|
+
|
|
455
|
+
- **A `dynamic view` may carry `#suite-<name>` tags** to say which test suites the journey runs in —
|
|
456
|
+
a separate prefix from `#cap-`, many-to-many (one view may carry several), each declared as
|
|
457
|
+
`tag suite-<name>` in the landscape's `specification` block like every LikeC4 tag, so a mistyped
|
|
458
|
+
suite is a parse error.
|
|
459
|
+
- **A view carrying only `#suite-` tags is now a use case.** The one opt-in predicate takes three
|
|
460
|
+
prefixes (`cap-`, `req-`, `suite-`), and so does the byte scan that lets a fleet with no use cases
|
|
461
|
+
skip the LikeC4 load. Such a view's hops are graded by `loam validate` like any other use case's
|
|
462
|
+
(`usecase.step-unbacked`, `usecase.step-contested`, `usecase.step-unlinked` — at fleet scope, and
|
|
463
|
+
per service for a flow beside a service's model), and every reader that names use cases sees it:
|
|
464
|
+
`loam diff`'s removed-operation victims, `loam delta` and `loam status`'s use-case blast radius,
|
|
465
|
+
and the context packs. A view that already carried `#cap-` or `#req-` is unaffected, and an
|
|
466
|
+
untagged view is still never graded. On a fleet that had drawn `#suite-`-tagged views before this
|
|
467
|
+
release, those views can now earn findings.
|
|
468
|
+
- **New section `loam list suites [<name>] [--json]`** — the answer to "which services must be up
|
|
469
|
+
for suite X". `[section]` accepts `suites`, and a new optional positional `[name]` narrows the
|
|
470
|
+
listing to one suite. Additive `--json` keys, emitted only with this section: `suites[]` (`name`,
|
|
471
|
+
`views[]` of `{id, file}`, `services[]` — enumerated service ids, sorted — and `unresolved[]` of
|
|
472
|
+
`{element, external}`: the participants that resolve to no service, `external` true when the
|
|
473
|
+
element or anything it is drawn inside carries `#external`, the walk stopping at the first
|
|
474
|
+
element bound to or titled for a service as `loam gate`'s partner reading does), `ungrouped[]` (`{id, file}` of every
|
|
475
|
+
use case carrying no `#suite-` tag, never narrowed by `[name]`), and `useCases` (`state`:
|
|
476
|
+
`absent` | `ok` | `unreadable`, beside `unreadable` and, when unreadable, `error`, spelled as
|
|
477
|
+
`loam list capabilities` spells them). Over an `architecture/` that does not parse the section
|
|
478
|
+
degrades instead of refusing, and `suites` / `ungrouped` are absent rather than `[]`. Only the
|
|
479
|
+
fleet map's use cases are listed. The text view prints one block per suite — its services, the
|
|
480
|
+
unresolved participants marked `!`, its use cases — then the use cases in no suite.
|
|
481
|
+
- **Refusals use existing codes.** An unknown suite name is `unknown-target` with close names; a
|
|
482
|
+
`[name]` after any other section, and `--needs-work` with `suites`, are `invalid-option` (a name
|
|
483
|
+
after another section used to be commander's "too many arguments", also `invalid-option` under
|
|
484
|
+
`--json`). The campaign flags (`--subsystem`, `--owners`) refuse `suites` as they refuse every
|
|
485
|
+
section but `services`.
|
|
486
|
+
- The MCP tool `loam_list` gains the matching optional `name` argument, and its `section`
|
|
487
|
+
description now lists `glossary` and `suites`. `name` without `section` is refused as invalid
|
|
488
|
+
params (`-32602`) rather than read as a section.
|
|
489
|
+
- `examples/docs` tags its one use case `#suite-smoke` and `#suite-notifications`, so
|
|
490
|
+
`loam list suites` there answers checkout-web, notification-service and order-service, with
|
|
491
|
+
`customer` and the `#external` topic `kafka.orderEvents` named as unresolved.
|
|
492
|
+
|
|
493
|
+
### Changed — the use-case byte scan opens on a `#` tag reference, not on a bare prefix
|
|
494
|
+
|
|
495
|
+
No code, flag or payload key was added, changed or retired.
|
|
496
|
+
|
|
497
|
+
- **loam starts LikeC4 for use cases only when a document references a reserved tag.** The scan in
|
|
498
|
+
front of every reader of the fleet's use cases, and of a service's own flows, used to open on the
|
|
499
|
+
bare text `cap-`, `req-` or `suite-` anywhere in a file. It now opens on `#cap-`, `#req-` or
|
|
500
|
+
`#suite-`, with only whitespace, line breaks or comments between the `#` and the name, as LikeC4
|
|
501
|
+
allows. That is the only way a view carries a tag, so no use case is missed.
|
|
502
|
+
- **A broken `.likec4` beside a service's model that names a prefix only in prose no longer fails
|
|
503
|
+
the service.** In `0.2.0-alpha.8`, a file with no tagged view that mentioned `cap-` or `req-`
|
|
504
|
+
anywhere — a title, a comment — was loaded, and if it did not parse, `validate --service` (and
|
|
505
|
+
`--all`, per service) reported `usecase.flow-invalid`, an error, exit 1. This release's `suite-`
|
|
506
|
+
prefix would have added a third such substring (an id like `test-suite-runner`). Now the file is
|
|
507
|
+
skipped, like any untagged diagram.
|
|
508
|
+
- **The same holds at fleet scope.** Suppose `architecture/` does not parse, carries no tagged view,
|
|
509
|
+
and only declares `tag cap-…` / `tag req-…` or names a prefix in prose. It now reads as declaring
|
|
510
|
+
no use case. `useCases.unreadable` is then `false` where it used to be `true` in
|
|
511
|
+
`loam status --json`, `loam delta --json` and `loam list capabilities --json`, and so is
|
|
512
|
+
`useCaseScan.unreadable` in `loam context --json`. `validate --all` still reports that map as
|
|
513
|
+
`landscape.invalid`.
|
|
514
|
+
- A `#` followed by a reserved prefix still opens the scan wherever it is written, a comment
|
|
515
|
+
included.
|
|
516
|
+
|
|
517
|
+
### Changed — `loam-feature` asks whether a feature is owed, and analyzes before it pins
|
|
518
|
+
|
|
519
|
+
No code, flag or payload key was added, changed or retired. What changes is the protocol text an
|
|
520
|
+
agent is handed, the `intent.md` a new feature starts from (and which of its text `intent.empty`
|
|
521
|
+
counts), and what the documentation says a feature's contract delta holds.
|
|
522
|
+
|
|
523
|
+
- **`loam instructions loam-feature` opens with a step 0.** A change that touches no requirement,
|
|
524
|
+
operation or fleet edge — a refactor, a dependency bump, a fix back to behaviour the living spec
|
|
525
|
+
already documents — needs no feature: a code PR, then `loam validate --service <id>` and
|
|
526
|
+
`loam vouch --pack --service <id>` in the service repo, and a person's vouch. WORKFLOW.md's "The
|
|
527
|
+
honestly-small change" now opens with the same lane.
|
|
528
|
+
- **A new, ungraded step 4b, "Analyze before you pin", sits between authoring and `loam rebase`.**
|
|
529
|
+
The agent reads `loam show <FEAT> --json`, `loam context <service> --feature <FEAT> --json` and the
|
|
530
|
+
`explore` neighbours, then writes questions under `## Open questions` in `intent.md`: ambiguous
|
|
531
|
+
SHALLs, values, limits and windows two services state differently (and which side refuses),
|
|
532
|
+
`Realizes:` lines that do not deliver their capability, each edge's timeout, retry, partial
|
|
533
|
+
failure and compensation, and neighbours left out of `--touches`. A person answers by editing the
|
|
534
|
+
file. `loam-implement` sends an ambiguity it meets to the same section.
|
|
535
|
+
- **`loam new` scaffolds an optional `## Open questions` heading in `intent.md`**, over a comment
|
|
536
|
+
only. **`intent.empty` now reads past that section:** questions an agent writes there are not the
|
|
537
|
+
feature's Why, so an intent whose only prose is open questions still gates the archive. The generated `loam-feature` command and skill pointers
|
|
538
|
+
list both new steps in their spine; an unedited pointer refreshes on the next `loam init`.
|
|
539
|
+
- **A feature's `openapi.yaml` / `asyncapi.yaml` is documented as holding only what the feature
|
|
540
|
+
adds, changes or removes.** SCHEMA.md, WORKFLOW.md, the `loam-spine` and `loam-feature` pages,
|
|
541
|
+
the `spec.md` scaffold's comment and the example deltas taught "a complete document, not a
|
|
542
|
+
patch". The merge never needed one: it walks only the paths, methods and slots a delta spells, a
|
|
543
|
+
local `$ref` resolves into the living contract, and an add-only delta needs no `loam rebase`.
|
|
544
|
+
Each needless quote is one more pin and, once a parallel feature removes the quoted operation,
|
|
545
|
+
either a blocking `openapi.baseline-invalid` (pinned) or an archive that puts the retired operation
|
|
546
|
+
back (unpinned); `loam-check`'s fix for the first now says to delete the quote. The
|
|
547
|
+
`loam-spine` page also no longer says path-level keys and components go unpinned —
|
|
548
|
+
`x-loam-baselines` covers them.
|
|
549
|
+
- **WORKFLOW.md gains "Build on the envelope":** a tracker export from `loam tasks <FEAT> --json`
|
|
550
|
+
keyed on unit ids (and what rewording re-keys one), a rollup of the per-service CI summaries
|
|
551
|
+
that reads a missing or old report as unknown, and `npx likec4 mcp` beside `loam mcp` on the same
|
|
552
|
+
docs repo.
|
|
553
|
+
- **The `loam-codes` page points at AGENTS.md's "Reference pages — fetch when needed" section** —
|
|
554
|
+
it named a "The reference pages" section that does not exist — and lists `loam-report` among the
|
|
555
|
+
pages `loam instructions` prints.
|
|
556
|
+
|
|
557
|
+
### Added — `loam new --modifies <Req-ID>`: a small change is new → edit → archive
|
|
558
|
+
|
|
559
|
+
- **New repeatable flag `--modifies <Req-ID>` on `loam new`.** Each value names a living
|
|
560
|
+
requirement, by its `Requirement-ID:`, in a service the feature `--touches`. The scaffold copies
|
|
561
|
+
that requirement into the service's delta, uncommented, under `## MODIFIED Requirements`, with
|
|
562
|
+
its `Based-On:` pin already set. It is the same pin `loam rebase` writes, so a rebase right after
|
|
563
|
+
reports it `unchanged`, and the living text moving before archive is still `delta.baseline-stale`.
|
|
564
|
+
An `arch.spec.md` requirement lands in a scaffolded `specs/<svc>/arch.spec.md`. Before this, the
|
|
565
|
+
author copied the requirement by hand and ran `loam rebase` to pin it.
|
|
566
|
+
- **When every touched service has a `--modifies` and no `--new-service` or `--capability` is
|
|
567
|
+
given, no `delta.likec4` is written.** A feature that only rewrites requirements changes no
|
|
568
|
+
architecture, and the scaffold already told authors to delete the file in that case. `created`
|
|
569
|
+
in `--json` simply omits it. Any other combination scaffolds `delta.likec4` as before.
|
|
570
|
+
- **Refusals use existing codes.** An id that no touched service's living spec carries is
|
|
571
|
+
`unknown-target`. The message names the close ids in those specs, or the untouched service that
|
|
572
|
+
does carry the id (`add --touches <svc>`). A value that is not a Requirement-ID, or an id two
|
|
573
|
+
touched documents both carry, is `invalid-option`. A living spec loam cannot decode is
|
|
574
|
+
`merge-failed`, as `loam rebase` refuses it. Every refusal writes nothing.
|
|
575
|
+
- The opt-in MCP author tool `loam_new` gains the matching `modifies` argument. No code or payload
|
|
576
|
+
key was added.
|
|
577
|
+
|
|
578
|
+
### Added — `loam_adopt`: the adopt brief on the MCP server
|
|
579
|
+
|
|
580
|
+
- **`loam mcp` serves a sixteenth read tool, `loam_adopt`**, mirroring `loam adopt [--service <id>]
|
|
581
|
+
[--subsystem <name>] [--targets] --json` (JSON arguments `service`, `subsystem`, `targets`). Its
|
|
582
|
+
result is the CLI's `adopt` envelope, verbatim. It is advertised `readOnlyHint: true` like every
|
|
583
|
+
base tool, because `adopt` writes nothing: it hands the agent the brief, and the agent writes the
|
|
584
|
+
baseline. Until now an MCP-only host could fetch the loam-adopt protocol through
|
|
585
|
+
`loam_instructions` and could not run its first step, `loam adopt --service <id> --json`. No
|
|
586
|
+
code, flag or payload key was added to the CLI; `tools/list` returns one more tool.
|
|
587
|
+
|
|
588
|
+
### Changed — `loam init --create` with no service prints the docs repo's first hour
|
|
589
|
+
|
|
590
|
+
- **A run that creates a docs repo and binds no service now ends with `next — the docs repo's first
|
|
591
|
+
hour:`**: `loam seed --from fleet.yaml` (or draw the map by hand), `loam init --docs <docs-repo>
|
|
592
|
+
--service <id> --agent-profile service` in each service's own repo, `loam adopt --service <id>
|
|
593
|
+
--json` there, and `loam status`. It used to print nothing after the scaffold list. These are
|
|
594
|
+
WORKFLOW.md's Day zero steps and the rungs `loam status` already names on an empty fleet. A run
|
|
595
|
+
that binds a service still prints the single-repo trial's hour, and a join still prints neither.
|
|
596
|
+
Human output only: no code, flag or payload key was added, and the `--json` envelope is unchanged
|
|
597
|
+
for both variants.
|
|
598
|
+
|
|
599
|
+
### Changed — every service bind loam prints names `--agent-profile service`
|
|
600
|
+
|
|
601
|
+
No code, flag or payload key was added, changed or retired. What changes is printed text and one
|
|
602
|
+
`--json` value.
|
|
603
|
+
|
|
604
|
+
- **The bind command now reads `loam init --docs <path> --service <id> --agent-profile service`**
|
|
605
|
+
in `loam status`'s `next.bind-service` step, its `loam explain` entry, the docs repo's first hour,
|
|
606
|
+
the generated AGENTS.md (`## The cycle`, step 0), and the `loam-adopt` and `loam-implement`
|
|
607
|
+
protocols. README, QUICKSTART and docs/SERVICE-TEAMS.md already told a fleet to bind each service
|
|
608
|
+
checkout this way, because `--agent-profile` defaults to `full`, and `full` writes `loam-feature`
|
|
609
|
+
and `loam-ship` into the service repo. But every bind command loam printed left the flag out, and
|
|
610
|
+
an agent runs the command loam prints. WORKFLOW.md's Day zero step 3 now spells it too.
|
|
611
|
+
- **`next.bind-service`'s `command` in `status --json` `next[]` carries the new text.** Its
|
|
612
|
+
`execution` block still names the same two placeholders.
|
|
613
|
+
- **Two binds keep their old form on purpose.**
|
|
614
|
+
- `loam-adopt` names the single-repo trial (`--create`, used only when the docs repo does not
|
|
615
|
+
exist yet) as the one bind that keeps the full profile.
|
|
616
|
+
- `doctor.service-unbound`'s fix is a re-run in a checkout that is already wired. An omitted
|
|
617
|
+
`--agent-profile` there keeps the profile that checkout recorded.
|
|
618
|
+
|
|
619
|
+
### Changed — the OpenSpec compatibility pin moves to v1.14.1, and a weekly canary watches the newest release
|
|
620
|
+
|
|
621
|
+
No code, CLI flag or payload key was added, changed or retired. The `audit-openspec` /
|
|
622
|
+
`migrate-openspec` payload shape is unchanged, but the value it reports as the certified release moves.
|
|
623
|
+
|
|
624
|
+
- **`audit-openspec` and `migrate-openspec` now report OpenSpec v1.14.1 as the certified release.**
|
|
625
|
+
The banner line and `baselines.release` under `--json` carry `1.14.1` / `v1.14.1` /
|
|
626
|
+
`87c3595ace6a2e22957f39ebe5b74c2f2316e0cb` (released 2026-10-05) instead of `1.9.0` / `2826b88`.
|
|
627
|
+
`baselines.mainCanary` is unchanged. The corpus gate's `--baseline release` is now the v1.14.1
|
|
628
|
+
corpus: 223 Markdown files, 765 requirements and 2475 scenarios, every file round-tripped. v1.9.0
|
|
629
|
+
stays as the new `--baseline previous` regression pin beside `legacy` (v1.7.0) and `canary`, and
|
|
630
|
+
the scheduled `ci.yml` matrix runs all four. The sweep found no parser gap. The grammar OpenSpec
|
|
631
|
+
1.13 added — a closing `#` run, `*`/`+` bullets on `REMOVED` and `RENAMED`, repeated delta
|
|
632
|
+
sections — was already read by the migration, and a new test pins the repeated sections.
|
|
633
|
+
`audit-openspec` and `migrate-openspec --apply`, run on OpenSpec's own v1.14.1 workspace, staged
|
|
634
|
+
all 251 living requirements and 747 scenarios across 36 services. The seven vendored fixtures stay
|
|
635
|
+
at v1.9.0, since v1.14.1 has no shape they lack. MIGRATING-from-OpenSpec.md's version matrix
|
|
636
|
+
records all of this.
|
|
637
|
+
- **The corpus sweep reads each file through the migration's own reader.** That reader is the one
|
|
638
|
+
`audit-openspec` and `--apply` use: loam's parser plus the grammar only OpenSpec accepts. The
|
|
639
|
+
sweep used to read with loam's parser alone. Every pinned corpus gives the same totals either way.
|
|
640
|
+
- **A new scheduled workflow, `.github/workflows/openspec-canary.yml`, watches the newest OpenSpec
|
|
641
|
+
release.** It runs weekly and on manual dispatch. It resolves the newest `vX.Y.Z` tag with
|
|
642
|
+
`git ls-remote` and sweeps it with the new `--baseline latest` mode of `npm run
|
|
643
|
+
test:openspec-corpus`. That mode reports how the totals moved from the certified pin instead of
|
|
644
|
+
refusing an unpinned commit, and still fails on a file that does not round-trip. The workflow also
|
|
645
|
+
audits OpenSpec's own workspace and diffs upstream's parser sources against the pin, in its step
|
|
646
|
+
summary. It has no push or pull-request trigger and blocks nothing; CONTRIBUTING.md's "The
|
|
647
|
+
OpenSpec canary" is the runbook. The README's note that `--baseline` defaults to `canary` was
|
|
648
|
+
wrong — it defaults to `release` — and now says so.
|
|
649
|
+
|
|
650
|
+
### Added — documentation: running loam where service work runs, a CLI-only first ten minutes, and what loam costs an agent
|
|
651
|
+
|
|
652
|
+
No code, flag, command or payload key was added, changed or retired; the binary is unchanged. The
|
|
653
|
+
new paths are `docs/SERVICE-TEAMS.md` and `scripts/payload-sizes.ts`.
|
|
654
|
+
|
|
655
|
+
- **README has a service-repo CI recipe** beside the docs repo's fleet-edges one. The workflow
|
|
656
|
+
checks out the service and the docs repo as sibling subdirectories of the workspace, so a
|
|
657
|
+
committed relative `docsDir` such as `../docs` resolves. It does not use `path: ../docs`, which
|
|
658
|
+
`actions/checkout` refuses. It runs `loam validate --service <id> --json` at the same pinned
|
|
659
|
+
version, and two `jq` programs turn the findings, or a refusal, into `::error` / `::warning`
|
|
660
|
+
annotations and a `$GITHUB_STEP_SUMMARY` table. A deploy pipeline can add
|
|
661
|
+
`loam gate --service <id> --since <ref>`. It is followed by setup for cloud agents in a service
|
|
662
|
+
repo: a `copilot-setup-steps.yml` and a Codex environment setup script, each cloning the docs
|
|
663
|
+
repo into the committed `docsDir` with a read token from a secret.
|
|
664
|
+
- **README's day-zero binding passes `--agent-profile service`**, and says why: the profile
|
|
665
|
+
defaults to `full`, which puts `loam-feature` and `loam-ship` into every service checkout.
|
|
666
|
+
QUICKSTART's command for joining a shared docs repo passes it too.
|
|
667
|
+
- **QUICKSTART opens with ten minutes of CLI only.** A two-service `fleet.yaml`, `loam seed`, one
|
|
668
|
+
provider `openapi.yaml` and one misspelled `op` on an edge give `spine.op-undefined`, with no
|
|
669
|
+
agent and no adoption. A requirements-only `loam new --touches … --touches …` then validates and
|
|
670
|
+
plans its archive before either service is adopted. Every output shown was produced by running
|
|
671
|
+
the commands. The single-project path moves to sections 3–6.
|
|
672
|
+
- **`docs/SERVICE-TEAMS.md`, one screen for a team whose repository is one service in a fleet:**
|
|
673
|
+
four commands, five slash commands, what a person signs when they vouch, and the fifteen codes
|
|
674
|
+
they will actually meet. README, QUICKSTART and the Docs list link to it.
|
|
675
|
+
- **COMPARISON.md names OpenSpec, Spec Kit and Kiro again, in one dated section.** "Spec-driven
|
|
676
|
+
change tools, as of October 2026" sets out what each offers for a change that crosses
|
|
677
|
+
repositories: OpenSpec Stores and worksets, `show --diff` and the Cloud Agent; Spec Kit's
|
|
678
|
+
contract-driven guide, `/speckit.converge` and MCP server; Kiro's Design-First specs and Kiro
|
|
679
|
+
Web. It records what is still open upstream (OpenSpec #725, Spec Kit #4583) and where loam
|
|
680
|
+
differs, including the two gaps it has against them. Each claim links its source. The rest of
|
|
681
|
+
the page stays an ecosystem map, and `test/docs-drift.test.ts` now allows the three names only
|
|
682
|
+
inside that dated section.
|
|
683
|
+
- **`docs/BENCHMARKS.md` has a table of agent-facing payload sizes**, in bytes and ~tokens:
|
|
684
|
+
the generated `AGENTS.md`, `loam adopt --service <id> --json` with and without `--targets`,
|
|
685
|
+
every `loam instructions` page with and without `--no-fix-tables`, and `loam explain <code>
|
|
686
|
+
--json` across the whole vocabulary. `npx tsx scripts/payload-sizes.ts --write` generates it from
|
|
687
|
+
the tree, and `test/docs-facts.test.ts` regenerates it and fails when the committed table
|
|
688
|
+
differs. README's adopt row now points at that table instead of quoting a dated figure.
|
|
689
|
+
`test/docs-drift.test.ts` now also requires every exact `@ybotok/loam@<version>` in README to be
|
|
690
|
+
this checkout's version.
|
|
691
|
+
|
|
692
|
+
### Changed — `loam archive`'s overwrite warnings name the current consumers
|
|
693
|
+
|
|
694
|
+
No code or flag was added. Existing warnings now fill a payload key that was always empty.
|
|
695
|
+
|
|
696
|
+
- **`openapi.op-modified` and `asyncapi.message-modified` now say who the overwrite affects.**
|
|
697
|
+
Each message ends with one of three phrases: `N current consumer(s) still name it`,
|
|
698
|
+
`whether current consumers name it could not be fully answered`, or
|
|
699
|
+
`no current consumer names it`. The warning's `details` (until now always `[]`) list the
|
|
700
|
+
consumers, in `archive --json` `warnings[]` and, when the contract merge is refused as
|
|
701
|
+
`not-coherent`, in the same warning's entry in `issues[]`:
|
|
702
|
+
- edges of the `architecture/` project;
|
|
703
|
+
- other services' living requirements, in either spec file;
|
|
704
|
+
- use-case hops;
|
|
705
|
+
- and last, any consumer whose documents could not be read, or any caller drawn without an
|
|
706
|
+
`op`.
|
|
707
|
+
|
|
708
|
+
The human plan prints the same lines under the warning. This is the scan `loam diff` runs for
|
|
709
|
+
`diff.op-modified` and `diff.message-modified`. Here it runs over the living fleet as it stands
|
|
710
|
+
before the merge, and only when one of these warnings fires. On the 120-service benchmark fleet
|
|
711
|
+
that costs about 130 ms. Severity is unchanged, and neither warning gates the archive. Whether a
|
|
712
|
+
change breaks a consumer is still not graded (`oasdiff breaking` / `asyncapi diff` do that).
|
|
713
|
+
- `openapi.component-modified` and `asyncapi.component-modified` do not name consumers. A
|
|
714
|
+
component's consumers are the operations or messages whose `$ref` closure reaches it, and loam
|
|
715
|
+
has no index of those.
|
|
716
|
+
|
|
717
|
+
### Fixed — an MCP tool argument is never read in another argument's slot
|
|
718
|
+
|
|
719
|
+
No code, flag or payload key was added, changed or retired. Two MCP input schemas each gained one
|
|
720
|
+
JSON Schema keyword, named below.
|
|
721
|
+
|
|
722
|
+
- **A `tools/call` that supplies an optional positional argument while leaving out an earlier one is
|
|
723
|
+
refused as invalid params (`-32602`)**, naming the argument left out, as a missing required
|
|
724
|
+
argument already is. Positionals reach the command by position, so the later value used to be read
|
|
725
|
+
as the earlier one: `loam_instructions` with `args` and no `workflow` took the first arg as the
|
|
726
|
+
workflow name. An empty `args` still passes. The defect is present in `0.2.0-alpha.8`.
|
|
727
|
+
- **The MCP tools' advertised input schemas now say that `name` needs `section` and a non-empty
|
|
728
|
+
`args` needs `workflow`**, the rule a tool call already enforces, so a client that validates a
|
|
729
|
+
call against `tools/list` is no longer refused `-32602` for a call the schema allowed.
|
|
730
|
+
`loam_list`'s schema gains `dependentRequired` (`name` → `section`); `loam_instructions`' gains
|
|
731
|
+
`dependentSchemas`, requiring `workflow` once `args` has a member, because an empty `args` still
|
|
732
|
+
passes. Both are standard JSON Schema keywords; no tool, argument or other schema key changed.
|
|
733
|
+
|
|
734
|
+
### Fixed — `loam validate` reports an unreadable verification record from the docs repo
|
|
735
|
+
|
|
736
|
+
No code, flag or payload key was added. An existing code fires in one more place.
|
|
737
|
+
|
|
738
|
+
- **`evidence.record-unreadable` (warn) now fires on the feature target too**, under
|
|
739
|
+
`validate --feature <FEAT>`, `validate <FEAT>` and every active feature under `validate --all`, run
|
|
740
|
+
from anywhere. A `features/<FEAT>/verification.yaml` or `verification/<svc>.yaml` with git
|
|
741
|
+
conflict markers, broken YAML, a shape that is not a record, or a summary that contradicts its
|
|
742
|
+
claims used to pass `validate --all` in the docs repo with no finding. `verify` refused the file,
|
|
743
|
+
and `status`, `gate` and `tasks` reported it. Until now only the service target raised this code,
|
|
744
|
+
and only from the service's own repository. The finding names the file and gives the same repair
|
|
745
|
+
as the service-target finding. Its `locations` hold that file as `primary`. It is a warning, as
|
|
746
|
+
it is on the service target. `valid`, the exit code and `loam archive` are unchanged, and
|
|
747
|
+
`--strict` fails on it. In a service repository's `validate --all` the same file is reported
|
|
748
|
+
twice: once by the service target and once by the feature target.
|
|
749
|
+
|
|
750
|
+
### Fixed — `loam diff` reads a spec saved as UTF-16 without a byte-order mark the same way on both sides
|
|
751
|
+
|
|
752
|
+
No code, flag, path or payload key was added, changed or retired.
|
|
753
|
+
|
|
754
|
+
- On disk, a `spec.md` or `arch.spec.md` saved as UTF-16 without a byte-order mark was refused as
|
|
755
|
+
unreadable. At the base ref, the same bytes were read as a readable spec with no requirements.
|
|
756
|
+
That happened because only the working-tree side checked for NUL characters. So an untouched file
|
|
757
|
+
was suspended on one side only, and a branch that re-saved the file as UTF-8 reported every
|
|
758
|
+
requirement in it as `diff.requirement-added`. The base side now applies the same decoding rule as
|
|
759
|
+
the working tree. Both sides report the file as unreadable, and the spec axis is suspended. The
|
|
760
|
+
contract axes are unchanged: their readers on both sides already applied the same rule.
|
|
761
|
+
|
|
762
|
+
### Fixed — a writer waiting on the lock no longer deletes the next writer's live lock
|
|
763
|
+
|
|
764
|
+
No code, flag, path or payload key was added, changed or retired. What changes is that concurrent
|
|
765
|
+
writers no longer fail, or share one commit window, when a lock holder exits while another writer
|
|
766
|
+
waits for it.
|
|
767
|
+
|
|
768
|
+
- **A stale `.loam-lock` is broken only while the holder judged dead still holds it.** A writer
|
|
769
|
+
that finds the lock held reads the holder, and if that pid no longer exists on this host, it
|
|
770
|
+
breaks the lock. Under CPU load the holder could release normally and exit between that read and
|
|
771
|
+
the pid check, and a third writer could take the lock in the same gap. The waiter then deleted
|
|
772
|
+
the third writer's live lock and took one of its own, so two writers were inside one commit window.
|
|
773
|
+
One of four concurrent `loam gherkin --json` runs failed as `merge-failed` (`ENOENT … rename
|
|
774
|
+
'….feature.loam-<pid>-…tmp'`): the second writer's recovery read the first's in-flight journal as
|
|
775
|
+
an interrupted commit and swept its staged files. The code is shared by every writer that takes
|
|
776
|
+
the lock (`archive`, `unarchive`, `new`, `seed`, `rebase`, `subsystem`, `verify --record`, `vouch`,
|
|
777
|
+
`gherkin`), and the defect is present in `0.2.0-alpha.8`. The waiter now re-reads the lock after
|
|
778
|
+
its verdict, and moves it to a private name before removing it. Anything it moved that is not the
|
|
779
|
+
lock it judged goes back in place. A crashed holder's lock is still reclaimed as before.
|
|
780
|
+
|
|
781
|
+
### Fixed — `validate --all --base` grades the landscape by what the branch changed in it
|
|
782
|
+
|
|
783
|
+
No code, flag, path or payload key was added, changed or retired; the unscoped `validate --all` is
|
|
784
|
+
unchanged.
|
|
785
|
+
|
|
786
|
+
- **A call edge the branch adds or edits in `architecture/` is graded against its provider's
|
|
787
|
+
contract**, even when the provider is not in scope. README's own example — `authorisePayment` on
|
|
788
|
+
an edge into a contract spelling it `authorizePayment` — used to pass both PR gates on the example
|
|
789
|
+
fleet and, even under `--strict`, on a freshly seeded spine-first fleet. It now fails as
|
|
790
|
+
`spine.op-undefined` (and `spine.op-deprecated`, `spine.op-link-missing` and `spine.resolved` read
|
|
791
|
+
the same way), filed on the `landscape` target. Only those edges are graded: the provider is not
|
|
792
|
+
pulled into scope, so its own documents — on a seeded fleet, its `service.no-model` — stay out of
|
|
793
|
+
the PR's verdict. An edge is new when its (source, target, op) is; a reformat grades nothing.
|
|
794
|
+
- **An edge whose metadata or tags hold a word containing `op` is graded on the op it names.**
|
|
795
|
+
`--base` finds the branch's edges by reading the map's text, and that reading took
|
|
796
|
+
`op-version 'v2'` above `op 'cancelOrdr'` for op `v2`, and a `#op-critical` tag for an op naming
|
|
797
|
+
the description below it. The edge it read matched no edge in the parsed map, so the branch's
|
|
798
|
+
misspelt `cancelOrdr` was graded nowhere: exit 0 and `valid: true`, where the unscoped run
|
|
799
|
+
reports `spine.op-undefined`. A key is read whole now, and the op only from `metadata { }`.
|
|
800
|
+
- **An edge written after a bare carriage return is graded on its own source.** After a `link`,
|
|
801
|
+
`description` or `technology` property ending in a bare `\r`, the parent's sourceless
|
|
802
|
+
`-> target` edge was read as an edge FROM `link` (or `description`, or `technology`), and an edge
|
|
803
|
+
after a `// comment` ending in a bare `\r` was not read at all. Either way the branch's op on that
|
|
804
|
+
edge was graded nowhere, with the same exit 0. LikeC4 breaks a line at a bare `\r`, and the
|
|
805
|
+
reading does too now.
|
|
806
|
+
- **The landscape re-grades a fleet-wide vocabulary only when the file that carries it changed.**
|
|
807
|
+
`permissions.*` rides a scoped run only when `architecture/permissions.yaml` changed,
|
|
808
|
+
`capability.*` only when `architecture/capabilities.yaml` or `capabilities/` did,
|
|
809
|
+
`obligation.invalid` and `obligation.unapplied` only when `architecture/obligations.yaml` did,
|
|
810
|
+
`glossary.*` only when `glossary/` did, and `agents.stale` / `docs.binary-behind` only when
|
|
811
|
+
`AGENTS.md` did. A one-line map edit on the example fleet is no longer red under `--strict` on four
|
|
812
|
+
warnings that were all there at the base.
|
|
813
|
+
- README's spine-first section now says that under the `--base` gate a PR adding a service's
|
|
814
|
+
`openapi.yaml` stays red until that service has a `model.likec4` — `service.no-model` is the
|
|
815
|
+
ratchet working as designed.
|
|
816
|
+
- **Each service target in scope reports a committed map that does not parse, even when the branch
|
|
817
|
+
left `architecture/` alone.** On such a branch the scoped run emits no `landscape` target, yet
|
|
818
|
+
each service in scope still said `spine.landscape-invalid … the parser output is reported once,
|
|
819
|
+
on the landscape target`, with empty `details`. So the parser output was reported nowhere. Each
|
|
820
|
+
service's `spine.landscape-invalid` now carries the parser output in `details` whenever the run
|
|
821
|
+
has no `landscape` target, as `validate --service` always has. A feature target does not report
|
|
822
|
+
the map, so a run whose only targets are features still says nothing about it. The defect
|
|
823
|
+
predates this release's `--base` changes and is present in `0.2.0-alpha.8`.
|
|
824
|
+
|
|
825
|
+
### Fixed — one LikeC4 typo no longer floods agent-facing output
|
|
826
|
+
|
|
827
|
+
No code, flag, path or payload key was added, changed or removed; what moves is the TEXT of the
|
|
828
|
+
diagnostics LikeC4 hands loam, and one warning that stops cascading.
|
|
829
|
+
|
|
830
|
+
- **A parser's "Expecting: one of these possible Token sequences" message is cut to its first four
|
|
831
|
+
alternatives plus `… (+N more)`**, keeping the `but found: '…'` line — on every surface that
|
|
832
|
+
prints a LikeC4 diagnostic (`validate`, `context`, `show`, `tasks`, `doctor`, `gate`,
|
|
833
|
+
`archive`). One dropped quote in the example fleet's order-service model made `loam validate
|
|
834
|
+
--service order-service` print 18,503 lines (434 KB; 458 KB as `--json`); it now prints 27 lines
|
|
835
|
+
(1.7 KB; 5.5 KB as `--json`).
|
|
836
|
+
- **Each LikeC4 diagnostic is reported once, in line order.** The same message at the same line of
|
|
837
|
+
the same file used to be listed twice, and the lexer's error led the parser's even when it sat a
|
|
838
|
+
dozen lines below the fault. The `N error(s)` count in `c4.invalid`, `delta.invalid` and the other
|
|
839
|
+
parse findings now counts distinct diagnostics.
|
|
840
|
+
- **`covers.unknown` is no longer raised for a service whose own `model.likec4` does not parse.**
|
|
841
|
+
`c4.invalid` is the one finding; the six `covers.unknown` warnings the example fleet showed for one
|
|
842
|
+
dropped quote named ids that were spelled right. The same suspension already applied to a model
|
|
843
|
+
extending a map that does not parse.
|
|
844
|
+
- **A broken document no longer bloats anybody else's briefing.** A dropped quote in a feature's
|
|
845
|
+
`delta.likec4` made `loam context order-service --json` 626 KB instead of 24 KB (now 27 KB). The
|
|
846
|
+
fleet map's parse error reached `useCaseScan.error` on every service's `context --json` and the
|
|
847
|
+
`loam gate` "landscape cannot be used" message raw — 173 KB and 152 KB — and now goes through the
|
|
848
|
+
same spelling as every other diagnostic, which adds its 1-based `L<n>:` line (23 KB and 2 KB).
|
|
849
|
+
|
|
850
|
+
### Fixed — `doctor.agent-files-stale` fires on a changed body, not on every release
|
|
851
|
+
|
|
852
|
+
No code, flag, path or payload key was added, changed or retired; what a repo NOTICES is that
|
|
853
|
+
`loam doctor` stops warning about generated command and skill files whose only difference is the
|
|
854
|
+
version stamp.
|
|
855
|
+
|
|
856
|
+
- The stamp sits inside every generated command and skill file, so every release used to turn every
|
|
857
|
+
one of them stale in every repo, even when the release had not changed a word of the pointer — a
|
|
858
|
+
warning that is always on, which then hid the release where a pointer really did change. A file is
|
|
859
|
+
now stale only when its body, the stamp line set aside, is not what this loam writes, AND its stamp
|
|
860
|
+
is missing or older. Line endings do not count (a Windows checkout that converts them to CRLF is
|
|
861
|
+
not a change). A customized body under a current stamp stays quiet, as before. Same code, same severity;
|
|
862
|
+
the `agents.staleFiles` list in `loam doctor --json` simply stops naming stamp-only files.
|
|
863
|
+
|
|
864
|
+
### Fixed — relationship order is the same on every run
|
|
865
|
+
|
|
866
|
+
No code, flag, path or payload key was added, changed or retired; lists built from relationships
|
|
867
|
+
come back in one stable order.
|
|
868
|
+
|
|
869
|
+
- LikeC4 indexes relationships by a hash of the document's path, and loam stages documents under a
|
|
870
|
+
fresh random folder per run, so an edge whose hash happened to come out all digits (about one in
|
|
871
|
+
2,200) jumped to the front of the list on that run only; inside a project of several documents the
|
|
872
|
+
base order was whichever file finished reading first. A scheduled CI run failed on it. Every loader
|
|
873
|
+
now returns relationships sorted by source, target, title, `op`, `publishes`, `consumes` and tags,
|
|
874
|
+
so anything listed from edges — `loam explore`'s neighbours, the candidates a
|
|
875
|
+
`usecase.step-contested` finding names, findings emitted per edge — keeps one order between runs
|
|
876
|
+
and machines. That order is no longer the declaration order.
|
|
877
|
+
|
|
878
|
+
### Fixed — the repository no longer claims a version is published
|
|
879
|
+
|
|
880
|
+
No code, flag, path or payload key was added, changed or retired; only documentation moved.
|
|
881
|
+
|
|
882
|
+
- README called the version in `package.json` "the newest published version", by construction of
|
|
883
|
+
the test that pinned it — and kept saying so for `0.2.0-alpha.8`, whose publish failed. It now
|
|
884
|
+
names that version as the version of the checkout and points at the npm dist-tags as the source of
|
|
885
|
+
truth; the drift test forbids the publication claim in README and in the CHANGELOG section of the
|
|
886
|
+
checkout's own version. The `0.2.0-alpha.8` section below carries a dated note that it was never
|
|
887
|
+
published.
|
|
888
|
+
- The `0.2.0-alpha.3` section promised that installing `@ybotok/loam@alpha` gets it. That release was tagged
|
|
889
|
+
and withdrawn before its publish step, which only the `0.2.0-alpha.4` section said. It now carries
|
|
890
|
+
the same kind of dated note.
|
|
891
|
+
- README said Codex reads `.codex/skills/`. That is the folder the `codex` target writes, and Codex
|
|
892
|
+
still loads it, but Codex's skills documentation names only `.agents/skills`. README now says so
|
|
893
|
+
and points at the `agents` target for the documented root. The `codex` target's path is
|
|
894
|
+
unchanged.
|
|
895
|
+
- README and WORKFLOW no longer quote byte sizes and code counts that had gone stale (the
|
|
896
|
+
`loam-check` page, the AGENTS.md orientation, the finding-code total). A finding- or refusal-code
|
|
897
|
+
count quoted on a shipped page is now graded against `loam explain --codes`.
|
|
898
|
+
|
|
899
|
+
### Changed — `loam archive` moves the delta's bytes and nothing else's
|
|
900
|
+
|
|
901
|
+
- **The requirements merge splices instead of re-serializing.** A living requirement the delta does
|
|
902
|
+
not touch is written back byte for byte; only ADDED requirements and the ones a MODIFIED replaces
|
|
903
|
+
are serialized. Before, the whole `## Requirements` run was re-serialized, so a living spec
|
|
904
|
+
written without a blank line under `### Requirement:` had one inserted under every untouched
|
|
905
|
+
requirement by its first archive — the diff, `loam vouch --pack` and the re-vouch all widened to
|
|
906
|
+
the whole section. `loam unarchive` is unchanged and still restores every byte. Each kept slice
|
|
907
|
+
is matched to its requirement by `Requirement-ID:` (the heading when there is none), never by
|
|
908
|
+
position; if the slices and the living requirements do not match one to one, the whole run is
|
|
909
|
+
re-serialized as before rather than one requirement's text landing in another's place.
|
|
910
|
+
- **`archive --json` gains the additive `revouchOwed` key** (`[{service, path, owner}]`, on the
|
|
911
|
+
dry run and the real merge alike): every vouched living `spec.md` / `arch.spec.md` the merge
|
|
912
|
+
moves under its vouch stamp, with the frontmatter's `owner` (`null` when none is recorded).
|
|
913
|
+
`content.stale` follows on each — unchanged and still a warning; no new code, nothing `--strict`
|
|
914
|
+
lets through. The human view prints the same list under a `re-vouch owed` line, naming
|
|
915
|
+
`loam vouch --service <id>` and saying the re-vouches belong in the archive's own PR, which
|
|
916
|
+
`loam validate --all --base <ref> --strict` keeps red until they land. The `/loam-ship` protocol
|
|
917
|
+
gains that step, and README's docs-repo PR recipe says so.
|
|
918
|
+
- The human view of `loam archive` no longer prints its `archive <id>` header twice.
|
|
919
|
+
|
|
920
|
+
### Fixed — the adopt brief's spec examples
|
|
921
|
+
|
|
922
|
+
- The `spec.md` and `arch.spec.md` examples in `loam adopt`'s brief name the service being adopted
|
|
923
|
+
in their frontmatter `service:` and title, where they used to say `payment-service` for every
|
|
924
|
+
service — a verbatim paste failed `frontmatter.field-mismatch`. Their requirements now carry the
|
|
925
|
+
canonical blank line under `### Requirement:`, so a spec copied from them is not reflowed by its
|
|
926
|
+
first archive.
|
|
927
|
+
|
|
928
|
+
### Fixed — fleet readouts tell the truth about trust
|
|
929
|
+
|
|
930
|
+
A vouched spec edited after the vouch (`content.stale`, computable from the docs repo) was reported
|
|
931
|
+
by `loam validate` and by no fleet readout; `sources.unwalked` fired in every service repo straight
|
|
932
|
+
after a fresh vouch; and the human `status`/`list` views printed a verification count without
|
|
933
|
+
saying who answered it. Additive throughout — no code, flag or key changed or was removed, and the
|
|
934
|
+
maturity rung of every service is unchanged.
|
|
935
|
+
|
|
936
|
+
- **New stable code `next.revouch`** in `loam status`'s fleet `next[]` (and under `--service`): one
|
|
937
|
+
step per vouched service whose `spec.md` or `arch.spec.md` changed since the vouch, naming the
|
|
938
|
+
documents. Its `command` is `loam vouch --service <id>` and its `execution` is
|
|
939
|
+
`kind: "human-review"`, `runnable: false`, `cwd: "service-repo"` — a re-vouch is a person's act.
|
|
940
|
+
While one stands, `status` no longer reports `next.fleet-clean`.
|
|
941
|
+
- **New payload key `services.contentStale`** on `loam status --json` (fleet form): of `vouched`, the
|
|
942
|
+
services with a document moved under its stamp — a subset, like `sampledVouched`. The human line
|
|
943
|
+
reads `N vouched (M stale)`.
|
|
944
|
+
- **New payload key `scorecard.provenance.contentStale`** on `loam validate --all --json`: services
|
|
945
|
+
with a `content.stale` finding in that run. The text row reads `N vouched — M stale content ·
|
|
946
|
+
K stale digest(s) · …` (it used to say `0 stale digest(s)` beside the warning).
|
|
947
|
+
- **New payload key `answered` inside `verification`** on `loam status --json` (the feature form, and each
|
|
948
|
+
`features[].verification` of the fleet form) and on each `features[]` row of `loam gate --json`,
|
|
949
|
+
which spreads the same record: the confirmed claims per `answered_by`
|
|
950
|
+
(`runner`, `external-runner`, `agent`), the split `list --json` already carried. The human views
|
|
951
|
+
print it beside the verdict — `status <FEAT>` as `verified 19/19 confirmed (13 runner · 6 agent)`,
|
|
952
|
+
`list`'s verification column as `7/7 (5 runner · 2 agent)`. The verdict and its definition are
|
|
953
|
+
unchanged.
|
|
954
|
+
- **`loam list`** shows such a service as `vouched (stale)` (or `vouched (sampled, stale)`) and counts
|
|
955
|
+
it in the maturity line; its `missing` — in text and in `--json`, and likewise in `explore` and the
|
|
956
|
+
context pack, which share it — now reads `re-vouch (<file> changed since the vouch)`, so
|
|
957
|
+
`list --needs-work` (with `--review-order` and `--owners`) lists it instead of answering
|
|
958
|
+
"nothing to do". The `maturity` value stays `vouched`.
|
|
959
|
+
- **`sources.unwalked`** no longer counts loam's own files — `loam.json` and the agent files recorded
|
|
960
|
+
in its `agentFiles` — or any `.gitignore` toward its denominator. `.gitignore` is the file that
|
|
961
|
+
defines what git tracks, so it is the measuring instrument, never a part of the service a spec
|
|
962
|
+
owes; `package.json`, READMEs and the like stay in. A freshly vouched service repo is no longer
|
|
963
|
+
red under `validate --service --strict` for files it cannot honestly list in `sources`.
|
|
964
|
+
- **`loam list` legend** names the `e` (events / AsyncAPI) column: `[e]vents`.
|
|
965
|
+
- **SCHEMA.md**: the per-service CI summary shape gains `contentStale` (the count of `content.stale`
|
|
966
|
+
findings), and its jq derivation counts it. **WORKFLOW.md** states that `loam gate --strict` also
|
|
967
|
+
fails on a join partner's `content.stale`.
|
|
968
|
+
|
|
969
|
+
### Added
|
|
970
|
+
|
|
971
|
+
- **New warning `frontmatter.owner-mismatch`** from `loam validate` (`--service` and `--all`) when
|
|
972
|
+
one service's `spec.md` and `arch.spec.md` both name an `owner` and the two differ. `loam show`
|
|
973
|
+
and `loam context` print only `spec.md`'s owner, so the other value was a second answer nobody
|
|
974
|
+
saw. Its `details` list each file's value (`spec.md: <owner>`, `arch.spec.md: <owner>`). Silent
|
|
975
|
+
when either file names no owner (that stays `frontmatter.field-missing`). It compares the two
|
|
976
|
+
files only, never CODEOWNERS. As a warning it fails `loam validate --strict`, so a service that
|
|
977
|
+
passed `--strict` with two different owners now exits 1 there until they agree.
|
|
978
|
+
- **`openapi.generated-stale` now says what moved.** The finding gains `details` naming each
|
|
979
|
+
operationId that differs between the build's contract and the committed copy — `served, not in
|
|
980
|
+
the committed copy: <id>` and `committed, no longer served: <id>` — and its message counts both
|
|
981
|
+
sides, or says every operationId matches when the difference is inside an operation or outside
|
|
982
|
+
`paths`. The digest still decides whether it fires; same code, same severity.
|
|
983
|
+
- **`diff.op-modified` (warn)**: an operationId present at the base ref and now, whose operation
|
|
984
|
+
changed — its own definition, its method or path, or anything its internal `$ref`s reach, so a
|
|
985
|
+
field renamed in a shared `components` schema counts. **`diff.message-modified` (warn)** is the
|
|
986
|
+
same for an AsyncAPI message: its declaration or anything its payload `$ref`s reach. Both name
|
|
987
|
+
the authored consumers in `details[]` (the same scan `diff.op-deprecated` runs) and point at
|
|
988
|
+
oasdiff or `asyncapi diff`; neither grades whether the change is breaking, so neither sets
|
|
989
|
+
`breaking` or changes the exit code. A PR that renamed a required field used to print "no
|
|
990
|
+
fleet-meaningful changes" and exit 0 with nothing else.
|
|
991
|
+
- **The `modified` count in `loam diff --json`'s `summary` now counts both new codes** beside
|
|
992
|
+
`diff.requirement-modified`. The key is unchanged; a diff over a changed contract reports a
|
|
993
|
+
higher number than before.
|
|
994
|
+
- A newly deprecated operation is still reported once, as `diff.op-deprecated`, not also as
|
|
995
|
+
`diff.op-modified`.
|
|
996
|
+
|
|
997
|
+
### Changed
|
|
998
|
+
|
|
999
|
+
- **An AsyncAPI 1.x or 2.x document is `asyncapi.invalid`, once.** loam reads AsyncAPI 3.0 only,
|
|
1000
|
+
and a document whose `asyncapi:` field says `1.` or `2.` used to read as a 3.0 contract with no
|
|
1001
|
+
messages: one `spine.message-undefined` per landscape edge, one `spec-event.message-undefined` per
|
|
1002
|
+
requirement and a `spine.message-unproduced` on every consumer, none naming the version. It now
|
|
1003
|
+
takes the unreadable path every broken contract takes — one `asyncapi.invalid` whose `details`
|
|
1004
|
+
name the version and `asyncapi convert --target-version=3.0.0`, and nothing on the event axis
|
|
1005
|
+
graded against it. The same applies to a feature's own `asyncapi.yaml` under `--feature`. The
|
|
1006
|
+
`asyncapi.invalid` message, and the `context` / `delta` lines for an unreadable event contract,
|
|
1007
|
+
now say "does not read as AsyncAPI 3.0" rather than "does not parse". **It is an error:
|
|
1008
|
+
`loam validate` now exits 1 on such a document even where no edge or requirement names its
|
|
1009
|
+
messages** — a document that used to pass. `asyncapi convert --target-version=3.0.0` upgrades it.
|
|
1010
|
+
- **`loam diff --base <ref>` exits 1 while a CHANGED `asyncapi.yaml` says 1.x or 2.x.** The
|
|
1011
|
+
document is listed in that service's `unreadable[]` once for each side on which it says 1.x or
|
|
1012
|
+
2.x, counted in the `unreadable` total of `summary`, and the service reads
|
|
1013
|
+
`change: "changed"`; it used to read as a contract with no messages and exit 0. One the branch
|
|
1014
|
+
did not touch does not do this — see the next entry. `asyncapi convert --target-version=3.0.0`
|
|
1015
|
+
upgrades it.
|
|
1016
|
+
- **`loam diff --base <ref>` skips an unreadable `openapi.yaml` or `asyncapi.yaml` that the base
|
|
1017
|
+
ref holds byte for byte.** Such a contract is no longer listed in `unreadable[]`, counted in
|
|
1018
|
+
`summary`, or the reason a service reads `change: "changed"`, so an unconverted
|
|
1019
|
+
AsyncAPI 2.x file, or a broken OpenAPI file, that the branch never touched no longer fails every
|
|
1020
|
+
PR's diff with exit 1. A contract the branch changed, or one only one side cannot read, still
|
|
1021
|
+
suspends its axis and exits 1. `spec.md` and `arch.spec.md` are not skipped: an unreadable one
|
|
1022
|
+
stays suspended even when untouched, because another service's removal may be the one that
|
|
1023
|
+
could not be checked against it. No key is added or removed.
|
|
1024
|
+
- **Suggested element ids follow `loam seed`'s convention.** For a service the fleet map does not
|
|
1025
|
+
draw yet, the `loam adopt` brief and the `loam new` delta scaffold now suggest `svc_<id>` (dots
|
|
1026
|
+
and dashes folded to `_`, e.g. `svc_payment_service`) instead of a camel-cased name, and
|
|
1027
|
+
`loam new --touches <id>` reuses the element id the map already draws the service as. A service
|
|
1028
|
+
the map nests inside another element is quoted by its last segment with a note naming the parent,
|
|
1029
|
+
since a dotted id is no top-level declaration. The commented example in a freshly seeded
|
|
1030
|
+
`architecture/landscape.likec4` uses the seeded ids too. Element ids stay cosmetic —
|
|
1031
|
+
`metadata { service '<id>' }` is the join — so nothing already written needs to change.
|
|
1032
|
+
- **`seed-landscape-edited` gives advice that can be followed.** The refusal and `loam explain
|
|
1033
|
+
seed-landscape-edited` used to say "fold the edits into fleet.yaml and delete the file", which is
|
|
1034
|
+
impossible once an edge carries `metadata { op }` (fleet.yaml cannot). They now say seed runs
|
|
1035
|
+
once, before any operation is drawn, and that the map grows from then on through
|
|
1036
|
+
`loam adopt --service <id>` and feature C4 deltas. Same code, same exit code.
|
|
1037
|
+
- **`migrate-openspec` and `audit-openspec` read the OpenSpec 1.13 requirement grammar.** A
|
|
1038
|
+
`RENAMED` pair written with a `*` or `+` bullet, or with no bullet, is a pair (it was
|
|
1039
|
+
`openspec.renamed-malformed`); a `REMOVED` section written as a bullet list (`-`, `*` or `+`) of
|
|
1040
|
+
requirement headings is read as those removals (it was `openspec.change-empty`); and a closing
|
|
1041
|
+
`#` run on a `### Requirement:` or `#### Scenario:` heading is dropped at ingest, so
|
|
1042
|
+
`### Requirement: Invoice totals ###` stages as `Invoice totals` and a MODIFIED delta naming it
|
|
1043
|
+
no longer fails `loam validate` as `delta.modified-unknown`. loam's own parser is unchanged.
|
|
1044
|
+
- **An inbound landscape edge into the provider with no `metadata { op }`** (and no
|
|
1045
|
+
`publishes`/`consumes`) is now listed in the `details[]` of `diff.op-removed`,
|
|
1046
|
+
`diff.op-removed-consumed`, `diff.op-deprecated` and `diff.op-modified` as a consumer whose
|
|
1047
|
+
operation is unknown. A removal with such a caller and no named one now says "could NOT be fully
|
|
1048
|
+
answered" instead of "no current landscape edge or living requirement names it". It stays a
|
|
1049
|
+
warning, exit 0: a map `loam seed` drew, every edge without an op, does not fail every removal.
|
|
1050
|
+
- **Without `--json`, a commander usage error** (a missing `--base` on `loam diff`, an unknown flag
|
|
1051
|
+
or command, a missing argument) now prints a fix line naming `loam <command> --help` and the code
|
|
1052
|
+
line `invalid-option · loam explain invalid-option` under commander's own message, like every
|
|
1053
|
+
other refusal. stdout stays empty, exit 1; the `--json` envelope is unchanged.
|
|
1054
|
+
- README's `.github/workflows/fleet-edges.yml` **pins loam to an exact version** (`LOAM`) and says
|
|
1055
|
+
how to move the pin, **adds an oasdiff step** per changed `services/*openapi.yaml`, and no longer
|
|
1056
|
+
labels the `diff` step "did you break a consumer?". `test/docs-drift.test.ts` keeps the pin equal
|
|
1057
|
+
to the version README names.
|
|
1058
|
+
|
|
1059
|
+
### Contributors — release preflight and test harness
|
|
1060
|
+
|
|
1061
|
+
- `npm run release:check` checks that every dependency a `package-lock.json` entry names resolves
|
|
1062
|
+
to another entry, the way Node looks it up, and then runs `npm ci --dry-run --offline` on a
|
|
1063
|
+
scratch copy of `package.json` and `package-lock.json` (new `scripts/lockfile-check.mjs`): the
|
|
1064
|
+
check that would have refused `0.2.0-alpha.8`'s stripped lockfile before the tag instead of after
|
|
1065
|
+
it. The structural walk comes first because npm's own verdict depends on the npm asked: npm 11
|
|
1066
|
+
refuses that lockfile, npm 10 (Node 22.22.3) installs it.
|
|
1067
|
+
- The test harness creates its temporary directories under the canonical (symlink-resolved) temp
|
|
1068
|
+
root, so the suite passes on macOS without a `TMPDIR` override.
|
|
1069
|
+
|
|
1070
|
+
### Changed — a federated verification record is one file per service (a file-format event)
|
|
1071
|
+
|
|
1072
|
+
- **New path written: `features/<FEAT>/verification/<svc>.yaml`.** `loam verify <FEAT> --service
|
|
1073
|
+
<svc> --record` (and `--results` / `--contract-results` under `--service`, and the bound form that
|
|
1074
|
+
substitutes `--service` from loam.json) now writes that service's attestation to a file of its
|
|
1075
|
+
own instead of into one shared `verification.yaml`. Each file is a `schema: 2` record holding
|
|
1076
|
+
exactly one attestation and that service's answers; the directory is the record, and the verdict
|
|
1077
|
+
and counts are computed from the union of its files when anything reads it. What a repo notices:
|
|
1078
|
+
two teams recording two different services on two docs-repo branches no longer conflict when
|
|
1079
|
+
their PRs merge — the shared file conflicted every time (add/add from an empty start, content
|
|
1080
|
+
conflicts on `summary:`, `claims:` and `attestations:` once one attestation had landed).
|
|
1081
|
+
- **Old path still read, forever: `features/<FEAT>/verification.yaml`**, active and archived, in
|
|
1082
|
+
every shape loam has written. A one-file federated record reads exactly as before until a service
|
|
1083
|
+
records again; that service's new file then supersedes its share of the old one (the old file is
|
|
1084
|
+
not rewritten), and the write that leaves nothing in it counting removes it. An all-at-once
|
|
1085
|
+
(schema-1) record is removed by the first `--service` write, which lists its answers in
|
|
1086
|
+
`discarded` — the same answers the old overwrite listed.
|
|
1087
|
+
- **The all-at-once form is unchanged on disk.** `loam verify <FEAT> --record` without `--service`
|
|
1088
|
+
still writes the one file `verification.yaml` — it answers the whole checklist from one place, so
|
|
1089
|
+
there is no second writer to collide with and no service to name a file after — and is still
|
|
1090
|
+
refused `record-federated` over any record carrying attestations, now including per-service files.
|
|
1091
|
+
- **When services answered different versions of the checklist**, a record kept as per-service files
|
|
1092
|
+
counts the answers of the files that answered the version the feature asks NOW — `loam verify`
|
|
1093
|
+
(read and `--record`), `loam tasks`, and `status`, `list` and `gate` for an in-flight feature all
|
|
1094
|
+
derive it — and falls back to the newest file (by `recorded` date, a later service id breaking a
|
|
1095
|
+
same-day tie) only for an archived feature's frozen view or when no file answered the current
|
|
1096
|
+
version. The others stay in `attestations` and are named by `verify.checklist-forked` until they
|
|
1097
|
+
record again — now on the recording run's own output (`notices` under `--json`) as well as the
|
|
1098
|
+
read view, so the service whose answers a tally leaves out is named where the tally is printed. A
|
|
1099
|
+
same-day record from a service with a later id against an older checklist no longer drops the
|
|
1100
|
+
current answers and makes every reader call the record stale. A one-file record keeps its old
|
|
1101
|
+
reading. The `verify.checklist-forked` message no longer suggests accepting the split.
|
|
1102
|
+
- **`record-unreadable` names the file that failed and the repair that is safe for it.** Git conflict
|
|
1103
|
+
markers in any record file are detected before the YAML is parsed and named with their lines. A
|
|
1104
|
+
service's own `verification/<svc>.yaml` is that service's word alone, so its advice is to repair
|
|
1105
|
+
it or delete it and record that service again; the one-file `verification.yaml` may hold other
|
|
1106
|
+
services' attestations, so its advice is to repair it or restore it from version control (after a
|
|
1107
|
+
merge, the copy on the branch you merged into) — never to delete it. The old advice ("delete it
|
|
1108
|
+
and record again") erased other teams' attestations when followed on a shared record, and a
|
|
1109
|
+
`verify.record-miscounted` record's "record the answers again" looped straight back into the same
|
|
1110
|
+
refusal. The same per-file advice reaches `evidence.record-unreadable`, and the `status` and
|
|
1111
|
+
`gate` sentences for an unreadable record no longer say "delete it". A per-service file holding
|
|
1112
|
+
anything but its own service's attestation is unreadable too.
|
|
1113
|
+
- **`path` in a recording `verify --json` payload** is the file this run wrote — the service's own
|
|
1114
|
+
`verification/<svc>.yaml` under `--service`. The `path` inside `recorded` on the read and frozen views, and the
|
|
1115
|
+
`verification` artifact's `path` in `status`, name the `verification/` directory once it exists,
|
|
1116
|
+
the one file otherwise. `archive` and `unarchive` carry the directory with the feature they move.
|
|
1117
|
+
No code or flag was added, changed or retired, and no payload key was added or removed. What
|
|
1118
|
+
changes is the VALUE of four `path` keys, and each may now name a DIRECTORY: `path` on a recording
|
|
1119
|
+
`verify --json`, the `path` inside `recorded` on the read and frozen views, the `verification` artifact's
|
|
1120
|
+
`path` in `status --json`, and the `path` on a `next.verify` step in `status --json` (which follows
|
|
1121
|
+
the artifact's). A script that opens one of them as a file must now expect
|
|
1122
|
+
`features/<FEAT>/verification` — a directory — once any service has recorded under `--service`.
|
|
1123
|
+
- **A loam older than this release does not read `verification/<svc>.yaml`.** It sees only
|
|
1124
|
+
`verification.yaml`, so `status`, `gate` and `verify` under an older pin report a feature recorded
|
|
1125
|
+
this way as having no record (`gate --strict` then exits 1 on `gate.feature-unverified`), and an
|
|
1126
|
+
older all-at-once `--record` writes a `verification.yaml` that this release ignores beside the
|
|
1127
|
+
directory (its answers carry no attestation). Upgrade every repository that records or gates a
|
|
1128
|
+
feature together — the docs repo's pipelines and each service repo's pin.
|
|
1129
|
+
- **`features/<FEAT>/verification/` is loam's from this release.** Every non-dot `*.yaml` in it is
|
|
1130
|
+
read as one service's attestation file, and one that is not (a hand-made `k6.yaml`, say) is
|
|
1131
|
+
`record-unreadable`: `verify` refuses and `status` shows the record as unreadable. Keep anything
|
|
1132
|
+
else beside the feature, not inside that directory.
|
|
1133
|
+
|
|
1134
|
+
### Changed — `verified` means a run of the stamped scenario, and the runner-neutral path is findable
|
|
1135
|
+
|
|
1136
|
+
- **A cucumber run must have run the stamped scenario's step count.** `verify --results` used to
|
|
1137
|
+
confirm a `scenario.tested` claim from any report element carrying its `@loam-digest-…` tag whose
|
|
1138
|
+
steps passed — a one-step stub tagged by hand confirmed five-step scenarios. A matched cucumber
|
|
1139
|
+
element must now have run exactly as many steps as `loam gherkin` emits for the scenario (`And` /
|
|
1140
|
+
`But` included); otherwise the claim is `unconfirmed`, noted `ran N step(s) where the stamped
|
|
1141
|
+
scenario has M`. The step text is not compared (outlines fill their placeholders in, dialects
|
|
1142
|
+
differ on keyword spacing). cucumber-js's hook entries inside `steps[]` (`hidden`, keyword
|
|
1143
|
+
`Before`/`After`) are read as hooks, not steps: they no longer count toward the steps, and a
|
|
1144
|
+
failing one is reported `failed in a before/after hook`. `loamScenarioReport` entries carry no
|
|
1145
|
+
steps and are held to their status, as before. What a repo notices: a report whose elements do
|
|
1146
|
+
not run every stamped step stops confirming.
|
|
1147
|
+
- **A `--results` file that is not JSON is refused naming both accepted shapes.** A JUnit XML report
|
|
1148
|
+
died with the parser's raw "Unexpected token '<'". It is still `answers-unreadable`, and the
|
|
1149
|
+
message now says the file is XML or not JSON and names what `--results` reads: cucumber JSON
|
|
1150
|
+
(cucumber-js, cucumber-jvm, behave, SpecFlow/Reqnroll, pytest-bdd `--cucumberjson`, godog) or
|
|
1151
|
+
loam's runner-neutral `{"loamScenarioReport": 1, …}`. A `--contract-results` file that is not JSON
|
|
1152
|
+
names its one shape the same way.
|
|
1153
|
+
- **`loamScenarioReport` is named wherever cucumber is.** The `--results` description in
|
|
1154
|
+
`loam verify --help` now reads "cucumber JSON, or loam's runner-neutral loamScenarioReport JSON for
|
|
1155
|
+
any other runner"; the `check` in the `done` block `loam tasks` prints for a `scenario.tested` unit names both
|
|
1156
|
+
report shapes; the `verify.scenario-attested` message's suggested command reads
|
|
1157
|
+
`--results <report.json>` and names both; WORKFLOW, the verify, implement and done-check protocols
|
|
1158
|
+
and the cycle `loam explain` teaches say the same. So does the `next.verify-attested` step in
|
|
1159
|
+
`status --json` — its `statement` names both shapes and its `command` now reads
|
|
1160
|
+
`loam verify <FEAT> --results <report.json>` (it was `--results <cucumber-report.json>`) — and
|
|
1161
|
+
`loam explain next.verify-attested`.
|
|
1162
|
+
- **The all-at-once `--record` checks each evidence entry shaped like a `file:line`.** Without
|
|
1163
|
+
`--service` it accepted any non-empty evidence — `src/foo.py:1` for a file that does not exist —
|
|
1164
|
+
and printed `verified`, while the federated form refused the same citation. An evidence entry of
|
|
1165
|
+
an agent-confirmed answer that is shaped like a `file:line` (one token ending in `:<digits>`, not
|
|
1166
|
+
a URL) must now be a canonical relative path to a regular file inside the repository loam.json was
|
|
1167
|
+
found in, with that line in it; otherwise `answers-unevidenced`, exit 1, where it used to be
|
|
1168
|
+
recorded. Every other entry — prose ("manual QA by Alice"), a URL, a path with no line — is
|
|
1169
|
+
recorded exactly as before. The form still binds to no commit and stamps no pins. Evidence that
|
|
1170
|
+
lives in another service's repository is recorded from there, with `--service`. A missing cited
|
|
1171
|
+
file is now named in the repository's own spelling ("does not exist in this repository") in both
|
|
1172
|
+
forms, never through the machine's absolute path.
|
|
1173
|
+
- No code, flag, path or payload key was added, changed or retired.
|
|
1174
|
+
|
|
1175
|
+
### Added — `loam gate --since <ref>`: the deploy gate no longer forgets a feature at archive
|
|
1176
|
+
|
|
1177
|
+
- **`loam gate` used to grade in-flight features only**, so a feature left the verification check
|
|
1178
|
+
the moment it was archived — and `loam archive` reads no verification record, by design. On the
|
|
1179
|
+
example fleet `gate --service order-service --strict` went from exit 1 (FEAT-112 unverified) to
|
|
1180
|
+
exit 0 after `loam archive FEAT-112`; a contract-only feature escaped even a vouched service,
|
|
1181
|
+
because `content_digest` covers the spec bodies and not `openapi.yaml`/`asyncapi.yaml`. Without
|
|
1182
|
+
the new flag that is still exactly what the gate does, and `--help`, WORKFLOW.md and
|
|
1183
|
+
`loam explain gate.feature-unverified` now say so.
|
|
1184
|
+
- **New flag `--since <ref>`** on `loam gate`: a docs-repo git ref — the docs commit the pipeline's
|
|
1185
|
+
last deploy recorded. Every feature archived after it that carries a `specs/<service>/` delta is
|
|
1186
|
+
graded too, by its frozen verification record, under the existing warn-only
|
|
1187
|
+
`gate.feature-unverified` (or the reused `verify.scenario-attested`). "Archived after" means its
|
|
1188
|
+
`features/archive/<FEAT>/` directory is absent at the ref, or differs from what it held there —
|
|
1189
|
+
committed since or not — so a feature unarchived, edited and archived again is graded as the new
|
|
1190
|
+
ship it is, not skipped as one an earlier deploy answered for. When git will not say what changed
|
|
1191
|
+
under `features/archive/`, every archived feature touching the service is graded. Its message
|
|
1192
|
+
names the ref and points at `loam unarchive <FEAT>` rather than a `loam verify --record` an
|
|
1193
|
+
archived feature refuses. The ref is read from the docs repo's own git exactly as
|
|
1194
|
+
`loam diff --base` reads its base, and refused the same way: `repository-unavailable` when git
|
|
1195
|
+
cannot answer from the docs repo (or cannot list `features/archive/` at the ref), `unknown-target`
|
|
1196
|
+
when the ref names no commit. No new code; no deployment state is stored.
|
|
1197
|
+
- **An archived record is held to the checklist its feature shipped with.** A record gone stale
|
|
1198
|
+
before the archive (a scenario appended after it was written) read `stale`/`unverified` in flight
|
|
1199
|
+
and `verified` with no finding once archived, because an archived record is never judged against
|
|
1200
|
+
a checklist re-derived after the merge. `loam archive` now writes the digest of the checklist a
|
|
1201
|
+
fresh `loam verify` derives just before the merge — when the feature carries a readable record —
|
|
1202
|
+
into its snapshot manifest as a new key, **`checklist`**, in
|
|
1203
|
+
`features/archive/<FEAT>/.loam-before/manifest.json` (additive: the manifest stays version 3,
|
|
1204
|
+
`unarchive` ignores the key, and an older loam reading the manifest is unaffected). `gate --since`
|
|
1205
|
+
grades the frozen record against it: a record answering an older checklist is `state: "stale"`,
|
|
1206
|
+
`verdict: "unverified"`, with a `gate.feature-unverified` saying the feature moved after its
|
|
1207
|
+
record was written. A manifest without the key — an archive made by an older loam — is graded as
|
|
1208
|
+
before, as frozen history, and the row says so. `list`, `status` and `verify` keep reading an
|
|
1209
|
+
archived record as frozen history, never stale.
|
|
1210
|
+
- **New `--json` payload keys:** top-level `since` — `{ ref, commit }` under `--since`, `null`
|
|
1211
|
+
without it — and, only on a row `--since` added, `features[].archived` (`true`) and
|
|
1212
|
+
`features[].checklistAtArchive` (the captured digest, or `null` when the archive captured none).
|
|
1213
|
+
An in-flight row carries neither key, so a payload without the flag is unchanged apart from
|
|
1214
|
+
`since: null`. The text view titles the block "verification — in flight, and archived since
|
|
1215
|
+
<ref>", marks those rows `· archived`, and adds a line under a recorded row whose archive
|
|
1216
|
+
captured no checklist.
|
|
1217
|
+
- **The MCP `loam_gate` tool** gains the matching `since` string argument.
|
|
1218
|
+
|
|
1219
|
+
## [0.2.0-alpha.8] - 2026-09-21
|
|
1220
|
+
|
|
1221
|
+
The work packet: the verify checklist read forwards. Still a prerelease on the `alpha` line.
|
|
1222
|
+
**Not published (noted 2026-10-10):** the `v0.2.0-alpha.8` tag was cut, but its release run failed at
|
|
1223
|
+
`npm ci` — npm on Windows had stripped other platforms' optional packages from the tagged commit's
|
|
1224
|
+
`package-lock.json` during the version bump (restored on `main` the same day) — so the publish never
|
|
1225
|
+
ran. `npm i @ybotok/loam@alpha` does not install this version, and `npm view @ybotok/loam dist-tags`
|
|
1226
|
+
is the record of what does. Everything in it is additive — no stable code was added,
|
|
1227
|
+
changed or retired, no existing flag, path or payload key moved, and nothing a repository has
|
|
1228
|
+
committed needs to change. What a repo NOTICES after upgrading: one new read-only command
|
|
1229
|
+
(`loam tasks`), new keys on two `--json` payloads it may already consume, and a corrected SCHEMA
|
|
1230
|
+
sentence about how scenario digests are salted — the code did not move, the sentence did.
|
|
1231
|
+
|
|
1232
|
+
### Added — `loam tasks`, the derived answer to "what do I do next, and what closes it"
|
|
1233
|
+
|
|
1234
|
+
The question an authored task file answers is a good one; the file is the wrong place to answer it,
|
|
1235
|
+
because a second mutable state drifts from the spec it was copied out of. So this is that answer
|
|
1236
|
+
DERIVED — the `loam verify` checklist read forwards, one work unit per promise the feature makes,
|
|
1237
|
+
each carrying the step it is waiting on and the fact that retires it, recomputed on every run from
|
|
1238
|
+
the delta, the generated suite and the verification record. SCHEMA's "Deliberate limits of the
|
|
1239
|
+
workflow surface" row refusing an authored `tasks.md` as authoritative state **stands unchanged**;
|
|
1240
|
+
this is the projection that row has always pointed at, and it is a projection precisely so that
|
|
1241
|
+
nothing here can disagree with the documents it was derived from.
|
|
1242
|
+
|
|
1243
|
+
- **New read-only command `loam tasks <FEAT> [--service <id>] [--next] [--json]`.** It writes
|
|
1244
|
+
nothing, anywhere, ever, and creates no artifact. Every unit it reports is one claim of the same
|
|
1245
|
+
checklist `loam verify` derives, **under the same id** — a unit and the answer that closes it are
|
|
1246
|
+
one identity on both commands. The one unit with no claim behind it is minted for an ADDED or
|
|
1247
|
+
MODIFIED requirement carrying no scenario at all (kind requirement.described): nothing to test
|
|
1248
|
+
means nothing for the checklist to ask, and a promise with no acceptance criteria is still work.
|
|
1249
|
+
- **`units[]` is the projection**, one entry per unit, carrying `id`, `kind`, `subject`, `at`,
|
|
1250
|
+
`claim`, `dependsOn[]`, a `done` block, and — where the ledger has them — `digest`, `operation`,
|
|
1251
|
+
`token` and a `source` naming the document, the 1-based line and, for a scenario, its body
|
|
1252
|
+
verbatim.
|
|
1253
|
+
- **`at` is the step the unit is waiting at**, one of `describe`, `generate`, `implement` or
|
|
1254
|
+
`attested` — never a percentage. It is spelled `at` rather than `stage` or `verdict` on purpose:
|
|
1255
|
+
both of those already mean something else on payloads an agent reads beside this one.
|
|
1256
|
+
- **Done is a fact, not a tick.** A closed unit stops appearing, and exactly one thing closes one: a
|
|
1257
|
+
claim recorded `confirmed` in a `verification.yaml` that is **not stale** against the current
|
|
1258
|
+
checklist. A stale record closes nothing — the words moved under the answers. An agent-attested
|
|
1259
|
+
scenario never vanishes: it stays visible at `attested`, because attested is not verified and a
|
|
1260
|
+
work list that hid the difference would be the gate that teaches everyone to say yes.
|
|
1261
|
+
- **`frontier` is the ids of the first ten open units in derived order**, providers before consumers
|
|
1262
|
+
on the exact token join `c4.calls.token == api.exposes.operation` — never a reading of prose —
|
|
1263
|
+
with `frontierElided` saying how many open units it left off. **The order is advice and never a
|
|
1264
|
+
lock**: nothing is assigned, nothing is reserved, and working out of order is legal and invisible
|
|
1265
|
+
to every other command.
|
|
1266
|
+
- **`--next` is the leaf packet**: the same payload with `units` narrowed to the frontier head, and
|
|
1267
|
+
what an orchestrator hands one coding agent. Everything else is left whole on purpose, so a cheap
|
|
1268
|
+
model handed one unit cannot read it as the whole job. The text view prints that leaf in full —
|
|
1269
|
+
the scenario's Given/When/Then verbatim and never reflowed (those are the bytes the digest was
|
|
1270
|
+
taken over), its digest, the `done` block whose `check` is the fact that closes the unit, and the
|
|
1271
|
+
literal follow-up command. Literal rather than a shape to fill in, because "the appropriate verify
|
|
1272
|
+
invocation" is a guess the reader will get wrong.
|
|
1273
|
+
- **`--service <id>` is a pure lens** over the same derivation, and adds `waitingOn[]`: the open
|
|
1274
|
+
units in OTHER services that this service's units depend on.
|
|
1275
|
+
- **`stamps` is `read` or `unknown`.** `unknown` means the bound service repo's generated suite could
|
|
1276
|
+
not be read — could-not-look, and never nothing-wrong.
|
|
1277
|
+
- **The rest of the payload**: `feature`, `service` (the lens, or null), `services[]`, `path`,
|
|
1278
|
+
`intent`, `checklist` (the digest of the question set the units derive from), `record` (`absent`,
|
|
1279
|
+
`recorded`, `stale` or `unreadable`), `size{requirements,scenarios,operations,messages,calls}` —
|
|
1280
|
+
the size of the DELTA and never of the open work, so that "this feature is small" and "this
|
|
1281
|
+
feature is nearly done" cannot become one sentence — `contracts{expose[],call[]}`,
|
|
1282
|
+
`counts{open,done,attested}`, and `review[]`, the four instructions for re-deriving this answer
|
|
1283
|
+
and reviewing it.
|
|
1284
|
+
- **A broken `delta.likec4` or contract axis exits 1 with `ok: true`**, naming what it could not read
|
|
1285
|
+
in `unreadable` (`architecture`, `contracts[]`). That is `loam delta`'s own rule rather than a new
|
|
1286
|
+
one: a feeder that could not be read empties the packet, an agent consuming an empty packet reads
|
|
1287
|
+
"nothing to do", so the envelope stays informative and true while the exit code stops a pipeline
|
|
1288
|
+
building on it.
|
|
1289
|
+
- **New MCP read-only tool `loam_tasks`**, declaring `readOnlyHint` like every other read tool; the
|
|
1290
|
+
base table is fifteen tools where it was fourteen.
|
|
1291
|
+
- **No stable code was added, changed or retired, and no existing flag, path or payload key moved.**
|
|
1292
|
+
The change is purely additive: everything `loam tasks` reports is a projection of state other
|
|
1293
|
+
commands already derive and grade.
|
|
1294
|
+
|
|
1295
|
+
### Added — `loam delta --json` carries the joins and the lines it had been dropping
|
|
1296
|
+
|
|
1297
|
+
- **New additive keys per requirement**: `line` (where it sits in the document it was parsed from, so
|
|
1298
|
+
a consumer can send an author to the line rather than to the file), `basedOn`, `requires`,
|
|
1299
|
+
`capabilities`, `realizes`, `publishes` and `consumes`. Every one of them is already written on the
|
|
1300
|
+
requirement and every one names work — which permission gates it, which capability it belongs to,
|
|
1301
|
+
which named promise it realizes, which messages it produces and consumes — and an implementer
|
|
1302
|
+
handed this projection had to go and read the Markdown it claimed to have projected to find them.
|
|
1303
|
+
- **New additive keys per scenario**: `line`, and `digest`, the 16-hex scenario body hash. That
|
|
1304
|
+
digest is the exact join key the rest of the toolchain already uses — the `@loam-digest-<16hex>`
|
|
1305
|
+
tag `loam gherkin` stamps and the string `loam verify --results` matches a cucumber report on — so
|
|
1306
|
+
a consumer of this brief can now name the test that answers an acceptance criterion instead of
|
|
1307
|
+
regenerating the suite to find out. It is salted by the owning service and the axis, like every
|
|
1308
|
+
other spelling of it, so an arch requirement's scenarios digest differently from identically
|
|
1309
|
+
worded business ones.
|
|
1310
|
+
- **Nothing existing moved.** Every key this payload already carried is untouched, the text output is
|
|
1311
|
+
unchanged, and no stable code was added or changed.
|
|
1312
|
+
|
|
1313
|
+
### Added — `loam gherkin --json` says which stamp belongs to which scenario
|
|
1314
|
+
|
|
1315
|
+
- **New additive key `files[].stamped`**, a list of `{scenario, digest}` pairs. `digests` was a bare
|
|
1316
|
+
array and `scenarios` a bare count, so the one thing a consumer needed — which stamp belongs to
|
|
1317
|
+
which scenario — was the one thing this payload could not say, and the only way to learn it was to
|
|
1318
|
+
read the generated file back off disk. A `kept` row reads its pairs off the file that is really on
|
|
1319
|
+
disk, because its numbers describe the in-flight emission that stayed rather than the one that was
|
|
1320
|
+
withheld.
|
|
1321
|
+
- **`digests` and `scenarios` are unchanged beside it**, and no stable code was added or changed: the
|
|
1322
|
+
envelope permits adding a key and never changing one.
|
|
1323
|
+
|
|
13
1324
|
## [0.2.0-alpha.7] - 2026-09-07
|
|
14
1325
|
|
|
15
1326
|
Infrastructure belongs on the fleet map, and the render is navigable at fleet scale. Still a
|
|
@@ -1150,7 +2461,10 @@ now rewrites the bytes of every view beneath it.
|
|
|
1150
2461
|
## [0.2.0-alpha.3] - 2026-09-03
|
|
1151
2462
|
|
|
1152
2463
|
The generated subsystem views, made readable — and the defect that hunt found underneath them.
|
|
1153
|
-
Still a prerelease on the `alpha` line
|
|
2464
|
+
Still a prerelease on the `alpha` line. **Not published (noted 2026-10-10):** the `v0.2.0-alpha.3`
|
|
2465
|
+
tag was cut, but it was withdrawn before its publish step — the `0.2.0-alpha.4` section above says
|
|
2466
|
+
so, and that release carries everything here — so `npm i @ybotok/loam@alpha` never installed this
|
|
2467
|
+
version, and `npm view @ybotok/loam dist-tags` is the record of what does. Nothing below is
|
|
1154
2468
|
breaking, and one entry is a change every repo NOTICES: the generated file's bytes move, so the
|
|
1155
2469
|
first `loam validate --all` after upgrading asks for one `loam subsystem sync`.
|
|
1156
2470
|
|