@ybotok/loam 0.2.0-alpha.6 → 0.2.0-alpha.7

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.
Files changed (105) hide show
  1. package/CHANGELOG.md +162 -0
  2. package/README.md +4 -4
  3. package/ROADMAP.md +27 -6
  4. package/SCHEMA.md +152 -32
  5. package/dist/commands/archive/run.js +49 -22
  6. package/dist/commands/archive/run.js.map +1 -1
  7. package/dist/commands/seed/commit.js +34 -1
  8. package/dist/commands/seed/commit.js.map +1 -1
  9. package/dist/commands/seed/plan.js +64 -13
  10. package/dist/commands/seed/plan.js.map +1 -1
  11. package/dist/commands/subsystem/edit.js +4 -2
  12. package/dist/commands/subsystem/edit.js.map +1 -1
  13. package/dist/commands/subsystem/move.js +55 -17
  14. package/dist/commands/subsystem/move.js.map +1 -1
  15. package/dist/commands/subsystem/sync.js +142 -45
  16. package/dist/commands/subsystem/sync.js.map +1 -1
  17. package/dist/commands/subsystem/txn/report.js +55 -5
  18. package/dist/commands/subsystem/txn/report.js.map +1 -1
  19. package/dist/commands/subsystem/txn/txn.js +118 -21
  20. package/dist/commands/subsystem/txn/txn.js.map +1 -1
  21. package/dist/commands/subsystem/txn/views.js +51 -8
  22. package/dist/commands/subsystem/txn/views.js.map +1 -1
  23. package/dist/commands/validate/checks/health-deps.js +9 -6
  24. package/dist/commands/validate/checks/health-deps.js.map +1 -1
  25. package/dist/commands/validate/fleet/census.js +26 -3
  26. package/dist/commands/validate/fleet/census.js.map +1 -1
  27. package/dist/commands/validate/fleet/landscape.js +12 -9
  28. package/dist/commands/validate/fleet/landscape.js.map +1 -1
  29. package/dist/commands/validate/fleet/map/attest.js +4 -5
  30. package/dist/commands/validate/fleet/map/attest.js.map +1 -1
  31. package/dist/commands/validate/fleet/map/bindings.js +15 -2
  32. package/dist/commands/validate/fleet/map/bindings.js.map +1 -1
  33. package/dist/commands/validate/fleet/map/consumers.js +43 -122
  34. package/dist/commands/validate/fleet/map/consumers.js.map +1 -1
  35. package/dist/commands/validate/fleet/scorecard/scorecard.js.map +1 -1
  36. package/dist/commands/validate/fleet/views/fleet-project.js +1 -1
  37. package/dist/commands/validate/fleet/views/fleet-project.js.map +1 -1
  38. package/dist/commands/validate/fleet/views/ids.js +33 -7
  39. package/dist/commands/validate/fleet/views/ids.js.map +1 -1
  40. package/dist/commands/validate/fleet/views/root-documents.js +9 -7
  41. package/dist/commands/validate/fleet/views/root-documents.js.map +1 -1
  42. package/dist/commands/validate/fleet/views/stale.js +31 -14
  43. package/dist/commands/validate/fleet/views/stale.js.map +1 -1
  44. package/dist/core/agent/agents-md/command-map.js +1 -1
  45. package/dist/core/agent/agents-md/command-map.js.map +1 -1
  46. package/dist/core/agent/agents-md/map/subsystem.js +27 -14
  47. package/dist/core/agent/agents-md/map/subsystem.js.map +1 -1
  48. package/dist/core/agent/agents-md/refusals.js +5 -3
  49. package/dist/core/agent/agents-md/refusals.js.map +1 -1
  50. package/dist/core/agent/agents-md.js +16 -7
  51. package/dist/core/agent/agents-md.js.map +1 -1
  52. package/dist/core/agent/workflows/check.js +5 -6
  53. package/dist/core/agent/workflows/check.js.map +1 -1
  54. package/dist/core/agent/workflows/reference/spine.js +12 -6
  55. package/dist/core/agent/workflows/reference/spine.js.map +1 -1
  56. package/dist/core/brief/brief.js +25 -8
  57. package/dist/core/brief/brief.js.map +1 -1
  58. package/dist/core/brief/targets.js +64 -0
  59. package/dist/core/brief/targets.js.map +1 -1
  60. package/dist/core/brief/unchecked.js +1 -1
  61. package/dist/core/brief/unchecked.js.map +1 -1
  62. package/dist/core/c4/project/architecture.js +31 -9
  63. package/dist/core/c4/project/architecture.js.map +1 -1
  64. package/dist/core/c4/project/documents.js +5 -4
  65. package/dist/core/c4/project/documents.js.map +1 -1
  66. package/dist/core/c4/project/load.js +8 -4
  67. package/dist/core/c4/project/load.js.map +1 -1
  68. package/dist/core/c4/project/staged.js +7 -3
  69. package/dist/core/c4/project/staged.js.map +1 -1
  70. package/dist/core/deployment/delta.js +12 -10
  71. package/dist/core/deployment/delta.js.map +1 -1
  72. package/dist/core/gate/checks.js +5 -1
  73. package/dist/core/gate/checks.js.map +1 -1
  74. package/dist/core/gate/partners.js +60 -17
  75. package/dist/core/gate/partners.js.map +1 -1
  76. package/dist/core/repo/paths.js +34 -29
  77. package/dist/core/repo/paths.js.map +1 -1
  78. package/dist/core/repo/tree/render/body.js +45 -6
  79. package/dist/core/repo/tree/render/body.js.map +1 -1
  80. package/dist/core/repo/tree/render/service/header.js +125 -0
  81. package/dist/core/repo/tree/render/service/header.js.map +1 -0
  82. package/dist/core/repo/tree/render/service/peers.js +28 -0
  83. package/dist/core/repo/tree/render/service/peers.js.map +1 -0
  84. package/dist/core/repo/tree/render/service/view-id.js +67 -0
  85. package/dist/core/repo/tree/render/service/view-id.js.map +1 -0
  86. package/dist/core/repo/tree/render/service/views.js +78 -0
  87. package/dist/core/repo/tree/render/service/views.js.map +1 -0
  88. package/dist/core/repo/tree/render/stale.js +19 -2
  89. package/dist/core/repo/tree/render/stale.js.map +1 -1
  90. package/dist/core/repo/tree/render/views.js +102 -33
  91. package/dist/core/repo/tree/render/views.js.map +1 -1
  92. package/dist/core/verify/checklist.js +42 -10
  93. package/dist/core/verify/checklist.js.map +1 -1
  94. package/dist/core/vocabulary/issue.js.map +1 -1
  95. package/dist/core/vocabulary/maturity.js +41 -1
  96. package/dist/core/vocabulary/maturity.js.map +1 -1
  97. package/examples/README.md +7 -3
  98. package/examples/docs/architecture/landscape.likec4 +13 -7
  99. package/examples/docs/architecture/services.likec4 +70 -0
  100. package/examples/docs/architecture/subsystems.likec4 +3 -0
  101. package/examples/docs/services/order-service/model.likec4 +10 -8
  102. package/examples/docs/services/payment-service/model.likec4 +6 -4
  103. package/examples/docs/services/platform/identity-service/model.likec4 +9 -7
  104. package/examples/docs/services/platform/notification-service/model.likec4 +4 -5
  105. package/package.json +1 -1
