@ybotok/loam 0.2.0-alpha.2 → 0.2.0-alpha.5
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 +124 -0
- package/MIGRATING-from-OpenSpec.md +4 -1
- package/README.md +11 -10
- package/ROADMAP.md +103 -17
- package/SCHEMA.md +86 -7
- package/dist/commands/archive/run.js +46 -2
- package/dist/commands/archive/run.js.map +1 -1
- package/dist/commands/seed/plan.js +1 -1
- package/dist/commands/seed/plan.js.map +1 -1
- package/dist/commands/subsystem/sync.js +111 -20
- package/dist/commands/subsystem/sync.js.map +1 -1
- package/dist/commands/subsystem/txn/txn.js +20 -1
- package/dist/commands/subsystem/txn/txn.js.map +1 -1
- package/dist/commands/subsystem/txn/views.js +23 -11
- package/dist/commands/subsystem/txn/views.js.map +1 -1
- package/dist/commands/validate/fleet/landscape.js +6 -0
- package/dist/commands/validate/fleet/landscape.js.map +1 -1
- package/dist/commands/validate/fleet/views/ids.js +6 -1
- package/dist/commands/validate/fleet/views/ids.js.map +1 -1
- package/dist/commands/validate/fleet/views/projects.js +90 -0
- package/dist/commands/validate/fleet/views/projects.js.map +1 -0
- package/dist/commands/validate/fleet/views/stale.js +15 -39
- package/dist/commands/validate/fleet/views/stale.js.map +1 -1
- package/dist/core/agent/agents-md/command-map.js +3 -1
- package/dist/core/agent/agents-md/command-map.js.map +1 -1
- package/dist/core/agent/agents-md/map/subsystem.js +18 -2
- package/dist/core/agent/agents-md/map/subsystem.js.map +1 -1
- package/dist/core/agent/agents-md.js +4 -0
- package/dist/core/agent/agents-md.js.map +1 -1
- package/dist/core/agent/workflows/adopt.js +13 -3
- package/dist/core/agent/workflows/adopt.js.map +1 -1
- package/dist/core/agent/workflows/check.js +1 -0
- package/dist/core/agent/workflows/check.js.map +1 -1
- package/dist/core/brief/checks.js +9 -0
- package/dist/core/brief/checks.js.map +1 -1
- package/dist/core/brief/targets.js +7 -4
- package/dist/core/brief/targets.js.map +1 -1
- package/dist/core/brief/unchecked.js +1 -1
- package/dist/core/brief/unchecked.js.map +1 -1
- package/dist/core/docs.js +11 -7
- package/dist/core/docs.js.map +1 -1
- package/dist/core/doctor/doctor.js +17 -6
- package/dist/core/doctor/doctor.js.map +1 -1
- package/dist/core/explain/families.js +1 -1
- package/dist/core/explain/families.js.map +1 -1
- package/dist/core/repo/paths.js +39 -1
- package/dist/core/repo/paths.js.map +1 -1
- package/dist/core/repo/tree/render/body.js +219 -0
- package/dist/core/repo/tree/render/body.js.map +1 -0
- package/dist/core/repo/tree/render/projects.js +124 -0
- package/dist/core/repo/tree/render/projects.js.map +1 -0
- package/dist/core/repo/tree/render/stale.js +26 -0
- package/dist/core/repo/tree/render/stale.js.map +1 -0
- package/dist/core/repo/tree/render/text.js +84 -0
- package/dist/core/repo/tree/render/text.js.map +1 -0
- package/dist/core/repo/tree/{views.js → render/views.js} +38 -34
- package/dist/core/repo/tree/render/views.js.map +1 -0
- package/examples/README.md +6 -1
- package/examples/docs/architecture/subsystems.likec4 +4 -0
- package/examples/docs/services/checkout-web/likec4.config.json +4 -0
- package/examples/docs/services/order-service/likec4.config.json +4 -0
- package/examples/docs/services/payment-service/likec4.config.json +4 -0
- package/examples/docs/services/payment-service/model.likec4 +4 -2
- package/examples/docs/services/platform/identity-service/likec4.config.json +4 -0
- package/examples/docs/services/platform/notification-service/likec4.config.json +4 -0
- package/package.json +1 -1
- package/dist/core/repo/tree/views.js.map +0 -1
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"check.js","sourceRoot":"","sources":["../../../../src/core/agent/workflows/check.ts"],"names":[],"mappings":"AAEA;;;;;GAKG;AACH,MAAM,CAAC,MAAM,UAAU,GAAmB;IACxC,IAAI,EAAE,YAAY;IAClB,WAAW,EAAE,qDAAqD;IAClE,YAAY,EAAE,iCAAiC;IAC/C,OAAO,EACL,uMAAuM;IACzM,4EAA4E;IAC5E,uEAAuE;IACvE,2EAA2E;IAC3E,+DAA+D;IAC/D,4EAA4E;IAC5E,uEAAuE;IACvE,0EAA0E;IAC1E,qEAAqE;IACrE,2EAA2E;IAC3E,4EAA4E;IAC5E,SAAS;IACT,UAAU,EAAE,8CAA8C;IAC1D,2EAA2E;IAC3E,2EAA2E;IAC3E,gDAAgD;IAChD,YAAY,EAAE,EAAE;IAChB,KAAK,EAAE;QACL,8GAA8G;QAC9G,2DAA2D;QAC3D,wEAAwE;QACxE,6CAA6C;KAC9C;IACD,IAAI,EAAE;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;CA6SP;CACA,CAAC","sourcesContent":["import type { CommandContent } from \"../contract.js\";\n\n/**\n * The fix tables: run loam's checks and repair what they find, code by code.\n * The largest body on purpose — it is the other half of the codes-drift\n * documentation corpus (with ../agents-md/), so a new finding code gets its\n * row here.\n */\nexport const LOAM_CHECK: CommandContent = {\n name: \"loam-check\",\n description: \"Validate the loam docs repo and fix what it reports\",\n argumentHint: \"[--all | <FEAT-id> | <service>]\",\n purpose:\n \"Run loam's checks and fix what they find. Branch on `findings[].code`, never on the prose — and remember that a coherence finding marked `gates` stops `loam archive` even when it is only a warning.\",\n // No `$1`, unlike the other five. This body carries no placeholder at all —\n // `loam-check`'s argument is a target you pass to `loam validate`, not\n // something the protocol interpolates — and the argument hint's FIRST form\n // is `--all`, so `loam instructions loam-check $1` expanded to\n // `loam instructions loam-check --all`, which commander reads as an unknown\n // option and refuses with exit 1. A pointer file whose one instruction\n // fails leaves an agent with no protocol at all, which is a state the fat\n // body could never reach. The command still tolerates a leading-dash\n // argument (see `allowUnknownOption` in commands/instructions.ts); this is\n // the half of the fix that stops loam printing the broken line in the first\n // place.\n invocation: \"loam instructions loam-check --no-fix-tables\",\n // The one protocol whose body names no placeholder: its target (`--all`, a\n // feature id, a service id) is spelled inside the `loam validate` line the\n // agent chooses, not substituted into the page.\n placeholders: [],\n spine: [\n \"`loam validate --all` in the docs repo for the fleet, or `loam validate <target>` for one feature or service\",\n \"fix every error, and every finding that gates the archive\",\n \"leave an advisory warning standing only if you can say why, and say it\",\n \"re-run until the grade is the one you meant\",\n ],\n body: `Run loam's checks and fix what they find.\n\n- whole fleet: \\`loam validate --all --json\\` (run in the docs repo)\n- one target: \\`loam validate <FEAT-id | service-id> --json\\` — the feature reading is\n tried first; \\`--feature <id>\\` / \\`--service <id>\\` force one when a name could be both,\n and \\`target.ambiguous\\` (warn) says when a name really was both and which reading was taken\n- \\`--strict\\` (any mode) exits 1 on any warning too. Exit code only: \\`valid\\` and the\n payload do not change, so the stricter grade lives in the CI invocation, not the repo\n- \\`--errors-only\\` drops the \\`ok\\` confirmations from the text view; the \\`--json\\`\n payload is byte-identical either way\n- under \\`--all\\`, warn codes tied to a contract axis (\\`service.no-spec\\`,\n \\`spec.no-requirements\\`, \\`service.no-openapi\\`'s warn form,\n \\`spine.op-link-missing\\`, \\`permissions.unenforced\\`, \\`capability.unrealized\\`)\n collapse in the TEXT view to one \"axis not started\" banner per axis when that\n axis has zero participating services fleet-wide (\\`scorecard.adoption\\`) —\n expected during staged adoption; \\`--json\\` carries every finding unchanged\n- refusals before any grading: \\`docs-missing\\` / \\`services-missing\\` (\\`docsDir\\` in\n ./loam.json points at nothing, or at a directory with no \\`services/\\`) — run\n \\`loam doctor\\` and fix the wiring, do not read an empty report as a clean fleet\n\nBranch on \\`findings[].code\\`, not the prose. Errors fail validate; a coherence finding\nwith \\`gates: true\\` will stop \\`loam archive\\` even when it is a warning. Fix every\nerror and every gating finding. Leave an advisory warning only if you can say why,\nand say it.\n\n\\`--service <id>\\` — one service's own axes (\\`--all\\` runs these for every service):\n\n| code | what it means | what to do |\n|---|---|---|\n| \\`service.unknown\\` | no \\`services/<id>/\\` at all — the id is a typo until proven otherwise | use one of the ids the message offers, or \\`loam list services\\`; never \\`loam adopt\\` a misspelling |\n| \\`service.no-model\\` | the directory is real but there is no model.likec4 | run \\`loam adopt\\` for it — without the C4 center nothing else has anywhere to hang |\n| \\`c4.invalid\\` | model.likec4 does not parse | fix this first — an unreadable axis makes every other check meaningless |\n| \\`c4.no-relationships\\` (warn) | the model parses, declares elements, and no relationship joins anything — while its own evidence says it should reach something: more than one nested element, or dependencies declared in this service's health.yaml. A model that reaches nothing is almost never true; without this check, a 2-element model with 0 relationships and a 19-element model with 19 relationships could receive the same green verdict | draw the edges the service actually has — what its containers call, what reads and writes its stores, what it depends on at runtime. A bare root-plus-one-container baseline with no other evidence stays silent on purpose: nothing proves it thin |\n| \\`requirements.missing-scenarios\\` | a requirement with no scenario | add the acceptance criteria; do not delete the requirement |\n| \\`requirements.stepless-scenario\\` | a scenario with a heading and no recognizable steps — it satisfies the coverage rule and tests nothing: cucumber runs it vacuously green and \\`loam verify --results\\` can never confirm it. Graded on spec.md and arch.spec.md alike, in \\`--service\\` and \\`--feature\\` scope both, because a stepless scenario in a delta merges into the living spec and is called covered forever after | write the body as \\`- **Given/When/Then**\\` bullets |\n| \\`requirements.assertionless-scenario\\` | a scenario with steps and no \\`Then\\` anywhere in its body — it arranges and acts and asserts nothing, so the run confirms only that the setup did not throw and the \\`scenario.tested\\` claim is answered on that. Disjoint from \\`stepless\\` by construction (that one has no steps at all), graded in the same four places | add the \\`- **Then**\\` the scenario was written for. \\`And\\`/\\`But\\` are continuations: with no \\`Then\\` above them they continue the setup |\n| \\`requirements.examples-unbound\\` (error) | a scenario with an \\`Examples\\` table uses a \\`<placeholder>\\` no column supplies — cucumber substitutes nothing, so the step definition receives the literal angle-bracket text. Steps, docstrings and data-table cells all count as uses, because cucumber substitutes into all three | add the column, or correct the placeholder to one the header names |\n| \\`requirements.examples-unreferenced\\` (warn) | an \\`Examples\\` column no step, docstring or data-table cell refers to — usually the fossil of a rename, and the outline then runs its rows as identical passes that read as cases of coverage | reference the column from a step, or drop it |\n| \\`spec.merge-conflict\\` | a requirement document still holds \\`<<<<<<<\\` / \\`=======\\` / \\`>>>>>>>\\` markers — nobody wrote what it now says. Graded on living spec.md/arch.spec.md and on a feature's deltas of them, in both scopes | resolve the conflict in the file the finding names. \\`loam archive\\` refuses (\\`merge-failed\\`) on a conflicted living document it would rewrite, and \\`--approve\\` does not override it: the merge rewrites the requirements section and takes whichever marker lines fall inside it, so a conflict anyone can see would become a document nobody can tell is wrong |\n| \\`spec.duplicate-requirement\\` | one \\`### Requirement:\\` name defined twice in one living spec.md or arch.spec.md — a merge edits only the first, the rest live on as stale copies | keep exactly one block per name (merge the bodies); the two files are separate namespaces, so a name in both is fine |\n| \\`spec.requirement-id-invalid\\` | a \\`Requirement-ID\\` violates \\`[A-Za-z][A-Za-z0-9._-]{0,127}\\` | replace it with a portable stable ID; do not remove an established ID merely to silence the check |\n| \\`spec.requirement-id-repeated\\` | one requirement declares \\`Requirement-ID\\` more than once | keep exactly one identity line |\n| \\`spec.requirement-id-duplicate\\` | one ID identifies multiple requirements in the same file | give each requirement its own stable ID |\n| \\`openapi.generated-stale\\` (warn) | \\`loam.json\\` names \\`contracts.openapi\\` — where this repo's BUILD writes its contract — and that document differs from the committed \\`services/<id>/openapi.yaml\\`. The docs-repo copy is what every operation join is graded against, so the fleet is being checked against a contract the service no longer serves. Compared by a digest over canonical JSON, so key order and formatting never fire it. Service-repo only; silent with no \\`contracts\\` block | review the difference and copy the generated document across — loam never writes it for you, because the committed contract is one somebody agreed to, not a cache of the build |\n| \\`contracts.source-missing\\` (error) | \\`contracts.openapi\\` names a path and nothing is there — usually CI validating before it builds | run the build first, or correct the path |\n| \\`contracts.source-invalid\\` (error) | \\`contracts.openapi\\` resolves outside the repository, or names a file that does not parse as YAML or JSON | correct the path, or fix the document the build emitted |\n| \\`spec.unknown-directive\\` (warn) | a requirement body line whose key is one or two edits from a directive and is not one — \\`Realises:\\`, \\`Capabilties:\\`, \\`Opertaions:\\`. This is the one case where the corpus is quieter than it looks: an unrecognised key is prose, so the join it resembles simply does not exist and every check over it stays green for want of anything to fail. Measured by edit distance, not by prefix, so ordinary prose keys (\\`Note:\\`, \\`Context:\\`, \\`Owner:\\`) do not fire it; fenced blocks and table cells are skipped, because a directive spelling inside an example is a sample value | correct the spelling, or reword the line so it does not read as a directive |\n| \\`spec.repeated-operations\\` (warn) | a second \\`Operations:\\` line in one requirement body — the last line REPLACES the earlier ones, whose list is silently lost (living specs and feature deltas alike) | merge them into one comma-separated \\`Operations:\\` line |\n| \\`spec.repeated-covers\\` (warn) | same for \\`Covers:\\` — the last line wins, the earlier list is silently lost | merge them into one comma-separated \\`Covers:\\` line |\n| \\`spec.no-requirements\\` (warn) | a LIVING spec.md with no \\`### Requirement:\\` block at all — every requirement-driven check below it is vacuous, not passing | write the requirements, or say in the file why the service has none yet |\n| \\`service.unreadable\\` | an artifact of this service could not be read (permissions, a dangling symlink) — nothing about the service was checked, and the rest of the fleet still was | fix the file the finding names; a service silently skipped would have read as a clean one |\n| \\`service.no-spec\\` (warn) | no living spec.md — a part-adopted service, legal but unchecked | write it; until it exists, requirement coverage and API governance are vacuous |\n| \\`service.no-openapi\\` (error, or warn) | no openapi.yaml — deleted, renamed, or never written. ERROR when something already written down joins into the absent file: a LIVING non-REMOVED requirement's \\`Operations:\\` line, or a landscape edge carrying \\`metadata { op }\\` into this service. The finding's \\`details\\` list the stranded operationIds. WARN when nothing joins into it but the landscape cannot prove nobody calls this service (it is absent, or it does not parse). Silent when the landscape parses and no edge calls an operation here — a worker, a cron or a UI owes no contract | error: put the file back — every link the \\`details\\` name resolves to nothing until it is there. If the operations are genuinely gone, retire them through a feature delta (REMOVED requirement + \\`x-loam-remove\\` marker) instead of deleting the contract out from under them. warn: write the contract, or model the service in the fleet map so it shows no one expects an API |\n| \\`openapi.invalid\\` | openapi.yaml exists but does not parse — an unreadable contract proves nothing, so no \\`api.*\\` or spine finding is graded against it (before this code, the empty parse graded every inbound edge \\`spine.op-undefined\\`). The same code fires for a FEATURE's own openapi.yaml under \\`--feature\\`, where it gates the archive and suspends the service's contract-axis checks the same way | fix the YAML first — the API axis is unchecked until it reads |\n| \\`openapi.remove-marker-living\\` | a living openapi.yaml contains feature-only \\`x-loam-remove: true\\` | remove the marker from living; retire the operation through a feature delta with a matching REMOVED requirement |\n| \\`openapi.duplicate-operationid\\` (warn) | one operationId occupies two (path, method) slots in a living contract — every join on the id (a requirement's \\`Operations:\\` line, an edge's \\`metadata { op }\\`, a removal marker) then picks one of them arbitrarily | give each slot its own id; the identity of an operation is its path and method, and the id is how the other axes name it |\n| \\`openapi.response-undescribed\\` (warn) | operation(s) whose responses declare no schema at all (204/304 excepted) — the contract names the operation and says nothing about what it returns, so a reader still needs the code. A presence probe: what a declared schema SAYS is still never checked | declare \\`content\\` with a \\`schema\\` on at least one response of each named operation |\n| \\`openapi.ref-unresolved\\` (warn here; error at archive plan time) | internal \\`$ref\\`(s) in the living contract that resolve to nothing in the document — the operation validates while its schema is silently absent. External refs (not starting \\`#/\\`) are never checked | define the missing component or fix the pointer the \\`details\\` name |\n| \\`openapi.baseline-stale\\` | the living operation — or a pinned path-level key or component — changed after this delta edited it | somebody landed a change to it in between. Re-read the living value, fold in what you still mean, then \\`loam rebase <FEAT>\\` |\n| \\`openapi.baseline-missing\\` (warn, gates archive; one per service) | operations, path-level keys or components in this feature's openapi.yaml carry no baseline (\\`x-loam-based-on\\` / \\`x-loam-baselines\\`) | run \\`loam rebase <FEAT>\\`. Until you do, archive refuses: the merge cannot tell the surfaces you EDITED from the ones you merely restated, so it would upsert all of them — and every restated one reverts whatever landed on it. \\`--approve\\` merges unpinned deliberately |\n| \\`openapi.baseline-invalid\\` | an \\`x-loam-based-on\\` that is not a digest, one on an operation the living contract has no slot for, a malformed \\`x-loam-baselines\\` record, or a record entry naming a surface the delta does not restate (or whose living value is gone) | \\`loam rebase\\` writes both; a new value has no living version to be based on, so it carries no entry |\n| \\`spec-api.op-undefined\\` | a LIVING requirement's \\`Operations:\\` line names an operationId this service's openapi.yaml does not define (the same code fires inside a feature delta) | define the endpoint, or correct the \\`Operations:\\` line — the two axes disagree about what the service exposes |\n| \\`api.ungoverned\\` (warn) | operation(s) no requirement's \\`Operations:\\` line names | write the requirement, or link an existing one |\n| \\`api.ops-unlinked\\` (warn) | operations AND requirements exist but zero \\`Operations:\\` lines join them — the API axis is vacuously green | link each requirement to the operations it governs |\n| \\`api.requirement-deprecated\\` (warn) | a requirement's \\`Operations:\\` list resolves only to operations the OpenAPI marks \\`deprecated: true\\` — the behaviour it governs is on its way out | migrate the requirement to the replacement operation, or retire it with the ops it governs |\n| \\`spine.landscape-invalid\\` | the living landscape does not parse, so the C4↔API spine cannot be checked | fix architecture/landscape.likec4 first |\n| \\`spine.op-undefined\\` | a landscape edge calls an operation this service's OpenAPI does not define | a broken contract between services — fix the edge or add the endpoint |\n| \\`spine.op-link-missing\\` (warn) | a landscape \"Calls\" edge into this service with no \\`metadata { op }\\` | link it to the operationId |\n| \\`spine.op-deprecated\\` (warn) | a landscape edge calls an operation this service's OpenAPI marks \\`deprecated: true\\` — the consumer is standing on a contract being retired | migrate the consumer to the replacement operation; deprecation is the first step of retiring an op, and the op stays defined until a human removes it |\n| \\`service.no-asyncapi\\` | no asyncapi.yaml, and message link(s) already point into it — a landscape edge's \\`metadata { publishes }\\` / \\`metadata { consumes }\\`, or a LIVING requirement's \\`Publishes:\\` / \\`Consumes:\\` line. The \\`details\\` list the stranded message names. Silent when nothing joins into it: an async contract is optional, and most services touch no topic | put the file back — every link the \\`details\\` name resolves to nothing until it is there |\n| \\`asyncapi.invalid\\` | asyncapi.yaml exists but does not parse — an unreadable contract proves nothing, so no \\`event.*\\` or message-spine finding is graded against it. The same code fires for a FEATURE's own asyncapi.yaml under \\`--feature\\`, where it gates the archive and suspends the service's event-axis checks the same way | fix the YAML first — the event axis is unchecked until it reads |\n| \\`asyncapi.remove-marker-living\\` | a living asyncapi.yaml carries feature-only \\`x-loam-remove: true\\` — at a \\`channels.<key>\\` / \\`operations.<key>\\` / \\`components.messages.<key>\\` slot, or nested on an inline channel message | remove the marker from the living contract; retire a slot through a feature delta's asyncapi.yaml instead |\n| \\`asyncapi.duplicate-message\\` (warn) | one message name is declared in two slots — every join on the name (an edge's \\`metadata { publishes }\\`, a requirement's \\`Publishes:\\` line) then picks one of them arbitrarily | give each declaration its own name, or factor the second into a \\`$ref\\` at the first |\n| \\`asyncapi.payload-undescribed\\` (warn) | message(s) whose payload declares no shape at all — absent, or bare \\`type: object\\` with not one property. The name joins the spine while nothing defines what a consumer would read. A presence probe: a payload declaring a non-JSON \\`schemaFormat\\` (Avro) is never judged | declare \\`properties\\` on the payload, or the \\`schemaFormat\\` of its non-JSON schema |\n| \\`asyncapi.ref-unresolved\\` (warn) | internal \\`$ref\\`(s) that resolve to nothing in the document — the target used to vanish silently: an operation's message-list entry contributed no name, and a dangling channel alias left a phantom key | fix or define the pointers the \\`details\\` name |\n| \\`spine.message-undefined\\` | a landscape edge \\`publishes\\`/\\`consumes\\` a message this service's asyncapi.yaml declares no \\`action: send\\`/\\`receive\\` operation for | declare the operation, or correct the edge — the fleet map and the contract disagree about what this service puts on the wire |\n| \\`spec-event.message-undefined\\` | a LIVING requirement's \\`Publishes:\\` / \\`Consumes:\\` line names a message this service's asyncapi.yaml does not declare in that direction | declare it, or correct the line |\n| \\`spine.message-unproduced\\` | this service consumes a message that NO service in the fleet and NO \\`#external\\` element declares is sent — no producer anywhere. The inverse of \\`spine.op-undefined\\`, and it has no HTTP analog: on the API axis the provider owns the contract and the check is local, while an event's schema lives in the producer's repo | find the producer and have it declare the message (an outside-the-fleet producer is declared on its \\`#external\\` element: an edge with \\`metadata { publishes '<msg>' }\\`), or correct the name — a consumer joined to nothing reads a payload nothing defines |\n| \\`spine.message-external\\` (warn) | this service consumes a message whose only declared producer is an \\`#external\\` element — produced outside the fleet, so no contract will ever appear in a \\`services/<id>/\\`, and this service's own asyncapi.yaml currently defines no shape for it. Fires on positive evidence (the \\`publishes\\` edge), so unreadable contracts elsewhere do not suspend it | declare the message in THIS service's asyncapi.yaml with its payload — the consumer's copy is the only contract the fleet can hold; once it declares a shape, the finding stops |\n| \\`asyncapi.message-contested\\` (warn) | two or more services declare they send one message name — every consumer's join picks one of them arbitrarily | namespace the message by its owning domain; one message has one producer, and which contract a consumer reads must not be a coin flip |\n| \\`event.messages-unlinked\\` (warn) | messages AND requirements exist but zero \\`Publishes:\\`/\\`Consumes:\\` lines join them — the event axis is vacuously green | link each requirement to the messages it governs |\n| \\`event.ungoverned\\` (warn) | message(s) declared in asyncapi.yaml that no requirement's \\`Publishes:\\`/\\`Consumes:\\` line names — \\`api.ungoverned\\`'s event twin, and the per-message half \\`event.messages-unlinked\\` cannot see: that code stays silent once even one message is linked | write the requirement that governs each named message, or drop the declaration |\n| \\`covers.unknown\\` (warn) | a \\`Covers:\\` entry in arch.spec.md resolves to no element, edge, alert or SLI — living and feature deltas alike | fix the id (the message offers close ones); a mistyped entry silently costs the coverage it was written for |\n| \\`api.response-ungoverned\\` (warn) | an operation declares a 4xx/5xx response that no scenario of any requirement governing it reaches. \\`api.ungoverned\\` grades whole OPERATIONS, so an endpoint with one happy-path requirement and twelve declared failure codes was fully governed by every other check — and the refusals are where the decision layer lives | write the refusal as a scenario, or as a row in an existing one. A scenario reaches a code when the number appears in its body, \\`Examples\\` rows included |\n| \\`obligation.unknown\\` | a \\`#obl-<name>\\` tag on the LIVING fleet map that \\`architecture/obligations.yaml\\` does not declare. An ERROR, and the asymmetry with its sibling below is the point: a tag that resolves to nothing reads exactly like a rule the fleet keeps, in review and in every derived view. \\`details\\` names every object carrying it | fix the tag (the message offers close names), or declare the obligation |\n| \\`obligation.unapplied\\` (warn) | a declared obligation that no \\`#obl-\\` tag applies anywhere on the map — the mirror of \\`permissions.unenforced\\`, and it earns its place for the same reason: a vocabulary is worth what cites it. Either a decision that was reversed and left its word behind, or one nobody has placed yet | tag the elements and edges it governs, or drop the declaration |\n| \\`obligation.uncovered\\` (warn) | a tagged element or edge that no LIVING requirement's \\`Covers:\\` line names — the obligation is placed and nothing says it is met. \\`c4.uncovered\\`'s question asked of the map the fleet actually runs on, rather than of a feature's \\`delta.likec4\\`. One finding per tagged object, \\`subject\\` = the service that owns it, so a fleet report reads as a worklist per team. Only a tag that RESOLVES is asked: an undeclared one has earned its error already | write the requirement in that service's \\`arch.spec.md\\` with the \\`Covers:\\` entry the message spells out, and a scenario that proves it — or drop the tag if the decision no longer applies there |\n| \\`obligation.adr-missing\\` | an obligation's \\`adr:\\` field names no file. The ADR is where the decision itself is written, so a pointer at nothing leaves every tag citing a rule nobody can read | fix the path (relative to the docs repo root), write the record, or drop the field |\n| \\`obligation.invalid\\` | \\`architecture/obligations.yaml\\` exists but does not read as a vocabulary. Reported ALONE and the family suspended behind it: every \\`#obl-\\` tag resolves against this file, so grading them on top would be a cascade, not a diagnosis. An id that a LikeC4 tag name cannot carry refuses the file here too — the id IS the tag suffix, and a rejected character truncates the tag rather than failing it | fix the YAML — the shape is \\`obligations: {<id>: {description, adr}}\\`, ids spelled with letters, digits, \\`_\\` and \\`-\\` only |\n| \\`permissions.unknown\\` | a \\`Requires:\\` entry in spec.md or arch.spec.md names a \\`<subject>/<permission>\\` that \\`architecture/permissions.yaml\\` does not declare. An ERROR where its \\`Covers:\\` sibling is a warning: an invented permission reads exactly like a real one in the requirement, in the generated scenario, and in the test somebody then writes against it | fix the spelling (the message offers close ones), or declare the permission in the fleet vocabulary |\n| \\`permissions.invalid\\` | \\`architecture/permissions.yaml\\` exists but does not read as a vocabulary. Reported alone: every \\`Requires:\\` line in the fleet resolves against it, so grading them on top would be a cascade, not a diagnosis | fix the YAML — the shape is \\`subjects: {<kind>: …}\\` then \\`permissions: {<kind>: {<name>: …}}\\`, and a permission under an undeclared subject kind is refused |\n| \\`permissions.unenforced\\` (warn) | a declared permission no requirement's \\`Requires:\\` line names, anywhere in the fleet — the mirror of \\`api.ungoverned\\`. Either a rule that was removed and left its word behind, or a word nobody adopted | write the requirement that gates on it, or drop the declaration; a vocabulary is worth what cites it |\n| \\`capability.unknown\\` | a \\`Capability:\\` entry in spec.md or arch.spec.md — living at the service target, or a feature's delta of either, where it gates \\`loam archive\\` (\\`--approve\\` overrides) — that NEITHER \\`architecture/capabilities.yaml\\` nor \\`capabilities/<id>/spec.md\\` declares. Silent when the fleet has neither (those FILES are this axis's opt-in, unlike \\`Requires:\\`) or the YAML is invalid | fix the spelling (the message offers close ones), or declare the capability — a name alone goes in the YAML, a name with prose gets the document |\n| \\`capability.invalid\\` | \\`architecture/capabilities.yaml\\` exists but does not read as a vocabulary. Reported exactly once per \\`--all\\` run, and the whole family is suspended behind it — including the archive gate, so an unknown name CAN merge while the file is broken; fix the YAML before trusting the axis | fix the YAML — the shape is \\`capabilities: {<id>: {description, owner}}\\`, ids flat keys with \\`/\\` allowed for nesting |\n| \\`capability.unrealized\\` (warn) | a declared capability that no living non-REMOVED requirement's \\`Capability:\\` line names — one warning PER capability, subject = the id, and the message names which side declared it. Either a promise nobody implemented or a word nobody adopted, and invisible from inside the file | write the requirement that realizes it (\\`loam list capabilities\\` shows the whole rollup), or drop the declaration |\n| \\`capability.doc-missing\\` (warn) | a directory in EITHER capability tree that holds no \\`spec.md\\` and has no capability beneath it — what \\`mkdir\\` leaves behind halfway through creating one. Under living \\`capabilities/\\` it declares nothing, so a \\`Capability:\\` line naming it still reports \\`capability.unknown\\` while a reader browsing the tree counts it as a capability. Under \\`features/<FEAT>/capabilities/\\` it is quieter: an empty directory is not a delta, so the merge carries nothing and the business change ships as zero. \\`subject\\` is the directory as it sits on disk, so it says which tree | living: write \\`capabilities/<id>/spec.md\\` (the narrative, then \\`## Requirements\\`). In a feature: write \\`features/<FEAT>/capabilities/<id>/spec.md\\` (a \\`## ADDED Requirements\\` section, ids and all). Either way, or remove the directory |\n| \\`capability.requirement-unidentified\\` | a requirement in \\`capabilities/<id>/spec.md\\` — living, or a feature's \\`features/<FEAT>/capabilities/<id>/spec.md\\` delta of it, where it gates \\`loam archive\\` (\\`--approve\\` overrides) — with no \\`Requirement-ID:\\` line. Optional in a service spec for OpenSpec compatibility, REQUIRED here: a capability document outlives every service that realizes it, so rewording a heading would be a removal and an addition, and every join made to it would break silently. Graded on what the delta would MERGE, so a REMOVED requirement is exempt | add \\`Requirement-ID: <id>\\` as the first body line |\n| \\`capability.requirement-service-scoped\\` | a requirement in \\`capabilities/<id>/spec.md\\` — living, or a feature's delta of it, where it gates \\`loam archive\\` (\\`--approve\\` overrides) — carrying \\`Operations:\\`, \\`Covers:\\`, \\`Publishes:\\` or \\`Consumes:\\` — each resolves against one service's own contract or model, so the requirement is a service requirement filed at the wrong altitude. This is the checkable half of \"a capability requirement is observable outside the fleet\"; the naming-no-service half is an authoring rule PR review holds, because matching service names in prose is a heuristic loam will not run | move it into that service's spec.md, where the line resolves — what belongs in the capability document is the promise, not the mechanism |\n| \\`capability.requirement-inert-join\\` | a requirement in \\`capabilities/<id>/spec.md\\` — living, or a feature's delta of it, where it gates \\`loam archive\\` (\\`--approve\\` overrides) — carrying \\`Capability:\\` or \\`Realizes:\\`: the axis's own two joins, which point INTO the tree and so do nothing written inside it. The rollup joins SERVICE requirements to capabilities, so a \\`Capability:\\` line here claims the document it is already in, and a \\`Realizes:\\` line here points at a promise loam will never see anybody keep. Refused rather than ignored because both parse and both read exactly like the working joins one directory over | delete the line; the join that matters is written on the service requirement, as \\`Realizes: <capability-id>#<Requirement-ID>\\` |\n| \\`capability.realizes-unknown\\` | a \\`Realizes:\\` entry in spec.md or arch.spec.md — living at the service target, or a feature's delta of either, where it gates \\`loam archive\\` (\\`--approve\\` overrides) — that names no capability requirement. Five distinct failures, each with its own fix in the message: the entry is not spelled \\`<capability-id>#<Requirement-ID>\\`; the capability is undeclared; it is declared but has no document; the document declares no requirements yet; or it declares requirements but not this id. Silent under the same ladder as \\`capability.unknown\\` | follow the message — it names which of the five happened, and offers close names where there are any |\n| \\`capability.realizes-stale\\` (warn) | a PINNED \\`Realizes:\\` entry whose capability requirement has been rewritten since the pin was stamped. The join still resolves — the address is fine and the promise behind it moved, which no existence check can see. Only pinned entries are graded, so a corpus that has never run \\`loam rebase --living\\` produces none of these, ever | re-read \\`capabilities/<id>/spec.md\\` and decide which side is now wrong: change this requirement, change the promise, or — when this requirement is still right as it stands — \\`loam rebase --living\\` to record the re-read |\n| \\`capability.requirement-unrealized\\` (warn) | a requirement in a capability document that no living service requirement's \\`Realizes:\\` line names, one warning PER requirement, subject = the \\`<capability>#<id>\\` entry an author would type to fix it. The sharper sibling of \\`capability.unrealized\\`, and the one that survives a healthy-looking row: a capability with four requirements and three realized reports nothing at all through the other code | add the line to each service requirement that carries part of the promise — or leave it, if the promise is genuinely not implemented yet: writing the business document ahead of the fleet is the intended use, which is why this warns and never gates |\n| \\`capability.uncovered\\` (warn, GATES archive) | a requirement a feature's \\`features/<FEAT>/capabilities/<id>/spec.md\\` ADDS that no ADDED or MODIFIED requirement in the same feature's \\`specs/\\` deltas names with \\`Realizes: <capability-id>#<Requirement-ID>\\`, subject = the capability id. A WARNING because the document is legal — writing a promise ahead of the fleet is the intended use — that GATES because the merge is what is unsafe: once it lands, the only thing that will ever mention the promise again is \\`capability.requirement-unrealized\\`, a fleet-scope warning nobody reads. MODIFIED is exempt (the promise may already be realized by living text), and a requirement with no \\`Requirement-ID:\\` is left to \\`capability.requirement-unidentified\\` alone | add the \\`Realizes:\\` line to the service requirement that carries part of the promise; or, if the promise CROSSES services, write the flow that keeps it as \\`features/<FEAT>/usecases/<name>.likec4\\` — a views-only document holding one \\`dynamic view\\` tagged \\`#cap-<capability>\\` and \\`#req-<requirement>\\`, which \\`loam archive\\` merges into \\`architecture/usecases/\\` and \\`loam unarchive\\` takes back; or \\`loam archive <FEAT> --approve\\` to land the promise ahead of the fleet and let \\`capability.requirement-unrealized\\` carry it |\n| \\`usecase.flow-invalid\\` | a \\`features/<FEAT>/usecases/*.likec4\\` loam could not read against the map this feature's own merge would leave behind — a parse error, a hop naming an element the merge does not land, or a landscape merge that itself refuses. The flows were NOT graded, and the archive would copy them into \\`architecture/\\` for the next reader's \\`loam validate --all\\` to fail on. \\`--approve\\` does NOT move it (\\`overridable: false\\`): that flag overrides loam's judgment about coherence, never its ability to read an axis | fix what the message names. A hop into an element the feature adds is legal — the flow is graded against the POST-MERGE map — so the usual cause is a spelling that matches neither the living landscape nor \\`delta.likec4\\` |\n| \\`capability.remove-requirement-realized\\` | the same join in the removal direction: a feature's capability delta REMOVES a requirement that something the merge leaves behind still names with \\`Realizes:\\` — a living service requirement this feature does not also change, or one of its own ADDED/MODIFIED requirements. Without it the archive succeeded and the very next \\`loam validate --all\\` failed with \\`capability.realizes-unknown\\` against a service document nobody in the feature had touched. An ERROR, exactly as \\`openapi.remove-op-consumed\\` is for the identical shape one axis over; \\`--approve\\` overrides | retire the realizing requirement in the same feature, or drop its \\`Realizes:\\` line (a MODIFIED does that by restating the requirement without it), or \\`loam archive <FEAT> --approve\\` to break the join deliberately |\n| \\`glossary.term-exists\\` | a \\`features/<FEAT>/glossary/<term>.md\\` whose term the LIVING glossary already defines. The merge is a whole-file copy, so it would replace an authored definition wholesale — and \\`--approve\\` does NOT move this one (\\`overridable: false\\` in the \\`--json\\` refusal), because approving it is approving a deletion nobody described. A feature-local glossary document INTRODUCES a term; it does not rewrite one | delete the feature's copy and edit \\`glossary/<term>.md\\` directly in the same change, where git produces an ordinary conflict if somebody else is editing it too |\n| \\`usecase.flow-exists\\` | a \\`features/<FEAT>/usecases/<name>.likec4\\` whose flow the LIVING \\`architecture/usecases/\\` already holds. The merge is a whole-file copy, so it would replace an authored hop sequence wholesale — and \\`--approve\\` does NOT move this one (\\`overridable: false\\` in the \\`--json\\` refusal), because a \\`dynamic view\\`'s ordered steps are written down in no other document the fleet keeps. A feature-local flow INTRODUCES a use case; it does not rewrite one | delete the feature's copy and edit \\`architecture/usecases/<name>.likec4\\` directly in the same change, where git produces an ordinary conflict if somebody else is editing it too |\n| \\`deployment.doc-exists\\` | a \\`features/<FEAT>/deployment/<name>.likec4\\` whose file the LIVING \\`architecture/\\` already holds. The merge is a whole-file copy, so it would replace an authored topology wholesale, and \\`--approve\\` does NOT move it — the same act, and the same severity, as \\`usecase.flow-exists\\` one axis over. The collision is on the FILE, never on what is inside it: two features may both \\`extend\\` one living region from documents of their own and both archive | delete the feature's copy and edit \\`architecture/<name>.likec4\\` directly in the same change — or, to ADD to what that file declares, rename the feature's document and say \\`extend\\` in it |\n| \\`deployment.doc-invalid\\` | a \\`features/<FEAT>/deployment/*.likec4\\` loam could not read against the map this feature's own merge would leave behind — a parse error, an unresolved element, or a landscape merge that itself refuses. The corpus is the feature's \\`deployment/\\` PLUS the living \\`architecture/\\` documents it \\`extend\\`s, so the fault may be in either and every message names its file. \\`--approve\\` does not move it: the flag overrides loam's judgement about coherence, never its ability to read an axis | fix what the messages name. A document instancing an element only this feature's \\`delta.likec4\\` adds is LEGAL and resolves — that is what the merge preview is for; one naming an element the merge does not land is the case this refuses |\n| \\`deployment.doc-reserved\\` | a \\`features/<FEAT>/deployment/<name>.likec4\\` whose name loam owns inside \\`architecture/\\`: \\`subsystems.likec4\\` (generated, overwritten wholesale by \\`loam subsystem sync\\`, and parsed by nothing — so topology landing there is invisible from the moment it archives), \\`landscape.likec4\\` (spliced into, never replaced), or any path under \\`usecases/\\` (the flow tree). Refused whether or not the target exists, which is the point: two of the three fail silently when it does not | rename the feature's document — its name is yours to choose, and \\`extend\\` is how it adds to what a living document already declares |\n| \\`glossary.unlinked\\` (warn) | a term defined in \\`glossary/<term>.md\\` that no document OUTSIDE \\`glossary/\\` links to — the mirror of \\`permissions.unenforced\\` and \\`capability.unrealized\\`, and it earns its place for their reason: a vocabulary is worth what cites it. A term cited only by OTHER TERMS is still unlinked and the citing terms are listed in \\`details\\` — a glossary is a network of definitions, so \\`order-line\\` linking to \\`order\\` says the glossary is consistent, not that the fleet uses the word. When NOTHING outside cites anything it is one finding for the whole glossary rather than one per term, because that is one afternoon's work and not N breaches. Warn and never a gate: writing the vocabulary ahead of the fleet is the intended use | cite the term from the requirement, capability or intent that uses it — \\`[Order](../../glossary/order.md)\\` — or delete the definition. \\`loam list glossary\\` shows every term with the documents that cite it |\n| \\`link.unresolved\\` | a markdown link in an authored document whose relative target names nothing. ONE finding per document, listing every broken link with its line, because a rotted set of links is one editing session rather than ten separate breaches. The corpus is every authored markdown document: both spec axes living and delta, \\`runbook.md\\`, every \\`adrs/\\` file at all three altitudes, \\`intent.md\\`, and each capability document — the fleet's own ADRs and living capability tree are graded once, on the \\`landscape\\` target, and never repeated per service. Three things are deliberately NOT graded, each of them a question loam has no answer to: a target outside the docs repo (that tree may not be checked out beside these documents), a bare \\`#section\\` anchor, and anything inside a fenced block or an inline code span. CASE **is** graded — a link resolves only when each path segment is spelled the way the directory stores it, and when only the case is wrong the finding names the stored spelling, so a link that works on a Windows laptop and 404s on GitHub is caught here rather than in review | fix the path, or write the document it points at. A link here is a join of the same kind as \\`Operations:\\` and \\`Covers:\\` — write it as a relative path, which is the only form that can resolve |\n| \\`link.unreadable\\` | a document in that corpus could not be decoded — almost always UTF-16, which PowerShell's \\`>\\` and \\`Out-File\\` write unasked. Its links were not checked; everything else about the service or feature still was, which is exactly why this is not \\`service.unreadable\\` | re-save the file as UTF-8 (\\`Out-File -Encoding utf8\\`) |\n| \\`docs.commit-interrupted\\` | a \\`.loam-commit\\` journal sits in the docs repo: a writer's commit never finished, so the living docs may be half-merged — nothing graded in the same run can be trusted to describe one state, and a green here would certify bytes no run produced | run the command the finding names (it recovers first), or \\`loam doctor\\` for the file-by-file account; the finding leads every validate mode until the journal is gone |\n| \\`health.invalid\\` (warn) | health.yaml exists but does not parse — alert/SLI ids are unreadable, so \\`Covers: alert:/sli:\\` entries and health coverage are unchecked (a missing health.yaml is legal and silent; an unreadable one used to masquerade as \\`covers.unknown\\` typos) | fix the YAML — the health axis resumes once it reads |\n| \\`health.uncovered\\` (warn) | health.yaml declares an alert or SLI no arch.spec.md requirement covers | write the arch requirement with \\`Covers: alert:<id>\\` / \\`Covers: sli:<id>\\` — a signal nothing tests is dashboard decoration |\n| \\`health.dependency-unmodelled\\` (warn) | a \\`dependencies:\\` id in health.yaml that nothing in this service's OWN model.likec4 answers to — by element id, \\`metadata { service }\\` binding, or title. Resolved against the service model, never the landscape: the model carries everything the service touches (private stores included, as nested containers), and the on-call file must name the same world. Either a dependency nobody modelled, or one that no longer exists — for example, a disabled optional service registry left as \\`critical: startup\\` under a green validator | model it (an external system as its own element with the real edge; a private store as a nested container), correct the id to one of the element's three names, or delete the entry |\n| \\`gherkin.missing\\` (warn) | a living scenario no generated .feature scenario carries a digest for — the suite has no test for these words. Fires only in the service's repo, once \\`<gherkinDir>/loam/\\` exists | \\`loam gherkin --service <id>\\` regenerates the suite |\n| \\`gherkin.stale\\` (warn) | a generated scenario's digest matches no living scenario while its requirement still exists — the spec moved under the file (a reworded scenario reports stale + missing together) | regenerate; never edit a generated file to catch it up |\n| \\`gherkin.orphaned\\` (warn) | a generated file whose requirement no longer exists in the living spec (a file for a feature still in flight is exempt — it answers to its feature until it archives) | regenerate; the orphaned file is deleted and reported |\n| \\`gherkin.path-outside\\` | this repo's \\`gherkinDir\\` resolves outside the repository — a \\`..\\` segment, an absolute path, or a symlink pointing away — so generating would write where nothing here can review it. Refused before any file is read, not repaired | point \\`gherkinDir\\` in loam.json at a directory INSIDE this repository; it is the suite's own home, and a path outside it is a write nobody's diff would show |\n| \\`evidence.checked\\` (ok) | every evidence pin on the federated verification records this service attested re-resolved clean against this working tree. Fires only in the service's own repo, like \\`sources.*\\` — from the docs repo the whole \\`evidence\\` family is silent | nothing — it is the confirmation |\n| \\`evidence.unpinned\\` (ok) | agent-confirmed citations recorded before evidence pins existed, so nothing can grade their drift | the next \\`loam verify <FEAT> --service <id> --record\\` stamps pins; not work on its own |\n| \\`evidence.unresolved\\` (warn) | a pinned citation cannot be re-checked: the cited file is gone, unreadable, unsafe, not a regular file, or the cited line is past its end | re-read the evidence in this tree, re-answer, and re-record from this repo — a pin lint never moves the verdict by itself |\n| \\`evidence.moved\\` (warn) | a cited file's normalized sha256 differs from the pin while the cited line's text survives — the mildest drift: the file changed around the citation | re-read the cited file; re-record once the answers are re-checked |\n| \\`evidence.line-changed\\` (warn) | the cited line no longer says what the pin recorded — the citation no longer shows what it showed | re-read the evidence and re-answer; the claim may still be true, but this line no longer backs it |\n| \\`evidence.token-missing\\` (warn) | the cited file contained the literal the claim asserts (the operationId, message name or edge op the pin carries as \\`token\\`) at the attested commit, and no longer does — a token the commit never contained is warned once at record time and never stamped, so this grade always convicts a disappearance | check whether the code moved or the claim stopped being true; re-answer and re-record from this repo |\n| \\`evidence.record-unreadable\\` (warn) | a feature's verification.yaml exists but cannot be read as a record (broken YAML, a malformed \\`evidence_pins\\` block, a summary contradicting its claims) — none of its evidence was checked, and a silent skip would read as clean | repair the YAML by hand, or re-record from this repo; loam never overwrites a record it could not read |\n\n\\`--feature <FEAT-id>\\` — a change's three axes against each other. The SAME checks\ngate \\`loam archive\\` (errors block it; warnings never do), so a clean run here is\nwhat lets a feature ship:\n\n| code | what it means | what to do |\n|---|---|---|\n| \\`delta.invalid\\` | delta.likec4 does not parse | fix this first — an unreadable axis makes every other check meaningless |\n| \\`delta.nothing-tagged\\` | the delta declares elements/relationships but none carry \\`#<FEAT>\\` | tag what IS the change — untagged parts are context, and archive merges only tags |\n| \\`delta.service-unknown\\` | a \\`specs/<svc>/\\` directory addresses a service that neither exists as \\`services/<svc>/\\` nor is introduced by this feature's own tagged delta.likec4 — a typo in \\`--touches\\`, which archive would otherwise materialise as a phantom service directory | fix the id (the message offers close ones); suspended while \\`delta.invalid\\` stands, because an unparseable delta cannot be asked what it introduces |\n| \\`delta.service-id-invalid\\` | a \\`specs/<svc>/\\` DIRECTORY NAME that is not a legal service id (a space, a \\`..\\`, a Windows-reserved stem like \\`NUL\\`, a trailing dot) — archive would materialise \\`services/<svc>/\\` from it, a directory every authoring command then refuses to address (\\`service.id-invalid\\`). Never suspended: the name is illegal whatever the delta says | rename the specs/ directory to a legal id; \\`--approve\\` does not override this one — the breakage is mechanical, not a judgment call |\n| \\`spec.merge-conflict\\` | a spec delta (\\`specs/<svc>/spec.md\\` or \\`arch.spec.md\\`) still holds \\`<<<<<<<\\` / \\`=======\\` / \\`>>>>>>>\\` markers — the same code and the same sentence as service scope, because the markers merge into the living document as prose under somebody's requirement | resolve the conflict before archive; the merge rewrites the requirements section and the marker lines inside it disappear with it |\n| \\`requirements.stepless-scenario\\` | a scenario in a spec delta with a heading and no steps — same code as service scope, and it merges into the living spec as a requirement the coverage rule calls covered from then on | write the body as \\`- **Given/When/Then**\\` bullets |\n| \\`requirements.assertionless-scenario\\` | a scenario in a spec delta with steps and no \\`Then\\` — same code as service scope, and it merges into the living spec as acceptance criteria that assert nothing | add the \\`- **Then**\\` the scenario was written for |\n| \\`requirements.examples-unbound\\` / \\`requirements.examples-unreferenced\\` | a delta scenario whose \\`Examples\\` header and \\`<placeholders>\\` disagree — same codes and same sentences as service scope | add or correct the column; reference it, or drop it |\n| \\`spec-api.op-undefined\\` | a requirement governs an operation its provider's OpenAPI does not define | define the endpoint, or correct the \\`Operations:\\` line |\n| \\`spec-api.op-pending\\` (warn) | the governed operation is defined by another feature in flight | archive that feature first |\n| \\`c4-api.op-undefined\\` | an edge calls an operation the target does not expose | a broken contract between services — fix the caller or add the endpoint |\n| \\`c4-api.op-pending\\` (warn) | the called operation is defined by another feature in flight | archive that feature first |\n| \\`c4-api.op-deprecated\\` (warn) | a NEW tagged edge consumes an operation the living provider's OpenAPI marks \\`deprecated: true\\` — building new consumption on a dying op (quiet when this feature's own openapi delta drops the flag: that IS the un-deprecation) | point the edge at the replacement operation, or say why the deprecated one is right; never gates archive |\n| \\`c4-api.op-removing\\` | a NEW tagged edge consumes an operation this feature removes | remove or redirect the new edge; one feature cannot retire and add consumption of the same operation |\n| \\`openapi.remove-target-missing\\` | an \\`x-loam-remove: true\\` marker's exact path+method does not exist in living OpenAPI | update the stale marker to the current slot, or drop it if the operation is already gone |\n| \\`openapi.remove-target-mismatch\\` | the marker's operationId differs from the living operation at that path+method | name the living operation exactly; loam never deletes the different operation occupying the slot |\n| \\`openapi.remove-marker-missing\\` | a REMOVED requirement governs an operation but the feature has no removal marker | add the exact path/method operation with its operationId and \\`x-loam-remove: true\\` to the feature openapi.yaml |\n| \\`openapi.remove-marker-unjustified\\` | a removal marker is not named by any REMOVED requirement's \\`Operations:\\` line | remove the marker or retire the governing requirement in the same feature |\n| \\`openapi.remove-marker-anonymous\\` | an \\`x-loam-remove: true\\` marker with no \\`operationId\\` — loam cannot tell which operation it retires, and the marker itself would be written into the living contract | name the operation the marker retires, exactly as the living contract spells it |\n| \\`openapi.remove-op-consumed\\` | the feature retires an operation the LIVING fleet still consumes — a landscape edge's \\`metadata { op }\\`, or another service's living requirement naming it | fix the consumer in the same feature (redirect the edge, retire its requirement), or archive with \\`--approve\\` if the consumer is already gone in code |\n| \\`c4-event.message-undefined\\` | a tagged edge \\`publishes\\`/\\`consumes\\` a message its bound service's asyncapi contract — as this feature's merge would leave it; feature ∪ living where no delta exists — declares no matching \\`action: send\\`/\\`receive\\` operation for | declare the operation in the feature's asyncapi.yaml, or correct the edge |\n| \\`c4-event.message-pending\\` (warn) | the named message is introduced by another feature in flight | archive that feature first |\n| \\`spec-event.message-undefined\\` | a delta requirement's \\`Publishes:\\`/\\`Consumes:\\` line names a message the service's asyncapi contract — as this feature's merge would leave it, so a retired declaration stops answering and a leftover line on a surviving requirement is caught before it breaks the post-archive fleet — does not declare in that direction; the same code validate gives the living-side breach | declare it in the feature's asyncapi.yaml, retire the line with its requirement, or correct it |\n| \\`spec-event.message-pending\\` (warn) | the named message is introduced by another feature in flight | archive that feature first |\n| \\`asyncapi.baseline-missing\\` (warn, gates archive; one per service) | slots or component surfaces in this feature's asyncapi.yaml carry no baseline pin (\\`x-loam-based-on\\` / \\`x-loam-baselines\\`) | run \\`loam rebase <FEAT>\\` — until then the merge cannot tell the slots and surfaces you EDITED from the ones you merely restated, and every restated one would revert whatever landed on it. \\`--approve\\` merges unpinned deliberately |\n| \\`asyncapi.baseline-stale\\` | the living slot, or the living component surface, changed after this delta pinned it | somebody landed a change in between. Re-read the living value, fold in what you still mean, then \\`loam rebase <FEAT>\\` |\n| \\`asyncapi.baseline-invalid\\` | an \\`x-loam-based-on\\` that is not a digest or one on a slot the living contract has no counterpart for; a malformed \\`x-loam-baselines\\` record, or an entry of it naming a component surface the delta does not declare (or whose living value is gone) | \\`loam rebase\\` writes both; a new channel, operation, message or component has no living version to be based on, so it carries no pin |\n| \\`asyncapi.remove-target-missing\\` | an \\`x-loam-remove: true\\` marker addresses a slot the living asyncapi does not have | update the stale marker to the current key, or drop it if the slot is already gone |\n| \\`asyncapi.remove-target-mismatch\\` | the marker's message name differs from the living declaration at that key | name the living message exactly; loam never deletes a different message occupying the slot |\n| \\`asyncapi.remove-marker-missing\\` | a REMOVED requirement's \\`Publishes:\\`/\\`Consumes:\\` line names a message the living contract still declares, with no matching marker in the feature's asyncapi.yaml | add the \\`components.messages\\` entry with the message's name and \\`x-loam-remove: true\\`; a message the living contract declares only INLINE under a channel is retired by that channel's own removal marker (\\`channels.<key>: {x-loam-remove: true}\\`) instead |\n| \\`asyncapi.remove-marker-unjustified\\` | a message-removal marker no REMOVED requirement's \\`Publishes:\\`/\\`Consumes:\\` line justifies (channel and operation slots need exactness only — requirements never join on their keys) | remove the marker, or retire the governing requirement in the same feature |\n| \\`asyncapi.remove-message-consumed\\` | the feature retires a message the LIVING fleet still consumes — a landscape \\`consumes\\` edge, or another service's living \\`Consumes:\\` requirement | fix the consumer in the same feature (drop the edge, retire its requirement), or archive with \\`--approve\\` if it is already gone in code |\n| \\`asyncapi.message-conflict\\` (warn) | two features in flight add or edit the same (service, message) | whichever archives second replaces the other's declaration wholesale — coordinate before either ships |\n| \\`c4.op-ungoverned\\` (warn) | an operation is called but no requirement governs it | write the requirement |\n| \\`c4.op-link-missing\\` (warn) | a \"Calls\" edge in the delta with no \\`metadata { op }\\` | link it to the operationId |\n| \\`c4.service-binding-invalid\\` | an explicit \\`metadata { service }\\` binding — a tagged element's, or an untagged child's anywhere inside its block, since the merge splices the whole authored block verbatim — is not a legal \\`services/<id>/\\` name (a space, a \\`../\\`, a Windows-reserved stem); archive would write the name into the living landscape, and a \\`../\\` collapses its \\`services/\\` probe out of the docs repo | fix the binding to the directory the element means; \\`--approve\\` does not override this one — the path the name becomes is mechanical, not a judgment call |\n| \\`api.op-unconsumed\\` (warn) | an added operation no edge consumes | model the caller, or say why it is provider-only |\n| \\`service.no-requirement-delta\\` (warn) | a new service with no spec delta | write \\`specs/<svc>/spec.md\\` |\n| \\`scaffold.placeholder\\` (warn, gates archive) | a feature document still carries \\`loam new\\`'s exact placeholder text — a \\`TODO — name the behaviour\\` requirement, a \\`TODO — name the case\\` scenario, an \\`<angle-bracket>\\` fill-in, or a \\`TODO — what this service owns\\` description | write the real content; the merge would otherwise publish a requirement or description nobody authored. \\`--approve\\` overrides |\n| \\`intent.empty\\` (warn, gates archive) | intent.md is missing, or its Why/Scope hold nothing outside the scaffold's own comments | write why the feature exists — it is the first thing a reviewer reads. \\`--approve\\` overrides |\n| \\`archedge.uncovered\\` (warn) | no scenario names a tagged edge (a heuristic) | write the scenario, or say why the edge needs none |\n| \\`c4.uncovered\\` (warn) | a NEW tagged element or edge in delta.likec4 that no arch requirement covers via \\`Covers:\\` — the mechanical check, not the heuristic above | add the requirement to \\`specs/<svc>/arch.spec.md\\` (outbox? retries? alerts?), or the architectural obligations ship unchecked |\n| \\`delta.unknown-section\\` | a heading that nearly matches the delta grammar | fix it — everything under it merges as NOTHING today, silently |\n| \\`delta.no-delta-sections\\` | requirements, but no \\`## ADDED/MODIFIED/REMOVED Requirements\\` section anywhere — the whole delta would merge nothing | put every changed requirement under its delta section |\n| \\`delta.requirement-not-merged\\` (warn, gates archive) | a requirement under a prose heading (\\`## Behavior\\`) instead of a delta section | move it under \\`## ADDED\\`/\\`## MODIFIED\\`/\\`## REMOVED Requirements\\` — as written, archive drops it. If it really is documentation, quote it under \\`## Requirements\\`, which is exempt |\n| \\`delta.requirement-id-invalid\\` / \\`delta.requirement-id-repeated\\` / \\`delta.requirement-id-duplicate\\` | a stable ID is malformed or ambiguous inside the delta | repair it before archive; identity is never inferred from an invalid declaration |\n| \\`delta.living-requirement-id-invalid\\` | the living file's IDs are malformed or ambiguous | repair the living spec first; the delta cannot select it safely |\n| \\`delta.requirement-identity-collision\\` | the delta's ID and heading point at different living requirements | fix the ID or heading; archive will not guess which identity should win |\n| \\`delta.modified-unknown\\` | MODIFIED a requirement the living spec does not have | use ADDED, or fix the name (a spelling slip reads as a different requirement) |\n| \\`delta.removed-unknown\\` | REMOVED one that does not exist | drop the section, or fix the name |\n| \\`delta.added-duplicate\\` | ADDED a name the living spec already has | use MODIFIED — as written, the merge REPLACES the living requirement |\n| \\`delta.added-near-duplicate\\` (warn) | ADDED a name that differs only in case from a living requirement — the merge matches exactly, so both would coexist | match the living spelling and use MODIFIED, or pick a distinct name |\n| \\`delta.living-duplicate-requirement\\` | two requirements in the LIVING document share one heading — MODIFIED rewrites only the first and REMOVED deletes both, so no delta applies to it predictably | fix the living spec first (merge the bodies, or give each a distinct heading and a \\`Requirement-ID\\`); the delta cannot select safely until you do |\n| \\`delta.modified-pending\\` (warn) | the requirement is introduced by another feature in flight | archive that feature first — \\`loam dependencies --json\\` computes the whole ordering (\\`order\\`), so you do not have to work it out per finding |\n| \\`delta.removed-pending\\` (warn) | REMOVED something another feature in flight introduces | archive that feature first; \\`loam dependencies --json\\` says in which order |\n| \\`delta.added-conflict\\` (warn) | two features in flight add the same requirement | whichever archives first lands it; the other's ADDED then collides with the living spec (\\`delta.added-duplicate\\`, error) and its archive is refused — coordinate now, or rework the later one as MODIFIED after the first ships. No ordering fixes it, which is why \\`loam dependencies\\` reports it under \\`conflicts\\` rather than in \\`order\\` |\n| \\`delta.modified-conflict\\` (warn) | another feature in flight MODIFIES or REMOVES the same living requirement | both deltas apply cleanly, so whichever archives second replaces the other's text wholesale — \\`loam dependencies\\` lists it under \\`conflicts\\`; merge the two intentions before either ships |\n| \\`delta.baseline-stale\\` | the living requirement changed after this delta was written against it | someone landed a change in between and this MODIFIED would replace it outright. Re-read the living requirement, fold in what you still mean, then \\`loam rebase <FEAT>\\`. Re-pinning without re-reading is how you overwrite them on purpose |\n| \\`delta.baseline-missing\\` (warn, gates archive) | a MODIFIED/REMOVED requirement with no \\`Based-On:\\` | run \\`loam rebase <FEAT>\\` — until then nothing can say whether the living text moved under this delta, and archive refuses to merge it. Expected on deltas adopted from OpenSpec, which never carried the line: one rebase pins the whole feature, and \\`--approve\\` merges unpinned deliberately |\n| \\`delta.baseline-invalid\\` | a \\`Based-On:\\` that is not a digest, declared twice, or sitting on an ADDED requirement | \\`loam rebase\\` writes the value; an ADDED requirement has no living version to be based on, so drop the line or make it MODIFIED |\n\n\\`--all\\` — everything above for every target, plus the fleet cross-check:\n\n| code | what it means | what to do |\n|---|---|---|\n| \\`landscape.missing\\` | \\`architecture/landscape.likec4\\` does not exist — an ERROR as soon as \\`services/\\` holds one service, a warning only in an empty docs repo | create it: a \\`specification { ... }\\` block declaring the kinds, then a \\`model { ... }\\` block with one bound element per service (\\`metadata { service '<id>' }\\`) and one edge per cross-service call carrying \\`metadata { op '<operationId>' }\\`. With no fleet map every cross-service check is blind, not passing |\n| \\`landscape.invalid\\` | the living landscape does not parse | fix it first — the fleet cross-check cannot run against a document nobody can read |\n| \\`service.id-invalid\\` | a \\`services/<id>/\\` directory whose name is not a legal service id — every authoring command refuses it, so nothing in that directory can be changed through loam. Fleet scope only: the SET of service directories is what makes the id a question, and a \\`--service\\` run is already refused by its own guard before it gets this far (the guard refuses only a name that neither exists as a directory nor is a legal id — a badly-named directory that DOES exist resolves through the enumeration and grades as an ordinary target, still without this finding). Graded before the map is opened, so it stands even when the landscape is missing or unreadable | rename the directory, then update its \\`service:\\` frontmatter, its \\`metadata { service }\\` binding in the landscape, and any \\`features/<FEAT>/specs/<id>/\\` naming it |\n| \\`subsystem.unmarked\\` | a directory under \\`services/\\` that is neither a subsystem (no \\`subsystem.yaml\\`) nor a service (no service artifact), with directories beneath it — the shape an ordinary merge produces when one branch deletes an emptied subsystem's marker while another moves a service in. The services beneath STAY enumerated and the finding names every one of them: the fleet is never reported smaller than it is | restore \\`subsystem.yaml\\` in that directory (an empty file is a valid marker), or move the directories out |\n| \\`subsystem.marker-misplaced\\` | \\`subsystem.yaml\\` sitting BESIDE service artifacts (spec.md, model.likec4, …) — a directory is a subsystem or a service, never both; it stays classified as a service | delete the marker, or move the artifacts into a service directory of their own inside the group |\n| \\`subsystem.name-collision\\` | one name claimed by two or more directories in the \\`services/\\` tree — service ids and subsystem names share one flat namespace, unique at any depth, because a service's identity is its leaf directory name alone and placement never disambiguates it. The message names every claimant | rename all but one, then update whichever frontmatter, landscape bindings and \\`features/<FEAT>/specs/<id>/\\` name the renamed service |\n| \\`subsystem.name-invalid\\` | a subsystem directory name that breaks the id grammar (a space, a \\`..\\`, a Windows-reserved stem, a trailing dot) — subsystem names take the service-id grammar because both become directory names in one shared tree. Its services stay enumerated meanwhile | rename the directory to a legal name |\n| \\`subsystem.invalid\\` | a \\`subsystem.yaml\\` that exists but cannot be read as subsystem metadata — presence still classifies the directory as a subsystem (content is metadata, never identity), and exactly one finding is emitted per file | fix the file: a YAML mapping with optional \\`title\\`, \\`description\\` and \\`owner\\`; never \\`members\\` — the directory itself is the membership record. An empty file is also a valid marker |\n| \\`subsystem.views-stale\\` | \\`architecture/subsystems.likec4\\` — the GENERATED file holding one LikeC4 view per subsystem — does not match the tree: it drifted after a move, it is absent while subsystems exist, or it lingers after the last subsystem was removed. One error on one file, graded by byte comparison against the deterministic re-render (no check ever reads the file's content) | run \\`loam subsystem sync\\` — the single regenerator. Never edit the file by hand; authored views belong in the landscape or a file of your own |\n| \\`subsystem.view-id-collision\\` | \\`architecture/landscape.likec4\\` declares a view whose id is one loam GENERATES into \\`architecture/subsystems.likec4\\`. LikeC4 merges every file in \\`architecture/\\` into one project and view ids are a flat namespace there, so the renderer refuses the WHOLE project and draws nothing — while every other check here stays green. Only a real collision is reported: a \\`subsystem_\\`-looking id matching no current subsystem breaks nothing yet, and fires on the run after somebody creates that subsystem | rename the view in \\`architecture/landscape.likec4\\`. Ids beginning \\`subsystem_\\` are loam's to mint; \\`architecture/subsystems.likec4\\` is generated (\\`loam subsystem sync\\`) and is never hand-edited |\n| \\`landscape.merge-conflict\\` | the fleet map still holds conflict markers — it is two halves of two different maps, and every cross-service check reads it. Checked BEFORE the parse and returns like \\`landscape.invalid\\`: nothing is concluded from the file, so no \\`landscape.service-unmodelled\\` fires behind it | resolve it keeping BOTH sides' elements and edges; ten people adopting ten services into one landscape.likec4 in the same week is how it happens. \\`loam archive\\` refuses on it too (\\`merge-failed\\`, not overridable with \\`--approve\\`) |\n| \\`landscape.service-unmodelled\\` | a \\`services/<svc>/\\` no element in the landscape resolves to | draw it, or bind an existing element with \\`metadata { service '<svc>' }\\` — the fleet map is incomplete until you do |\n| \\`landscape.service-undocumented\\` (warn) | a landscape element with no \\`services/<id>/\\` | document the service, bind the element to the directory it means, or tag it \\`#external\\` if it is not ours |\n| \\`landscape.binding-unknown\\` | an element's \\`metadata { service }\\` names a directory that does not exist | fix the id or create the service — a binding is a claim, and this one is false |\n| \\`landscape.binding-duplicate\\` (warn) | two elements resolve to the same \\`services/<id>/\\` | every element→service join picks one of them arbitrarily, so the other's edges are filed under a service that does not own them — bind each element to the directory it actually is, or tag the stray one \\`#external\\` |\n| \\`landscape.kind-tag-graded\\` | the \\`specification\\` block declares \\`#external\\` or \\`#platform\\` on an element KIND, and at least one element of that kind stands for a real \\`services/<id>/\\` — bound with \\`metadata { service }\\`, or titled like one. Since LikeC4 1.59.0 a kind's tags land on every element of it, and loam reads both tags as \"not ours, stop grading\", so the exemption is silencing the checks for a service of yours; the run would otherwise end on \\`landscape.matched\\` over a map nothing checked | remove the tag from the kind and write \\`#external\\` / \\`#platform\\` on the individual elements that really are foreign — or stop drawing your own services as that kind. A graded tag on a kind whose elements stand for no directory of yours (the \\`element topic { #external }\\` idiom for the topics inside a broker) is correct and stays silent, and any OTHER tag is safe on any kind — loam does not read it |\n| \\`landscape.platform-candidate\\` (warn) | an \\`#external\\` element consumed by three or more services and not tagged \\`#platform\\` — a hub on its way to making the fleet view unreadable | declare \\`tag platform\\` in the \\`specification\\` block and tag the element (LikeC4 refuses an undeclared tag, so both steps are needed): the fleet view then excludes it, and a platform view over \\`include * -> element.tag = #platform\\` keeps \"who depends on it\" answerable |\n| \\`landscape.datastore-private\\` (warn) | a datastore element with exactly one consumer service at fleet level — drawn as a peer, it reads as a system in its own right | it is that service's internals: move it into \\`services/<id>/model.likec4\\` as a nested container and delete it from the landscape, or add the second consumer's edge if another service really reaches the same data |\n| \\`landscape.datastore-shared\\` (warn) | a datastore element consumed by two or more services — the strongest coupling in the fleet, stated rather than inferred | shared means the same DATA: if they read the same tables or keys, keep it drawn and let the warning state the coupling; if they only share a host or cluster (two schemas, two lock paths), model one private store per service and put the blast radius in the runbook |\n| \\`usecase.capability-unresolved\\` | a \\`dynamic view\\` tagged \\`#cap-<slug>\\` whose slug matches no declared capability id, or more than one. The tag spells the id with every character outside \\`[A-Za-z0-9_-]\\` flattened to \\`-\\` (that is all a LikeC4 tag name accepts) and that flattening is not injective, so \\`payments/refunds\\` and \\`payments-refunds\\` both answer to one tag. Silent while \\`architecture/capabilities.yaml\\` is absent or invalid, like the rest of that family — the FILE is the capability axis's opt-in | zero matches: fix the tag, or declare the capability in the fleet vocabulary (the message offers close ids, each already spelled as the tag you would write). More than one: rename one of the colliding ids — nothing in the tag can say which the flow realizes, and a guess files every hop of it under an owner who never claimed it |\n| \\`usecase.requirement-unresolved\\` | a \\`dynamic view\\` tagged \\`#req-<slug>\\` whose slug names no requirement of the capability that view is about. Six failures with six fixes, and the message says which: the view resolves no \\`#cap-\\` tag (a \\`Requirement-ID\\` is unique only inside its own document, so the id has no scope), it resolves two (nothing says which document), the capability has no \\`capabilities/<id>/spec.md\\`, that document declares no requirements yet, it declares none flattening to this slug, or two of its ids flatten to one slug. It suppresses NOTHING: only a RESOLVED claim counts as a promise kept, so a broken tag leaves \\`capability.requirement-unrealized\\` firing beside it | follow the message. Several \\`#req-\\` tags on one view are legal and normal — a flow commonly keeps two of a capability's promises |\n| \\`usecase.step-unbacked\\` | a hop of a capability-tagged \\`dynamic view\\` that NO relationship in the model backs, on either tier. LikeC4 reports no error for it — a dynamic view is allowed to show a call the model never declared — so the diagram renders, the project parses, and every other check stays green. The finding names the file the view was written in, the step's ordinal and title, and the oriented pair | draw the edge in \\`model { }\\` (with \\`metadata { op '<operationId>' }\\` where it is a call), or, if the hop is a return step, write it as \\`<target> <- <source>\\`: a reply is attributed to the CALL it answers, so the \\`<-\\` spelling looks the hop up under the reversed pair instead |\n| \\`usecase.step-contested\\` (warn) | two or more relationships back one hop and they name DIFFERENT operations — an absent \\`metadata { op }\\` counts as a value there, not as a gap — so nothing can say which one the hop exercises. \\`details\\` lists every candidate as \\`<source> -> <target> \"<title>\" (op: <id>)\\` | give the candidates one operation, or draw the hop between the elements that name exactly one. loam names them rather than picking, because a guessed operation reads exactly like a right one in every rollup built over the flow. It suppresses \\`usecase.step-unlinked\\` on the same hop: one breach, one finding |\n| \\`usecase.step-unlinked\\` (warn) | the relationship backing a hop carries no \\`metadata { op }\\` and no \\`publishes\\`/\\`consumes\\`, while the hop's CALLER is not a person and its target resolves to a real \\`services/<id>/\\` — the flow reaches the fleet map and stops short of the contract it drives. \\`spine.op-link-missing\\` at use-case altitude. A hop FROM a person never fires it — the first hop of almost every sequence diagram is somebody using the app, and an app owes no operationId for a click — and neither does a hop INTO an \\`#external\\` system, an actor, a database, a cache or a queue: none of those owns a contract to name. A \\`publishes\\`/\\`consumes\\` edge already reaches an API — an AsyncAPI one | add \\`metadata { op '<operationId>' }\\` to the edge in \\`model { }\\`, or \\`metadata { publishes '<message>' }\\` / \\`consumes\\` where the hop is an event |\n| \\`feature.unreadable\\` | an artifact of a feature in flight could not be read — nothing about that feature was graded, and the rest of the fleet still was | fix the file the finding names; a feature silently skipped would have read as a clean one |\n| \\`sources.unverifiable-from-here\\` (ok) | one finding PER SERVICE whose \\`sources\\` only its own repo can check — a confirmation, not work, so it never trips \\`--strict\\`, and it is present in \\`--service\\` runs from the docs repo too | run \\`loam validate --service <id>\\` from inside that service's repo to actually grade its provenance; the fleet rollup line is derived from these findings |\n| \\`docs.binary-behind\\` (warn) | the docs repo's AGENTS.md stamp names a loam NEWER than the one running. This is the one finding that changes what a PASS means: a binary predating a grammar addition does not fail on the newer directive, it reads it as prose, has no join to fail, and reports green — so the corpus was checked less than the run implies. Warn rather than error because a mixed-version fleet is ordinary; \\`--strict\\` is the lever if it should gate | upgrade loam and re-run. Do NOT edit AGENTS.md — its stamp is correct and this binary is the older half |\n| \\`agents.stale\\` (warn) | the docs repo's AGENTS.md has no version stamp, or one older than the running binary — its flag and code tables may describe a loam that no longer exists | review AGENTS.md against the current \\`loam --help\\`, then update the \\`<!-- generated by loam vX.Y.Z -->\\` line; a hand-curated file silences it by bumping the stamp itself. Never regenerate the file — the team's edits outrank the template |\n\nfrontmatter and provenance — services' spec.md and arch.spec.md, and features'\nintent.md, both modes:\n\n| code | what it means | what to do |\n|---|---|---|\n| \\`frontmatter.missing\\` (warn) | no frontmatter at all | add owner, status and sources |\n| \\`frontmatter.malformed\\` | the frontmatter block does not parse as YAML — owner, status and sources are unreadable, which is not the same fact as absent | fix the header; the field checks and the sources chain resume once it parses — never add fields to a block YAML refuses |\n| \\`frontmatter.field-mismatch\\` | the doc names a different service/feature than the one it lives under | fix the frontmatter, or move the file |\n| \\`frontmatter.status-unknown\\` | a status nobody defined (\\`verifed\\`) | use the documented vocabulary — a typo here reads as unverified forever |\n| \\`frontmatter.field-missing\\` (warn) | owner, status or the identity field is absent | fill them in |\n| \\`sources.absent\\` (warn) | the doc names no sources | nothing ties it to the code, so nothing can tell you when it goes stale |\n| \\`sources.path-missing\\` | a listed source no longer exists, or is a glob pattern (no longer supported) | the code moved — re-read it and update the doc, do not just fix the path. For a pattern, name files or directories instead |\n| \\`sources.path-outside\\` | a listed source resolves OUTSIDE the service's repository — an absolute path, a \\`..\\` escape, or a path that only exists on one machine. loam will not hash bytes beyond the repo boundary, so the tie the entry claims cannot be checked | name the path repo-relative, inside the repository; if the document was really written from another repo's files, it belongs to that repo |\n| \\`sources.stale\\` (warn) | the source files changed since the doc was vouched for — the finding names which paths were added, changed or removed, from the \\`sources_files\\` index the stamp carries | re-read the code at exactly those paths, correct the doc, then ask a human to \\`loam vouch --service <id>\\` |\n| \\`sources.empty\\` (warn) | the declared \\`sources\\` exist but expand to no files at all — an empty directory, or a tree the repository ignores. A digest over nothing reads as current forever | point \\`sources\\` at the files the document was actually written from; \\`loam vouch\\` refuses to stamp this state with the same sentence |\n| \\`sources.skipped\\` (warn) | a path under a listed source was found but NOT hashed: a symlink loam will not follow (its target is outside the repository, dangling, or not a file or directory) — the digest cannot go stale over bytes it never read | not yours to close by editing the document: either the symlink should not be under a listed source, or \\`sources\\` should name the real path. Report it |\n| \\`sources.unvouched\\` (warn) | \\`sources\\` with no \\`sources_digest\\` — nobody ever stamped it | leave it: vouching is a human's reading, not yours |\n| \\`sources.unwalked\\` (warn) | \\`sources\\` leaves whole top-level paths of the service repo untouched — the \\`details\\` name them, measured against the files git tracks. The one completeness signal loam can compute: every other check compares the documents with each other, and a corpus agrees with itself perfectly while describing a third of a service. Silent where git cannot answer (not a repository, not installed) | open them. Each one is either a part of the service nobody read — go back to the walk \\`loam adopt\\` printed — or something this document legitimately does not owe, and then say which and why in the hand-back. Do NOT add paths to \\`sources\\` that you did not read: the list is a record of what was opened, and padding it destroys the only tie the document has to the code |\n| \\`content.stale\\` (warn) | the spec's body changed since it was vouched — \\`status: verified\\` is standing over words nobody has read. Unlike \\`sources.*\\` it needs no service repo, so it fires from the docs repo too | if you edited the doc, that is the point: report it and ask a human to re-vouch. Never revert the doc or touch the digest just to silence it |\n| \\`sources.sampled-vouch\\` (warn) | a person vouched after reading a recorded SAMPLE of the document — the stamp carries \\`vouch_scope: sampled k/n seed=…\\`, so \\`status: verified\\` stands over sections nobody read. The status and the \\`vouched\\` rung are unchanged and correct; this is what says the reading behind them was partial. A \\`vouch_scope\\` that does not decode fires it too, deliberately: an unreadable scope must never read as a full vouch. Doc-side, so it fires from the docs repo as well | not yours to clear. NEVER edit the document, and never touch or delete the \\`vouch_scope\\` field, to silence it — deleting it would forge a full vouch out of a partial one. A human runs a full \\`loam vouch --service <id>\\` (no \\`--sample\\`) once they have read the rest, which removes the field; \\`loam vouch --pack --service <id>\\` lists exactly what that sample left unread |\n| \\`sources.resolved\\` (ok) | every listed source still exists in the service repo | nothing — it is the confirmation |\n| \\`sources.current\\` (ok) | the stamped \\`sources_digest\\` still matches the code — nothing moved since a human vouched | nothing — it is the confirmation |\n| \\`sources.walked\\` (ok) | no whole top-level path of the service repo is left untouched by \\`sources\\` | nothing — it is the confirmation |\n\n\\`loam archive\\` — breaches the merge computation sees, reported at plan time.\nMost never appear in \\`validate\\`; the ones that do are graded harder here,\nbecause the merge is about to WRITE the breach into the living contract\n(\\`openapi.ref-unresolved\\` and \\`asyncapi.ref-unresolved\\` are validate warns but\nplan errors; \\`openapi.remove-marker-path-level\\` fires in validate on a living\ncontract too):\n\n| code | what it means | what to do |\n|---|---|---|\n| \\`living.requirement-outside-requirements\\` | the LIVING spec holds a requirement outside \\`## Requirements\\`, and the merge rewrites only that section — it would land in the file twice | re-home the requirement under \\`## Requirements\\`, then re-run; \\`--approve\\` does not override this |\n| \\`openapi.op-modified\\` (warn) | the feature redefines an operation the living OpenAPI already has — the merge overwrites the living definition wholesale | make sure the redefinition is intended; if not, align the feature's openapi.yaml with the living one |\n| \\`openapi.path-item-modified\\` (warn) | the delta redefines a PATH-level key (\\`parameters\\`, \\`servers\\`, \\`summary\\`, an \\`x-\\`) the living OpenAPI already has — the overwrite applies to EVERY operation on that path, including ones this feature never mentions | keep path-level keys out of the delta unless changing them for every operation on the path is the intent |\n| \\`service.no-model\\` (warn) | the archive creates \\`services/<id>/\\` but nothing writes its \\`model.likec4\\` | run \\`loam adopt --service <id>\\` after the archive lands, or the new service ships without a C4 center and \\`validate --all\\` grades it incomplete |\n| \\`openapi.component-modified\\` (warn) | a component this delta declares — whether the merged operations reference it or the components ARE the delta — already exists in the living OpenAPI with different content, and the merge copies the feature's version over it wholesale | make sure the redefinition is intended; if not, align the feature's component with the living one |\n| \\`openapi.ref-unresolved\\` | a \\`$ref\\` reachable from the merged operations or a merged component resolves in neither the feature's OpenAPI nor the living one — the merge would write a dangling reference | define the missing component or fix the ref; \\`--approve\\` merges the dangling reference anyway. External refs (not starting \\`#/\\`) are never checked |\n| \\`openapi.remove-marker-path-level\\` | an \\`x-loam-remove: true\\` written at PATH level, beside the methods instead of inside the operation being retired — it names no operation, so it retires nothing, and it is not a contract key either | move the marker inside the operation (with its \\`operationId\\`), or delete it. The merge is safe either way — a feature-only key is never published into the living contract — but the removal you asked for will not happen |\n| \\`asyncapi.message-modified\\` (warn) | the delta redefines a \\`components.messages\\` slot the living AsyncAPI already has — the merge overwrites the living message wholesale | make sure the redefinition is intended; if not, align the feature's asyncapi.yaml with the living one, then \\`loam rebase\\` so the restatement classifies as a quote |\n| \\`asyncapi.channel-modified\\` (warn) | the delta redefines a \\`channels\\` slot the living AsyncAPI already has — the merge overwrites it wholesale, inline channel messages included (they are channel interior, never slots of their own) | make sure the redefinition is intended; if not, align the feature's channel with the living one |\n| \\`asyncapi.operation-modified\\` (warn) | the delta redefines an \\`operations\\` slot the living AsyncAPI already has — the merge overwrites it wholesale | make sure the redefinition is intended; if not, align the feature's operation with the living one |\n| \\`asyncapi.component-modified\\` (warn) | the delta declares a component SURFACE outside \\`components.messages\\` — a \\`components.schemas\\` entry, a security scheme — that the living AsyncAPI already has with different content, and the merge copies the feature's version over it wholesale | make sure the redefinition is intended; if not, align the feature's component with the living one, then \\`loam rebase\\` so the restatement classifies as a quote |\n| \\`asyncapi.ref-unresolved\\` | a \\`$ref\\` the MERGED asyncapi.yaml would carry resolves in neither the feature's document nor the living one — including a removal that deletes a slot the living document still references. A target the feature's own document DECLARES no longer fires: the merge now copies component surfaces too, so the pointer resolves in the document it lands in | define the missing target, fix the pointer, or retire the referrer in the same delta; \\`--approve\\` merges the dangling reference anyway. External refs (not starting \\`#/\\`) are never checked |\n| \\`asyncapi.remove-marker-inline\\` | an \\`x-loam-remove: true\\` nested on an INLINE channel message (\\`channels.<ck>.messages.<mk>\\`) — inline messages are channel interior, never slots, so the marker retires nothing; when the channel is otherwise unchanged it does not even surface as an edit | retire the whole channel (\\`x-loam-remove: true\\` at \\`channels.<ck>\\`), or declare the message under \\`components.messages\\` (the channel aliasing it by \\`$ref\\`) and put the marker there. The merge is safe either way — the deep strip keeps the key out of living — but the removal you asked for will not happen |\n\n\\`loam validate <target>\\` — the positional form's own finding, both modes:\n\n| code | what it means | what to do |\n|---|---|---|\n| \\`target.ambiguous\\` (warn) | the positional name resolves to a service AND a feature — the run validated the feature (the tie-break that has always held) and this warning says so, with \\`resolvedKind\\` in \\`--json\\` | pass \\`--service <id>\\` or \\`--feature <id>\\` to force the reading you meant; a silent precedence rule would be a report about a target nobody asked for |\n\nconfirmations — the \\`ok\\`-severity findings a clean axis reports, so silence is\nnever the signal (\\`--errors-only\\` hides them from the text view; \\`--json\\`\nalways carries them):\n\n| code | what it means | what to do |\n|---|---|---|\n| \\`c4.valid\\` (ok) | the service's model.likec4 parses and the C4 axis was actually graded | nothing — it is the confirmation |\n| \\`delta.valid\\` (ok) | the feature's delta.likec4 parses and its tagged change is well-formed | nothing — it is the confirmation |\n| \\`coherence.ok\\` (ok) | the feature's axes agree with each other and with the fleet in flight — nothing gates its archive | nothing — it is the confirmation |\n| \\`requirements.covered\\` (ok) | every requirement carries at least one scenario | nothing — it is the confirmation |\n| \\`api.covered\\` (ok) | every operation in openapi.yaml is governed by some requirement's \\`Operations:\\` line | nothing — it is the confirmation |\n| \\`event.covered\\` (ok) | the event axis joins clean — every declared message is linked and governed | nothing — it is the confirmation |\n| \\`spine.resolved\\` (ok) | every inbound landscape call into this service resolves to an operation its OpenAPI defines | nothing — it is the confirmation |\n| \\`landscape.matched\\` (ok) | every \\`services/<id>/\\` resolves to a landscape element and every binding holds | nothing — it is the confirmation |\n\n\\`sources.stale\\`, \\`content.stale\\` and \\`sources.sampled-vouch\\` are the warnings you\ncannot close by yourself. Fix what the code now says, then hand it back — the stamp\nis a person's claim to have read it, and the last of the three says how much of it\nthey read.\n`,\n};\n"]}
|
|
1
|
+
{"version":3,"file":"check.js","sourceRoot":"","sources":["../../../../src/core/agent/workflows/check.ts"],"names":[],"mappings":"AAEA;;;;;GAKG;AACH,MAAM,CAAC,MAAM,UAAU,GAAmB;IACxC,IAAI,EAAE,YAAY;IAClB,WAAW,EAAE,qDAAqD;IAClE,YAAY,EAAE,iCAAiC;IAC/C,OAAO,EACL,uMAAuM;IACzM,4EAA4E;IAC5E,uEAAuE;IACvE,2EAA2E;IAC3E,+DAA+D;IAC/D,4EAA4E;IAC5E,uEAAuE;IACvE,0EAA0E;IAC1E,qEAAqE;IACrE,2EAA2E;IAC3E,4EAA4E;IAC5E,SAAS;IACT,UAAU,EAAE,8CAA8C;IAC1D,2EAA2E;IAC3E,2EAA2E;IAC3E,gDAAgD;IAChD,YAAY,EAAE,EAAE;IAChB,KAAK,EAAE;QACL,8GAA8G;QAC9G,2DAA2D;QAC3D,wEAAwE;QACxE,6CAA6C;KAC9C;IACD,IAAI,EAAE;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;CA8SP;CACA,CAAC","sourcesContent":["import type { CommandContent } from \"../contract.js\";\n\n/**\n * The fix tables: run loam's checks and repair what they find, code by code.\n * The largest body on purpose — it is the other half of the codes-drift\n * documentation corpus (with ../agents-md/), so a new finding code gets its\n * row here.\n */\nexport const LOAM_CHECK: CommandContent = {\n name: \"loam-check\",\n description: \"Validate the loam docs repo and fix what it reports\",\n argumentHint: \"[--all | <FEAT-id> | <service>]\",\n purpose:\n \"Run loam's checks and fix what they find. Branch on `findings[].code`, never on the prose — and remember that a coherence finding marked `gates` stops `loam archive` even when it is only a warning.\",\n // No `$1`, unlike the other five. This body carries no placeholder at all —\n // `loam-check`'s argument is a target you pass to `loam validate`, not\n // something the protocol interpolates — and the argument hint's FIRST form\n // is `--all`, so `loam instructions loam-check $1` expanded to\n // `loam instructions loam-check --all`, which commander reads as an unknown\n // option and refuses with exit 1. A pointer file whose one instruction\n // fails leaves an agent with no protocol at all, which is a state the fat\n // body could never reach. The command still tolerates a leading-dash\n // argument (see `allowUnknownOption` in commands/instructions.ts); this is\n // the half of the fix that stops loam printing the broken line in the first\n // place.\n invocation: \"loam instructions loam-check --no-fix-tables\",\n // The one protocol whose body names no placeholder: its target (`--all`, a\n // feature id, a service id) is spelled inside the `loam validate` line the\n // agent chooses, not substituted into the page.\n placeholders: [],\n spine: [\n \"`loam validate --all` in the docs repo for the fleet, or `loam validate <target>` for one feature or service\",\n \"fix every error, and every finding that gates the archive\",\n \"leave an advisory warning standing only if you can say why, and say it\",\n \"re-run until the grade is the one you meant\",\n ],\n body: `Run loam's checks and fix what they find.\n\n- whole fleet: \\`loam validate --all --json\\` (run in the docs repo)\n- one target: \\`loam validate <FEAT-id | service-id> --json\\` — the feature reading is\n tried first; \\`--feature <id>\\` / \\`--service <id>\\` force one when a name could be both,\n and \\`target.ambiguous\\` (warn) says when a name really was both and which reading was taken\n- \\`--strict\\` (any mode) exits 1 on any warning too. Exit code only: \\`valid\\` and the\n payload do not change, so the stricter grade lives in the CI invocation, not the repo\n- \\`--errors-only\\` drops the \\`ok\\` confirmations from the text view; the \\`--json\\`\n payload is byte-identical either way\n- under \\`--all\\`, warn codes tied to a contract axis (\\`service.no-spec\\`,\n \\`spec.no-requirements\\`, \\`service.no-openapi\\`'s warn form,\n \\`spine.op-link-missing\\`, \\`permissions.unenforced\\`, \\`capability.unrealized\\`)\n collapse in the TEXT view to one \"axis not started\" banner per axis when that\n axis has zero participating services fleet-wide (\\`scorecard.adoption\\`) —\n expected during staged adoption; \\`--json\\` carries every finding unchanged\n- refusals before any grading: \\`docs-missing\\` / \\`services-missing\\` (\\`docsDir\\` in\n ./loam.json points at nothing, or at a directory with no \\`services/\\`) — run\n \\`loam doctor\\` and fix the wiring, do not read an empty report as a clean fleet\n\nBranch on \\`findings[].code\\`, not the prose. Errors fail validate; a coherence finding\nwith \\`gates: true\\` will stop \\`loam archive\\` even when it is a warning. Fix every\nerror and every gating finding. Leave an advisory warning only if you can say why,\nand say it.\n\n\\`--service <id>\\` — one service's own axes (\\`--all\\` runs these for every service):\n\n| code | what it means | what to do |\n|---|---|---|\n| \\`service.unknown\\` | no \\`services/<id>/\\` at all — the id is a typo until proven otherwise | use one of the ids the message offers, or \\`loam list services\\`; never \\`loam adopt\\` a misspelling |\n| \\`service.no-model\\` | the directory is real but there is no model.likec4 | run \\`loam adopt\\` for it — without the C4 center nothing else has anywhere to hang |\n| \\`c4.invalid\\` | model.likec4 does not parse | fix this first — an unreadable axis makes every other check meaningless |\n| \\`c4.no-relationships\\` (warn) | the model parses, declares elements, and no relationship joins anything — while its own evidence says it should reach something: more than one nested element, or dependencies declared in this service's health.yaml. A model that reaches nothing is almost never true; without this check, a 2-element model with 0 relationships and a 19-element model with 19 relationships could receive the same green verdict | draw the edges the service actually has — what its containers call, what reads and writes its stores, what it depends on at runtime. A bare root-plus-one-container baseline with no other evidence stays silent on purpose: nothing proves it thin |\n| \\`requirements.missing-scenarios\\` | a requirement with no scenario | add the acceptance criteria; do not delete the requirement |\n| \\`requirements.stepless-scenario\\` | a scenario with a heading and no recognizable steps — it satisfies the coverage rule and tests nothing: cucumber runs it vacuously green and \\`loam verify --results\\` can never confirm it. Graded on spec.md and arch.spec.md alike, in \\`--service\\` and \\`--feature\\` scope both, because a stepless scenario in a delta merges into the living spec and is called covered forever after | write the body as \\`- **Given/When/Then**\\` bullets |\n| \\`requirements.assertionless-scenario\\` | a scenario with steps and no \\`Then\\` anywhere in its body — it arranges and acts and asserts nothing, so the run confirms only that the setup did not throw and the \\`scenario.tested\\` claim is answered on that. Disjoint from \\`stepless\\` by construction (that one has no steps at all), graded in the same four places | add the \\`- **Then**\\` the scenario was written for. \\`And\\`/\\`But\\` are continuations: with no \\`Then\\` above them they continue the setup |\n| \\`requirements.examples-unbound\\` (error) | a scenario with an \\`Examples\\` table uses a \\`<placeholder>\\` no column supplies — cucumber substitutes nothing, so the step definition receives the literal angle-bracket text. Steps, docstrings and data-table cells all count as uses, because cucumber substitutes into all three | add the column, or correct the placeholder to one the header names |\n| \\`requirements.examples-unreferenced\\` (warn) | an \\`Examples\\` column no step, docstring or data-table cell refers to — usually the fossil of a rename, and the outline then runs its rows as identical passes that read as cases of coverage | reference the column from a step, or drop it |\n| \\`spec.merge-conflict\\` | a requirement document still holds \\`<<<<<<<\\` / \\`=======\\` / \\`>>>>>>>\\` markers — nobody wrote what it now says. Graded on living spec.md/arch.spec.md and on a feature's deltas of them, in both scopes | resolve the conflict in the file the finding names. \\`loam archive\\` refuses (\\`merge-failed\\`) on a conflicted living document it would rewrite, and \\`--approve\\` does not override it: the merge rewrites the requirements section and takes whichever marker lines fall inside it, so a conflict anyone can see would become a document nobody can tell is wrong |\n| \\`spec.duplicate-requirement\\` | one \\`### Requirement:\\` name defined twice in one living spec.md or arch.spec.md — a merge edits only the first, the rest live on as stale copies | keep exactly one block per name (merge the bodies); the two files are separate namespaces, so a name in both is fine |\n| \\`spec.requirement-id-invalid\\` | a \\`Requirement-ID\\` violates \\`[A-Za-z][A-Za-z0-9._-]{0,127}\\` | replace it with a portable stable ID; do not remove an established ID merely to silence the check |\n| \\`spec.requirement-id-repeated\\` | one requirement declares \\`Requirement-ID\\` more than once | keep exactly one identity line |\n| \\`spec.requirement-id-duplicate\\` | one ID identifies multiple requirements in the same file | give each requirement its own stable ID |\n| \\`openapi.generated-stale\\` (warn) | \\`loam.json\\` names \\`contracts.openapi\\` — where this repo's BUILD writes its contract — and that document differs from the committed \\`services/<id>/openapi.yaml\\`. The docs-repo copy is what every operation join is graded against, so the fleet is being checked against a contract the service no longer serves. Compared by a digest over canonical JSON, so key order and formatting never fire it. Service-repo only; silent with no \\`contracts\\` block | review the difference and copy the generated document across — loam never writes it for you, because the committed contract is one somebody agreed to, not a cache of the build |\n| \\`contracts.source-missing\\` (error) | \\`contracts.openapi\\` names a path and nothing is there — usually CI validating before it builds | run the build first, or correct the path |\n| \\`contracts.source-invalid\\` (error) | \\`contracts.openapi\\` resolves outside the repository, or names a file that does not parse as YAML or JSON | correct the path, or fix the document the build emitted |\n| \\`spec.unknown-directive\\` (warn) | a requirement body line whose key is one or two edits from a directive and is not one — \\`Realises:\\`, \\`Capabilties:\\`, \\`Opertaions:\\`. This is the one case where the corpus is quieter than it looks: an unrecognised key is prose, so the join it resembles simply does not exist and every check over it stays green for want of anything to fail. Measured by edit distance, not by prefix, so ordinary prose keys (\\`Note:\\`, \\`Context:\\`, \\`Owner:\\`) do not fire it; fenced blocks and table cells are skipped, because a directive spelling inside an example is a sample value | correct the spelling, or reword the line so it does not read as a directive |\n| \\`spec.repeated-operations\\` (warn) | a second \\`Operations:\\` line in one requirement body — the last line REPLACES the earlier ones, whose list is silently lost (living specs and feature deltas alike) | merge them into one comma-separated \\`Operations:\\` line |\n| \\`spec.repeated-covers\\` (warn) | same for \\`Covers:\\` — the last line wins, the earlier list is silently lost | merge them into one comma-separated \\`Covers:\\` line |\n| \\`spec.no-requirements\\` (warn) | a LIVING spec.md with no \\`### Requirement:\\` block at all — every requirement-driven check below it is vacuous, not passing | write the requirements, or say in the file why the service has none yet |\n| \\`service.unreadable\\` | an artifact of this service could not be read (permissions, a dangling symlink) — nothing about the service was checked, and the rest of the fleet still was | fix the file the finding names; a service silently skipped would have read as a clean one |\n| \\`service.no-spec\\` (warn) | no living spec.md — a part-adopted service, legal but unchecked | write it; until it exists, requirement coverage and API governance are vacuous |\n| \\`service.no-openapi\\` (error, or warn) | no openapi.yaml — deleted, renamed, or never written. ERROR when something already written down joins into the absent file: a LIVING non-REMOVED requirement's \\`Operations:\\` line, or a landscape edge carrying \\`metadata { op }\\` into this service. The finding's \\`details\\` list the stranded operationIds. WARN when nothing joins into it but the landscape cannot prove nobody calls this service (it is absent, or it does not parse). Silent when the landscape parses and no edge calls an operation here — a worker, a cron or a UI owes no contract | error: put the file back — every link the \\`details\\` name resolves to nothing until it is there. If the operations are genuinely gone, retire them through a feature delta (REMOVED requirement + \\`x-loam-remove\\` marker) instead of deleting the contract out from under them. warn: write the contract, or model the service in the fleet map so it shows no one expects an API |\n| \\`openapi.invalid\\` | openapi.yaml exists but does not parse — an unreadable contract proves nothing, so no \\`api.*\\` or spine finding is graded against it (before this code, the empty parse graded every inbound edge \\`spine.op-undefined\\`). The same code fires for a FEATURE's own openapi.yaml under \\`--feature\\`, where it gates the archive and suspends the service's contract-axis checks the same way | fix the YAML first — the API axis is unchecked until it reads |\n| \\`openapi.remove-marker-living\\` | a living openapi.yaml contains feature-only \\`x-loam-remove: true\\` | remove the marker from living; retire the operation through a feature delta with a matching REMOVED requirement |\n| \\`openapi.duplicate-operationid\\` (warn) | one operationId occupies two (path, method) slots in a living contract — every join on the id (a requirement's \\`Operations:\\` line, an edge's \\`metadata { op }\\`, a removal marker) then picks one of them arbitrarily | give each slot its own id; the identity of an operation is its path and method, and the id is how the other axes name it |\n| \\`openapi.response-undescribed\\` (warn) | operation(s) whose responses declare no schema at all (204/304 excepted) — the contract names the operation and says nothing about what it returns, so a reader still needs the code. A presence probe: what a declared schema SAYS is still never checked | declare \\`content\\` with a \\`schema\\` on at least one response of each named operation |\n| \\`openapi.ref-unresolved\\` (warn here; error at archive plan time) | internal \\`$ref\\`(s) in the living contract that resolve to nothing in the document — the operation validates while its schema is silently absent. External refs (not starting \\`#/\\`) are never checked | define the missing component or fix the pointer the \\`details\\` name |\n| \\`openapi.baseline-stale\\` | the living operation — or a pinned path-level key or component — changed after this delta edited it | somebody landed a change to it in between. Re-read the living value, fold in what you still mean, then \\`loam rebase <FEAT>\\` |\n| \\`openapi.baseline-missing\\` (warn, gates archive; one per service) | operations, path-level keys or components in this feature's openapi.yaml carry no baseline (\\`x-loam-based-on\\` / \\`x-loam-baselines\\`) | run \\`loam rebase <FEAT>\\`. Until you do, archive refuses: the merge cannot tell the surfaces you EDITED from the ones you merely restated, so it would upsert all of them — and every restated one reverts whatever landed on it. \\`--approve\\` merges unpinned deliberately |\n| \\`openapi.baseline-invalid\\` | an \\`x-loam-based-on\\` that is not a digest, one on an operation the living contract has no slot for, a malformed \\`x-loam-baselines\\` record, or a record entry naming a surface the delta does not restate (or whose living value is gone) | \\`loam rebase\\` writes both; a new value has no living version to be based on, so it carries no entry |\n| \\`spec-api.op-undefined\\` | a LIVING requirement's \\`Operations:\\` line names an operationId this service's openapi.yaml does not define (the same code fires inside a feature delta) | define the endpoint, or correct the \\`Operations:\\` line — the two axes disagree about what the service exposes |\n| \\`api.ungoverned\\` (warn) | operation(s) no requirement's \\`Operations:\\` line names | write the requirement, or link an existing one |\n| \\`api.ops-unlinked\\` (warn) | operations AND requirements exist but zero \\`Operations:\\` lines join them — the API axis is vacuously green | link each requirement to the operations it governs |\n| \\`api.requirement-deprecated\\` (warn) | a requirement's \\`Operations:\\` list resolves only to operations the OpenAPI marks \\`deprecated: true\\` — the behaviour it governs is on its way out | migrate the requirement to the replacement operation, or retire it with the ops it governs |\n| \\`spine.landscape-invalid\\` | the living landscape does not parse, so the C4↔API spine cannot be checked | fix architecture/landscape.likec4 first |\n| \\`spine.op-undefined\\` | a landscape edge calls an operation this service's OpenAPI does not define | a broken contract between services — fix the edge or add the endpoint |\n| \\`spine.op-link-missing\\` (warn) | a landscape \"Calls\" edge into this service with no \\`metadata { op }\\` | link it to the operationId |\n| \\`spine.op-deprecated\\` (warn) | a landscape edge calls an operation this service's OpenAPI marks \\`deprecated: true\\` — the consumer is standing on a contract being retired | migrate the consumer to the replacement operation; deprecation is the first step of retiring an op, and the op stays defined until a human removes it |\n| \\`service.no-asyncapi\\` | no asyncapi.yaml, and message link(s) already point into it — a landscape edge's \\`metadata { publishes }\\` / \\`metadata { consumes }\\`, or a LIVING requirement's \\`Publishes:\\` / \\`Consumes:\\` line. The \\`details\\` list the stranded message names. Silent when nothing joins into it: an async contract is optional, and most services touch no topic | put the file back — every link the \\`details\\` name resolves to nothing until it is there |\n| \\`asyncapi.invalid\\` | asyncapi.yaml exists but does not parse — an unreadable contract proves nothing, so no \\`event.*\\` or message-spine finding is graded against it. The same code fires for a FEATURE's own asyncapi.yaml under \\`--feature\\`, where it gates the archive and suspends the service's event-axis checks the same way | fix the YAML first — the event axis is unchecked until it reads |\n| \\`asyncapi.remove-marker-living\\` | a living asyncapi.yaml carries feature-only \\`x-loam-remove: true\\` — at a \\`channels.<key>\\` / \\`operations.<key>\\` / \\`components.messages.<key>\\` slot, or nested on an inline channel message | remove the marker from the living contract; retire a slot through a feature delta's asyncapi.yaml instead |\n| \\`asyncapi.duplicate-message\\` (warn) | one message name is declared in two slots — every join on the name (an edge's \\`metadata { publishes }\\`, a requirement's \\`Publishes:\\` line) then picks one of them arbitrarily | give each declaration its own name, or factor the second into a \\`$ref\\` at the first |\n| \\`asyncapi.payload-undescribed\\` (warn) | message(s) whose payload declares no shape at all — absent, or bare \\`type: object\\` with not one property. The name joins the spine while nothing defines what a consumer would read. A presence probe: a payload declaring a non-JSON \\`schemaFormat\\` (Avro) is never judged | declare \\`properties\\` on the payload, or the \\`schemaFormat\\` of its non-JSON schema |\n| \\`asyncapi.ref-unresolved\\` (warn) | internal \\`$ref\\`(s) that resolve to nothing in the document — the target used to vanish silently: an operation's message-list entry contributed no name, and a dangling channel alias left a phantom key | fix or define the pointers the \\`details\\` name |\n| \\`spine.message-undefined\\` | a landscape edge \\`publishes\\`/\\`consumes\\` a message this service's asyncapi.yaml declares no \\`action: send\\`/\\`receive\\` operation for | declare the operation, or correct the edge — the fleet map and the contract disagree about what this service puts on the wire |\n| \\`spec-event.message-undefined\\` | a LIVING requirement's \\`Publishes:\\` / \\`Consumes:\\` line names a message this service's asyncapi.yaml does not declare in that direction | declare it, or correct the line |\n| \\`spine.message-unproduced\\` | this service consumes a message that NO service in the fleet and NO \\`#external\\` element declares is sent — no producer anywhere. The inverse of \\`spine.op-undefined\\`, and it has no HTTP analog: on the API axis the provider owns the contract and the check is local, while an event's schema lives in the producer's repo | find the producer and have it declare the message (an outside-the-fleet producer is declared on its \\`#external\\` element: an edge with \\`metadata { publishes '<msg>' }\\`), or correct the name — a consumer joined to nothing reads a payload nothing defines |\n| \\`spine.message-external\\` (warn) | this service consumes a message whose only declared producer is an \\`#external\\` element — produced outside the fleet, so no contract will ever appear in a \\`services/<id>/\\`, and this service's own asyncapi.yaml currently defines no shape for it. Fires on positive evidence (the \\`publishes\\` edge), so unreadable contracts elsewhere do not suspend it | declare the message in THIS service's asyncapi.yaml with its payload — the consumer's copy is the only contract the fleet can hold; once it declares a shape, the finding stops |\n| \\`asyncapi.message-contested\\` (warn) | two or more services declare they send one message name — every consumer's join picks one of them arbitrarily | namespace the message by its owning domain; one message has one producer, and which contract a consumer reads must not be a coin flip |\n| \\`event.messages-unlinked\\` (warn) | messages AND requirements exist but zero \\`Publishes:\\`/\\`Consumes:\\` lines join them — the event axis is vacuously green | link each requirement to the messages it governs |\n| \\`event.ungoverned\\` (warn) | message(s) declared in asyncapi.yaml that no requirement's \\`Publishes:\\`/\\`Consumes:\\` line names — \\`api.ungoverned\\`'s event twin, and the per-message half \\`event.messages-unlinked\\` cannot see: that code stays silent once even one message is linked | write the requirement that governs each named message, or drop the declaration |\n| \\`covers.unknown\\` (warn) | a \\`Covers:\\` entry in arch.spec.md resolves to no element, edge, alert or SLI — living and feature deltas alike | fix the id (the message offers close ones); a mistyped entry silently costs the coverage it was written for |\n| \\`api.response-ungoverned\\` (warn) | an operation declares a 4xx/5xx response that no scenario of any requirement governing it reaches. \\`api.ungoverned\\` grades whole OPERATIONS, so an endpoint with one happy-path requirement and twelve declared failure codes was fully governed by every other check — and the refusals are where the decision layer lives | write the refusal as a scenario, or as a row in an existing one. A scenario reaches a code when the number appears in its body, \\`Examples\\` rows included |\n| \\`obligation.unknown\\` | a \\`#obl-<name>\\` tag on the LIVING fleet map that \\`architecture/obligations.yaml\\` does not declare. An ERROR, and the asymmetry with its sibling below is the point: a tag that resolves to nothing reads exactly like a rule the fleet keeps, in review and in every derived view. \\`details\\` names every object carrying it | fix the tag (the message offers close names), or declare the obligation |\n| \\`obligation.unapplied\\` (warn) | a declared obligation that no \\`#obl-\\` tag applies anywhere on the map — the mirror of \\`permissions.unenforced\\`, and it earns its place for the same reason: a vocabulary is worth what cites it. Either a decision that was reversed and left its word behind, or one nobody has placed yet | tag the elements and edges it governs, or drop the declaration |\n| \\`obligation.uncovered\\` (warn) | a tagged element or edge that no LIVING requirement's \\`Covers:\\` line names — the obligation is placed and nothing says it is met. \\`c4.uncovered\\`'s question asked of the map the fleet actually runs on, rather than of a feature's \\`delta.likec4\\`. One finding per tagged object, \\`subject\\` = the service that owns it, so a fleet report reads as a worklist per team. Only a tag that RESOLVES is asked: an undeclared one has earned its error already | write the requirement in that service's \\`arch.spec.md\\` with the \\`Covers:\\` entry the message spells out, and a scenario that proves it — or drop the tag if the decision no longer applies there |\n| \\`obligation.adr-missing\\` | an obligation's \\`adr:\\` field names no file. The ADR is where the decision itself is written, so a pointer at nothing leaves every tag citing a rule nobody can read | fix the path (relative to the docs repo root), write the record, or drop the field |\n| \\`obligation.invalid\\` | \\`architecture/obligations.yaml\\` exists but does not read as a vocabulary. Reported ALONE and the family suspended behind it: every \\`#obl-\\` tag resolves against this file, so grading them on top would be a cascade, not a diagnosis. An id that a LikeC4 tag name cannot carry refuses the file here too — the id IS the tag suffix, and a rejected character truncates the tag rather than failing it | fix the YAML — the shape is \\`obligations: {<id>: {description, adr}}\\`, ids spelled with letters, digits, \\`_\\` and \\`-\\` only |\n| \\`permissions.unknown\\` | a \\`Requires:\\` entry in spec.md or arch.spec.md names a \\`<subject>/<permission>\\` that \\`architecture/permissions.yaml\\` does not declare. An ERROR where its \\`Covers:\\` sibling is a warning: an invented permission reads exactly like a real one in the requirement, in the generated scenario, and in the test somebody then writes against it | fix the spelling (the message offers close ones), or declare the permission in the fleet vocabulary |\n| \\`permissions.invalid\\` | \\`architecture/permissions.yaml\\` exists but does not read as a vocabulary. Reported alone: every \\`Requires:\\` line in the fleet resolves against it, so grading them on top would be a cascade, not a diagnosis | fix the YAML — the shape is \\`subjects: {<kind>: …}\\` then \\`permissions: {<kind>: {<name>: …}}\\`, and a permission under an undeclared subject kind is refused |\n| \\`permissions.unenforced\\` (warn) | a declared permission no requirement's \\`Requires:\\` line names, anywhere in the fleet — the mirror of \\`api.ungoverned\\`. Either a rule that was removed and left its word behind, or a word nobody adopted | write the requirement that gates on it, or drop the declaration; a vocabulary is worth what cites it |\n| \\`capability.unknown\\` | a \\`Capability:\\` entry in spec.md or arch.spec.md — living at the service target, or a feature's delta of either, where it gates \\`loam archive\\` (\\`--approve\\` overrides) — that NEITHER \\`architecture/capabilities.yaml\\` nor \\`capabilities/<id>/spec.md\\` declares. Silent when the fleet has neither (those FILES are this axis's opt-in, unlike \\`Requires:\\`) or the YAML is invalid | fix the spelling (the message offers close ones), or declare the capability — a name alone goes in the YAML, a name with prose gets the document |\n| \\`capability.invalid\\` | \\`architecture/capabilities.yaml\\` exists but does not read as a vocabulary. Reported exactly once per \\`--all\\` run, and the whole family is suspended behind it — including the archive gate, so an unknown name CAN merge while the file is broken; fix the YAML before trusting the axis | fix the YAML — the shape is \\`capabilities: {<id>: {description, owner}}\\`, ids flat keys with \\`/\\` allowed for nesting |\n| \\`capability.unrealized\\` (warn) | a declared capability that no living non-REMOVED requirement's \\`Capability:\\` line names — one warning PER capability, subject = the id, and the message names which side declared it. Either a promise nobody implemented or a word nobody adopted, and invisible from inside the file | write the requirement that realizes it (\\`loam list capabilities\\` shows the whole rollup), or drop the declaration |\n| \\`capability.doc-missing\\` (warn) | a directory in EITHER capability tree that holds no \\`spec.md\\` and has no capability beneath it — what \\`mkdir\\` leaves behind halfway through creating one. Under living \\`capabilities/\\` it declares nothing, so a \\`Capability:\\` line naming it still reports \\`capability.unknown\\` while a reader browsing the tree counts it as a capability. Under \\`features/<FEAT>/capabilities/\\` it is quieter: an empty directory is not a delta, so the merge carries nothing and the business change ships as zero. \\`subject\\` is the directory as it sits on disk, so it says which tree | living: write \\`capabilities/<id>/spec.md\\` (the narrative, then \\`## Requirements\\`). In a feature: write \\`features/<FEAT>/capabilities/<id>/spec.md\\` (a \\`## ADDED Requirements\\` section, ids and all). Either way, or remove the directory |\n| \\`capability.requirement-unidentified\\` | a requirement in \\`capabilities/<id>/spec.md\\` — living, or a feature's \\`features/<FEAT>/capabilities/<id>/spec.md\\` delta of it, where it gates \\`loam archive\\` (\\`--approve\\` overrides) — with no \\`Requirement-ID:\\` line. Optional in a service spec for OpenSpec compatibility, REQUIRED here: a capability document outlives every service that realizes it, so rewording a heading would be a removal and an addition, and every join made to it would break silently. Graded on what the delta would MERGE, so a REMOVED requirement is exempt | add \\`Requirement-ID: <id>\\` as the first body line |\n| \\`capability.requirement-service-scoped\\` | a requirement in \\`capabilities/<id>/spec.md\\` — living, or a feature's delta of it, where it gates \\`loam archive\\` (\\`--approve\\` overrides) — carrying \\`Operations:\\`, \\`Covers:\\`, \\`Publishes:\\` or \\`Consumes:\\` — each resolves against one service's own contract or model, so the requirement is a service requirement filed at the wrong altitude. This is the checkable half of \"a capability requirement is observable outside the fleet\"; the naming-no-service half is an authoring rule PR review holds, because matching service names in prose is a heuristic loam will not run | move it into that service's spec.md, where the line resolves — what belongs in the capability document is the promise, not the mechanism |\n| \\`capability.requirement-inert-join\\` | a requirement in \\`capabilities/<id>/spec.md\\` — living, or a feature's delta of it, where it gates \\`loam archive\\` (\\`--approve\\` overrides) — carrying \\`Capability:\\` or \\`Realizes:\\`: the axis's own two joins, which point INTO the tree and so do nothing written inside it. The rollup joins SERVICE requirements to capabilities, so a \\`Capability:\\` line here claims the document it is already in, and a \\`Realizes:\\` line here points at a promise loam will never see anybody keep. Refused rather than ignored because both parse and both read exactly like the working joins one directory over | delete the line; the join that matters is written on the service requirement, as \\`Realizes: <capability-id>#<Requirement-ID>\\` |\n| \\`capability.realizes-unknown\\` | a \\`Realizes:\\` entry in spec.md or arch.spec.md — living at the service target, or a feature's delta of either, where it gates \\`loam archive\\` (\\`--approve\\` overrides) — that names no capability requirement. Five distinct failures, each with its own fix in the message: the entry is not spelled \\`<capability-id>#<Requirement-ID>\\`; the capability is undeclared; it is declared but has no document; the document declares no requirements yet; or it declares requirements but not this id. Silent under the same ladder as \\`capability.unknown\\` | follow the message — it names which of the five happened, and offers close names where there are any |\n| \\`capability.realizes-stale\\` (warn) | a PINNED \\`Realizes:\\` entry whose capability requirement has been rewritten since the pin was stamped. The join still resolves — the address is fine and the promise behind it moved, which no existence check can see. Only pinned entries are graded, so a corpus that has never run \\`loam rebase --living\\` produces none of these, ever | re-read \\`capabilities/<id>/spec.md\\` and decide which side is now wrong: change this requirement, change the promise, or — when this requirement is still right as it stands — \\`loam rebase --living\\` to record the re-read |\n| \\`capability.requirement-unrealized\\` (warn) | a requirement in a capability document that no living service requirement's \\`Realizes:\\` line names, one warning PER requirement, subject = the \\`<capability>#<id>\\` entry an author would type to fix it. The sharper sibling of \\`capability.unrealized\\`, and the one that survives a healthy-looking row: a capability with four requirements and three realized reports nothing at all through the other code | add the line to each service requirement that carries part of the promise — or leave it, if the promise is genuinely not implemented yet: writing the business document ahead of the fleet is the intended use, which is why this warns and never gates |\n| \\`capability.uncovered\\` (warn, GATES archive) | a requirement a feature's \\`features/<FEAT>/capabilities/<id>/spec.md\\` ADDS that no ADDED or MODIFIED requirement in the same feature's \\`specs/\\` deltas names with \\`Realizes: <capability-id>#<Requirement-ID>\\`, subject = the capability id. A WARNING because the document is legal — writing a promise ahead of the fleet is the intended use — that GATES because the merge is what is unsafe: once it lands, the only thing that will ever mention the promise again is \\`capability.requirement-unrealized\\`, a fleet-scope warning nobody reads. MODIFIED is exempt (the promise may already be realized by living text), and a requirement with no \\`Requirement-ID:\\` is left to \\`capability.requirement-unidentified\\` alone | add the \\`Realizes:\\` line to the service requirement that carries part of the promise; or, if the promise CROSSES services, write the flow that keeps it as \\`features/<FEAT>/usecases/<name>.likec4\\` — a views-only document holding one \\`dynamic view\\` tagged \\`#cap-<capability>\\` and \\`#req-<requirement>\\`, which \\`loam archive\\` merges into \\`architecture/usecases/\\` and \\`loam unarchive\\` takes back; or \\`loam archive <FEAT> --approve\\` to land the promise ahead of the fleet and let \\`capability.requirement-unrealized\\` carry it |\n| \\`usecase.flow-invalid\\` | a \\`features/<FEAT>/usecases/*.likec4\\` loam could not read against the map this feature's own merge would leave behind — a parse error, a hop naming an element the merge does not land, or a landscape merge that itself refuses. The flows were NOT graded, and the archive would copy them into \\`architecture/\\` for the next reader's \\`loam validate --all\\` to fail on. \\`--approve\\` does NOT move it (\\`overridable: false\\`): that flag overrides loam's judgment about coherence, never its ability to read an axis | fix what the message names. A hop into an element the feature adds is legal — the flow is graded against the POST-MERGE map — so the usual cause is a spelling that matches neither the living landscape nor \\`delta.likec4\\` |\n| \\`capability.remove-requirement-realized\\` | the same join in the removal direction: a feature's capability delta REMOVES a requirement that something the merge leaves behind still names with \\`Realizes:\\` — a living service requirement this feature does not also change, or one of its own ADDED/MODIFIED requirements. Without it the archive succeeded and the very next \\`loam validate --all\\` failed with \\`capability.realizes-unknown\\` against a service document nobody in the feature had touched. An ERROR, exactly as \\`openapi.remove-op-consumed\\` is for the identical shape one axis over; \\`--approve\\` overrides | retire the realizing requirement in the same feature, or drop its \\`Realizes:\\` line (a MODIFIED does that by restating the requirement without it), or \\`loam archive <FEAT> --approve\\` to break the join deliberately |\n| \\`glossary.term-exists\\` | a \\`features/<FEAT>/glossary/<term>.md\\` whose term the LIVING glossary already defines. The merge is a whole-file copy, so it would replace an authored definition wholesale — and \\`--approve\\` does NOT move this one (\\`overridable: false\\` in the \\`--json\\` refusal), because approving it is approving a deletion nobody described. A feature-local glossary document INTRODUCES a term; it does not rewrite one | delete the feature's copy and edit \\`glossary/<term>.md\\` directly in the same change, where git produces an ordinary conflict if somebody else is editing it too |\n| \\`usecase.flow-exists\\` | a \\`features/<FEAT>/usecases/<name>.likec4\\` whose flow the LIVING \\`architecture/usecases/\\` already holds. The merge is a whole-file copy, so it would replace an authored hop sequence wholesale — and \\`--approve\\` does NOT move this one (\\`overridable: false\\` in the \\`--json\\` refusal), because a \\`dynamic view\\`'s ordered steps are written down in no other document the fleet keeps. A feature-local flow INTRODUCES a use case; it does not rewrite one | delete the feature's copy and edit \\`architecture/usecases/<name>.likec4\\` directly in the same change, where git produces an ordinary conflict if somebody else is editing it too |\n| \\`deployment.doc-exists\\` | a \\`features/<FEAT>/deployment/<name>.likec4\\` whose file the LIVING \\`architecture/\\` already holds. The merge is a whole-file copy, so it would replace an authored topology wholesale, and \\`--approve\\` does NOT move it — the same act, and the same severity, as \\`usecase.flow-exists\\` one axis over. The collision is on the FILE, never on what is inside it: two features may both \\`extend\\` one living region from documents of their own and both archive | delete the feature's copy and edit \\`architecture/<name>.likec4\\` directly in the same change — or, to ADD to what that file declares, rename the feature's document and say \\`extend\\` in it |\n| \\`deployment.doc-invalid\\` | a \\`features/<FEAT>/deployment/*.likec4\\` loam could not read against the map this feature's own merge would leave behind — a parse error, an unresolved element, or a landscape merge that itself refuses. The corpus is the feature's \\`deployment/\\` PLUS the living \\`architecture/\\` documents it \\`extend\\`s, so the fault may be in either and every message names its file. \\`--approve\\` does not move it: the flag overrides loam's judgement about coherence, never its ability to read an axis | fix what the messages name. A document instancing an element only this feature's \\`delta.likec4\\` adds is LEGAL and resolves — that is what the merge preview is for; one naming an element the merge does not land is the case this refuses |\n| \\`deployment.doc-reserved\\` | a \\`features/<FEAT>/deployment/<name>.likec4\\` whose name loam owns inside \\`architecture/\\`: \\`subsystems.likec4\\` (generated, overwritten wholesale by \\`loam subsystem sync\\`, and parsed by nothing — so topology landing there is invisible from the moment it archives), \\`landscape.likec4\\` (spliced into, never replaced), or any path under \\`usecases/\\` (the flow tree). Refused whether or not the target exists, which is the point: two of the three fail silently when it does not | rename the feature's document — its name is yours to choose, and \\`extend\\` is how it adds to what a living document already declares |\n| \\`glossary.unlinked\\` (warn) | a term defined in \\`glossary/<term>.md\\` that no document OUTSIDE \\`glossary/\\` links to — the mirror of \\`permissions.unenforced\\` and \\`capability.unrealized\\`, and it earns its place for their reason: a vocabulary is worth what cites it. A term cited only by OTHER TERMS is still unlinked and the citing terms are listed in \\`details\\` — a glossary is a network of definitions, so \\`order-line\\` linking to \\`order\\` says the glossary is consistent, not that the fleet uses the word. When NOTHING outside cites anything it is one finding for the whole glossary rather than one per term, because that is one afternoon's work and not N breaches. Warn and never a gate: writing the vocabulary ahead of the fleet is the intended use | cite the term from the requirement, capability or intent that uses it — \\`[Order](../../glossary/order.md)\\` — or delete the definition. \\`loam list glossary\\` shows every term with the documents that cite it |\n| \\`link.unresolved\\` | a markdown link in an authored document whose relative target names nothing. ONE finding per document, listing every broken link with its line, because a rotted set of links is one editing session rather than ten separate breaches. The corpus is every authored markdown document: both spec axes living and delta, \\`runbook.md\\`, every \\`adrs/\\` file at all three altitudes, \\`intent.md\\`, and each capability document — the fleet's own ADRs and living capability tree are graded once, on the \\`landscape\\` target, and never repeated per service. Three things are deliberately NOT graded, each of them a question loam has no answer to: a target outside the docs repo (that tree may not be checked out beside these documents), a bare \\`#section\\` anchor, and anything inside a fenced block or an inline code span. CASE **is** graded — a link resolves only when each path segment is spelled the way the directory stores it, and when only the case is wrong the finding names the stored spelling, so a link that works on a Windows laptop and 404s on GitHub is caught here rather than in review | fix the path, or write the document it points at. A link here is a join of the same kind as \\`Operations:\\` and \\`Covers:\\` — write it as a relative path, which is the only form that can resolve |\n| \\`link.unreadable\\` | a document in that corpus could not be decoded — almost always UTF-16, which PowerShell's \\`>\\` and \\`Out-File\\` write unasked. Its links were not checked; everything else about the service or feature still was, which is exactly why this is not \\`service.unreadable\\` | re-save the file as UTF-8 (\\`Out-File -Encoding utf8\\`) |\n| \\`docs.commit-interrupted\\` | a \\`.loam-commit\\` journal sits in the docs repo: a writer's commit never finished, so the living docs may be half-merged — nothing graded in the same run can be trusted to describe one state, and a green here would certify bytes no run produced | run the command the finding names (it recovers first), or \\`loam doctor\\` for the file-by-file account; the finding leads every validate mode until the journal is gone |\n| \\`health.invalid\\` (warn) | health.yaml exists but does not parse — alert/SLI ids are unreadable, so \\`Covers: alert:/sli:\\` entries and health coverage are unchecked (a missing health.yaml is legal and silent; an unreadable one used to masquerade as \\`covers.unknown\\` typos) | fix the YAML — the health axis resumes once it reads |\n| \\`health.uncovered\\` (warn) | health.yaml declares an alert or SLI no arch.spec.md requirement covers | write the arch requirement with \\`Covers: alert:<id>\\` / \\`Covers: sli:<id>\\` — a signal nothing tests is dashboard decoration |\n| \\`health.dependency-unmodelled\\` (warn) | a \\`dependencies:\\` id in health.yaml that nothing in this service's OWN model.likec4 answers to — by element id, \\`metadata { service }\\` binding, or title. Resolved against the service model, never the landscape: the model carries everything the service touches (private stores included, as nested containers), and the on-call file must name the same world. Either a dependency nobody modelled, or one that no longer exists — for example, a disabled optional service registry left as \\`critical: startup\\` under a green validator | model it (an external system as its own element with the real edge; a private store as a nested container), correct the id to one of the element's three names, or delete the entry |\n| \\`gherkin.missing\\` (warn) | a living scenario no generated .feature scenario carries a digest for — the suite has no test for these words. Fires only in the service's repo, once \\`<gherkinDir>/loam/\\` exists | \\`loam gherkin --service <id>\\` regenerates the suite |\n| \\`gherkin.stale\\` (warn) | a generated scenario's digest matches no living scenario while its requirement still exists — the spec moved under the file (a reworded scenario reports stale + missing together) | regenerate; never edit a generated file to catch it up |\n| \\`gherkin.orphaned\\` (warn) | a generated file whose requirement no longer exists in the living spec (a file for a feature still in flight is exempt — it answers to its feature until it archives) | regenerate; the orphaned file is deleted and reported |\n| \\`gherkin.path-outside\\` | this repo's \\`gherkinDir\\` resolves outside the repository — a \\`..\\` segment, an absolute path, or a symlink pointing away — so generating would write where nothing here can review it. Refused before any file is read, not repaired | point \\`gherkinDir\\` in loam.json at a directory INSIDE this repository; it is the suite's own home, and a path outside it is a write nobody's diff would show |\n| \\`evidence.checked\\` (ok) | every evidence pin on the federated verification records this service attested re-resolved clean against this working tree. Fires only in the service's own repo, like \\`sources.*\\` — from the docs repo the whole \\`evidence\\` family is silent | nothing — it is the confirmation |\n| \\`evidence.unpinned\\` (ok) | agent-confirmed citations recorded before evidence pins existed, so nothing can grade their drift | the next \\`loam verify <FEAT> --service <id> --record\\` stamps pins; not work on its own |\n| \\`evidence.unresolved\\` (warn) | a pinned citation cannot be re-checked: the cited file is gone, unreadable, unsafe, not a regular file, or the cited line is past its end | re-read the evidence in this tree, re-answer, and re-record from this repo — a pin lint never moves the verdict by itself |\n| \\`evidence.moved\\` (warn) | a cited file's normalized sha256 differs from the pin while the cited line's text survives — the mildest drift: the file changed around the citation | re-read the cited file; re-record once the answers are re-checked |\n| \\`evidence.line-changed\\` (warn) | the cited line no longer says what the pin recorded — the citation no longer shows what it showed | re-read the evidence and re-answer; the claim may still be true, but this line no longer backs it |\n| \\`evidence.token-missing\\` (warn) | the cited file contained the literal the claim asserts (the operationId, message name or edge op the pin carries as \\`token\\`) at the attested commit, and no longer does — a token the commit never contained is warned once at record time and never stamped, so this grade always convicts a disappearance | check whether the code moved or the claim stopped being true; re-answer and re-record from this repo |\n| \\`evidence.record-unreadable\\` (warn) | a feature's verification.yaml exists but cannot be read as a record (broken YAML, a malformed \\`evidence_pins\\` block, a summary contradicting its claims) — none of its evidence was checked, and a silent skip would read as clean | repair the YAML by hand, or re-record from this repo; loam never overwrites a record it could not read |\n\n\\`--feature <FEAT-id>\\` — a change's three axes against each other. The SAME checks\ngate \\`loam archive\\` (errors block it; warnings never do), so a clean run here is\nwhat lets a feature ship:\n\n| code | what it means | what to do |\n|---|---|---|\n| \\`delta.invalid\\` | delta.likec4 does not parse | fix this first — an unreadable axis makes every other check meaningless |\n| \\`delta.nothing-tagged\\` | the delta declares elements/relationships but none carry \\`#<FEAT>\\` | tag what IS the change — untagged parts are context, and archive merges only tags |\n| \\`delta.service-unknown\\` | a \\`specs/<svc>/\\` directory addresses a service that neither exists as \\`services/<svc>/\\` nor is introduced by this feature's own tagged delta.likec4 — a typo in \\`--touches\\`, which archive would otherwise materialise as a phantom service directory | fix the id (the message offers close ones); suspended while \\`delta.invalid\\` stands, because an unparseable delta cannot be asked what it introduces |\n| \\`delta.service-id-invalid\\` | a \\`specs/<svc>/\\` DIRECTORY NAME that is not a legal service id (a space, a \\`..\\`, a Windows-reserved stem like \\`NUL\\`, a trailing dot) — archive would materialise \\`services/<svc>/\\` from it, a directory every authoring command then refuses to address (\\`service.id-invalid\\`). Never suspended: the name is illegal whatever the delta says | rename the specs/ directory to a legal id; \\`--approve\\` does not override this one — the breakage is mechanical, not a judgment call |\n| \\`spec.merge-conflict\\` | a spec delta (\\`specs/<svc>/spec.md\\` or \\`arch.spec.md\\`) still holds \\`<<<<<<<\\` / \\`=======\\` / \\`>>>>>>>\\` markers — the same code and the same sentence as service scope, because the markers merge into the living document as prose under somebody's requirement | resolve the conflict before archive; the merge rewrites the requirements section and the marker lines inside it disappear with it |\n| \\`requirements.stepless-scenario\\` | a scenario in a spec delta with a heading and no steps — same code as service scope, and it merges into the living spec as a requirement the coverage rule calls covered from then on | write the body as \\`- **Given/When/Then**\\` bullets |\n| \\`requirements.assertionless-scenario\\` | a scenario in a spec delta with steps and no \\`Then\\` — same code as service scope, and it merges into the living spec as acceptance criteria that assert nothing | add the \\`- **Then**\\` the scenario was written for |\n| \\`requirements.examples-unbound\\` / \\`requirements.examples-unreferenced\\` | a delta scenario whose \\`Examples\\` header and \\`<placeholders>\\` disagree — same codes and same sentences as service scope | add or correct the column; reference it, or drop it |\n| \\`spec-api.op-undefined\\` | a requirement governs an operation its provider's OpenAPI does not define | define the endpoint, or correct the \\`Operations:\\` line |\n| \\`spec-api.op-pending\\` (warn) | the governed operation is defined by another feature in flight | archive that feature first |\n| \\`c4-api.op-undefined\\` | an edge calls an operation the target does not expose | a broken contract between services — fix the caller or add the endpoint |\n| \\`c4-api.op-pending\\` (warn) | the called operation is defined by another feature in flight | archive that feature first |\n| \\`c4-api.op-deprecated\\` (warn) | a NEW tagged edge consumes an operation the living provider's OpenAPI marks \\`deprecated: true\\` — building new consumption on a dying op (quiet when this feature's own openapi delta drops the flag: that IS the un-deprecation) | point the edge at the replacement operation, or say why the deprecated one is right; never gates archive |\n| \\`c4-api.op-removing\\` | a NEW tagged edge consumes an operation this feature removes | remove or redirect the new edge; one feature cannot retire and add consumption of the same operation |\n| \\`openapi.remove-target-missing\\` | an \\`x-loam-remove: true\\` marker's exact path+method does not exist in living OpenAPI | update the stale marker to the current slot, or drop it if the operation is already gone |\n| \\`openapi.remove-target-mismatch\\` | the marker's operationId differs from the living operation at that path+method | name the living operation exactly; loam never deletes the different operation occupying the slot |\n| \\`openapi.remove-marker-missing\\` | a REMOVED requirement governs an operation but the feature has no removal marker | add the exact path/method operation with its operationId and \\`x-loam-remove: true\\` to the feature openapi.yaml |\n| \\`openapi.remove-marker-unjustified\\` | a removal marker is not named by any REMOVED requirement's \\`Operations:\\` line | remove the marker or retire the governing requirement in the same feature |\n| \\`openapi.remove-marker-anonymous\\` | an \\`x-loam-remove: true\\` marker with no \\`operationId\\` — loam cannot tell which operation it retires, and the marker itself would be written into the living contract | name the operation the marker retires, exactly as the living contract spells it |\n| \\`openapi.remove-op-consumed\\` | the feature retires an operation the LIVING fleet still consumes — a landscape edge's \\`metadata { op }\\`, or another service's living requirement naming it | fix the consumer in the same feature (redirect the edge, retire its requirement), or archive with \\`--approve\\` if the consumer is already gone in code |\n| \\`c4-event.message-undefined\\` | a tagged edge \\`publishes\\`/\\`consumes\\` a message its bound service's asyncapi contract — as this feature's merge would leave it; feature ∪ living where no delta exists — declares no matching \\`action: send\\`/\\`receive\\` operation for | declare the operation in the feature's asyncapi.yaml, or correct the edge |\n| \\`c4-event.message-pending\\` (warn) | the named message is introduced by another feature in flight | archive that feature first |\n| \\`spec-event.message-undefined\\` | a delta requirement's \\`Publishes:\\`/\\`Consumes:\\` line names a message the service's asyncapi contract — as this feature's merge would leave it, so a retired declaration stops answering and a leftover line on a surviving requirement is caught before it breaks the post-archive fleet — does not declare in that direction; the same code validate gives the living-side breach | declare it in the feature's asyncapi.yaml, retire the line with its requirement, or correct it |\n| \\`spec-event.message-pending\\` (warn) | the named message is introduced by another feature in flight | archive that feature first |\n| \\`asyncapi.baseline-missing\\` (warn, gates archive; one per service) | slots or component surfaces in this feature's asyncapi.yaml carry no baseline pin (\\`x-loam-based-on\\` / \\`x-loam-baselines\\`) | run \\`loam rebase <FEAT>\\` — until then the merge cannot tell the slots and surfaces you EDITED from the ones you merely restated, and every restated one would revert whatever landed on it. \\`--approve\\` merges unpinned deliberately |\n| \\`asyncapi.baseline-stale\\` | the living slot, or the living component surface, changed after this delta pinned it | somebody landed a change in between. Re-read the living value, fold in what you still mean, then \\`loam rebase <FEAT>\\` |\n| \\`asyncapi.baseline-invalid\\` | an \\`x-loam-based-on\\` that is not a digest or one on a slot the living contract has no counterpart for; a malformed \\`x-loam-baselines\\` record, or an entry of it naming a component surface the delta does not declare (or whose living value is gone) | \\`loam rebase\\` writes both; a new channel, operation, message or component has no living version to be based on, so it carries no pin |\n| \\`asyncapi.remove-target-missing\\` | an \\`x-loam-remove: true\\` marker addresses a slot the living asyncapi does not have | update the stale marker to the current key, or drop it if the slot is already gone |\n| \\`asyncapi.remove-target-mismatch\\` | the marker's message name differs from the living declaration at that key | name the living message exactly; loam never deletes a different message occupying the slot |\n| \\`asyncapi.remove-marker-missing\\` | a REMOVED requirement's \\`Publishes:\\`/\\`Consumes:\\` line names a message the living contract still declares, with no matching marker in the feature's asyncapi.yaml | add the \\`components.messages\\` entry with the message's name and \\`x-loam-remove: true\\`; a message the living contract declares only INLINE under a channel is retired by that channel's own removal marker (\\`channels.<key>: {x-loam-remove: true}\\`) instead |\n| \\`asyncapi.remove-marker-unjustified\\` | a message-removal marker no REMOVED requirement's \\`Publishes:\\`/\\`Consumes:\\` line justifies (channel and operation slots need exactness only — requirements never join on their keys) | remove the marker, or retire the governing requirement in the same feature |\n| \\`asyncapi.remove-message-consumed\\` | the feature retires a message the LIVING fleet still consumes — a landscape \\`consumes\\` edge, or another service's living \\`Consumes:\\` requirement | fix the consumer in the same feature (drop the edge, retire its requirement), or archive with \\`--approve\\` if it is already gone in code |\n| \\`asyncapi.message-conflict\\` (warn) | two features in flight add or edit the same (service, message) | whichever archives second replaces the other's declaration wholesale — coordinate before either ships |\n| \\`c4.op-ungoverned\\` (warn) | an operation is called but no requirement governs it | write the requirement |\n| \\`c4.op-link-missing\\` (warn) | a \"Calls\" edge in the delta with no \\`metadata { op }\\` | link it to the operationId |\n| \\`c4.service-binding-invalid\\` | an explicit \\`metadata { service }\\` binding — a tagged element's, or an untagged child's anywhere inside its block, since the merge splices the whole authored block verbatim — is not a legal \\`services/<id>/\\` name (a space, a \\`../\\`, a Windows-reserved stem); archive would write the name into the living landscape, and a \\`../\\` collapses its \\`services/\\` probe out of the docs repo | fix the binding to the directory the element means; \\`--approve\\` does not override this one — the path the name becomes is mechanical, not a judgment call |\n| \\`api.op-unconsumed\\` (warn) | an added operation no edge consumes | model the caller, or say why it is provider-only |\n| \\`service.no-requirement-delta\\` (warn) | a new service with no spec delta | write \\`specs/<svc>/spec.md\\` |\n| \\`scaffold.placeholder\\` (warn, gates archive) | a feature document still carries \\`loam new\\`'s exact placeholder text — a \\`TODO — name the behaviour\\` requirement, a \\`TODO — name the case\\` scenario, an \\`<angle-bracket>\\` fill-in, or a \\`TODO — what this service owns\\` description | write the real content; the merge would otherwise publish a requirement or description nobody authored. \\`--approve\\` overrides |\n| \\`intent.empty\\` (warn, gates archive) | intent.md is missing, or its Why/Scope hold nothing outside the scaffold's own comments | write why the feature exists — it is the first thing a reviewer reads. \\`--approve\\` overrides |\n| \\`archedge.uncovered\\` (warn) | no scenario names a tagged edge (a heuristic) | write the scenario, or say why the edge needs none |\n| \\`c4.uncovered\\` (warn) | a NEW tagged element or edge in delta.likec4 that no arch requirement covers via \\`Covers:\\` — the mechanical check, not the heuristic above | add the requirement to \\`specs/<svc>/arch.spec.md\\` (outbox? retries? alerts?), or the architectural obligations ship unchecked |\n| \\`delta.unknown-section\\` | a heading that nearly matches the delta grammar | fix it — everything under it merges as NOTHING today, silently |\n| \\`delta.no-delta-sections\\` | requirements, but no \\`## ADDED/MODIFIED/REMOVED Requirements\\` section anywhere — the whole delta would merge nothing | put every changed requirement under its delta section |\n| \\`delta.requirement-not-merged\\` (warn, gates archive) | a requirement under a prose heading (\\`## Behavior\\`) instead of a delta section | move it under \\`## ADDED\\`/\\`## MODIFIED\\`/\\`## REMOVED Requirements\\` — as written, archive drops it. If it really is documentation, quote it under \\`## Requirements\\`, which is exempt |\n| \\`delta.requirement-id-invalid\\` / \\`delta.requirement-id-repeated\\` / \\`delta.requirement-id-duplicate\\` | a stable ID is malformed or ambiguous inside the delta | repair it before archive; identity is never inferred from an invalid declaration |\n| \\`delta.living-requirement-id-invalid\\` | the living file's IDs are malformed or ambiguous | repair the living spec first; the delta cannot select it safely |\n| \\`delta.requirement-identity-collision\\` | the delta's ID and heading point at different living requirements | fix the ID or heading; archive will not guess which identity should win |\n| \\`delta.modified-unknown\\` | MODIFIED a requirement the living spec does not have | use ADDED, or fix the name (a spelling slip reads as a different requirement) |\n| \\`delta.removed-unknown\\` | REMOVED one that does not exist | drop the section, or fix the name |\n| \\`delta.added-duplicate\\` | ADDED a name the living spec already has | use MODIFIED — as written, the merge REPLACES the living requirement |\n| \\`delta.added-near-duplicate\\` (warn) | ADDED a name that differs only in case from a living requirement — the merge matches exactly, so both would coexist | match the living spelling and use MODIFIED, or pick a distinct name |\n| \\`delta.living-duplicate-requirement\\` | two requirements in the LIVING document share one heading — MODIFIED rewrites only the first and REMOVED deletes both, so no delta applies to it predictably | fix the living spec first (merge the bodies, or give each a distinct heading and a \\`Requirement-ID\\`); the delta cannot select safely until you do |\n| \\`delta.modified-pending\\` (warn) | the requirement is introduced by another feature in flight | archive that feature first — \\`loam dependencies --json\\` computes the whole ordering (\\`order\\`), so you do not have to work it out per finding |\n| \\`delta.removed-pending\\` (warn) | REMOVED something another feature in flight introduces | archive that feature first; \\`loam dependencies --json\\` says in which order |\n| \\`delta.added-conflict\\` (warn) | two features in flight add the same requirement | whichever archives first lands it; the other's ADDED then collides with the living spec (\\`delta.added-duplicate\\`, error) and its archive is refused — coordinate now, or rework the later one as MODIFIED after the first ships. No ordering fixes it, which is why \\`loam dependencies\\` reports it under \\`conflicts\\` rather than in \\`order\\` |\n| \\`delta.modified-conflict\\` (warn) | another feature in flight MODIFIES or REMOVES the same living requirement | both deltas apply cleanly, so whichever archives second replaces the other's text wholesale — \\`loam dependencies\\` lists it under \\`conflicts\\`; merge the two intentions before either ships |\n| \\`delta.baseline-stale\\` | the living requirement changed after this delta was written against it | someone landed a change in between and this MODIFIED would replace it outright. Re-read the living requirement, fold in what you still mean, then \\`loam rebase <FEAT>\\`. Re-pinning without re-reading is how you overwrite them on purpose |\n| \\`delta.baseline-missing\\` (warn, gates archive) | a MODIFIED/REMOVED requirement with no \\`Based-On:\\` | run \\`loam rebase <FEAT>\\` — until then nothing can say whether the living text moved under this delta, and archive refuses to merge it. Expected on deltas adopted from OpenSpec, which never carried the line: one rebase pins the whole feature, and \\`--approve\\` merges unpinned deliberately |\n| \\`delta.baseline-invalid\\` | a \\`Based-On:\\` that is not a digest, declared twice, or sitting on an ADDED requirement | \\`loam rebase\\` writes the value; an ADDED requirement has no living version to be based on, so drop the line or make it MODIFIED |\n\n\\`--all\\` — everything above for every target, plus the fleet cross-check:\n\n| code | what it means | what to do |\n|---|---|---|\n| \\`landscape.missing\\` | \\`architecture/landscape.likec4\\` does not exist — an ERROR as soon as \\`services/\\` holds one service, a warning only in an empty docs repo | create it: a \\`specification { ... }\\` block declaring the kinds, then a \\`model { ... }\\` block with one bound element per service (\\`metadata { service '<id>' }\\`) and one edge per cross-service call carrying \\`metadata { op '<operationId>' }\\`. With no fleet map every cross-service check is blind, not passing |\n| \\`landscape.invalid\\` | the living landscape does not parse | fix it first — the fleet cross-check cannot run against a document nobody can read |\n| \\`service.id-invalid\\` | a \\`services/<id>/\\` directory whose name is not a legal service id — every authoring command refuses it, so nothing in that directory can be changed through loam. Fleet scope only: the SET of service directories is what makes the id a question, and a \\`--service\\` run is already refused by its own guard before it gets this far (the guard refuses only a name that neither exists as a directory nor is a legal id — a badly-named directory that DOES exist resolves through the enumeration and grades as an ordinary target, still without this finding). Graded before the map is opened, so it stands even when the landscape is missing or unreadable | rename the directory, then update its \\`service:\\` frontmatter, its \\`metadata { service }\\` binding in the landscape, and any \\`features/<FEAT>/specs/<id>/\\` naming it |\n| \\`subsystem.unmarked\\` | a directory under \\`services/\\` that is neither a subsystem (no \\`subsystem.yaml\\`) nor a service (no service artifact), with directories beneath it — the shape an ordinary merge produces when one branch deletes an emptied subsystem's marker while another moves a service in. The services beneath STAY enumerated and the finding names every one of them: the fleet is never reported smaller than it is | restore \\`subsystem.yaml\\` in that directory (an empty file is a valid marker), or move the directories out |\n| \\`subsystem.marker-misplaced\\` | \\`subsystem.yaml\\` sitting BESIDE service artifacts (spec.md, model.likec4, …) — a directory is a subsystem or a service, never both; it stays classified as a service | delete the marker, or move the artifacts into a service directory of their own inside the group |\n| \\`subsystem.name-collision\\` | one name claimed by two or more directories in the \\`services/\\` tree — service ids and subsystem names share one flat namespace, unique at any depth, because a service's identity is its leaf directory name alone and placement never disambiguates it. The message names every claimant | rename all but one, then update whichever frontmatter, landscape bindings and \\`features/<FEAT>/specs/<id>/\\` name the renamed service |\n| \\`subsystem.name-invalid\\` | a subsystem directory name that breaks the id grammar (a space, a \\`..\\`, a Windows-reserved stem, a trailing dot) — subsystem names take the service-id grammar because both become directory names in one shared tree. Its services stay enumerated meanwhile | rename the directory to a legal name |\n| \\`subsystem.invalid\\` | a \\`subsystem.yaml\\` that exists but cannot be read as subsystem metadata — presence still classifies the directory as a subsystem (content is metadata, never identity), and exactly one finding is emitted per file | fix the file: a YAML mapping with optional \\`title\\`, \\`description\\` and \\`owner\\`; never \\`members\\` — the directory itself is the membership record. An empty file is also a valid marker |\n| \\`subsystem.views-stale\\` | \\`architecture/subsystems.likec4\\` — the GENERATED file holding one LikeC4 view per subsystem — does not match the tree: it drifted after a move, it is absent while subsystems exist, or it lingers after the last subsystem was removed. One error on one file, graded by byte comparison against the deterministic re-render (no check ever reads the file's content) | run \\`loam subsystem sync\\` — the single regenerator. Never edit the file by hand; authored views belong in the landscape or a file of your own |\n| \\`service.likec4-config-missing\\` (warn) | a \\`services/<…>/<id>/model.likec4\\` with no \\`likec4.config.json\\` beside it — the root LikeC4 project excludes \\`services/**\\`, so a model renders only as a project of its own, and this one has none: a box on the fleet map with nothing renderable inside it. One warning per service, on the fleet target, only when the root \\`likec4.config.json\\` exists (\\`loam doctor\\` prints that one); never gating, because no loam check reads the model any differently | run \\`loam subsystem sync\\` — it creates the file (create-only: a file that already exists is yours and is never touched). Never hand-write it; the bytes are a function of the service id, and the writer spells them once |\n| \\`subsystem.view-id-collision\\` | \\`architecture/landscape.likec4\\` declares a view whose id is one loam GENERATES into \\`architecture/subsystems.likec4\\`. LikeC4 merges every file in \\`architecture/\\` into one project and view ids are a flat namespace there, so the renderer refuses the WHOLE project and draws nothing — while every other check here stays green. Only a real collision is reported: a \\`subsystem_\\`-looking id matching no current subsystem breaks nothing yet, and fires on the run after somebody creates that subsystem | rename the view in \\`architecture/landscape.likec4\\`. Ids beginning \\`subsystem_\\` are loam's to mint; \\`architecture/subsystems.likec4\\` is generated (\\`loam subsystem sync\\`) and is never hand-edited |\n| \\`landscape.merge-conflict\\` | the fleet map still holds conflict markers — it is two halves of two different maps, and every cross-service check reads it. Checked BEFORE the parse and returns like \\`landscape.invalid\\`: nothing is concluded from the file, so no \\`landscape.service-unmodelled\\` fires behind it | resolve it keeping BOTH sides' elements and edges; ten people adopting ten services into one landscape.likec4 in the same week is how it happens. \\`loam archive\\` refuses on it too (\\`merge-failed\\`, not overridable with \\`--approve\\`) |\n| \\`landscape.service-unmodelled\\` | a \\`services/<svc>/\\` no element in the landscape resolves to | draw it, or bind an existing element with \\`metadata { service '<svc>' }\\` — the fleet map is incomplete until you do |\n| \\`landscape.service-undocumented\\` (warn) | a landscape element with no \\`services/<id>/\\` | document the service, bind the element to the directory it means, or tag it \\`#external\\` if it is not ours |\n| \\`landscape.binding-unknown\\` | an element's \\`metadata { service }\\` names a directory that does not exist | fix the id or create the service — a binding is a claim, and this one is false |\n| \\`landscape.binding-duplicate\\` (warn) | two elements resolve to the same \\`services/<id>/\\` | every element→service join picks one of them arbitrarily, so the other's edges are filed under a service that does not own them — bind each element to the directory it actually is, or tag the stray one \\`#external\\` |\n| \\`landscape.kind-tag-graded\\` | the \\`specification\\` block declares \\`#external\\` or \\`#platform\\` on an element KIND, and at least one element of that kind stands for a real \\`services/<id>/\\` — bound with \\`metadata { service }\\`, or titled like one. Since LikeC4 1.59.0 a kind's tags land on every element of it, and loam reads both tags as \"not ours, stop grading\", so the exemption is silencing the checks for a service of yours; the run would otherwise end on \\`landscape.matched\\` over a map nothing checked | remove the tag from the kind and write \\`#external\\` / \\`#platform\\` on the individual elements that really are foreign — or stop drawing your own services as that kind. A graded tag on a kind whose elements stand for no directory of yours (the \\`element topic { #external }\\` idiom for the topics inside a broker) is correct and stays silent, and any OTHER tag is safe on any kind — loam does not read it |\n| \\`landscape.platform-candidate\\` (warn) | an \\`#external\\` element consumed by three or more services and not tagged \\`#platform\\` — a hub on its way to making the fleet view unreadable | declare \\`tag platform\\` in the \\`specification\\` block and tag the element (LikeC4 refuses an undeclared tag, so both steps are needed): the fleet view then excludes it, and a platform view over \\`include * -> element.tag = #platform\\` keeps \"who depends on it\" answerable |\n| \\`landscape.datastore-private\\` (warn) | a datastore element with exactly one consumer service at fleet level — drawn as a peer, it reads as a system in its own right | it is that service's internals: move it into \\`services/<id>/model.likec4\\` as a nested container and delete it from the landscape, or add the second consumer's edge if another service really reaches the same data |\n| \\`landscape.datastore-shared\\` (warn) | a datastore element consumed by two or more services — the strongest coupling in the fleet, stated rather than inferred | shared means the same DATA: if they read the same tables or keys, keep it drawn and let the warning state the coupling; if they only share a host or cluster (two schemas, two lock paths), model one private store per service and put the blast radius in the runbook |\n| \\`usecase.capability-unresolved\\` | a \\`dynamic view\\` tagged \\`#cap-<slug>\\` whose slug matches no declared capability id, or more than one. The tag spells the id with every character outside \\`[A-Za-z0-9_-]\\` flattened to \\`-\\` (that is all a LikeC4 tag name accepts) and that flattening is not injective, so \\`payments/refunds\\` and \\`payments-refunds\\` both answer to one tag. Silent while \\`architecture/capabilities.yaml\\` is absent or invalid, like the rest of that family — the FILE is the capability axis's opt-in | zero matches: fix the tag, or declare the capability in the fleet vocabulary (the message offers close ids, each already spelled as the tag you would write). More than one: rename one of the colliding ids — nothing in the tag can say which the flow realizes, and a guess files every hop of it under an owner who never claimed it |\n| \\`usecase.requirement-unresolved\\` | a \\`dynamic view\\` tagged \\`#req-<slug>\\` whose slug names no requirement of the capability that view is about. Six failures with six fixes, and the message says which: the view resolves no \\`#cap-\\` tag (a \\`Requirement-ID\\` is unique only inside its own document, so the id has no scope), it resolves two (nothing says which document), the capability has no \\`capabilities/<id>/spec.md\\`, that document declares no requirements yet, it declares none flattening to this slug, or two of its ids flatten to one slug. It suppresses NOTHING: only a RESOLVED claim counts as a promise kept, so a broken tag leaves \\`capability.requirement-unrealized\\` firing beside it | follow the message. Several \\`#req-\\` tags on one view are legal and normal — a flow commonly keeps two of a capability's promises |\n| \\`usecase.step-unbacked\\` | a hop of a capability-tagged \\`dynamic view\\` that NO relationship in the model backs, on either tier. LikeC4 reports no error for it — a dynamic view is allowed to show a call the model never declared — so the diagram renders, the project parses, and every other check stays green. The finding names the file the view was written in, the step's ordinal and title, and the oriented pair | draw the edge in \\`model { }\\` (with \\`metadata { op '<operationId>' }\\` where it is a call), or, if the hop is a return step, write it as \\`<target> <- <source>\\`: a reply is attributed to the CALL it answers, so the \\`<-\\` spelling looks the hop up under the reversed pair instead |\n| \\`usecase.step-contested\\` (warn) | two or more relationships back one hop and they name DIFFERENT operations — an absent \\`metadata { op }\\` counts as a value there, not as a gap — so nothing can say which one the hop exercises. \\`details\\` lists every candidate as \\`<source> -> <target> \"<title>\" (op: <id>)\\` | give the candidates one operation, or draw the hop between the elements that name exactly one. loam names them rather than picking, because a guessed operation reads exactly like a right one in every rollup built over the flow. It suppresses \\`usecase.step-unlinked\\` on the same hop: one breach, one finding |\n| \\`usecase.step-unlinked\\` (warn) | the relationship backing a hop carries no \\`metadata { op }\\` and no \\`publishes\\`/\\`consumes\\`, while the hop's CALLER is not a person and its target resolves to a real \\`services/<id>/\\` — the flow reaches the fleet map and stops short of the contract it drives. \\`spine.op-link-missing\\` at use-case altitude. A hop FROM a person never fires it — the first hop of almost every sequence diagram is somebody using the app, and an app owes no operationId for a click — and neither does a hop INTO an \\`#external\\` system, an actor, a database, a cache or a queue: none of those owns a contract to name. A \\`publishes\\`/\\`consumes\\` edge already reaches an API — an AsyncAPI one | add \\`metadata { op '<operationId>' }\\` to the edge in \\`model { }\\`, or \\`metadata { publishes '<message>' }\\` / \\`consumes\\` where the hop is an event |\n| \\`feature.unreadable\\` | an artifact of a feature in flight could not be read — nothing about that feature was graded, and the rest of the fleet still was | fix the file the finding names; a feature silently skipped would have read as a clean one |\n| \\`sources.unverifiable-from-here\\` (ok) | one finding PER SERVICE whose \\`sources\\` only its own repo can check — a confirmation, not work, so it never trips \\`--strict\\`, and it is present in \\`--service\\` runs from the docs repo too | run \\`loam validate --service <id>\\` from inside that service's repo to actually grade its provenance; the fleet rollup line is derived from these findings |\n| \\`docs.binary-behind\\` (warn) | the docs repo's AGENTS.md stamp names a loam NEWER than the one running. This is the one finding that changes what a PASS means: a binary predating a grammar addition does not fail on the newer directive, it reads it as prose, has no join to fail, and reports green — so the corpus was checked less than the run implies. Warn rather than error because a mixed-version fleet is ordinary; \\`--strict\\` is the lever if it should gate | upgrade loam and re-run. Do NOT edit AGENTS.md — its stamp is correct and this binary is the older half |\n| \\`agents.stale\\` (warn) | the docs repo's AGENTS.md has no version stamp, or one older than the running binary — its flag and code tables may describe a loam that no longer exists | review AGENTS.md against the current \\`loam --help\\`, then update the \\`<!-- generated by loam vX.Y.Z -->\\` line; a hand-curated file silences it by bumping the stamp itself. Never regenerate the file — the team's edits outrank the template |\n\nfrontmatter and provenance — services' spec.md and arch.spec.md, and features'\nintent.md, both modes:\n\n| code | what it means | what to do |\n|---|---|---|\n| \\`frontmatter.missing\\` (warn) | no frontmatter at all | add owner, status and sources |\n| \\`frontmatter.malformed\\` | the frontmatter block does not parse as YAML — owner, status and sources are unreadable, which is not the same fact as absent | fix the header; the field checks and the sources chain resume once it parses — never add fields to a block YAML refuses |\n| \\`frontmatter.field-mismatch\\` | the doc names a different service/feature than the one it lives under | fix the frontmatter, or move the file |\n| \\`frontmatter.status-unknown\\` | a status nobody defined (\\`verifed\\`) | use the documented vocabulary — a typo here reads as unverified forever |\n| \\`frontmatter.field-missing\\` (warn) | owner, status or the identity field is absent | fill them in |\n| \\`sources.absent\\` (warn) | the doc names no sources | nothing ties it to the code, so nothing can tell you when it goes stale |\n| \\`sources.path-missing\\` | a listed source no longer exists, or is a glob pattern (no longer supported) | the code moved — re-read it and update the doc, do not just fix the path. For a pattern, name files or directories instead |\n| \\`sources.path-outside\\` | a listed source resolves OUTSIDE the service's repository — an absolute path, a \\`..\\` escape, or a path that only exists on one machine. loam will not hash bytes beyond the repo boundary, so the tie the entry claims cannot be checked | name the path repo-relative, inside the repository; if the document was really written from another repo's files, it belongs to that repo |\n| \\`sources.stale\\` (warn) | the source files changed since the doc was vouched for — the finding names which paths were added, changed or removed, from the \\`sources_files\\` index the stamp carries | re-read the code at exactly those paths, correct the doc, then ask a human to \\`loam vouch --service <id>\\` |\n| \\`sources.empty\\` (warn) | the declared \\`sources\\` exist but expand to no files at all — an empty directory, or a tree the repository ignores. A digest over nothing reads as current forever | point \\`sources\\` at the files the document was actually written from; \\`loam vouch\\` refuses to stamp this state with the same sentence |\n| \\`sources.skipped\\` (warn) | a path under a listed source was found but NOT hashed: a symlink loam will not follow (its target is outside the repository, dangling, or not a file or directory) — the digest cannot go stale over bytes it never read | not yours to close by editing the document: either the symlink should not be under a listed source, or \\`sources\\` should name the real path. Report it |\n| \\`sources.unvouched\\` (warn) | \\`sources\\` with no \\`sources_digest\\` — nobody ever stamped it | leave it: vouching is a human's reading, not yours |\n| \\`sources.unwalked\\` (warn) | \\`sources\\` leaves whole top-level paths of the service repo untouched — the \\`details\\` name them, measured against the files git tracks. The one completeness signal loam can compute: every other check compares the documents with each other, and a corpus agrees with itself perfectly while describing a third of a service. Silent where git cannot answer (not a repository, not installed) | open them. Each one is either a part of the service nobody read — go back to the walk \\`loam adopt\\` printed — or something this document legitimately does not owe, and then say which and why in the hand-back. Do NOT add paths to \\`sources\\` that you did not read: the list is a record of what was opened, and padding it destroys the only tie the document has to the code |\n| \\`content.stale\\` (warn) | the spec's body changed since it was vouched — \\`status: verified\\` is standing over words nobody has read. Unlike \\`sources.*\\` it needs no service repo, so it fires from the docs repo too | if you edited the doc, that is the point: report it and ask a human to re-vouch. Never revert the doc or touch the digest just to silence it |\n| \\`sources.sampled-vouch\\` (warn) | a person vouched after reading a recorded SAMPLE of the document — the stamp carries \\`vouch_scope: sampled k/n seed=…\\`, so \\`status: verified\\` stands over sections nobody read. The status and the \\`vouched\\` rung are unchanged and correct; this is what says the reading behind them was partial. A \\`vouch_scope\\` that does not decode fires it too, deliberately: an unreadable scope must never read as a full vouch. Doc-side, so it fires from the docs repo as well | not yours to clear. NEVER edit the document, and never touch or delete the \\`vouch_scope\\` field, to silence it — deleting it would forge a full vouch out of a partial one. A human runs a full \\`loam vouch --service <id>\\` (no \\`--sample\\`) once they have read the rest, which removes the field; \\`loam vouch --pack --service <id>\\` lists exactly what that sample left unread |\n| \\`sources.resolved\\` (ok) | every listed source still exists in the service repo | nothing — it is the confirmation |\n| \\`sources.current\\` (ok) | the stamped \\`sources_digest\\` still matches the code — nothing moved since a human vouched | nothing — it is the confirmation |\n| \\`sources.walked\\` (ok) | no whole top-level path of the service repo is left untouched by \\`sources\\` | nothing — it is the confirmation |\n\n\\`loam archive\\` — breaches the merge computation sees, reported at plan time.\nMost never appear in \\`validate\\`; the ones that do are graded harder here,\nbecause the merge is about to WRITE the breach into the living contract\n(\\`openapi.ref-unresolved\\` and \\`asyncapi.ref-unresolved\\` are validate warns but\nplan errors; \\`openapi.remove-marker-path-level\\` fires in validate on a living\ncontract too):\n\n| code | what it means | what to do |\n|---|---|---|\n| \\`living.requirement-outside-requirements\\` | the LIVING spec holds a requirement outside \\`## Requirements\\`, and the merge rewrites only that section — it would land in the file twice | re-home the requirement under \\`## Requirements\\`, then re-run; \\`--approve\\` does not override this |\n| \\`openapi.op-modified\\` (warn) | the feature redefines an operation the living OpenAPI already has — the merge overwrites the living definition wholesale | make sure the redefinition is intended; if not, align the feature's openapi.yaml with the living one |\n| \\`openapi.path-item-modified\\` (warn) | the delta redefines a PATH-level key (\\`parameters\\`, \\`servers\\`, \\`summary\\`, an \\`x-\\`) the living OpenAPI already has — the overwrite applies to EVERY operation on that path, including ones this feature never mentions | keep path-level keys out of the delta unless changing them for every operation on the path is the intent |\n| \\`service.no-model\\` (warn) | the archive creates \\`services/<id>/\\` but nothing writes its \\`model.likec4\\` | run \\`loam adopt --service <id>\\` after the archive lands, or the new service ships without a C4 center and \\`validate --all\\` grades it incomplete |\n| \\`openapi.component-modified\\` (warn) | a component this delta declares — whether the merged operations reference it or the components ARE the delta — already exists in the living OpenAPI with different content, and the merge copies the feature's version over it wholesale | make sure the redefinition is intended; if not, align the feature's component with the living one |\n| \\`openapi.ref-unresolved\\` | a \\`$ref\\` reachable from the merged operations or a merged component resolves in neither the feature's OpenAPI nor the living one — the merge would write a dangling reference | define the missing component or fix the ref; \\`--approve\\` merges the dangling reference anyway. External refs (not starting \\`#/\\`) are never checked |\n| \\`openapi.remove-marker-path-level\\` | an \\`x-loam-remove: true\\` written at PATH level, beside the methods instead of inside the operation being retired — it names no operation, so it retires nothing, and it is not a contract key either | move the marker inside the operation (with its \\`operationId\\`), or delete it. The merge is safe either way — a feature-only key is never published into the living contract — but the removal you asked for will not happen |\n| \\`asyncapi.message-modified\\` (warn) | the delta redefines a \\`components.messages\\` slot the living AsyncAPI already has — the merge overwrites the living message wholesale | make sure the redefinition is intended; if not, align the feature's asyncapi.yaml with the living one, then \\`loam rebase\\` so the restatement classifies as a quote |\n| \\`asyncapi.channel-modified\\` (warn) | the delta redefines a \\`channels\\` slot the living AsyncAPI already has — the merge overwrites it wholesale, inline channel messages included (they are channel interior, never slots of their own) | make sure the redefinition is intended; if not, align the feature's channel with the living one |\n| \\`asyncapi.operation-modified\\` (warn) | the delta redefines an \\`operations\\` slot the living AsyncAPI already has — the merge overwrites it wholesale | make sure the redefinition is intended; if not, align the feature's operation with the living one |\n| \\`asyncapi.component-modified\\` (warn) | the delta declares a component SURFACE outside \\`components.messages\\` — a \\`components.schemas\\` entry, a security scheme — that the living AsyncAPI already has with different content, and the merge copies the feature's version over it wholesale | make sure the redefinition is intended; if not, align the feature's component with the living one, then \\`loam rebase\\` so the restatement classifies as a quote |\n| \\`asyncapi.ref-unresolved\\` | a \\`$ref\\` the MERGED asyncapi.yaml would carry resolves in neither the feature's document nor the living one — including a removal that deletes a slot the living document still references. A target the feature's own document DECLARES no longer fires: the merge now copies component surfaces too, so the pointer resolves in the document it lands in | define the missing target, fix the pointer, or retire the referrer in the same delta; \\`--approve\\` merges the dangling reference anyway. External refs (not starting \\`#/\\`) are never checked |\n| \\`asyncapi.remove-marker-inline\\` | an \\`x-loam-remove: true\\` nested on an INLINE channel message (\\`channels.<ck>.messages.<mk>\\`) — inline messages are channel interior, never slots, so the marker retires nothing; when the channel is otherwise unchanged it does not even surface as an edit | retire the whole channel (\\`x-loam-remove: true\\` at \\`channels.<ck>\\`), or declare the message under \\`components.messages\\` (the channel aliasing it by \\`$ref\\`) and put the marker there. The merge is safe either way — the deep strip keeps the key out of living — but the removal you asked for will not happen |\n\n\\`loam validate <target>\\` — the positional form's own finding, both modes:\n\n| code | what it means | what to do |\n|---|---|---|\n| \\`target.ambiguous\\` (warn) | the positional name resolves to a service AND a feature — the run validated the feature (the tie-break that has always held) and this warning says so, with \\`resolvedKind\\` in \\`--json\\` | pass \\`--service <id>\\` or \\`--feature <id>\\` to force the reading you meant; a silent precedence rule would be a report about a target nobody asked for |\n\nconfirmations — the \\`ok\\`-severity findings a clean axis reports, so silence is\nnever the signal (\\`--errors-only\\` hides them from the text view; \\`--json\\`\nalways carries them):\n\n| code | what it means | what to do |\n|---|---|---|\n| \\`c4.valid\\` (ok) | the service's model.likec4 parses and the C4 axis was actually graded | nothing — it is the confirmation |\n| \\`delta.valid\\` (ok) | the feature's delta.likec4 parses and its tagged change is well-formed | nothing — it is the confirmation |\n| \\`coherence.ok\\` (ok) | the feature's axes agree with each other and with the fleet in flight — nothing gates its archive | nothing — it is the confirmation |\n| \\`requirements.covered\\` (ok) | every requirement carries at least one scenario | nothing — it is the confirmation |\n| \\`api.covered\\` (ok) | every operation in openapi.yaml is governed by some requirement's \\`Operations:\\` line | nothing — it is the confirmation |\n| \\`event.covered\\` (ok) | the event axis joins clean — every declared message is linked and governed | nothing — it is the confirmation |\n| \\`spine.resolved\\` (ok) | every inbound landscape call into this service resolves to an operation its OpenAPI defines | nothing — it is the confirmation |\n| \\`landscape.matched\\` (ok) | every \\`services/<id>/\\` resolves to a landscape element and every binding holds | nothing — it is the confirmation |\n\n\\`sources.stale\\`, \\`content.stale\\` and \\`sources.sampled-vouch\\` are the warnings you\ncannot close by yourself. Fix what the code now says, then hand it back — the stamp\nis a person's claim to have read it, and the last of the three says how much of it\nthey read.\n`,\n};\n"]}
|
|
@@ -246,5 +246,14 @@ export const VALIDATE_CHECKS = [
|
|
|
246
246
|
via: VIA_ALL,
|
|
247
247
|
what: "two landscape elements resolve to the same services/<id>/ — every element→service join picks one of them arbitrarily, so the other's edges are filed under a service that does not own them",
|
|
248
248
|
},
|
|
249
|
+
// Fires on a fresh baseline by construction: the adopt protocol's step 3
|
|
250
|
+
// writes the model and `subsystem sync` writes the project file, so a
|
|
251
|
+
// service is owed the second command from the moment the first file lands.
|
|
252
|
+
{
|
|
253
|
+
code: "service.likec4-config-missing",
|
|
254
|
+
severity: "warn",
|
|
255
|
+
via: VIA_ALL,
|
|
256
|
+
what: "model.likec4 exists and services/<id>/likec4.config.json does not — the renderer's per-service project file, which `loam subsystem sync` writes; without it the model is a box on the fleet map with nothing inside it",
|
|
257
|
+
},
|
|
249
258
|
];
|
|
250
259
|
//# sourceMappingURL=checks.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"checks.js","sourceRoot":"","sources":["../../../src/core/brief/checks.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AAEH,wEAAwE;AACxE,yEAAyE;AACzE,wEAAwE;AAExE;;;GAGG;AACH,MAAM,WAAW,GAAG,8BAA8B,CAAC;AACnD,MAAM,CAAC,MAAM,OAAO,GAAG,qBAAqB,CAAC;AAc7C,2GAA2G;AAC3G,MAAM,CAAC,MAAM,eAAe,GAAiB;IAC3C,EAAE,IAAI,EAAE,kBAAkB,EAAE,QAAQ,EAAE,OAAO,EAAE,GAAG,EAAE,WAAW,EAAE,IAAI,EAAE,yDAAyD,EAAE;IAClI,EAAE,IAAI,EAAE,YAAY,EAAE,QAAQ,EAAE,OAAO,EAAE,GAAG,EAAE,WAAW,EAAE,IAAI,EAAE,uCAAuC,EAAE;IAC1G;QACE,IAAI,EAAE,gCAAgC;QACtC,QAAQ,EAAE,OAAO;QACjB,GAAG,EAAE,WAAW;QAChB,IAAI,EAAE,kEAAkE;KACzE;IACD;QACE,IAAI,EAAE,4BAA4B;QAClC,QAAQ,EAAE,OAAO;QACjB,GAAG,EAAE,WAAW;QAChB,IAAI,EAAE,wJAAwJ;KAC/J;IACD;QACE,IAAI,EAAE,6BAA6B;QACnC,QAAQ,EAAE,OAAO;QACjB,GAAG,EAAE,WAAW;QAChB,IAAI,EAAE,6HAA6H;KACpI;IACD;QACE,IAAI,EAAE,8BAA8B;QACpC,QAAQ,EAAE,OAAO;QACjB,GAAG,EAAE,WAAW;QAChB,IAAI,EAAE,4FAA4F;KACnG;IACD;QACE,IAAI,EAAE,+BAA+B;QACrC,QAAQ,EAAE,OAAO;QACjB,GAAG,EAAE,WAAW;QAChB,IAAI,EAAE,2FAA2F;KAClG;IACD,qEAAqE;IACrE,wEAAwE;IACxE,4EAA4E;IAC5E,yCAAyC;IACzC;QACE,IAAI,EAAE,0BAA0B;QAChC,QAAQ,EAAE,MAAM;QAChB,GAAG,EAAE,WAAW;QAChB,IAAI,EAAE,+JAA+J;KACtK;IACD;QACE,IAAI,EAAE,sBAAsB;QAC5B,QAAQ,EAAE,MAAM;QAChB,GAAG,EAAE,WAAW;QAChB,IAAI,EAAE,8HAA8H;KACrI;IACD,8EAA8E;IAC9E,wEAAwE;IACxE,uEAAuE;IACvE;QACE,IAAI,EAAE,iBAAiB;QACvB,QAAQ,EAAE,MAAM;QAChB,GAAG,EAAE,WAAW;QAChB,IAAI,EAAE,0HAA0H;KACjI;IACD;QACE,4EAA4E;QAC5E,0EAA0E;QAC1E,2EAA2E;QAC3E,yEAAyE;QACzE,4EAA4E;QAC5E,sCAAsC;QACtC,IAAI,EAAE,oBAAoB;QAC1B,QAAQ,EAAE,MAAM;QAChB,GAAG,EAAE,WAAW;QAChB,IAAI,EAAE,kQAAkQ;KACzQ;IACD;QACE,IAAI,EAAE,iBAAiB;QACvB,QAAQ,EAAE,OAAO;QACjB,GAAG,EAAE,WAAW;QAChB,IAAI,EAAE,kKAAkK;KACzK;IACD;QACE,IAAI,EAAE,gBAAgB;QACtB,QAAQ,EAAE,MAAM;QAChB,GAAG,EAAE,WAAW;QAChB,IAAI,EAAE,+EAA+E;KACtF;IACD;QACE,IAAI,EAAE,kBAAkB;QACxB,QAAQ,EAAE,MAAM;QAChB,GAAG,EAAE,WAAW;QAChB,IAAI,EAAE,gJAAgJ;KACvJ;IACD,2EAA2E;IAC3E,2EAA2E;IAC3E,uEAAuE;IACvE,sBAAsB;IACtB;QACE,IAAI,EAAE,uBAAuB;QAC7B,QAAQ,EAAE,OAAO;QACjB,GAAG,EAAE,WAAW;QAChB,IAAI,EAAE,mKAAmK;KAC1K;IACD;QACE,IAAI,EAAE,sBAAsB;QAC5B,QAAQ,EAAE,MAAM;QAChB,GAAG,EAAE,WAAW;QAChB,IAAI,EAAE,sIAAsI;KAC7I;IACD,2EAA2E;IAC3E,iEAAiE;IACjE,uEAAuE;IACvE,2EAA2E;IAC3E,sCAAsC;IACtC;QACE,IAAI,EAAE,4BAA4B;QAClC,QAAQ,EAAE,MAAM;QAChB,GAAG,EAAE,WAAW;QAChB,IAAI,EAAE,sJAAsJ;KAC7J;IACD;QACE,IAAI,EAAE,oBAAoB;QAC1B,QAAQ,EAAE,OAAO;QACjB,GAAG,EAAE,WAAW;QAChB,IAAI,EAAE,6HAA6H;KACpI;IACD;QACE,IAAI,EAAE,uBAAuB;QAC7B,QAAQ,EAAE,MAAM;QAChB,GAAG,EAAE,WAAW;QAChB,IAAI,EAAE,sEAAsE;KAC7E;IACD;QACE,IAAI,EAAE,qBAAqB;QAC3B,QAAQ,EAAE,MAAM;QAChB,GAAG,EAAE,WAAW;QAChB,IAAI,EAAE,uIAAuI;KAC9I;IACD;QACE,IAAI,EAAE,gBAAgB;QACtB,QAAQ,EAAE,MAAM;QAChB,GAAG,EAAE,WAAW;QAChB,IAAI,EAAE,2IAA2I;KAClJ;IACD;QACE,IAAI,EAAE,gBAAgB;QACtB,QAAQ,EAAE,MAAM;QAChB,GAAG,EAAE,WAAW;QAChB,IAAI,EAAE,iMAAiM;KACxM;IACD;QACE,IAAI,EAAE,kBAAkB;QACxB,QAAQ,EAAE,MAAM;QAChB,GAAG,EAAE,WAAW;QAChB,IAAI,EAAE,+JAA+J;KACtK;IACD;QACE,IAAI,EAAE,8BAA8B;QACpC,QAAQ,EAAE,MAAM;QAChB,GAAG,EAAE,WAAW;QAChB,IAAI,EAAE,oSAAoS;KAC3S;IACD;QACE,IAAI,EAAE,uBAAuB;QAC7B,QAAQ,EAAE,OAAO;QACjB,GAAG,EAAE,WAAW;QAChB,IAAI,EAAE,uJAAuJ;KAC9J;IACD;QACE,IAAI,EAAE,4BAA4B;QAClC,QAAQ,EAAE,OAAO;QACjB,GAAG,EAAE,WAAW;QAChB,IAAI,EAAE,2EAA2E;KAClF;IACD;QACE,IAAI,EAAE,4BAA4B;QAClC,QAAQ,EAAE,OAAO;QACjB,GAAG,EAAE,WAAW;QAChB,IAAI,EAAE,iFAAiF;KACxF;IACD,EAAE,IAAI,EAAE,2BAA2B,EAAE,QAAQ,EAAE,MAAM,EAAE,GAAG,EAAE,WAAW,EAAE,IAAI,EAAE,mCAAmC,EAAE;IACpH,EAAE,IAAI,EAAE,gBAAgB,EAAE,QAAQ,EAAE,MAAM,EAAE,GAAG,EAAE,WAAW,EAAE,IAAI,EAAE,4BAA4B,EAAE;IAClG;QACE,IAAI,EAAE,sBAAsB;QAC5B,QAAQ,EAAE,OAAO;QACjB,GAAG,EAAE,WAAW;QAChB,IAAI,EAAE,+HAA+H;KACtI;IACD;QACE,IAAI,EAAE,mBAAmB;QACzB,QAAQ,EAAE,MAAM;QAChB,GAAG,EAAE,WAAW;QAChB,IAAI,EAAE,0HAA0H;KACjI;IACD;QACE,IAAI,EAAE,kBAAkB;QACxB,QAAQ,EAAE,MAAM;QAChB,GAAG,EAAE,WAAW;QAChB,IAAI,EAAE,6WAA6W;KACpX;IACD,4EAA4E;IAC5E,8EAA8E;IAC9E,+EAA+E;IAC/E,2EAA2E;IAC3E,uEAAuE;IACvE,kEAAkE;IAClE;QACE,IAAI,EAAE,8BAA8B;QACpC,QAAQ,EAAE,OAAO;QACjB,GAAG,EAAE,OAAO;QACZ,IAAI,EAAE,0LAA0L;KACjM;IACD;QACE,IAAI,EAAE,mBAAmB;QACzB,QAAQ,EAAE,OAAO;QACjB,GAAG,EAAE,OAAO;QACZ,IAAI,EAAE,yNAAyN;KAChO;IACD;QACE,IAAI,EAAE,mBAAmB;QACzB,QAAQ,EAAE,OAAO;QACjB,GAAG,EAAE,OAAO;QACZ,IAAI,EAAE,kJAAkJ;KACzJ;IACD;QACE,IAAI,EAAE,2BAA2B;QACjC,QAAQ,EAAE,OAAO;QACjB,GAAG,EAAE,OAAO;QACZ,IAAI,EAAE,yIAAyI;KAChJ;IACD;QACE,IAAI,EAAE,6BAA6B;QACnC,QAAQ,EAAE,MAAM;QAChB,GAAG,EAAE,OAAO;QACZ,IAAI,EAAE,6LAA6L;KACpM;CACF,CAAC","sourcesContent":["/**\n * The checks half of the adoption brief: every check `loam validate` will run\n * against a baseline, each naming the invocation that surfaces it. Its\n * counterpart — the UNCHECKED list, the checks that do not exist — lives in\n * unchecked.ts: honesty no validator will ever supply, and an agent that does\n * not know where the checking stops will assume it never does.\n */\n\n/* ------------------------------------------------------------------ */\n/* The checks */\n/* ------------------------------------------------------------------ */\n\n/**\n * The two invocations a baseline meets. `<id>` is a placeholder for the service\n * id — the JSON contract keeps it symbolic, the text renderer substitutes it.\n */\nconst VIA_SERVICE = \"loam validate --service <id>\";\nexport const VIA_ALL = \"loam validate --all\";\n\nexport interface BriefCheck {\n code: string;\n severity: \"error\" | \"warn\";\n /**\n * The invocation that surfaces this check. Most run under `--service`, but\n * the fleet cross-check does not — attributing it there taught agents to\n * expect a finding that run never reports.\n */\n via: typeof VIA_SERVICE | typeof VIA_ALL;\n what: string;\n}\n\n/** What `loam validate` will run against the result — each check names the invocation that surfaces it. */\nexport const VALIDATE_CHECKS: BriefCheck[] = [\n { code: \"service.no-model\", severity: \"error\", via: VIA_SERVICE, what: \"there is no model.likec4 — every other check stops here\" },\n { code: \"c4.invalid\", severity: \"error\", via: VIA_SERVICE, what: \"model.likec4 does not parse as LikeC4\" },\n {\n code: \"requirements.missing-scenarios\",\n severity: \"error\",\n via: VIA_SERVICE,\n what: \"a requirement in spec.md or arch.spec.md has no `#### Scenario:`\",\n },\n {\n code: \"spec.duplicate-requirement\",\n severity: \"error\",\n via: VIA_SERVICE,\n what: \"one `### Requirement:` name defined twice in one living spec.md or arch.spec.md — a later merge edits only the first, the rest live on as stale copies\",\n },\n {\n code: \"spec.requirement-id-invalid\",\n severity: \"error\",\n via: VIA_SERVICE,\n what: \"an optional `Requirement-ID:` violates `[A-Za-z][A-Za-z0-9._-]{0,127}` — stable identities must be portable and unambiguous\",\n },\n {\n code: \"spec.requirement-id-repeated\",\n severity: \"error\",\n via: VIA_SERVICE,\n what: \"one requirement declares `Requirement-ID:` more than once — keep exactly one identity line\",\n },\n {\n code: \"spec.requirement-id-duplicate\",\n severity: \"error\",\n via: VIA_SERVICE,\n what: \"one stable Requirement-ID identifies multiple requirements in one spec.md or arch.spec.md\",\n },\n // The keep-last pair: a second `Operations:` / `Covers:` line in one\n // requirement body REPLACES the first — assignment, not append — so the\n // author's two-line list silently loses its first line. The semantics stay;\n // the loss no longer happens in silence.\n {\n code: \"spec.repeated-operations\",\n severity: \"warn\",\n via: VIA_SERVICE,\n what: \"a second `Operations:` line in one requirement body — the last replaces the earlier ones, whose list is silently dropped; merge into one comma-separated line\",\n },\n {\n code: \"spec.repeated-covers\",\n severity: \"warn\",\n via: VIA_SERVICE,\n what: \"a second `Covers:` line in one requirement body — same keep-last rule, same silent loss; merge into one comma-separated line\",\n },\n // The graded absences: the brief marks spec.md and openapi.yaml required, and\n // validate agrees they belong — as warns, because partial adoption is a\n // supported state and these are its honest progress meter, not a gate.\n {\n code: \"service.no-spec\",\n severity: \"warn\",\n via: VIA_SERVICE,\n what: \"spec.md does not exist yet — legal mid-adoption, but requirement coverage and API governance are unchecked until it does\",\n },\n {\n // `warn` is the floor, not the whole story, and the difference matters to a\n // baseline: the absence is graded an ERROR once something already written\n // down joins into the file — a living requirement's `Operations:` line, or\n // an op-linked landscape edge (`landscape.expects`, landscape.ts). For a\n // service the fleet already calls, `required: true` on this target and this\n // row are the same fact stated twice.\n code: \"service.no-openapi\",\n severity: \"warn\",\n via: VIA_SERVICE,\n what: \"openapi.yaml does not exist yet — quiet only when the landscape proves no other service calls an operation on this one, and an ERROR rather than a warning once a living `Operations:` line or a landscape edge already names an operation it would have defined\",\n },\n {\n code: \"openapi.invalid\",\n severity: \"error\",\n via: VIA_SERVICE,\n what: \"openapi.yaml exists but does not parse — an unreadable contract proves nothing, so the `api.*` checks and the spine's op resolution are suspended until it reads\",\n },\n {\n code: \"api.ungoverned\",\n severity: \"warn\",\n via: VIA_SERVICE,\n what: \"an operationId in openapi.yaml that no requirement's `Operations:` line names\",\n },\n {\n code: \"api.ops-unlinked\",\n severity: \"warn\",\n via: VIA_SERVICE,\n what: \"openapi.yaml defines operations and spec.md has requirements, but no `Operations:` line joins them — every cross-axis check is vacuously green\",\n },\n // The `Operations:` spine, checked against the LIVING contract. It used to\n // fire only inside a feature delta, so a baseline could ship a requirement\n // governing an operation nobody defines and stay green until the first\n // feature touched it.\n {\n code: \"spec-api.op-undefined\",\n severity: \"error\",\n via: VIA_SERVICE,\n what: \"a living requirement's `Operations:` line names an operationId this service's openapi.yaml does not define — the two axes disagree about what the service exposes\",\n },\n {\n code: \"spec.no-requirements\",\n severity: \"warn\",\n via: VIA_SERVICE,\n what: \"spec.md exists but holds no `### Requirement:` block at all — every requirement-driven check below it is vacuous rather than passing\",\n },\n // The deprecation pair fires on a fresh baseline too — a legacy service is\n // exactly where ops marked `deprecated: true` coexist with their\n // replacements, and documenting that lifecycle honestly is part of the\n // adoption. Mark the flag where the code retires an op; loam reads it, and\n // has no removal semantics beyond it.\n {\n code: \"api.requirement-deprecated\",\n severity: \"warn\",\n via: VIA_SERVICE,\n what: \"a requirement whose `Operations:` list resolves only to operations openapi.yaml marks `deprecated: true` — the behaviour it governs is being retired\",\n },\n {\n code: \"spine.op-undefined\",\n severity: \"error\",\n via: VIA_SERVICE,\n what: \"a landscape edge into this service calls an operation its openapi.yaml does not define — a broken contract between services\",\n },\n {\n code: \"spine.op-link-missing\",\n severity: \"warn\",\n via: VIA_SERVICE,\n what: 'a landscape \"Calls\" edge into this service with no `metadata { op }`',\n },\n {\n code: \"spine.op-deprecated\",\n severity: \"warn\",\n via: VIA_SERVICE,\n what: \"a landscape edge into this service calls an operation openapi.yaml marks `deprecated: true` — the consumer should be migrating off it\",\n },\n {\n code: \"covers.unknown\",\n severity: \"warn\",\n via: VIA_SERVICE,\n what: \"a `Covers:` entry in arch.spec.md that resolves to no element, edge, alert or SLI — a typo silently costs the coverage it was written for\",\n },\n {\n code: \"health.invalid\",\n severity: \"warn\",\n via: VIA_SERVICE,\n what: \"health.yaml exists but does not parse — its alert/SLI ids are unreadable, so `Covers: alert:/sli:` entries and health coverage go unchecked until it reads (a missing health.yaml stays silent)\",\n },\n {\n code: \"health.uncovered\",\n severity: \"warn\",\n via: VIA_SERVICE,\n what: \"an alert or SLI declared in health.yaml that no arch.spec.md requirement covers — expected until the arch spec is written; each one is a signal nothing tests\",\n },\n {\n code: \"health.dependency-unmodelled\",\n severity: \"warn\",\n via: VIA_SERVICE,\n what: \"a health.yaml `dependencies:` id that nothing in this service's OWN model.likec4 answers to (element id, `metadata { service }` binding, or title) — the model carries everything the service touches, private stores included as nested containers, and the on-call file must name the same world\",\n },\n {\n code: \"frontmatter.malformed\",\n severity: \"error\",\n via: VIA_SERVICE,\n what: \"the frontmatter block does not parse as YAML — owner, status and sources are unreadable, and the field checks are suspended until the header is fixed\",\n },\n {\n code: \"frontmatter.field-mismatch\",\n severity: \"error\",\n via: VIA_SERVICE,\n what: \"spec.md declares a different `service:` than the directory it lives under\",\n },\n {\n code: \"frontmatter.status-unknown\",\n severity: \"error\",\n via: VIA_SERVICE,\n what: \"a status outside `draft` / `verified` — a typo here reads as unverified forever\",\n },\n { code: \"frontmatter.field-missing\", severity: \"warn\", via: VIA_SERVICE, what: \"no `owner`, `status` or `service`\" },\n { code: \"sources.absent\", severity: \"warn\", via: VIA_SERVICE, what: \"spec.md names no `sources`\" },\n {\n code: \"sources.path-missing\",\n severity: \"error\",\n via: VIA_SERVICE,\n what: \"a listed source does not exist, or is a glob pattern — no longer supported (checked when loam runs inside the service's repo)\",\n },\n {\n code: \"sources.unvouched\",\n severity: \"warn\",\n via: VIA_SERVICE,\n what: \"`sources` with no digest — nobody has vouched for the document yet. Expected on a fresh baseline; only a human closes it\",\n },\n {\n code: \"sources.unwalked\",\n severity: \"warn\",\n via: VIA_SERVICE,\n what: \"`sources` leaves whole top-level paths of the service repo untouched — the finding names them (checked when loam runs inside the service's repo, against the files git tracks). It grades the WALK, not the writing: the only completeness signal loam can compute, because it is the only one that compares the document against the repository rather than against itself\",\n },\n // sources.stale, sources.current and content.stale are deliberately absent:\n // all three compare against digests `loam vouch` stamps, and a fresh baseline\n // has no stamp to compare — the table lists what a baseline can actually meet.\n // The gherkin.* staleness chain is absent the same way: it fires only once\n // `loam gherkin` has generated a suite under <gherkinDir>/loam/ in the\n // service's own repo, and a fresh baseline has not generated one.\n {\n code: \"landscape.service-unmodelled\",\n severity: \"error\",\n via: VIA_ALL,\n what: \"nothing in architecture/landscape.likec4 resolves to services/<id>/ — the fleet map is incomplete until an element exists or an existing one is bound with `metadata { service '<id>' }`\",\n },\n {\n code: \"landscape.missing\",\n severity: \"error\",\n via: VIA_ALL,\n what: \"architecture/landscape.likec4 does not exist at all — an error as soon as services/ holds one service (a warning only in an empty docs repo): with no fleet map, every cross-service check is blind rather than passing\",\n },\n {\n code: \"landscape.invalid\",\n severity: \"error\",\n via: VIA_ALL,\n what: \"architecture/landscape.likec4 exists but does not parse — fix it before anything else; the whole cross-service layer is unchecked until it reads\",\n },\n {\n code: \"landscape.binding-unknown\",\n severity: \"error\",\n via: VIA_ALL,\n what: \"an element's `metadata { service }` names a directory that does not exist — a binding is a claim about this repo, and this one is false\",\n },\n {\n code: \"landscape.binding-duplicate\",\n severity: \"warn\",\n via: VIA_ALL,\n what: \"two landscape elements resolve to the same services/<id>/ — every element→service join picks one of them arbitrarily, so the other's edges are filed under a service that does not own them\",\n },\n];\n"]}
|
|
1
|
+
{"version":3,"file":"checks.js","sourceRoot":"","sources":["../../../src/core/brief/checks.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AAEH,wEAAwE;AACxE,yEAAyE;AACzE,wEAAwE;AAExE;;;GAGG;AACH,MAAM,WAAW,GAAG,8BAA8B,CAAC;AACnD,MAAM,CAAC,MAAM,OAAO,GAAG,qBAAqB,CAAC;AAc7C,2GAA2G;AAC3G,MAAM,CAAC,MAAM,eAAe,GAAiB;IAC3C,EAAE,IAAI,EAAE,kBAAkB,EAAE,QAAQ,EAAE,OAAO,EAAE,GAAG,EAAE,WAAW,EAAE,IAAI,EAAE,yDAAyD,EAAE;IAClI,EAAE,IAAI,EAAE,YAAY,EAAE,QAAQ,EAAE,OAAO,EAAE,GAAG,EAAE,WAAW,EAAE,IAAI,EAAE,uCAAuC,EAAE;IAC1G;QACE,IAAI,EAAE,gCAAgC;QACtC,QAAQ,EAAE,OAAO;QACjB,GAAG,EAAE,WAAW;QAChB,IAAI,EAAE,kEAAkE;KACzE;IACD;QACE,IAAI,EAAE,4BAA4B;QAClC,QAAQ,EAAE,OAAO;QACjB,GAAG,EAAE,WAAW;QAChB,IAAI,EAAE,wJAAwJ;KAC/J;IACD;QACE,IAAI,EAAE,6BAA6B;QACnC,QAAQ,EAAE,OAAO;QACjB,GAAG,EAAE,WAAW;QAChB,IAAI,EAAE,6HAA6H;KACpI;IACD;QACE,IAAI,EAAE,8BAA8B;QACpC,QAAQ,EAAE,OAAO;QACjB,GAAG,EAAE,WAAW;QAChB,IAAI,EAAE,4FAA4F;KACnG;IACD;QACE,IAAI,EAAE,+BAA+B;QACrC,QAAQ,EAAE,OAAO;QACjB,GAAG,EAAE,WAAW;QAChB,IAAI,EAAE,2FAA2F;KAClG;IACD,qEAAqE;IACrE,wEAAwE;IACxE,4EAA4E;IAC5E,yCAAyC;IACzC;QACE,IAAI,EAAE,0BAA0B;QAChC,QAAQ,EAAE,MAAM;QAChB,GAAG,EAAE,WAAW;QAChB,IAAI,EAAE,+JAA+J;KACtK;IACD;QACE,IAAI,EAAE,sBAAsB;QAC5B,QAAQ,EAAE,MAAM;QAChB,GAAG,EAAE,WAAW;QAChB,IAAI,EAAE,8HAA8H;KACrI;IACD,8EAA8E;IAC9E,wEAAwE;IACxE,uEAAuE;IACvE;QACE,IAAI,EAAE,iBAAiB;QACvB,QAAQ,EAAE,MAAM;QAChB,GAAG,EAAE,WAAW;QAChB,IAAI,EAAE,0HAA0H;KACjI;IACD;QACE,4EAA4E;QAC5E,0EAA0E;QAC1E,2EAA2E;QAC3E,yEAAyE;QACzE,4EAA4E;QAC5E,sCAAsC;QACtC,IAAI,EAAE,oBAAoB;QAC1B,QAAQ,EAAE,MAAM;QAChB,GAAG,EAAE,WAAW;QAChB,IAAI,EAAE,kQAAkQ;KACzQ;IACD;QACE,IAAI,EAAE,iBAAiB;QACvB,QAAQ,EAAE,OAAO;QACjB,GAAG,EAAE,WAAW;QAChB,IAAI,EAAE,kKAAkK;KACzK;IACD;QACE,IAAI,EAAE,gBAAgB;QACtB,QAAQ,EAAE,MAAM;QAChB,GAAG,EAAE,WAAW;QAChB,IAAI,EAAE,+EAA+E;KACtF;IACD;QACE,IAAI,EAAE,kBAAkB;QACxB,QAAQ,EAAE,MAAM;QAChB,GAAG,EAAE,WAAW;QAChB,IAAI,EAAE,gJAAgJ;KACvJ;IACD,2EAA2E;IAC3E,2EAA2E;IAC3E,uEAAuE;IACvE,sBAAsB;IACtB;QACE,IAAI,EAAE,uBAAuB;QAC7B,QAAQ,EAAE,OAAO;QACjB,GAAG,EAAE,WAAW;QAChB,IAAI,EAAE,mKAAmK;KAC1K;IACD;QACE,IAAI,EAAE,sBAAsB;QAC5B,QAAQ,EAAE,MAAM;QAChB,GAAG,EAAE,WAAW;QAChB,IAAI,EAAE,sIAAsI;KAC7I;IACD,2EAA2E;IAC3E,iEAAiE;IACjE,uEAAuE;IACvE,2EAA2E;IAC3E,sCAAsC;IACtC;QACE,IAAI,EAAE,4BAA4B;QAClC,QAAQ,EAAE,MAAM;QAChB,GAAG,EAAE,WAAW;QAChB,IAAI,EAAE,sJAAsJ;KAC7J;IACD;QACE,IAAI,EAAE,oBAAoB;QAC1B,QAAQ,EAAE,OAAO;QACjB,GAAG,EAAE,WAAW;QAChB,IAAI,EAAE,6HAA6H;KACpI;IACD;QACE,IAAI,EAAE,uBAAuB;QAC7B,QAAQ,EAAE,MAAM;QAChB,GAAG,EAAE,WAAW;QAChB,IAAI,EAAE,sEAAsE;KAC7E;IACD;QACE,IAAI,EAAE,qBAAqB;QAC3B,QAAQ,EAAE,MAAM;QAChB,GAAG,EAAE,WAAW;QAChB,IAAI,EAAE,uIAAuI;KAC9I;IACD;QACE,IAAI,EAAE,gBAAgB;QACtB,QAAQ,EAAE,MAAM;QAChB,GAAG,EAAE,WAAW;QAChB,IAAI,EAAE,2IAA2I;KAClJ;IACD;QACE,IAAI,EAAE,gBAAgB;QACtB,QAAQ,EAAE,MAAM;QAChB,GAAG,EAAE,WAAW;QAChB,IAAI,EAAE,iMAAiM;KACxM;IACD;QACE,IAAI,EAAE,kBAAkB;QACxB,QAAQ,EAAE,MAAM;QAChB,GAAG,EAAE,WAAW;QAChB,IAAI,EAAE,+JAA+J;KACtK;IACD;QACE,IAAI,EAAE,8BAA8B;QACpC,QAAQ,EAAE,MAAM;QAChB,GAAG,EAAE,WAAW;QAChB,IAAI,EAAE,oSAAoS;KAC3S;IACD;QACE,IAAI,EAAE,uBAAuB;QAC7B,QAAQ,EAAE,OAAO;QACjB,GAAG,EAAE,WAAW;QAChB,IAAI,EAAE,uJAAuJ;KAC9J;IACD;QACE,IAAI,EAAE,4BAA4B;QAClC,QAAQ,EAAE,OAAO;QACjB,GAAG,EAAE,WAAW;QAChB,IAAI,EAAE,2EAA2E;KAClF;IACD;QACE,IAAI,EAAE,4BAA4B;QAClC,QAAQ,EAAE,OAAO;QACjB,GAAG,EAAE,WAAW;QAChB,IAAI,EAAE,iFAAiF;KACxF;IACD,EAAE,IAAI,EAAE,2BAA2B,EAAE,QAAQ,EAAE,MAAM,EAAE,GAAG,EAAE,WAAW,EAAE,IAAI,EAAE,mCAAmC,EAAE;IACpH,EAAE,IAAI,EAAE,gBAAgB,EAAE,QAAQ,EAAE,MAAM,EAAE,GAAG,EAAE,WAAW,EAAE,IAAI,EAAE,4BAA4B,EAAE;IAClG;QACE,IAAI,EAAE,sBAAsB;QAC5B,QAAQ,EAAE,OAAO;QACjB,GAAG,EAAE,WAAW;QAChB,IAAI,EAAE,+HAA+H;KACtI;IACD;QACE,IAAI,EAAE,mBAAmB;QACzB,QAAQ,EAAE,MAAM;QAChB,GAAG,EAAE,WAAW;QAChB,IAAI,EAAE,0HAA0H;KACjI;IACD;QACE,IAAI,EAAE,kBAAkB;QACxB,QAAQ,EAAE,MAAM;QAChB,GAAG,EAAE,WAAW;QAChB,IAAI,EAAE,6WAA6W;KACpX;IACD,4EAA4E;IAC5E,8EAA8E;IAC9E,+EAA+E;IAC/E,2EAA2E;IAC3E,uEAAuE;IACvE,kEAAkE;IAClE;QACE,IAAI,EAAE,8BAA8B;QACpC,QAAQ,EAAE,OAAO;QACjB,GAAG,EAAE,OAAO;QACZ,IAAI,EAAE,0LAA0L;KACjM;IACD;QACE,IAAI,EAAE,mBAAmB;QACzB,QAAQ,EAAE,OAAO;QACjB,GAAG,EAAE,OAAO;QACZ,IAAI,EAAE,yNAAyN;KAChO;IACD;QACE,IAAI,EAAE,mBAAmB;QACzB,QAAQ,EAAE,OAAO;QACjB,GAAG,EAAE,OAAO;QACZ,IAAI,EAAE,kJAAkJ;KACzJ;IACD;QACE,IAAI,EAAE,2BAA2B;QACjC,QAAQ,EAAE,OAAO;QACjB,GAAG,EAAE,OAAO;QACZ,IAAI,EAAE,yIAAyI;KAChJ;IACD;QACE,IAAI,EAAE,6BAA6B;QACnC,QAAQ,EAAE,MAAM;QAChB,GAAG,EAAE,OAAO;QACZ,IAAI,EAAE,6LAA6L;KACpM;IACD,yEAAyE;IACzE,sEAAsE;IACtE,2EAA2E;IAC3E;QACE,IAAI,EAAE,+BAA+B;QACrC,QAAQ,EAAE,MAAM;QAChB,GAAG,EAAE,OAAO;QACZ,IAAI,EAAE,wNAAwN;KAC/N;CACF,CAAC","sourcesContent":["/**\n * The checks half of the adoption brief: every check `loam validate` will run\n * against a baseline, each naming the invocation that surfaces it. Its\n * counterpart — the UNCHECKED list, the checks that do not exist — lives in\n * unchecked.ts: honesty no validator will ever supply, and an agent that does\n * not know where the checking stops will assume it never does.\n */\n\n/* ------------------------------------------------------------------ */\n/* The checks */\n/* ------------------------------------------------------------------ */\n\n/**\n * The two invocations a baseline meets. `<id>` is a placeholder for the service\n * id — the JSON contract keeps it symbolic, the text renderer substitutes it.\n */\nconst VIA_SERVICE = \"loam validate --service <id>\";\nexport const VIA_ALL = \"loam validate --all\";\n\nexport interface BriefCheck {\n code: string;\n severity: \"error\" | \"warn\";\n /**\n * The invocation that surfaces this check. Most run under `--service`, but\n * the fleet cross-check does not — attributing it there taught agents to\n * expect a finding that run never reports.\n */\n via: typeof VIA_SERVICE | typeof VIA_ALL;\n what: string;\n}\n\n/** What `loam validate` will run against the result — each check names the invocation that surfaces it. */\nexport const VALIDATE_CHECKS: BriefCheck[] = [\n { code: \"service.no-model\", severity: \"error\", via: VIA_SERVICE, what: \"there is no model.likec4 — every other check stops here\" },\n { code: \"c4.invalid\", severity: \"error\", via: VIA_SERVICE, what: \"model.likec4 does not parse as LikeC4\" },\n {\n code: \"requirements.missing-scenarios\",\n severity: \"error\",\n via: VIA_SERVICE,\n what: \"a requirement in spec.md or arch.spec.md has no `#### Scenario:`\",\n },\n {\n code: \"spec.duplicate-requirement\",\n severity: \"error\",\n via: VIA_SERVICE,\n what: \"one `### Requirement:` name defined twice in one living spec.md or arch.spec.md — a later merge edits only the first, the rest live on as stale copies\",\n },\n {\n code: \"spec.requirement-id-invalid\",\n severity: \"error\",\n via: VIA_SERVICE,\n what: \"an optional `Requirement-ID:` violates `[A-Za-z][A-Za-z0-9._-]{0,127}` — stable identities must be portable and unambiguous\",\n },\n {\n code: \"spec.requirement-id-repeated\",\n severity: \"error\",\n via: VIA_SERVICE,\n what: \"one requirement declares `Requirement-ID:` more than once — keep exactly one identity line\",\n },\n {\n code: \"spec.requirement-id-duplicate\",\n severity: \"error\",\n via: VIA_SERVICE,\n what: \"one stable Requirement-ID identifies multiple requirements in one spec.md or arch.spec.md\",\n },\n // The keep-last pair: a second `Operations:` / `Covers:` line in one\n // requirement body REPLACES the first — assignment, not append — so the\n // author's two-line list silently loses its first line. The semantics stay;\n // the loss no longer happens in silence.\n {\n code: \"spec.repeated-operations\",\n severity: \"warn\",\n via: VIA_SERVICE,\n what: \"a second `Operations:` line in one requirement body — the last replaces the earlier ones, whose list is silently dropped; merge into one comma-separated line\",\n },\n {\n code: \"spec.repeated-covers\",\n severity: \"warn\",\n via: VIA_SERVICE,\n what: \"a second `Covers:` line in one requirement body — same keep-last rule, same silent loss; merge into one comma-separated line\",\n },\n // The graded absences: the brief marks spec.md and openapi.yaml required, and\n // validate agrees they belong — as warns, because partial adoption is a\n // supported state and these are its honest progress meter, not a gate.\n {\n code: \"service.no-spec\",\n severity: \"warn\",\n via: VIA_SERVICE,\n what: \"spec.md does not exist yet — legal mid-adoption, but requirement coverage and API governance are unchecked until it does\",\n },\n {\n // `warn` is the floor, not the whole story, and the difference matters to a\n // baseline: the absence is graded an ERROR once something already written\n // down joins into the file — a living requirement's `Operations:` line, or\n // an op-linked landscape edge (`landscape.expects`, landscape.ts). For a\n // service the fleet already calls, `required: true` on this target and this\n // row are the same fact stated twice.\n code: \"service.no-openapi\",\n severity: \"warn\",\n via: VIA_SERVICE,\n what: \"openapi.yaml does not exist yet — quiet only when the landscape proves no other service calls an operation on this one, and an ERROR rather than a warning once a living `Operations:` line or a landscape edge already names an operation it would have defined\",\n },\n {\n code: \"openapi.invalid\",\n severity: \"error\",\n via: VIA_SERVICE,\n what: \"openapi.yaml exists but does not parse — an unreadable contract proves nothing, so the `api.*` checks and the spine's op resolution are suspended until it reads\",\n },\n {\n code: \"api.ungoverned\",\n severity: \"warn\",\n via: VIA_SERVICE,\n what: \"an operationId in openapi.yaml that no requirement's `Operations:` line names\",\n },\n {\n code: \"api.ops-unlinked\",\n severity: \"warn\",\n via: VIA_SERVICE,\n what: \"openapi.yaml defines operations and spec.md has requirements, but no `Operations:` line joins them — every cross-axis check is vacuously green\",\n },\n // The `Operations:` spine, checked against the LIVING contract. It used to\n // fire only inside a feature delta, so a baseline could ship a requirement\n // governing an operation nobody defines and stay green until the first\n // feature touched it.\n {\n code: \"spec-api.op-undefined\",\n severity: \"error\",\n via: VIA_SERVICE,\n what: \"a living requirement's `Operations:` line names an operationId this service's openapi.yaml does not define — the two axes disagree about what the service exposes\",\n },\n {\n code: \"spec.no-requirements\",\n severity: \"warn\",\n via: VIA_SERVICE,\n what: \"spec.md exists but holds no `### Requirement:` block at all — every requirement-driven check below it is vacuous rather than passing\",\n },\n // The deprecation pair fires on a fresh baseline too — a legacy service is\n // exactly where ops marked `deprecated: true` coexist with their\n // replacements, and documenting that lifecycle honestly is part of the\n // adoption. Mark the flag where the code retires an op; loam reads it, and\n // has no removal semantics beyond it.\n {\n code: \"api.requirement-deprecated\",\n severity: \"warn\",\n via: VIA_SERVICE,\n what: \"a requirement whose `Operations:` list resolves only to operations openapi.yaml marks `deprecated: true` — the behaviour it governs is being retired\",\n },\n {\n code: \"spine.op-undefined\",\n severity: \"error\",\n via: VIA_SERVICE,\n what: \"a landscape edge into this service calls an operation its openapi.yaml does not define — a broken contract between services\",\n },\n {\n code: \"spine.op-link-missing\",\n severity: \"warn\",\n via: VIA_SERVICE,\n what: 'a landscape \"Calls\" edge into this service with no `metadata { op }`',\n },\n {\n code: \"spine.op-deprecated\",\n severity: \"warn\",\n via: VIA_SERVICE,\n what: \"a landscape edge into this service calls an operation openapi.yaml marks `deprecated: true` — the consumer should be migrating off it\",\n },\n {\n code: \"covers.unknown\",\n severity: \"warn\",\n via: VIA_SERVICE,\n what: \"a `Covers:` entry in arch.spec.md that resolves to no element, edge, alert or SLI — a typo silently costs the coverage it was written for\",\n },\n {\n code: \"health.invalid\",\n severity: \"warn\",\n via: VIA_SERVICE,\n what: \"health.yaml exists but does not parse — its alert/SLI ids are unreadable, so `Covers: alert:/sli:` entries and health coverage go unchecked until it reads (a missing health.yaml stays silent)\",\n },\n {\n code: \"health.uncovered\",\n severity: \"warn\",\n via: VIA_SERVICE,\n what: \"an alert or SLI declared in health.yaml that no arch.spec.md requirement covers — expected until the arch spec is written; each one is a signal nothing tests\",\n },\n {\n code: \"health.dependency-unmodelled\",\n severity: \"warn\",\n via: VIA_SERVICE,\n what: \"a health.yaml `dependencies:` id that nothing in this service's OWN model.likec4 answers to (element id, `metadata { service }` binding, or title) — the model carries everything the service touches, private stores included as nested containers, and the on-call file must name the same world\",\n },\n {\n code: \"frontmatter.malformed\",\n severity: \"error\",\n via: VIA_SERVICE,\n what: \"the frontmatter block does not parse as YAML — owner, status and sources are unreadable, and the field checks are suspended until the header is fixed\",\n },\n {\n code: \"frontmatter.field-mismatch\",\n severity: \"error\",\n via: VIA_SERVICE,\n what: \"spec.md declares a different `service:` than the directory it lives under\",\n },\n {\n code: \"frontmatter.status-unknown\",\n severity: \"error\",\n via: VIA_SERVICE,\n what: \"a status outside `draft` / `verified` — a typo here reads as unverified forever\",\n },\n { code: \"frontmatter.field-missing\", severity: \"warn\", via: VIA_SERVICE, what: \"no `owner`, `status` or `service`\" },\n { code: \"sources.absent\", severity: \"warn\", via: VIA_SERVICE, what: \"spec.md names no `sources`\" },\n {\n code: \"sources.path-missing\",\n severity: \"error\",\n via: VIA_SERVICE,\n what: \"a listed source does not exist, or is a glob pattern — no longer supported (checked when loam runs inside the service's repo)\",\n },\n {\n code: \"sources.unvouched\",\n severity: \"warn\",\n via: VIA_SERVICE,\n what: \"`sources` with no digest — nobody has vouched for the document yet. Expected on a fresh baseline; only a human closes it\",\n },\n {\n code: \"sources.unwalked\",\n severity: \"warn\",\n via: VIA_SERVICE,\n what: \"`sources` leaves whole top-level paths of the service repo untouched — the finding names them (checked when loam runs inside the service's repo, against the files git tracks). It grades the WALK, not the writing: the only completeness signal loam can compute, because it is the only one that compares the document against the repository rather than against itself\",\n },\n // sources.stale, sources.current and content.stale are deliberately absent:\n // all three compare against digests `loam vouch` stamps, and a fresh baseline\n // has no stamp to compare — the table lists what a baseline can actually meet.\n // The gherkin.* staleness chain is absent the same way: it fires only once\n // `loam gherkin` has generated a suite under <gherkinDir>/loam/ in the\n // service's own repo, and a fresh baseline has not generated one.\n {\n code: \"landscape.service-unmodelled\",\n severity: \"error\",\n via: VIA_ALL,\n what: \"nothing in architecture/landscape.likec4 resolves to services/<id>/ — the fleet map is incomplete until an element exists or an existing one is bound with `metadata { service '<id>' }`\",\n },\n {\n code: \"landscape.missing\",\n severity: \"error\",\n via: VIA_ALL,\n what: \"architecture/landscape.likec4 does not exist at all — an error as soon as services/ holds one service (a warning only in an empty docs repo): with no fleet map, every cross-service check is blind rather than passing\",\n },\n {\n code: \"landscape.invalid\",\n severity: \"error\",\n via: VIA_ALL,\n what: \"architecture/landscape.likec4 exists but does not parse — fix it before anything else; the whole cross-service layer is unchecked until it reads\",\n },\n {\n code: \"landscape.binding-unknown\",\n severity: \"error\",\n via: VIA_ALL,\n what: \"an element's `metadata { service }` names a directory that does not exist — a binding is a claim about this repo, and this one is false\",\n },\n {\n code: \"landscape.binding-duplicate\",\n severity: \"warn\",\n via: VIA_ALL,\n what: \"two landscape elements resolve to the same services/<id>/ — every element→service join picks one of them arbitrarily, so the other's edges are filed under a service that does not own them\",\n },\n // Fires on a fresh baseline by construction: the adopt protocol's step 3\n // writes the model and `subsystem sync` writes the project file, so a\n // service is owed the second command from the moment the first file lands.\n {\n code: \"service.likec4-config-missing\",\n severity: \"warn\",\n via: VIA_ALL,\n what: \"model.likec4 exists and services/<id>/likec4.config.json does not — the renderer's per-service project file, which `loam subsystem sync` writes; without it the model is a box on the fleet map with nothing inside it\",\n },\n];\n"]}
|
|
@@ -45,10 +45,13 @@ model {
|
|
|
45
45
|
// and it never reads a STATIC view's contents in any document, so nothing below
|
|
46
46
|
// is read by any check (docs/DESIGN.md rule 26 — the one thing loam does read is
|
|
47
47
|
// a dynamic view's declared steps, and only in the fleet landscape).
|
|
48
|
-
// Keep it for the renderer, which does draw it — 'npx likec4 start
|
|
49
|
-
//
|
|
50
|
-
// '
|
|
51
|
-
//
|
|
48
|
+
// Keep it for the renderer, which does draw it — 'npx likec4 start' from the docs
|
|
49
|
+
// repo ROOT, once 'loam subsystem sync' has written services/<id>/likec4.config.json:
|
|
50
|
+
// that file is loam's, never yours, and it is what registers this model as a
|
|
51
|
+
// LikeC4 project of its own beside the fleet map. The root project stays scoped
|
|
52
|
+
// to architecture/ (the root likec4.config.json) because every model declares
|
|
53
|
+
// its own 'specification' block, and a root that merged them would read each
|
|
54
|
+
// declaration as a duplicate.
|
|
52
55
|
// Scoped to one service 'include *' is cheap; the same line in
|
|
53
56
|
// architecture/landscape.likec4 walks every call in the fleet and takes minutes.
|
|
54
57
|
views {
|