@ybotok/loam 0.2.0-alpha.7 → 0.2.0-alpha.9
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +1315 -1
- package/COMPARISON.md +91 -3
- package/CONTRIBUTING.md +37 -0
- package/MIGRATING-from-OpenSpec.md +40 -22
- package/QUICKSTART.md +181 -9
- package/README.md +256 -57
- package/ROADMAP.md +83 -7
- package/SCHEMA.md +569 -98
- package/WORKFLOW.md +272 -62
- package/dist/cli.js +26 -8
- package/dist/cli.js.map +1 -1
- package/dist/commands/adopt/adopt.js +5 -10
- package/dist/commands/adopt/adopt.js.map +1 -1
- package/dist/commands/archive/archive.js +15 -18
- package/dist/commands/archive/archive.js.map +1 -1
- package/dist/commands/archive/plan/contracts/asyncapi.js +27 -41
- package/dist/commands/archive/plan/contracts/asyncapi.js.map +1 -1
- package/dist/commands/archive/plan/contracts/consumers.js +56 -0
- package/dist/commands/archive/plan/contracts/consumers.js.map +1 -0
- package/dist/commands/archive/plan/contracts/openapi.js +25 -42
- package/dist/commands/archive/plan/contracts/openapi.js.map +1 -1
- package/dist/commands/archive/plan/contracts/verdict-lines.js +41 -0
- package/dist/commands/archive/plan/contracts/verdict-lines.js.map +1 -0
- package/dist/commands/archive/plan/gate.js +3 -7
- package/dist/commands/archive/plan/gate.js.map +1 -1
- package/dist/commands/archive/plan/landscape.js +4 -4
- package/dist/commands/archive/plan/landscape.js.map +1 -1
- package/dist/commands/archive/plan/refusal.js +8 -18
- package/dist/commands/archive/plan/refusal.js.map +1 -1
- package/dist/commands/archive/plan/specs.js +134 -25
- package/dist/commands/archive/plan/specs.js.map +1 -1
- package/dist/commands/archive/plan/state.js +1 -0
- package/dist/commands/archive/plan/state.js.map +1 -1
- package/dist/commands/archive/run.js +33 -11
- package/dist/commands/archive/run.js.map +1 -1
- package/dist/commands/archive/shipped.js +49 -0
- package/dist/commands/archive/shipped.js.map +1 -0
- package/dist/commands/context/context.js +4 -7
- package/dist/commands/context/context.js.map +1 -1
- package/dist/commands/context/print.js +2 -2
- package/dist/commands/context/print.js.map +1 -1
- package/dist/commands/delta/delta.js +44 -12
- package/dist/commands/delta/delta.js.map +1 -1
- package/dist/commands/delta/print.js +1 -1
- package/dist/commands/delta/print.js.map +1 -1
- package/dist/commands/dependencies.js +3 -7
- package/dist/commands/dependencies.js.map +1 -1
- package/dist/commands/diff/diff.js +20 -27
- package/dist/commands/diff/diff.js.map +1 -1
- package/dist/commands/explore.js +4 -8
- package/dist/commands/explore.js.map +1 -1
- package/dist/commands/gate/gate.js +45 -8
- package/dist/commands/gate/gate.js.map +1 -1
- package/dist/commands/gate/print.js +15 -3
- package/dist/commands/gate/print.js.map +1 -1
- package/dist/commands/gherkin/commit.js +13 -54
- package/dist/commands/gherkin/commit.js.map +1 -1
- package/dist/commands/gherkin/gherkin.js +36 -20
- package/dist/commands/gherkin/gherkin.js.map +1 -1
- package/dist/commands/gherkin/reconcile.js +10 -13
- package/dist/commands/gherkin/reconcile.js.map +1 -1
- package/dist/commands/init/first-hour.js +44 -0
- package/dist/commands/init/first-hour.js.map +1 -1
- package/dist/commands/init/init.js +18 -22
- package/dist/commands/init/init.js.map +1 -1
- package/dist/commands/list/campaign/campaign.js +4 -6
- package/dist/commands/list/campaign/campaign.js.map +1 -1
- package/dist/commands/list/json.js +0 -4
- package/dist/commands/list/json.js.map +1 -1
- package/dist/commands/list/list.js +38 -23
- package/dist/commands/list/list.js.map +1 -1
- package/dist/commands/list/print.js +38 -6
- package/dist/commands/list/print.js.map +1 -1
- package/dist/commands/list/suites/suites.js +140 -0
- package/dist/commands/list/suites/suites.js.map +1 -0
- package/dist/commands/list/views.js +28 -33
- package/dist/commands/list/views.js.map +1 -1
- package/dist/commands/mcp/dispatch.js +91 -36
- package/dist/commands/mcp/dispatch.js.map +1 -1
- package/dist/commands/mcp/mcp.js +56 -15
- package/dist/commands/mcp/mcp.js.map +1 -1
- package/dist/commands/mcp/serve.js.map +1 -1
- package/dist/commands/migrate-openspec/materialize/active.js +27 -22
- package/dist/commands/migrate-openspec/materialize/active.js.map +1 -1
- package/dist/commands/migrate-openspec/materialize/stage.js +4 -3
- package/dist/commands/migrate-openspec/materialize/stage.js.map +1 -1
- package/dist/commands/migrate-openspec/materialize/target.js +2 -2
- package/dist/commands/migrate-openspec/materialize/target.js.map +1 -1
- package/dist/commands/migrate-openspec/openspec/decisions.js +9 -14
- package/dist/commands/migrate-openspec/openspec/decisions.js.map +1 -1
- package/dist/commands/migrate-openspec/openspec/error.js +7 -8
- package/dist/commands/migrate-openspec/openspec/error.js.map +1 -1
- package/dist/commands/migrate-openspec/openspec/mapping.js +1 -9
- package/dist/commands/migrate-openspec/openspec/mapping.js.map +1 -1
- package/dist/commands/migrate-openspec/openspec/paths.js +3 -6
- package/dist/commands/migrate-openspec/openspec/paths.js.map +1 -1
- package/dist/commands/new/capability-template.js +118 -0
- package/dist/commands/new/capability-template.js.map +1 -0
- package/dist/commands/new/commit.js +8 -22
- package/dist/commands/new/commit.js.map +1 -1
- package/dist/commands/new/modifies.js +206 -0
- package/dist/commands/new/modifies.js.map +1 -0
- package/dist/commands/new/new.js +59 -14
- package/dist/commands/new/new.js.map +1 -1
- package/dist/commands/new/templates.js +48 -124
- package/dist/commands/new/templates.js.map +1 -1
- package/dist/commands/open.js +3 -7
- package/dist/commands/open.js.map +1 -1
- package/dist/commands/policy/gate.js +27 -0
- package/dist/commands/policy/gate.js.map +1 -1
- package/dist/commands/rebase/plan.js +2 -2
- package/dist/commands/rebase/plan.js.map +1 -1
- package/dist/commands/rebase/rebase.js +60 -35
- package/dist/commands/rebase/rebase.js.map +1 -1
- package/dist/commands/seed/commit.js +21 -25
- package/dist/commands/seed/commit.js.map +1 -1
- package/dist/commands/seed/seed.js +6 -9
- package/dist/commands/seed/seed.js.map +1 -1
- package/dist/commands/show/show.js +3 -7
- package/dist/commands/show/show.js.map +1 -1
- package/dist/commands/status/print.js +13 -4
- package/dist/commands/status/print.js.map +1 -1
- package/dist/commands/status/status.js +3 -7
- package/dist/commands/status/status.js.map +1 -1
- package/dist/commands/steps/steps.js +3 -7
- package/dist/commands/steps/steps.js.map +1 -1
- package/dist/commands/subsystem/edit.js +3 -5
- package/dist/commands/subsystem/edit.js.map +1 -1
- package/dist/commands/subsystem/move.js +5 -8
- package/dist/commands/subsystem/move.js.map +1 -1
- package/dist/commands/subsystem/query.js +2 -4
- package/dist/commands/subsystem/query.js.map +1 -1
- package/dist/commands/subsystem/subsystem.js +4 -7
- package/dist/commands/subsystem/subsystem.js.map +1 -1
- package/dist/commands/subsystem/sync.js +14 -41
- package/dist/commands/subsystem/sync.js.map +1 -1
- package/dist/commands/subsystem/txn/txn.js +17 -38
- package/dist/commands/subsystem/txn/txn.js.map +1 -1
- package/dist/commands/subsystem/txn/views.js +32 -5
- package/dist/commands/subsystem/txn/views.js.map +1 -1
- package/dist/commands/tasks/render.js +116 -0
- package/dist/commands/tasks/render.js.map +1 -0
- package/dist/commands/tasks/tasks.js +113 -0
- package/dist/commands/tasks/tasks.js.map +1 -0
- package/dist/commands/unarchive/manifest.js +6 -9
- package/dist/commands/unarchive/manifest.js.map +1 -1
- package/dist/commands/unarchive/restore.js +17 -28
- package/dist/commands/unarchive/restore.js.map +1 -1
- package/dist/commands/unarchive/unarchive.js +22 -30
- package/dist/commands/unarchive/unarchive.js.map +1 -1
- package/dist/commands/validate/arch-coverage.js +6 -15
- package/dist/commands/validate/arch-coverage.js.map +1 -1
- package/dist/commands/validate/checks/fleet-shape.js +4 -49
- package/dist/commands/validate/checks/fleet-shape.js.map +1 -1
- package/dist/commands/validate/checks/requirements.js +18 -8
- package/dist/commands/validate/checks/requirements.js.map +1 -1
- package/dist/commands/validate/checks/sources.js +6 -2
- package/dist/commands/validate/checks/sources.js.map +1 -1
- package/dist/commands/validate/feature.js +23 -19
- package/dist/commands/validate/feature.js.map +1 -1
- package/dist/commands/validate/fleet/landscape.js +23 -23
- package/dist/commands/validate/fleet/landscape.js.map +1 -1
- package/dist/commands/validate/fleet/map/attest.js +6 -10
- package/dist/commands/validate/fleet/map/attest.js.map +1 -1
- package/dist/commands/validate/fleet/map/bindings.js +1 -3
- package/dist/commands/validate/fleet/map/bindings.js.map +1 -1
- package/dist/commands/validate/fleet/map/usecases.js +5 -11
- package/dist/commands/validate/fleet/map/usecases.js.map +1 -1
- package/dist/commands/validate/fleet/obligations.js +1 -5
- package/dist/commands/validate/fleet/obligations.js.map +1 -1
- package/dist/commands/validate/fleet/scorecard/print.js +5 -1
- package/dist/commands/validate/fleet/scorecard/print.js.map +1 -1
- package/dist/commands/validate/fleet/scorecard/scorecard.js +17 -11
- package/dist/commands/validate/fleet/scorecard/scorecard.js.map +1 -1
- package/dist/commands/validate/fleet/usecases/usecases.js +6 -4
- package/dist/commands/validate/fleet/usecases/usecases.js.map +1 -1
- package/dist/commands/validate/fleet/views/fleet-project.js.map +1 -1
- package/dist/commands/validate/fleet/views/root-documents.js +8 -28
- package/dist/commands/validate/fleet/views/root-documents.js.map +1 -1
- package/dist/commands/validate/links/corpus.js +1 -4
- package/dist/commands/validate/links/corpus.js.map +1 -1
- package/dist/commands/validate/links/glossary.js.map +1 -1
- package/dist/commands/validate/report.js +5 -4
- package/dist/commands/validate/report.js.map +1 -1
- package/dist/commands/validate/scope.js +113 -0
- package/dist/commands/validate/scope.js.map +1 -0
- package/dist/commands/validate/service/api.js +16 -104
- package/dist/commands/validate/service/api.js.map +1 -1
- package/dist/commands/validate/service/contracts/generated.js +134 -0
- package/dist/commands/validate/service/contracts/generated.js.map +1 -0
- package/dist/commands/validate/service/events/events.js +1 -1
- package/dist/commands/validate/service/events/events.js.map +1 -1
- package/dist/commands/validate/service/events/message-producers.js +1 -1
- package/dist/commands/validate/service/events/message-producers.js.map +1 -1
- package/dist/commands/validate/service/evidence-pins.js +56 -10
- package/dist/commands/validate/service/evidence-pins.js.map +1 -1
- package/dist/commands/validate/service/service.js +16 -26
- package/dist/commands/validate/service/service.js.map +1 -1
- package/dist/commands/validate/service/specs.js +31 -38
- package/dist/commands/validate/service/specs.js.map +1 -1
- package/dist/commands/validate/service/spine.js +8 -11
- package/dist/commands/validate/service/spine.js.map +1 -1
- package/dist/commands/validate/service/usecases/usecases.js +3 -3
- package/dist/commands/validate/service/usecases/usecases.js.map +1 -1
- package/dist/commands/validate/validate.js +44 -26
- package/dist/commands/validate/validate.js.map +1 -1
- package/dist/commands/verify/cross/diff.js +2 -6
- package/dist/commands/verify/cross/diff.js.map +1 -1
- package/dist/commands/verify/evidence/contract.js +1 -1
- package/dist/commands/verify/evidence/contract.js.map +1 -1
- package/dist/commands/verify/evidence/read.js +12 -3
- package/dist/commands/verify/evidence/read.js.map +1 -1
- package/dist/commands/verify/frozen.js +45 -23
- package/dist/commands/verify/frozen.js.map +1 -1
- package/dist/commands/verify/record.js +69 -17
- package/dist/commands/verify/record.js.map +1 -1
- package/dist/commands/verify/report.js +27 -44
- package/dist/commands/verify/report.js.map +1 -1
- package/dist/commands/verify/results.js +112 -30
- package/dist/commands/verify/results.js.map +1 -1
- package/dist/commands/verify/verify.js +54 -51
- package/dist/commands/verify/verify.js.map +1 -1
- package/dist/commands/vouch/pack/pack.js +2 -2
- package/dist/commands/vouch/pack/pack.js.map +1 -1
- package/dist/commands/vouch/pack/print.js +1 -6
- package/dist/commands/vouch/pack/print.js.map +1 -1
- package/dist/commands/vouch/pack/sections.js +1 -2
- package/dist/commands/vouch/pack/sections.js.map +1 -1
- package/dist/commands/vouch/run.js +26 -81
- package/dist/commands/vouch/run.js.map +1 -1
- package/dist/commands/vouch/sample/plan.js +20 -34
- package/dist/commands/vouch/sample/plan.js.map +1 -1
- package/dist/commands/vouch/vet/verify.js +39 -7
- package/dist/commands/vouch/vet/verify.js.map +1 -1
- package/dist/commands/vouch/vouch.js +10 -17
- package/dist/commands/vouch/vouch.js.map +1 -1
- package/dist/core/agent/agents-md/command-map.js +56 -101
- package/dist/core/agent/agents-md/command-map.js.map +1 -1
- package/dist/core/agent/agents-md/map/explain.js +4 -3
- package/dist/core/agent/agents-md/map/explain.js.map +1 -1
- package/dist/core/agent/agents-md/map/lenses/context.js +4 -3
- package/dist/core/agent/agents-md/map/lenses/context.js.map +1 -1
- package/dist/core/agent/agents-md/map/lenses/diff.js +33 -20
- package/dist/core/agent/agents-md/map/lenses/diff.js.map +1 -1
- package/dist/core/agent/agents-md/map/lenses/gate.js +13 -5
- package/dist/core/agent/agents-md/map/lenses/gate.js.map +1 -1
- package/dist/core/agent/agents-md/map/mcp.js +10 -7
- package/dist/core/agent/agents-md/map/mcp.js.map +1 -1
- package/dist/core/agent/agents-md/map/status.js +9 -3
- package/dist/core/agent/agents-md/map/status.js.map +1 -1
- package/dist/core/agent/agents-md/map/subsystem.js +4 -3
- package/dist/core/agent/agents-md/map/subsystem.js.map +1 -1
- package/dist/core/agent/agents-md/map/usecases.js +24 -6
- package/dist/core/agent/agents-md/map/usecases.js.map +1 -1
- package/dist/core/agent/agents-md/refusals.js +32 -18
- package/dist/core/agent/agents-md/refusals.js.map +1 -1
- package/dist/core/agent/agents-md.js +9 -3
- package/dist/core/agent/agents-md.js.map +1 -1
- package/dist/core/agent/agents-stamp.js +10 -0
- package/dist/core/agent/agents-stamp.js.map +1 -1
- package/dist/core/agent/scaffold.js +10 -15
- package/dist/core/agent/scaffold.js.map +1 -1
- package/dist/core/agent/tools/dialects.js +1 -0
- package/dist/core/agent/tools/dialects.js.map +1 -1
- package/dist/core/agent/tools/registry.js +41 -76
- package/dist/core/agent/tools/registry.js.map +1 -1
- package/dist/core/agent/workflows/adopt.js +5 -3
- package/dist/core/agent/workflows/adopt.js.map +1 -1
- package/dist/core/agent/workflows/check.js +20 -16
- package/dist/core/agent/workflows/check.js.map +1 -1
- package/dist/core/agent/workflows/closing.js +54 -21
- package/dist/core/agent/workflows/closing.js.map +1 -1
- package/dist/core/agent/workflows/feature.js +79 -11
- package/dist/core/agent/workflows/feature.js.map +1 -1
- package/dist/core/agent/workflows/implement.js +91 -13
- package/dist/core/agent/workflows/implement.js.map +1 -1
- package/dist/core/agent/workflows/reference/authoring.js +3 -1
- package/dist/core/agent/workflows/reference/authoring.js.map +1 -1
- package/dist/core/agent/workflows/reference/codes.js +8 -6
- package/dist/core/agent/workflows/reference/codes.js.map +1 -1
- package/dist/core/agent/workflows/reference/done-check.js +37 -15
- package/dist/core/agent/workflows/reference/done-check.js.map +1 -1
- package/dist/core/agent/workflows/reference/reference.js.map +1 -1
- package/dist/core/agent/workflows/reference/spine.js +50 -18
- package/dist/core/agent/workflows/reference/spine.js.map +1 -1
- package/dist/core/asyncapi/baseline/plan.js +14 -81
- package/dist/core/asyncapi/baseline/plan.js.map +1 -1
- package/dist/core/asyncapi/digest.js +30 -25
- package/dist/core/asyncapi/digest.js.map +1 -1
- package/dist/core/asyncapi/merge/components.js +5 -4
- package/dist/core/asyncapi/merge/components.js.map +1 -1
- package/dist/core/asyncapi/merge/error.js +7 -2
- package/dist/core/asyncapi/merge/error.js.map +1 -1
- package/dist/core/asyncapi/merge/markers.js +30 -50
- package/dist/core/asyncapi/merge/markers.js.map +1 -1
- package/dist/core/asyncapi/merge/merge.js +31 -53
- package/dist/core/asyncapi/merge/merge.js.map +1 -1
- package/dist/core/asyncapi/merge/pin.js +10 -36
- package/dist/core/asyncapi/merge/pin.js.map +1 -1
- package/dist/core/asyncapi/model.js.map +1 -1
- package/dist/core/asyncapi/read.js +36 -12
- package/dist/core/asyncapi/read.js.map +1 -1
- package/dist/core/brief/brief.js +6 -18
- package/dist/core/brief/brief.js.map +1 -1
- package/dist/core/brief/landscape.js +8 -12
- package/dist/core/brief/landscape.js.map +1 -1
- package/dist/core/brief/map/extend.js +8 -5
- package/dist/core/brief/map/extend.js.map +1 -1
- package/dist/core/brief/map/owed.js +8 -26
- package/dist/core/brief/map/owed.js.map +1 -1
- package/dist/core/brief/targets.js +15 -5
- package/dist/core/brief/targets.js.map +1 -1
- package/dist/core/c4/likec4.js +147 -16
- package/dist/core/c4/likec4.js.map +1 -1
- package/dist/core/c4/locate/unlocated.js +150 -0
- package/dist/core/c4/locate/unlocated.js.map +1 -0
- package/dist/core/c4/parsed/values.js +54 -1
- package/dist/core/c4/parsed/values.js.map +1 -1
- package/dist/core/c4/prefetch/fleet.js +1 -9
- package/dist/core/c4/prefetch/fleet.js.map +1 -1
- package/dist/core/c4/project/documents.js +31 -6
- package/dist/core/c4/project/documents.js.map +1 -1
- package/dist/core/c4/project/load.js +6 -17
- package/dist/core/c4/project/load.js.map +1 -1
- package/dist/core/c4/resolve/external.js +51 -0
- package/dist/core/c4/resolve/external.js.map +1 -0
- package/dist/core/c4/resolve/service.js +25 -1
- package/dist/core/c4/resolve/service.js.map +1 -1
- package/dist/core/c4/seed/template.js +15 -1
- package/dist/core/c4/seed/template.js.map +1 -1
- package/dist/core/c4/service-model/shape.js +2 -22
- package/dist/core/c4/service-model/shape.js.map +1 -1
- package/dist/core/c4/source-mask.js +288 -4
- package/dist/core/c4/source-mask.js.map +1 -1
- package/dist/core/c4/source-scan.js +203 -147
- package/dist/core/c4/source-scan.js.map +1 -1
- package/dist/core/c4/splice/authored-source.js +19 -0
- package/dist/core/c4/splice/authored-source.js.map +1 -1
- package/dist/core/c4/splice/delta-blocks.js +5 -18
- package/dist/core/c4/splice/delta-blocks.js.map +1 -1
- package/dist/core/c4/splice/landscape-merge.js +13 -8
- package/dist/core/c4/splice/landscape-merge.js.map +1 -1
- package/dist/core/c4/splice/model/merge.js +10 -6
- package/dist/core/c4/splice/model/merge.js.map +1 -1
- package/dist/core/c4/splice/placement.js +108 -18
- package/dist/core/c4/splice/placement.js.map +1 -1
- package/dist/core/c4/workspace.js +13 -22
- package/dist/core/c4/workspace.js.map +1 -1
- package/dist/core/capabilities/capabilities.js +6 -6
- package/dist/core/capabilities/capabilities.js.map +1 -1
- package/dist/core/capabilities/delta/doc.js +7 -10
- package/dist/core/capabilities/delta/doc.js.map +1 -1
- package/dist/core/capabilities/findings.js +15 -20
- package/dist/core/capabilities/findings.js.map +1 -1
- package/dist/core/capabilities/realizes/findings.js +12 -19
- package/dist/core/capabilities/realizes/findings.js.map +1 -1
- package/dist/core/capabilities/realizes/join.js +0 -20
- package/dist/core/capabilities/realizes/join.js.map +1 -1
- package/dist/core/capabilities/rollup.js +6 -0
- package/dist/core/capabilities/rollup.js.map +1 -1
- package/dist/core/capabilities/tree.js +6 -28
- package/dist/core/capabilities/tree.js.map +1 -1
- package/dist/core/capabilities/usecase-join.js +3 -13
- package/dist/core/capabilities/usecase-join.js.map +1 -1
- package/dist/core/coherence/authoring/scaffold.js +11 -3
- package/dist/core/coherence/authoring/scaffold.js.map +1 -1
- package/dist/core/coherence/authoring/sentinels.js +2 -1
- package/dist/core/coherence/authoring/sentinels.js.map +1 -1
- package/dist/core/coherence/coherence.js +30 -16
- package/dist/core/coherence/coherence.js.map +1 -1
- package/dist/core/coherence/consumed/operations.js +70 -0
- package/dist/core/coherence/consumed/operations.js.map +1 -0
- package/dist/core/coherence/consumed/refusal.js +38 -0
- package/dist/core/coherence/consumed/refusal.js.map +1 -0
- package/dist/core/coherence/declared.js +60 -34
- package/dist/core/coherence/declared.js.map +1 -1
- package/dist/core/coherence/events/declared.js +44 -59
- package/dist/core/coherence/events/declared.js.map +1 -1
- package/dist/core/coherence/events/events.js +119 -8
- package/dist/core/coherence/events/events.js.map +1 -1
- package/dist/core/coherence/events/grades.js +31 -8
- package/dist/core/coherence/events/grades.js.map +1 -1
- package/dist/core/coherence/events/lookups.js +14 -82
- package/dist/core/coherence/events/lookups.js.map +1 -1
- package/dist/core/coherence/lookups.js +18 -78
- package/dist/core/coherence/lookups.js.map +1 -1
- package/dist/core/coherence/retired/archived.js +285 -0
- package/dist/core/coherence/retired/archived.js.map +1 -0
- package/dist/core/coherence/retired/event-pair.js +62 -0
- package/dist/core/coherence/retired/event-pair.js.map +1 -0
- package/dist/core/coherence/retired/merged.js +79 -0
- package/dist/core/coherence/retired/merged.js.map +1 -0
- package/dist/core/coherence/retired/retirement.js +49 -0
- package/dist/core/coherence/retired/retirement.js.map +1 -0
- package/dist/core/coherence/retired/revival/arrivals.js +131 -0
- package/dist/core/coherence/retired/revival/arrivals.js.map +1 -0
- package/dist/core/coherence/retired/revival/revived.js +297 -0
- package/dist/core/coherence/retired/revival/revived.js.map +1 -0
- package/dist/core/consumers/after.js +113 -0
- package/dist/core/consumers/after.js.map +1 -0
- package/dist/core/consumers/consumers.js +296 -0
- package/dist/core/consumers/consumers.js.map +1 -0
- package/dist/core/consumers/current.js +211 -0
- package/dist/core/consumers/current.js.map +1 -0
- package/dist/core/consumers/scan.js +46 -0
- package/dist/core/consumers/scan.js.map +1 -0
- package/dist/core/consumers/state.js +27 -0
- package/dist/core/consumers/state.js.map +1 -0
- package/dist/core/delta/delta.js +1 -1
- package/dist/core/delta/delta.js.map +1 -1
- package/dist/core/deployment/delta.js +3 -6
- package/dist/core/deployment/delta.js.map +1 -1
- package/dist/core/diff/base-git.js +94 -12
- package/dist/core/diff/base-git.js.map +1 -1
- package/dist/core/diff/base-state.js +28 -16
- package/dist/core/diff/base-state.js.map +1 -1
- package/dist/core/diff/joins.js +136 -0
- package/dist/core/diff/joins.js.map +1 -0
- package/dist/core/diff/scope/changed-paths.js +22 -6
- package/dist/core/diff/scope/changed-paths.js.map +1 -1
- package/dist/core/diff/scope/edges.js +82 -0
- package/dist/core/diff/scope/edges.js.map +1 -0
- package/dist/core/diff/semantic.js +100 -31
- package/dist/core/diff/semantic.js.map +1 -1
- package/dist/core/doctor/agents.js +34 -18
- package/dist/core/doctor/agents.js.map +1 -1
- package/dist/core/doctor/report.js.map +1 -1
- package/dist/core/document/apply.js +10 -2
- package/dist/core/document/apply.js.map +1 -1
- package/dist/core/document/frontmatter.js +55 -0
- package/dist/core/document/frontmatter.js.map +1 -1
- package/dist/core/document/parse.js +29 -6
- package/dist/core/document/parse.js.map +1 -1
- package/dist/core/document/scenarios.js +17 -0
- package/dist/core/document/scenarios.js.map +1 -1
- package/dist/core/document/spec.js +4 -11
- package/dist/core/document/spec.js.map +1 -1
- package/dist/core/envelope/config.js +10 -0
- package/dist/core/envelope/config.js.map +1 -1
- package/dist/core/envelope/json.js +34 -0
- package/dist/core/envelope/json.js.map +1 -1
- package/dist/core/explain/families.js +17 -12
- package/dist/core/explain/families.js.map +1 -1
- package/dist/core/explain/refusals.js +3 -3
- package/dist/core/explain/refusals.js.map +1 -1
- package/dist/core/explain/terms.js +18 -9
- package/dist/core/explain/terms.js.map +1 -1
- package/dist/core/explore/explore.js +2 -7
- package/dist/core/explore/explore.js.map +1 -1
- package/dist/core/fleet-context.js +34 -95
- package/dist/core/fleet-context.js.map +1 -1
- package/dist/core/gate/checks.js +5 -2
- package/dist/core/gate/checks.js.map +1 -1
- package/dist/core/gate/partners.js +4 -2
- package/dist/core/gate/partners.js.map +1 -1
- package/dist/core/gate/report.js.map +1 -1
- package/dist/core/gate/verification.js +75 -12
- package/dist/core/gate/verification.js.map +1 -1
- package/dist/core/gherkin/stale.js +13 -37
- package/dist/core/gherkin/stale.js.map +1 -1
- package/dist/core/glossary/tree.js +6 -19
- package/dist/core/glossary/tree.js.map +1 -1
- package/dist/core/kernel/concurrency.js +19 -0
- package/dist/core/kernel/concurrency.js.map +1 -1
- package/dist/core/kernel/records.js +11 -0
- package/dist/core/kernel/records.js.map +1 -1
- package/dist/core/links/backlinks.js +1 -3
- package/dist/core/links/backlinks.js.map +1 -1
- package/dist/core/links/corpus.js +5 -36
- package/dist/core/links/corpus.js.map +1 -1
- package/dist/core/mcp/argv.js +101 -0
- package/dist/core/mcp/argv.js.map +1 -1
- package/dist/core/mcp/author-tools.js +2 -1
- package/dist/core/mcp/author-tools.js.map +1 -1
- package/dist/core/mcp/protocol.js +2 -2
- package/dist/core/mcp/protocol.js.map +1 -1
- package/dist/core/mcp/tools.js +43 -33
- package/dist/core/mcp/tools.js.map +1 -1
- package/dist/core/openapi/baseline/gate.js +53 -12
- package/dist/core/openapi/baseline/gate.js.map +1 -1
- package/dist/core/openapi/baseline/plan.js +22 -8
- package/dist/core/openapi/baseline/plan.js.map +1 -1
- package/dist/core/openapi/baseline/record.js +0 -1
- package/dist/core/openapi/baseline/record.js.map +1 -1
- package/dist/core/openapi/contract-text.js +33 -0
- package/dist/core/openapi/contract-text.js.map +1 -0
- package/dist/core/openapi/contract-tree.js +56 -0
- package/dist/core/openapi/contract-tree.js.map +1 -0
- package/dist/core/openapi/depth.js +38 -24
- package/dist/core/openapi/depth.js.map +1 -1
- package/dist/core/openapi/digest.js +52 -2
- package/dist/core/openapi/digest.js.map +1 -1
- package/dist/core/openapi/doc.js +12 -4
- package/dist/core/openapi/doc.js.map +1 -1
- package/dist/core/openapi/merge/components.js +25 -60
- package/dist/core/openapi/merge/components.js.map +1 -1
- package/dist/core/openapi/merge/error.js +3 -13
- package/dist/core/openapi/merge/error.js.map +1 -1
- package/dist/core/openapi/merge/markers.js +26 -30
- package/dist/core/openapi/merge/markers.js.map +1 -1
- package/dist/core/openapi/merge/merge.js +26 -52
- package/dist/core/openapi/merge/merge.js.map +1 -1
- package/dist/core/openapi/merge/pin.js +15 -35
- package/dist/core/openapi/merge/pin.js.map +1 -1
- package/dist/core/openspec/decide/verdict.js.map +1 -1
- package/dist/core/openspec/inventory.js +4 -5
- package/dist/core/openspec/inventory.js.map +1 -1
- package/dist/core/openspec/model/mapping.js +30 -28
- package/dist/core/openspec/model/mapping.js.map +1 -1
- package/dist/core/openspec/model/model.js +12 -7
- package/dist/core/openspec/model/model.js.map +1 -1
- package/dist/core/openspec/scan/changes.js +10 -4
- package/dist/core/openspec/scan/changes.js.map +1 -1
- package/dist/core/openspec/scan/config.js +7 -16
- package/dist/core/openspec/scan/config.js.map +1 -1
- package/dist/core/openspec/scan/shape.js +63 -19
- package/dist/core/openspec/scan/shape.js.map +1 -1
- package/dist/core/openspec/scan/walk.js +21 -38
- package/dist/core/openspec/scan/walk.js.map +1 -1
- package/dist/core/pack/living.js +2 -16
- package/dist/core/pack/living.js.map +1 -1
- package/dist/core/provenance/findings.js +48 -12
- package/dist/core/provenance/findings.js.map +1 -1
- package/dist/core/provenance/git.js +33 -57
- package/dist/core/provenance/git.js.map +1 -1
- package/dist/core/provenance/gitq/moves.js +71 -114
- package/dist/core/provenance/gitq/moves.js.map +1 -1
- package/dist/core/provenance/gitq/vouched-ancestor.js +2 -2
- package/dist/core/provenance/gitq/vouched-ancestor.js.map +1 -1
- package/dist/core/provenance/sources.js +23 -6
- package/dist/core/provenance/sources.js.map +1 -1
- package/dist/core/provenance/stamp.js +5 -17
- package/dist/core/provenance/stamp.js.map +1 -1
- package/dist/core/repo/entries.js.map +1 -1
- package/dist/core/repo/repo.js +24 -14
- package/dist/core/repo/repo.js.map +1 -1
- package/dist/core/repo/tree/find.js +12 -3
- package/dist/core/repo/tree/find.js.map +1 -1
- package/dist/core/repo/tree/fs.js +27 -1
- package/dist/core/repo/tree/fs.js.map +1 -1
- package/dist/core/repo/tree/walk.js +8 -30
- package/dist/core/repo/tree/walk.js.map +1 -1
- package/dist/core/results.js +47 -12
- package/dist/core/results.js.map +1 -1
- package/dist/core/staging/commit.js +10 -12
- package/dist/core/staging/commit.js.map +1 -1
- package/dist/core/staging/lock.js +74 -12
- package/dist/core/staging/lock.js.map +1 -1
- package/dist/core/staging/recovery/intent.js +2 -5
- package/dist/core/staging/recovery/intent.js.map +1 -1
- package/dist/core/staging/recovery/recover.js +60 -1
- package/dist/core/staging/recovery/recover.js.map +1 -1
- package/dist/core/staging/snapshot/manifest.js +152 -0
- package/dist/core/staging/snapshot/manifest.js.map +1 -0
- package/dist/core/staging/{snapshot.js → snapshot/snapshot.js} +10 -34
- package/dist/core/staging/snapshot/snapshot.js.map +1 -0
- package/dist/core/status/actions/execution.js +11 -0
- package/dist/core/status/actions/execution.js.map +1 -1
- package/dist/core/status/feature/artifacts.js +2 -2
- package/dist/core/status/feature/artifacts.js.map +1 -1
- package/dist/core/status/feature/feature.js +5 -11
- package/dist/core/status/feature/feature.js.map +1 -1
- package/dist/core/status/fleet/fleet.js +6 -2
- package/dist/core/status/fleet/fleet.js.map +1 -1
- package/dist/core/status/fleet/next.js +26 -3
- package/dist/core/status/fleet/next.js.map +1 -1
- package/dist/core/status/report.js.map +1 -1
- package/dist/core/status/verification.js +30 -15
- package/dist/core/status/verification.js.map +1 -1
- package/dist/core/tasks/packet.js +174 -0
- package/dist/core/tasks/packet.js.map +1 -0
- package/dist/core/tasks/record.js +43 -0
- package/dist/core/tasks/record.js.map +1 -0
- package/dist/core/tasks/sources.js +181 -0
- package/dist/core/tasks/sources.js.map +1 -0
- package/dist/core/tasks/stamps.js +76 -0
- package/dist/core/tasks/stamps.js.map +1 -0
- package/dist/core/tasks/unit.js +0 -0
- package/dist/core/tasks/unit.js.map +1 -0
- package/dist/core/usecases/capability.js +11 -14
- package/dist/core/usecases/capability.js.map +1 -1
- package/dist/core/usecases/delta/flows.js +5 -7
- package/dist/core/usecases/delta/flows.js.map +1 -1
- package/dist/core/usecases/fleet.js +178 -31
- package/dist/core/usecases/fleet.js.map +1 -1
- package/dist/core/usecases/operations.js +5 -4
- package/dist/core/usecases/operations.js.map +1 -1
- package/dist/core/usecases/service/flows.js +9 -6
- package/dist/core/usecases/service/flows.js.map +1 -1
- package/dist/core/usecases/suites/participants.js +83 -0
- package/dist/core/usecases/suites/participants.js.map +1 -0
- package/dist/core/usecases/suites/suites.js +45 -0
- package/dist/core/usecases/suites/suites.js.map +1 -0
- package/dist/core/verify/answers.js +12 -5
- package/dist/core/verify/answers.js.map +1 -1
- package/dist/core/verify/build.js +102 -82
- package/dist/core/verify/build.js.map +1 -1
- package/dist/core/verify/checklist.js +3 -0
- package/dist/core/verify/checklist.js.map +1 -1
- package/dist/core/verify/location.js +67 -0
- package/dist/core/verify/location.js.map +1 -0
- package/dist/core/verify/pins/lint.js +3 -2
- package/dist/core/verify/pins/lint.js.map +1 -1
- package/dist/core/verify/pins/pin.js +1 -1
- package/dist/core/verify/pins/pin.js.map +1 -1
- package/dist/core/verify/read/file.js +224 -0
- package/dist/core/verify/read/file.js.map +1 -0
- package/dist/core/verify/{file.js → read/shape.js} +16 -104
- package/dist/core/verify/read/shape.js.map +1 -0
- package/dist/core/verify/read/union.js +105 -0
- package/dist/core/verify/read/union.js.map +1 -0
- package/dist/core/verify/record.js +35 -12
- package/dist/core/verify/record.js.map +1 -1
- package/dist/core/verify/store/commit.js +40 -30
- package/dist/core/verify/store/commit.js.map +1 -1
- package/dist/core/verify/store/render.js +8 -4
- package/dist/core/verify/store/render.js.map +1 -1
- package/dist/core/vocabulary/issue.js +1 -0
- package/dist/core/vocabulary/issue.js.map +1 -1
- package/dist/core/vocabulary/maturity.js +12 -2
- package/dist/core/vocabulary/maturity.js.map +1 -1
- package/examples/README.md +21 -6
- package/examples/docs/architecture/landscape.likec4 +8 -0
- package/examples/docs/architecture/usecases/order-notification.likec4 +6 -2
- package/examples/docs/features/FEAT-101-payment-splitting/specs/payment-service/asyncapi.yaml +11 -8
- package/examples/docs/features/FEAT-101-payment-splitting/specs/payment-split-service/openapi.yaml +6 -6
- package/examples/docs/features/archive/FEAT-088-refunds/specs/payment-service/openapi.yaml +13 -10
- package/examples/docs/services/order-service/asyncapi.yaml +3 -2
- package/examples/docs/services/platform/notification-service/asyncapi.yaml +2 -1
- package/package.json +1 -1
- package/dist/commands/validate/checks/vocabulary.js +0 -16
- package/dist/commands/validate/checks/vocabulary.js.map +0 -1
- package/dist/core/agent/agents-md/artifacts.js +0 -221
- package/dist/core/agent/agents-md/artifacts.js.map +0 -1
- package/dist/core/agent/agents-md/cycle.js +0 -180
- package/dist/core/agent/agents-md/cycle.js.map +0 -1
- package/dist/core/agent/agents-md/shipped/archive-gate.js +0 -98
- package/dist/core/agent/agents-md/shipped/archive-gate.js.map +0 -1
- package/dist/core/agent/agents-md/spine.js +0 -40
- package/dist/core/agent/agents-md/spine.js.map +0 -1
- package/dist/core/coherence/events/merged.js +0 -40
- package/dist/core/coherence/events/merged.js.map +0 -1
- package/dist/core/diff/current-state.js +0 -96
- package/dist/core/diff/current-state.js.map +0 -1
- package/dist/core/diff/victims.js +0 -256
- package/dist/core/diff/victims.js.map +0 -1
- package/dist/core/staging/snapshot.js.map +0 -1
- package/dist/core/verify/file.js.map +0 -1
package/SCHEMA.md
CHANGED
|
@@ -89,24 +89,25 @@ docs/
|
|
|
89
89
|
delta.likec4 C4 delta (architecture) [authored]
|
|
90
90
|
specs/<svc>/spec.md requirement delta (ADDED/MODIFIED/REMOVED + scenarios) [authored]
|
|
91
91
|
specs/<svc>/arch.spec.md architectural requirement delta, same algebra [authored]
|
|
92
|
-
specs/<svc>/openapi.yaml the endpoints
|
|
93
|
-
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]
|
|
94
94
|
capabilities/<cap>/spec.md capability delta, same requirement grammar [authored]
|
|
95
95
|
glossary/<term>.md a word this feature brings with it; create-only [authored]
|
|
96
96
|
usecases/<name>.likec4 a FLOW this feature brings; views-only, create-only [authored]
|
|
97
97
|
adrs/NNNN-*.md feature-level decisions [authored]
|
|
98
|
-
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]
|
|
99
100
|
features/archive/<FEAT>/ a shipped change — the same files, plus:
|
|
100
|
-
.loam-before/ the bytes the merge overwrote
|
|
101
|
+
.loam-before/ the bytes the merge overwrote, and the checklist it shipped with [written by loam]
|
|
101
102
|
.loam-lock transient: one writer at a time [archive/unarchive only]
|
|
102
103
|
.loam-commit transient: the commit intent journal [archive/unarchive only]
|
|
103
104
|
```
|
|
104
105
|
|
|
105
106
|
There is no `flows/` tree, and there will not be one. A cross-service **interaction flow** is a
|
|
106
107
|
`dynamic view` under `architecture/` — the one place that already holds every cross-service edge —
|
|
107
|
-
tagged with the capability it realizes
|
|
108
|
-
service's directory, and the view's steps join to the
|
|
109
|
-
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.
|
|
110
111
|
|
|
111
112
|
A flow that stays INSIDE one service — the hop sequence behind an arch.spec.md requirement whose
|
|
112
113
|
`Covers:` names containers — cannot live under `architecture/`: loam parses that directory as its
|
|
@@ -147,6 +148,41 @@ would leave behind — so a flow may draw a hop into a service the feature's own
|
|
|
147
148
|
and a hop into an element the merge does NOT land is refused at plan time rather than archived into
|
|
148
149
|
a map the next reader's `loam validate --all` fails on.
|
|
149
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
|
+
|
|
150
186
|
### Two shapes of a service model
|
|
151
187
|
|
|
152
188
|
`services/<…>/<svc>/model.likec4` comes in two shapes, and **the file's own grammar decides which**
|
|
@@ -708,7 +744,10 @@ so running loam from a subdirectory works; `loam init` writes the file where it
|
|
|
708
744
|
this repo, digests that document and compares it with the committed
|
|
709
745
|
`services/<id>/openapi.yaml`: `openapi.generated-stale` (warn) when they differ,
|
|
710
746
|
`contracts.source-missing` (error) when nothing is there, `contracts.source-invalid` (error) when
|
|
711
|
-
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.
|
|
712
751
|
|
|
713
752
|
**loam reads it and never writes it.** The copy into the docs repo stays a human `cp` reviewed in
|
|
714
753
|
a pull request — that is what keeps the committed contract a document somebody agreed to rather
|
|
@@ -922,7 +961,12 @@ two checked files:
|
|
|
922
961
|
`--all` alike, from the docs repo included. A `verified` spec whose current body hash mismatches
|
|
923
962
|
gets `content.stale` (**warning**, the `sources.stale` grading: the doc changed since it was
|
|
924
963
|
vouched, and only a person can say whether verified still holds). A verified doc with no
|
|
925
|
-
`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.
|
|
926
970
|
- `vouch_scope` — written by `loam vouch --sample <n>`, never by hand, and present only when the
|
|
927
971
|
person who vouched read a recorded SAMPLE of the document rather than all of it. One flat string,
|
|
928
972
|
`sampled <k>/<n> seed=<16 hex>`: k sections read of n, and the seed that chose them. `status`
|
|
@@ -1197,9 +1241,64 @@ under any flag — `x-loam-remove` and `x-loam-based-on` are stripped at path le
|
|
|
1197
1241
|
level alike, on every merge branch. A path item consisting of nothing but a marker publishes no
|
|
1198
1242
|
phantom path. The same feature must carry a `REMOVED` requirement whose `Operations:` line names
|
|
1199
1243
|
that id. Coherence gates an absent path+method, an id mismatch, an unjustified marker, a removed
|
|
1200
|
-
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.
|
|
1201
1252
|
`serviceOperationIds` evaluates the feature as living upserts minus removals, so other same-feature
|
|
1202
|
-
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
|
|
1203
1302
|
text and `openapiRemovals` JSON, never copies the marker into living OpenAPI, and deliberately does
|
|
1204
1303
|
not garbage-collect components. A removal marker in a living `openapi.yaml` is
|
|
1205
1304
|
`openapi.remove-marker-living` (error). The usual staged-write rollback and `unarchive` snapshot
|
|
@@ -1211,9 +1310,11 @@ A `paymentService -> kafka 'Publishes PaymentAuthorized'` edge used to read as f
|
|
|
1211
1310
|
while naming no contract anybody could check. The event axis is the API axis's mirror, in **AsyncAPI
|
|
1212
1311
|
3.0** — only 3.0, because its operations are named top-level objects carrying
|
|
1213
1312
|
`action: send|receive`, the exact analog of an `operationId`, where 2.x spells the same thing as
|
|
1214
|
-
`publish`/`subscribe` under a channel and leaves whose perspective they take ambiguous. A
|
|
1215
|
-
|
|
1216
|
-
|
|
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
|
|
1217
1318
|
a real AsyncAPI parser: the slice loam joins on is small enough that the parser's model would be
|
|
1218
1319
|
dead weight, and adding it would be a fourth runtime dependency.
|
|
1219
1320
|
|
|
@@ -1276,9 +1377,14 @@ the edges point at channels it is the channels that collect three consumers each
|
|
|
1276
1377
|
broker leaves a row of untagged hubs behind it.
|
|
1277
1378
|
|
|
1278
1379
|
**The feature lifecycle, mirrored from the OpenAPI axis.** A feature changes this contract through
|
|
1279
|
-
`features/<FEAT>/specs/<svc>/asyncapi.yaml` — a
|
|
1280
|
-
|
|
1281
|
-
|
|
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
|
|
1282
1388
|
(section, key) over the three named sections `channels.<key>`, `operations.<key>` and
|
|
1283
1389
|
`components.messages.<key>` — and the join token stays the message name. Three feature-only keys,
|
|
1284
1390
|
spelled and shaped exactly as on the OpenAPI axis: `x-loam-based-on` inside a slot's value (written
|
|
@@ -1330,7 +1436,36 @@ decision that reverses that stance: the removal family exists, a message-slot re
|
|
|
1330
1436
|
justified by a `REMOVED` requirement's `Publishes:`/`Consumes:` line (channel and operation slots
|
|
1331
1437
|
need exactness only — requirements join on message names and never on channel or operation keys, so
|
|
1332
1438
|
demanding a justifying requirement there would invent a join the corpus cannot express), and
|
|
1333
|
-
`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
|
|
1334
1469
|
survives as an **operational warning**, not a veto: passing the gate proves the *documents* agree
|
|
1335
1470
|
nobody consumes the message — it does not prove no consumer is still reading the topic with lag, so
|
|
1336
1471
|
sequence the runtime retirement accordingly.
|
|
@@ -1444,19 +1579,35 @@ the source for tests.
|
|
|
1444
1579
|
never the resolution itself — a pin says "I read this version", so `loam rebase` reports every
|
|
1445
1580
|
requirement that moved and says plainly that restamping without re-reading merges your text over
|
|
1446
1581
|
theirs. There is deliberately no three-way merge: requirement prose does not merge.
|
|
1447
|
-
- **The same baseline on the contract axis
|
|
1448
|
-
|
|
1449
|
-
|
|
1450
|
-
|
|
1451
|
-
|
|
1452
|
-
|
|
1453
|
-
|
|
1454
|
-
|
|
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
|
|
1455
1601
|
vendor-extension shape this axis already uses for `x-loam-remove`, written by `loam rebase` and
|
|
1456
1602
|
stripped on merge exactly like `Based-On:`. The digest is taken over the operation's *canonical*
|
|
1457
1603
|
form (object keys sorted), which is the same comparison `isDeepStrictEqual` makes when the merge
|
|
1458
1604
|
decides an operation "differs" — so a pin goes stale exactly when the merge would say something
|
|
1459
|
-
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
|
|
1460
1611
|
that one rule yields both merge verdicts by itself: an operation the author only quoted is
|
|
1461
1612
|
byte-equal to living, so its pin equals its own content and **the merge skips it entirely**
|
|
1462
1613
|
(reported as `· quotes …` in the plan, so the plan never silently writes less than the delta
|
|
@@ -1466,7 +1617,8 @@ the source for tests.
|
|
|
1466
1617
|
decision, reverting an operation nobody edited is a bug. `openapi.baseline-missing` is a warning
|
|
1467
1618
|
that gates the archive, exactly like its requirement-axis sibling — every unpinned restatement is
|
|
1468
1619
|
a candidate rollback — and it is reported **once per service** rather than per operation: the fix
|
|
1469
|
-
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
|
|
1470
1622
|
since the path-item/component baselines landed:** the same discipline now reaches the two surfaces
|
|
1471
1623
|
the operation pin cannot — path-level keys (`parameters`, `servers`, `summary`, `x-*`) and every
|
|
1472
1624
|
`components/<kind>/<name>` — through a single feature-only root record **`x-loam-baselines`** (`{
|
|
@@ -1481,6 +1633,93 @@ the source for tests.
|
|
|
1481
1633
|
component copying is verdict-driven, with genuinely new components copied only when reachable from
|
|
1482
1634
|
content actually written. Removal markers still carry no pin, since
|
|
1483
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.
|
|
1484
1723
|
|
|
1485
1724
|
A requirement may carry a stable identity in its body: `Requirement-ID: <id>`, where the ID matches
|
|
1486
1725
|
`[A-Za-z][A-Za-z0-9._-]{0,127}`. It is optional for backward compatibility. When present in a delta,
|
|
@@ -1533,7 +1772,10 @@ scenarios exercise. Comma-separated, same keep-last-line quirk (and the same rep
|
|
|
1533
1772
|
|
|
1534
1773
|
An entry that resolves to nothing is `covers.unknown` (**warning**, with close ids offered where
|
|
1535
1774
|
they exist): the typo guard, because a mistyped entry silently costs exactly the coverage it was
|
|
1536
|
-
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.
|
|
1537
1779
|
|
|
1538
1780
|
**Coverage obligations** — both warnings, deliberately: they never gate `archive`, and `--strict` is
|
|
1539
1781
|
the CI escalation.
|
|
@@ -1649,12 +1891,15 @@ digest:
|
|
|
1649
1891
|
- stamps: line 1 is `# generated by loam v<version> — …` (the AGENTS.md stamp pattern — the file is
|
|
1650
1892
|
never to be hand-edited), and each scenario carries the tag `@loam-digest-<16hex>` on the line
|
|
1651
1893
|
above its `Scenario:` keyword: the first 16 hex characters of `loam verify`'s scenario body hash —
|
|
1652
|
-
sha256 of the body lines joined and edge-trimmed, salted with the
|
|
1653
|
-
exact recipe `scenario.tested` claim ids fold in — so the
|
|
1654
|
-
disagree about what a scenario says
|
|
1655
|
-
|
|
1656
|
-
|
|
1657
|
-
(
|
|
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
|
|
1658
1903
|
`loam verify --results` matches on.
|
|
1659
1904
|
|
|
1660
1905
|
**Staleness** (`loam validate --service <id>`, service-repo-scoped like the `sources.*` chain):
|
|
@@ -1727,6 +1972,12 @@ requirement also needs a current `Based-On:` pin before archive; `loam rebase <F
|
|
|
1727
1972
|
Omitting or leaving the intent as scaffold text raises the gating `intent.empty` /
|
|
1728
1973
|
`scaffold.placeholder` warning, so the old one-file shape is not archive-ready.
|
|
1729
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
|
+
|
|
1730
1981
|
Each axis may be legitimately empty, and the checks grade absence differently from
|
|
1731
1982
|
emptiness-by-accident:
|
|
1732
1983
|
|
|
@@ -1737,7 +1988,8 @@ emptiness-by-accident:
|
|
|
1737
1988
|
declared-but-untagged is almost always a forgotten tag, while declaring nothing is a legible
|
|
1738
1989
|
statement that the architecture is unchanged.
|
|
1739
1990
|
- **openapi delta** — absent whenever the feature adds no operations; a requirement change that
|
|
1740
|
-
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.
|
|
1741
1993
|
- **requirement delta** — the axis that is nearly always present, because a feature that changes no
|
|
1742
1994
|
requirement changes no promised behaviour. The exception is architecture-only work: a new service
|
|
1743
1995
|
in the C4 delta with no `specs/<svc>/spec.md` gets `service.no-requirement-delta` as a warning,
|
|
@@ -1750,6 +2002,16 @@ a feature cannot fold into the living docs with its reason unwritten. Its siblin
|
|
|
1750
2002
|
`scaffold.placeholder` refuses the rest of an unedited `loam new` scaffold the same way — the exact
|
|
1751
2003
|
template strings, a requirement or description nobody authored, must not become living truth.
|
|
1752
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
|
+
|
|
1753
2015
|
### Where a capability lives (a declared name, and the document behind it)
|
|
1754
2016
|
|
|
1755
2017
|
A business capability — "payment splitting" — is spread across the living specs of every service
|
|
@@ -1916,13 +2178,19 @@ refusing it, which is why the flattening is a whitelist and not a list of substi
|
|
|
1916
2178
|
document declares no requirements yet, it declares none flattening to this slug (close ids
|
|
1917
2179
|
offered, each already spelled as the tag to write), or two of its ids flatten to one slug.
|
|
1918
2180
|
|
|
1919
|
-
**
|
|
2181
|
+
**Any reserved prefix opts a view in.** A view carrying `#req-` and no `#cap-` is an author who
|
|
1920
2182
|
asked to be graded and forgot half the claim; it earns the `unscoped` message rather than silence.
|
|
1921
|
-
|
|
1922
|
-
|
|
1923
|
-
|
|
1924
|
-
|
|
1925
|
-
|
|
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`.
|
|
1926
2194
|
|
|
1927
2195
|
Being SEEN is not the same as claiming something, and the difference is worth stating because it
|
|
1928
2196
|
looks like a gap and is not. A view with no resolved `#cap-` tag claims no capability, so it seeds
|
|
@@ -2107,7 +2375,10 @@ ready. Several useful product shapes remain outside that boundary on purpose:
|
|
|
2107
2375
|
a third thing to keep truthful.
|
|
2108
2376
|
- **Authored `tasks.md` as authoritative state** — loam derives its actionable checklist (`loam
|
|
2109
2377
|
delta`, `loam verify`), so it cannot drift from the delta. Imported task lists are preserved as
|
|
2110
|
-
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.
|
|
2111
2382
|
- **Per-tool workflow forks** — `AGENTS.md` is the tool-agnostic contract. Generated adapters wrap
|
|
2112
2383
|
shared bodies and are never independent process definitions.
|
|
2113
2384
|
|
|
@@ -2198,6 +2469,19 @@ which carry them across loam upgrades. Correspondingly, `archive` refuses to rep
|
|
|
2198
2469
|
version-3 claim graded at its service's current directory: the previous archive's writes are still
|
|
2199
2470
|
in the docs and those files are the only way back.
|
|
2200
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
|
+
|
|
2201
2485
|
Some checks run at plan time rather than in the gate, because only the computed merge can see them.
|
|
2202
2486
|
A **living** requirement sitting outside `## Requirements` refuses the whole archive
|
|
2203
2487
|
(`living.requirement-outside-requirements`, error): the merge rewrites only that section, so the
|
|
@@ -2205,7 +2489,9 @@ requirement would land in the file twice — and `--approve` does not override i
|
|
|
2205
2489
|
duplication is mechanical, not a judgment call; re-home the requirement first. A delta that
|
|
2206
2490
|
redefines a path+method the living OpenAPI already has (`openapi.op-modified`, warning) merges
|
|
2207
2491
|
anyway, but the plan says so — the overwrite is wholesale, and the living definition it replaces
|
|
2208
|
-
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
|
|
2209
2495
|
`--approve`: the alternative was archiving with the architecture axis silently dropped, which is the
|
|
2210
2496
|
data loss this tool exists to prevent.
|
|
2211
2497
|
|
|
@@ -2213,7 +2499,8 @@ The landscape merge **splices authored source, it does not re-serialize**. Each
|
|
|
2213
2499
|
relationship is copied into the living landscape byte-for-byte from `delta.likec4` — technology,
|
|
2214
2500
|
style, icons, links, metadata, nested children, every tag — with exactly one edit: the feature's own
|
|
2215
2501
|
tag is stripped (the additions are baseline now — the drop above is why unarchive restores bytes),
|
|
2216
|
-
|
|
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'`).
|
|
2217
2504
|
Placement is deterministic and **service-grouped**, and never moves an authored byte — it only
|
|
2218
2505
|
chooses where the new blocks go. A top-level element whose service the model already draws (the
|
|
2219
2506
|
`metadata { service }` binding first, the title as fallback) lands immediately after the last
|
|
@@ -2231,22 +2518,32 @@ services) yields the byte-identical landscape to B then A, so concurrent archive
|
|
|
2231
2518
|
different lines and git merges them without help. The insertions carry no marker comment — which
|
|
2232
2519
|
archive added what is the git history's to say. A **nested** element lands inside its parent's block
|
|
2233
2520
|
— inside the living parent when the landscape already has it, riding verbatim inside the spliced
|
|
2234
|
-
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
|
|
2235
2528
|
reference resolution is not order-dependent, so a relationship placed before an element it names
|
|
2236
2529
|
still parses — and because splicing is text surgery, the computed landscape is **parsed in memory
|
|
2237
2530
|
before anything is written**: a result LikeC4 rejects — a splice bug, or legal inputs the living
|
|
2238
2531
|
document cannot absorb, like an element kind or tag its `specification` block never declares —
|
|
2239
2532
|
refuses the archive at plan time (`merge-failed`, nothing written), the same discipline as the
|
|
2240
|
-
unparseable delta. A living declaration
|
|
2241
|
-
|
|
2242
|
-
|
|
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.
|
|
2243
2539
|
|
|
2244
2540
|
**Which document an addition lands in.** The C4 merge asks it per declaration, not per feature. An
|
|
2245
2541
|
element whose PARENT resolves to a service whose `model.likec4` extends the map is spliced into that
|
|
2246
2542
|
model — and so is the interior a delta writes inside a service element the same archive introduces,
|
|
2247
2543
|
since those containers carry no feature tag and would otherwise ride onto the map inside their
|
|
2248
2544
|
parent's bytes. The parent chain has to be spelled in the map's own ids for either to happen. The
|
|
2249
|
-
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
|
|
2250
2547
|
the `model { }` block itself is appended to a model that has none (an empty file, a `views { }`-only
|
|
2251
2548
|
file and a tags-only `specification { }` are all extending models, and all three are legal to
|
|
2252
2549
|
archive into). A
|
|
@@ -2349,7 +2646,9 @@ design. The heading must be reachable, too — a leading UTF-8 BOM used to hide
|
|
|
2349
2646
|
|
|
2350
2647
|
## The verification record
|
|
2351
2648
|
|
|
2352
|
-
|
|
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
|
|
2353
2652
|
**checklist** from the feature's own artifacts — the same files `validate` reads, never the code:
|
|
2354
2653
|
|
|
2355
2654
|
| kind | one claim per | reads |
|
|
@@ -2373,18 +2672,26 @@ nobody wrote must not carry over as if it still applied.
|
|
|
2373
2672
|
anything that does not answer the *current* checklist, each under its own `--json` `error.code`:
|
|
2374
2673
|
`answers-unreadable` (not JSON, or a verdict outside `confirmed` / `unconfirmed`),
|
|
2375
2674
|
`answers-mismatch` (an id nobody asked about, a claim with no answer, or one answered twice),
|
|
2376
|
-
`answers-unevidenced` (a `confirmed` with no `file:line` behind it
|
|
2377
|
-
|
|
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.
|
|
2378
2683
|
|
|
2379
2684
|
**`--results <report.json>` answers the `scenario.tested` claims mechanically, from a cucumber JSON
|
|
2380
|
-
test report** — the format `cucumber-js --format json` emits, and cucumber-jvm, behave
|
|
2381
|
-
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
|
|
2382
2687
|
`elements[]` (scenarios) with `name`, `tags[] {name}` and `steps[] {result {status}}` — plus the two
|
|
2383
2688
|
places those dialects put a failure the steps never see: cucumber-jvm's per-element
|
|
2384
2689
|
`before[]`/`after[]` hook results and behave's element-level `status`. Every other field is ignored,
|
|
2385
2690
|
tagless elements (backgrounds, hand-written scenarios) are invisible, and a file that is not that
|
|
2386
2691
|
array at all refuses under `answers-unreadable` rather than quietly answering every claim "not
|
|
2387
|
-
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.
|
|
2388
2695
|
|
|
2389
2696
|
Matching is by digest and nothing else: a claim is answered by the report scenarios carrying its
|
|
2390
2697
|
`@loam-digest-<16hex>` tag — the tag `loam gherkin` stamped, the same 16 hex of the same body hash
|
|
@@ -2393,12 +2700,21 @@ name), so a business run can never answer an arch claim however identically the
|
|
|
2393
2700
|
worded. Names never match anything: a reworded spec scenario matches nothing until the suite is
|
|
2394
2701
|
regenerated and re-run — an agent must not be able to SAY a scenario is tested; only a green run
|
|
2395
2702
|
may. The verdicts: every matching occurrence (a digest the report holds twice is a re-run, and all
|
|
2396
|
-
occurrences count) ran at least one step, every step `passed`, every before/after hook `passed`,
|
|
2397
|
-
|
|
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
|
|
2398
2707
|
`<report-path>: <feature uri or name> › <scenario name>` per occurrence; a failed / undefined /
|
|
2399
2708
|
pending / ambiguous step (`failed at step N`), a failing hook (`failed in a before/after hook`), a
|
|
2400
2709
|
failed element status, a skipped-only run (`skipped`), or no match at all (`not found in report`) →
|
|
2401
|
-
`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.
|
|
2402
2718
|
|
|
2403
2719
|
`--results` OWNS every `scenario.tested` claim **in the same run**: an answers-file entry for one is
|
|
2404
2720
|
refused (`answers-mismatch` — the runner owns it), `--record` alongside must answer exactly the
|
|
@@ -2430,6 +2746,8 @@ own runner-neutral shape, chosen by its marker key:
|
|
|
2430
2746
|
```
|
|
2431
2747
|
|
|
2432
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.
|
|
2433
2751
|
`status` is `passed` or `failed` — nothing else. `test` is optional free text: the runner's own name
|
|
2434
2752
|
for the run, used as the evidence string a reader follows. `loamScenarioReport: 1` is the format
|
|
2435
2753
|
version this loam reads; any other value refuses rather than being partly understood.
|
|
@@ -2455,12 +2773,18 @@ carrying the marker is always graded as this shape, even when malformed: falling
|
|
|
2455
2773
|
cucumber parser would tell an author their report is not a cucumber array, which is true, useless,
|
|
2456
2774
|
and hides the real mistake.
|
|
2457
2775
|
|
|
2458
|
-
**
|
|
2459
|
-
|
|
2460
|
-
|
|
2461
|
-
|
|
2462
|
-
|
|
2463
|
-
|
|
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.
|
|
2464
2788
|
|
|
2465
2789
|
### Contract-test reports answer the `api.exposes` claims
|
|
2466
2790
|
|
|
@@ -2554,7 +2878,7 @@ service's as a pure lens — no repository binding, usable from the docs repo, r
|
|
|
2554
2878
|
`unknown-service` when the checklist has no claims owned by that id — and both agents must have
|
|
2555
2879
|
answered the same scope. Combining the flag with `--record`, `--results` or `--contract-results`
|
|
2556
2880
|
refuses `invalid-option`, as does any arity other than exactly two files and an archived feature
|
|
2557
|
-
(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
|
|
2558
2882
|
**not** block the diff — the lens never reads the record, and the moment a cross-examination is most
|
|
2559
2883
|
needed is before re-recording over a broken one.
|
|
2560
2884
|
|
|
@@ -2574,7 +2898,7 @@ block pins each file's path and the sha256 of the exact bytes read.
|
|
|
2574
2898
|
confirmed/unconfirmed/unanswered. The payload also deliberately carries **no `verified`, `verdict`
|
|
2575
2899
|
or `attested` key at all**: two answer sets agreeing — usually two contexts of the same model, so
|
|
2576
2900
|
correlated — ranks where a reviewer reads first; it is not verification, it upgrades nothing, and it
|
|
2577
|
-
leaves
|
|
2901
|
+
leaves the verification record byte-identical (no docs lock is taken, and the diff runs while a writer
|
|
2578
2902
|
holds one).
|
|
2579
2903
|
|
|
2580
2904
|
### Attested is not verified
|
|
@@ -2609,28 +2933,79 @@ the decision:
|
|
|
2609
2933
|
So "a green run is the only thing that confirms a scenario" is a claim about the **record**, not
|
|
2610
2934
|
about the CLI's flags: the short path still works, and it is legible afterwards as the short path.
|
|
2611
2935
|
|
|
2612
|
-
### The record is federated: one
|
|
2936
|
+
### The record is federated: one attestation per service, one file each
|
|
2613
2937
|
|
|
2614
2938
|
A cross-service feature's claims belong to several services, and each service's code lives in its
|
|
2615
2939
|
own repository — so no single place can honestly answer all of them.
|
|
2616
2940
|
`loam verify <FEAT> --service <id> --record answers.json`, run **in that service's own repo**,
|
|
2617
2941
|
narrows the checklist to that service's claims and binds the answers to this repository's commit.
|
|
2618
|
-
The result is
|
|
2619
|
-
|
|
2620
|
-
|
|
2621
|
-
|
|
2622
|
-
|
|
2623
|
-
|
|
2624
|
-
|
|
2625
|
-
|
|
2626
|
-
|
|
2627
|
-
|
|
2628
|
-
|
|
2629
|
-
|
|
2630
|
-
|
|
2631
|
-
|
|
2632
|
-
|
|
2633
|
-
|
|
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:
|
|
2634
3009
|
|
|
2635
3010
|
- `docs-busy` — another writer held the lock longer than the record was willing to wait (5 s).
|
|
2636
3011
|
Nothing was read or written; re-run once it finishes. The contention runs both ways: `archive`,
|
|
@@ -2651,9 +3026,10 @@ Recording refuses rather than guesses, each under its own `--json` `error.code`:
|
|
|
2651
3026
|
- `repository-unavailable` — `--service` from a repo that declares none, or one whose commit cannot
|
|
2652
3027
|
be read. An attestation with nothing to bind to is not an attestation.
|
|
2653
3028
|
- **`record-federated`** — the legacy all-at-once form (`--record` with **no** `--service`) over a
|
|
2654
|
-
record that already carries attestations
|
|
2655
|
-
|
|
2656
|
-
|
|
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`.
|
|
2657
3033
|
- `record-unreadable` — see below.
|
|
2658
3034
|
|
|
2659
3035
|
Reading is unrestricted: `loam verify <FEAT> --service <id>` **without** `--record` is a pure lens
|
|
@@ -2662,12 +3038,26 @@ in `services`, which is how you find out which repositories still owe an attesta
|
|
|
2662
3038
|
|
|
2663
3039
|
The record is YAML, like the other data artifacts, and holds the claim text next to each verdict so
|
|
2664
3040
|
it reads without loam. Because it has to survive without loam, human edits are expected — and read
|
|
2665
|
-
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
|
|
2666
3044
|
what the readers use — `summary` with its counts, `recorded`, `checklist`, claims each with an `id`
|
|
2667
|
-
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
|
|
2668
3047
|
overwritten**. It used to count as *no record at all*, which was the wrong direction on the one
|
|
2669
3048
|
artifact loam cannot reconstruct — "no record" invites writing a fresh one over evidence somebody's
|
|
2670
|
-
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
|
|
2671
3061
|
still pass through untouched. Every verdict names who answered it — `answered_by: runner` (a
|
|
2672
3062
|
cucumber report's green run, mechanical), `answered_by: external-runner` (a contract-test report's
|
|
2673
3063
|
operationId-matched green entry, mechanical too), or `answered_by: agent` (somebody's word about the
|
|
@@ -2737,7 +3127,7 @@ next `--record` makes it drift-checkable), warn `evidence.unresolved` (the cited
|
|
|
2737
3127
|
unsafe, not a regular file, or the cited line is past its end), `evidence.moved` (the file's digest
|
|
2738
3128
|
differs while the cited line still reads as pinned), `evidence.line-changed` (the cited line no
|
|
2739
3129
|
longer says what was recorded), `evidence.token-missing` (the file contained the token at the
|
|
2740
|
-
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
|
|
2741
3131
|
exists but cannot be read — none of its evidence was checked, and a silent skip would read as clean,
|
|
2742
3132
|
on the very file a hand edit to the pins most plausibly breaks). Archived records are deliberately
|
|
2743
3133
|
not linted — frozen history, and post-ship code drift is normal evolution. From the docs repo the
|
|
@@ -2747,13 +3137,18 @@ verdict in either direction, `attested` never silently becomes `verified`, and t
|
|
|
2747
3137
|
fail `validate`; `--strict` remains the CI lever it always was.
|
|
2748
3138
|
|
|
2749
3139
|
It lives inside the feature, so `archive` carries it into `features/archive/<FEAT>/` with everything
|
|
2750
|
-
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
|
|
2751
3142
|
record as **stale** if the feature moved under it — while the feature is active. Once archived there
|
|
2752
3143
|
is no current checklist to be stale against (the merge itself moved the feature's operations into
|
|
2753
3144
|
the living OpenAPI, so a re-derived checklist could only disagree): `loam verify` on an archived
|
|
2754
3145
|
feature renders the record verbatim as **frozen history** (`frozen: true` under `--json`, no
|
|
2755
3146
|
staleness judgment), and `--record` refuses (`invalid-option` — there is no current checklist for
|
|
2756
|
-
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.
|
|
2757
3152
|
|
|
2758
3153
|
**Verification does not gate `archive`.** Coherence gates because loam computed it from the
|
|
2759
3154
|
documents; a verdict is an agent's word about code loam never read, and a gate in front of shipping
|
|
@@ -2762,6 +3157,72 @@ comes later, and an `unconfirmed` claim with a note is worth more there than a `
|
|
|
2762
3157
|
can back up. `loam verify` exits 0 either way; branch on `verified` in the `--json` payload, or on
|
|
2763
3158
|
`verdict` when you need to tell `attested` from `unverified`.
|
|
2764
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
|
+
|
|
2765
3226
|
## Operating at fleet scale
|
|
2766
3227
|
|
|
2767
3228
|
One docs repo, a hundred services, many teams. The repo scales the way any shared codebase does —
|
|
@@ -2913,11 +3374,14 @@ repairing anything:
|
|
|
2913
3374
|
|
|
2914
3375
|
And it reports the **agent surface**: which tool and profile the repo is wired for, how many command and skill
|
|
2915
3376
|
files it holds, how many a newer loam would lay down (`doctor.agent-files-missing`), and how many
|
|
2916
|
-
have fallen behind (`doctor.agent-files-stale`).
|
|
2917
|
-
`<!-- 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.
|
|
2918
3383
|
The digest manifest is what authorizes `init` to refresh an unchanged pointer; a different body is
|
|
2919
|
-
treated as human-owned and never overwritten.
|
|
2920
|
-
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
|
|
2921
3385
|
than of a record: `loam.json` stores tools and profile, not deliveries, and a repo initialized
|
|
2922
3386
|
`--no-skills` is not reported as missing the selected skills.
|
|
2923
3387
|
|
|
@@ -3020,6 +3484,7 @@ and publishes it wherever the fleet view reads:
|
|
|
3020
3484
|
"errors": 0,
|
|
3021
3485
|
"warnings": 2,
|
|
3022
3486
|
"sources": { "current": 1, "stale": 0, "unvouched": 0, "absent": 0 },
|
|
3487
|
+
"contentStale": 0,
|
|
3023
3488
|
"generatedAt": "2026-08-03T12:00:00Z"
|
|
3024
3489
|
}
|
|
3025
3490
|
```
|
|
@@ -3038,10 +3503,16 @@ loam validate --service "$SVC" --json | jq '{
|
|
|
3038
3503
|
unvouched: [.targets[0].findings[] | select(.code == "sources.unvouched")] | length,
|
|
3039
3504
|
absent: [.targets[0].findings[] | select(.code == "sources.absent")] | length
|
|
3040
3505
|
},
|
|
3506
|
+
contentStale: [.targets[0].findings[] | select(.code == "content.stale")] | length,
|
|
3041
3507
|
generatedAt: (now | todate)
|
|
3042
3508
|
}'
|
|
3043
3509
|
```
|
|
3044
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
|
+
|
|
3045
3516
|
`generatedAt` is the CI's clock, never loam's — loam output carries no timestamps, by design, so the
|
|
3046
3517
|
same repo state always yields the same bytes. That is also what makes the timestamp the liveness
|
|
3047
3518
|
signal: the rule is that every service repo's CI publishes this summary on every run, so a summary
|
|
@@ -3111,7 +3582,7 @@ says what its architecture means, and two generated models of the same code disa
|
|
|
3111
3582
|
every run — a done-check built on diffing them would flap until somebody turned it off. What is
|
|
3112
3583
|
deterministic is the **question**: which files, in which grammar, bound to which existing elements;
|
|
3113
3584
|
which operations, which edges, which scenarios. loam owns the questions and the checking; an agent
|
|
3114
|
-
owns the reading. `sources` and
|
|
3585
|
+
owns the reading. `sources` and the verification record are where the answers are written down.
|
|
3115
3586
|
|
|
3116
3587
|
## Status
|
|
3117
3588
|
|