@@ -1 +1 @@
1
- {"version":3,"file":"spine.js","sourceRoot":"","sources":["../../../../../src/core/agent/workflows/reference/spine.ts"],"names":[],"mappings":"AAqBA,MAAM,CAAC,MAAM,UAAU,GAAmB;IACxC,IAAI,EAAE,YAAY;IAClB,WAAW,EAAE,mFAAmF;IAChG,0EAA0E;IAC1E,4EAA4E;IAC5E,yEAAyE;IACzE,YAAY,EAAE,EAAE;IAChB,OAAO,EACL,qNAAqN;IACvN,UAAU,EAAE,8BAA8B;IAC1C,YAAY,EAAE,EAAE;IAChB,KAAK,EAAE;QACL,cAAc;QACd,wCAAwC;QACxC,yBAAyB;QACzB,sCAAsC;KACvC;IACD,IAAI,EAAE;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;CAsRP;CACA,CAAC","sourcesContent":["/**\n * The `loam-spine` reference page: how the artifacts JOIN.\n *\n * Operation and message ids, coverage and permission references, the\n * capability tree and its `Realizes:` join, the requirement baseline pins\n * (`Based-On:` / `x-loam-based-on`), how to draw a shared broker, and how to\n * declare a message produced outside the fleet.\n *\n * This is the grammar a reader consults while writing ONE document, which is\n * why it is a printed page rather than a section of AGENTS.md: it is consulted\n * at a moment you can name, and paying for it on every invocation of every\n * session bought nothing. `## Which element IS which service` deliberately\n * stayed behind in ../../agents-md/spine.ts — an agent cannot read the\n * landscape at all without it, so it is orientation rather than reference.\n *\n * The text is the AGENTS.md section verbatim; nothing was rewritten to move\n * it, so a cross-reference from the remaining file still names the same\n * headings. See ./reference.ts for why these four pages exist.\n */\nimport type { CommandContent } from \"../../contract.js\";\n\nexport const LOAM_SPINE: CommandContent = {\n name: \"loam-spine\",\n description: \"Reference: the ID spine — every join between the artifacts, and the baseline pins\",\n // A reference page takes no arguments: it is one document, printed whole.\n // The empty hint is what `loam instructions` prints beside the name, and an\n // invented one (\"<none>\") would read as an argument somebody could pass.\n argumentHint: \"\",\n purpose:\n \"The exact joins the artifacts are held together by — operationId, message name, `Covers:`, `Requires:`, `Capability:`/`Realizes:` — and the `Based-On:` pins that stop two features in flight reverting each other.\",\n invocation: \"loam instructions loam-spine\",\n placeholders: [],\n spine: [\n \"The ID spine\",\n \"The requirement baseline — `Based-On:`\",\n \"Drawing a shared broker\",\n \"A message produced outside the fleet\",\n ],\n body: `The ID spine: how the artifacts join, and what pins a change to the version it was written against.\n\n## The ID spine\n\nThe spine is a family of exact joins, not one magic id: feature tag; service\nbinding; synchronous operation; asynchronous message; architecture coverage;\nauthorization permission. Three artifacts describe the same synchronous call in\nthree languages. They are joined by the **operationId**, and that join is what\n\\`loam validate\\` checks:\n\n- architecture — a C4 edge names the operation it calls:\n \\`checkoutWeb -> paymentService 'Calls authorizePayment' { metadata { op 'authorizePayment' } }\\`\n- behaviour — a requirement declares the operations it governs:\n \\`Operations: authorizePayment\\`\n- contract — the provider's \\`openapi.yaml\\` defines it:\n \\`operationId: authorizePayment\\`\n\nSpell it identically in all three places. A mismatch is an error, not a warning:\nan edge calling an operation nobody defines is a broken contract between services.\n\nFeatures are joined the same way: a delta's new elements and edges carry the\nfeature id as a LikeC4 tag (\\`#FEAT-101\\`), and that tag is exactly what\n\\`loam archive\\` folds into the living landscape. Untagged elements in a delta are\ncontext for the diagram, not changes.\n\nThe other joins use the same discipline:\n\n- \\`metadata { publishes|consumes 'message' }\\` on an edge ↔\n \\`Publishes:\\`/\\`Consumes:\\` in a requirement ↔ the AsyncAPI 3 message name;\n- \\`Covers:\\` in an architecture requirement ↔ a C4 element/edge or\n \\`alert:<id>\\` / \\`sli:<id>\\` from health.yaml;\n- \\`Requires: <subject>/<permission>\\` in either requirement file ↔ the fleet\n declaration in \\`architecture/permissions.yaml\\`;\n- \\`Capability:\\` in either requirement file ↔ a declared id in\n \\`architecture/capabilities.yaml\\`. A LIST, comma-separated — the relation is\n many-to-many in both directions.\n\nThe permissions file is opt-in: a fleet with no \\`Requires:\\` line owes none.\nOnce a requirement names a permission, an undeclared pair is\n\\`permissions.unknown\\` (error). An unreadable vocabulary is\n\\`permissions.invalid\\`; a declaration nothing requires is\n\\`permissions.unenforced\\` (warn). The subject is what the permission is checked\nON (user, profile, service), not necessarily the caller. \\`owned_by\\` and\n\\`enforced_by\\` are explanatory and are not resolved against services; this is\na checked document join, not proof that the identity provider implements it.\n\nThe capability axis opts in the OTHER way: the FILES are the opt-in, not the\nline. A fleet with neither \\`architecture/capabilities.yaml\\` nor a\n\\`capabilities/\\` directory gets no capability findings at all, however many\n\\`Capability:\\` lines its requirements already carry.\n\nA capability may be declared on either side, and the vocabulary is the UNION.\nA name alone is a line in \\`architecture/capabilities.yaml\\` —\n\\`capabilities: {<id>: {description, owner}}\\`, nested ids such as\n\\`payments/refunds\\` kept as one flat key. A name with prose behind it gets a\ndocument instead: \\`capabilities/<id>/spec.md\\`, the AUTHORED business tree,\none directory per capability with nesting spelled by the tree\n(\\`capabilities/payments/refunds/spec.md\\`). The directory is the list — there\nis no manifest — and its existence is what opts the fleet in, so \\`loam init\\`\ndoes not scaffold it. A directory holding neither the document nor a\ncapability beneath it is \\`capability.doc-missing\\` (warn).\n\nThe document carries narrative and then \\`## Requirements\\`, in the same\ngrammar every spec.md uses, with two rules of its own. Every requirement needs\na \\`Requirement-ID:\\` — these documents outlive the services that realize them,\nso identity is the line and not the heading; without one it is\n\\`capability.requirement-unidentified\\` (error). And a capability requirement\nmust be observable OUTSIDE the fleet: \\`Operations:\\`, \\`Covers:\\`,\n\\`Publishes:\\` and \\`Consumes:\\` all resolve against one service's own contract,\nso carrying any of them is \\`capability.requirement-service-scoped\\` (error) —\nwrite it in that service's spec.md instead. Naming no service is an authoring\nrule that PR review holds, not one loam checks: matching service names in prose\nwould be a heuristic, and loam does not guess. The axis's own two joins are\nrefused here as well (\\`capability.requirement-inert-join\\`, error): they point\nINTO the tree, so nothing reads them written inside it.\n\nA FEATURE changes a capability document with a delta, not by editing it:\n\\`features/<FEAT>/capabilities/<id>/spec.md\\`, the same\n\\`## ADDED|MODIFIED|REMOVED Requirements\\` grammar the service deltas use, the\nsame nesting (\\`features/<FEAT>/capabilities/payments/refunds/\\`), the same\n\\`Based-On:\\` pins, and the same \\`delta.*\\` refusals with the capability id as\nthe subject. \\`scaffold.placeholder\\` reads it too, so an unedited template\nheading refuses the archive here as it does in a spec delta, and\n\\`capability.doc-missing\\` (warn) names a directory in it that holds no\n\\`spec.md\\` — an empty one is not a delta, so the merge would carry nothing.\n\\`loam archive\\` merges it into the living document in the same\ntransaction as the service merge, creating \\`capabilities/<id>/spec.md\\` — and\nso the whole tree — when this is the first feature to mention it; the heading\nis the id and the narrative above \\`## Requirements\\` is yours to write, because\nloam merges requirements and never prose. The document's three rules above are\ngraded on the delta BEFORE the merge, so a capability requirement filed at the\nwrong altitude is refused while you still hold it. Editing the living document\ndirectly is still legal; the delta is what makes two features touching one\npromise collide loudly instead of silently. \\`loam new <FEAT> --capability <id>\\`\nscaffolds that delta, and \\`loam show <FEAT>\\` and \\`loam delta <FEAT>\\` list the\npromises a feature changes — a capability delta names no service, so without\nthose two a business-only feature reads as a feature carrying nothing.\n\nA service requirement says which promise it serves with \\`Realizes:\\`, entries\nspelled \\`<capability-id>#<Requirement-ID>\\`. This is the join the tree exists\nfor, and it is NOT the same claim as \\`Capability:\\` beside it — that names a\ntheme, this names one promise, and a requirement commonly carries both. The\ncapability half of an entry is what makes the target addressable, since a\n\\`Requirement-ID\\` is unique only inside its own document; the separator is the\nLAST \\`#\\`, because the requirement half's grammar excludes one and the\ncapability half's does not. Written by whoever implements the requirement,\nnever by the analyst — a business document must not be edited every time the\nfleet rearranges which service carries which part.\n\nBoth directions are graded. An entry naming no capability requirement is\n\\`capability.realizes-unknown\\` (error; in a feature delta it gates\n\\`loam archive\\`, \\`--approve\\`-overridable), and its message says which of the\nfive failures happened — malformed entry, undeclared capability, declared but\nundocumented, document with no requirements, or an id that document does not\ndeclare. A capability requirement nothing keeps is\n\\`capability.requirement-unrealized\\` (warn, one per requirement): it never\ngates, because writing the business document ahead of the fleet is the intended\nuse. \\`loam list capabilities --json\\` carries the whole join — each capability's\nrequirements, and what realizes each one.\n\n\\`Realizes:\\` is not the only way to keep a promise, and for a criterion that\nCROSSES services it is not enough: \"I enter a login and a password and I am in\"\nbelongs to no single service's spec, because each promises only its own part. A\nUSE CASE can carry it, because it IS the hop sequence. So a \\`dynamic view\\`\nalready tagged \\`#cap-<slug>\\` may carry \\`#req-<slug>\\` as well, naming one of\nthat capability's requirements — the architect's answer to the analyst's\npromise, graded by the same four \\`usecase.*\\` codes down to the operation. The\nsecond tag is SCOPED by the first: a \\`Requirement-ID\\` is unique only inside its\nown document, so a \\`#req-\\` tag on a view that resolves no capability, or two, is\n\\`usecase.requirement-unresolved\\` (error) rather than a guess. Several \\`#req-\\`\ntags on one view are legal — a flow commonly keeps two promises. Both tags spell\ntheir id with every character outside \\`[A-Za-z0-9_-]\\` flattened to \\`-\\`,\nbecause that is all a LikeC4 tag name accepts.\n\nAgainst that vocabulary an undeclared name is \\`capability.unknown\\` (error; in\na feature delta it gates \\`loam archive\\`, \\`--approve\\`-overridable), an\nunreadable \\`capabilities.yaml\\` is \\`capability.invalid\\` exactly once per run\nwith every grade that resolves against the vocabulary suspended behind it, and\na declared capability no living requirement realizes is\n\\`capability.unrealized\\` (warn, one per capability). The fleet total is\nreadable: \\`loam list capabilities\\` reports each capability's realizing\nrequirements, their services and the draft/verified split, and\n\\`loam explore --capability <id>\\` seeds an exploration from the realizing\nservices.\n\n## The requirement baseline — \\`Based-On:\\`\n\nA MODIFIED requirement carries its FULL new text, not a diff, so archive does not\nmerge your wording into the living one — it REPLACES it. When two features rewrite\nthe same requirement, the loser loses everything they wrote, scenarios and all.\n\n\\`loam validate\\` names the other feature while both are in flight\n(\\`delta.modified-conflict\\`), but that warning cannot see the case that actually\nhappens: the first feature archives, stops being an active feature, the second\nrevalidates GREEN, and its archive lands text written against a document that no\nlonger exists — \\`+0 ~1 -0\\`, exit 0, nobody told.\n\nSo a MODIFIED or REMOVED requirement pins the living version it was written\nagainst, directly under its identity:\n\n\\`\\`\\`markdown\n### Requirement: Cancel an order\nRequirement-ID: REQ-042\nBased-On: 3bcc7a40f2b23ff3\n\\`\\`\\`\n\nYou do not compute that value — \\`loam rebase FEAT-101\\` writes it, for every\nMODIFIED/REMOVED requirement in the feature, from the living text as it stands\nright now. Run it when you finish authoring a delta, and again after you resolve\na collision. Archive refuses on a stale pin (\\`delta.baseline-stale\\`) and says\nboth digests.\n\n**Restamping is not resolving.** A pin claims \"I read this version\". If\n\\`loam rebase\\` reports a requirement as moved, re-read the living requirement and\nfold in what you still mean BEFORE you ship — otherwise you have re-pinned to a\ndocument you never read, and archive will merge your text over theirs with\nloam's blessing. There is no automatic three-way merge and there will not be one:\nrequirement prose does not merge, people do.\n\nA delta adopted from OpenSpec has no such line and gets a warning\n(\\`delta.baseline-missing\\`) — one that GATES the archive: unpinned is exactly\nthe shape that reverts someone else's landed change at exit 0. A migrated\ncorpus is not stranded by this: \\`loam rebase <FEAT>\\` pins every delta in one\ncommand, and a human's \\`--approve\\` archives unpinned when the loss is meant.\n\n### The same pin on the contract axis — and why it matters more there\n\nA feature's \\`openapi.yaml\\` is a COMPLETE document, not a patch: you restate the\nliving contract around the slot you are changing, and the merge upserts every\noperation the document spells. So the collision on this axis needs no overlap at\nall. Two features touching one service, editing DIFFERENT operations, destroy\neach other — the second to archive quietly pushes its authoring-time copy of the\noperation it never meant to touch back over whatever landed in between.\n\nThe same marker fixes it, as a vendor extension beside \\`x-loam-remove\\`:\n\n\\`\\`\\`yaml\npaths:\n /orders/{id}/cancel:\n post:\n operationId: cancelOrder\n x-loam-based-on: 86feabe621ed887d\n\\`\\`\\`\n\n\\`loam rebase\\` writes one on EVERY operation in the delta, pinned to the living\nversion. That is what lets the merge tell a QUOTE from an EDIT:\n\n- pin equals the operation's own content → you quoted it → **the merge skips it\n entirely** and the living contract keeps whatever it holds. Archive prints\n \\`· quotes …\\` so the plan never silently writes less than your delta spells.\n- pin equals the living operation → you edited it, nobody else did → merged.\n- pin matches neither → you edited it AND somebody landed a change to it →\n \\`openapi.baseline-stale\\`, refused.\n\nNot covered, deliberately, and worth knowing: path-level keys (\\`parameters\\`,\n\\`servers\\`) and \\`components/schemas\\` are still upserted wholesale, so a restated\nschema can still revert another feature's change to it —\n\\`openapi.path-item-modified\\` and \\`openapi.component-modified\\` are your only\nwarning there. Removal markers carry no pin either; \\`openapi.remove-target-mismatch\\`\nis what guards those.\n\n## Drawing a shared broker\n\nA broker drawn as ONE element is the node every service in the fleet points at,\nand any view over it is a star nobody can read. Nothing in loam catches this —\nloam parses the model and renders no view, so the map can be perfectly valid and\ncompletely illegible. Two habits keep it readable, and both are cheap only\nbefore the fleet is drawn:\n\n**Model the topic, not the broker.** Nest a topic per channel inside the broker's\nelement and point the edges at it — \\`paymentService -> kafka.paymentEvents\\`, not\n\\`paymentService -> kafka\\`. One hub of degree sixty becomes twelve of degree\nfive, and it is the truer model besides: what a producer and a consumer share is\nthe topic, never the broker. Every check joins the same way, because an edge into\na nested element resolves to the element that owns it. Declare the kind once and\ntag it there —\n\n specification {\n element topic {\n #external\n style { shape queue }\n }\n }\n\n— because **LikeC4 does not inherit tags**: a topic nested inside an \\`#external\\`\nbroker is not external itself, and \\`loam validate --all\\` will ask for a\n\\`services/payment.events/\\` nobody owes (\\`landscape.service-undocumented\\`).\n\n**Scope the views.** \\`exclude element.tag = #external\\` keeps the fleet overview to\nthe calls between our own services; a second view over the broker draws the event\nspine on its own. Views are LikeC4's and loam computes none, so this costs loam\nnothing and is worth doing anyway — an \\`include *\\` over the whole fleet map is\nalso the one view whose computation takes minutes rather than milliseconds. The\nscaffolded landscape applies the same pruning to ubiquitous infrastructure with\n\\`#platform\\`: the fleet view excludes it, and the platform view's\n\\`include * -> element.tag = #platform\\` (the relationship form — the obvious\nspelling draws boxes with no edges) keeps \"who depends on it\" answerable.\n\n## A message produced outside the fleet\n\nA platform service that is not ours — a config service, an auth broker —\nproduces messages our services consume, and no \\`services/<id>/\\` will ever hold\nits contract. Declare the production on the landscape, where the element\nalready says \\`#external\\`:\n\n externalConfig = softwareSystem 'external-config' {\n #external\n }\n externalConfig -> kafka.configTopic 'publishes config refreshes' {\n metadata { publishes 'config.ConfigRefreshed' }\n }\n\n\\`spine.message-unproduced\\` then stops firing — the message HAS a producer,\noutside the fleet — and the contract question moves to the one file that can\nanswer it: the consuming service's own asyncapi.yaml. While that file declares\nno shape for the message, \\`spine.message-external\\` (warn) says so; once the\nconsumer declares the payload, the axis is quiet. A truthful map and a green\nbuild no longer exclude each other, which is the exact choice this used to force.\n`,\n};\n"]}
1
+ {"version":3,"file":"spine.js","sourceRoot":"","sources":["../../../../../src/core/agent/workflows/reference/spine.ts"],"names":[],"mappings":"AAqBA,MAAM,CAAC,MAAM,UAAU,GAAmB;IACxC,IAAI,EAAE,YAAY;IAClB,WAAW,EAAE,mFAAmF;IAChG,0EAA0E;IAC1E,4EAA4E;IAC5E,yEAAyE;IACzE,YAAY,EAAE,EAAE;IAChB,OAAO,EACL,qNAAqN;IACvN,UAAU,EAAE,8BAA8B;IAC1C,YAAY,EAAE,EAAE;IAChB,KAAK,EAAE;QACL,cAAc;QACd,wCAAwC;QACxC,yBAAyB;QACzB,sCAAsC;KACvC;IACD,IAAI,EAAE;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;CA4RP;CACA,CAAC","sourcesContent":["/**\n * The `loam-spine` reference page: how the artifacts JOIN.\n *\n * Operation and message ids, coverage and permission references, the\n * capability tree and its `Realizes:` join, the requirement baseline pins\n * (`Based-On:` / `x-loam-based-on`), how to draw a shared broker, and how to\n * declare a message produced outside the fleet.\n *\n * This is the grammar a reader consults while writing ONE document, which is\n * why it is a printed page rather than a section of AGENTS.md: it is consulted\n * at a moment you can name, and paying for it on every invocation of every\n * session bought nothing. `## Which element IS which service` deliberately\n * stayed behind in ../../agents-md/spine.ts — an agent cannot read the\n * landscape at all without it, so it is orientation rather than reference.\n *\n * The text is the AGENTS.md section verbatim; nothing was rewritten to move\n * it, so a cross-reference from the remaining file still names the same\n * headings. See ./reference.ts for why these four pages exist.\n */\nimport type { CommandContent } from \"../../contract.js\";\n\nexport const LOAM_SPINE: CommandContent = {\n name: \"loam-spine\",\n description: \"Reference: the ID spine — every join between the artifacts, and the baseline pins\",\n // A reference page takes no arguments: it is one document, printed whole.\n // The empty hint is what `loam instructions` prints beside the name, and an\n // invented one (\"<none>\") would read as an argument somebody could pass.\n argumentHint: \"\",\n purpose:\n \"The exact joins the artifacts are held together by — operationId, message name, `Covers:`, `Requires:`, `Capability:`/`Realizes:` — and the `Based-On:` pins that stop two features in flight reverting each other.\",\n invocation: \"loam instructions loam-spine\",\n placeholders: [],\n spine: [\n \"The ID spine\",\n \"The requirement baseline — `Based-On:`\",\n \"Drawing a shared broker\",\n \"A message produced outside the fleet\",\n ],\n body: `The ID spine: how the artifacts join, and what pins a change to the version it was written against.\n\n## The ID spine\n\nThe spine is a family of exact joins, not one magic id: feature tag; service\nbinding; synchronous operation; asynchronous message; architecture coverage;\nauthorization permission. Three artifacts describe the same synchronous call in\nthree languages. They are joined by the **operationId**, and that join is what\n\\`loam validate\\` checks:\n\n- architecture — a C4 edge names the operation it calls:\n \\`checkoutWeb -> paymentService 'Calls authorizePayment' { metadata { op 'authorizePayment' } }\\`\n- behaviour — a requirement declares the operations it governs:\n \\`Operations: authorizePayment\\`\n- contract — the provider's \\`openapi.yaml\\` defines it:\n \\`operationId: authorizePayment\\`\n\nSpell it identically in all three places. A mismatch is an error, not a warning:\nan edge calling an operation nobody defines is a broken contract between services.\n\nFeatures are joined the same way: a delta's new elements and edges carry the\nfeature id as a LikeC4 tag (\\`#FEAT-101\\`), and that tag is exactly what\n\\`loam archive\\` folds into the living landscape. Untagged elements in a delta are\ncontext for the diagram, not changes.\n\nThe other joins use the same discipline:\n\n- \\`metadata { publishes|consumes 'message' }\\` on an edge ↔\n \\`Publishes:\\`/\\`Consumes:\\` in a requirement ↔ the AsyncAPI 3 message name;\n- \\`Covers:\\` in an architecture requirement ↔ a C4 element/edge or\n \\`alert:<id>\\` / \\`sli:<id>\\` from health.yaml;\n- \\`Requires: <subject>/<permission>\\` in either requirement file ↔ the fleet\n declaration in \\`architecture/permissions.yaml\\`;\n- \\`Capability:\\` in either requirement file ↔ a declared id in\n \\`architecture/capabilities.yaml\\`. A LIST, comma-separated — the relation is\n many-to-many in both directions.\n\nThe permissions file is opt-in: a fleet with no \\`Requires:\\` line owes none.\nOnce a requirement names a permission, an undeclared pair is\n\\`permissions.unknown\\` (error). An unreadable vocabulary is\n\\`permissions.invalid\\`; a declaration nothing requires is\n\\`permissions.unenforced\\` (warn). The subject is what the permission is checked\nON (user, profile, service), not necessarily the caller. \\`owned_by\\` and\n\\`enforced_by\\` are explanatory and are not resolved against services; this is\na checked document join, not proof that the identity provider implements it.\n\nThe capability axis opts in the OTHER way: the FILES are the opt-in, not the\nline. A fleet with neither \\`architecture/capabilities.yaml\\` nor a\n\\`capabilities/\\` directory gets no capability findings at all, however many\n\\`Capability:\\` lines its requirements already carry.\n\nA capability may be declared on either side, and the vocabulary is the UNION.\nA name alone is a line in \\`architecture/capabilities.yaml\\` —\n\\`capabilities: {<id>: {description, owner}}\\`, nested ids such as\n\\`payments/refunds\\` kept as one flat key. A name with prose behind it gets a\ndocument instead: \\`capabilities/<id>/spec.md\\`, the AUTHORED business tree,\none directory per capability with nesting spelled by the tree\n(\\`capabilities/payments/refunds/spec.md\\`). The directory is the list — there\nis no manifest — and its existence is what opts the fleet in, so \\`loam init\\`\ndoes not scaffold it. A directory holding neither the document nor a\ncapability beneath it is \\`capability.doc-missing\\` (warn).\n\nThe document carries narrative and then \\`## Requirements\\`, in the same\ngrammar every spec.md uses, with two rules of its own. Every requirement needs\na \\`Requirement-ID:\\` — these documents outlive the services that realize them,\nso identity is the line and not the heading; without one it is\n\\`capability.requirement-unidentified\\` (error). And a capability requirement\nmust be observable OUTSIDE the fleet: \\`Operations:\\`, \\`Covers:\\`,\n\\`Publishes:\\` and \\`Consumes:\\` all resolve against one service's own contract,\nso carrying any of them is \\`capability.requirement-service-scoped\\` (error) —\nwrite it in that service's spec.md instead. Naming no service is an authoring\nrule that PR review holds, not one loam checks: matching service names in prose\nwould be a heuristic, and loam does not guess. The axis's own two joins are\nrefused here as well (\\`capability.requirement-inert-join\\`, error): they point\nINTO the tree, so nothing reads them written inside it.\n\nA FEATURE changes a capability document with a delta, not by editing it:\n\\`features/<FEAT>/capabilities/<id>/spec.md\\`, the same\n\\`## ADDED|MODIFIED|REMOVED Requirements\\` grammar the service deltas use, the\nsame nesting (\\`features/<FEAT>/capabilities/payments/refunds/\\`), the same\n\\`Based-On:\\` pins, and the same \\`delta.*\\` refusals with the capability id as\nthe subject. \\`scaffold.placeholder\\` reads it too, so an unedited template\nheading refuses the archive here as it does in a spec delta, and\n\\`capability.doc-missing\\` (warn) names a directory in it that holds no\n\\`spec.md\\` — an empty one is not a delta, so the merge would carry nothing.\n\\`loam archive\\` merges it into the living document in the same\ntransaction as the service merge, creating \\`capabilities/<id>/spec.md\\` — and\nso the whole tree — when this is the first feature to mention it; the heading\nis the id and the narrative above \\`## Requirements\\` is yours to write, because\nloam merges requirements and never prose. The document's three rules above are\ngraded on the delta BEFORE the merge, so a capability requirement filed at the\nwrong altitude is refused while you still hold it. Editing the living document\ndirectly is still legal; the delta is what makes two features touching one\npromise collide loudly instead of silently. \\`loam new <FEAT> --capability <id>\\`\nscaffolds that delta, and \\`loam show <FEAT>\\` and \\`loam delta <FEAT>\\` list the\npromises a feature changes — a capability delta names no service, so without\nthose two a business-only feature reads as a feature carrying nothing.\n\nA service requirement says which promise it serves with \\`Realizes:\\`, entries\nspelled \\`<capability-id>#<Requirement-ID>\\`. This is the join the tree exists\nfor, and it is NOT the same claim as \\`Capability:\\` beside it — that names a\ntheme, this names one promise, and a requirement commonly carries both. The\ncapability half of an entry is what makes the target addressable, since a\n\\`Requirement-ID\\` is unique only inside its own document; the separator is the\nLAST \\`#\\`, because the requirement half's grammar excludes one and the\ncapability half's does not. Written by whoever implements the requirement,\nnever by the analyst — a business document must not be edited every time the\nfleet rearranges which service carries which part.\n\nBoth directions are graded. An entry naming no capability requirement is\n\\`capability.realizes-unknown\\` (error; in a feature delta it gates\n\\`loam archive\\`, \\`--approve\\`-overridable), and its message says which of the\nfive failures happened — malformed entry, undeclared capability, declared but\nundocumented, document with no requirements, or an id that document does not\ndeclare. A capability requirement nothing keeps is\n\\`capability.requirement-unrealized\\` (warn, one per requirement): it never\ngates, because writing the business document ahead of the fleet is the intended\nuse. \\`loam list capabilities --json\\` carries the whole join — each capability's\nrequirements, and what realizes each one.\n\n\\`Realizes:\\` is not the only way to keep a promise, and for a criterion that\nCROSSES services it is not enough: \"I enter a login and a password and I am in\"\nbelongs to no single service's spec, because each promises only its own part. A\nUSE CASE can carry it, because it IS the hop sequence. So a \\`dynamic view\\`\nalready tagged \\`#cap-<slug>\\` may carry \\`#req-<slug>\\` as well, naming one of\nthat capability's requirements — the architect's answer to the analyst's\npromise, graded by the same four \\`usecase.*\\` codes down to the operation. The\nsecond tag is SCOPED by the first: a \\`Requirement-ID\\` is unique only inside its\nown document, so a \\`#req-\\` tag on a view that resolves no capability, or two, is\n\\`usecase.requirement-unresolved\\` (error) rather than a guess. Several \\`#req-\\`\ntags on one view are legal — a flow commonly keeps two promises. Both tags spell\ntheir id with every character outside \\`[A-Za-z0-9_-]\\` flattened to \\`-\\`,\nbecause that is all a LikeC4 tag name accepts.\n\nAgainst that vocabulary an undeclared name is \\`capability.unknown\\` (error; in\na feature delta it gates \\`loam archive\\`, \\`--approve\\`-overridable), an\nunreadable \\`capabilities.yaml\\` is \\`capability.invalid\\` exactly once per run\nwith every grade that resolves against the vocabulary suspended behind it, and\na declared capability no living requirement realizes is\n\\`capability.unrealized\\` (warn, one per capability). The fleet total is\nreadable: \\`loam list capabilities\\` reports each capability's realizing\nrequirements, their services and the draft/verified split, and\n\\`loam explore --capability <id>\\` seeds an exploration from the realizing\nservices.\n\n## The requirement baseline — \\`Based-On:\\`\n\nA MODIFIED requirement carries its FULL new text, not a diff, so archive does not\nmerge your wording into the living one — it REPLACES it. When two features rewrite\nthe same requirement, the loser loses everything they wrote, scenarios and all.\n\n\\`loam validate\\` names the other feature while both are in flight\n(\\`delta.modified-conflict\\`), but that warning cannot see the case that actually\nhappens: the first feature archives, stops being an active feature, the second\nrevalidates GREEN, and its archive lands text written against a document that no\nlonger exists — \\`+0 ~1 -0\\`, exit 0, nobody told.\n\nSo a MODIFIED or REMOVED requirement pins the living version it was written\nagainst, directly under its identity:\n\n\\`\\`\\`markdown\n### Requirement: Cancel an order\nRequirement-ID: REQ-042\nBased-On: 3bcc7a40f2b23ff3\n\\`\\`\\`\n\nYou do not compute that value — \\`loam rebase FEAT-101\\` writes it, for every\nMODIFIED/REMOVED requirement in the feature, from the living text as it stands\nright now. Run it when you finish authoring a delta, and again after you resolve\na collision. Archive refuses on a stale pin (\\`delta.baseline-stale\\`) and says\nboth digests.\n\n**Restamping is not resolving.** A pin claims \"I read this version\". If\n\\`loam rebase\\` reports a requirement as moved, re-read the living requirement and\nfold in what you still mean BEFORE you ship — otherwise you have re-pinned to a\ndocument you never read, and archive will merge your text over theirs with\nloam's blessing. There is no automatic three-way merge and there will not be one:\nrequirement prose does not merge, people do.\n\nA delta adopted from OpenSpec has no such line and gets a warning\n(\\`delta.baseline-missing\\`) — one that GATES the archive: unpinned is exactly\nthe shape that reverts someone else's landed change at exit 0. A migrated\ncorpus is not stranded by this: \\`loam rebase <FEAT>\\` pins every delta in one\ncommand, and a human's \\`--approve\\` archives unpinned when the loss is meant.\n\n### The same pin on the contract axis — and why it matters more there\n\nA feature's \\`openapi.yaml\\` is a COMPLETE document, not a patch: you restate the\nliving contract around the slot you are changing, and the merge upserts every\noperation the document spells. So the collision on this axis needs no overlap at\nall. Two features touching one service, editing DIFFERENT operations, destroy\neach other — the second to archive quietly pushes its authoring-time copy of the\noperation it never meant to touch back over whatever landed in between.\n\nThe same marker fixes it, as a vendor extension beside \\`x-loam-remove\\`:\n\n\\`\\`\\`yaml\npaths:\n /orders/{id}/cancel:\n post:\n operationId: cancelOrder\n x-loam-based-on: 86feabe621ed887d\n\\`\\`\\`\n\n\\`loam rebase\\` writes one on EVERY operation in the delta, pinned to the living\nversion. That is what lets the merge tell a QUOTE from an EDIT:\n\n- pin equals the operation's own content → you quoted it → **the merge skips it\n entirely** and the living contract keeps whatever it holds. Archive prints\n \\`· quotes …\\` so the plan never silently writes less than your delta spells.\n- pin equals the living operation → you edited it, nobody else did → merged.\n- pin matches neither → you edited it AND somebody landed a change to it →\n \\`openapi.baseline-stale\\`, refused.\n\nNot covered, deliberately, and worth knowing: path-level keys (\\`parameters\\`,\n\\`servers\\`) and \\`components/schemas\\` are still upserted wholesale, so a restated\nschema can still revert another feature's change to it —\n\\`openapi.path-item-modified\\` and \\`openapi.component-modified\\` are your only\nwarning there. Removal markers carry no pin either; \\`openapi.remove-target-mismatch\\`\nis what guards those.\n\n## Drawing a shared broker\n\nA broker drawn as ONE element is the node every service in the fleet points at,\nand any view over it is a star nobody can read. Nothing in loam catches this —\nloam parses the model and renders no view, so the map can be perfectly valid and\ncompletely illegible. Two habits keep it readable, and both are cheap only\nbefore the fleet is drawn:\n\n**Model the topic, not the broker.** Nest a topic per channel inside the broker's\nelement and point the edges at it — \\`paymentService -> kafka.paymentEvents\\`, not\n\\`paymentService -> kafka\\`. One hub of degree sixty becomes twelve of degree\nfive, and it is the truer model besides: what a producer and a consumer share is\nthe topic, never the broker. Every check joins the same way, because an edge into\na nested element resolves to the element that owns it. Declare the kind once —\n\n specification {\n element topic {\n style { shape queue }\n }\n }\n\nThe KIND is what does the work: since 2026-09-07 loam reads \\`database\\`, \\`queue\\`,\n\\`topic\\`, \\`cache\\` and \\`bucket\\` as infrastructure rather than as candidate\nservices, so a topic drawn this way is out of the service census and nobody is\nasked for a \\`services/payment.events/\\` (measured: the same element spelled\n\\`container\\` earns \\`landscape.service-undocumented\\`, spelled \\`topic\\` it earns\nnothing). This used to need \\`#external\\` ON THE KIND as a workaround, because\n**LikeC4 does not inherit tags** and a topic nested inside an \\`#external\\` broker\nis not external itself. That workaround is no longer owed. Tag the kind\n\\`#external\\` if you want the topics out of the fleet VIEW as well — that is what\nthe next paragraph's \\`exclude\\` reads, and it is a rendering choice now rather\nthan a way to silence a check.\n\n**Scope the views.** \\`exclude element.tag = #external\\` keeps the fleet overview to\nthe calls between our own services; a second view over the broker draws the event\nspine on its own. Views are LikeC4's and loam computes none, so this costs loam\nnothing and is worth doing anyway — an \\`include *\\` over the whole fleet map is\nalso the one view whose computation takes minutes rather than milliseconds. The\nscaffolded landscape applies the same pruning to ubiquitous infrastructure with\n\\`#platform\\`: the fleet view excludes it, and the platform view's\n\\`include * -> element.tag = #platform\\` (the relationship form — the obvious\nspelling draws boxes with no edges) keeps \"who depends on it\" answerable.\n\n## A message produced outside the fleet\n\nA platform service that is not ours — a config service, an auth broker —\nproduces messages our services consume, and no \\`services/<id>/\\` will ever hold\nits contract. Declare the production on the landscape, where the element\nalready says \\`#external\\`:\n\n externalConfig = softwareSystem 'external-config' {\n #external\n }\n externalConfig -> kafka.configTopic 'publishes config refreshes' {\n metadata { publishes 'config.ConfigRefreshed' }\n }\n\n\\`spine.message-unproduced\\` then stops firing — the message HAS a producer,\noutside the fleet — and the contract question moves to the one file that can\nanswer it: the consuming service's own asyncapi.yaml. While that file declares\nno shape for the message, \\`spine.message-external\\` (warn) says so; once the\nconsumer declares the payload, the axis is quiet. A truthful map and a green\nbuild no longer exclude each other, which is the exact choice this used to force.\n`,\n};\n"]}
@@ -29,7 +29,7 @@ import { UNCHECKED } from "./unchecked.js";
29
29
  import { landscapeContext } from "./landscape.js";
