@ybotok/loam 0.2.0-alpha.6 → 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 +1477 -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 +108 -11
- package/SCHEMA.md +721 -130
- 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 +82 -33
- 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 +55 -26
- package/dist/commands/seed/commit.js.map +1 -1
- package/dist/commands/seed/plan.js +64 -13
- package/dist/commands/seed/plan.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 +7 -7
- package/dist/commands/subsystem/edit.js.map +1 -1
- package/dist/commands/subsystem/move.js +60 -25
- 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 +141 -71
- package/dist/commands/subsystem/sync.js.map +1 -1
- package/dist/commands/subsystem/txn/report.js +55 -5
- package/dist/commands/subsystem/txn/report.js.map +1 -1
- package/dist/commands/subsystem/txn/txn.js +121 -45
- package/dist/commands/subsystem/txn/txn.js.map +1 -1
- package/dist/commands/subsystem/txn/views.js +82 -12
- 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/health-deps.js +9 -6
- package/dist/commands/validate/checks/health-deps.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/census.js +26 -3
- package/dist/commands/validate/fleet/census.js.map +1 -1
- package/dist/commands/validate/fleet/landscape.js +35 -32
- package/dist/commands/validate/fleet/landscape.js.map +1 -1
- package/dist/commands/validate/fleet/map/attest.js +7 -12
- package/dist/commands/validate/fleet/map/attest.js.map +1 -1
- package/dist/commands/validate/fleet/map/bindings.js +16 -5
- package/dist/commands/validate/fleet/map/bindings.js.map +1 -1
- package/dist/commands/validate/fleet/map/consumers.js +43 -122
- package/dist/commands/validate/fleet/map/consumers.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 +1 -1
- package/dist/commands/validate/fleet/views/fleet-project.js.map +1 -1
- package/dist/commands/validate/fleet/views/ids.js +33 -7
- package/dist/commands/validate/fleet/views/ids.js.map +1 -1
- package/dist/commands/validate/fleet/views/root-documents.js +17 -35
- package/dist/commands/validate/fleet/views/root-documents.js.map +1 -1
- package/dist/commands/validate/fleet/views/stale.js +31 -14
- package/dist/commands/validate/fleet/views/stale.js.map +1 -1
- package/dist/commands/validate/links/corpus.js +1 -4
- package/dist/commands/validate/links/corpus.js.map +1 -1
- package/dist/commands/validate/links/glossary.js.map +1 -1
- package/dist/commands/validate/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 +57 -102
- 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 +31 -17
- 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 +37 -21
- package/dist/core/agent/agents-md/refusals.js.map +1 -1
- package/dist/core/agent/agents-md.js +25 -10
- 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 +25 -22
- 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 +62 -24
- 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 +30 -25
- 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 +79 -5
- package/dist/core/brief/targets.js.map +1 -1
- package/dist/core/brief/unchecked.js +1 -1
- package/dist/core/brief/unchecked.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/architecture.js +31 -9
- package/dist/core/c4/project/architecture.js.map +1 -1
- package/dist/core/c4/project/documents.js +36 -10
- package/dist/core/c4/project/documents.js.map +1 -1
- package/dist/core/c4/project/load.js +14 -21
- package/dist/core/c4/project/load.js.map +1 -1
- package/dist/core/c4/project/staged.js +7 -3
- package/dist/core/c4/project/staged.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 +15 -16
- 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 +10 -3
- package/dist/core/gate/checks.js.map +1 -1
- package/dist/core/gate/partners.js +64 -19
- 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/paths.js +34 -29
- package/dist/core/repo/paths.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/render/body.js +45 -6
- package/dist/core/repo/tree/render/body.js.map +1 -1
- package/dist/core/repo/tree/render/service/header.js +125 -0
- package/dist/core/repo/tree/render/service/header.js.map +1 -0
- package/dist/core/repo/tree/render/service/peers.js +28 -0
- package/dist/core/repo/tree/render/service/peers.js.map +1 -0
- package/dist/core/repo/tree/render/service/view-id.js +67 -0
- package/dist/core/repo/tree/render/service/view-id.js.map +1 -0
- package/dist/core/repo/tree/render/service/views.js +78 -0
- package/dist/core/repo/tree/render/service/views.js.map +1 -0
- package/dist/core/repo/tree/render/stale.js +19 -2
- package/dist/core/repo/tree/render/stale.js.map +1 -1
- package/dist/core/repo/tree/render/views.js +102 -33
- package/dist/core/repo/tree/render/views.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 +45 -10
- 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 +53 -3
- package/dist/core/vocabulary/maturity.js.map +1 -1
- package/examples/README.md +28 -9
- package/examples/docs/architecture/landscape.likec4 +21 -7
- package/examples/docs/architecture/services.likec4 +70 -0
- package/examples/docs/architecture/subsystems.likec4 +3 -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/order-service/model.likec4 +10 -8
- package/examples/docs/services/payment-service/model.likec4 +6 -4
- package/examples/docs/services/platform/identity-service/model.likec4 +9 -7
- package/examples/docs/services/platform/notification-service/asyncapi.yaml +2 -1
- package/examples/docs/services/platform/notification-service/model.likec4 +4 -5
- 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/SCHEMA.md
CHANGED
|
@@ -62,6 +62,7 @@ docs/
|
|
|
62
62
|
architecture/
|
|
63
63
|
landscape.likec4 global C4 (system landscape)
|
|
64
64
|
subsystems.likec4 one view per subsystem, views only [GENERATED: loam subsystem sync]
|
|
65
|
+
services.likec4 one overview view per service, views only [GENERATED: loam subsystem sync]
|
|
65
66
|
permissions.yaml system-wide authorization vocabulary [authored, opt-in]
|
|
66
67
|
capabilities.yaml system-wide capability vocabulary [authored, opt-in]
|
|
67
68
|
obligations.yaml system-wide architectural obligations [authored, opt-in]
|
|
@@ -88,24 +89,25 @@ docs/
|
|
|
88
89
|
delta.likec4 C4 delta (architecture) [authored]
|
|
89
90
|
specs/<svc>/spec.md requirement delta (ADDED/MODIFIED/REMOVED + scenarios) [authored]
|
|
90
91
|
specs/<svc>/arch.spec.md architectural requirement delta, same algebra [authored]
|
|
91
|
-
specs/<svc>/openapi.yaml the endpoints
|
|
92
|
-
specs/<svc>/asyncapi.yaml the event
|
|
92
|
+
specs/<svc>/openapi.yaml only the endpoints it adds/changes/removes [authored]
|
|
93
|
+
specs/<svc>/asyncapi.yaml only the event slots it adds/changes/removes [authored]
|
|
93
94
|
capabilities/<cap>/spec.md capability delta, same requirement grammar [authored]
|
|
94
95
|
glossary/<term>.md a word this feature brings with it; create-only [authored]
|
|
95
96
|
usecases/<name>.likec4 a FLOW this feature brings; views-only, create-only [authored]
|
|
96
97
|
adrs/NNNN-*.md feature-level decisions [authored]
|
|
97
|
-
verification
|
|
98
|
+
verification/<svc>.yaml the done-check, one file per attesting service: claims + verdicts + evidence [loam verify --service <id> --record]
|
|
99
|
+
verification.yaml the same answered all at once, and every record from before the directory — read forever [loam verify --record]
|
|
98
100
|
features/archive/<FEAT>/ a shipped change — the same files, plus:
|
|
99
|
-
.loam-before/ the bytes the merge overwrote
|
|
101
|
+
.loam-before/ the bytes the merge overwrote, and the checklist it shipped with [written by loam]
|
|
100
102
|
.loam-lock transient: one writer at a time [archive/unarchive only]
|
|
101
103
|
.loam-commit transient: the commit intent journal [archive/unarchive only]
|
|
102
104
|
```
|
|
103
105
|
|
|
104
106
|
There is no `flows/` tree, and there will not be one. A cross-service **interaction flow** is a
|
|
105
107
|
`dynamic view` under `architecture/` — the one place that already holds every cross-service edge —
|
|
106
|
-
tagged with the capability it realizes
|
|
107
|
-
service's directory, and the view's steps join to the
|
|
108
|
-
makes the flow checkable rather than merely drawn.
|
|
108
|
+
tagged with the capability it realizes, or with the suites it runs in (below). A flow that spans
|
|
109
|
+
services does not belong under one service's directory, and the view's steps join to the
|
|
110
|
+
landscape's own relationships, which is what makes the flow checkable rather than merely drawn.
|
|
109
111
|
|
|
110
112
|
A flow that stays INSIDE one service — the hop sequence behind an arch.spec.md requirement whose
|
|
111
113
|
`Covers:` names containers — cannot live under `architecture/`: loam parses that directory as its
|
|
@@ -135,7 +137,7 @@ model.likec4 stays THE model:
|
|
|
135
137
|
`c4.valid`, `Covers:` and every other model check read it alone, and a sibling that does not parse
|
|
136
138
|
is one `usecase.flow-invalid` while the model keeps grading. Files loam does not read: a `.likec4`
|
|
137
139
|
in a subsystem directory, in a feature directory outside `delta.likec4`/`usecases/`/`deployment/`,
|
|
138
|
-
the generated `subsystems.likec4`, a sibling the service's own `likec4.config.json` `exclude`
|
|
140
|
+
the generated `subsystems.likec4` and `services.likec4`, a sibling the service's own `likec4.config.json` `exclude`
|
|
139
141
|
covers (it is in no project the renderer loads either), and every UNTAGGED `dynamic view`
|
|
140
142
|
anywhere — a diagram, never a use case.
|
|
141
143
|
|
|
@@ -146,6 +148,41 @@ would leave behind — so a flow may draw a hop into a service the feature's own
|
|
|
146
148
|
and a hop into an element the merge does NOT land is refused at plan time rather than archived into
|
|
147
149
|
a map the next reader's `loam validate --all` fails on.
|
|
148
150
|
|
|
151
|
+
**A fleet flow can also say which SUITE it runs with: `#suite-<name>`.** A suite is a set of
|
|
152
|
+
journeys a test run exercises together — a smoke suite, a payments suite — and a journey joins one
|
|
153
|
+
by carrying the tag on its `dynamic view`, declared like every LikeC4 tag in the landscape's one
|
|
154
|
+
`specification` block (`tag suite-smoke`), so a mistyped suite is a parse error rather than a second,
|
|
155
|
+
empty suite. It is a SEPARATE prefix, not `#cap-`: a capability says what a flow is FOR, a suite says
|
|
156
|
+
what it is RUN WITH, and membership is **many-to-many** — one view may carry several `#suite-` tags,
|
|
157
|
+
and a suite holds as many views as carry its tag, which is why it is a tag and not a directory.
|
|
158
|
+
|
|
159
|
+
**Tagging is the opt-in, and it grades.** A view carrying only `#suite-` tags — no `#cap-`, no
|
|
160
|
+
`#req-` — IS a use case: every reader takes it (the predicate and the byte gate below now read three
|
|
161
|
+
prefixes), and `loam validate --all` grades its hops like any other's (`usecase.step-unbacked`,
|
|
162
|
+
`usecase.step-contested`, `usecase.step-unlinked`). A journey a test stand is provisioned from must
|
|
163
|
+
be held to the model it is drawn over. The suite tag claims no capability and no requirement, so it
|
|
164
|
+
earns nothing from the capability axis. An UNTAGGED view stays ungraded, exactly as before.
|
|
165
|
+
|
|
166
|
+
`loam list suites [<name>] [--json]` is the answer the tag exists for: **which services must be up
|
|
167
|
+
for suite X.** For each suite it lists the views carrying the tag, the enumerated services their
|
|
168
|
+
steps resolve to — through the same element→service resolver the step grades use, so a step into
|
|
169
|
+
one of order-service's containers counts order-service — and every participant that resolves to NO
|
|
170
|
+
service, by element id, with whether it (or anything it is drawn inside) carries `#external`: the
|
|
171
|
+
customer who drives the flow, a topic, a partner's API. The walk up stops at the first element
|
|
172
|
+
bound to or titled for a service, as `loam gate`'s partner reading does, so a service drawn inside
|
|
173
|
+
an `#external` grouping is still ours. Those are named and never dropped, because an environment
|
|
174
|
+
list that is silently short comes up, and the journey then fails against something nobody was
|
|
175
|
+
told to start. The payload is `suites[]` (`name`, `views[]` of `{id, file}`, `services[]`, `unresolved[]`
|
|
176
|
+
of `{element, external}`), `ungrouped[]` — the use cases carrying no `#suite-` tag, which every
|
|
177
|
+
per-suite line would otherwise leave out — and `useCases` (`state`: `absent` when the fleet
|
|
178
|
+
declares no use case, `ok`, or `unreadable` with `error`, beside the `unreadable` boolean
|
|
179
|
+
`list capabilities` spells). Over an unreadable `architecture/` the section degrades rather than
|
|
180
|
+
refusing, and `suites` and `ungrouped` are ABSENT rather than `[]`. A `<name>` narrows `suites[]`
|
|
181
|
+
only; an unknown one refuses `unknown-target` with the close names. The listing reads the FLEET
|
|
182
|
+
map's use cases; a `#suite-` tag on a flow inside one service's own project is graded there like any
|
|
183
|
+
other use-case tag, and is not listed — that flow runs inside one service, whose environment is the
|
|
184
|
+
service itself.
|
|
185
|
+
|
|
149
186
|
### Two shapes of a service model
|
|
150
187
|
|
|
151
188
|
`services/<…>/<svc>/model.likec4` comes in two shapes, and **the file's own grammar decides which**
|
|
@@ -153,8 +190,8 @@ a map the next reader's `loam validate --all` fails on.
|
|
|
153
190
|
top-level `specification { }` block containing an `element` declaration:
|
|
154
191
|
|
|
155
192
|
- an **extending model** declares no element kind. It is parsed INSIDE the fleet's `architecture/`
|
|
156
|
-
project — the landscape plus every other `architecture/**/*.likec4` except the generated
|
|
157
|
-
`subsystems.likec4` — and says what is inside the service with `extend <fqn> { … }` on the element
|
|
193
|
+
project — the landscape plus every other `architecture/**/*.likec4` except the generated views
|
|
194
|
+
files (`subsystems.likec4`, `services.likec4`) — and says what is inside the service with `extend <fqn> { … }` on the element
|
|
158
195
|
the map already binds to the directory. `<fqn>` is the FULLY-QUALIFIED id: `extend orderService`
|
|
159
196
|
resolves to nothing when the element is `marketplace.orderService`. No `specification` is needed,
|
|
160
197
|
no partner is re-declared, and `model {}` or no model at all are both fine. `extend` accepts
|
|
@@ -181,7 +218,7 @@ exactly why its copies of the map's elements can drift.
|
|
|
181
218
|
|---|---|---|
|
|
182
219
|
| `c4.declaration-diverged` (warn, service) | standalone | an element this model and the fleet map (`architecture/`) both declare, whose `kind`, `title`, tag SET or `metadata { service }` binding differ. The two are joined by id, then by the `metadata { service }` binding both wrote, then by the map element whose id ends with the model's as a dotted tail (`paymentService` ↔ `marketplace.paymentService`) — a tail two map elements answer to joins nothing, and a tail that is a BARE last segment (`db`, `api`) is a key only for a FLEET-LEVEL map element: one no ancestor of which stands for a service, so a top-level element or a box the map draws inside a plain grouping element, never an element drawn inside a service's own. `description` is deliberately not compared — it is prose, nothing grades it, and the other four each change what a view shows or what loam resolves. When the tag sets are all that differ and copying the map's declaration would not clear them — because the difference comes off `specification { element <kind> { … } }` on one side or the other — the message says so and names the document whose specification declares them, rather than asking for a copy. One finding per divergent element; silent while the map does not parse |
|
|
183
220
|
| `c4.element-unowned` (warn, service) | extending | the model declares an element OUTSIDE the element that resolves to this service. Three remedies, first first: a store or component this service owns goes INSIDE the `extend <fqn> { }` block, where its id becomes `<fqn>.<name>`; a system the service reaches belongs in the map, declared once; another service's internals belong in that service's model |
|
|
184
|
-
| `c4.fleet-project-invalid` (warn, landscape) | extending | `--all` only, and not while `landscape.excluded` holds — this load reads what the renderer reads and so has no floor under the map, and one `exclude` entry covering the map turns every extending model's `extend` into an unresolved reference (measured on `examples/docs`: 161 warnings from one line). loam loads the fleet project the way the renderer does — a walk of the docs root, minus everything the root `exclude` covers, minus the generated `subsystems.likec4`, minus every STANDALONE service directory (`service.model-unexcluded` owns that state), minus `features/` (a delta carries its own `specification` and the directory is transient) — and reports each error NOT already present in the map alone or in any per-service project the same run made. A `.likec4` in a SUBSYSTEM directory, above every service, is in that set. This is the class no per-service grade can see: two services declaring the same tag locally, two models adding the same child id, a broken untagged sibling nobody opted in. All of them are renderer-fatal. `locations[0]` is the file the error is in and the message tail names the failure it actually is |
|
|
221
|
+
| `c4.fleet-project-invalid` (warn, landscape) | extending | `--all` only, and not while `landscape.excluded` holds — this load reads what the renderer reads and so has no floor under the map, and one `exclude` entry covering the map turns every extending model's `extend` into an unresolved reference (measured on `examples/docs`: 161 warnings from one line). loam loads the fleet project the way the renderer does — a walk of the docs root, minus everything the root `exclude` covers, minus the generated views files (`subsystems.likec4`, `services.likec4`), minus every STANDALONE service directory (`service.model-unexcluded` owns that state), minus `features/` (a delta carries its own `specification` and the directory is transient) — and reports each error NOT already present in the map alone or in any per-service project the same run made. A `.likec4` in a SUBSYSTEM directory, above every service, is in that set. This is the class no per-service grade can see: two services declaring the same tag locally, two models adding the same child id, a broken untagged sibling nobody opted in. All of them are renderer-fatal. `locations[0]` is the file the error is in and the message tail names the failure it actually is |
|
|
185
222
|
| `service.likec4-config-stray` (warn, landscape) | extending | a `likec4.config.json` beside an extending model. The renderer registers it as a project of its own rooted at that directory, so the docs root stops being a one-project workspace (`likec4 validate .` then refuses without `--project`), and wherever that nested project claims the model it parses alone, out of the root project where the map's kinds are: the model does not resolve and `export json --project fleet` loses the service's containers (measured, likec4 1.59.2 — which of the two happens depends on the workspace; on `examples/docs` the fleet export was byte-identical with and without the file, on a seeded fleet the containers went). Run `loam subsystem sync`: it deletes the file. Deleting it by hand works too. loam never writes one beside an extending model |
|
|
186
223
|
| `service.model-excluded` (warn, landscape) | extending | the root `likec4.config.json`'s `exclude` covers this model's directory, so the renderer never loads it and the service is a box with nothing inside it. An entry is read the way the renderer reads it (measured, likec4 1.59.2): a bare entry is a raw path PREFIX starting on a `/` boundary, so `services`, `services/`, `services/*`, `services/**`, `services/**/*`, a wildcard segment in the middle and the bare `services/pay` all cover `services/payment-service` — while the glob form `services/pay/**` wants the literal separator and covers nothing there. An entry shaped like a FILE rather than a directory (`services/**/model.likec4`, `**/model.likec4`) is reported too, with a different repair: `loam subsystem sync` maintains only the `services/` directory entries, so that shape has to be narrowed or deleted by hand. The finding quotes the entry verbatim, because the repair is to edit that line; `loam subsystem sync` is the repair for the directory shape |
|
|
187
224
|
| `service.model-unexcluded` (warn, landscape) | standalone | the mirror, and the one that costs most: the root `exclude` does NOT cover a standalone model's directory, so the renderer merges it into the root project and reports every kind and element it declares as a duplicate — blanking the whole project, not just this service. An entry already covering the directory in any of the spellings above satisfies it, and `sync` appends no second glob for one. Same repair |
|
|
@@ -218,24 +255,34 @@ So a hop naming `<service-fqn>.<container>` resolves in neither the preview nor
|
|
|
218
255
|
leaves, and `usecase.flow-invalid` refuses — where the un-routed preview resolved it, passed the
|
|
219
256
|
gate `--approve` cannot override, and landed a flow the next `loam validate --all` failed on.
|
|
220
257
|
|
|
221
|
-
**Private stores.**
|
|
222
|
-
the service's `extend <fqn> { store = database … }`
|
|
223
|
-
map when the map draws that service's containers
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
258
|
+
**Private stores.** **Where a store is drawn is the team's choice.** A store one service owns may be
|
|
259
|
+
nested UNDER that service's element — written in the service's `extend <fqn> { store = database … }`
|
|
260
|
+
block, or inside the service's element on the map when the map draws that service's containers — or
|
|
261
|
+
drawn beside the services as an element of its own. Both are first-class and loam grades neither as
|
|
262
|
+
wrong. Until 2026-09-07 the second earned `landscape.datastore-private` at one consumer, a warning
|
|
263
|
+
whose whole message prescribed deleting the peer while the identical store nested was silent: it
|
|
264
|
+
graded the drawing rather than the architecture, and it is retired. Infrastructure is not a service
|
|
265
|
+
either — an element whose kind is `database`, `queue`, `topic`, `cache` or `bucket` is out of the
|
|
266
|
+
service census, so an owned store drawn at fleet level is not `landscape.service-undocumented`, is
|
|
267
|
+
not a `loam gate` partner, and mints no `service.exists` verification claim. The `extend` placement
|
|
268
|
+
needs an EXTENDING model: a model that declares its own `specification` is parsed alone, so
|
|
269
|
+
`extend <fqn>` resolves nothing there, and for such a service the map is the only nesting site (or
|
|
270
|
+
migrate the model — see "Two shapes of a service model" above).
|
|
271
|
+
|
|
272
|
+
What the choice DOES decide is one thing, and it is a fact about LikeC4 rather than a preference: **a
|
|
273
|
+
second consumer's edge resolves only against a declaration the MAP holds**, because a per-service
|
|
274
|
+
project is `architecture/` plus one model. Draw a container-level edge to `<owner-fqn>.<store>` in
|
|
275
|
+
another service's model while the store lives in the owner's `extend` block and that model is
|
|
276
|
+
`c4.invalid` ("Could not resolve reference to Referenceable named '<store>'"). So a store a second
|
|
277
|
+
consumer will name has to be on the map — nested inside the owner's element there, or beside the
|
|
278
|
+
services. That was always the load-bearing half; the prescription around it was not.
|
|
279
|
+
|
|
280
|
+
Nesting closes no door in the other direction: loam counts a
|
|
229
281
|
consumer a service's EXTENDING model attests exactly as it counts one the map draws, so a nested
|
|
230
282
|
store two services reach is `landscape.datastore-shared` — the store's owner plus every service
|
|
231
283
|
whose model draws an outbound call at it or below it. The second consumer draws that call as a
|
|
232
284
|
container-level edge to `<owner-fqn>.<store>` in its OWN model, and the service-level twin on the
|
|
233
|
-
map as usual.
|
|
234
|
-
is `architecture/` plus one model, so the edge resolves only when the MAP declares the store —
|
|
235
|
-
written in the owner's `extend` block, the same edge is `c4.invalid` on the consumer ("Could not
|
|
236
|
-
resolve reference to Referenceable named '<store>'"). Nest it on the map when a second consumer is
|
|
237
|
-
foreseeable, and in the model when it is not. The choice is cheap in ONE direction, and the
|
|
238
|
-
direction is the whole of the rule. The two placements give the store the same id,
|
|
285
|
+
map as usual. The two placements give the store the same id,
|
|
239
286
|
`<owner-fqn>.<store>`, so moving the declaration OUT of the owner's `extend` block into the owner's
|
|
240
287
|
element on the map rewrites no edge and no `Covers:` line — measured on `examples/docs`, where
|
|
241
288
|
moving `db` out of order-service's `extend` block into `orderService` on the map left the model at
|
|
@@ -246,7 +293,10 @@ order-service's `extend` block is `c4.invalid` on checkout-web's model ("Could n
|
|
|
246
293
|
reference to Referenceable named 'db'"), or `landscape.invalid` plus one `spine.landscape-invalid`
|
|
247
294
|
per service when that second edge is drawn on the map. `landscape.datastore-shared` needs a second
|
|
248
295
|
consumer to fire, so it always prints in the state where map→extend breaks, and its message says
|
|
249
|
-
so.
|
|
296
|
+
so. It is the one datastore grade left, it fires at two consumers for both placements alike, and it
|
|
297
|
+
states a coupling rather than prescribing a move: shared means the same DATA, and if two services
|
|
298
|
+
only share a host or a cluster the honest model is one private store each. The consumer census is
|
|
299
|
+
fleet-scope: it reads
|
|
250
300
|
EXTENDING models only, because their ids are the map's fully-qualified ids and the join is exact,
|
|
251
301
|
while a standalone model's ids are its own file's and are never matched to a landscape element.
|
|
252
302
|
|
|
@@ -433,11 +483,12 @@ the file behind.
|
|
|
433
483
|
`loam subsystem sync` is the one regenerator (it also *removes* the file when the tree has no
|
|
434
484
|
subsystems), every `loam subsystem` write verb regenerates it inside the same transaction, and
|
|
435
485
|
`validate --all` grades staleness by **content comparison** (line endings normalized; a CRLF
|
|
436
|
-
checkout changes no fact) —
|
|
437
|
-
`subsystem.views-stale`,
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
486
|
+
checkout changes no fact) — one error,
|
|
487
|
+
`subsystem.views-stale`, per stale generated file, each naming its own path (there are two of them;
|
|
488
|
+
`architecture/services.likec4` is below); nothing in loam ever parses either: a views-only document
|
|
489
|
+
does not parse standalone (its `include` lines reference elements the landscape defines), and both
|
|
490
|
+
hold **static** views, whose contents loam never reads in any document. Only the LikeC4 renderer,
|
|
491
|
+
which merges the whole `architecture/` project, resolves their includes.
|
|
441
492
|
|
|
442
493
|
`sync` refuses to rewrite this file while the `architecture/` project cannot be read. The render
|
|
443
494
|
needs the map's elements and its global-style census, and a tolerant "no elements" answer wrote a
|
|
@@ -452,7 +503,111 @@ surface. With no file yet, and inside a tree-changing verb's commit window (`new
|
|
|
452
503
|
`rename`, whose contract is that the file lands with the change), the tolerant shape is written as
|
|
453
504
|
before and the same note is printed.
|
|
454
505
|
|
|
455
|
-
|
|
506
|
+
### `architecture/services.likec4` — one overview view per service
|
|
507
|
+
|
|
508
|
+
The **second generated views file**, written by the same `loam subsystem sync`, in the same commit,
|
|
509
|
+
removed by it when the map draws no service, and graded by the same `subsystem.views-stale` on its
|
|
510
|
+
own path. Views only, one per service the fleet map draws, sorted by service directory id:
|
|
511
|
+
|
|
512
|
+
```
|
|
513
|
+
view service_payment_2dservice of marketplace.paymentService {
|
|
514
|
+
title 'Payments / payment-service'
|
|
515
|
+
global style services
|
|
516
|
+
include *
|
|
517
|
+
include kafka.orderEvents
|
|
518
|
+
include marketplace.checkoutWeb
|
|
519
|
+
include stripe
|
|
520
|
+
exclude * -> *
|
|
521
|
+
include -> marketplace.paymentService
|
|
522
|
+
include marketplace.paymentService ->
|
|
523
|
+
include marketplace.paymentService.* -> marketplace.paymentService.*
|
|
524
|
+
}
|
|
525
|
+
```
|
|
526
|
+
|
|
527
|
+
**Why the peers are named one by one, measured at the 1.59.2 pin on a 120-service and a 58-service
|
|
528
|
+
fleet.** The obvious recipe — `include <fqn> -> *` and `include * -> <fqn>` — is wrong at fleet
|
|
529
|
+
scale and looks perfect on a small flat fixture, which is how it would have shipped. LikeC4 draws a
|
|
530
|
+
counterpart at the highest ancestor that is not also an ancestor of the subject, so on a map that
|
|
531
|
+
GROUPS its services (which is exactly what a 100-service fleet needs at fleet level) every peer
|
|
532
|
+
collapses to its group box: the naive view of one service in the 120-service fleet drew six group
|
|
533
|
+
boxes and **0 of its 10 nodes had drill-down**. loam already reads elements and relationships, so it
|
|
534
|
+
names each peer itself — the far endpoint of every edge touching the service or anything inside it,
|
|
535
|
+
spelled as the map declares it, so an edge at a topic names the topic and not the broker, which is
|
|
536
|
+
the truer picture and the one the map already draws. `exclude * -> *` then drops the peer-to-peer
|
|
537
|
+
cross traffic (23 edges down to 7 on the same fixture), and the three lines after it put back
|
|
538
|
+
exactly the subject's own: the UNARY `include -> <fqn>` / `include <fqn> ->`, never the binary
|
|
539
|
+
wildcard, which re-introduces the parent group boxes as extra nodes (16 instead of 10). `include
|
|
540
|
+
<fqn>.* -> <fqn>.*` is the interior wiring, and it is legal on a childless element — it simply
|
|
541
|
+
matches nothing — which is what makes one line work for both model shapes: an extending model's
|
|
542
|
+
containers are in the root project and appear, a standalone model's are not and the line is silent.
|
|
543
|
+
|
|
544
|
+
Measured at 58 services in 11 groups: a median of **10 nodes and 8 edges** per generated view, every
|
|
545
|
+
peer that is a service carrying drill-down into its own generated view, and `npx likec4 export json`
|
|
546
|
+
— which layouts every view — costing about **7 ms per generated view** (1.3 s with 120 of them
|
|
547
|
+
against 451 ms without). `likec4 start` layouts on demand, so a reader pays for the one view they
|
|
548
|
+
open, and the generated views are all small BY CONSTRUCTION; the expensive view is the hand-authored
|
|
549
|
+
fleet one, and it is unaffected.
|
|
550
|
+
|
|
551
|
+
**Drill-down is the `of`, and it costs one keyword.** `view <id> of <element-fqn>` names the view's
|
|
552
|
+
subject, and LikeC4 then computes `navigateTo` on that element's node in **every other view that
|
|
553
|
+
draws it** — no authored links anywhere. So one generated view per service makes the fleet map,
|
|
554
|
+
every subsystem view and every other service view click-through into the service. The subsystem
|
|
555
|
+
views take the same keyword when the map holds an element owning exactly that subsystem's members,
|
|
556
|
+
which closes the mesh — `fleet → payments → payments/service-22 → identity/service-45 → identity`,
|
|
557
|
+
every hop generated, none authored.
|
|
558
|
+
|
|
559
|
+
**Two views of one element are legal, and the FIRST DOCUMENT ALPHABETICALLY wins** the default
|
|
560
|
+
navigation target — measured by renaming the files, LikeC4's load order being alphabetical by path.
|
|
561
|
+
Both stay reachable: inside a view of the subject, the subject's own node navigates to the OTHER
|
|
562
|
+
view of the same element, so the renderer cycles between siblings and nothing is orphaned.
|
|
563
|
+
`architecture/landscape.likec4` sorts before `architecture/services.likec4`, so a team's authored
|
|
564
|
+
`view of <element>` written in the map keeps precedence over the generated one, and neither is lost.
|
|
565
|
+
loam does not detect this and never will: **docs/DESIGN.md rule 26 forbids loam reading what a view
|
|
566
|
+
SHOWS**, in any document, so it may not look at an authored `view of` to decide whether to emit. It
|
|
567
|
+
emits for every service the map draws, and the alphabet settles the rest.
|
|
568
|
+
|
|
569
|
+
**The first line is the deed, and every writer checks it.** Both generated views files open with a
|
|
570
|
+
`// generated by loam` mark, and that mark is what makes the path loam's rather than the team's.
|
|
571
|
+
These are the only two filenames loam RESERVES inside `architecture/`, a directory that is otherwise
|
|
572
|
+
entirely authored, so a repository that already had a file at one of those paths must not lose it to
|
|
573
|
+
an upgrade — the archive's promise that authored bytes stay authored holds here too. Every writer of
|
|
574
|
+
these paths reads the first line first: `loam subsystem sync` reports that one file `refused`,
|
|
575
|
+
writes nothing for it, still does the rest of its work, and exits 1; `loam subsystem new`, `rm`,
|
|
576
|
+
`move` and `rename` and `loam seed` refuse the WHOLE operation with `already-exists`, writing,
|
|
577
|
+
renaming and staging nothing, because their commit window exists precisely to land the generated
|
|
578
|
+
file in the same commit as the change to the tree — completing half of that would manufacture the
|
|
579
|
+
`subsystem.views-stale` state the window exists never to leave behind. The consequence to know: a
|
|
580
|
+
file with no mark is never regenerated, and an EMPTY file carries no mark either, so a truncated one
|
|
581
|
+
must be deleted by hand before sync will write it again. loam's own staging is atomic and cannot
|
|
582
|
+
produce one.
|
|
583
|
+
|
|
584
|
+
**Titles are folders, because there is nothing else.** LikeC4 has **no view-folder syntax** —
|
|
585
|
+
`views { group 'X' { … } }` is a parse error at the pinned version and the AST carries no view-group
|
|
586
|
+
type at all. What it has is the title: a view title is split on `/` into browser folders and only
|
|
587
|
+
the last segment is shown. So a per-service view is titled `<subsystem path> / <service directory
|
|
588
|
+
id>`, built from the same path helper the subsystem views use, and the two generated files fold into
|
|
589
|
+
ONE folder tree mirroring `services/`. An unfiled service is its own id at the root. The service
|
|
590
|
+
**directory id** is used rather than the map element's title: it is the name every loam command
|
|
591
|
+
addresses the service by, it cannot drift from the map, and a directory id cannot contain `/`. A
|
|
592
|
+
`dynamic view` you author under `services/<…>/<id>/usecases/` lands in the same folder when you
|
|
593
|
+
title it `'<subsystem path> / <service id> / <use case>'` — a convention loam states and never
|
|
594
|
+
grades.
|
|
595
|
+
|
|
596
|
+
**Which services appear.** Exactly those the element→service join resolves, the same join the
|
|
597
|
+
subsystem views use. A service the map does not draw is omitted — its absence is already
|
|
598
|
+
`landscape.service-unmodelled`, and rendering only what the committed map resolves is what keeps the
|
|
599
|
+
output a function of committed bytes. **View ids** are `service_` plus the same hex escaping the
|
|
600
|
+
subsystem view ids use, so the two generated files cannot collide with each other, and an authored
|
|
601
|
+
view claiming one is `subsystem.view-id-collision` — which names the file and the reserved prefix. A
|
|
602
|
+
`service_` id is reserved only for a service the map actually draws. **Style**: `global style
|
|
603
|
+
services`, presence-detected off the parsed map's declared style ids exactly as `subsystems` is, and
|
|
604
|
+
written only when a group of that name is declared — a reference to an undeclared group is a parse
|
|
605
|
+
error that blanks the whole project.
|
|
606
|
+
|
|
607
|
+
### The per-service LikeC4 project file
|
|
608
|
+
|
|
609
|
+
A different artifact from the two generated views files above, and the tree's other renderer
|
|
610
|
+
concern: `services/<…>/<svc>/likec4.config.json`,
|
|
456
611
|
which `loam subsystem sync` also writes, beside every **standalone** `model.likec4` that has none.
|
|
457
612
|
An extending model needs no such file and never gets one: it belongs to the root project, which is
|
|
458
613
|
where it parses, and a project file beside it is `service.likec4-config-stray` — which `sync`
|
|
@@ -589,7 +744,10 @@ so running loam from a subdirectory works; `loam init` writes the file where it
|
|
|
589
744
|
this repo, digests that document and compares it with the committed
|
|
590
745
|
`services/<id>/openapi.yaml`: `openapi.generated-stale` (warn) when they differ,
|
|
591
746
|
`contracts.source-missing` (error) when nothing is there, `contracts.source-invalid` (error) when
|
|
592
|
-
the path escapes the repository or the file does not parse.
|
|
747
|
+
the path escapes the repository or the file does not parse. The stale warning's `details` name
|
|
748
|
+
the operationIds that differ (served but not in the committed copy, committed but no longer
|
|
749
|
+
served), and its message says when the two sets agree, so a reworded description no longer reads
|
|
750
|
+
the same as twenty vanished endpoints.
|
|
593
751
|
|
|
594
752
|
**loam reads it and never writes it.** The copy into the docs repo stays a human `cp` reviewed in
|
|
595
753
|
a pull request — that is what keeps the committed contract a document somebody agreed to rather
|
|
@@ -803,7 +961,12 @@ two checked files:
|
|
|
803
961
|
`--all` alike, from the docs repo included. A `verified` spec whose current body hash mismatches
|
|
804
962
|
gets `content.stale` (**warning**, the `sources.stale` grading: the doc changed since it was
|
|
805
963
|
vouched, and only a person can say whether verified still holds). A verified doc with no
|
|
806
|
-
`content_digest` predates the stamp and stays quiet — the fix is re-vouching.
|
|
964
|
+
`content_digest` predates the stamp and stays quiet — the fix is re-vouching. Every fleet readout
|
|
965
|
+
carries the same fact, because it is computable from the docs repo: `loam list` shows
|
|
966
|
+
`vouched (stale)` and owes the row `re-vouch` in `missing` (so `--needs-work` lists it), `loam
|
|
967
|
+
status` counts it under `services.contentStale` and emits `next.revouch` instead of
|
|
968
|
+
`next.fleet-clean`, and `validate --all`'s scorecard counts it under `provenance.contentStale`.
|
|
969
|
+
The rung stays `vouched` — a person did stamp the document — exactly as for `vouch_scope` below.
|
|
807
970
|
- `vouch_scope` — written by `loam vouch --sample <n>`, never by hand, and present only when the
|
|
808
971
|
person who vouched read a recorded SAMPLE of the document rather than all of it. One flat string,
|
|
809
972
|
`sampled <k>/<n> seed=<16 hex>`: k sections read of n, and the seed that chose them. `status`
|
|
@@ -975,7 +1138,7 @@ There is no manifest of services: the directories under `services/` **are** the
|
|
|
975
1138
|
touched/untouched, never a set difference over the attested calls, because loam cannot tell a
|
|
976
1139
|
call the map forgot from one the fleet deliberately does not draw at its own altitude. "Edge in
|
|
977
1140
|
the map" means an edge in the `architecture/` PROJECT — the landscape plus every other `.likec4`
|
|
978
|
-
under it, minus the generated `subsystems.likec4` — and `loam adopt` reads the same set. An
|
|
1141
|
+
under it, minus the generated views files (`subsystems.likec4`, `services.likec4`) — and `loam adopt` reads the same set. An
|
|
979
1142
|
`#external` element is never the subject, whatever directory it is bound to.
|
|
980
1143
|
|
|
981
1144
|
**`architecture/landscape.likec4` is required, not optional.** Its absence is a finding, never a
|
|
@@ -1078,9 +1241,64 @@ under any flag — `x-loam-remove` and `x-loam-based-on` are stripped at path le
|
|
|
1078
1241
|
level alike, on every merge branch. A path item consisting of nothing but a marker publishes no
|
|
1079
1242
|
phantom path. The same feature must carry a `REMOVED` requirement whose `Operations:` line names
|
|
1080
1243
|
that id. Coherence gates an absent path+method, an id mismatch, an unjustified marker, a removed
|
|
1081
|
-
requirement with no marker,
|
|
1244
|
+
requirement with no marker, a new tagged C4 edge consuming the operation being removed, and a
|
|
1245
|
+
removal anything would still consume after the merge (`openapi.remove-op-consumed`, error,
|
|
1246
|
+
`--approve` overrides — **Who still consumes it** below says what counts). A `REMOVED`
|
|
1247
|
+
requirement owes a marker only while there is a living operation for it to retire: not for an id
|
|
1248
|
+
the service's living contract does not define (a consumer's `REMOVED` requirement naming another
|
|
1249
|
+
service's operation), and not for one this delta already replaces at its address (the overwrite
|
|
1250
|
+
below — no marker could be written there, since the replacement holds the slot). A living
|
|
1251
|
+
contract that does not parse proves neither exemption, so the marker stays owed.
|
|
1082
1252
|
`serviceOperationIds` evaluates the feature as living upserts minus removals, so other same-feature
|
|
1083
|
-
checks see the post-archive contract.
|
|
1253
|
+
checks see the post-archive contract.
|
|
1254
|
+
|
|
1255
|
+
**An overwrite can remove an operation too, and is graded as one.** The merge writes per method and
|
|
1256
|
+
path, so a delta defining ANOTHER operationId where a living operation lives replaces it there — and
|
|
1257
|
+
when the contract the merge would write then defines the living one nowhere, it has left the
|
|
1258
|
+
contract with no marker for any of the checks above to read. Coherence asks the removal's own
|
|
1259
|
+
consumer question of it, on the merged contract: `openapi.remove-op-consumed` (error, `--approve`
|
|
1260
|
+
overrides, exactly as for a marker) while anything would still consume it after the merge. With no
|
|
1261
|
+
marker there is no `REMOVED` requirement either, so the provider's own requirement naming the old id
|
|
1262
|
+
stays unless the feature `MODIFIES` it to name the new operation or `REMOVES` it — and a `REMOVED`
|
|
1263
|
+
one owes no marker here. A new tagged edge onto the replaced operation is `c4-api.op-removing`. The
|
|
1264
|
+
fix is the delta's choice: define the replaced operation at another address in the same delta if it
|
|
1265
|
+
is to stay; for a rename, redraw the living edge and restate the governing requirement. An
|
|
1266
|
+
overwrite of an operation nothing names stays the advisory `api.op-unconsumed` it always was.
|
|
1267
|
+
|
|
1268
|
+
**Who still consumes it — one answer for the gate, `loam diff` and `loam archive`.** The consumers
|
|
1269
|
+
of an operation are read once, the same way for every reader (`core/consumers/`):
|
|
1270
|
+
|
|
1271
|
+
- every edge of the `architecture/` **project** whose `metadata { op }` names it and whose target
|
|
1272
|
+
resolves to the provider — any document the spine grades, never `landscape.likec4` alone (when
|
|
1273
|
+
only a sibling document fails to parse, the map file's own edges still count);
|
|
1274
|
+
- another service's requirement — in `spec.md` **or** `arch.spec.md` — whose `Operations:` line
|
|
1275
|
+
names it, **unless that service's own contract defines the same id** — then the line names that
|
|
1276
|
+
service's own operation: `validate` resolves a spec.md `Operations:` line against its own
|
|
1277
|
+
service's contract, and `loam dependencies` reads a line in either file naming an operation its
|
|
1278
|
+
own service provides as no dependency on anybody (an unreadable or absent contract proves no
|
|
1279
|
+
ownership, and the line counts);
|
|
1280
|
+
- the use-case hops the model attributes to it (a contested hop is a doubt, never a consumer);
|
|
1281
|
+
- the provider's OWN documents still naming it — its `spec.md` requirements, and edges it draws to
|
|
1282
|
+
itself. Not its `arch.spec.md` lines: `validate` grades an `Operations:` line in spec.md only, so
|
|
1283
|
+
one in arch.spec.md strands nothing (another service's does count — it is a dependency on this
|
|
1284
|
+
contract, as `loam dependencies` reads it). The gate counts these, because the merge strands them as surely; `loam diff` and the archive's
|
|
1285
|
+
`openapi.op-modified` warnings answer who ELSE is affected, and leave them out (a service owes no
|
|
1286
|
+
operationId to itself).
|
|
1287
|
+
|
|
1288
|
+
The gate reads all of it **after the feature's merge**: a requirement the same feature `MODIFIES`
|
|
1289
|
+
to drop the id, or `REMOVES`, no longer counts — another service's too, so retiring the consumer in
|
|
1290
|
+
the same feature is a fix the gate sees — and one of its own `ADDED`/`MODIFIED` requirements naming
|
|
1291
|
+
the id does. A delta the merge would refuse to apply fails closed: the living requirements count,
|
|
1292
|
+
plus every one the delta adds or modifies. Edges and hops are read as they stand: the landscape
|
|
1293
|
+
merge only ADDS edges and a feature's flows are create-only, so no feature document can take one
|
|
1294
|
+
away — drop or redraw a living edge by editing the living map in the same change. The refusal lists
|
|
1295
|
+
the consumers in its `details[]`, in the order `loam diff` lists the victims of the same removal.
|
|
1296
|
+
A consumer document nobody could read refuses too, and is named there: a spec file that does not
|
|
1297
|
+
decode (another service's, or the provider's own), or a service id two directories claim — the gate
|
|
1298
|
+
cannot prove such a document does not name what is leaving, so it says it could not check, never
|
|
1299
|
+
that the fleet still consumes it. An edge drawn with no `metadata { op }` and a contested use-case
|
|
1300
|
+
hop are doubts about a drawing: `loam diff` lists them, and they never refuse, since `loam seed`
|
|
1301
|
+
draws every edge without an op. Archive deletes only the matching method, reports removals in
|
|
1084
1302
|
text and `openapiRemovals` JSON, never copies the marker into living OpenAPI, and deliberately does
|
|
1085
1303
|
not garbage-collect components. A removal marker in a living `openapi.yaml` is
|
|
1086
1304
|
`openapi.remove-marker-living` (error). The usual staged-write rollback and `unarchive` snapshot
|
|
@@ -1092,9 +1310,11 @@ A `paymentService -> kafka 'Publishes PaymentAuthorized'` edge used to read as f
|
|
|
1092
1310
|
while naming no contract anybody could check. The event axis is the API axis's mirror, in **AsyncAPI
|
|
1093
1311
|
3.0** — only 3.0, because its operations are named top-level objects carrying
|
|
1094
1312
|
`action: send|receive`, the exact analog of an `operationId`, where 2.x spells the same thing as
|
|
1095
|
-
`publish`/`subscribe` under a channel and leaves whose perspective they take ambiguous. A
|
|
1096
|
-
|
|
1097
|
-
|
|
1313
|
+
`publish`/`subscribe` under a channel and leaves whose perspective they take ambiguous. A document
|
|
1314
|
+
whose `asyncapi:` field says 1.x or 2.x is refused as one `asyncapi.invalid` naming the version, and
|
|
1315
|
+
nothing on the event axis is graded against it: reading it as a 3.0 contract with no messages used
|
|
1316
|
+
to turn every edge and requirement into a misleading `spine.message-undefined`. The AsyncAPI CLI's
|
|
1317
|
+
`asyncapi convert --target-version=3.0.0` upgrades it. Like OpenAPI it is read with a shallow structural walk over the `yaml` parse, never
|
|
1098
1318
|
a real AsyncAPI parser: the slice loam joins on is small enough that the parser's model would be
|
|
1099
1319
|
dead weight, and adding it would be a fourth runtime dependency.
|
|
1100
1320
|
|
|
@@ -1157,9 +1377,14 @@ the edges point at channels it is the channels that collect three consumers each
|
|
|
1157
1377
|
broker leaves a row of untagged hubs behind it.
|
|
1158
1378
|
|
|
1159
1379
|
**The feature lifecycle, mirrored from the OpenAPI axis.** A feature changes this contract through
|
|
1160
|
-
`features/<FEAT>/specs/<svc>/asyncapi.yaml` — a
|
|
1161
|
-
|
|
1162
|
-
|
|
1380
|
+
`features/<FEAT>/specs/<svc>/asyncapi.yaml` — a self-contained AsyncAPI 3.0 document holding **only
|
|
1381
|
+
the slots the feature adds, changes or removes**, exactly as an openapi delta holds only its own
|
|
1382
|
+
operations: the merge walks only the slots the delta declares, so a slot it leaves out stays as the
|
|
1383
|
+
living contract has it, and an edited slot is written out whole. A slot restated without being
|
|
1384
|
+
changed is legal and the machinery tells it from an edit, but it buys nothing and costs a pin,
|
|
1385
|
+
which a parallel feature's removal turns into `asyncapi.baseline-invalid` and `asyncapi.slot-revived`
|
|
1386
|
+
— or, unpinned, into `asyncapi.slot-revived` alone, the contract axis's revival check (under *Living
|
|
1387
|
+
spec vs delta* below) on this axis's identity. Identity is the **slot** — the pair
|
|
1163
1388
|
(section, key) over the three named sections `channels.<key>`, `operations.<key>` and
|
|
1164
1389
|
`components.messages.<key>` — and the join token stays the message name. Three feature-only keys,
|
|
1165
1390
|
spelled and shaped exactly as on the OpenAPI axis: `x-loam-based-on` inside a slot's value (written
|
|
@@ -1211,7 +1436,36 @@ decision that reverses that stance: the removal family exists, a message-slot re
|
|
|
1211
1436
|
justified by a `REMOVED` requirement's `Publishes:`/`Consumes:` line (channel and operation slots
|
|
1212
1437
|
need exactness only — requirements join on message names and never on channel or operation keys, so
|
|
1213
1438
|
demanding a justifying requirement there would invent a join the corpus cannot express), and
|
|
1214
|
-
`asyncapi.remove-message-consumed` gates a removal the
|
|
1439
|
+
`asyncapi.remove-message-consumed` gates a removal the fleet would still consume after the merge —
|
|
1440
|
+
a `consumes` edge of the `architecture/` project, another service's `Consumes:` line in either spec
|
|
1441
|
+
file (no ownership rule here: a consumer declares the message in its own contract as `receive` by
|
|
1442
|
+
construction), a use-case hop backed by such an edge — or whose provider's own `Publishes:` line the
|
|
1443
|
+
merge would leave behind, or its own `publishes` edge (the spine grades that edge on its source,
|
|
1444
|
+
`spine.message-undefined`; drop or redraw it in the living map), which covers a message slot re-keyed to another name or a send operation
|
|
1445
|
+
turned around as surely as a marker: read after the merge, as on the HTTP axis, and a `REMOVED`
|
|
1446
|
+
requirement owes no marker for a message the merge already takes out. **A consumer counts only
|
|
1447
|
+
while nothing would still send the message after the merge** — no service, and no `#external`
|
|
1448
|
+
element whose edge publishes it: the join is fleet-global, and a consumer with another producer is
|
|
1449
|
+
not stranded — `spine.message-unproduced` grades a consumer by exactly that, an outside producer
|
|
1450
|
+
included (*The direction is not decoration* above), and `loam diff` reports such a removal as
|
|
1451
|
+
`diff.message-removed`, "still sent by …", naming an outside producer as `<title> (#external)`.
|
|
1452
|
+
The senders are read after the merge too: a service this feature carries an event delta for sends
|
|
1453
|
+
what the merged contract would (the delta itself, where it creates the contract), and vouches for
|
|
1454
|
+
nothing where the merge cannot be simulated, so a feature retiring the message from every sender
|
|
1455
|
+
is still refused; an unreadable contract never vouches. The outside producer is read off the
|
|
1456
|
+
living `architecture/` project with validate's own rule, and only while that project parses. The
|
|
1457
|
+
landscape merge only adds to the map, so no feature takes its edge or tag away — but the rule also
|
|
1458
|
+
asks that no `services/<id>/` directory stand for the element, and the merge creates one from each
|
|
1459
|
+
`specs/<id>/` the feature carries. So the rule is asked over the directories as the merge leaves
|
|
1460
|
+
them: a feature that onboards the outside system as a service (`specs/legacy-gateway/`, its element
|
|
1461
|
+
titled or bound `legacy-gateway`, or a topic nested inside it) takes the producer away, and a
|
|
1462
|
+
retirement in it is refused unless something else still sends the message — the new service's own
|
|
1463
|
+
contract, in its `specs/legacy-gateway/` delta, for one. A `publishes` edge
|
|
1464
|
+
from an `#external` element that only the feature's own C4 delta draws does not count either —
|
|
1465
|
+
draw it in the living map, or archive with `--approve`. Another sender does not excuse the
|
|
1466
|
+
provider's own `Publishes:` lines and `publishes` edges — those are graded against its own
|
|
1467
|
+
contract, whoever else sends the message — nor its own unreadable spec files; another service's
|
|
1468
|
+
unreadable spec file, or an id two directories claim, refuses only while nothing would send. The lag argument
|
|
1215
1469
|
survives as an **operational warning**, not a veto: passing the gate proves the *documents* agree
|
|
1216
1470
|
nobody consumes the message — it does not prove no consumer is still reading the topic with lag, so
|
|
1217
1471
|
sequence the runtime retirement accordingly.
|
|
@@ -1325,19 +1579,35 @@ the source for tests.
|
|
|
1325
1579
|
never the resolution itself — a pin says "I read this version", so `loam rebase` reports every
|
|
1326
1580
|
requirement that moved and says plainly that restamping without re-reading merges your text over
|
|
1327
1581
|
theirs. There is deliberately no three-way merge: requirement prose does not merge.
|
|
1328
|
-
- **The same baseline on the contract axis
|
|
1329
|
-
|
|
1330
|
-
|
|
1331
|
-
|
|
1332
|
-
|
|
1333
|
-
|
|
1334
|
-
|
|
1335
|
-
|
|
1582
|
+
- **The same baseline on the contract axis.** A feature's `openapi.yaml` is a **self-contained
|
|
1583
|
+
document holding only what the feature adds, changes or removes** — not a copy of the living
|
|
1584
|
+
contract. The merge walks only the paths and methods the delta spells: an operation it leaves out
|
|
1585
|
+
stays exactly as the living contract has it, a local `$ref` (`#/components/schemas/Order`)
|
|
1586
|
+
resolves into the living document, and a brand-new operation, path-level key or component has no
|
|
1587
|
+
living version to pin, so an add-only delta never trips `openapi.baseline-missing` and needs no
|
|
1588
|
+
`loam rebase` at all. What the merge does upsert is every operation the document *does* spell,
|
|
1589
|
+
so an operation quoted without being changed — copied for context, or a whole contract pasted in
|
|
1590
|
+
— is a collision waiting for somebody else's edit: two deltas over one service, each quoting the
|
|
1591
|
+
operation the other edits, destroy each other, because whichever archives second pushes its
|
|
1592
|
+
authoring-time copy back over what landed in between. A quote also costs something even pinned:
|
|
1593
|
+
it is one more pin, and a pinned quote whose living operation a parallel feature removes is
|
|
1594
|
+
`openapi.baseline-invalid` — the living slot is gone, so the pin is founded on nothing — an
|
|
1595
|
+
error that blocks the archive until the quote is deleted. Unpinned, the same quote has no living
|
|
1596
|
+
operation behind it, so the baseline axis cannot tell it from an addition — and it used to
|
|
1597
|
+
archive as one, at exit 0, putting the retired operation back. The **revival check** below is
|
|
1598
|
+
what refuses it now. So a delta quotes nothing it does not change, and every operation in it that
|
|
1599
|
+
the living contract already holds carries
|
|
1600
|
+
**`x-loam-based-on`** — the same 16-hex digest, in the
|
|
1336
1601
|
vendor-extension shape this axis already uses for `x-loam-remove`, written by `loam rebase` and
|
|
1337
1602
|
stripped on merge exactly like `Based-On:`. The digest is taken over the operation's *canonical*
|
|
1338
1603
|
form (object keys sorted), which is the same comparison `isDeepStrictEqual` makes when the merge
|
|
1339
1604
|
decides an operation "differs" — so a pin goes stale exactly when the merge would say something
|
|
1340
|
-
moved, never over a reordered key. The pin is
|
|
1605
|
+
moved, never over a reordered key. The pin is keyed by the slot, method and path, on both sides,
|
|
1606
|
+
so an operation written with no `operationId` — living or in the delta — is pinned, graded and
|
|
1607
|
+
quoted exactly as a named one: `loam rebase` always pinned against whatever stood at the slot,
|
|
1608
|
+
and the gate once read named operations alone, so it refused the pin rebase had just written as
|
|
1609
|
+
one over "no operation at that slot" and never asked an unnamed quote for a pin. The pin is
|
|
1610
|
+
always the digest of the **living** operation, and
|
|
1341
1611
|
that one rule yields both merge verdicts by itself: an operation the author only quoted is
|
|
1342
1612
|
byte-equal to living, so its pin equals its own content and **the merge skips it entirely**
|
|
1343
1613
|
(reported as `· quotes …` in the plan, so the plan never silently writes less than the delta
|
|
@@ -1347,7 +1617,8 @@ the source for tests.
|
|
|
1347
1617
|
decision, reverting an operation nobody edited is a bug. `openapi.baseline-missing` is a warning
|
|
1348
1618
|
that gates the archive, exactly like its requirement-axis sibling — every unpinned restatement is
|
|
1349
1619
|
a candidate rollback — and it is reported **once per service** rather than per operation: the fix
|
|
1350
|
-
is one command, and twenty-nine identical findings
|
|
1620
|
+
is one command, and the twenty-nine identical findings a contract pasted whole from a
|
|
1621
|
+
thirty-operation service would raise teach people to filter the code out. **Covered
|
|
1351
1622
|
since the path-item/component baselines landed:** the same discipline now reaches the two surfaces
|
|
1352
1623
|
the operation pin cannot — path-level keys (`parameters`, `servers`, `summary`, `x-*`) and every
|
|
1353
1624
|
`components/<kind>/<name>` — through a single feature-only root record **`x-loam-baselines`** (`{
|
|
@@ -1362,6 +1633,93 @@ the source for tests.
|
|
|
1362
1633
|
component copying is verdict-driven, with genuinely new components copied only when reachable from
|
|
1363
1634
|
content actually written. Removal markers still carry no pin, since
|
|
1364
1635
|
`openapi.remove-target-mismatch` already checks that exact slot.
|
|
1636
|
+
- **A retired operation does not come back by accident.** An operation the living contract does
|
|
1637
|
+
not hold is either new or *retired*, and the living contract alone cannot say which: both are
|
|
1638
|
+
simply absent. So the feature gate also reads what every **archived** feature took out — the
|
|
1639
|
+
`x-loam-remove: true` markers left in `features/archive/<FEAT>/specs/<svc>/openapi.yaml` and
|
|
1640
|
+
`asyncapi.yaml`, through the same relocation rule the gate applies to an in-flight delta (a
|
|
1641
|
+
marker on an id the same delta redefines elsewhere moved it, and retired nothing). A delta entry
|
|
1642
|
+
that is not itself a removal, that its service's living contract does not hold, and that an
|
|
1643
|
+
archived feature retired is **`openapi.op-revived`** (by operationId — a
|
|
1644
|
+
retired operation quoted at a new path is the same operation back) or **`asyncapi.slot-revived`**
|
|
1645
|
+
(a `channels` or `operations` slot by its key; a message by its *name*, wherever it is declared).
|
|
1646
|
+
A relocation retires nothing, but it VACATES an address — and so does a retirement, once a later
|
|
1647
|
+
feature re-adds the id somewhere else — so the index records, per id, every address an archived
|
|
1648
|
+
feature's executed marker vacated, which feature, and for a move where its delta put the id. The
|
|
1649
|
+
finding is judged on the contract the archive would WRITE: the check runs the archive's own
|
|
1650
|
+
merge of the delta over the living contract and compares where each operationId stands there with
|
|
1651
|
+
where it stands now. An (operationId, address) the merged contract has and the living one lacks
|
|
1652
|
+
is `openapi.op-revived` when the living contract holds the id nowhere and an archived feature
|
|
1653
|
+
retired it (anywhere — by identity) or moved it away from that very address, or when the merged
|
|
1654
|
+
contract answers the id at two or more addresses and an archived feature retired it at, or moved
|
|
1655
|
+
it away from, that very address: archived, it would answer the id at both. An id that the living
|
|
1656
|
+
contract still holds and the merged contract answers at exactly one address is never a revival —
|
|
1657
|
+
a delta moving it back with its own removal marker where it lives now, or swapping another
|
|
1658
|
+
operation in there (named or not: the merge writes per method and path), leaves it at one
|
|
1659
|
+
address — while a marker standing where the id does not live deletes nothing, and excuses
|
|
1660
|
+
nothing. One address is no exemption for an id the living contract holds nowhere: that is the
|
|
1661
|
+
first case, so a quote of an address a move vacated is the finding even after a hand edit took
|
|
1662
|
+
the id out of where the move put it. Reading the merged contract replaced a
|
|
1663
|
+
case-by-case re-derivation of the merge that each review found a new hole in.
|
|
1664
|
+
On the event axis the same rule runs on the message name, over the event merge: the addresses a
|
|
1665
|
+
feature vacates are `components.messages` keys (a marker there) and the inline declarations a
|
|
1666
|
+
channel removal took. A delta that restates the channel an inline declaration stands in (with
|
|
1667
|
+
the message as a `$ref`) or retires that channel takes the name out, since a channel upsert
|
|
1668
|
+
replaces the channel's interior; a marker nested on the inline message does not
|
|
1669
|
+
(`asyncapi.remove-marker-inline`), so the finding names the channel as the remedy. A channel or
|
|
1670
|
+
operation slot's identity is its key, so moving one is a retirement plus an addition, graded as
|
|
1671
|
+
such. A message declared inline under a channel the delta brings back is that channel's
|
|
1672
|
+
interior, and the channel's finding stands for it — unless the merged contract declares the
|
|
1673
|
+
name somewhere else too (an archived feature re-declared it under `components` when it retired
|
|
1674
|
+
the channel): then the inline declaration is a second place of its own, and gets its own
|
|
1675
|
+
finding, since the channel's says nothing of the duplicate `--approve` would archive.
|
|
1676
|
+
Both are warnings that **gate** the archive, `openapi.baseline-missing`'s doctrine: the document
|
|
1677
|
+
is legal, and re-adding a retired operation can be exactly what a feature means — a retirement
|
|
1678
|
+
rolled back — so `--approve` carries the call, and the archive output then names what came back
|
|
1679
|
+
and whose retirement it reverses. The message offers both remedies: delete the quote if it only
|
|
1680
|
+
restated the operation; archive with `--approve` if the feature brings it back on purpose (a pin
|
|
1681
|
+
over nothing is `openapi.baseline-invalid`, so a deliberate re-add drops it first). At a vacated
|
|
1682
|
+
address beside a living one it names the address to drop instead: delete the quote, or, to move
|
|
1683
|
+
the id back on purpose, take it out of where it lives now in the same delta (a removal marker
|
|
1684
|
+
there; for a message declared inline, restate or retire its channel; for a `components.messages`
|
|
1685
|
+
key, also restate every channel or operation of the merged contract still `$ref`-ing it, with
|
|
1686
|
+
the `$ref` pointed at the quoted key — the message names them, since a marker alone would leave
|
|
1687
|
+
them pointing at nothing and their send would stop sending the message) — and where the delta
|
|
1688
|
+
already spells that address, so no marker can stand there, archive with `--approve`. A slot
|
|
1689
|
+
holds one entry, so where the living contract has ANOTHER operation (or message) at the address
|
|
1690
|
+
the delta spells the id at, the merge replaces it there: the message names it ("now holds
|
|
1691
|
+
'requestCancellation', and archiving this delta replaces it there"), and the move-back remedy
|
|
1692
|
+
says to define it at another address (another key, for a message) in the same delta if it is
|
|
1693
|
+
to stay — never an address
|
|
1694
|
+
"the living contract no longer has", a remedy that once led to a clean gate and an archive that
|
|
1695
|
+
deleted the other entry unnamed. `loam rebase`
|
|
1696
|
+
never pins such an entry as new: it prints the same finding (`revived[]` in `--json`) and leaves
|
|
1697
|
+
the decision to the author. It asks the question of the delta as it will WRITE it, pins
|
|
1698
|
+
included: a quote pinned to its own content is skipped by the merge, and the same quote
|
|
1699
|
+
repinned to whatever now lives at its address is an edit the merge writes — so a revival
|
|
1700
|
+
rebase's own repin creates is in that list, `--dry-run` included, and the next validate agrees. A channel-slot removal retires the messages its living channel
|
|
1701
|
+
declared inline, which the delta never spells, so those names are read from the archived
|
|
1702
|
+
feature's undo snapshot (`.loam-before/`, the pre-image of the contract the merge ran against) —
|
|
1703
|
+
only while that pre-image still hashes to what the manifest recorded. A marker is a request, not a
|
|
1704
|
+
record of what happened: the OpenAPI merge deletes a living operation only when the one at the
|
|
1705
|
+
marker's own method and path carries the marker's `operationId`, and the event merge deletes a
|
|
1706
|
+
`channels` or `operations` slot only when the living contract has that key, and a
|
|
1707
|
+
`components.messages` slot only when the message declared there carries the marker's name. A
|
|
1708
|
+
marker that missed — archived past `remove-target-missing` or `remove-target-mismatch` under
|
|
1709
|
+
`--approve` — deleted nothing, so each marker counts only if the same pre-image held its target
|
|
1710
|
+
(a contract the archive created, or never wrote, lost nothing to it); a feature with no intact
|
|
1711
|
+
snapshot cannot be checked, and its markers count as written. Two marker
|
|
1712
|
+
shapes are never sources, because neither can match anything: a marker with no `operationId`
|
|
1713
|
+
(`openapi.remove-marker-anonymous`) and one written at path level
|
|
1714
|
+
(`openapi.remove-marker-path-level`) — a feature archived past either under `--approve` left the
|
|
1715
|
+
operation living.
|
|
1716
|
+
**The honest limit:** the check knows only what the files under `features/archive/` record. A
|
|
1717
|
+
removal made by hand-editing a living contract leaves no marker anywhere, and an archived feature
|
|
1718
|
+
deleted from disk takes its markers with it; both read as "never retired", and a quote of such an
|
|
1719
|
+
operation grades as an addition exactly as it did before the check existed. A channel's inline
|
|
1720
|
+
messages drop out the same way when its feature carries no intact pre-image (archived before the
|
|
1721
|
+
snapshot existed, or the snapshot edited or removed since) — the channel slot itself is still
|
|
1722
|
+
caught. No git history is read: the archive directory is the record.
|
|
1365
1723
|
|
|
1366
1724
|
A requirement may carry a stable identity in its body: `Requirement-ID: <id>`, where the ID matches
|
|
1367
1725
|
`[A-Za-z][A-Za-z0-9._-]{0,127}`. It is optional for backward compatibility. When present in a delta,
|
|
@@ -1414,7 +1772,10 @@ scenarios exercise. Comma-separated, same keep-last-line quirk (and the same rep
|
|
|
1414
1772
|
|
|
1415
1773
|
An entry that resolves to nothing is `covers.unknown` (**warning**, with close ids offered where
|
|
1416
1774
|
they exist): the typo guard, because a mistyped entry silently costs exactly the coverage it was
|
|
1417
|
-
written for.
|
|
1775
|
+
written for. It is suspended — every form at once — for a service whose `model.likec4` could not be
|
|
1776
|
+
read, whether the model itself does not parse (`c4.invalid`) or it extends a map that does not
|
|
1777
|
+
(`spine.landscape-invalid`): an entry naming a container of an unread model is not a typo, and the
|
|
1778
|
+
one repair brings the whole grade back on the next run.
|
|
1418
1779
|
|
|
1419
1780
|
**Coverage obligations** — both warnings, deliberately: they never gate `archive`, and `--strict` is
|
|
1420
1781
|
the CI escalation.
|
|
@@ -1440,9 +1801,9 @@ the CI escalation.
|
|
|
1440
1801
|
match, with a did-you-mean hint. Deliberately the service model and never the landscape read on
|
|
1441
1802
|
its own: the
|
|
1442
1803
|
landscape carries only what crosses the service boundary, the model carries everything the service
|
|
1443
|
-
touches (a private store nested inside the service's own element —
|
|
1444
|
-
|
|
1445
|
-
nested store
|
|
1804
|
+
touches (a private store nested inside the service's own element — one of the two placements
|
|
1805
|
+
"Private stores" above leaves to the team, and `landscape.datastore-shared` is what a second
|
|
1806
|
+
consumer of that nested store earns), and that is the set the on-call file must agree with. For an extending model
|
|
1446
1807
|
the set is its own SLICE: the elements it adds, plus the partners its own edges point at and their
|
|
1447
1808
|
ancestors — so a dependency named `kafka` resolves through an edge that names `kafka.orderEvents`,
|
|
1448
1809
|
and a peer service resolves through the `metadata { service }` binding on the map's element the
|
|
@@ -1530,12 +1891,15 @@ digest:
|
|
|
1530
1891
|
- stamps: line 1 is `# generated by loam v<version> — …` (the AGENTS.md stamp pattern — the file is
|
|
1531
1892
|
never to be hand-edited), and each scenario carries the tag `@loam-digest-<16hex>` on the line
|
|
1532
1893
|
above its `Scenario:` keyword: the first 16 hex characters of `loam verify`'s scenario body hash —
|
|
1533
|
-
sha256 of the body lines joined and edge-trimmed, salted with the
|
|
1534
|
-
exact recipe `scenario.tested` claim ids fold in — so the
|
|
1535
|
-
disagree about what a scenario says
|
|
1536
|
-
|
|
1537
|
-
|
|
1538
|
-
(
|
|
1894
|
+
sha256 of the body lines joined and edge-trimmed, salted with the owning SERVICE and, on the arch
|
|
1895
|
+
axis, with that axis's file name — the exact recipe `scenario.tested` claim ids fold in — so the
|
|
1896
|
+
stamp, the claim and the spec can never disagree about what a scenario says. Both salts are there
|
|
1897
|
+
for one reason: a digest that spanned two namespaces would let a green run answer a question
|
|
1898
|
+
nobody ran a test for. Identically-worded scenarios across `spec.md` and `arch.spec.md` can never
|
|
1899
|
+
share a digest (a business-axis test run cannot answer for an arch scenario), and neither can two
|
|
1900
|
+
services wording a scenario the same way (one repository's green run cannot answer another
|
|
1901
|
+
repository's claim). A tag rather than a comment because cucumber's JSON report carries tags per
|
|
1902
|
+
scenario (`elements[].tags`): the stamp rides through the runner untouched, which is what
|
|
1539
1903
|
`loam verify --results` matches on.
|
|
1540
1904
|
|
|
1541
1905
|
**Staleness** (`loam validate --service <id>`, service-repo-scoped like the `sources.*` chain):
|
|
@@ -1608,6 +1972,12 @@ requirement also needs a current `Based-On:` pin before archive; `loam rebase <F
|
|
|
1608
1972
|
Omitting or leaving the intent as scaffold text raises the gating `intent.empty` /
|
|
1609
1973
|
`scaffold.placeholder` warning, so the old one-file shape is not archive-ready.
|
|
1610
1974
|
|
|
1975
|
+
Below that floor is no feature at all. A change that touches no requirement, no operation and
|
|
1976
|
+
no fleet edge — a refactor, a dependency bump, a fix that brings the code back to behaviour the
|
|
1977
|
+
living spec already documents — leaves the docs repo untouched: it is a code PR, then
|
|
1978
|
+
`loam validate --service <id>` in the service repo, where `sources.stale` names the moved paths, and
|
|
1979
|
+
the re-vouch they owe (`loam vouch --pack` is the reading list; the vouch is a person's).
|
|
1980
|
+
|
|
1611
1981
|
Each axis may be legitimately empty, and the checks grade absence differently from
|
|
1612
1982
|
emptiness-by-accident:
|
|
1613
1983
|
|
|
@@ -1618,7 +1988,8 @@ emptiness-by-accident:
|
|
|
1618
1988
|
declared-but-untagged is almost always a forgotten tag, while declaring nothing is a legible
|
|
1619
1989
|
statement that the architecture is unchanged.
|
|
1620
1990
|
- **openapi delta** — absent whenever the feature adds no operations; a requirement change that
|
|
1621
|
-
stays inside the existing contract has no API axis to speak on.
|
|
1991
|
+
stays inside the existing contract has no API axis to speak on. Present, it holds only the
|
|
1992
|
+
operations the feature adds, changes or removes.
|
|
1622
1993
|
- **requirement delta** — the axis that is nearly always present, because a feature that changes no
|
|
1623
1994
|
requirement changes no promised behaviour. The exception is architecture-only work: a new service
|
|
1624
1995
|
in the C4 delta with no `specs/<svc>/spec.md` gets `service.no-requirement-delta` as a warning,
|
|
@@ -1631,6 +2002,16 @@ a feature cannot fold into the living docs with its reason unwritten. Its siblin
|
|
|
1631
2002
|
`scaffold.placeholder` refuses the rest of an unedited `loam new` scaffold the same way — the exact
|
|
1632
2003
|
template strings, a requirement or description nobody authored, must not become living truth.
|
|
1633
2004
|
|
|
2005
|
+
The scaffolded `intent.md` also carries an optional `## Open questions` heading over a comment.
|
|
2006
|
+
It is where `loam-feature`'s analyze step — between authoring and `loam rebase` — writes what the
|
|
2007
|
+
documents leave open: a SHALL two engineers could build two ways, a value or window two services
|
|
2008
|
+
state differently and which side refuses, a `Realizes:` that does not deliver its capability, an
|
|
2009
|
+
edge with no stated timeout, retry, partial-failure or compensation answer, a neighbour left out of
|
|
2010
|
+
`--touches`. A person answers by editing the file. Nothing grades the section: `intent.empty` reads
|
|
2011
|
+
past it, so questions written there are never mistaken for the Why, and they never gate the archive —
|
|
2012
|
+
every check loam has is a join between exact tokens, and whether two services MEAN the same thing by
|
|
2013
|
+
one is the part no join reads.
|
|
2014
|
+
|
|
1634
2015
|
### Where a capability lives (a declared name, and the document behind it)
|
|
1635
2016
|
|
|
1636
2017
|
A business capability — "payment splitting" — is spread across the living specs of every service
|
|
@@ -1797,13 +2178,19 @@ refusing it, which is why the flattening is a whitelist and not a list of substi
|
|
|
1797
2178
|
document declares no requirements yet, it declares none flattening to this slug (close ids
|
|
1798
2179
|
offered, each already spelled as the tag to write), or two of its ids flatten to one slug.
|
|
1799
2180
|
|
|
1800
|
-
**
|
|
2181
|
+
**Any reserved prefix opts a view in.** A view carrying `#req-` and no `#cap-` is an author who
|
|
1801
2182
|
asked to be graded and forgot half the claim; it earns the `unscoped` message rather than silence.
|
|
1802
|
-
|
|
1803
|
-
|
|
1804
|
-
|
|
1805
|
-
|
|
1806
|
-
|
|
2183
|
+
A view carrying only `#suite-<name>` is a use case too (see the layout section above). That opt-in
|
|
2184
|
+
is **one predicate**, and every reader takes it: `loam validate`, `loam diff`, `loam delta`,
|
|
2185
|
+
`loam status`, the context and explore packs, `loam list capabilities` and `loam list suites` all
|
|
2186
|
+
decide "is this a use case" the same way, so a flow one of them can see is a flow all of them can
|
|
2187
|
+
see. The byte scan that lets a fleet with no use cases skip the LikeC4 load looks for every prefix
|
|
2188
|
+
— `cap-`, `req-` and `suite-` — for the same reason: a gate narrower than the predicate is silence,
|
|
2189
|
+
not a smaller answer. It looks for each written as a tag REFERENCE: a `#`, then only what LikeC4
|
|
2190
|
+
skips between the `#` and a tag's name (whitespace, line breaks, comments), then the prefix — the
|
|
2191
|
+
only way a view carries a tag. A prefix in prose, a title or a `tag cap-…` declaration does not open
|
|
2192
|
+
it, so a hand-drawn `.likec4` beside a service's model that names `suite-` in its title, carries no
|
|
2193
|
+
tagged view and does not parse is not loaded and earns no `usecase.flow-invalid`.
|
|
1807
2194
|
|
|
1808
2195
|
Being SEEN is not the same as claiming something, and the difference is worth stating because it
|
|
1809
2196
|
looks like a gap and is not. A view with no resolved `#cap-` tag claims no capability, so it seeds
|
|
@@ -1988,7 +2375,10 @@ ready. Several useful product shapes remain outside that boundary on purpose:
|
|
|
1988
2375
|
a third thing to keep truthful.
|
|
1989
2376
|
- **Authored `tasks.md` as authoritative state** — loam derives its actionable checklist (`loam
|
|
1990
2377
|
delta`, `loam verify`), so it cannot drift from the delta. Imported task lists are preserved as
|
|
1991
|
-
non-authoritative migration evidence until a human chooses their disposition.
|
|
2378
|
+
non-authoritative migration evidence until a human chooses their disposition. The refusal stands,
|
|
2379
|
+
and the derived projection now exists as `loam tasks`, which answers the same question the file
|
|
2380
|
+
would have — recomputed from the delta, the generated suite and the verification record on every
|
|
2381
|
+
run, so there is nothing to tick and nothing to go stale.
|
|
1992
2382
|
- **Per-tool workflow forks** — `AGENTS.md` is the tool-agnostic contract. Generated adapters wrap
|
|
1993
2383
|
shared bodies and are never independent process definitions.
|
|
1994
2384
|
|
|
@@ -2079,6 +2469,19 @@ which carry them across loam upgrades. Correspondingly, `archive` refuses to rep
|
|
|
2079
2469
|
version-3 claim graded at its service's current directory: the previous archive's writes are still
|
|
2080
2470
|
in the docs and those files are the only way back.
|
|
2081
2471
|
|
|
2472
|
+
The manifest also carries one key that is not about bytes, added without a version bump because
|
|
2473
|
+
nothing that restores reads it: **`checklist`**, the digest of the checklist a fresh `loam verify`
|
|
2474
|
+
derived for the feature just before the merge — written when the feature carried a readable
|
|
2475
|
+
verification record then, absent otherwise and in every manifest an older loam wrote. It is the
|
|
2476
|
+
question the feature asked as it shipped. Once archived, a record is frozen history and the checklist
|
|
2477
|
+
cannot be re-derived (the merge moved the feature's operations into the living contract, so a
|
|
2478
|
+
derivation after it can only mismatch), so this is the one thing an archived record can still be
|
|
2479
|
+
held to: `loam gate --since` grades the record against it, and a record that answered an older
|
|
2480
|
+
checklist reads `stale`. `unarchive` takes the snapshot away with the rest of it, so a feature
|
|
2481
|
+
archived again gets a fresh capture. `list`, `status` and `verify` keep reading an archived record as
|
|
2482
|
+
frozen history; a manifest with no `checklist` leaves the gate reading it that way too, and saying
|
|
2483
|
+
so.
|
|
2484
|
+
|
|
2082
2485
|
Some checks run at plan time rather than in the gate, because only the computed merge can see them.
|
|
2083
2486
|
A **living** requirement sitting outside `## Requirements` refuses the whole archive
|
|
2084
2487
|
(`living.requirement-outside-requirements`, error): the merge rewrites only that section, so the
|
|
@@ -2086,7 +2489,9 @@ requirement would land in the file twice — and `--approve` does not override i
|
|
|
2086
2489
|
duplication is mechanical, not a judgment call; re-home the requirement first. A delta that
|
|
2087
2490
|
redefines a path+method the living OpenAPI already has (`openapi.op-modified`, warning) merges
|
|
2088
2491
|
anyway, but the plan says so — the overwrite is wholesale, and the living definition it replaces
|
|
2089
|
-
deserved an eye.
|
|
2492
|
+
deserved an eye. One that defines a different operationId there, leaving a consumed operation
|
|
2493
|
+
defined nowhere, never reaches the plan without `--approve`: the gate refuses it first
|
|
2494
|
+
(`openapi.remove-op-consumed`, the overwrite shape). An unparseable `delta.likec4` fails the archive at plan time regardless of
|
|
2090
2495
|
`--approve`: the alternative was archiving with the architecture axis silently dropped, which is the
|
|
2091
2496
|
data loss this tool exists to prevent.
|
|
2092
2497
|
|
|
@@ -2094,7 +2499,8 @@ The landscape merge **splices authored source, it does not re-serialize**. Each
|
|
|
2094
2499
|
relationship is copied into the living landscape byte-for-byte from `delta.likec4` — technology,
|
|
2095
2500
|
style, icons, links, metadata, nested children, every tag — with exactly one edit: the feature's own
|
|
2096
2501
|
tag is stripped (the additions are baseline now — the drop above is why unarchive restores bytes),
|
|
2097
|
-
|
|
2502
|
+
together with the `,` or `;` that set it apart in a tag list (`#FEAT-1, #x` lands as `#x`), and a
|
|
2503
|
+
construct the strip empties goes with it (`x = kind 'y' { #FEAT-1 }` lands as `x = kind 'y'`).
|
|
2098
2504
|
Placement is deterministic and **service-grouped**, and never moves an authored byte — it only
|
|
2099
2505
|
chooses where the new blocks go. A top-level element whose service the model already draws (the
|
|
2100
2506
|
`metadata { service }` binding first, the title as fallback) lands immediately after the last
|
|
@@ -2112,22 +2518,32 @@ services) yields the byte-identical landscape to B then A, so concurrent archive
|
|
|
2112
2518
|
different lines and git merges them without help. The insertions carry no marker comment — which
|
|
2113
2519
|
archive added what is the git history's to say. A **nested** element lands inside its parent's block
|
|
2114
2520
|
— inside the living parent when the landscape already has it, riding verbatim inside the spliced
|
|
2115
|
-
parent when the parent is new — never as a flat dotted id, which LikeC4 rejects at top level.
|
|
2521
|
+
parent when the parent is new — never as a flat dotted id, which LikeC4 rejects at top level. A
|
|
2522
|
+
landscape may be written in **several top-level `model { }` blocks**, which LikeC4 reads as one
|
|
2523
|
+
model, and the merge reads all of them: an ANCHORED addition — nested in a parent, joining its
|
|
2524
|
+
service's neighborhood, or joining its source's edges — lands in its anchor's block, and an
|
|
2525
|
+
unanchored one lands in the trailing region of the **first** block, the one every addition went to
|
|
2526
|
+
before a second block was readable. The walks never cross a block boundary, so the scheme stays
|
|
2527
|
+
order-independent, and a one-block map places exactly as before. LikeC4
|
|
2116
2528
|
reference resolution is not order-dependent, so a relationship placed before an element it names
|
|
2117
2529
|
still parses — and because splicing is text surgery, the computed landscape is **parsed in memory
|
|
2118
2530
|
before anything is written**: a result LikeC4 rejects — a splice bug, or legal inputs the living
|
|
2119
2531
|
document cannot absorb, like an element kind or tag its `specification` block never declares —
|
|
2120
2532
|
refuses the archive at plan time (`merge-failed`, nothing written), the same discipline as the
|
|
2121
|
-
unparseable delta. A living declaration
|
|
2122
|
-
|
|
2123
|
-
|
|
2533
|
+
unparseable delta. A living declaration the splice map cannot locate refuses the same way rather
|
|
2534
|
+
than be spliced around blind — most often one sharing its line with another statement, which is
|
|
2535
|
+
legal LikeC4 the splice map cannot see around, since it reads one statement per line. The refusal
|
|
2536
|
+
names the line LikeC4 parsed the declaration on and what the splice map read there instead, and
|
|
2537
|
+
says "shares that line" only when the line shows it: give each top-level declaration its own line,
|
|
2538
|
+
then re-run.
|
|
2124
2539
|
|
|
2125
2540
|
**Which document an addition lands in.** The C4 merge asks it per declaration, not per feature. An
|
|
2126
2541
|
element whose PARENT resolves to a service whose `model.likec4` extends the map is spliced into that
|
|
2127
2542
|
model — and so is the interior a delta writes inside a service element the same archive introduces,
|
|
2128
2543
|
since those containers carry no feature tag and would otherwise ride onto the map inside their
|
|
2129
2544
|
parent's bytes. The parent chain has to be spelled in the map's own ids for either to happen. The
|
|
2130
|
-
splice goes inside the `extend <fqn> { … }` block for the parent —
|
|
2545
|
+
splice goes inside the `extend <fqn> { … }` block for the parent — in whichever `model { }` block
|
|
2546
|
+
holds it, and appended to the first one when the model has none, as
|
|
2131
2547
|
the `model { }` block itself is appended to a model that has none (an empty file, a `views { }`-only
|
|
2132
2548
|
file and a tags-only `specification { }` are all extending models, and all three are legal to
|
|
2133
2549
|
archive into). A
|
|
@@ -2230,7 +2646,9 @@ design. The heading must be reachable, too — a leading UTF-8 BOM used to hide
|
|
|
2230
2646
|
|
|
2231
2647
|
## The verification record
|
|
2232
2648
|
|
|
2233
|
-
|
|
2649
|
+
The done-check is written down inside the feature: one `features/<FEAT>/verification/<svc>.yaml` per
|
|
2650
|
+
attesting service, or — for the all-at-once form, and every record written before the directory —
|
|
2651
|
+
the one file `features/<FEAT>/verification.yaml`. `loam verify <FEAT>` derives a
|
|
2234
2652
|
**checklist** from the feature's own artifacts — the same files `validate` reads, never the code:
|
|
2235
2653
|
|
|
2236
2654
|
| kind | one claim per | reads |
|
|
@@ -2254,18 +2672,26 @@ nobody wrote must not carry over as if it still applied.
|
|
|
2254
2672
|
anything that does not answer the *current* checklist, each under its own `--json` `error.code`:
|
|
2255
2673
|
`answers-unreadable` (not JSON, or a verdict outside `confirmed` / `unconfirmed`),
|
|
2256
2674
|
`answers-mismatch` (an id nobody asked about, a claim with no answer, or one answered twice),
|
|
2257
|
-
`answers-unevidenced` (a `confirmed` with no `file:line` behind it
|
|
2258
|
-
|
|
2675
|
+
`answers-unevidenced` (a `confirmed` with no `file:line` behind it, or one citing a file or a line
|
|
2676
|
+
that does not exist in the repository the record is written from — the all-at-once form included,
|
|
2677
|
+
which binds to no commit but no longer takes a citation on trust). The all-at-once form checks what
|
|
2678
|
+
is shaped like a citation and only that: an evidence entry that is one token ending in `:<digits>`
|
|
2679
|
+
and is not a URL must name a regular file inside the repository with that line, while prose ("manual
|
|
2680
|
+
QA by Alice"), a URL or a path with no line is recorded as it always was. The federated form keeps
|
|
2681
|
+
its stricter contract — every agent entry a `file:line` bound to the attested commit. An unchecked
|
|
2682
|
+
claim must never be able to masquerade as checked.
|
|
2259
2683
|
|
|
2260
2684
|
**`--results <report.json>` answers the `scenario.tested` claims mechanically, from a cucumber JSON
|
|
2261
|
-
test report** — the format `cucumber-js --format json` emits, and cucumber-jvm, behave
|
|
2262
|
-
speak too. The contract is exactly this and nothing more: a top-level array of features, each
|
|
2685
|
+
test report** — the format `cucumber-js --format json` emits, and cucumber-jvm, behave,
|
|
2686
|
+
SpecFlow/Reqnroll, pytest-bdd (`--cucumberjson`) and godog's cucumber formatter speak too. The contract is exactly this and nothing more: a top-level array of features, each
|
|
2263
2687
|
`elements[]` (scenarios) with `name`, `tags[] {name}` and `steps[] {result {status}}` — plus the two
|
|
2264
2688
|
places those dialects put a failure the steps never see: cucumber-jvm's per-element
|
|
2265
2689
|
`before[]`/`after[]` hook results and behave's element-level `status`. Every other field is ignored,
|
|
2266
2690
|
tagless elements (backgrounds, hand-written scenarios) are invisible, and a file that is not that
|
|
2267
2691
|
array at all refuses under `answers-unreadable` rather than quietly answering every claim "not
|
|
2268
|
-
found".
|
|
2692
|
+
found". A file that is not JSON at all — a JUnit XML report is the usual one — refuses the same way,
|
|
2693
|
+
and the refusal names both shapes `--results` reads, because the second one exists for exactly that
|
|
2694
|
+
runner.
|
|
2269
2695
|
|
|
2270
2696
|
Matching is by digest and nothing else: a claim is answered by the report scenarios carrying its
|
|
2271
2697
|
`@loam-digest-<16hex>` tag — the tag `loam gherkin` stamped, the same 16 hex of the same body hash
|
|
@@ -2274,12 +2700,21 @@ name), so a business run can never answer an arch claim however identically the
|
|
|
2274
2700
|
worded. Names never match anything: a reworded spec scenario matches nothing until the suite is
|
|
2275
2701
|
regenerated and re-run — an agent must not be able to SAY a scenario is tested; only a green run
|
|
2276
2702
|
may. The verdicts: every matching occurrence (a digest the report holds twice is a re-run, and all
|
|
2277
|
-
occurrences count) ran at least one step, every step `passed`, every before/after hook `passed`,
|
|
2278
|
-
|
|
2703
|
+
occurrences count) ran at least one step, every step `passed`, every before/after hook `passed`, the
|
|
2704
|
+
element-level status (when the dialect carries one) is `passed`, and **it ran exactly as many steps
|
|
2705
|
+
as the stamped scenario has** — the count `loam gherkin` emits for it, `And`/`But` included →
|
|
2706
|
+
`confirmed`, with evidence
|
|
2279
2707
|
`<report-path>: <feature uri or name> › <scenario name>` per occurrence; a failed / undefined /
|
|
2280
2708
|
pending / ambiguous step (`failed at step N`), a failing hook (`failed in a before/after hook`), a
|
|
2281
2709
|
failed element status, a skipped-only run (`skipped`), or no match at all (`not found in report`) →
|
|
2282
|
-
`unconfirmed` with the reason as the note
|
|
2710
|
+
`unconfirmed` with the reason as the note — and a run whose step count differs from the stamped
|
|
2711
|
+
scenario's is `unconfirmed` too (`ran N step(s) where the stamped scenario has M`). The digest is
|
|
2712
|
+
only a tag, and a tag is text: a one-step stub carrying the right digest used to confirm five-step
|
|
2713
|
+
scenarios, because nothing compared what ran with what was stamped. The count is what every dialect
|
|
2714
|
+
can be held to; the step TEXT is not compared, because an outline's steps reach the report with
|
|
2715
|
+
their placeholders filled in and dialects disagree about keyword spacing and localisation. The hook
|
|
2716
|
+
entries cucumber-js lists among `steps[]` (marked `hidden`, keyword `Before`/`After`) are read as
|
|
2717
|
+
hooks, never counted as steps — and a failing one still sinks the run.
|
|
2283
2718
|
|
|
2284
2719
|
`--results` OWNS every `scenario.tested` claim **in the same run**: an answers-file entry for one is
|
|
2285
2720
|
refused (`answers-mismatch` — the runner owns it), `--record` alongside must answer exactly the
|
|
@@ -2311,6 +2746,8 @@ own runner-neutral shape, chosen by its marker key:
|
|
|
2311
2746
|
```
|
|
2312
2747
|
|
|
2313
2748
|
`digest` is the 16 lowercase hex of the `@loam-digest-…` tag `loam gherkin` stamped on the scenario.
|
|
2749
|
+
An entry carries no steps, so it is held to its `status` alone — the step-count check above is a
|
|
2750
|
+
property of the cucumber shape.
|
|
2314
2751
|
`status` is `passed` or `failed` — nothing else. `test` is optional free text: the runner's own name
|
|
2315
2752
|
for the run, used as the evidence string a reader follows. `loamScenarioReport: 1` is the format
|
|
2316
2753
|
version this loam reads; any other value refuses rather than being partly understood.
|
|
@@ -2336,12 +2773,18 @@ carrying the marker is always graded as this shape, even when malformed: falling
|
|
|
2336
2773
|
cucumber parser would tell an author their report is not a cucumber array, which is true, useless,
|
|
2337
2774
|
and hides the real mistake.
|
|
2338
2775
|
|
|
2339
|
-
**
|
|
2340
|
-
|
|
2341
|
-
|
|
2342
|
-
|
|
2343
|
-
|
|
2344
|
-
|
|
2776
|
+
**Scenario digests are salted by both the owning service and the axis** (business versus arch), so
|
|
2777
|
+
two services wording a scenario identically do not share one: the hash folds in the service the
|
|
2778
|
+
claim is filed under. The service salt is what makes ordinary repeated wording CORRECT rather than
|
|
2779
|
+
merely refused — "the service returns 404 for an unknown id" is a sentence a fleet writes many
|
|
2780
|
+
times, and before the salt those scenarios shared a digest, so one repository's green run could
|
|
2781
|
+
appear to confirm claims from suites that never ran each other's tests, and the only defence was to
|
|
2782
|
+
decline to answer. `verify.digest-contested` (warn) survives as the guard on that invariant rather
|
|
2783
|
+
than as a diagnosis of ordinary authoring: if a digest is ever claimed by two services it is a
|
|
2784
|
+
truncated-hash collision, a single report still cannot say whose suite ran it, and those claims are
|
|
2785
|
+
left `unconfirmed` naming both services and the affected claim ids rather than confirmed from one
|
|
2786
|
+
run. Record each service's claims from its own repository with `--service`, which narrows the
|
|
2787
|
+
checklist before matching.
|
|
2345
2788
|
|
|
2346
2789
|
### Contract-test reports answer the `api.exposes` claims
|
|
2347
2790
|
|
|
@@ -2435,7 +2878,7 @@ service's as a pure lens — no repository binding, usable from the docs repo, r
|
|
|
2435
2878
|
`unknown-service` when the checklist has no claims owned by that id — and both agents must have
|
|
2436
2879
|
answered the same scope. Combining the flag with `--record`, `--results` or `--contract-results`
|
|
2437
2880
|
refuses `invalid-option`, as does any arity other than exactly two files and an archived feature
|
|
2438
|
-
(frozen history has no current checklist to answer). A
|
|
2881
|
+
(frozen history has no current checklist to answer). A verification record too damaged to read does
|
|
2439
2882
|
**not** block the diff — the lens never reads the record, and the moment a cross-examination is most
|
|
2440
2883
|
needed is before re-recording over a broken one.
|
|
2441
2884
|
|
|
@@ -2455,7 +2898,7 @@ block pins each file's path and the sha256 of the exact bytes read.
|
|
|
2455
2898
|
confirmed/unconfirmed/unanswered. The payload also deliberately carries **no `verified`, `verdict`
|
|
2456
2899
|
or `attested` key at all**: two answer sets agreeing — usually two contexts of the same model, so
|
|
2457
2900
|
correlated — ranks where a reviewer reads first; it is not verification, it upgrades nothing, and it
|
|
2458
|
-
leaves
|
|
2901
|
+
leaves the verification record byte-identical (no docs lock is taken, and the diff runs while a writer
|
|
2459
2902
|
holds one).
|
|
2460
2903
|
|
|
2461
2904
|
### Attested is not verified
|
|
@@ -2490,28 +2933,79 @@ the decision:
|
|
|
2490
2933
|
So "a green run is the only thing that confirms a scenario" is a claim about the **record**, not
|
|
2491
2934
|
about the CLI's flags: the short path still works, and it is legible afterwards as the short path.
|
|
2492
2935
|
|
|
2493
|
-
### The record is federated: one
|
|
2936
|
+
### The record is federated: one attestation per service, one file each
|
|
2494
2937
|
|
|
2495
2938
|
A cross-service feature's claims belong to several services, and each service's code lives in its
|
|
2496
2939
|
own repository — so no single place can honestly answer all of them.
|
|
2497
2940
|
`loam verify <FEAT> --service <id> --record answers.json`, run **in that service's own repo**,
|
|
2498
2941
|
narrows the checklist to that service's claims and binds the answers to this repository's commit.
|
|
2499
|
-
The result is
|
|
2500
|
-
|
|
2501
|
-
|
|
2502
|
-
|
|
2503
|
-
|
|
2504
|
-
|
|
2505
|
-
|
|
2506
|
-
|
|
2507
|
-
|
|
2508
|
-
|
|
2509
|
-
|
|
2510
|
-
|
|
2511
|
-
|
|
2512
|
-
|
|
2513
|
-
|
|
2514
|
-
|
|
2942
|
+
The result is that service's **attestation**, written to a file of its own:
|
|
2943
|
+
`features/<FEAT>/verification/<id>.yaml` — a `schema: 2` record holding exactly that service's
|
|
2944
|
+
attestation (the service id, the commit the answers were bound to, the checklist digest it answered,
|
|
2945
|
+
and its answered claims) and nothing of anybody else's. **The directory is the record**, the rule
|
|
2946
|
+
`glossary/` and `capabilities/` already follow: loam reads every file in it and adds them up, and
|
|
2947
|
+
the verdict and the counts are computed from the union at read time — exactly as they always were,
|
|
2948
|
+
since nothing ever trusted a stored `summary`. Each file's own `summary` counts the whole checklist,
|
|
2949
|
+
with the claims other services own honestly `unanswered` in it.
|
|
2950
|
+
|
|
2951
|
+
**Why one file per service.** The record used to be ONE `verification.yaml` for every service, and
|
|
2952
|
+
every attestation rewrote its `summary`, its `recorded` date and its checklist-ordered `claims`. Two
|
|
2953
|
+
teams recording two different services on two docs-repo branches therefore always conflicted when
|
|
2954
|
+
the PRs merged — add/add from an empty start, content conflicts on the summary, the claims and the
|
|
2955
|
+
attestations once one had landed — and sorting the attestations did not help, because two
|
|
2956
|
+
insertions at one anchor are a conflict whatever the order. The advisory lock below serializes
|
|
2957
|
+
writers on one checkout only; a fleet records from many. With a file per service, two teams' records
|
|
2958
|
+
touch two different paths and merge cleanly. Recording a service again replaces that service's file
|
|
2959
|
+
and nothing else; another service's file is never rewritten, and its pins never touched.
|
|
2960
|
+
|
|
2961
|
+
The record answers **one version of the checklist**: the one the feature asks now — `loam verify`
|
|
2962
|
+
(read and record alike), `tasks`, and `status`, `list` and `gate` for an in-flight feature all
|
|
2963
|
+
derive it, and the files that answered it are the ones that count. Only where no reader knows the
|
|
2964
|
+
current version (the frozen view of an archived feature) or no file answered it does the newest
|
|
2965
|
+
file decide (by `recorded`, a later service id breaking a same-day tie). Newest-by-date used to
|
|
2966
|
+
decide always, and `recorded` is a date: a service that recorded the current checklist lost a
|
|
2967
|
+
same-day tie to a service with a later id that had recorded an older one, so the current answers
|
|
2968
|
+
were dropped and every reader called the record stale. A file that answered a different version
|
|
2969
|
+
keeps its attestation in the list — `verify.checklist-forked` names it, on the recording run's
|
|
2970
|
+
output as well as the read view — but its answers do not count until that service records again.
|
|
2971
|
+
The one shared file used to settle this by rewriting: the last writer pruned everybody's answers to
|
|
2972
|
+
its own checklist. Per-service files are never rewritten by anyone else, so nothing can prune them,
|
|
2973
|
+
and counting an answer filed against another question set would let an id the feature no longer
|
|
2974
|
+
asks fill the slot of the question that replaced it.
|
|
2975
|
+
|
|
2976
|
+
**`features/<FEAT>/verification/` is loam's.** Every non-dot `*.yaml` in it is read as one
|
|
2977
|
+
service's attestation file — the file name is the claim about whose word it holds — so a file there
|
|
2978
|
+
that is not one (a hand-made `k6.yaml`, say) makes the record `record-unreadable`, exactly like a
|
|
2979
|
+
damaged attestation. Keep anything else beside the feature, not inside that directory.
|
|
2980
|
+
|
|
2981
|
+
**The one-file record is read forever.** A `verification.yaml` written before the directory existed
|
|
2982
|
+
— a federated record with several attestations in it, active or archived — reads exactly as it
|
|
2983
|
+
always did while no service has a file of its own. Once services do, it is read beside them: a
|
|
2984
|
+
service that records again supersedes its share of the one-file record (that attestation and its
|
|
2985
|
+
answers drop out of the union; the file itself is not rewritten), and the write that leaves nothing
|
|
2986
|
+
in it counting removes it. An all-at-once record (no `schema`, no attestations) never counts once a
|
|
2987
|
+
service has a file — its answers carry no commit attestation — so the first `--service` write
|
|
2988
|
+
removes it in the same commit and lists its answers in `discarded`. Answers a write takes off the
|
|
2989
|
+
disk that its new file does not carry are reported in `discarded`, tagged `unattested` (an
|
|
2990
|
+
all-at-once answer) or `off-checklist` (a claim the current checklist no longer asks). Nothing is
|
|
2991
|
+
dropped silently.
|
|
2992
|
+
|
|
2993
|
+
**The all-at-once form keeps the one file, by decision.** `--record` without `--service` answers the
|
|
2994
|
+
whole checklist from one place on one word, with no commit and no service — so there is no second
|
|
2995
|
+
writer for it to collide with and no service to name a file after, and every record it ever wrote
|
|
2996
|
+
(the example fleet's included) stays byte-for-byte what it is. There is one reader either way:
|
|
2997
|
+
`loam verify`, `status`, `list`, `tasks`, `gate` and `validate` all read the record through the same
|
|
2998
|
+
union, whichever files it is made of.
|
|
2999
|
+
|
|
3000
|
+
The write holds the same advisory lock archive does, and holds it from before the feature is
|
|
3001
|
+
resolved to after the record lands — two `--record` runs for different services serialize (the
|
|
3002
|
+
later one waits briefly, then writes its own file, so **both** attestations survive), and a record
|
|
3003
|
+
can no longer be written into a path an `archive` is renaming out from under it. The commit itself
|
|
3004
|
+
is staged beside the target and swapped in by one rename over the exact bytes the read found, so a
|
|
3005
|
+
kill mid-record leaves the previous record whole — never truncated YAML — and an edit that landed in
|
|
3006
|
+
between refuses instead of being buried. A write that also removes a superseded one-file record
|
|
3007
|
+
holds that removal to the same compare, and lands the service's own file first, so a kill between
|
|
3008
|
+
the two leaves a record that still reads whole:
|
|
2515
3009
|
|
|
2516
3010
|
- `docs-busy` — another writer held the lock longer than the record was willing to wait (5 s).
|
|
2517
3011
|
Nothing was read or written; re-run once it finishes. The contention runs both ways: `archive`,
|
|
@@ -2532,9 +3026,10 @@ Recording refuses rather than guesses, each under its own `--json` `error.code`:
|
|
|
2532
3026
|
- `repository-unavailable` — `--service` from a repo that declares none, or one whose commit cannot
|
|
2533
3027
|
be read. An attestation with nothing to bind to is not an attestation.
|
|
2534
3028
|
- **`record-federated`** — the legacy all-at-once form (`--record` with **no** `--service`) over a
|
|
2535
|
-
record that already carries attestations
|
|
2536
|
-
|
|
2537
|
-
|
|
3029
|
+
record that already carries attestations, in per-service files or in a one-file record. That form
|
|
3030
|
+
writes the whole record from one place, so it would put one unattested answer set over every
|
|
3031
|
+
other repository's evidence; loam refuses outright and names the attestors, and never silently
|
|
3032
|
+
migrates or merges. Re-run with `--service`.
|
|
2538
3033
|
- `record-unreadable` — see below.
|
|
2539
3034
|
|
|
2540
3035
|
Reading is unrestricted: `loam verify <FEAT> --service <id>` **without** `--record` is a pure lens
|
|
@@ -2543,12 +3038,26 @@ in `services`, which is how you find out which repositories still owe an attesta
|
|
|
2543
3038
|
|
|
2544
3039
|
The record is YAML, like the other data artifacts, and holds the claim text next to each verdict so
|
|
2545
3040
|
it reads without loam. Because it has to survive without loam, human edits are expected — and read
|
|
2546
|
-
back defensively. A file that exists but does not read as a record (
|
|
3041
|
+
back defensively. A record file that exists but does not read as a record (git conflict markers a
|
|
3042
|
+
merge left in it — named before the YAML parser sees them, since the parser either fails on a line
|
|
3043
|
+
that is not the problem or reads both sides as one plausible document; broken YAML; a shape missing
|
|
2547
3044
|
what the readers use — `summary` with its counts, `recorded`, `checklist`, claims each with an `id`
|
|
2548
|
-
and a known `verdict
|
|
3045
|
+
and a known `verdict`; or a `verification/<svc>.yaml` holding anything but that one service's
|
|
3046
|
+
attestation and answers) is **`record-unreadable`**: reported with the file and the line, and **never
|
|
2549
3047
|
overwritten**. It used to count as *no record at all*, which was the wrong direction on the one
|
|
2550
3048
|
artifact loam cannot reconstruct — "no record" invites writing a fresh one over evidence somebody's
|
|
2551
|
-
edit merely broke. Only a genuinely absent file is "no verification record".
|
|
3049
|
+
edit merely broke. Only a genuinely absent file is "no verification record".
|
|
3050
|
+
|
|
3051
|
+
The refusal names the ONE file that failed and the repair that is safe for it, because the two kinds
|
|
3052
|
+
of file call for opposite advice. A service's own `verification/<svc>.yaml` holds that service's
|
|
3053
|
+
word and nobody else's — after a merge, both sides of a conflict in it are that service's own
|
|
3054
|
+
attestations — so repairing it by hand, or deleting it and recording that service again from its
|
|
3055
|
+
repository, loses no other team's evidence. The one-file `verification.yaml` may hold every other
|
|
3056
|
+
service's attestation, so its advice is to repair it or **restore it from version control** (after a
|
|
3057
|
+
merge, the copy on the branch you merged into) and then record again — **never to delete it**. The
|
|
3058
|
+
old one-size advice, "delete it and record again", erased other teams' attestations when followed on
|
|
3059
|
+
a shared record, and the miscount advice, "record the answers again", sent the reader straight back
|
|
3060
|
+
into this refusal: `--record` will not write over a file it could not read. Both now end in a record. Extra keys a human adds
|
|
2552
3061
|
still pass through untouched. Every verdict names who answered it — `answered_by: runner` (a
|
|
2553
3062
|
cucumber report's green run, mechanical), `answered_by: external-runner` (a contract-test report's
|
|
2554
3063
|
operationId-matched green entry, mechanical too), or `answered_by: agent` (somebody's word about the
|
|
@@ -2618,7 +3127,7 @@ next `--record` makes it drift-checkable), warn `evidence.unresolved` (the cited
|
|
|
2618
3127
|
unsafe, not a regular file, or the cited line is past its end), `evidence.moved` (the file's digest
|
|
2619
3128
|
differs while the cited line still reads as pinned), `evidence.line-changed` (the cited line no
|
|
2620
3129
|
longer says what was recorded), `evidence.token-missing` (the file contained the token at the
|
|
2621
|
-
attested commit and no longer does) and `evidence.record-unreadable` (a verification
|
|
3130
|
+
attested commit and no longer does) and `evidence.record-unreadable` (a verification record file that
|
|
2622
3131
|
exists but cannot be read — none of its evidence was checked, and a silent skip would read as clean,
|
|
2623
3132
|
on the very file a hand edit to the pins most plausibly breaks). Archived records are deliberately
|
|
2624
3133
|
not linted — frozen history, and post-ship code drift is normal evolution. From the docs repo the
|
|
@@ -2628,13 +3137,18 @@ verdict in either direction, `attested` never silently becomes `verified`, and t
|
|
|
2628
3137
|
fail `validate`; `--strict` remains the CI lever it always was.
|
|
2629
3138
|
|
|
2630
3139
|
It lives inside the feature, so `archive` carries it into `features/archive/<FEAT>/` with everything
|
|
2631
|
-
else
|
|
3140
|
+
else — the `verification/` directory moves with the feature it sits in, and `unarchive` brings it
|
|
3141
|
+
back the same way; the frozen view reads it through the same union. `loam verify` re-run later compares `checklist` against the current digest and reports the
|
|
2632
3142
|
record as **stale** if the feature moved under it — while the feature is active. Once archived there
|
|
2633
3143
|
is no current checklist to be stale against (the merge itself moved the feature's operations into
|
|
2634
3144
|
the living OpenAPI, so a re-derived checklist could only disagree): `loam verify` on an archived
|
|
2635
3145
|
feature renders the record verbatim as **frozen history** (`frozen: true` under `--json`, no
|
|
2636
3146
|
staleness judgment), and `--record` refuses (`invalid-option` — there is no current checklist for
|
|
2637
|
-
the answers to answer): the record is about the words that shipped, and it stays that way.
|
|
3147
|
+
the answers to answer): the record is about the words that shipped, and it stays that way. One
|
|
3148
|
+
reader holds it to something all the same: `loam gate --since <ref>` grades an archived record
|
|
3149
|
+
against the checklist digest `archive` captured as the feature merged (the snapshot manifest's
|
|
3150
|
+
`checklist`, above), because the deploy question is whether what shipped was what was checked — a
|
|
3151
|
+
record that had gone stale before the archive is `stale` there, never `verified` again.
|
|
2638
3152
|
|
|
2639
3153
|
**Verification does not gate `archive`.** Coherence gates because loam computed it from the
|
|
2640
3154
|
documents; a verdict is an agent's word about code loam never read, and a gate in front of shipping
|
|
@@ -2643,6 +3157,72 @@ comes later, and an `unconfirmed` claim with a note is worth more there than a `
|
|
|
2643
3157
|
can back up. `loam verify` exits 0 either way; branch on `verified` in the `--json` payload, or on
|
|
2644
3158
|
`verdict` when you need to tell `attested` from `unverified`.
|
|
2645
3159
|
|
|
3160
|
+
### The same checklist read forwards (`loam tasks`)
|
|
3161
|
+
|
|
3162
|
+
`loam verify` answers *answered, and by whom*. `loam tasks <FEAT> [--service <id>] [--next]
|
|
3163
|
+
[--json]` answers the other half of the same question — *still open, at which step, and what fact
|
|
3164
|
+
closes it* — and it is the identical checklist read in the other direction. It is derived on every
|
|
3165
|
+
run from the feature's deltas, the generated suite and the verification record; it writes nothing,
|
|
3166
|
+
anywhere, ever, and it adds no artifact for a later run to disagree with.
|
|
3167
|
+
|
|
3168
|
+
**One unit per promise, and a unit's id is the claim's id.** Every claim the checklist derives is
|
|
3169
|
+
one work unit, under the same identity on both commands, so the thing to do and the answer that
|
|
3170
|
+
closes it can never be two different objects. One kind of unit has no claim behind it: an ADDED or
|
|
3171
|
+
MODIFIED requirement with no scenario under it asks the checklist nothing — there is no scenario to
|
|
3172
|
+
test — so `loam tasks` mints a unit of its own for it, of the kind requirement.described. That is
|
|
3173
|
+
the promise nobody has written acceptance criteria for yet, and it is where the loop starts.
|
|
3174
|
+
|
|
3175
|
+
**Where a unit stands** is its `at` field, and the four values are the steps of the loop this
|
|
3176
|
+
document already describes rather than a percentage: `describe` (the requirement carries no scenario
|
|
3177
|
+
at all, so there is nothing to generate a test from), `generate` (the scenario exists and this
|
|
3178
|
+
repository's generated suite does not carry its stamp yet), `implement` (everything else still
|
|
3179
|
+
open), `attested` (an agent's word confirmed the claim and no run has). It is spelled `at`
|
|
3180
|
+
deliberately, and not `stage` or `verdict`: both of those already mean something else on payloads an
|
|
3181
|
+
agent reads beside this one, and a third meaning for either word is how a consumer comes to branch
|
|
3182
|
+
on the wrong field.
|
|
3183
|
+
|
|
3184
|
+
**Done is a fact, not a tick.** A closed unit simply stops appearing, and exactly one thing closes
|
|
3185
|
+
one: a claim recorded `confirmed` in a verification record that is **not stale** against the current
|
|
3186
|
+
checklist. A stale record closes nothing — the words moved under the answers, which is the state
|
|
3187
|
+
`loam verify` already reports. An agent-attested scenario never vanishes either: it stays visible at
|
|
3188
|
+
`attested`, because attested is not verified, and a work list that hid the difference would be
|
|
3189
|
+
exactly the gate that teaches everyone the cheapest way past it is to say yes.
|
|
3190
|
+
|
|
3191
|
+
**The frontier** is the first ten open units in derived order, providers before consumers — ordered
|
|
3192
|
+
on the exact token join `c4.calls.token == api.exposes.operation`, never on a reading of prose.
|
|
3193
|
+
**The order is advice and never a lock**: nothing is assigned, nothing is reserved, and working out
|
|
3194
|
+
of order is legal and invisible to every other command.
|
|
3195
|
+
|
|
3196
|
+
**`--next` is the leaf**: the same packet with `units` narrowed to the frontier head, which is what
|
|
3197
|
+
an orchestrator hands a coding agent. Everything else — the counts, the frontier, the review
|
|
3198
|
+
notes — is left whole on purpose, so a cheap model handed one unit cannot read it as the whole job.
|
|
3199
|
+
The text view prints that leaf in full: the scenario's body verbatim and never reflowed (the bytes
|
|
3200
|
+
the digest was taken over), the digest itself — the same `@loam-digest-<16hex>` join key
|
|
3201
|
+
`loam gherkin` stamps and `loam verify --results` matches on — the `done` block whose `check` is the
|
|
3202
|
+
fact that closes the unit, and the literal follow-up command to run next, spelled out rather than
|
|
3203
|
+
left as a shape to fill in.
|
|
3204
|
+
|
|
3205
|
+
**`--service <id>` is a pure lens.** It narrows the units to one service's without binding a
|
|
3206
|
+
repository, and adds `waitingOn[]`: the open units in OTHER services that this service's units
|
|
3207
|
+
depend on. A team learns what it is blocked by from the same run that tells it what to do.
|
|
3208
|
+
|
|
3209
|
+
**What the payload says about what it could not see.** `stamps` is `read` or `unknown`, and
|
|
3210
|
+
`unknown` means the bound service repo's generated suite could not be read — could-not-look, never
|
|
3211
|
+
nothing-wrong, because a question loam was unable to ask must not read as one that came back clean.
|
|
3212
|
+
Beside it the payload carries `checklist` (the digest of the question set these units derive from),
|
|
3213
|
+
`record` (`absent`, `recorded`, `stale` or `unreadable`), `intent`, `services[]`,
|
|
3214
|
+
`size{requirements,scenarios,operations,messages,calls}` — the size of the DELTA and never of the
|
|
3215
|
+
open work, so that "this feature is small" and "this feature is nearly done" cannot become one
|
|
3216
|
+
sentence — `contracts{expose[],call[]}`, `counts{open,done,attested}` and `review[]`, the four
|
|
3217
|
+
instructions for re-deriving this answer and reviewing it, because a projection that says what to do
|
|
3218
|
+
must also say how to check it was read correctly. A broken `delta.likec4` or contract axis names
|
|
3219
|
+
what it could not read in `unreadable` and exits 1 with `ok: true` — `loam delta`'s own rule rather
|
|
3220
|
+
than a new one: a feeder nothing could read empties the packet, an empty packet reads as "nothing to
|
|
3221
|
+
do", so the envelope stays true while the exit code stops a pipeline building on it.
|
|
3222
|
+
|
|
3223
|
+
The command mints no finding or refusal code of its own, and `loam_tasks` serves the same projection
|
|
3224
|
+
read-only over MCP.
|
|
3225
|
+
|
|
2646
3226
|
## Operating at fleet scale
|
|
2647
3227
|
|
|
2648
3228
|
One docs repo, a hundred services, many teams. The repo scales the way any shared codebase does —
|
|
@@ -2794,11 +3374,14 @@ repairing anything:
|
|
|
2794
3374
|
|
|
2795
3375
|
And it reports the **agent surface**: which tool and profile the repo is wired for, how many command and skill
|
|
2796
3376
|
files it holds, how many a newer loam would lay down (`doctor.agent-files-missing`), and how many
|
|
2797
|
-
have fallen behind (`doctor.agent-files-stale`).
|
|
2798
|
-
`<!-- generated by loam vX.Y.Z -->` line every generated body opens with
|
|
3377
|
+
have fallen behind (`doctor.agent-files-stale`). A file has fallen behind when BOTH hold: its
|
|
3378
|
+
bytes, the `<!-- generated by loam vX.Y.Z -->` line every generated body opens with set aside, are
|
|
3379
|
+
not what this binary writes; and that stamp is missing or older than this binary. A stamp-only
|
|
3380
|
+
difference is not staleness — the stamp moves in every file at every release, and a warning that
|
|
3381
|
+
fires on every upgrade hides the release where a pointer's body really changed. A customized body
|
|
3382
|
+
under a current stamp is not staleness either: the stamp is the record that somebody reviewed it.
|
|
2799
3383
|
The digest manifest is what authorizes `init` to refresh an unchanged pointer; a different body is
|
|
2800
|
-
treated as human-owned and never overwritten.
|
|
2801
|
-
every edit around it is legitimate. Which deliveries a repo holds is asked of the files rather
|
|
3384
|
+
treated as human-owned and never overwritten. Which deliveries a repo holds is asked of the files rather
|
|
2802
3385
|
than of a record: `loam.json` stores tools and profile, not deliveries, and a repo initialized
|
|
2803
3386
|
`--no-skills` is not reported as missing the selected skills.
|
|
2804
3387
|
|
|
@@ -2901,6 +3484,7 @@ and publishes it wherever the fleet view reads:
|
|
|
2901
3484
|
"errors": 0,
|
|
2902
3485
|
"warnings": 2,
|
|
2903
3486
|
"sources": { "current": 1, "stale": 0, "unvouched": 0, "absent": 0 },
|
|
3487
|
+
"contentStale": 0,
|
|
2904
3488
|
"generatedAt": "2026-08-03T12:00:00Z"
|
|
2905
3489
|
}
|
|
2906
3490
|
```
|
|
@@ -2919,10 +3503,16 @@ loam validate --service "$SVC" --json | jq '{
|
|
|
2919
3503
|
unvouched: [.targets[0].findings[] | select(.code == "sources.unvouched")] | length,
|
|
2920
3504
|
absent: [.targets[0].findings[] | select(.code == "sources.absent")] | length
|
|
2921
3505
|
},
|
|
3506
|
+
contentStale: [.targets[0].findings[] | select(.code == "content.stale")] | length,
|
|
2922
3507
|
generatedAt: (now | todate)
|
|
2923
3508
|
}'
|
|
2924
3509
|
```
|
|
2925
3510
|
|
|
3511
|
+
`contentStale` counts the spec documents whose words moved after their vouch — the doc-side half of
|
|
3512
|
+
staleness, beside the code-side `sources.stale`. Unlike that half it is not owed to the service repo
|
|
3513
|
+
alone (the docs repo's own `validate --all` scorecard counts it too), but a summary that left it out
|
|
3514
|
+
would publish "0 stale" over a document nobody re-read.
|
|
3515
|
+
|
|
2926
3516
|
`generatedAt` is the CI's clock, never loam's — loam output carries no timestamps, by design, so the
|
|
2927
3517
|
same repo state always yields the same bytes. That is also what makes the timestamp the liveness
|
|
2928
3518
|
signal: the rule is that every service repo's CI publishes this summary on every run, so a summary
|
|
@@ -2947,9 +3537,10 @@ arrived. Per-service internals now live in `services/<svc>/model.likec4` **by de
|
|
|
2947
3537
|
where a second team adopting a service writes, so it adds nothing to this file. It is a default and
|
|
2948
3538
|
not a prohibition: the map MAY hold a service's interior, and sometimes must. A store a second
|
|
2949
3539
|
service will reach has to be declared where that consumer's project can see it — a per-service
|
|
2950
|
-
project is `architecture/` plus one model — so it is nested inside the owner's
|
|
2951
|
-
|
|
2952
|
-
|
|
3540
|
+
project is `architecture/` plus one model — so it is drawn ON THE MAP, nested inside the owner's
|
|
3541
|
+
element or beside the services, whichever the team prefers. Which of the two is nobody's business
|
|
3542
|
+
but the team's: "Private stores" above leaves the placement open and loam grades neither, so nothing
|
|
3543
|
+
here says a store must move. A delta spelling a parent in the legacy
|
|
2953
3544
|
title-join lands its container on the map too. What an extending model owns is what it DECLARES,
|
|
2954
3545
|
not everything below the service's element. `loam archive` routes rather than forbids — a delta's
|
|
2955
3546
|
additions nested under a service whose model extends the map, with the whole parent chain spelled
|
|
@@ -2991,7 +3582,7 @@ says what its architecture means, and two generated models of the same code disa
|
|
|
2991
3582
|
every run — a done-check built on diffing them would flap until somebody turned it off. What is
|
|
2992
3583
|
deterministic is the **question**: which files, in which grammar, bound to which existing elements;
|
|
2993
3584
|
which operations, which edges, which scenarios. loam owns the questions and the checking; an agent
|
|
2994
|
-
owns the reading. `sources` and
|
|
3585
|
+
owns the reading. `sources` and the verification record are where the answers are written down.
|
|
2995
3586
|
|
|
2996
3587
|
## Status
|
|
2997
3588
|
|