30
30
  import { landscapeArtifact } from "./map/owed.js";
31
31
  import { exampleFor, extendPoint, FQN } from "./map/extend.js";
32
- import { ARTIFACTS } from "./targets.js";
32
+ import { ARTIFACTS, USECASES_DIR } from "./targets.js";
33
33
  import { EXTERNAL_TAG } from "../vocabulary/maturity.js";
34
34
  /**
35
35
  * The architecture walk: the order a service is read in, and what each stop is
@@ -151,10 +151,21 @@ export async function serviceBrief(docsDir, service, at) {
151
151
  const extend = extendPoint(landscape);
152
152
  const targets = [];
153
153
  for (const { key, ...artifact } of ARTIFACTS) {
154
- // `adrs/` is a directory, and an empty one is not an existing artifact —
155
- // reporting it as present would tell the agent to diff against nothing.
156
- const dir = key === "adrsDir";
157
- const exists = dir ? await hasMarkdown(paths[key]) : existsSync(paths[key]);
154
+ // `adrs/` and `usecases/` are DIRECTORIES, and an empty one is not an
155
+ // existing artifact — reporting it as present would tell the agent to diff
156
+ // against nothing, and for `usecases/` it would also tell the agent an
157
+ // optional slot was already taken.
158
+ //
159
+ // `usecases/` is the one target with no `ServicePaths` entry, and that is
160
+ // deliberate rather than an omission: nothing loam checks JOINS on that
161
+ // path. The slot a service's flows are graded out of is its whole LikeC4
162
+ // project (`core/usecases/service/flows.ts` reads every `.likec4` under the
163
+ // directory), so `usecases/<name>.likec4` is a convention this brief
164
+ // publishes, not a path a writer writes — and `targets.ts` is where a
165
+ // convention belongs.
166
+ const ext = key === "adrsDir" ? ".md" : key === "usecasesDir" ? ".likec4" : null;
167
+ const at = key === "usecasesDir" ? join(paths.dir, USECASES_DIR) : paths[key];
168
+ const exists = ext === null ? existsSync(at) : await hasEntry(at, ext);
158
169
  targets.push({
159
170
  ...artifact,
160
171
  ...(key === "openapi" ? { required: owesApi } : {}),
@@ -164,7 +175,7 @@ export async function serviceBrief(docsDir, service, at) {
164
175
  // `extend <fqn> {` copied verbatim is a document that does not parse.
165
176
  shape: artifact.shape.map((rule) => rule.replaceAll(FQN, extend.id)),
166
177
  ...(artifact.example === undefined ? {} : { example: exampleFor(artifact.example, extend, service) }),
167
- path: dir ? `${rel(paths[key])}/` : rel(paths[key]),
178
+ path: ext === null ? rel(at) : `${rel(at)}/`,
168
179
  exists,
169
180
  action: exists ? "diff" : "create",
170
181
  });
@@ -225,7 +236,13 @@ export async function serviceBrief(docsDir, service, at) {
225
236
  rule: NEVER_OVERWRITE,
226
237
  };
227
238
  }
228
- async function hasMarkdown(dir) {
239
+ /**
240
+ * Does this directory already hold an artifact of the given extension? The
241
+ * "exists" question for the two DIRECTORY targets — `adrs/` (`.md`) and
242
+ * `usecases/` (`.likec4`) — where an empty or absent directory is the same
243
+ * answer, because neither is something to diff against.
244
+ */
245
+ async function hasEntry(dir, ext) {
229
246
  if (!existsSync(dir))
230
247
  return false;
231
248
  const entries = await readdir(dir, { withFileTypes: true });
@@ -233,7 +250,7 @@ async function hasMarkdown(dir) {
233
250
  // is false for a symlinked ADR and the directory reads as empty (repo.ts's
234
251
  // `entryIs` is the same rule for `services/` and `features/`).
235
252
  return entries.some((e) => {
236
- if (!e.name.endsWith(".md"))
253
+ if (!e.name.endsWith(ext))
237
254
  return false;
238
255
  if (e.isFile())
239
256
  return true;
@@ -1 +1 @@
1
- {"version":3,"file":"brief.js","sourceRoot":"","sources":["../../../src/core/brief/brief.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * The adoption brief: what an agent needs in order to write one service's\n * baseline documentation, and what will be checked when it does.\n *\n * `adopt` was specified for years as \"read the code, emit the C4\". It is not\n * being built, because it cannot be: nothing deterministic reads a legacy\n * service and says what its architecture MEANS, and a guessed model is worse\n * than no model — everyone downstream has to re-derive it to know whether to\n * believe it.\n *\n * So loam takes the half of the job that IS deterministic. It states the work\n * precisely (which files, in which grammar, bound to which existing elements),\n * it says exactly which checks the result will face, and then it says which\n * checks do not exist — because everything in that last list is honesty no\n * validator will ever supply, and an agent that does not know where the checking\n * stops will assume it never does.\n *\n * Nothing here reads code. `sources` in the frontmatter is the only line that\n * ties the result to a repository, which is why the brief argues for it instead\n * of merely listing it.\n */\nimport { existsSync, statSync } from \"node:fs\";\nimport { readdir } from \"node:fs/promises\";\nimport { join, relative } from \"node:path\";\nimport type { PathableService } from \"../kernel/ids/service.js\";\nimport { landscapePath, type ServicePaths } from \"../repo/paths.js\";\nimport { locateServicePaths } from \"../repo/service-target.js\";\nimport { VALIDATE_CHECKS, type BriefCheck } from \"./checks.js\";\nimport { UNCHECKED } from \"./unchecked.js\";\nimport { landscapeContext, type LandscapeContext } from \"./landscape.js\";\nimport { landscapeArtifact } from \"./map/owed.js\";\nimport { exampleFor, extendPoint, FQN } from \"./map/extend.js\";\nimport { ARTIFACTS, type BriefTarget } from \"./targets.js\";\nimport type { DocsDir } from \"../kernel/ids/dirs.js\";\nimport { EXTERNAL_TAG } from \"../vocabulary/maturity.js\";\n\n/* ------------------------------------------------------------------ */\n/* The walk — what to read, in what order, and where each finding lands */\n/* ------------------------------------------------------------------ */\n\nexport interface WalkStop {\n /** What to open, named as a thing to go and find rather than a file name. */\n where: string;\n /** What to take out of it. */\n find: string;\n /** The artifacts this stop feeds, by `artifact` name from `targets[]`. */\n lands: string[];\n}\n\n/**\n * The architecture walk: the order a service is read in, and what each stop is\n * for.\n *\n * The brief said \"read the code: entry points, HTTP routes and handlers,\n * published events, config, deploy manifests, tests\" — one sentence, six nouns,\n * no order and no landing site. What came back matched the sentence: the HTTP\n * surface documented well, because it is the noun an agent recognises fastest,\n * and the scheduler, the consumer group and the outbox missing entirely. Not\n * because they were judged unimportant — because nothing said to go and look,\n * and every check loam owns is computed from what was written rather than from\n * what is there. `UNCHECKED` has said COMPLETENESS is unverifiable since the\n * beginning; that is an argument for stating the sweep, not for leaving it\n * implied.\n *\n * The order is deliberate and it is not the order a reader browses in. Stops 1\n * and 2 fix the SHAPE of the service — which processes exist at all — before\n * any surface is enumerated, because an agent that starts at the HTTP routes\n * has already concluded the service is an API by the time it meets the\n * scheduler. Everything after them is one surface each.\n *\n * `lands` names artifacts rather than describing them, because the two lists\n * are joined: test/agent-contract.test.ts requires every name here to be a\n * target the same brief hands over, so a stop can never feed a file nobody was\n * asked to write.\n */\nexport const WALK: WalkStop[] = [\n {\n where: \"the build and dependency manifests — pom.xml, build.gradle, package.json, go.mod, requirements.txt, and whatever the repo's own README calls the build\",\n find: \"what this service is built from, and every client library it links against. A broker client, an HTTP client, a database driver: each one is a dependency that has to appear as an edge or a container later, and this list is the only place they are all named at once. A library whose SEMANTICS decide behaviour — a retry budget, a delivery guarantee, a serialization format — is an arch requirement (arch.spec.md), not a fact to leave in your head.\",\n lands: [\"model.likec4\", \"runbook.md\", \"arch.spec.md\"],\n },\n {\n where: \"the entry points — main/Application/server bootstrap, the DI or module wiring, and anything the deploy manifests actually start\",\n find: \"how many PROCESSES this service is, and what each one is. An API, a worker, a scheduler, a consumer — a service that is three of those and documented as one is the most common way a baseline is wrong, and it is wrong in a shape no later check can see.\",\n lands: [\"model.likec4\"],\n },\n {\n where: \"the HTTP surface — every route, controller, handler and middleware, counted\",\n find: \"the operation set, in full, and inside each operation the DECISIONS its code makes: the permission and role checks — each one is a `Requires:` entry, and a name `architecture/permissions.yaml` does not declare yet is a line to ADD to that shared file, never to invent locally — the fields required only when another field is present, the defaults and normalisations applied before anything is stored, the transitions a request is refused for. Count both — operations, and branches per operation — before you write, and say both counts in the hand-back. `api.ungoverned` can tell you an operation you WROTE has no requirement; nothing at all can tell you about the twelve you never wrote down, or about the thirty branches inside the one you did. A guard is not implementation detail: what a service REFUSES is behaviour its caller observes and a test pins, and it is the half of a legacy service that lives only in the code and in one developer's head.\",\n lands: [\"openapi.yaml\", \"spec.md\"],\n },\n {\n where: \"the message surface — producers, consumers, listener annotations, topic and queue names, the outbox table and its relay\",\n find: \"every message this service puts on and takes off the bus, with its direction, and — on the consuming side — the DECISIONS the handler makes about a message before it acts on one. The direction is the half that gets lost: a consumer documented as a producer is a contract the whole fleet then joins against backwards. The guards are the half nobody looks for at all: which messages are filtered out and on what field, what happens to one whose entity has since been deleted, what is idempotency-keyed against a replay, what is dead-lettered rather than retried. Each of those is a scenario, and none of them appears anywhere in the contract — asyncapi.yaml describes the message, never what this service refuses to do with it.\",\n lands: [\"asyncapi.yaml\", \"landscape.likec4\"],\n },\n {\n where: \"the scheduled and background work — cron entries, timers, retry loops, reconcilers, migrations that run at boot\",\n find: \"behaviour no request ever triggers, and the conditions under which it does or does not run. It has requirements like anything else, and it is invisible to every surface above — which is why a baseline written from routes alone describes a service that does half of what it does. Take the decisions with it: which rows a reconciler SELECTS and which it skips, what a retry loop gives up on and after how long, what a migration does when it finds the state it is meant to fix already fixed. A scheduled job documented as 'runs nightly' is a requirement with no behaviour in it.\",\n lands: [\"spec.md\", \"arch.spec.md\"],\n },\n {\n where: \"the state — datastores, caches, buckets, and the schema or migration directory that says what is in them\",\n find: \"what this service OWNS versus what it borrows. Ownership is the fact the C4 is for, and it is the one nobody can reconstruct later from the code without re-reading all of it.\",\n lands: [\"model.likec4\"],\n },\n {\n where: \"the outbound calls — HTTP clients, generated stubs, gRPC channels, and the config that says which host each one points at\",\n find: \"who this service calls, and which operation each call names. These become the fleet map's edges; an edge missing here is a dependency the fleet cannot see, and `loam explore` will under-report the blast radius of every future change to the callee. An edge that only configuration attests is PROVISIONAL until the runtime stop: hold it until the deploy manifests confirm nothing switches that client off.\",\n lands: [\"landscape.likec4\"],\n },\n {\n where: \"the runtime — config and env vars, deploy manifests, pipelines, alert rules, dashboards\",\n find: \"how it is run and what pages whom. Alerts are the honest source for health.yaml: an SLO nobody agreed to is invention, but an alert that already fires is a fact somebody wrote down. Deployment manifests OVERRIDE configuration files, and they usually live in another repository — go and find them before any dependency that only configuration attests goes in the model, and check for an environment variable that disables the feature. A capability switched off at deploy time is not a dependency, however completely the config wires it: delete its edge and container, and record it in runbook.md's 'configured but not a dependency, and why' list. A manifest read from another repository is named in the runbook's prose — never in `sources`, which may only hold paths of this service's own repository. An EFFECTIVE configuration value behaviour depends on — the timeout the chart sets, the flag that switches a code path — is an arch requirement in arch.spec.md, with a Covers: line naming what it decides; loam holds its coverage even though it can never check the value.\",\n lands: [\"runbook.md\", \"health.yaml\", \"arch.spec.md\"],\n },\n {\n where: \"the tests, integration and component ones first\",\n find: \"the behaviours somebody already thought worth pinning. This is the PRIMARY source for scenarios, not the last sweep: a behaviour a test already pins is written FROM that test, and the code is read only for what no test covers. A test suite is the only part of a legacy service that states intent rather than mechanism, and a scenario derived from one is the only kind that starts out true. Carry the shape across as well as the content — a parameterised test (`@ParameterizedTest`, `@parametrize`, `test.each`) is one scenario with a table, not one scenario per case. Then say in the hand-back how well this service is covered and how many of its tests became scenarios: that number is the ceiling on what this whole baseline can be worth, and nothing downstream can compute it.\",\n lands: [\"spec.md\", \"arch.spec.md\"],\n },\n];\n\n/**\n * The stop that closes the walk. Separate from `WALK` because it is not a place\n * to go — it is the question asked once the walking is done, and the only\n * defence against a sweep that felt thorough because the parts it covered were\n * covered well.\n */\nexport const WALK_CLOSE =\n \"Then list the directories you did NOT open, and say why for each. `loam validate --service <id>`, run inside the service's own repo, reports the same list back as `sources.unwalked` — it compares your `sources` against the files git tracks. A directory you deliberately skipped is a sentence in the hand-back; a directory you skipped without noticing is the gap this walk exists to find.\";\n\n/* ------------------------------------------------------------------ */\n/* The frontmatter */\n/* ------------------------------------------------------------------ */\n\nexport interface FrontmatterBrief {\n /** Field -> what to write in it. */\n fields: Record<string, string>;\n /** Fields to leave alone entirely. */\n never: string[];\n /** Why `sources` carries the weight. */\n why: string;\n}\n\nconst FRONTMATTER_BRIEF: FrontmatterBrief = {\n fields: {\n service: \"the directory this file lives under, exactly. A mismatch is an error (`frontmatter.field-mismatch`).\",\n status:\n \"`draft`, always. `verified` is a human's word, stamped by `loam vouch --service <id>` run inside the service's own repo — never written by hand.\",\n owner: \"the team or person who answers for this service.\",\n sources:\n \"the paths in THIS SERVICE'S repository you actually read to write the document — files and directories only, a directory covering everything beneath it; glob patterns are refused. Not the paths a reader would expect to have been read. This is NOT a reading list for a human: nobody follows these, and nothing about the document asks them to.\",\n },\n // `vouch_scope` is in this list for a sharper reason than its siblings: the\n // others are stamps an agent could only forge, while this one is a stamp an\n // agent could DELETE — and deleting it turns a vouch that read four sections\n // into one that read the document, on every surface at once.\n never: [\"last_verified\", \"sources_digest\", \"content_digest\", \"sources_files\", \"vouch_scope\"],\n why:\n \"`sources` is digest input, not a citation. It is the only mechanical tie between this document and the code — everything else loam checks is internal consistency, and a corpus can agree with itself perfectly while describing nothing that exists. `loam validate`, run inside the service's repo, checks every listed path is still there; `loam vouch` hashes their CONTENT so that a later `validate` can say the code has moved since anyone read it. That hash is the entire point, and it is why padding the list costs something real rather than merely being untidy: a path nobody read still gets hashed, so its next change reports as drift in a document it never described, and a path that WAS read and went unlisted moves under the document in silence. Neither failure is visible to a reader; both are decided by whether this list is honest.\",\n};\n\nexport interface Brief {\n service: string;\n /** Absolute docs repo root — the anchor the repo-relative paths hang off. */\n docsDir: DocsDir;\n /** Repo-relative path of services/<id>/. */\n path: string;\n targets: BriefTarget[];\n /** The order to read the service in, and what each stop feeds. */\n walk: WalkStop[];\n /** The question the walk ends on — see `WALK_CLOSE`. */\n walkClose: string;\n landscape: LandscapeContext;\n frontmatter: FrontmatterBrief;\n checks: BriefCheck[];\n unchecked: string[];\n /** The one instruction that outranks the rest. */\n rule: string;\n}\n\nconst NEVER_OVERWRITE =\n \"Do not overwrite an artifact that already exists. Read it, diff your findings against it, and report what disagrees — a document somebody wrote is evidence, and replacing it destroys the only copy of what they knew.\";\n\n/**\n * Assemble the brief for one service. Reads the docs repo; writes nothing.\n * `at` overrides where the artifacts are briefed to land — `adopt\n * --subsystem`'s target inside a group — and is only ever passed for a\n * service the enumeration does NOT answer: an existing service is briefed\n * where it already lives, because the brief's one rule is to never overwrite,\n * and a second address would brief a duplicate.\n */\nexport async function serviceBrief(\n docsDir: DocsDir,\n service: PathableService,\n at?: ServicePaths,\n): Promise<Brief> {\n const paths = at ?? (await locateServicePaths(docsDir, service));\n const rel = (abs: string): string => relative(docsDir, abs).split(/[\\\\/]/).join(\"/\");\n\n // Read before the artifact loop, not after it: what the fleet already says\n // about this service decides whether it owes an API contract at all. The\n // model path travels with it — located through the enumeration above, never\n // joined at `services/<id>/` — so the edgeless state can name the calls the\n // service's own model attests.\n const landscape = await landscapeContext(docsDir, service, paths);\n const owesApi = apiExpected(landscape);\n\n // The one id the model target's grammar and example are written against,\n // resolved here because `ARTIFACTS` is static on purpose (a target list that\n // varied per repository would be a second thing to keep true) and because this\n // is the only place the fleet map has already been read.\n const extend = extendPoint(landscape);\n\n const targets: BriefTarget[] = [];\n for (const { key, ...artifact } of ARTIFACTS) {\n // `adrs/` is a directory, and an empty one is not an existing artifact —\n // reporting it as present would tell the agent to diff against nothing.\n const dir = key === \"adrsDir\";\n const exists = dir ? await hasMarkdown(paths[key]) : existsSync(paths[key]);\n targets.push({\n ...artifact,\n ...(key === \"openapi\" ? { required: owesApi } : {}),\n // Every `<fqn>` in the model target, replaced by the element this service\n // actually extends. The placeholder is left literal in `targets.ts` — the\n // brief is handed to an agent that will WRITE the file, and\n // `extend <fqn> {` copied verbatim is a document that does not parse.\n shape: artifact.shape.map((rule) => rule.replaceAll(FQN, extend.id)),\n ...(artifact.example === undefined ? {} : { example: exampleFor(artifact.example, extend, service) }),\n path: dir ? `${rel(paths[key])}/` : rel(paths[key]),\n exists,\n action: exists ? \"diff\" : \"create\",\n });\n }\n\n // The eighth target is the fleet map, and it is conditional on TWO facts: an\n // element resolving to the service, and an edge touching it. A service the\n // landscape already draws AND reaches owes it nothing, and briefing an edit\n // there would invite an agent to draw a second box for a service that has\n // one. A service with a box and no edge — what `loam seed` leaves for every\n // service nobody listed under `calls:` — owes it the edges, and until\n // 2026-09-03 element existence alone read as \"the map is complete\" here, so\n // the brief declared the map finished for exactly the services it had not\n // reached. Everything else about the target is unconditional — the file is\n // shared, so `action` is `edit` whenever it exists and `create` only for the\n // very first service of a brand-new docs repo.\n //\n // `modelled === false` and not `!== true`: `null` is \"loam could not read the\n // map\", and it fell through the same branch as \"the map genuinely draws\n // nothing for this service\". The brief then handed over the UNDRAWN target —\n // whose shape rule is \"add one top-level element bound to its directory\" and\n // whose example is a block to splice — for services the map already declares\n // and binds, and an agent that spliced it earned `landscape.binding-duplicate`\n // for doing exactly what the brief asked. With the map unreadable there is no\n // write to ask for; `landscape.instruction` carries the whole answer and names\n // the documents that failed.\n if (landscape.modelled === false || landscape.touched === false) {\n targets.push({\n ...landscapeArtifact({\n service,\n expects: landscape.expects,\n present: landscape.present,\n servicePath: rel(paths.dir),\n // The existing element's id, in the edgeless state only: it switches\n // the example from a block to edges on that element.\n // …and whether it is `#external`, which decides what the shape rule may\n // promise: `landscape.service-isolated` never fires on such an element.\n ...(landscape.touched === false && landscape.elements[0] !== undefined\n ? { elementId: landscape.elements[0].id, external: landscape.elements[0].tags.includes(EXTERNAL_TAG) }\n : {}),\n attested: landscape.attested,\n }),\n path: rel(landscapePath(docsDir)),\n exists: landscape.present,\n action: landscape.present ? \"edit\" : \"create\",\n });\n }\n\n return {\n service,\n docsDir,\n path: rel(paths.dir),\n targets,\n walk: WALK,\n walkClose: WALK_CLOSE,\n landscape,\n frontmatter: FRONTMATTER_BRIEF,\n checks: VALIDATE_CHECKS,\n unchecked: UNCHECKED,\n rule: NEVER_OVERWRITE,\n };\n}\n\nasync function hasMarkdown(dir: string): Promise<boolean> {\n if (!existsSync(dir)) return false;\n const entries = await readdir(dir, { withFileTypes: true });\n // `statSync` for the symlink case: a Dirent never follows one, so `isFile()`\n // is false for a symlinked ADR and the directory reads as empty (repo.ts's\n // `entryIs` is the same rule for `services/` and `features/`).\n return entries.some((e) => {\n if (!e.name.endsWith(\".md\")) return false;\n if (e.isFile()) return true;\n if (!e.isSymbolicLink()) return false;\n try {\n return statSync(join(dir, e.name)).isFile();\n } catch {\n return false;\n }\n });\n}\n\n/**\n * Does the fleet expect an API of this service?\n *\n * The brief marked `openapi.yaml` required unconditionally, and printed the\n * absence as `MISSING` in capitals — while `loam validate` and `loam list`\n * derive the opposite from the same landscape (`service.no-openapi` stays quiet\n * when nothing calls the service, and `list`'s maturity ladder stops demanding\n * a contract). For a UI, a worker or a cron the brief was therefore telling an\n * agent to INVENT an API contract into the source of truth — the one class of\n * guess this whole module exists to refuse, and the one the brief itself names\n * in `openapi.yaml`'s shape rules (`ARTIFACTS`, targets.ts).\n *\n * Positive evidence only, exactly as `list` reads it: a landscape that is\n * absent or unreadable proves nothing about who calls this service, so the\n * contract is still owed. `expects` is the ops on inbound edges, which is\n * `list`'s `called` set seen from one service.\n *\n * `callersKnown` and not `parses`, because since W4 `parses` answers about the\n * whole `architecture/` PROJECT: a broken palette or use case beside a landscape\n * loam reads perfectly made this discard the map and demand an OpenAPI contract\n * of a browser UI — inventing an API into the source of truth, which is the one\n * class of guess this module exists to refuse. `callersKnown` is the narrower\n * fact this question actually needs, and it is the one `list` and `context` read.\n */\nfunction apiExpected(landscape: LandscapeContext): boolean {\n return !landscape.callersKnown || landscape.expects.length > 0;\n}\n"]}
1
+ {"version":3,"file":"brief.js","sourceRoot":"","sources":["../../../src/core/brief/brief.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * The adoption brief: what an agent needs in order to write one service's\n * baseline documentation, and what will be checked when it does.\n *\n * `adopt` was specified for years as \"read the code, emit the C4\". It is not\n * being built, because it cannot be: nothing deterministic reads a legacy\n * service and says what its architecture MEANS, and a guessed model is worse\n * than no model — everyone downstream has to re-derive it to know whether to\n * believe it.\n *\n * So loam takes the half of the job that IS deterministic. It states the work\n * precisely (which files, in which grammar, bound to which existing elements),\n * it says exactly which checks the result will face, and then it says which\n * checks do not exist — because everything in that last list is honesty no\n * validator will ever supply, and an agent that does not know where the checking\n * stops will assume it never does.\n *\n * Nothing here reads code. `sources` in the frontmatter is the only line that\n * ties the result to a repository, which is why the brief argues for it instead\n * of merely listing it.\n */\nimport { existsSync, statSync } from \"node:fs\";\nimport { readdir } from \"node:fs/promises\";\nimport { join, relative } from \"node:path\";\nimport type { PathableService } from \"../kernel/ids/service.js\";\nimport { landscapePath, type ServicePaths } from \"../repo/paths.js\";\nimport { locateServicePaths } from \"../repo/service-target.js\";\nimport { VALIDATE_CHECKS, type BriefCheck } from \"./checks.js\";\nimport { UNCHECKED } from \"./unchecked.js\";\nimport { landscapeContext, type LandscapeContext } from \"./landscape.js\";\nimport { landscapeArtifact } from \"./map/owed.js\";\nimport { exampleFor, extendPoint, FQN } from \"./map/extend.js\";\nimport { ARTIFACTS, USECASES_DIR, type BriefTarget } from \"./targets.js\";\nimport type { DocsDir } from \"../kernel/ids/dirs.js\";\nimport { EXTERNAL_TAG } from \"../vocabulary/maturity.js\";\n\n/* ------------------------------------------------------------------ */\n/* The walk — what to read, in what order, and where each finding lands */\n/* ------------------------------------------------------------------ */\n\nexport interface WalkStop {\n /** What to open, named as a thing to go and find rather than a file name. */\n where: string;\n /** What to take out of it. */\n find: string;\n /** The artifacts this stop feeds, by `artifact` name from `targets[]`. */\n lands: string[];\n}\n\n/**\n * The architecture walk: the order a service is read in, and what each stop is\n * for.\n *\n * The brief said \"read the code: entry points, HTTP routes and handlers,\n * published events, config, deploy manifests, tests\" — one sentence, six nouns,\n * no order and no landing site. What came back matched the sentence: the HTTP\n * surface documented well, because it is the noun an agent recognises fastest,\n * and the scheduler, the consumer group and the outbox missing entirely. Not\n * because they were judged unimportant — because nothing said to go and look,\n * and every check loam owns is computed from what was written rather than from\n * what is there. `UNCHECKED` has said COMPLETENESS is unverifiable since the\n * beginning; that is an argument for stating the sweep, not for leaving it\n * implied.\n *\n * The order is deliberate and it is not the order a reader browses in. Stops 1\n * and 2 fix the SHAPE of the service — which processes exist at all — before\n * any surface is enumerated, because an agent that starts at the HTTP routes\n * has already concluded the service is an API by the time it meets the\n * scheduler. Everything after them is one surface each.\n *\n * `lands` names artifacts rather than describing them, because the two lists\n * are joined: test/agent-contract.test.ts requires every name here to be a\n * target the same brief hands over, so a stop can never feed a file nobody was\n * asked to write.\n */\nexport const WALK: WalkStop[] = [\n {\n where: \"the build and dependency manifests — pom.xml, build.gradle, package.json, go.mod, requirements.txt, and whatever the repo's own README calls the build\",\n find: \"what this service is built from, and every client library it links against. A broker client, an HTTP client, a database driver: each one is a dependency that has to appear as an edge or a container later, and this list is the only place they are all named at once. A library whose SEMANTICS decide behaviour — a retry budget, a delivery guarantee, a serialization format — is an arch requirement (arch.spec.md), not a fact to leave in your head.\",\n lands: [\"model.likec4\", \"runbook.md\", \"arch.spec.md\"],\n },\n {\n where: \"the entry points — main/Application/server bootstrap, the DI or module wiring, and anything the deploy manifests actually start\",\n find: \"how many PROCESSES this service is, and what each one is. An API, a worker, a scheduler, a consumer — a service that is three of those and documented as one is the most common way a baseline is wrong, and it is wrong in a shape no later check can see.\",\n lands: [\"model.likec4\"],\n },\n {\n where: \"the HTTP surface — every route, controller, handler and middleware, counted\",\n find: \"the operation set, in full, and inside each operation the DECISIONS its code makes: the permission and role checks — each one is a `Requires:` entry, and a name `architecture/permissions.yaml` does not declare yet is a line to ADD to that shared file, never to invent locally — the fields required only when another field is present, the defaults and normalisations applied before anything is stored, the transitions a request is refused for. Count both — operations, and branches per operation — before you write, and say both counts in the hand-back. `api.ungoverned` can tell you an operation you WROTE has no requirement; nothing at all can tell you about the twelve you never wrote down, or about the thirty branches inside the one you did. A guard is not implementation detail: what a service REFUSES is behaviour its caller observes and a test pins, and it is the half of a legacy service that lives only in the code and in one developer's head.\",\n lands: [\"openapi.yaml\", \"spec.md\"],\n },\n {\n where: \"the message surface — producers, consumers, listener annotations, topic and queue names, the outbox table and its relay\",\n find: \"every message this service puts on and takes off the bus, with its direction, and — on the consuming side — the DECISIONS the handler makes about a message before it acts on one. The direction is the half that gets lost: a consumer documented as a producer is a contract the whole fleet then joins against backwards. The guards are the half nobody looks for at all: which messages are filtered out and on what field, what happens to one whose entity has since been deleted, what is idempotency-keyed against a replay, what is dead-lettered rather than retried. Each of those is a scenario, and none of them appears anywhere in the contract — asyncapi.yaml describes the message, never what this service refuses to do with it.\",\n lands: [\"asyncapi.yaml\", \"landscape.likec4\"],\n },\n {\n where: \"the scheduled and background work — cron entries, timers, retry loops, reconcilers, migrations that run at boot\",\n find: \"behaviour no request ever triggers, and the conditions under which it does or does not run. It has requirements like anything else, and it is invisible to every surface above — which is why a baseline written from routes alone describes a service that does half of what it does. Take the decisions with it: which rows a reconciler SELECTS and which it skips, what a retry loop gives up on and after how long, what a migration does when it finds the state it is meant to fix already fixed. A scheduled job documented as 'runs nightly' is a requirement with no behaviour in it.\",\n lands: [\"spec.md\", \"arch.spec.md\"],\n },\n {\n where: \"the state — datastores, caches, buckets, and the schema or migration directory that says what is in them\",\n find: \"what this service OWNS versus what it borrows. Ownership is the fact the C4 is for, and it is the one nobody can reconstruct later from the code without re-reading all of it.\",\n lands: [\"model.likec4\"],\n },\n {\n where: \"the outbound calls — HTTP clients, generated stubs, gRPC channels, and the config that says which host each one points at\",\n find: \"who this service calls, and which operation each call names. These become the fleet map's edges; an edge missing here is a dependency the fleet cannot see, and `loam explore` will under-report the blast radius of every future change to the callee. An edge that only configuration attests is PROVISIONAL until the runtime stop: hold it until the deploy manifests confirm nothing switches that client off.\",\n lands: [\"landscape.likec4\"],\n },\n {\n where: \"the runtime — config and env vars, deploy manifests, pipelines, alert rules, dashboards\",\n find: \"how it is run and what pages whom. Alerts are the honest source for health.yaml: an SLO nobody agreed to is invention, but an alert that already fires is a fact somebody wrote down. Deployment manifests OVERRIDE configuration files, and they usually live in another repository — go and find them before any dependency that only configuration attests goes in the model, and check for an environment variable that disables the feature. A capability switched off at deploy time is not a dependency, however completely the config wires it: delete its edge and container, and record it in runbook.md's 'configured but not a dependency, and why' list. A manifest read from another repository is named in the runbook's prose — never in `sources`, which may only hold paths of this service's own repository. An EFFECTIVE configuration value behaviour depends on — the timeout the chart sets, the flag that switches a code path — is an arch requirement in arch.spec.md, with a Covers: line naming what it decides; loam holds its coverage even though it can never check the value.\",\n lands: [\"runbook.md\", \"health.yaml\", \"arch.spec.md\"],\n },\n {\n where: \"the tests, integration and component ones first\",\n find: \"the behaviours somebody already thought worth pinning. This is the PRIMARY source for scenarios, not the last sweep: a behaviour a test already pins is written FROM that test, and the code is read only for what no test covers. A test suite is the only part of a legacy service that states intent rather than mechanism, and a scenario derived from one is the only kind that starts out true. Carry the shape across as well as the content — a parameterised test (`@ParameterizedTest`, `@parametrize`, `test.each`) is one scenario with a table, not one scenario per case. Then say in the hand-back how well this service is covered and how many of its tests became scenarios: that number is the ceiling on what this whole baseline can be worth, and nothing downstream can compute it.\",\n lands: [\"spec.md\", \"arch.spec.md\"],\n },\n];\n\n/**\n * The stop that closes the walk. Separate from `WALK` because it is not a place\n * to go — it is the question asked once the walking is done, and the only\n * defence against a sweep that felt thorough because the parts it covered were\n * covered well.\n */\nexport const WALK_CLOSE =\n \"Then list the directories you did NOT open, and say why for each. `loam validate --service <id>`, run inside the service's own repo, reports the same list back as `sources.unwalked` — it compares your `sources` against the files git tracks. A directory you deliberately skipped is a sentence in the hand-back; a directory you skipped without noticing is the gap this walk exists to find.\";\n\n/* ------------------------------------------------------------------ */\n/* The frontmatter */\n/* ------------------------------------------------------------------ */\n\nexport interface FrontmatterBrief {\n /** Field -> what to write in it. */\n fields: Record<string, string>;\n /** Fields to leave alone entirely. */\n never: string[];\n /** Why `sources` carries the weight. */\n why: string;\n}\n\nconst FRONTMATTER_BRIEF: FrontmatterBrief = {\n fields: {\n service: \"the directory this file lives under, exactly. A mismatch is an error (`frontmatter.field-mismatch`).\",\n status:\n \"`draft`, always. `verified` is a human's word, stamped by `loam vouch --service <id>` run inside the service's own repo — never written by hand.\",\n owner: \"the team or person who answers for this service.\",\n sources:\n \"the paths in THIS SERVICE'S repository you actually read to write the document — files and directories only, a directory covering everything beneath it; glob patterns are refused. Not the paths a reader would expect to have been read. This is NOT a reading list for a human: nobody follows these, and nothing about the document asks them to.\",\n },\n // `vouch_scope` is in this list for a sharper reason than its siblings: the\n // others are stamps an agent could only forge, while this one is a stamp an\n // agent could DELETE — and deleting it turns a vouch that read four sections\n // into one that read the document, on every surface at once.\n never: [\"last_verified\", \"sources_digest\", \"content_digest\", \"sources_files\", \"vouch_scope\"],\n why:\n \"`sources` is digest input, not a citation. It is the only mechanical tie between this document and the code — everything else loam checks is internal consistency, and a corpus can agree with itself perfectly while describing nothing that exists. `loam validate`, run inside the service's repo, checks every listed path is still there; `loam vouch` hashes their CONTENT so that a later `validate` can say the code has moved since anyone read it. That hash is the entire point, and it is why padding the list costs something real rather than merely being untidy: a path nobody read still gets hashed, so its next change reports as drift in a document it never described, and a path that WAS read and went unlisted moves under the document in silence. Neither failure is visible to a reader; both are decided by whether this list is honest.\",\n};\n\nexport interface Brief {\n service: string;\n /** Absolute docs repo root — the anchor the repo-relative paths hang off. */\n docsDir: DocsDir;\n /** Repo-relative path of services/<id>/. */\n path: string;\n targets: BriefTarget[];\n /** The order to read the service in, and what each stop feeds. */\n walk: WalkStop[];\n /** The question the walk ends on — see `WALK_CLOSE`. */\n walkClose: string;\n landscape: LandscapeContext;\n frontmatter: FrontmatterBrief;\n checks: BriefCheck[];\n unchecked: string[];\n /** The one instruction that outranks the rest. */\n rule: string;\n}\n\nconst NEVER_OVERWRITE =\n \"Do not overwrite an artifact that already exists. Read it, diff your findings against it, and report what disagrees — a document somebody wrote is evidence, and replacing it destroys the only copy of what they knew.\";\n\n/**\n * Assemble the brief for one service. Reads the docs repo; writes nothing.\n * `at` overrides where the artifacts are briefed to land — `adopt\n * --subsystem`'s target inside a group — and is only ever passed for a\n * service the enumeration does NOT answer: an existing service is briefed\n * where it already lives, because the brief's one rule is to never overwrite,\n * and a second address would brief a duplicate.\n */\nexport async function serviceBrief(\n docsDir: DocsDir,\n service: PathableService,\n at?: ServicePaths,\n): Promise<Brief> {\n const paths = at ?? (await locateServicePaths(docsDir, service));\n const rel = (abs: string): string => relative(docsDir, abs).split(/[\\\\/]/).join(\"/\");\n\n // Read before the artifact loop, not after it: what the fleet already says\n // about this service decides whether it owes an API contract at all. The\n // model path travels with it — located through the enumeration above, never\n // joined at `services/<id>/` — so the edgeless state can name the calls the\n // service's own model attests.\n const landscape = await landscapeContext(docsDir, service, paths);\n const owesApi = apiExpected(landscape);\n\n // The one id the model target's grammar and example are written against,\n // resolved here because `ARTIFACTS` is static on purpose (a target list that\n // varied per repository would be a second thing to keep true) and because this\n // is the only place the fleet map has already been read.\n const extend = extendPoint(landscape);\n\n const targets: BriefTarget[] = [];\n for (const { key, ...artifact } of ARTIFACTS) {\n // `adrs/` and `usecases/` are DIRECTORIES, and an empty one is not an\n // existing artifact — reporting it as present would tell the agent to diff\n // against nothing, and for `usecases/` it would also tell the agent an\n // optional slot was already taken.\n //\n // `usecases/` is the one target with no `ServicePaths` entry, and that is\n // deliberate rather than an omission: nothing loam checks JOINS on that\n // path. The slot a service's flows are graded out of is its whole LikeC4\n // project (`core/usecases/service/flows.ts` reads every `.likec4` under the\n // directory), so `usecases/<name>.likec4` is a convention this brief\n // publishes, not a path a writer writes — and `targets.ts` is where a\n // convention belongs.\n const ext = key === \"adrsDir\" ? \".md\" : key === \"usecasesDir\" ? \".likec4\" : null;\n const at = key === \"usecasesDir\" ? join(paths.dir, USECASES_DIR) : paths[key];\n const exists = ext === null ? existsSync(at) : await hasEntry(at, ext);\n targets.push({\n ...artifact,\n ...(key === \"openapi\" ? { required: owesApi } : {}),\n // Every `<fqn>` in the model target, replaced by the element this service\n // actually extends. The placeholder is left literal in `targets.ts` — the\n // brief is handed to an agent that will WRITE the file, and\n // `extend <fqn> {` copied verbatim is a document that does not parse.\n shape: artifact.shape.map((rule) => rule.replaceAll(FQN, extend.id)),\n ...(artifact.example === undefined ? {} : { example: exampleFor(artifact.example, extend, service) }),\n path: ext === null ? rel(at) : `${rel(at)}/`,\n exists,\n action: exists ? \"diff\" : \"create\",\n });\n }\n\n // The eighth target is the fleet map, and it is conditional on TWO facts: an\n // element resolving to the service, and an edge touching it. A service the\n // landscape already draws AND reaches owes it nothing, and briefing an edit\n // there would invite an agent to draw a second box for a service that has\n // one. A service with a box and no edge — what `loam seed` leaves for every\n // service nobody listed under `calls:` — owes it the edges, and until\n // 2026-09-03 element existence alone read as \"the map is complete\" here, so\n // the brief declared the map finished for exactly the services it had not\n // reached. Everything else about the target is unconditional — the file is\n // shared, so `action` is `edit` whenever it exists and `create` only for the\n // very first service of a brand-new docs repo.\n //\n // `modelled === false` and not `!== true`: `null` is \"loam could not read the\n // map\", and it fell through the same branch as \"the map genuinely draws\n // nothing for this service\". The brief then handed over the UNDRAWN target —\n // whose shape rule is \"add one top-level element bound to its directory\" and\n // whose example is a block to splice — for services the map already declares\n // and binds, and an agent that spliced it earned `landscape.binding-duplicate`\n // for doing exactly what the brief asked. With the map unreadable there is no\n // write to ask for; `landscape.instruction` carries the whole answer and names\n // the documents that failed.\n if (landscape.modelled === false || landscape.touched === false) {\n targets.push({\n ...landscapeArtifact({\n service,\n expects: landscape.expects,\n present: landscape.present,\n servicePath: rel(paths.dir),\n // The existing element's id, in the edgeless state only: it switches\n // the example from a block to edges on that element.\n // …and whether it is `#external`, which decides what the shape rule may\n // promise: `landscape.service-isolated` never fires on such an element.\n ...(landscape.touched === false && landscape.elements[0] !== undefined\n ? { elementId: landscape.elements[0].id, external: landscape.elements[0].tags.includes(EXTERNAL_TAG) }\n : {}),\n attested: landscape.attested,\n }),\n path: rel(landscapePath(docsDir)),\n exists: landscape.present,\n action: landscape.present ? \"edit\" : \"create\",\n });\n }\n\n return {\n service,\n docsDir,\n path: rel(paths.dir),\n targets,\n walk: WALK,\n walkClose: WALK_CLOSE,\n landscape,\n frontmatter: FRONTMATTER_BRIEF,\n checks: VALIDATE_CHECKS,\n unchecked: UNCHECKED,\n rule: NEVER_OVERWRITE,\n };\n}\n\n/**\n * Does this directory already hold an artifact of the given extension? The\n * \"exists\" question for the two DIRECTORY targets — `adrs/` (`.md`) and\n * `usecases/` (`.likec4`) — where an empty or absent directory is the same\n * answer, because neither is something to diff against.\n */\nasync function hasEntry(dir: string, ext: string): Promise<boolean> {\n if (!existsSync(dir)) return false;\n const entries = await readdir(dir, { withFileTypes: true });\n // `statSync` for the symlink case: a Dirent never follows one, so `isFile()`\n // is false for a symlinked ADR and the directory reads as empty (repo.ts's\n // `entryIs` is the same rule for `services/` and `features/`).\n return entries.some((e) => {\n if (!e.name.endsWith(ext)) return false;\n if (e.isFile()) return true;\n if (!e.isSymbolicLink()) return false;\n try {\n return statSync(join(dir, e.name)).isFile();\n } catch {\n return false;\n }\n });\n}\n\n/**\n * Does the fleet expect an API of this service?\n *\n * The brief marked `openapi.yaml` required unconditionally, and printed the\n * absence as `MISSING` in capitals — while `loam validate` and `loam list`\n * derive the opposite from the same landscape (`service.no-openapi` stays quiet\n * when nothing calls the service, and `list`'s maturity ladder stops demanding\n * a contract). For a UI, a worker or a cron the brief was therefore telling an\n * agent to INVENT an API contract into the source of truth — the one class of\n * guess this whole module exists to refuse, and the one the brief itself names\n * in `openapi.yaml`'s shape rules (`ARTIFACTS`, targets.ts).\n *\n * Positive evidence only, exactly as `list` reads it: a landscape that is\n * absent or unreadable proves nothing about who calls this service, so the\n * contract is still owed. `expects` is the ops on inbound edges, which is\n * `list`'s `called` set seen from one service.\n *\n * `callersKnown` and not `parses`, because since W4 `parses` answers about the\n * whole `architecture/` PROJECT: a broken palette or use case beside a landscape\n * loam reads perfectly made this discard the map and demand an OpenAPI contract\n * of a browser UI — inventing an API into the source of truth, which is the one\n * class of guess this module exists to refuse. `callersKnown` is the narrower\n * fact this question actually needs, and it is the one `list` and `context` read.\n */\nfunction apiExpected(landscape: LandscapeContext): boolean {\n return !landscape.callersKnown || landscape.expects.length > 0;\n}\n"]}
@@ -7,6 +7,21 @@
7
7
  * it against a docs repo, and the fleet map's own target, whose block names
8
8
  * the service, is assembled per service in map/owed.ts.
9
9
  */
10
+ /**
11
+ * The service-local use-case directory, and the one artifact key `ServicePaths`
12
+ * deliberately does NOT carry.
13
+ *
14
+ * `repo/paths.ts` spells the paths loam JOINS ON — the ones a check resolves, a
15
+ * writer writes and the tree walk classifies a directory by. This is not one of
16
+ * them: the slot a service's flows are graded out of is the service's whole
17
+ * LikeC4 project, every `.likec4` under its directory
18
+ * (`core/usecases/service/flows.ts`), and `usecases/<name>.likec4` is the
19
+ * CONVENTION the brief publishes for it. Putting the directory in
20
+ * `ARTIFACT_FILES` would be worse than redundant — `isServiceArtifactName`
21
+ * reads that table, so a `usecases/` directory anywhere under `services/` would
22
+ * mint a service.
23
+ */
24
+ export const USECASES_DIR = "usecases";
10
25
  /** The artifacts, in the order a baseline is best written in. */
11
26
  export const ARTIFACTS = [
12
27
  {
@@ -153,6 +168,55 @@ Covers: paymentService.db, paymentService -> kafka
153
168
  - **Given** an event in the outbox
154
169
  - **When** the broker is unavailable
155
170
  - **Then** the state stays committed and the event is published once it returns
171
+ `,
172
+ },
173
+ {
174
+ artifact: "usecases/<name>.likec4",
175
+ key: "usecasesDir",
176
+ // Never required, and here the optionality is a RULE rather than a
177
+ // judgement. docs/DESIGN.md rule 26 — "presence is never owed. A model with
178
+ // no views is missing nothing loam wants, and no check may grade its
179
+ // absence" — is enforced by scripts/arch-check.mjs, not merely written, so
180
+ // there is no `service.usecases-missing` and there never will be one.
181
+ //
182
+ // The slot is briefed anyway because a team cannot choose a slot nobody
183
+ // named. The grader landed 2026-09-03 (`core/usecases/service/flows.ts`) and
184
+ // `c4.valid` has been reporting ` · K tagged flow(s) graded` ever since, on
185
+ // an axis nothing scaffolded, briefed or named — so the only way to discover
186
+ // it was to read the source. Publication is the whole of this entry: every
187
+ // rule below is either a check that already exists or a convention loam
188
+ // states and never grades, and each says which it is.
189
+ required: false,
190
+ purpose: "optional — one requirement's hop sequence, drawn as a LikeC4 dynamic view over this service's own containers",
191
+ shape: [
192
+ "A `dynamic view` in a `.likec4` under this service's directory. THE SLOT IS THE PROJECT: `loam subsystem sync` registers every `.likec4` under `services/<…>/<id>/` as one LikeC4 project, and loam reads exactly the set the renderer reads — `model.likec4` plus every sibling, recursively. `usecases/<name>.likec4` is the convention this brief publishes, and `<name>` is yours. Never `architecture/usecases/`, which is a different project: it holds the map and no service's containers, so a hop naming one is `landscape.invalid` for the whole fleet.",
193
+ "`#req-<Requirement-ID>` as the FIRST thing in the view body, before `title` — written after it, LikeC4 reports five parse errors and none of them mentions tags. The tag is the opt-in: an untagged view is somebody's hand-drawn diagram and loam never says a word about it. The slug is the `Requirement-ID` from this service's own `spec.md` or `arch.spec.md` with every character outside `[A-Za-z0-9_-]` flattened to `-`; a slug naming no requirement of this service is `usecase.requirement-unresolved`. Several `#req-` tags on one view are legal and normal.",
194
+ "DECLARE the tag exactly once — in `architecture/landscape.likec4`'s `specification` block when this model EXTENDS the map (the usual shape, and the block the whole root project shares), or in `model.likec4`'s own `specification` when the model STANDS ALONE. Declaring it in both is a duplicate, and the code lands where the SECOND declaration's file is: `c4.invalid` when a tags-only block sits in model.likec4, `usecase.flow-invalid` when it sits in a sibling. Two SERVICES each declaring the same tag locally is the fleet-scope class one step out, `c4.fleet-project-invalid`.",
195
+ "Every hop must be backed by a relationship the model already declares, or it is `usecase.step-unbacked` — LikeC4 itself reports nothing for an unbacked hop, so the diagram renders and every other check stays green. A return step is written `<target> <- <source>`: a reply is attributed to the CALL it answers. A hop whose caller and provider resolve to the SAME service never owes an `operationId`, which is what makes this altitude cheap: `usecase.step-unlinked` is silent inside one service.",
196
+ "Namespace the view id by the service — `uc_payment_service_authorize`, not `uc_authorize`. An EXTENDING model's views live in the ROOT project beside the map, so view ids are one flat namespace across the whole fleet: two services minting `uc_authorize` makes the renderer refuse the entire project. Ids beginning `subsystem_` or `service_` are loam's own (`subsystem.view-id-collision`).",
197
+ "Title it `'<subsystem path> / <service id> / <use case>'`. LikeC4 has no view-folder syntax — it splits a view title on `/` into browser folders and shows only the last segment (measured at 1.59.2) — so that convention lands the flow in the same folder as the generated overview of its service in `architecture/services.likec4`. **Nothing checks this**: it is a convention loam states and never grades, exactly like the EARS form in spec.md.",
198
+ "OPTIONAL, and its absence is graded by nothing, ever. That is docs/DESIGN.md rule 26 and it is enforced. Write one where a requirement's `Covers:` names containers and the sequence is the thing that is hard to hold in the head; write none otherwise. `c4.valid` reports ` · K tagged flow(s) graded` when the project declares any, and that count is the only place a healthy flow appears at all.",
199
+ ],
200
+ example: `// One of <svc>'s own use cases: the hops behind ONE requirement of its
201
+ // arch.spec.md, over its OWN containers. Optional — nothing grades the
202
+ // absence of this file, and an untagged view here is never graded either.
203
+ views {
204
+ // Namespace the id by the service, underscores for hyphens: an extending
205
+ // model's views share ONE flat id namespace with the whole root project.
206
+ dynamic view uc_authorize_payment {
207
+ // The tag comes FIRST. Written after 'title' this is five parse errors.
208
+ // AUTH-1 is a Requirement-ID in this service's spec.md or arch.spec.md.
209
+ #req-AUTH-1
210
+ title '<subsystem path> / <svc> / Authorize a payment'
211
+ description 'What the service does between the request and the answer'
212
+
213
+ // Every hop is backed by a relationship model.likec4 declares. Both of
214
+ // these are inside one service, so neither owes an operationId.
215
+ <fqn>.api -> <fqn>.ledger 'Records the authorization'
216
+ // A reply is attributed to the call it answers, hence the '<-'.
217
+ <fqn>.api <- <fqn>.ledger 'Confirms the write'
218
+ }
219
+ }
156
220
  `,
157
221
  },
158
222
  {
@@ -1 +1 @@
1
- {"version":3,"file":"targets.js","sourceRoot":"","sources":["../../../src/core/brief/targets.ts"],"names":[],"mappings":"AAAA;;;;;;;;GAQG;AAiDH,iEAAiE;AACjE,MAAM,CAAC,MAAM,SAAS,GAA8E;IAClG;QACE,QAAQ,EAAE,cAAc;QACxB,GAAG,EAAE,OAAO;QACZ,QAAQ,EAAE,IAAI;QACd,OAAO,EAAE,oEAAoE;QAC7E,uEAAuE;QACvE,2EAA2E;QAC3E,4EAA4E;QAC5E,qEAAqE;QACrE,oEAAoE;QACpE,8CAA8C;QAC9C,KAAK,EAAE;YACL,ibAAib;YACjb,wjBAAwjB;YACxjB,6zBAA6zB;YAC7zB,6aAA6a;SAC9a;QACD,mEAAmE;QACnE,4EAA4E;QAC5E,sEAAsE;QACtE,6EAA6E;QAC7E,2EAA2E;QAC3E,iEAAiE;QACjE,wEAAwE;QACxE,0EAA0E;QAC1E,qCAAqC;QACrC,OAAO,EAAE;;;;;;;;;;;;;;;;;;;;;;;;CAwBZ;KACE;IACD;QACE,QAAQ,EAAE,SAAS;QACnB,GAAG,EAAE,MAAM;QACX,QAAQ,EAAE,IAAI;QACd,OAAO,EAAE,mFAAmF;QAC5F,KAAK,EAAE;YACL,wDAAwD;YACxD,kQAAkQ;YAClQ,wMAAwM;YACxM,wtBAAwtB;YACxtB,o0BAAo0B;YACp0B,wjBAAwjB;YACxjB,4fAA4f;YAC5f,slBAAslB;YACtlB,6RAA6R;YAC7R,6HAA6H;SAC9H;QACD,OAAO,EAAE;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;CAuCZ;KACE;IACD;QACE,QAAQ,EAAE,cAAc;QACxB,GAAG,EAAE,UAAU;QACf,QAAQ,EAAE,KAAK;QACf,OAAO,EACL,uIAAuI;QACzI,KAAK,EAAE;YACL,kJAAkJ;YAClJ,uRAAuR;YACvR,8sCAA8sC;YAC9sC,kNAAkN;YAClN,+MAA+M;SAChN;QACD,OAAO,EAAE;;;;;;;;;;;;;;;;;;;;;;CAsBZ;KACE;IACD;QACE,QAAQ,EAAE,cAAc;QACxB,GAAG,EAAE,SAAS;QACd,0EAA0E;QAC1E,6DAA6D;QAC7D,QAAQ,EAAE,IAAI;QACd,OAAO,EAAE,wDAAwD;QACjE,KAAK,EAAE;YACL,iCAAiC;YACjC,+NAA+N;YAC/N,yXAAyX;YACzX,kKAAkK;YAClK,wEAAwE;YACxE,uEAAuE;YACvE,0EAA0E;YAC1E,wEAAwE;YACxE,uEAAuE;YACvE,kCAAkC;YAClC,kXAAkX;SACnX;KACF;IACD;QACE,QAAQ,EAAE,eAAe;QACzB,GAAG,EAAE,UAAU;QACf,0EAA0E;QAC1E,6EAA6E;QAC7E,uEAAuE;QACvE,kDAAkD;QAClD,QAAQ,EAAE,KAAK;QACf,OAAO,EAAE,8EAA8E;QACvF,KAAK,EAAE;YACL,8SAA8S;YAC9S,qRAAqR;YACrR,wPAAwP;YACxP,4bAA4b;YAC5b,gQAAgQ;YAChQ,+FAA+F;SAChG;KACF;IACD;QACE,QAAQ,EAAE,OAAO;QACjB,GAAG,EAAE,SAAS;QACd,QAAQ,EAAE,KAAK;QACf,OAAO,EAAE,+CAA+C;QACxD,KAAK,EAAE;YACL,+FAA+F;YAC/F,2MAA2M;YAC3M,oJAAoJ;SACrJ;KACF;IACD;QACE,QAAQ,EAAE,YAAY;QACtB,GAAG,EAAE,SAAS;QACd,QAAQ,EAAE,KAAK;QACf,OAAO,EAAE,iEAAiE;QAC1E,KAAK,EAAE;YACL,kIAAkI;YAClI,2JAA2J;YAC3J,4PAA4P;SAC7P;KACF;IACD;QACE,QAAQ,EAAE,aAAa;QACvB,GAAG,EAAE,QAAQ;QACb,QAAQ,EAAE,KAAK;QACf,OAAO,EAAE,2DAA2D;QACpE,KAAK,EAAE;YACL,uFAAuF;YACvF,uHAAuH;SACxH;KACF;CACF,CAAC","sourcesContent":["/**\n * The artifacts of the adoption brief: what each file under `services/<svc>/`\n * is for, the grammar it has to be written in, and a minimal valid example\n * where showing one is shorter than describing it. Every `shape` rule here is\n * one a later check depends on, and test/agent-contract.test.ts pins the\n * mapping. This module is the list alone — `serviceBrief` (brief.ts) resolves\n * it against a docs repo, and the fleet map's own target, whose block names\n * the service, is assembled per service in map/owed.ts.\n */\n\n/* ------------------------------------------------------------------ */\n/* The artifacts */\n/* ------------------------------------------------------------------ */\n\nexport interface BriefTarget {\n /** File name as it appears under `services/<svc>/`. */\n artifact: string;\n /** Repo-relative path to write. */\n path: string;\n exists: boolean;\n /**\n * `create` a missing artifact; `diff` an existing one — never overwrite it.\n *\n * `edit` is the third case and belongs to exactly one target: the fleet map.\n * `architecture/landscape.likec4` is a SHARED document that already holds\n * every other service, so \"diff it and report what disagrees\" is the wrong\n * instruction — the file is not this service's baseline, it is the fleet's,\n * and what this service owes it is an addition inside the existing\n * `model { … }` block. Spelling that as `diff` taught agents to leave the\n * fleet map alone; spelling it as `create` would have taught them to\n * overwrite ten other services.\n */\n action: \"create\" | \"diff\" | \"edit\";\n /**\n * False for artifacts a baseline can legitimately ship without. True means\n * required for a COMPLETE baseline — validate grades a missing spec.md or\n * openapi.yaml as a warn (`service.no-spec` / `service.no-openapi`), not an\n * error: partial adoption is a supported state, and the warns are the\n * progress meter.\n *\n * Constant per artifact except one: `openapi.yaml` is required only where the\n * fleet map shows an API is expected (see `apiExpected` in brief.ts), because\n * the brief is read as an instruction and \"MISSING openapi.yaml\" told an agent\n * to write a contract for a service that serves no requests.\n */\n required: boolean;\n /** What the artifact is for, in one line. */\n purpose: string;\n /** The grammar. Every rule here is one a later check depends on. */\n shape: string[];\n /** A minimal valid instance, where showing one is shorter than describing it. */\n example?: string;\n}\n\n/** Where an artifact lives — `repo/paths.ts` spells the filenames, this only points at them. */\ntype PathKey = \"model\" | \"spec\" | \"archSpec\" | \"openapi\" | \"asyncapi\" | \"adrsDir\" | \"runbook\" | \"health\";\n\n/** The artifacts, in the order a baseline is best written in. */\nexport const ARTIFACTS: Array<Omit<BriefTarget, \"path\" | \"exists\" | \"action\"> & { key: PathKey }> = [\n {\n artifact: \"model.likec4\",\n key: \"model\",\n required: true,\n purpose: \"the service's C4 — what it is, what is inside it, what it talks to\",\n // Every rule below is one a later check depends on, and the mapping is\n // pinned in test/agent-contract.test.ts. Rules nobody checks — the `views`\n // block, \"exactly one top-level element\" — used to sit here reading exactly\n // like the enforced ones, which is how a brief teaches an agent that\n // `loam validate` passing means more than it does. They now live in\n // UNCHECKED, where their status is the point.\n shape: [\n \"No `specification` block of its own: the kinds and tags come from `architecture/landscape.likec4` (a kind this model needs that the map does not declare — `element database`, `element queue` — is added there, once). A model that declares its own element kinds is the standalone shape: it still parses, alone, and loam then reports where its copies of the map's elements diverge (`c4.declaration-diverged`). New models never use it.\",\n \"A `model { ... }` block whose body is `extend <id> { ... }` — the fully-qualified id of the element the fleet map already binds to this directory (the landscape section of this brief names it; write the element into the map first when none resolves) — with containers and components nested inside. The binding and the description live on the map's element; this file may add `#tags` and metadata to it and never a description. The whole file has to parse: `loam validate` runs LikeC4 in-process over the map plus this file and reports `c4.invalid` with line numbers.\",\n \"A store or component this service OWNS goes INSIDE the `extend <fqn> { }` block, where its id becomes `<fqn>.<name>`: that is the only place an extending model may declare an element, and one declared anywhere else is `c4.element-unowned` (warn) — the fleet project then carries this service's private copy of somebody else's box. A call to another system is an edge from the container that makes it to the other party's element as the map spells it — `marketplace.paymentService.api -> stripe 'Authorizes cards'` — never a copy of that element: the map declares a system this service merely REACHES once, and another service's internals belong in that service's own model. The map still carries the service-level edge the fleet checks read (`metadata { op '<operationId>' }` graded by `spine.op-undefined`); draw it there too.\",\n \"The map's element is the binding: `metadata { service '<id>' }` on the element you extend (the landscape target below). Without it the element's title has to equal the directory name — and then renaming the box silently unlinks every check that joined the two. Until something in the map resolves to `services/<id>/`, `loam validate --all` reports `landscape.service-unmodelled` and there is no element for this file to extend.\",\n ],\n // `<fqn>` and `<svc>` are PLACEHOLDERS this module leaves literal:\n // `serviceBrief` (brief.ts) substitutes the landscape element's real id and\n // the service being adopted, and for `<fqn>` the \"the element's id in\n // architecture/landscape.likec4\" wording where the map holds none. ARTIFACTS\n // stays a static constant — a target list that varied per repository would\n // be a second thing to keep true. The header carried the literal\n // `payment-service` for every service, beside an `extend` line that WAS\n // substituted, so the half of the example that was personalised taught an\n // agent the other half had been too.\n example: `// <svc>'s own C4 — what is INSIDE the element the fleet map binds to\n// this directory. The map declares the kinds and the element; this\n// file extends it and re-declares nothing.\nmodel {\n extend <fqn> {\n api = container 'HTTP API'\n ledger = database 'Ledger store'\n api -> ledger 'Writes every authorization'\n }\n // 'stripe' stands for the OTHER party, spelled as the fleet map already\n // declares it — loam has no id to substitute here, and inventing one is\n // the edge that makes the model unresolvable. The containers above are\n // this example's own: name yours after what they are.\n <fqn>.api -> stripe 'Authorizes cards'\n}\n\n// Views are LikeC4's, not loam's (docs/DESIGN.md rule 26). Render with\n// 'npx likec4 start' from the docs repo root: this file is part of the root\n// project, beside the map — nothing else to write.\nviews {\n view of <fqn> {\n include *\n }\n}\n`,\n },\n {\n artifact: \"spec.md\",\n key: \"spec\",\n required: true,\n purpose: \"the living requirements — what this service is required to do, as it stands today\",\n shape: [\n \"Frontmatter (below), then a `## Requirements` heading.\",\n \"`### Requirement: <name>` — ONE observable behaviour, written with SHALL, testable without reading the code. Add `Requirement-ID: <id>` in its body for stable identity across heading renames; legacy requirements without one continue to match by exact name.\",\n \"At least one `#### Scenario: <name>` per requirement, with Given/When/Then bullets. This is gated (`requirements.missing-scenarios`) — scenarios are the acceptance criteria and the source for tests.\",\n \"A markdown TABLE in a scenario body becomes the `Examples` of a `Scenario Outline`: the header names the placeholders the steps use (`<permission>`), one row per case. This is how a matrix is written — a permission matrix, a validation matrix, a status-code table is ONE scenario with twenty rows, never twenty scenarios. `loam gherkin` emits the outline, cucumber reports one result per row carrying that scenario's digest tag, and `loam verify --results` confirms the claim only when EVERY row passed. Every row must have the header's column count and a header needs at least one row: a table loam cannot read stays in the description, the scenario then runs once instead of once per case, and the emission says so out loud.\",\n \"A step takes an ARGUMENT by carrying it INDENTED under the bullet, and this is what lets one scenario assert a whole data pass rather than a sentence. An indented table becomes that step's Gherkin DATA TABLE — `- **Then** the outbox holds:` followed by the rows it must hold; a fenced block becomes its DOCSTRING, indentation intact — `- **When** the caller posts:` followed by the JSON body. Indentation is the entire disambiguation: indented under a step means this step's rows, at the margin means the scenario's cases, and both ride into the scenario digest. Write the ASSERTIONS OUT, one bullet each, including the negative ones (`no row is created in ...`, `... is not invoked`) and each meter separately: a scenario that summarises them reads complete, emits valid Gherkin, grades green, and tests a fraction of what it claims.\",\n \"`Requires: <subject>/<permission>[, ...]` in the requirement body names the authorization it is gated on, resolved against the FLEET vocabulary in `architecture/permissions.yaml`. An entry that is not declared there is `permissions.unknown` (error) — the same grade as `Operations:`, because an invented permission reads exactly like a real one everywhere downstream. Add the permission to that file when this service enforces one nobody has written down yet; the subject is the entity it is checked ON (`user`, `profile`, `service`), which is not always the caller.\",\n \"`Capability: <id>[, ...]` in the requirement body names the fleet capabilities this requirement realizes part of, declared in `architecture/capabilities.yaml`. A LIST, because the relation is many-to-many: one requirement commonly closes part of two capabilities. The FILE is this axis's opt-in — a fleet without it gets no capability findings — and once it exists an undeclared name is `capability.unknown` (error). Write the line only where the requirement genuinely carries part of the named capability.\",\n \"Write the requirement body in EARS form — `WHEN <trigger> THE SYSTEM SHALL <response>`, `IF <precondition> THEN THE SYSTEM SHALL <response>`, `WHERE <feature is included> THE SYSTEM SHALL <response>`, or a bare `THE SYSTEM SHALL <response>` when it is unconditional. **Nothing checks this** — it is a writing convention, and it is here because the grammar forces the trigger and the response to be named separately, while free prose lets a requirement read as complete while stating no condition at all. That is the exact shape a guard collapses into when it is summarised rather than enumerated.\",\n \"`Operations: <operationId>[, <operationId>]` in the requirement body names the API operations it governs. Every one must exist in this service's openapi.yaml — a name that does not is `spec-api.op-undefined` (error), reported against the LIVING spec, not only inside a feature delta.\",\n \"Document what the service DOES today, not what it should do. This is the baseline; changes to it belong in a feature delta.\",\n ],\n example: `---\nservice: payment-service\nstatus: draft\nowner: payments-team\nsources:\n - src/main/java/com/shop/payment/\n---\n\n# payment-service\n\n## Requirements\n\n### Requirement: Authorize a payment\nWHEN authorization is requested for a captured-eligible order THE SYSTEM SHALL\nauthorize it against the issuer before any capture.\n\nOperations: authorizePayment\n\n#### Scenario: Successful authorization\n- **Given** a valid card and an amount within its limit\n- **When** authorization is requested\n- **Then** the payment is authorized and an authorization id is returned\n\n### Requirement: Refunding is permission-gated\nIF the caller lacks the refund permission THEN THE SYSTEM SHALL refuse the\nrequest and leave the payment untouched.\n\nOperations: refundPayment\n\n#### Scenario: Refund by permission\n- **Given** a captured payment\n- **When** a caller holding <permission> requests a refund\n- **Then** the response is <status> and the payment is <state>\n\n| permission | status | state |\n|-----------------|--------|----------|\n| payments:refund | 200 | refunded |\n| payments:read | 403 | captured |\n| (none) | 401 | captured |\n`,\n },\n {\n artifact: \"arch.spec.md\",\n key: \"archSpec\",\n required: false,\n purpose:\n \"the living architecture requirements — the obligations the business spec never carries: outbox, retries, idempotency, metrics, alerts\",\n shape: [\n \"Same grammar and frontmatter as spec.md: `## Requirements`, `### Requirement:` + `#### Scenario:` with Given/When/Then. The same checks read it.\",\n \"A `Covers:` line per requirement names what its scenarios exercise: a C4 element id (`paymentService.db`), an edge (`paymentService -> kafka`), or a health signal from health.yaml (`alert:<id>` / `sli:<id>`). Every entry must resolve, or `covers.unknown` (warn) flags the typo.\",\n \"A requirement whose `Covers:` names containers may have its hop sequence drawn as a `dynamic view` in a `.likec4` beside model.likec4 (`usecases/<name>.likec4` by convention; the renderer's per-service project reads every `.likec4` under the directory, and loam reads the same set), tagged `#req-<Requirement-ID>` — with the tag declared in `architecture/landscape.likec4`'s `specification` where the model extends the map, or in model.likec4's own block where it declares one; declaring it in BOTH is a duplicate, and the code lands where the second declaration's FILE is, not at fleet altitude: `c4.invalid` (an ERROR, and the model's grading is suspended behind it) when the tags-only block is in model.likec4, `usecase.flow-invalid` when it is in a sibling. `c4.fleet-project-invalid` is the fleet-scope class one step out — TWO SERVICES each declaring the same tag locally, which no per-service project sees. Graded by `loam validate --service <id>` (`usecase.step-unbacked` when a hop names a call the model does not declare, `usecase.requirement-unresolved` when the tag names no `Requirement-ID` here or in spec.md). Optional; nothing grades its absence. Never `architecture/usecases/`, which cannot resolve a container.\",\n \"Optional, but expected for a service with real architecture: every alert/SLI health.yaml declares wants a covering requirement here (`health.uncovered`, warn) — a signal nothing tests is dashboard decoration.\",\n \"Write the obligations no business requirement states — the transactional outbox, what a caller's retry may assume, what pages whom. An arch scenario becomes an integration/ops test, not an acceptance test.\",\n ],\n example: `---\nservice: payment-service\nstatus: draft\nowner: payments-team\nsources:\n - src/main/java/com/shop/payment/\n---\n\n# payment-service — architecture\n\n## Requirements\n\n### Requirement: Events leave through the transactional outbox\nThe service SHALL write a domain event and its state change in one transaction,\npublished by an outbox relay — never a dual write.\n\nCovers: paymentService.db, paymentService -> kafka\n\n#### Scenario: Broker down at commit time\n- **Given** an event in the outbox\n- **When** the broker is unavailable\n- **Then** the state stays committed and the event is published once it returns\n`,\n },\n {\n artifact: \"openapi.yaml\",\n key: \"openapi\",\n // Overridden per service in `serviceBrief` (brief.ts): an API is required\n // only where the fleet expects one. See `apiExpected` there.\n required: true,\n purpose: \"the API contract — the operations this service exposes\",\n shape: [\n \"OpenAPI 3.x with a `paths` map.\",\n \"EVERY operation carries an `operationId`. That token is the spine: the C4 edge's `metadata { op }`, the requirement's `Operations:` line and this `operationId` are one name spelled three times, and they must be identical.\",\n \"loam reads operationIds, and probes the rest by PRESENCE only: an operation whose responses declare no schema warns (`openapi.response-undescribed`), an internal `$ref` to nothing warns (`openapi.ref-unresolved`). What a declared schema SAYS is still never checked — the schemas are for humans, codegen and review, so getting them wrong is invisible here and expensive later.\",\n \"Document the endpoints that exist. An operation the fleet already calls (see `landscape.expects`) must be in here, or the contract breaks the moment this lands.\",\n // The instruction the brief was missing, and the one that matters most:\n // it used to mark this artifact required unconditionally, in capitals,\n // which for a UI, a worker or a cron told the agent to invent an API into\n // the source of truth. `required` is derived now; this says so in prose\n // as well, because an agent reading the shape rules must not re-derive\n // \"MISSING\" as \"write something\".\n \"If this service exposes no HTTP API, there is no file to write. Do not invent one — an empty or imagined contract is a claim the whole fleet then joins against. `required` above is derived from the fleet map: it is false exactly when the landscape parses and no edge calls an operation on this service, which is the same evidence that keeps `service.no-openapi` quiet.\",\n ],\n },\n {\n artifact: \"asyncapi.yaml\",\n key: \"asyncapi\",\n // Never required. Unlike the HTTP contract, whose necessity the fleet map\n // can PROVE (an op-linked edge points here), most services in a legacy fleet\n // touch no topic at all — and a required artifact nobody owes is how a\n // whole adoption wave learns to ignore the brief.\n required: false,\n purpose: \"the async contract — the messages this service puts on and takes off the bus\",\n shape: [\n \"AsyncAPI **3.0** with `channels`, `operations` and `components.messages`. Only 3.0 is read: its operations are named top-level objects carrying `action: send|receive`, which is what makes a message's direction legible. A 2.x document declares no `operations` and reads as a contract with no messages.\",\n \"EVERY message carries a `name` (or is declared under the key you intend to reference). That token is the spine: the C4 edge's `metadata { publishes }` / `metadata { consumes }`, the requirement's `Publishes:` / `Consumes:` line and this name are one name spelled three times.\",\n \"Namespace the name by the domain that owns it — `payment.PaymentAuthorized`. One message has exactly one producer; two services declaring they send one name is `asyncapi.message-contested`, and then which contract a consumer reads is a coin flip.\",\n \"loam reads message names and the `action` of the operation carrying them. **It never joins on anything inside `payload`** — the one look inside is a presence probe: a payload declaring no shape at all warns (`asyncapi.payload-undescribed`), and a payload with a non-JSON `schemaFormat` is skipped outright. Write the payload as JSON Schema today; if the fleet later adopts Avro, that is a `schemaFormat` line here and nothing in loam changes.\",\n \"Keep payloads in `components.schemas` in THIS file rather than `$ref`-ing an external `.avsc`. External references are out of scope for the merge, exactly as on the OpenAPI axis, so a schema in another file will not travel with the message that needs it.\",\n \"If this service publishes and consumes nothing, there is no file to write. Do not invent one.\",\n ],\n },\n {\n artifact: \"adrs/\",\n key: \"adrsDir\",\n required: false,\n purpose: \"the decisions behind the shape of the service\",\n shape: [\n \"One file per decision: `adrs/NNNN-<slug>.md`, MADR-style (context · decision · consequences).\",\n \"Only decisions whose consequences are still visible in the code. A reconstructed rationale for something nobody decided is fiction, and it is the kind that survives review because it sounds reasonable.\",\n \"If a decision is evident but its reasoning is not, write the decision and say the reasoning is unknown. That is a fact; a plausible motive is not.\",\n ],\n },\n {\n artifact: \"runbook.md\",\n key: \"runbook\",\n required: false,\n purpose: \"how it is run — deploys, dependencies, what to do when it pages\",\n shape: [\n \"Frontmatter, then how it is deployed and configured, what it depends on, and what to do about the failures that actually happen.\",\n \"Written from what is in the repo (pipelines, manifests, alert definitions). Whoever carries the pager verifies it; leave `status: draft` until they have.\",\n \"A 'configured but not a dependency, and why' list: every capability the configuration wires up that the deploy manifest switches off. Without it, the next reader re-derives the dependency from the config and re-adds what the manifest already removed.\",\n ],\n },\n {\n artifact: \"health.yaml\",\n key: \"health\",\n required: false,\n purpose: \"SLIs, SLOs, checks and the dependencies that are critical\",\n shape: [\n \"SLIs and SLOs as they have been AGREED, plus health checks and critical dependencies.\",\n \"If no SLO has ever been agreed, say so and leave it out. An invented 99.9% becomes a number other teams plan against.\",\n ],\n },\n];\n"]}
1
+ {"version":3,"file":"targets.js","sourceRoot":"","sources":["../../../src/core/brief/targets.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * The artifacts of the adoption brief: what each file under `services/<svc>/`\n * is for, the grammar it has to be written in, and a minimal valid example\n * where showing one is shorter than describing it. Every `shape` rule here is\n * one a later check depends on, and test/agent-contract.test.ts pins the\n * mapping. This module is the list alone — `serviceBrief` (brief.ts) resolves\n * it against a docs repo, and the fleet map's own target, whose block names\n * the service, is assembled per service in map/owed.ts.\n */\n\n/* ------------------------------------------------------------------ */\n/* The artifacts */\n/* ------------------------------------------------------------------ */\n\nexport interface BriefTarget {\n /** File name as it appears under `services/<svc>/`. */\n artifact: string;\n /** Repo-relative path to write. */\n path: string;\n exists: boolean;\n /**\n * `create` a missing artifact; `diff` an existing one — never overwrite it.\n *\n * `edit` is the third case and belongs to exactly one target: the fleet map.\n * `architecture/landscape.likec4` is a SHARED document that already holds\n * every other service, so \"diff it and report what disagrees\" is the wrong\n * instruction — the file is not this service's baseline, it is the fleet's,\n * and what this service owes it is an addition inside the existing\n * `model { … }` block. Spelling that as `diff` taught agents to leave the\n * fleet map alone; spelling it as `create` would have taught them to\n * overwrite ten other services.\n */\n action: \"create\" | \"diff\" | \"edit\";\n /**\n * False for artifacts a baseline can legitimately ship without. True means\n * required for a COMPLETE baseline — validate grades a missing spec.md or\n * openapi.yaml as a warn (`service.no-spec` / `service.no-openapi`), not an\n * error: partial adoption is a supported state, and the warns are the\n * progress meter.\n *\n * Constant per artifact except one: `openapi.yaml` is required only where the\n * fleet map shows an API is expected (see `apiExpected` in brief.ts), because\n * the brief is read as an instruction and \"MISSING openapi.yaml\" told an agent\n * to write a contract for a service that serves no requests.\n */\n required: boolean;\n /** What the artifact is for, in one line. */\n purpose: string;\n /** The grammar. Every rule here is one a later check depends on. */\n shape: string[];\n /** A minimal valid instance, where showing one is shorter than describing it. */\n example?: string;\n}\n\n/** Where an artifact lives — `repo/paths.ts` spells the filenames, this only points at them. */\ntype PathKey = \"model\" | \"spec\" | \"archSpec\" | \"openapi\" | \"asyncapi\" | \"adrsDir\" | \"runbook\" | \"health\";\n\n/**\n * The service-local use-case directory, and the one artifact key `ServicePaths`\n * deliberately does NOT carry.\n *\n * `repo/paths.ts` spells the paths loam JOINS ON — the ones a check resolves, a\n * writer writes and the tree walk classifies a directory by. This is not one of\n * them: the slot a service's flows are graded out of is the service's whole\n * LikeC4 project, every `.likec4` under its directory\n * (`core/usecases/service/flows.ts`), and `usecases/<name>.likec4` is the\n * CONVENTION the brief publishes for it. Putting the directory in\n * `ARTIFACT_FILES` would be worse than redundant — `isServiceArtifactName`\n * reads that table, so a `usecases/` directory anywhere under `services/` would\n * mint a service.\n */\nexport const USECASES_DIR = \"usecases\";\n\n/** The artifacts, in the order a baseline is best written in. */\nexport const ARTIFACTS: Array<\n Omit<BriefTarget, \"path\" | \"exists\" | \"action\"> & { key: PathKey | \"usecasesDir\" }\n> = [\n {\n artifact: \"model.likec4\",\n key: \"model\",\n required: true,\n purpose: \"the service's C4 — what it is, what is inside it, what it talks to\",\n // Every rule below is one a later check depends on, and the mapping is\n // pinned in test/agent-contract.test.ts. Rules nobody checks — the `views`\n // block, \"exactly one top-level element\" — used to sit here reading exactly\n // like the enforced ones, which is how a brief teaches an agent that\n // `loam validate` passing means more than it does. They now live in\n // UNCHECKED, where their status is the point.\n shape: [\n \"No `specification` block of its own: the kinds and tags come from `architecture/landscape.likec4` (a kind this model needs that the map does not declare — `element database`, `element queue` — is added there, once). A model that declares its own element kinds is the standalone shape: it still parses, alone, and loam then reports where its copies of the map's elements diverge (`c4.declaration-diverged`). New models never use it.\",\n \"A `model { ... }` block whose body is `extend <id> { ... }` — the fully-qualified id of the element the fleet map already binds to this directory (the landscape section of this brief names it; write the element into the map first when none resolves) — with containers and components nested inside. The binding and the description live on the map's element; this file may add `#tags` and metadata to it and never a description. The whole file has to parse: `loam validate` runs LikeC4 in-process over the map plus this file and reports `c4.invalid` with line numbers.\",\n \"A store or component this service OWNS goes INSIDE the `extend <fqn> { }` block, where its id becomes `<fqn>.<name>`: that is the only place an extending model may declare an element, and one declared anywhere else is `c4.element-unowned` (warn) — the fleet project then carries this service's private copy of somebody else's box. A call to another system is an edge from the container that makes it to the other party's element as the map spells it — `marketplace.paymentService.api -> stripe 'Authorizes cards'` — never a copy of that element: the map declares a system this service merely REACHES once, and another service's internals belong in that service's own model. The map still carries the service-level edge the fleet checks read (`metadata { op '<operationId>' }` graded by `spine.op-undefined`); draw it there too.\",\n \"The map's element is the binding: `metadata { service '<id>' }` on the element you extend (the landscape target below). Without it the element's title has to equal the directory name — and then renaming the box silently unlinks every check that joined the two. Until something in the map resolves to `services/<id>/`, `loam validate --all` reports `landscape.service-unmodelled` and there is no element for this file to extend.\",\n ],\n // `<fqn>` and `<svc>` are PLACEHOLDERS this module leaves literal:\n // `serviceBrief` (brief.ts) substitutes the landscape element's real id and\n // the service being adopted, and for `<fqn>` the \"the element's id in\n // architecture/landscape.likec4\" wording where the map holds none. ARTIFACTS\n // stays a static constant — a target list that varied per repository would\n // be a second thing to keep true. The header carried the literal\n // `payment-service` for every service, beside an `extend` line that WAS\n // substituted, so the half of the example that was personalised taught an\n // agent the other half had been too.\n example: `// <svc>'s own C4 — what is INSIDE the element the fleet map binds to\n// this directory. The map declares the kinds and the element; this\n// file extends it and re-declares nothing.\nmodel {\n extend <fqn> {\n api = container 'HTTP API'\n ledger = database 'Ledger store'\n api -> ledger 'Writes every authorization'\n }\n // 'stripe' stands for the OTHER party, spelled as the fleet map already\n // declares it — loam has no id to substitute here, and inventing one is\n // the edge that makes the model unresolvable. The containers above are\n // this example's own: name yours after what they are.\n <fqn>.api -> stripe 'Authorizes cards'\n}\n\n// Views are LikeC4's, not loam's (docs/DESIGN.md rule 26). Render with\n// 'npx likec4 start' from the docs repo root: this file is part of the root\n// project, beside the map — nothing else to write.\nviews {\n view of <fqn> {\n include *\n }\n}\n`,\n },\n {\n artifact: \"spec.md\",\n key: \"spec\",\n required: true,\n purpose: \"the living requirements — what this service is required to do, as it stands today\",\n shape: [\n \"Frontmatter (below), then a `## Requirements` heading.\",\n \"`### Requirement: <name>` — ONE observable behaviour, written with SHALL, testable without reading the code. Add `Requirement-ID: <id>` in its body for stable identity across heading renames; legacy requirements without one continue to match by exact name.\",\n \"At least one `#### Scenario: <name>` per requirement, with Given/When/Then bullets. This is gated (`requirements.missing-scenarios`) — scenarios are the acceptance criteria and the source for tests.\",\n \"A markdown TABLE in a scenario body becomes the `Examples` of a `Scenario Outline`: the header names the placeholders the steps use (`<permission>`), one row per case. This is how a matrix is written — a permission matrix, a validation matrix, a status-code table is ONE scenario with twenty rows, never twenty scenarios. `loam gherkin` emits the outline, cucumber reports one result per row carrying that scenario's digest tag, and `loam verify --results` confirms the claim only when EVERY row passed. Every row must have the header's column count and a header needs at least one row: a table loam cannot read stays in the description, the scenario then runs once instead of once per case, and the emission says so out loud.\",\n \"A step takes an ARGUMENT by carrying it INDENTED under the bullet, and this is what lets one scenario assert a whole data pass rather than a sentence. An indented table becomes that step's Gherkin DATA TABLE — `- **Then** the outbox holds:` followed by the rows it must hold; a fenced block becomes its DOCSTRING, indentation intact — `- **When** the caller posts:` followed by the JSON body. Indentation is the entire disambiguation: indented under a step means this step's rows, at the margin means the scenario's cases, and both ride into the scenario digest. Write the ASSERTIONS OUT, one bullet each, including the negative ones (`no row is created in ...`, `... is not invoked`) and each meter separately: a scenario that summarises them reads complete, emits valid Gherkin, grades green, and tests a fraction of what it claims.\",\n \"`Requires: <subject>/<permission>[, ...]` in the requirement body names the authorization it is gated on, resolved against the FLEET vocabulary in `architecture/permissions.yaml`. An entry that is not declared there is `permissions.unknown` (error) — the same grade as `Operations:`, because an invented permission reads exactly like a real one everywhere downstream. Add the permission to that file when this service enforces one nobody has written down yet; the subject is the entity it is checked ON (`user`, `profile`, `service`), which is not always the caller.\",\n \"`Capability: <id>[, ...]` in the requirement body names the fleet capabilities this requirement realizes part of, declared in `architecture/capabilities.yaml`. A LIST, because the relation is many-to-many: one requirement commonly closes part of two capabilities. The FILE is this axis's opt-in — a fleet without it gets no capability findings — and once it exists an undeclared name is `capability.unknown` (error). Write the line only where the requirement genuinely carries part of the named capability.\",\n \"Write the requirement body in EARS form — `WHEN <trigger> THE SYSTEM SHALL <response>`, `IF <precondition> THEN THE SYSTEM SHALL <response>`, `WHERE <feature is included> THE SYSTEM SHALL <response>`, or a bare `THE SYSTEM SHALL <response>` when it is unconditional. **Nothing checks this** — it is a writing convention, and it is here because the grammar forces the trigger and the response to be named separately, while free prose lets a requirement read as complete while stating no condition at all. That is the exact shape a guard collapses into when it is summarised rather than enumerated.\",\n \"`Operations: <operationId>[, <operationId>]` in the requirement body names the API operations it governs. Every one must exist in this service's openapi.yaml — a name that does not is `spec-api.op-undefined` (error), reported against the LIVING spec, not only inside a feature delta.\",\n \"Document what the service DOES today, not what it should do. This is the baseline; changes to it belong in a feature delta.\",\n ],\n example: `---\nservice: payment-service\nstatus: draft\nowner: payments-team\nsources:\n - src/main/java/com/shop/payment/\n---\n\n# payment-service\n\n## Requirements\n\n### Requirement: Authorize a payment\nWHEN authorization is requested for a captured-eligible order THE SYSTEM SHALL\nauthorize it against the issuer before any capture.\n\nOperations: authorizePayment\n\n#### Scenario: Successful authorization\n- **Given** a valid card and an amount within its limit\n- **When** authorization is requested\n- **Then** the payment is authorized and an authorization id is returned\n\n### Requirement: Refunding is permission-gated\nIF the caller lacks the refund permission THEN THE SYSTEM SHALL refuse the\nrequest and leave the payment untouched.\n\nOperations: refundPayment\n\n#### Scenario: Refund by permission\n- **Given** a captured payment\n- **When** a caller holding <permission> requests a refund\n- **Then** the response is <status> and the payment is <state>\n\n| permission | status | state |\n|-----------------|--------|----------|\n| payments:refund | 200 | refunded |\n| payments:read | 403 | captured |\n| (none) | 401 | captured |\n`,\n },\n {\n artifact: \"arch.spec.md\",\n key: \"archSpec\",\n required: false,\n purpose:\n \"the living architecture requirements — the obligations the business spec never carries: outbox, retries, idempotency, metrics, alerts\",\n shape: [\n \"Same grammar and frontmatter as spec.md: `## Requirements`, `### Requirement:` + `#### Scenario:` with Given/When/Then. The same checks read it.\",\n \"A `Covers:` line per requirement names what its scenarios exercise: a C4 element id (`paymentService.db`), an edge (`paymentService -> kafka`), or a health signal from health.yaml (`alert:<id>` / `sli:<id>`). Every entry must resolve, or `covers.unknown` (warn) flags the typo.\",\n \"A requirement whose `Covers:` names containers may have its hop sequence drawn as a `dynamic view` in a `.likec4` beside model.likec4 (`usecases/<name>.likec4` by convention; the renderer's per-service project reads every `.likec4` under the directory, and loam reads the same set), tagged `#req-<Requirement-ID>` — with the tag declared in `architecture/landscape.likec4`'s `specification` where the model extends the map, or in model.likec4's own block where it declares one; declaring it in BOTH is a duplicate, and the code lands where the second declaration's FILE is, not at fleet altitude: `c4.invalid` (an ERROR, and the model's grading is suspended behind it) when the tags-only block is in model.likec4, `usecase.flow-invalid` when it is in a sibling. `c4.fleet-project-invalid` is the fleet-scope class one step out — TWO SERVICES each declaring the same tag locally, which no per-service project sees. Graded by `loam validate --service <id>` (`usecase.step-unbacked` when a hop names a call the model does not declare, `usecase.requirement-unresolved` when the tag names no `Requirement-ID` here or in spec.md). Optional; nothing grades its absence. Never `architecture/usecases/`, which cannot resolve a container.\",\n \"Optional, but expected for a service with real architecture: every alert/SLI health.yaml declares wants a covering requirement here (`health.uncovered`, warn) — a signal nothing tests is dashboard decoration.\",\n \"Write the obligations no business requirement states — the transactional outbox, what a caller's retry may assume, what pages whom. An arch scenario becomes an integration/ops test, not an acceptance test.\",\n ],\n example: `---\nservice: payment-service\nstatus: draft\nowner: payments-team\nsources:\n - src/main/java/com/shop/payment/\n---\n\n# payment-service — architecture\n\n## Requirements\n\n### Requirement: Events leave through the transactional outbox\nThe service SHALL write a domain event and its state change in one transaction,\npublished by an outbox relay — never a dual write.\n\nCovers: paymentService.db, paymentService -> kafka\n\n#### Scenario: Broker down at commit time\n- **Given** an event in the outbox\n- **When** the broker is unavailable\n- **Then** the state stays committed and the event is published once it returns\n`,\n },\n {\n artifact: \"usecases/<name>.likec4\",\n key: \"usecasesDir\",\n // Never required, and here the optionality is a RULE rather than a\n // judgement. docs/DESIGN.md rule 26 — \"presence is never owed. A model with\n // no views is missing nothing loam wants, and no check may grade its\n // absence\" — is enforced by scripts/arch-check.mjs, not merely written, so\n // there is no `service.usecases-missing` and there never will be one.\n //\n // The slot is briefed anyway because a team cannot choose a slot nobody\n // named. The grader landed 2026-09-03 (`core/usecases/service/flows.ts`) and\n // `c4.valid` has been reporting ` · K tagged flow(s) graded` ever since, on\n // an axis nothing scaffolded, briefed or named — so the only way to discover\n // it was to read the source. Publication is the whole of this entry: every\n // rule below is either a check that already exists or a convention loam\n // states and never grades, and each says which it is.\n required: false,\n purpose:\n \"optional — one requirement's hop sequence, drawn as a LikeC4 dynamic view over this service's own containers\",\n shape: [\n \"A `dynamic view` in a `.likec4` under this service's directory. THE SLOT IS THE PROJECT: `loam subsystem sync` registers every `.likec4` under `services/<…>/<id>/` as one LikeC4 project, and loam reads exactly the set the renderer reads — `model.likec4` plus every sibling, recursively. `usecases/<name>.likec4` is the convention this brief publishes, and `<name>` is yours. Never `architecture/usecases/`, which is a different project: it holds the map and no service's containers, so a hop naming one is `landscape.invalid` for the whole fleet.\",\n \"`#req-<Requirement-ID>` as the FIRST thing in the view body, before `title` — written after it, LikeC4 reports five parse errors and none of them mentions tags. The tag is the opt-in: an untagged view is somebody's hand-drawn diagram and loam never says a word about it. The slug is the `Requirement-ID` from this service's own `spec.md` or `arch.spec.md` with every character outside `[A-Za-z0-9_-]` flattened to `-`; a slug naming no requirement of this service is `usecase.requirement-unresolved`. Several `#req-` tags on one view are legal and normal.\",\n \"DECLARE the tag exactly once — in `architecture/landscape.likec4`'s `specification` block when this model EXTENDS the map (the usual shape, and the block the whole root project shares), or in `model.likec4`'s own `specification` when the model STANDS ALONE. Declaring it in both is a duplicate, and the code lands where the SECOND declaration's file is: `c4.invalid` when a tags-only block sits in model.likec4, `usecase.flow-invalid` when it sits in a sibling. Two SERVICES each declaring the same tag locally is the fleet-scope class one step out, `c4.fleet-project-invalid`.\",\n \"Every hop must be backed by a relationship the model already declares, or it is `usecase.step-unbacked` — LikeC4 itself reports nothing for an unbacked hop, so the diagram renders and every other check stays green. A return step is written `<target> <- <source>`: a reply is attributed to the CALL it answers. A hop whose caller and provider resolve to the SAME service never owes an `operationId`, which is what makes this altitude cheap: `usecase.step-unlinked` is silent inside one service.\",\n \"Namespace the view id by the service — `uc_payment_service_authorize`, not `uc_authorize`. An EXTENDING model's views live in the ROOT project beside the map, so view ids are one flat namespace across the whole fleet: two services minting `uc_authorize` makes the renderer refuse the entire project. Ids beginning `subsystem_` or `service_` are loam's own (`subsystem.view-id-collision`).\",\n \"Title it `'<subsystem path> / <service id> / <use case>'`. LikeC4 has no view-folder syntax — it splits a view title on `/` into browser folders and shows only the last segment (measured at 1.59.2) — so that convention lands the flow in the same folder as the generated overview of its service in `architecture/services.likec4`. **Nothing checks this**: it is a convention loam states and never grades, exactly like the EARS form in spec.md.\",\n \"OPTIONAL, and its absence is graded by nothing, ever. That is docs/DESIGN.md rule 26 and it is enforced. Write one where a requirement's `Covers:` names containers and the sequence is the thing that is hard to hold in the head; write none otherwise. `c4.valid` reports ` · K tagged flow(s) graded` when the project declares any, and that count is the only place a healthy flow appears at all.\",\n ],\n example: `// One of <svc>'s own use cases: the hops behind ONE requirement of its\n// arch.spec.md, over its OWN containers. Optional — nothing grades the\n// absence of this file, and an untagged view here is never graded either.\nviews {\n // Namespace the id by the service, underscores for hyphens: an extending\n // model's views share ONE flat id namespace with the whole root project.\n dynamic view uc_authorize_payment {\n // The tag comes FIRST. Written after 'title' this is five parse errors.\n // AUTH-1 is a Requirement-ID in this service's spec.md or arch.spec.md.\n #req-AUTH-1\n title '<subsystem path> / <svc> / Authorize a payment'\n description 'What the service does between the request and the answer'\n\n // Every hop is backed by a relationship model.likec4 declares. Both of\n // these are inside one service, so neither owes an operationId.\n <fqn>.api -> <fqn>.ledger 'Records the authorization'\n // A reply is attributed to the call it answers, hence the '<-'.\n <fqn>.api <- <fqn>.ledger 'Confirms the write'\n }\n}\n`,\n },\n {\n artifact: \"openapi.yaml\",\n key: \"openapi\",\n // Overridden per service in `serviceBrief` (brief.ts): an API is required\n // only where the fleet expects one. See `apiExpected` there.\n required: true,\n purpose: \"the API contract — the operations this service exposes\",\n shape: [\n \"OpenAPI 3.x with a `paths` map.\",\n \"EVERY operation carries an `operationId`. That token is the spine: the C4 edge's `metadata { op }`, the requirement's `Operations:` line and this `operationId` are one name spelled three times, and they must be identical.\",\n \"loam reads operationIds, and probes the rest by PRESENCE only: an operation whose responses declare no schema warns (`openapi.response-undescribed`), an internal `$ref` to nothing warns (`openapi.ref-unresolved`). What a declared schema SAYS is still never checked — the schemas are for humans, codegen and review, so getting them wrong is invisible here and expensive later.\",\n \"Document the endpoints that exist. An operation the fleet already calls (see `landscape.expects`) must be in here, or the contract breaks the moment this lands.\",\n // The instruction the brief was missing, and the one that matters most:\n // it used to mark this artifact required unconditionally, in capitals,\n // which for a UI, a worker or a cron told the agent to invent an API into\n // the source of truth. `required` is derived now; this says so in prose\n // as well, because an agent reading the shape rules must not re-derive\n // \"MISSING\" as \"write something\".\n \"If this service exposes no HTTP API, there is no file to write. Do not invent one — an empty or imagined contract is a claim the whole fleet then joins against. `required` above is derived from the fleet map: it is false exactly when the landscape parses and no edge calls an operation on this service, which is the same evidence that keeps `service.no-openapi` quiet.\",\n ],\n },\n {\n artifact: \"asyncapi.yaml\",\n key: \"asyncapi\",\n // Never required. Unlike the HTTP contract, whose necessity the fleet map\n // can PROVE (an op-linked edge points here), most services in a legacy fleet\n // touch no topic at all — and a required artifact nobody owes is how a\n // whole adoption wave learns to ignore the brief.\n required: false,\n purpose: \"the async contract — the messages this service puts on and takes off the bus\",\n shape: [\n \"AsyncAPI **3.0** with `channels`, `operations` and `components.messages`. Only 3.0 is read: its operations are named top-level objects carrying `action: send|receive`, which is what makes a message's direction legible. A 2.x document declares no `operations` and reads as a contract with no messages.\",\n \"EVERY message carries a `name` (or is declared under the key you intend to reference). That token is the spine: the C4 edge's `metadata { publishes }` / `metadata { consumes }`, the requirement's `Publishes:` / `Consumes:` line and this name are one name spelled three times.\",\n \"Namespace the name by the domain that owns it — `payment.PaymentAuthorized`. One message has exactly one producer; two services declaring they send one name is `asyncapi.message-contested`, and then which contract a consumer reads is a coin flip.\",\n \"loam reads message names and the `action` of the operation carrying them. **It never joins on anything inside `payload`** — the one look inside is a presence probe: a payload declaring no shape at all warns (`asyncapi.payload-undescribed`), and a payload with a non-JSON `schemaFormat` is skipped outright. Write the payload as JSON Schema today; if the fleet later adopts Avro, that is a `schemaFormat` line here and nothing in loam changes.\",\n \"Keep payloads in `components.schemas` in THIS file rather than `$ref`-ing an external `.avsc`. External references are out of scope for the merge, exactly as on the OpenAPI axis, so a schema in another file will not travel with the message that needs it.\",\n \"If this service publishes and consumes nothing, there is no file to write. Do not invent one.\",\n ],\n },\n {\n artifact: \"adrs/\",\n key: \"adrsDir\",\n required: false,\n purpose: \"the decisions behind the shape of the service\",\n shape: [\n \"One file per decision: `adrs/NNNN-<slug>.md`, MADR-style (context · decision · consequences).\",\n \"Only decisions whose consequences are still visible in the code. A reconstructed rationale for something nobody decided is fiction, and it is the kind that survives review because it sounds reasonable.\",\n \"If a decision is evident but its reasoning is not, write the decision and say the reasoning is unknown. That is a fact; a plausible motive is not.\",\n ],\n },\n {\n artifact: \"runbook.md\",\n key: \"runbook\",\n required: false,\n purpose: \"how it is run — deploys, dependencies, what to do when it pages\",\n shape: [\n \"Frontmatter, then how it is deployed and configured, what it depends on, and what to do about the failures that actually happen.\",\n \"Written from what is in the repo (pipelines, manifests, alert definitions). Whoever carries the pager verifies it; leave `status: draft` until they have.\",\n \"A 'configured but not a dependency, and why' list: every capability the configuration wires up that the deploy manifest switches off. Without it, the next reader re-derives the dependency from the config and re-adds what the manifest already removed.\",\n ],\n },\n {\n artifact: \"health.yaml\",\n key: \"health\",\n required: false,\n purpose: \"SLIs, SLOs, checks and the dependencies that are critical\",\n shape: [\n \"SLIs and SLOs as they have been AGREED, plus health checks and critical dependencies.\",\n \"If no SLO has ever been agreed, say so and leave it out. An invented 99.9% becomes a number other teams plan against.\",\n ],\n },\n];\n"]}
@@ -29,7 +29,7 @@ export const UNCHECKED = [
29
29
  // second one the day its second service starts publishing, and every service
30
30
  // after that makes it worse by exactly one edge — which is why it arrives as
31
31
  // a surprise on a map that was fine last quarter.
32
- "Whether the fleet map is LEGIBLE — nothing in loam draws it, so nothing in loam notices. The shape that goes first is a shared broker: Kafka, a bus, a gateway, drawn as ONE element, is the node every service in the fleet points at, and a view over it is a star with sixty spokes. Model the TOPIC rather than the broker — `paymentEvents = topic 'payment.events'` nested inside the broker's element, with the edges pointing at `kafka.paymentEvents` — and one hub of degree sixty becomes twelve of degree five, which is also the truer model: the contract a producer and a consumer share is the topic, never the broker. Declare `element topic { #external, style { shape queue } }` in the `specification` block and give the nested elements that kind: LikeC4 does not inherit tags, so a topic under an `#external` broker is NOT external itself and `loam validate --all` asks for a `services/payment.events/` nobody owes (`landscape.service-undocumented`). Then scope the views, which is where the rest of the legibility lives: `exclude element.tag = #external` keeps the fleet overview to the calls between our own services, and a second view over the broker draws the event spine on its own.",
32
+ "Whether the fleet map is LEGIBLE — nothing in loam draws it, so nothing in loam notices. The shape that goes first is a shared broker: Kafka, a bus, a gateway, drawn as ONE element, is the node every service in the fleet points at, and a view over it is a star with sixty spokes. Model the TOPIC rather than the broker — `paymentEvents = topic 'payment.events'` nested inside the broker's element, with the edges pointing at `kafka.paymentEvents` — and one hub of degree sixty becomes twelve of degree five, which is also the truer model: the contract a producer and a consumer share is the topic, never the broker. Declare `element topic { style { shape queue } }` in the `specification` block and give the nested elements that kind: loam reads `database`, `queue`, `topic`, `cache` and `bucket` as infrastructure rather than as candidate services, so a topic drawn this way is out of the service census and nobody is asked for a `services/payment.events/` (the same element spelled `container` earns `landscape.service-undocumented`). Adding `#external` to that kind is now a VIEW choice — LikeC4 does not inherit tags, so a topic under an `#external` broker is not external itself, and tagging the kind is what puts the topics behind the `exclude` below — not a way to silence a check. Then scope the views, which is where the rest of the legibility lives: `exclude element.tag = #external` keeps the fleet overview to the calls between our own services, and a second view over the broker draws the event spine on its own.",
33
33
  // The third view entry, and the one docs/DESIGN.md rule 26 makes permanent:
34
34
  // what a rendered view SHOWS is never computed, so whether an adopted service
35
35
  // is on the picture is a fact loam cannot have. It is here rather than a