@ybotok/loam 0.2.0-alpha.2 → 0.2.0-alpha.5
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +124 -0
- package/MIGRATING-from-OpenSpec.md +4 -1
- package/README.md +11 -10
- package/ROADMAP.md +103 -17
- package/SCHEMA.md +86 -7
- package/dist/commands/archive/run.js +46 -2
- package/dist/commands/archive/run.js.map +1 -1
- package/dist/commands/seed/plan.js +1 -1
- package/dist/commands/seed/plan.js.map +1 -1
- package/dist/commands/subsystem/sync.js +111 -20
- package/dist/commands/subsystem/sync.js.map +1 -1
- package/dist/commands/subsystem/txn/txn.js +20 -1
- package/dist/commands/subsystem/txn/txn.js.map +1 -1
- package/dist/commands/subsystem/txn/views.js +23 -11
- package/dist/commands/subsystem/txn/views.js.map +1 -1
- package/dist/commands/validate/fleet/landscape.js +6 -0
- package/dist/commands/validate/fleet/landscape.js.map +1 -1
- package/dist/commands/validate/fleet/views/ids.js +6 -1
- package/dist/commands/validate/fleet/views/ids.js.map +1 -1
- package/dist/commands/validate/fleet/views/projects.js +90 -0
- package/dist/commands/validate/fleet/views/projects.js.map +1 -0
- package/dist/commands/validate/fleet/views/stale.js +15 -39
- package/dist/commands/validate/fleet/views/stale.js.map +1 -1
- package/dist/core/agent/agents-md/command-map.js +3 -1
- package/dist/core/agent/agents-md/command-map.js.map +1 -1
- package/dist/core/agent/agents-md/map/subsystem.js +18 -2
- package/dist/core/agent/agents-md/map/subsystem.js.map +1 -1
- package/dist/core/agent/agents-md.js +4 -0
- package/dist/core/agent/agents-md.js.map +1 -1
- package/dist/core/agent/workflows/adopt.js +13 -3
- package/dist/core/agent/workflows/adopt.js.map +1 -1
- package/dist/core/agent/workflows/check.js +1 -0
- package/dist/core/agent/workflows/check.js.map +1 -1
- package/dist/core/brief/checks.js +9 -0
- package/dist/core/brief/checks.js.map +1 -1
- package/dist/core/brief/targets.js +7 -4
- package/dist/core/brief/targets.js.map +1 -1
- package/dist/core/brief/unchecked.js +1 -1
- package/dist/core/brief/unchecked.js.map +1 -1
- package/dist/core/docs.js +11 -7
- package/dist/core/docs.js.map +1 -1
- package/dist/core/doctor/doctor.js +17 -6
- package/dist/core/doctor/doctor.js.map +1 -1
- package/dist/core/explain/families.js +1 -1
- package/dist/core/explain/families.js.map +1 -1
- package/dist/core/repo/paths.js +39 -1
- package/dist/core/repo/paths.js.map +1 -1
- package/dist/core/repo/tree/render/body.js +219 -0
- package/dist/core/repo/tree/render/body.js.map +1 -0
- package/dist/core/repo/tree/render/projects.js +124 -0
- package/dist/core/repo/tree/render/projects.js.map +1 -0
- package/dist/core/repo/tree/render/stale.js +26 -0
- package/dist/core/repo/tree/render/stale.js.map +1 -0
- package/dist/core/repo/tree/render/text.js +84 -0
- package/dist/core/repo/tree/render/text.js.map +1 -0
- package/dist/core/repo/tree/{views.js → render/views.js} +38 -34
- package/dist/core/repo/tree/render/views.js.map +1 -0
- package/examples/README.md +6 -1
- package/examples/docs/architecture/subsystems.likec4 +4 -0
- package/examples/docs/services/checkout-web/likec4.config.json +4 -0
- package/examples/docs/services/order-service/likec4.config.json +4 -0
- package/examples/docs/services/payment-service/likec4.config.json +4 -0
- package/examples/docs/services/payment-service/model.likec4 +4 -2
- package/examples/docs/services/platform/identity-service/likec4.config.json +4 -0
- package/examples/docs/services/platform/notification-service/likec4.config.json +4 -0
- package/package.json +1 -1
- package/dist/core/repo/tree/views.js.map +0 -1
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"families.js","sourceRoot":"","sources":["../../../src/core/explain/families.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * The family registry: `loam explain`'s content for the four code families no\n * fix table anywhere grades — `doctor.*`, `next.*`, `diff.*` and `gate.*`.\n *\n * WHY THIS IS NOT THE DUPLICATION fix-tables.ts FORBIDS. That module's header\n * argues against a second hand-maintained copy of the /loam-check prose, and\n * the argument is right: the tables ship in the binary, test/codes-drift.test.ts\n * holds every emitted finding code against them, and a registry restating them\n * would be free to trail the bytes `loam instructions loam-check` prints. None\n * of that applies here. These 65 codes have no fix-table row in any workflow\n * body — /loam-check grades DOCUMENTS, and these four families grade a\n * repository's wiring (`doctor`), a recommendation (`next`), a change between\n * two refs (`diff`) and a deploy question (`gate`). There is nothing to trail:\n * this registry is not a second copy, it is the only copy. The precedent is the\n * sibling module refusals.ts, a hand-written map that is the sole source for\n * the `ErrorCode` meanings for exactly the same reason — their prose lives in\n * doc comments, which are erased, so there is no runtime constant to read.\n *\n * What that costs, stated rather than hidden: nothing mechanical proves an\n * entry below still matches the message its emitter prints. Every meaning and\n * every fix here was written FROM the emitter — `doctor` findings carry a `fix`\n * field of their own (core/doctor/), a `next` step carries a `statement` and a\n * literal `command` (core/status/), the `DiffCode` union carries a doc comment\n * per member (core/diff/semantic.ts) — and the wording is theirs, not a\n * paraphrase invented here. Two guards catch the two failures that CAN be\n * caught: test/explain.test.ts asserts no code below is also answered by a fix\n * table (the two sources must never be able to disagree), and\n * test/agent-commands-runnable.test.ts parses every backticked `loam …` in this\n * file against the real program, so a fix naming a command loam does not have —\n * or a flag it does not declare — fails at gate time. A two-token span is a\n * NAME rather than an invocation there and is only checked for the command\n * existing, so a fix that means an invocation must carry its arguments.\n *\n * `severityNote` is the EMITTER's own word, not a fix table's parenthetical:\n * `doctor` grades blocker/warning, `diff` and `gate` grade ok/warn/error, and a\n * `next` step is graded not at all — it is a recommendation, so its note is\n * `step`. Where a code's severity depends on what was found (`doctor.docs-locked`,\n * `gate.partners-unknown`) the note says so in prose, the way the tables' own\n * long notes do (`warn here; error at archive plan time`).\n *\n * `openspec.*` and `mapping.*` — 49 codes — are deliberately NOT here, and the\n * reason is in test/explain.test.ts beside their backlog entries: they are the\n * only codes in the vocabulary whose subject is not a loam document at all.\n */\n\n/**\n * One code's answer. Structurally a `FindingEntry` (lookup.ts) minus `scope`,\n * which the family supplies — declared here rather than imported so this module\n * imports nothing at all and the package's file-level graph stays a tree.\n *\n * Every entry below is written on ONE line, however long that line runs, and\n * that is a deliberate concession to a counted limit rather than a style: 65\n * codes at four lines each is 260 lines of punctuation before a word of prose,\n * which does not fit the 400-line file limit — and src/core/explain/ is at its\n * five-file cap, so there is no second file to put the overflow in. The fields\n * stay named for the same reason a tuple was refused: `severityNote`, `meaning`\n * and `fix` say which column is which without counting commas.\n */\nexport interface FamilyEntry {\n /** The emitter's own severity word, or the prose a conditional one needs. */\n readonly severityNote: string;\n /** What the code is about, in the words the emitter already prints. */\n readonly meaning: string;\n /** What to do — for `next.*`, that the step's own `command` IS the action. */\n readonly fix: string;\n}\n\n/** One family: the invocation that emits it, and its codes. */\ninterface Family {\n /** The scope label, spelled as the fix tables spell theirs — the invocation that grades it. */\n readonly scope: string;\n readonly codes: Readonly<Record<string, FamilyEntry>>;\n}\n\n/**\n * `loam doctor` — the preflight. Every finding here already carries a `fix`\n * field in doctor's own report (core/doctor/doctor.ts, config.ts, agents.ts,\n * residue.ts); these rows say the same thing without a repository in front of\n * them, which is the whole difference between a report and a lookup.\n */\nconst DOCTOR: Family = {\n scope: \"loam doctor\",\n codes: {\n \"doctor.config-missing\": { severityNote: \"blocker\", meaning: \"No loam.json at or above the working directory, so nothing says which docs repo this checkout belongs to — every other command refuses with `no-config` until there is one.\", fix: \"Run `loam init --docs <dir>` in this repository; add `--create` when the docs repo does not exist yet.\" },\n \"doctor.config-invalid\": { severityNote: \"blocker\", meaning: \"A loam.json is there and `parseConfig` — the same loader every command uses — refuses it; the finding carries the parser's own words. doctor deliberately has no second validator: the one it used to carry accepted a config the commands rejected, and reported a healthy repo in which nothing could run.\", fix: \"Repair the file the finding names, or delete it and re-run `loam init`.\" },\n \"doctor.docs-missing\": { severityNote: \"blocker\", meaning: \"`docsDir` resolves to something that is missing or is not a directory — the docs repo was never cloned, or the path is wrong.\", fix: \"Fix `docsDir` in the loam.json the finding names, clone the docs repo to that path, or run `loam init --docs <dir> --create`.\" },\n \"doctor.services-missing\": { severityNote: \"blocker\", meaning: \"`docsDir` is a directory and holds no `services/`, so it is some other directory — most often the service repo itself, after a typo in the pointer.\", fix: \"Point `docsDir` at the shared docs repo, or run `loam init --docs <dir> --create` to scaffold one. Do not scaffold a second source of truth beside the real one.\" },\n \"doctor.docs-absolute\": { severityNote: \"warning\", meaning: \"`docsDir` is stored as an absolute path and loam.json is committed, so the pointer names a directory that exists on exactly one machine: it works for whoever ran `loam init` and for nobody who clones the repo afterwards.\", fix: \"Rewrite `docsDir` as a path relative to the loam.json holding it, such as `../docs`.\" },\n \"doctor.docs-unreadable\": { severityNote: \"warning\", meaning: \"The docs repo exists and this process cannot read it, so every command that reads the fleet will fail or report an empty one.\", fix: \"Grant read access to the directory the finding names — check ownership and mode.\" },\n \"doctor.docs-readonly\": { severityNote: \"warning\", meaning: \"The docs repo is readable and not writable: read-only commands work and the writing ones do not.\", fix: \"Grant write access if you need `loam new`, `loam adopt` or `loam archive`. A read-only checkout is a legitimate state for a reviewer, which is why this is a warning.\" },\n \"doctor.landscape-missing\": { severityNote: \"warning\", meaning: \"architecture/landscape.likec4 is missing, and it is the one artifact every cross-service check reads — nothing about who calls whom can be checked without it.\", fix: \"Create it with one element per service and an edge per call: `loam init --create` writes the empty map, and `loam seed --from fleet.yaml` templates one from stated facts.\" },\n \"doctor.landscape-unreadable\": { severityNote: \"blocker\", meaning: \"The landscape file exists and the read itself failed — permissions or an I/O error, carried verbatim in the message.\", fix: \"Check permissions on the path the finding names.\" },\n \"doctor.landscape-merge-conflict\": { severityNote: \"blocker\", meaning: \"The landscape still contains merge conflict markers, named by line. Checked BEFORE the parser on purpose: both sides of a conflict can be syntactically valid LikeC4, so the file sometimes parses and is still two halves of two different maps.\", fix: \"Resolve the conflict keeping BOTH services' elements and edges — the usual cause is several services adopted into one map in the same week — then re-run `loam doctor`.\" },\n \"doctor.landscape-invalid\": { severityNote: \"blocker\", meaning: \"The landscape exists, carries no conflict markers, and does not parse; the message quotes the first parser error with its line and counts the rest.\", fix: \"Fix the file — every fleet-wide check is blind until it parses.\" },\n \"doctor.likec4-config-missing\": { severityNote: \"warning\", meaning: \"The docs repo has no likec4.config.json, so the tree is not a loadable LikeC4 workspace: every service model and feature delta declares its own `specification` block, and a renderer pointed at the repo root merges them and reports every declaration as a duplicate. No loam check reads this file — the repo is only unrenderable, which is why it is a warning.\", fix: \"Write the project file the finding prints; it scopes the root project to architecture/, so `npx likec4 start` in the docs repo renders the fleet map. A service model or a feature delta is rendered by pointing the renderer at its own directory instead.\" },\n \"doctor.docs-locked\": { severityNote: \"warning while a live process holds it; blocker when its holder is dead or it names none\", meaning: \"A `.loam-lock` is present. Held by a running process it is a fact about right now. Held by a process that no longer exists on this host — or unreadable, so no holder can be named at all — nothing is ever going to release it, and every writing command refuses with `docs-busy` forever.\", fix: \"Wait and re-run while the holder is alive; nothing is read or written while it is held. Delete the lock file the finding names when its holder is dead or unnameable, then run `loam doctor` again for an interrupted commit underneath it.\" },\n \"doctor.commit-unreadable\": { severityNote: \"blocker\", meaning: \"A `.loam-commit` journal is present and cannot be read: a commit was interrupted and the one record of which files it had already written is unreadable.\", fix: \"Hand this to a human — compare the root's files against version control before running any loam command that writes there. Every journaled writer refuses with `commit-interrupted` while it is present.\" },\n \"doctor.commit-interrupted\": { severityNote: \"blocker\", meaning: \"A journaled writer — archive, unarchive, rebase, vouch, new, gherkin, or a recording verify — was killed mid-commit. The message names the host, pid and time, and every file or directory move that may be half-done. This is the surface that used to report a healthy repo over half-merged living docs.\", fix: \"Re-run the command the finding names: it recovers first, under the lock, rolling the staged bytes forward, and refuses rather than guessing if a file has been edited since. An interrupted `loam unarchive` is FINISHED, never undone — the merged text it was replacing is written down nowhere.\" },\n \"doctor.staging-temps\": { severityNote: \"warning\", meaning: \"Orphaned `.loam-*.tmp` scratch files from a killed writer, never linked into place — so nothing reads them and nothing depends on them. A journal's own recorded temps are excluded, because those are the durable copy its roll-forward recovery renames in. Litter, not damage.\", fix: \"The next journaled writer through that root removes its own; delete any that remain. They cost disk and nothing else.\" },\n \"doctor.inventory-unreadable\": { severityNote: \"warning\", meaning: \"The config and the docs repo are both fine and enumerating `services/` or `features/` threw, so doctor's fleet counts — and its verdict on the service binding — are about a fleet nobody could read.\", fix: \"Check that services/ and features/ under the docs repo are readable directories.\" },\n \"doctor.service-unbound\": { severityNote: \"warning\", meaning: \"loam.json declares no `service`, so nothing binds this checkout to one governed boundary and the service-repo checks have nothing to run against — `sources.stale`, a recording `loam verify`, `loam gherkin`. Deliberately not emitted inside the docs repo itself, where having no binding is the correct state rather than a gap.\", fix: \"Run `loam init --docs <dir> --service <id>` here.\" },\n \"doctor.service-unknown\": { severityNote: \"warning\", meaning: \"loam.json names a `service` the fleet has no services/<id>/ for — and the fleet WAS read, so this is a claim about a real inventory rather than one nobody could take.\", fix: \"Run `loam adopt --service <id>` to onboard it, or fix `service` in loam.json.\" },\n \"doctor.agent-files-missing\": { severityNote: \"warning\", meaning: \"Some of the command and skill files this loam lays down for the repo's recorded tools and profile are not here: the repo was initialized by an older loam, or they were deleted. An out-of-date command set costs entry points, never a refusal, which is why it is a warning.\", fix: \"Re-run `loam init --docs <dir> --tools <ids>` here. It creates absent files, refreshes only pointers whose current bytes still match their recorded digest, preserves every customized file, and keeps this repo's service binding.\" },\n \"doctor.agent-files-stale\": { severityNote: \"warning\", meaning: \"Command or skill files that ARE here carry no loam stamp, or one older than this binary, so the protocol and code tables they instruct an agent with may describe a loam that no longer exists. Staleness reads only the stamp; safe refresh is a separate digest check performed by init.\", fix: \"Re-run `loam init` to refresh a pointer only when its recorded digest still matches. Review a customized file against the current body yourself and update its stamp, or delete it so init can recreate it; your edits always outrank the template.\" },\n },\n};\n\n/**\n * `loam status` — both forms print `next[]`, and each entry carries a `code` to\n * branch on, one sentence, and the literal `command` to run. So the \"what to\n * do\" column is genuinely empty for this whole family: the step IS the action,\n * and each `fix` below names the step's own command rather than inventing a\n * second instruction beside it.\n */\nconst NEXT_STEPS: Family = {\n scope: \"loam status\",\n codes: {\n \"next.adopt-bound\": { severityNote: \"step\", meaning: \"The fleet form's first entry: this repository's loam.json says it is a service the fleet has no services/<id>/ for, so nothing about the boundary you are standing in is written down yet. Ahead of every other service's adoption work, because it is the only step that is about HERE.\", fix: \"The step IS the action: run its `command`, `loam adopt --service <id> --json`.\" },\n \"next.author-landscape\": { severityNote: \"step\", meaning: \"The first rung of the first-hour ladder, over a fleet with no services and no features: architecture/landscape.likec4 does not exist, or is still the scaffold's untouched map with no service drawn on it.\", fix: \"The step IS the action: run its `command` — `loam init --create` when the file is absent, `loam seed --from fleet.yaml` when it is the untouched scaffold. loam never guesses who calls whom.\" },\n \"next.bind-service\": { severityNote: \"step\", meaning: \"First-hour ladder: no service repository is bound to this fleet yet, so no command can ever run from a service's own checkout.\", fix: \"The step IS the action: run its `command`, `loam init --docs <path-to-this-docs-repo> --service <service-id>`, from a service's own repo, and commit the loam.json it writes.\" },\n \"next.adopt-first\": { severityNote: \"step\", meaning: \"First-hour ladder: services/ is empty, so nothing about any service is written down and nothing can be graded against anything.\", fix: \"The step IS the action: run its `command`, `loam adopt --service <service-id> --json`; the brief it prints walks an agent through writing the baseline docs as draft.\" },\n \"next.adopt\": { severityNote: \"step\", meaning: \"One service in the fleet has no spec.md — nothing about it is written down, so no feature can be graded against it. One entry per undocumented service.\", fix: \"The step IS the action: run its `command`, `loam adopt --service <id> --json`.\" },\n \"next.complete-service\": { severityNote: \"step\", meaning: \"Adoption does not end at spec.md: a service has one and no model.likec4, the state archive warns about and the fleet gate reports incomplete until something models it.\", fix: \"The step IS the action: run its `command`, `loam validate --service <id> --json`.\" },\n \"next.feature\": { severityNote: \"step\", meaning: \"One feature in flight that is not ready to ship, named with the stage it sits at and what it is missing — or, when it is blocked by another feature, the features it is waiting on.\", fix: \"The step IS the action: run its `command`, `loam status <FEAT-id> --json`, for that feature's own next list.\" },\n \"next.archive\": { severityNote: \"step\", meaning: \"In the fleet form, a feature that is authored and verified: ship it, and everything queued behind it is released. In a feature's own list it is always the last entry, saying the feature ships once everything above it is done.\", fix: \"The step IS the action: run its `command`, `loam archive <FEAT-id> --dry-run --json`; the dry run writes nothing and lists every file the merge would touch.\" },\n \"next.elided\": { severityNote: \"step\", meaning: \"The fleet list is capped, and this says how many steps of the same kinds it left out. The list is ordered most-unblocking first, so what was elided is the least urgent — a list nobody reads is not navigation.\", fix: \"The step IS the action: run its `command`, `loam list --json`, or work down the printed list and run status again.\" },\n \"next.fleet-clean\": { severityNote: \"step\", meaning: \"Services exist, nothing is in flight, and nothing is owed. Only that truthful case: over an EMPTY fleet the same sentence was vacuously true, and the first-hour ladder owns that repository now.\", fix: \"The step IS the action: run its `command`, `loam validate --all --json`, then start the next feature.\" },\n \"next.fleet-gate\": { severityNote: \"step\", meaning: \"The last entry of every fleet list, and always present: `status` grades nothing, so the least it can do is say where grading happens — `loam validate --all` is the gate this repository has to pass and the one CI runs.\", fix: \"The step IS the action: run its `command`, `loam validate --all --json`.\" },\n \"next.recover-commit\": { severityNote: \"step\", meaning: \"Always first, and never elided by the fleet form's cap: a journaled writer was killed mid-commit, so everything below this step is derived from files that may be half-written. Where the journal itself cannot be read, nothing below can be trusted until a human compares the living docs against version control.\", fix: \"The step IS the action: run its `command` — the interrupted command re-run, which recovers first under the lock, or `loam doctor --json` when the journal is unreadable and no loam command can repair it.\" },\n \"next.archived\": { severityNote: \"step\", meaning: \"The whole `next[]` for a feature that already shipped: it is archived, and its delta is folded into the living docs.\", fix: \"The step IS the action: run its `command`, `loam show <FEAT-id> --json`.\" },\n \"next.author-intent\": { severityNote: \"step\", meaning: \"The feature has no intent document — what it is for, in prose, before anything is derived from it.\", fix: \"The step IS the action: write the file the step names, then run its `command`, `loam validate <FEAT-id> --json`. The command never writes the artifact: loam does not author.\" },\n \"next.touch-service\": { severityNote: \"step\", meaning: \"The feature carries no per-service delta at all, so nothing says which governed boundaries it changes.\", fix: \"The step IS the action: run its `command`, `loam list services --json`, then add specs/<service>/spec.md for every service the feature changes.\" },\n \"next.author-spec\": { severityNote: \"step\", meaning: \"One service the feature touches has no requirement delta — the ADDED / MODIFIED / REMOVED requirements the feature makes to that service, each with a scenario.\", fix: \"The step IS the action: write the file the step names, then run its `command`, `loam delta <FEAT-id> --service <id> --json`, which projects the delta onto the living document.\" },\n \"next.author-openapi\": { severityNote: \"step\", meaning: \"Nothing gives one of the feature's services a living openapi.yaml, so this feature is the only thing that can write down its API surface. Deliberately not phrased as the service being absent from the living docs: a service adopted without a contract is the commoner case, and that wording sent readers looking for a directory that was right there.\", fix: \"The step IS the action: write the file the step names, then run its `command`, `loam delta <FEAT-id> --service <id> --json`.\" },\n \"next.author-scenarios\": { severityNote: \"step\", meaning: \"Requirements in one service's delta carry no `#### Scenario:`, so they generate no test and no verify claim — a promise that reaches neither the suite nor the done-check. One entry per service and never per requirement, because a delta with forty bare requirements would otherwise BE the payload.\", fix: \"The step IS the action: write a scenario under each bare requirement, then run its `command`, `loam delta <FEAT-id> --service <id> --json`.\" },\n \"next.rebase\": { severityNote: \"step\", meaning: \"The feature carries requirements or operations with no baseline pin, on any of the three axes; until they have one the merge cannot tell what it EDITS from what it merely quotes.\", fix: \"The step IS the action: run its `command`, `loam rebase <FEAT-id>`.\" },\n \"next.archive-first\": { severityNote: \"step\", meaning: \"The feature builds on other features still in flight, which have to archive before it can.\", fix: \"The step IS the action: run its `command`, `loam dependencies <FEAT-id> --json`.\" },\n \"next.fix-coherence\": { severityNote: \"step\", meaning: \"The feature has findings that stop it, named starting with the first. Everything EITHER gate refuses on is counted, so a warning marked as gating archive is in the tally even though `validate` grades the feature valid — which is why its exit code alone will not show you why archive refuses.\", fix: \"The step IS the action: run its `command`, `loam validate <FEAT-id> --json`.\" },\n \"next.generate-tests\": { severityNote: \"step\", meaning: \"The feature's delta for a service carries scenarios no test run has answered yet. `loam gherkin` writes the .feature files whose green run is the only thing that may answer a `scenario.tested` claim, and it writes into the service's own repository — so the step names which repo to run it in when that is not this one.\", fix: \"The step IS the action: run its `command`, `loam gherkin <FEAT-id> --service <id>`, from that service's own checkout.\" },\n \"next.verify\": { severityNote: \"step\", meaning: \"The done-check has not been started, or its record cannot be used. Five situations share this code because they share the work — nothing recorded, a verification.yaml that does not read as a record, a record the feature has moved out from under, a record that answers no claims at all, and a feature with nothing to derive a checklist from yet — and the step's own statement says which.\", fix: \"The step IS the action: run its `command`, `loam verify <FEAT-id> --json`, which derives the checklist and prints what it asks. A record nothing can read is repaired or deleted by hand: nothing overwrites it while it is unreadable.\" },\n \"next.verify-unconfirmed\": { severityNote: \"step\", meaning: \"Claims on the feature are open — unconfirmed or unanswered — and this repository is not one of the services that owes an answer, so the recording form is not a step anyone can take from here.\", fix: \"The step IS the action: run its `command`, `loam verify <FEAT-id> --json`. The open claims are closed from each affected service's own repository.\" },\n \"next.attest-service\": { severityNote: \"step\", meaning: \"Claims are open AND this repository is one of the services that owes an answer, so the recording form is nameable here: `--record --service` binds the answers to this repo's HEAD and refuses anywhere else.\", fix: \"The step IS the action: run its `command`, `loam verify <FEAT-id> --record answers.json --service <id>`, answering the claims filed under this service; the rest belong to their own repositories.\" },\n \"next.verify-attested\": { severityNote: \"step\", meaning: \"Every claim is answered and some scenario claims are confirmed on an agent's word rather than on a test run (`verify.scenario-attested`) — complete work resting on an assertion, which is what `ready` has always meant here.\", fix: \"The step IS the action: run its `command`, `loam verify <FEAT-id> --results <cucumber-report.json>`, and the record stops resting on an assertion.\" },\n },\n};\n\n/**\n * `loam diff` — what CHANGED between a base git ref of the docs repo and the\n * working tree, not what is WRONG with either. That is why most rows below have\n * no repair: the meanings are the `DiffCode` union's own doc comments\n * (core/diff/semantic.ts), and the two error rows are the only ones naming work.\n */\nconst DIFF: Family = {\n scope: \"loam diff --base <ref>\",\n codes: {\n \"diff.service-added\": { severityNote: \"ok\", meaning: \"A services/<id>/ directory exists now that did not exist at the base ref.\", fix: \"Nothing to fix: `loam diff` says what changed between two points of the living docs, never what is wrong with them. This is the class of the change, for a reviewer to read.\" },\n \"diff.service-removed\": { severityNote: \"warn\", meaning: \"A service present at the base ref is gone from the working tree; the finding names the directory it occupied.\", fix: \"A review question rather than a defect: nothing in the working tree describes that boundary any more. `loam validate --all` is what grades the fleet that is left.\" },\n \"diff.requirement-added\": { severityNote: \"ok\", meaning: \"A living requirement not present at base — by `Requirement-ID:` where one is authored, by heading otherwise, the same join `loam rebase` pins against.\", fix: \"Nothing to fix: this is the class of the change, for a reviewer to read.\" },\n \"diff.requirement-removed\": { severityNote: \"warn\", meaning: \"A requirement present at the base ref is no longer in the living spec under the same identity.\", fix: \"A review question. A requirement that CHANGED identity — a new `Requirement-ID:`, a renamed heading — reads here as a removal plus an addition, which is the join being honest about what it can see rather than guessing a rename.\" },\n \"diff.requirement-modified\": { severityNote: \"ok\", meaning: \"The same requirement identity is on both sides and its digest moved. A rebase pin never moves that digest, so this is a real edit to the requirement's text.\", fix: \"Nothing to fix: this is the class of the change, for a reviewer to read.\" },\n \"diff.op-added\": { severityNote: \"ok\", meaning: \"An operationId in the living contract that was absent at the base ref; the message names its method and path.\", fix: \"Nothing to fix: this is the class of the change, for a reviewer to read.\" },\n \"diff.op-removed\": { severityNote: \"warn\", meaning: \"A base operationId is gone from the living contract and no current landscape edge or living requirement names it — or, in the second wording this code also carries, whether one does could NOT be fully answered because some consumer documents were not scannable, and the details name them.\", fix: \"Read which of the two sentences it is. Nobody could be scanned is never nobody names it: the unreadable documents carry the run to exit 1 on their own, and the removal stays unreviewed until they can be read.\" },\n \"diff.op-removed-consumed\": { severityNote: \"error\", meaning: \"An operation was removed since the base ref and the current fleet still names it — a landscape edge, or a foreign service's living requirement. The details list the victims, and the finding sets `breaking`.\", fix: \"These joins break. Either restore the operation, or land the consumers' own deltas in the same change so nothing in the fleet is left calling something that is gone.\" },\n \"diff.op-deprecated\": { severityNote: \"warn\", meaning: \"`deprecated: true` was introduced on an operation since the base ref. The details name the current consumers, under one of three deliberately honest phrasings: some still name it, none does, or whether any does could not be fully answered.\", fix: \"Deprecation is a promise to the consumers the details name, and nothing is broken yet. Read which phrasing it is before treating a silent list as an empty one.\" },\n \"diff.message-added\": { severityNote: \"ok\", meaning: \"An AsyncAPI message name absent at the base ref.\", fix: \"Nothing to fix: this is the class of the change, for a reviewer to read.\" },\n \"diff.message-removed\": { severityNote: \"warn\", meaning: \"A base message is gone. The finding says which of three cases it is: another current service still declares the name, nothing currently consumes it, or whether anything does could not be fully answered because some consumer documents were not scannable.\", fix: \"Read which case it is. A name another service still declares is not orphaned, and an unanswerable scan is not an empty one — only positive evidence vouches here.\" },\n \"diff.message-removed-consumed\": { severityNote: \"error\", meaning: \"A message was removed since the base ref and the current fleet still consumes it — a `Consumes:` line, or a consumes edge. The details list the victims, and the finding sets `breaking`.\", fix: \"These joins break. Either restore the message, or land the consumers' own deltas in the same change.\" },\n \"diff.consumer-added\": { severityNote: \"ok\", meaning: \"A cross-service join appeared since base: another service's living requirements now name this service's operation or message. Filed on the PROVIDER — the service whose surface the join lands on.\", fix: \"Nothing to fix, and the change class most worth reading: a new consumer is a new obligation on this service's contract, and removing that operation later is what `diff.op-removed-consumed` will be about.\" },\n \"diff.consumer-removed\": { severityNote: \"ok\", meaning: \"A cross-service join present at the base ref is gone: a consumer's living requirements no longer name this service's operation or message.\", fix: \"Nothing to fix: this is the class of the change, for a reviewer to read.\" },\n },\n};\n\n/**\n * `loam gate` — the can-i-deploy question, a pure query over evidence other\n * commands already recorded. Nothing here re-derives a coherence plan, so every\n * repair below is somebody else's command.\n */\nconst GATE: Family = {\n scope: \"loam gate\",\n codes: {\n \"gate.partners-unknown\": { severityNote: \"warn when the map is absent; error when it exists and cannot be used\", meaning: \"The partner set for the gated service could not be derived from architecture/landscape.likec4 — it is absent, or it exists and does not parse. That means nobody could look, never that nothing joins this service. Absent is a warning because a repo before its first adopt legitimately has no map yet; a broken map is an error because `loam validate --all` fails that repo for the same file, and a deploy gate passing where the fleet gate fails would be the quieter of two contradictory verdicts.\", fix: \"Fix the landscape and re-run. `loam validate --all` diagnoses the file itself; this check only refuses the false silence.\" },\n \"gate.service-undocumented\": { severityNote: \"error\", meaning: \"The service being deployed sits below `documented` on the adoption ladder, so the docs cannot say what its joins even are and no recorded evidence can answer the deploy question. The details list what the next rung wants.\", fix: \"Author the baseline — `loam adopt --service <id>` briefs exactly what is missing — and this check passes once the required artifact set exists.\" },\n \"gate.partner-undocumented\": { severityNote: \"warn\", meaning: \"A service that joins the gated one has no services/<id>/ directory at all, or has one sitting below `documented`: its side of the join is not recorded, so the gate cannot see what that partner promises. An `#external` partner is exempt on purpose — somebody else's system is unrecorded by design.\", fix: \"Adopt the partner (`loam adopt --service <id>` briefs it), bind its landscape element to the right service with `metadata { service '<id>' }`, or tag it `#external` if it is not ours.\" },\n \"gate.feature-unverified\": { severityNote: \"warn\", meaning: \"An active feature touching the gated service is not verified: no record at all, a verification.yaml that does not read as a record, a record the feature moved out from under, a record answering no claims, or open claims counted in the message. A record that is merely `attested` is NOT reported under this code — it carries `verify.scenario-attested` instead, because complete work resting on an assertion is a different fact from work nobody checked.\", fix: \"Derive the checklist and answer every claim with evidence: `loam verify <FEAT-id> --json`. A record nothing can read is repaired or deleted by hand; nothing overwrites it while it is unreadable.\" },\n },\n};\n\n/** In listing order, though every consumer sorts by code — the order is documentation, not contract. */\nconst FAMILIES: readonly Family[] = [DOCTOR, NEXT_STEPS, DIFF, GATE];\n\n/** One family row, in the shape `FindingEntry` (lookup.ts) takes. */\nexport interface FamilyFinding extends FamilyEntry {\n readonly scope: string;\n}\n\n/**\n * The registry's answer for one subject, or null.\n *\n * Built per call over constants already in memory, never cached at module\n * level, for the reason lookup.ts's header gives: tests fork and chdir, and the\n * MCP server outlives many calls.\n */\nexport function familyFinding(subject: string): FamilyFinding | null {\n for (const family of FAMILIES) {\n const entry = family.codes[subject];\n if (entry !== undefined) return { scope: family.scope, ...entry };\n }\n return null;\n}\n\n/** Every code the registry answers, sorted — the same plain sort the code inventory uses. */\nexport function familyCodes(): string[] {\n return FAMILIES.flatMap((family) => Object.keys(family.codes)).sort();\n}\n"]}
|
|
1
|
+
{"version":3,"file":"families.js","sourceRoot":"","sources":["../../../src/core/explain/families.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * The family registry: `loam explain`'s content for the four code families no\n * fix table anywhere grades — `doctor.*`, `next.*`, `diff.*` and `gate.*`.\n *\n * WHY THIS IS NOT THE DUPLICATION fix-tables.ts FORBIDS. That module's header\n * argues against a second hand-maintained copy of the /loam-check prose, and\n * the argument is right: the tables ship in the binary, test/codes-drift.test.ts\n * holds every emitted finding code against them, and a registry restating them\n * would be free to trail the bytes `loam instructions loam-check` prints. None\n * of that applies here. These 65 codes have no fix-table row in any workflow\n * body — /loam-check grades DOCUMENTS, and these four families grade a\n * repository's wiring (`doctor`), a recommendation (`next`), a change between\n * two refs (`diff`) and a deploy question (`gate`). There is nothing to trail:\n * this registry is not a second copy, it is the only copy. The precedent is the\n * sibling module refusals.ts, a hand-written map that is the sole source for\n * the `ErrorCode` meanings for exactly the same reason — their prose lives in\n * doc comments, which are erased, so there is no runtime constant to read.\n *\n * What that costs, stated rather than hidden: nothing mechanical proves an\n * entry below still matches the message its emitter prints. Every meaning and\n * every fix here was written FROM the emitter — `doctor` findings carry a `fix`\n * field of their own (core/doctor/), a `next` step carries a `statement` and a\n * literal `command` (core/status/), the `DiffCode` union carries a doc comment\n * per member (core/diff/semantic.ts) — and the wording is theirs, not a\n * paraphrase invented here. Two guards catch the two failures that CAN be\n * caught: test/explain.test.ts asserts no code below is also answered by a fix\n * table (the two sources must never be able to disagree), and\n * test/agent-commands-runnable.test.ts parses every backticked `loam …` in this\n * file against the real program, so a fix naming a command loam does not have —\n * or a flag it does not declare — fails at gate time. A two-token span is a\n * NAME rather than an invocation there and is only checked for the command\n * existing, so a fix that means an invocation must carry its arguments.\n *\n * `severityNote` is the EMITTER's own word, not a fix table's parenthetical:\n * `doctor` grades blocker/warning, `diff` and `gate` grade ok/warn/error, and a\n * `next` step is graded not at all — it is a recommendation, so its note is\n * `step`. Where a code's severity depends on what was found (`doctor.docs-locked`,\n * `gate.partners-unknown`) the note says so in prose, the way the tables' own\n * long notes do (`warn here; error at archive plan time`).\n *\n * `openspec.*` and `mapping.*` — 49 codes — are deliberately NOT here, and the\n * reason is in test/explain.test.ts beside their backlog entries: they are the\n * only codes in the vocabulary whose subject is not a loam document at all.\n */\n\n/**\n * One code's answer. Structurally a `FindingEntry` (lookup.ts) minus `scope`,\n * which the family supplies — declared here rather than imported so this module\n * imports nothing at all and the package's file-level graph stays a tree.\n *\n * Every entry below is written on ONE line, however long that line runs, and\n * that is a deliberate concession to a counted limit rather than a style: 65\n * codes at four lines each is 260 lines of punctuation before a word of prose,\n * which does not fit the 400-line file limit — and src/core/explain/ is at its\n * five-file cap, so there is no second file to put the overflow in. The fields\n * stay named for the same reason a tuple was refused: `severityNote`, `meaning`\n * and `fix` say which column is which without counting commas.\n */\nexport interface FamilyEntry {\n /** The emitter's own severity word, or the prose a conditional one needs. */\n readonly severityNote: string;\n /** What the code is about, in the words the emitter already prints. */\n readonly meaning: string;\n /** What to do — for `next.*`, that the step's own `command` IS the action. */\n readonly fix: string;\n}\n\n/** One family: the invocation that emits it, and its codes. */\ninterface Family {\n /** The scope label, spelled as the fix tables spell theirs — the invocation that grades it. */\n readonly scope: string;\n readonly codes: Readonly<Record<string, FamilyEntry>>;\n}\n\n/**\n * `loam doctor` — the preflight. Every finding here already carries a `fix`\n * field in doctor's own report (core/doctor/doctor.ts, config.ts, agents.ts,\n * residue.ts); these rows say the same thing without a repository in front of\n * them, which is the whole difference between a report and a lookup.\n */\nconst DOCTOR: Family = {\n scope: \"loam doctor\",\n codes: {\n \"doctor.config-missing\": { severityNote: \"blocker\", meaning: \"No loam.json at or above the working directory, so nothing says which docs repo this checkout belongs to — every other command refuses with `no-config` until there is one.\", fix: \"Run `loam init --docs <dir>` in this repository; add `--create` when the docs repo does not exist yet.\" },\n \"doctor.config-invalid\": { severityNote: \"blocker\", meaning: \"A loam.json is there and `parseConfig` — the same loader every command uses — refuses it; the finding carries the parser's own words. doctor deliberately has no second validator: the one it used to carry accepted a config the commands rejected, and reported a healthy repo in which nothing could run.\", fix: \"Repair the file the finding names, or delete it and re-run `loam init`.\" },\n \"doctor.docs-missing\": { severityNote: \"blocker\", meaning: \"`docsDir` resolves to something that is missing or is not a directory — the docs repo was never cloned, or the path is wrong.\", fix: \"Fix `docsDir` in the loam.json the finding names, clone the docs repo to that path, or run `loam init --docs <dir> --create`.\" },\n \"doctor.services-missing\": { severityNote: \"blocker\", meaning: \"`docsDir` is a directory and holds no `services/`, so it is some other directory — most often the service repo itself, after a typo in the pointer.\", fix: \"Point `docsDir` at the shared docs repo, or run `loam init --docs <dir> --create` to scaffold one. Do not scaffold a second source of truth beside the real one.\" },\n \"doctor.docs-absolute\": { severityNote: \"warning\", meaning: \"`docsDir` is stored as an absolute path and loam.json is committed, so the pointer names a directory that exists on exactly one machine: it works for whoever ran `loam init` and for nobody who clones the repo afterwards.\", fix: \"Rewrite `docsDir` as a path relative to the loam.json holding it, such as `../docs`.\" },\n \"doctor.docs-unreadable\": { severityNote: \"warning\", meaning: \"The docs repo exists and this process cannot read it, so every command that reads the fleet will fail or report an empty one.\", fix: \"Grant read access to the directory the finding names — check ownership and mode.\" },\n \"doctor.docs-readonly\": { severityNote: \"warning\", meaning: \"The docs repo is readable and not writable: read-only commands work and the writing ones do not.\", fix: \"Grant write access if you need `loam new`, `loam adopt` or `loam archive`. A read-only checkout is a legitimate state for a reviewer, which is why this is a warning.\" },\n \"doctor.landscape-missing\": { severityNote: \"warning\", meaning: \"architecture/landscape.likec4 is missing, and it is the one artifact every cross-service check reads — nothing about who calls whom can be checked without it.\", fix: \"Create it with one element per service and an edge per call: `loam init --create` writes the empty map, and `loam seed --from fleet.yaml` templates one from stated facts.\" },\n \"doctor.landscape-unreadable\": { severityNote: \"blocker\", meaning: \"The landscape file exists and the read itself failed — permissions or an I/O error, carried verbatim in the message.\", fix: \"Check permissions on the path the finding names.\" },\n \"doctor.landscape-merge-conflict\": { severityNote: \"blocker\", meaning: \"The landscape still contains merge conflict markers, named by line. Checked BEFORE the parser on purpose: both sides of a conflict can be syntactically valid LikeC4, so the file sometimes parses and is still two halves of two different maps.\", fix: \"Resolve the conflict keeping BOTH services' elements and edges — the usual cause is several services adopted into one map in the same week — then re-run `loam doctor`.\" },\n \"doctor.landscape-invalid\": { severityNote: \"blocker\", meaning: \"The landscape exists, carries no conflict markers, and does not parse; the message quotes the first parser error with its line and counts the rest.\", fix: \"Fix the file — every fleet-wide check is blind until it parses.\" },\n \"doctor.likec4-config-missing\": { severityNote: \"warning\", meaning: \"The docs repo has no likec4.config.json, so the tree is not a loadable LikeC4 workspace: every service model and feature delta declares its own `specification` block, and a renderer pointed at the repo root merges them and reports every declaration as a duplicate. No loam check reads this file — the repo is only unrenderable, which is why it is a warning.\", fix: \"Write the project file the finding prints; it scopes the root project to architecture/, so `npx likec4 start` in the docs repo renders the fleet map. Then run `loam subsystem sync`: it writes one create-only likec4.config.json beside every service model, so each model renders from that same root as a project of its own — only a feature delta still renders from its own directory.\" },\n \"doctor.docs-locked\": { severityNote: \"warning while a live process holds it; blocker when its holder is dead or it names none\", meaning: \"A `.loam-lock` is present. Held by a running process it is a fact about right now. Held by a process that no longer exists on this host — or unreadable, so no holder can be named at all — nothing is ever going to release it, and every writing command refuses with `docs-busy` forever.\", fix: \"Wait and re-run while the holder is alive; nothing is read or written while it is held. Delete the lock file the finding names when its holder is dead or unnameable, then run `loam doctor` again for an interrupted commit underneath it.\" },\n \"doctor.commit-unreadable\": { severityNote: \"blocker\", meaning: \"A `.loam-commit` journal is present and cannot be read: a commit was interrupted and the one record of which files it had already written is unreadable.\", fix: \"Hand this to a human — compare the root's files against version control before running any loam command that writes there. Every journaled writer refuses with `commit-interrupted` while it is present.\" },\n \"doctor.commit-interrupted\": { severityNote: \"blocker\", meaning: \"A journaled writer — archive, unarchive, rebase, vouch, new, gherkin, or a recording verify — was killed mid-commit. The message names the host, pid and time, and every file or directory move that may be half-done. This is the surface that used to report a healthy repo over half-merged living docs.\", fix: \"Re-run the command the finding names: it recovers first, under the lock, rolling the staged bytes forward, and refuses rather than guessing if a file has been edited since. An interrupted `loam unarchive` is FINISHED, never undone — the merged text it was replacing is written down nowhere.\" },\n \"doctor.staging-temps\": { severityNote: \"warning\", meaning: \"Orphaned `.loam-*.tmp` scratch files from a killed writer, never linked into place — so nothing reads them and nothing depends on them. A journal's own recorded temps are excluded, because those are the durable copy its roll-forward recovery renames in. Litter, not damage.\", fix: \"The next journaled writer through that root removes its own; delete any that remain. They cost disk and nothing else.\" },\n \"doctor.inventory-unreadable\": { severityNote: \"warning\", meaning: \"The config and the docs repo are both fine and enumerating `services/` or `features/` threw, so doctor's fleet counts — and its verdict on the service binding — are about a fleet nobody could read.\", fix: \"Check that services/ and features/ under the docs repo are readable directories.\" },\n \"doctor.service-unbound\": { severityNote: \"warning\", meaning: \"loam.json declares no `service`, so nothing binds this checkout to one governed boundary and the service-repo checks have nothing to run against — `sources.stale`, a recording `loam verify`, `loam gherkin`. Deliberately not emitted inside the docs repo itself, where having no binding is the correct state rather than a gap.\", fix: \"Run `loam init --docs <dir> --service <id>` here.\" },\n \"doctor.service-unknown\": { severityNote: \"warning\", meaning: \"loam.json names a `service` the fleet has no services/<id>/ for — and the fleet WAS read, so this is a claim about a real inventory rather than one nobody could take.\", fix: \"Run `loam adopt --service <id>` to onboard it, or fix `service` in loam.json.\" },\n \"doctor.agent-files-missing\": { severityNote: \"warning\", meaning: \"Some of the command and skill files this loam lays down for the repo's recorded tools and profile are not here: the repo was initialized by an older loam, or they were deleted. An out-of-date command set costs entry points, never a refusal, which is why it is a warning.\", fix: \"Re-run `loam init --docs <dir> --tools <ids>` here. It creates absent files, refreshes only pointers whose current bytes still match their recorded digest, preserves every customized file, and keeps this repo's service binding.\" },\n \"doctor.agent-files-stale\": { severityNote: \"warning\", meaning: \"Command or skill files that ARE here carry no loam stamp, or one older than this binary, so the protocol and code tables they instruct an agent with may describe a loam that no longer exists. Staleness reads only the stamp; safe refresh is a separate digest check performed by init.\", fix: \"Re-run `loam init` to refresh a pointer only when its recorded digest still matches. Review a customized file against the current body yourself and update its stamp, or delete it so init can recreate it; your edits always outrank the template.\" },\n },\n};\n\n/**\n * `loam status` — both forms print `next[]`, and each entry carries a `code` to\n * branch on, one sentence, and the literal `command` to run. So the \"what to\n * do\" column is genuinely empty for this whole family: the step IS the action,\n * and each `fix` below names the step's own command rather than inventing a\n * second instruction beside it.\n */\nconst NEXT_STEPS: Family = {\n scope: \"loam status\",\n codes: {\n \"next.adopt-bound\": { severityNote: \"step\", meaning: \"The fleet form's first entry: this repository's loam.json says it is a service the fleet has no services/<id>/ for, so nothing about the boundary you are standing in is written down yet. Ahead of every other service's adoption work, because it is the only step that is about HERE.\", fix: \"The step IS the action: run its `command`, `loam adopt --service <id> --json`.\" },\n \"next.author-landscape\": { severityNote: \"step\", meaning: \"The first rung of the first-hour ladder, over a fleet with no services and no features: architecture/landscape.likec4 does not exist, or is still the scaffold's untouched map with no service drawn on it.\", fix: \"The step IS the action: run its `command` — `loam init --create` when the file is absent, `loam seed --from fleet.yaml` when it is the untouched scaffold. loam never guesses who calls whom.\" },\n \"next.bind-service\": { severityNote: \"step\", meaning: \"First-hour ladder: no service repository is bound to this fleet yet, so no command can ever run from a service's own checkout.\", fix: \"The step IS the action: run its `command`, `loam init --docs <path-to-this-docs-repo> --service <service-id>`, from a service's own repo, and commit the loam.json it writes.\" },\n \"next.adopt-first\": { severityNote: \"step\", meaning: \"First-hour ladder: services/ is empty, so nothing about any service is written down and nothing can be graded against anything.\", fix: \"The step IS the action: run its `command`, `loam adopt --service <service-id> --json`; the brief it prints walks an agent through writing the baseline docs as draft.\" },\n \"next.adopt\": { severityNote: \"step\", meaning: \"One service in the fleet has no spec.md — nothing about it is written down, so no feature can be graded against it. One entry per undocumented service.\", fix: \"The step IS the action: run its `command`, `loam adopt --service <id> --json`.\" },\n \"next.complete-service\": { severityNote: \"step\", meaning: \"Adoption does not end at spec.md: a service has one and no model.likec4, the state archive warns about and the fleet gate reports incomplete until something models it.\", fix: \"The step IS the action: run its `command`, `loam validate --service <id> --json`.\" },\n \"next.feature\": { severityNote: \"step\", meaning: \"One feature in flight that is not ready to ship, named with the stage it sits at and what it is missing — or, when it is blocked by another feature, the features it is waiting on.\", fix: \"The step IS the action: run its `command`, `loam status <FEAT-id> --json`, for that feature's own next list.\" },\n \"next.archive\": { severityNote: \"step\", meaning: \"In the fleet form, a feature that is authored and verified: ship it, and everything queued behind it is released. In a feature's own list it is always the last entry, saying the feature ships once everything above it is done.\", fix: \"The step IS the action: run its `command`, `loam archive <FEAT-id> --dry-run --json`; the dry run writes nothing and lists every file the merge would touch.\" },\n \"next.elided\": { severityNote: \"step\", meaning: \"The fleet list is capped, and this says how many steps of the same kinds it left out. The list is ordered most-unblocking first, so what was elided is the least urgent — a list nobody reads is not navigation.\", fix: \"The step IS the action: run its `command`, `loam list --json`, or work down the printed list and run status again.\" },\n \"next.fleet-clean\": { severityNote: \"step\", meaning: \"Services exist, nothing is in flight, and nothing is owed. Only that truthful case: over an EMPTY fleet the same sentence was vacuously true, and the first-hour ladder owns that repository now.\", fix: \"The step IS the action: run its `command`, `loam validate --all --json`, then start the next feature.\" },\n \"next.fleet-gate\": { severityNote: \"step\", meaning: \"The last entry of every fleet list, and always present: `status` grades nothing, so the least it can do is say where grading happens — `loam validate --all` is the gate this repository has to pass and the one CI runs.\", fix: \"The step IS the action: run its `command`, `loam validate --all --json`.\" },\n \"next.recover-commit\": { severityNote: \"step\", meaning: \"Always first, and never elided by the fleet form's cap: a journaled writer was killed mid-commit, so everything below this step is derived from files that may be half-written. Where the journal itself cannot be read, nothing below can be trusted until a human compares the living docs against version control.\", fix: \"The step IS the action: run its `command` — the interrupted command re-run, which recovers first under the lock, or `loam doctor --json` when the journal is unreadable and no loam command can repair it.\" },\n \"next.archived\": { severityNote: \"step\", meaning: \"The whole `next[]` for a feature that already shipped: it is archived, and its delta is folded into the living docs.\", fix: \"The step IS the action: run its `command`, `loam show <FEAT-id> --json`.\" },\n \"next.author-intent\": { severityNote: \"step\", meaning: \"The feature has no intent document — what it is for, in prose, before anything is derived from it.\", fix: \"The step IS the action: write the file the step names, then run its `command`, `loam validate <FEAT-id> --json`. The command never writes the artifact: loam does not author.\" },\n \"next.touch-service\": { severityNote: \"step\", meaning: \"The feature carries no per-service delta at all, so nothing says which governed boundaries it changes.\", fix: \"The step IS the action: run its `command`, `loam list services --json`, then add specs/<service>/spec.md for every service the feature changes.\" },\n \"next.author-spec\": { severityNote: \"step\", meaning: \"One service the feature touches has no requirement delta — the ADDED / MODIFIED / REMOVED requirements the feature makes to that service, each with a scenario.\", fix: \"The step IS the action: write the file the step names, then run its `command`, `loam delta <FEAT-id> --service <id> --json`, which projects the delta onto the living document.\" },\n \"next.author-openapi\": { severityNote: \"step\", meaning: \"Nothing gives one of the feature's services a living openapi.yaml, so this feature is the only thing that can write down its API surface. Deliberately not phrased as the service being absent from the living docs: a service adopted without a contract is the commoner case, and that wording sent readers looking for a directory that was right there.\", fix: \"The step IS the action: write the file the step names, then run its `command`, `loam delta <FEAT-id> --service <id> --json`.\" },\n \"next.author-scenarios\": { severityNote: \"step\", meaning: \"Requirements in one service's delta carry no `#### Scenario:`, so they generate no test and no verify claim — a promise that reaches neither the suite nor the done-check. One entry per service and never per requirement, because a delta with forty bare requirements would otherwise BE the payload.\", fix: \"The step IS the action: write a scenario under each bare requirement, then run its `command`, `loam delta <FEAT-id> --service <id> --json`.\" },\n \"next.rebase\": { severityNote: \"step\", meaning: \"The feature carries requirements or operations with no baseline pin, on any of the three axes; until they have one the merge cannot tell what it EDITS from what it merely quotes.\", fix: \"The step IS the action: run its `command`, `loam rebase <FEAT-id>`.\" },\n \"next.archive-first\": { severityNote: \"step\", meaning: \"The feature builds on other features still in flight, which have to archive before it can.\", fix: \"The step IS the action: run its `command`, `loam dependencies <FEAT-id> --json`.\" },\n \"next.fix-coherence\": { severityNote: \"step\", meaning: \"The feature has findings that stop it, named starting with the first. Everything EITHER gate refuses on is counted, so a warning marked as gating archive is in the tally even though `validate` grades the feature valid — which is why its exit code alone will not show you why archive refuses.\", fix: \"The step IS the action: run its `command`, `loam validate <FEAT-id> --json`.\" },\n \"next.generate-tests\": { severityNote: \"step\", meaning: \"The feature's delta for a service carries scenarios no test run has answered yet. `loam gherkin` writes the .feature files whose green run is the only thing that may answer a `scenario.tested` claim, and it writes into the service's own repository — so the step names which repo to run it in when that is not this one.\", fix: \"The step IS the action: run its `command`, `loam gherkin <FEAT-id> --service <id>`, from that service's own checkout.\" },\n \"next.verify\": { severityNote: \"step\", meaning: \"The done-check has not been started, or its record cannot be used. Five situations share this code because they share the work — nothing recorded, a verification.yaml that does not read as a record, a record the feature has moved out from under, a record that answers no claims at all, and a feature with nothing to derive a checklist from yet — and the step's own statement says which.\", fix: \"The step IS the action: run its `command`, `loam verify <FEAT-id> --json`, which derives the checklist and prints what it asks. A record nothing can read is repaired or deleted by hand: nothing overwrites it while it is unreadable.\" },\n \"next.verify-unconfirmed\": { severityNote: \"step\", meaning: \"Claims on the feature are open — unconfirmed or unanswered — and this repository is not one of the services that owes an answer, so the recording form is not a step anyone can take from here.\", fix: \"The step IS the action: run its `command`, `loam verify <FEAT-id> --json`. The open claims are closed from each affected service's own repository.\" },\n \"next.attest-service\": { severityNote: \"step\", meaning: \"Claims are open AND this repository is one of the services that owes an answer, so the recording form is nameable here: `--record --service` binds the answers to this repo's HEAD and refuses anywhere else.\", fix: \"The step IS the action: run its `command`, `loam verify <FEAT-id> --record answers.json --service <id>`, answering the claims filed under this service; the rest belong to their own repositories.\" },\n \"next.verify-attested\": { severityNote: \"step\", meaning: \"Every claim is answered and some scenario claims are confirmed on an agent's word rather than on a test run (`verify.scenario-attested`) — complete work resting on an assertion, which is what `ready` has always meant here.\", fix: \"The step IS the action: run its `command`, `loam verify <FEAT-id> --results <cucumber-report.json>`, and the record stops resting on an assertion.\" },\n },\n};\n\n/**\n * `loam diff` — what CHANGED between a base git ref of the docs repo and the\n * working tree, not what is WRONG with either. That is why most rows below have\n * no repair: the meanings are the `DiffCode` union's own doc comments\n * (core/diff/semantic.ts), and the two error rows are the only ones naming work.\n */\nconst DIFF: Family = {\n scope: \"loam diff --base <ref>\",\n codes: {\n \"diff.service-added\": { severityNote: \"ok\", meaning: \"A services/<id>/ directory exists now that did not exist at the base ref.\", fix: \"Nothing to fix: `loam diff` says what changed between two points of the living docs, never what is wrong with them. This is the class of the change, for a reviewer to read.\" },\n \"diff.service-removed\": { severityNote: \"warn\", meaning: \"A service present at the base ref is gone from the working tree; the finding names the directory it occupied.\", fix: \"A review question rather than a defect: nothing in the working tree describes that boundary any more. `loam validate --all` is what grades the fleet that is left.\" },\n \"diff.requirement-added\": { severityNote: \"ok\", meaning: \"A living requirement not present at base — by `Requirement-ID:` where one is authored, by heading otherwise, the same join `loam rebase` pins against.\", fix: \"Nothing to fix: this is the class of the change, for a reviewer to read.\" },\n \"diff.requirement-removed\": { severityNote: \"warn\", meaning: \"A requirement present at the base ref is no longer in the living spec under the same identity.\", fix: \"A review question. A requirement that CHANGED identity — a new `Requirement-ID:`, a renamed heading — reads here as a removal plus an addition, which is the join being honest about what it can see rather than guessing a rename.\" },\n \"diff.requirement-modified\": { severityNote: \"ok\", meaning: \"The same requirement identity is on both sides and its digest moved. A rebase pin never moves that digest, so this is a real edit to the requirement's text.\", fix: \"Nothing to fix: this is the class of the change, for a reviewer to read.\" },\n \"diff.op-added\": { severityNote: \"ok\", meaning: \"An operationId in the living contract that was absent at the base ref; the message names its method and path.\", fix: \"Nothing to fix: this is the class of the change, for a reviewer to read.\" },\n \"diff.op-removed\": { severityNote: \"warn\", meaning: \"A base operationId is gone from the living contract and no current landscape edge or living requirement names it — or, in the second wording this code also carries, whether one does could NOT be fully answered because some consumer documents were not scannable, and the details name them.\", fix: \"Read which of the two sentences it is. Nobody could be scanned is never nobody names it: the unreadable documents carry the run to exit 1 on their own, and the removal stays unreviewed until they can be read.\" },\n \"diff.op-removed-consumed\": { severityNote: \"error\", meaning: \"An operation was removed since the base ref and the current fleet still names it — a landscape edge, or a foreign service's living requirement. The details list the victims, and the finding sets `breaking`.\", fix: \"These joins break. Either restore the operation, or land the consumers' own deltas in the same change so nothing in the fleet is left calling something that is gone.\" },\n \"diff.op-deprecated\": { severityNote: \"warn\", meaning: \"`deprecated: true` was introduced on an operation since the base ref. The details name the current consumers, under one of three deliberately honest phrasings: some still name it, none does, or whether any does could not be fully answered.\", fix: \"Deprecation is a promise to the consumers the details name, and nothing is broken yet. Read which phrasing it is before treating a silent list as an empty one.\" },\n \"diff.message-added\": { severityNote: \"ok\", meaning: \"An AsyncAPI message name absent at the base ref.\", fix: \"Nothing to fix: this is the class of the change, for a reviewer to read.\" },\n \"diff.message-removed\": { severityNote: \"warn\", meaning: \"A base message is gone. The finding says which of three cases it is: another current service still declares the name, nothing currently consumes it, or whether anything does could not be fully answered because some consumer documents were not scannable.\", fix: \"Read which case it is. A name another service still declares is not orphaned, and an unanswerable scan is not an empty one — only positive evidence vouches here.\" },\n \"diff.message-removed-consumed\": { severityNote: \"error\", meaning: \"A message was removed since the base ref and the current fleet still consumes it — a `Consumes:` line, or a consumes edge. The details list the victims, and the finding sets `breaking`.\", fix: \"These joins break. Either restore the message, or land the consumers' own deltas in the same change.\" },\n \"diff.consumer-added\": { severityNote: \"ok\", meaning: \"A cross-service join appeared since base: another service's living requirements now name this service's operation or message. Filed on the PROVIDER — the service whose surface the join lands on.\", fix: \"Nothing to fix, and the change class most worth reading: a new consumer is a new obligation on this service's contract, and removing that operation later is what `diff.op-removed-consumed` will be about.\" },\n \"diff.consumer-removed\": { severityNote: \"ok\", meaning: \"A cross-service join present at the base ref is gone: a consumer's living requirements no longer name this service's operation or message.\", fix: \"Nothing to fix: this is the class of the change, for a reviewer to read.\" },\n },\n};\n\n/**\n * `loam gate` — the can-i-deploy question, a pure query over evidence other\n * commands already recorded. Nothing here re-derives a coherence plan, so every\n * repair below is somebody else's command.\n */\nconst GATE: Family = {\n scope: \"loam gate\",\n codes: {\n \"gate.partners-unknown\": { severityNote: \"warn when the map is absent; error when it exists and cannot be used\", meaning: \"The partner set for the gated service could not be derived from architecture/landscape.likec4 — it is absent, or it exists and does not parse. That means nobody could look, never that nothing joins this service. Absent is a warning because a repo before its first adopt legitimately has no map yet; a broken map is an error because `loam validate --all` fails that repo for the same file, and a deploy gate passing where the fleet gate fails would be the quieter of two contradictory verdicts.\", fix: \"Fix the landscape and re-run. `loam validate --all` diagnoses the file itself; this check only refuses the false silence.\" },\n \"gate.service-undocumented\": { severityNote: \"error\", meaning: \"The service being deployed sits below `documented` on the adoption ladder, so the docs cannot say what its joins even are and no recorded evidence can answer the deploy question. The details list what the next rung wants.\", fix: \"Author the baseline — `loam adopt --service <id>` briefs exactly what is missing — and this check passes once the required artifact set exists.\" },\n \"gate.partner-undocumented\": { severityNote: \"warn\", meaning: \"A service that joins the gated one has no services/<id>/ directory at all, or has one sitting below `documented`: its side of the join is not recorded, so the gate cannot see what that partner promises. An `#external` partner is exempt on purpose — somebody else's system is unrecorded by design.\", fix: \"Adopt the partner (`loam adopt --service <id>` briefs it), bind its landscape element to the right service with `metadata { service '<id>' }`, or tag it `#external` if it is not ours.\" },\n \"gate.feature-unverified\": { severityNote: \"warn\", meaning: \"An active feature touching the gated service is not verified: no record at all, a verification.yaml that does not read as a record, a record the feature moved out from under, a record answering no claims, or open claims counted in the message. A record that is merely `attested` is NOT reported under this code — it carries `verify.scenario-attested` instead, because complete work resting on an assertion is a different fact from work nobody checked.\", fix: \"Derive the checklist and answer every claim with evidence: `loam verify <FEAT-id> --json`. A record nothing can read is repaired or deleted by hand; nothing overwrites it while it is unreadable.\" },\n },\n};\n\n/** In listing order, though every consumer sorts by code — the order is documentation, not contract. */\nconst FAMILIES: readonly Family[] = [DOCTOR, NEXT_STEPS, DIFF, GATE];\n\n/** One family row, in the shape `FindingEntry` (lookup.ts) takes. */\nexport interface FamilyFinding extends FamilyEntry {\n readonly scope: string;\n}\n\n/**\n * The registry's answer for one subject, or null.\n *\n * Built per call over constants already in memory, never cached at module\n * level, for the reason lookup.ts's header gives: tests fork and chdir, and the\n * MCP server outlives many calls.\n */\nexport function familyFinding(subject: string): FamilyFinding | null {\n for (const family of FAMILIES) {\n const entry = family.codes[subject];\n if (entry !== undefined) return { scope: family.scope, ...entry };\n }\n return null;\n}\n\n/** Every code the registry answers, sorted — the same plain sort the code inventory uses. */\nexport function familyCodes(): string[] {\n return FAMILIES.flatMap((family) => Object.keys(family.codes)).sort();\n}\n"]}
|
package/dist/core/repo/paths.js
CHANGED
|
@@ -186,7 +186,7 @@ export function fleetAdrsDir(docsDir) {
|
|
|
186
186
|
* The GENERATED subsystem views — beside the fleet map because the LikeC4
|
|
187
187
|
* renderer merges the whole `architecture/` project, and a view can only
|
|
188
188
|
* `include` elements the landscape beside it defines. `loam subsystem sync`
|
|
189
|
-
* owns the bytes (`core/repo/tree/views.ts` renders them); nothing in loam
|
|
189
|
+
* owns the bytes (`core/repo/tree/render/views.ts` renders them); nothing in loam
|
|
190
190
|
* ever parses the file, and staleness against the tree is a byte compare.
|
|
191
191
|
*/
|
|
192
192
|
export function subsystemViewsPath(docsDir) {
|
|
@@ -274,4 +274,42 @@ export function agentsPath(docsDir) {
|
|
|
274
274
|
export function archiveDir(docsDir) {
|
|
275
275
|
return join(featuresDir(docsDir), ARCHIVE_DIR);
|
|
276
276
|
}
|
|
277
|
+
/**
|
|
278
|
+
* The LikeC4 project file — the one thing in a docs repo loam writes for a
|
|
279
|
+
* tool other than itself — and it lives at two altitudes.
|
|
280
|
+
*
|
|
281
|
+
* At the ROOT, `loam init --create` writes the project `fleet`, scoped to
|
|
282
|
+
* `architecture/` (`core/docs.ts` composes those bytes). Beside each
|
|
283
|
+
* `services/<…>/<id>/model.likec4`, `loam subsystem sync` writes a project of
|
|
284
|
+
* the service's own: loam parses every `.likec4` file ALONE, so each model
|
|
285
|
+
* declares its own `specification` block and the root project must exclude
|
|
286
|
+
* `services/**` — which left every adopted service a box on the fleet map with
|
|
287
|
+
* nothing renderable inside it, in the renderer opened at the docs root.
|
|
288
|
+
* `core/repo/tree/render/projects.ts` composes the per-service bytes and
|
|
289
|
+
* records what was measured.
|
|
290
|
+
*
|
|
291
|
+
* Spelled here and NOT in `ARTIFACT_FILES`, and the distinction is
|
|
292
|
+
* load-bearing: a filename in that table classifies a directory as a service,
|
|
293
|
+
* and this file is the renderer's — a stray one must mint nothing. The root's
|
|
294
|
+
* NAME sits beside the filename because the per-service rule needs it too (a
|
|
295
|
+
* service id equal to the root project's name would be silently renamed by
|
|
296
|
+
* the renderer), and two spellings of the word are how the scaffold and the
|
|
297
|
+
* rule drift apart.
|
|
298
|
+
*/
|
|
299
|
+
export const LIKEC4_PROJECT_FILENAME = "likec4.config.json";
|
|
300
|
+
/** The root project's name, exactly as `loam init --create` scaffolds it. */
|
|
301
|
+
export const LIKEC4_ROOT_PROJECT = "fleet";
|
|
302
|
+
export function rootProjectPath(docsDir) {
|
|
303
|
+
return join(docsDir, LIKEC4_PROJECT_FILENAME);
|
|
304
|
+
}
|
|
305
|
+
/**
|
|
306
|
+
* The two files the per-service project question is asked of — the model that
|
|
307
|
+
* needs rendering and the project file that renders it — in one spelling for
|
|
308
|
+
* the writer (`subsystem sync`) and the grader (`validate --all`) alike. Handed
|
|
309
|
+
* INTO `render/projects.ts` rather than imported there, because that package
|
|
310
|
+
* cannot import this module: `render/stale.ts` records the cycle.
|
|
311
|
+
*/
|
|
312
|
+
export function serviceRenderPaths(dir) {
|
|
313
|
+
return { model: join(dir, ARTIFACT_FILES.model), project: join(dir, LIKEC4_PROJECT_FILENAME) };
|
|
314
|
+
}
|
|
277
315
|
//# sourceMappingURL=paths.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"paths.js","sourceRoot":"","sources":["../../../src/core/repo/paths.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * Where every artifact lives. Artifact filenames are spelled here and nowhere\n * else, so a rename is one edit rather than a grep.\n *\n * This is also the module that carries loam's one path guarantee.\n * `unfiledServicePaths(docsDir, service)` spells `<docsDir>/services/<service>/`,\n * so both parts are caller-controlled path input, and `node:path` cannot help:\n * `join(...paths: string[])` accepts every string there is. The guarantee IS\n * the parameter brands and nothing else — `PathableService` for the id (a name\n * whose provenance is the repository, unconstructible from document text),\n * `DocsDir`/`FeatureDir` for the roots (a resolved config or validated\n * `--docs`; a directory an enumeration read), and `ServiceDir` for\n * `servicePathsAt` (the enumeration's own resolved directory, narrower still)\n * — the `kernel/ids/` package holds the only casts, so an unchecked value at\n * these call sites does not compile.\n * Code that spells `services/<id>/` or a feature layout with a bare join is\n * outside the guarantee — `commands/new.ts` and the openspec migrator both do,\n * held instead by `resolveInside` at the write.\n */\nimport { join } from \"node:path\";\nimport { serviceDirOf, type DocsDir, type FeatureDir, type ServiceDir } from \"../kernel/ids/dirs.js\";\nimport type { PathableService } from \"../kernel/ids/service.js\";\nimport type { SubsystemName } from \"../kernel/ids/subsystem.js\";\n\n/** Directory under features/ holding shipped features. Never a feature itself. */\nexport const ARCHIVE_DIR = \"archive\";\n\n/* ------------------------------------------------------------------ */\n/* Paths — artifact filenames are spelled here and nowhere else */\n/* ------------------------------------------------------------------ */\n\n/**\n * The living artifact filenames, as one table, because three questions read\n * them: `servicePathsAt` below spells the paths, the tree walk classifies\n * a directory as a service by whether any of these names sit in it, and\n * `core/diff/base-state.ts` classifies the SAME way over a base git ref's\n * tree listing (exported for that reader — a filename respelled there would\n * be the drift this table exists to prevent). The walk cannot import this\n * module — `repo → repo/tree` is the package direction, and an edge back\n * would be the cycle `scripts/package-graph.mjs` refuses — so `repo.ts`\n * injects `isServiceArtifactName` into the walk request instead.\n */\nexport const ARTIFACT_FILES = {\n model: \"model.likec4\",\n spec: \"spec.md\",\n archSpec: \"arch.spec.md\",\n openapi: \"openapi.yaml\",\n asyncapi: \"asyncapi.yaml\",\n runbook: \"runbook.md\",\n health: \"health.yaml\",\n steps: \"steps.yaml\",\n} as const;\nconst ADRS_DIR = \"adrs\";\n\n/** Does a directory entry name a living service artifact? The classification half of the table above. */\nexport function isServiceArtifactName(name: string, kind: \"file\" | \"dir\"): boolean {\n if (kind === \"dir\") return name === ADRS_DIR;\n return Object.values(ARTIFACT_FILES).some((file) => file === name);\n}\n\nexport interface ServicePaths {\n dir: ServiceDir;\n model: string;\n spec: string;\n archSpec: string;\n openapi: string;\n /** The async contract — the event axis's sibling of `openapi`. */\n asyncapi: string;\n runbook: string;\n health: string;\n /**\n * The step catalogue — which phrases this service's suite has DECIDED to\n * define. Authored and opt-in; its CONTENTS are read by `loam steps` alone and\n * nothing in `validate` consults them\n * (`core/gherkin/steps/catalogue.ts` records why).\n *\n * Its EXISTENCE is read more widely, and that is not the same statement: the\n * filename is in `ARTIFACT_FILES`, so `isServiceArtifactName` classifies it\n * and the tree walk treats a directory holding one as a service. That is\n * correct — it is a service artifact — and it is worth writing down, because\n * it means a `steps.yaml` written at the wrong path mints a service.\n */\n steps: string;\n adrsDir: string;\n}\n\n/**\n * The artifact paths of a service whose directory is already KNOWN — the\n * enumeration's `dir`, at whatever depth the tree walk found it. This is the\n * spelling every reader of an existing service must use: joining\n * `services/<id>/` at the root is only true for a fleet nobody has filed, and\n * a moved service would silently grade as absent through it.\n */\nexport function servicePathsAt(dir: ServiceDir): ServicePaths {\n return {\n dir,\n model: join(dir, ARTIFACT_FILES.model),\n spec: join(dir, ARTIFACT_FILES.spec),\n archSpec: join(dir, ARTIFACT_FILES.archSpec),\n openapi: join(dir, ARTIFACT_FILES.openapi),\n asyncapi: join(dir, ARTIFACT_FILES.asyncapi),\n runbook: join(dir, ARTIFACT_FILES.runbook),\n health: join(dir, ARTIFACT_FILES.health),\n steps: join(dir, ARTIFACT_FILES.steps),\n adrsDir: join(dir, ADRS_DIR),\n };\n}\n\n/**\n * The UNFILED spelling — `<docsDir>/services/<id>/`, the tree's root level.\n * This is where creation lands (`adopt`'s brief, a new service materialised by\n * `archive`, the OpenSpec migration) and the honest fallback for a service the\n * enumeration does not answer; for a service that EXISTS, resolve through the\n * enumeration instead (`servicePathsAt(entry.dir)`, or\n * `locateServicePaths` in `service-target.ts`, which chooses between the two).\n * Renamed from `servicePaths` when the tree landed, precisely so the compiler\n * would put every remaining caller in front of a reviewer: a root join that\n * survives unreviewed is a moved service silently grading as absent.\n */\nexport function unfiledServicePaths(docsDir: DocsDir, service: PathableService): ServicePaths {\n return servicePathsAt(serviceDirOf(join(docsDir, \"services\", service)));\n}\n\n/**\n * The creation spelling INSIDE a subsystem — `<subsystemDir>/<id>/`, the\n * target `adopt --subsystem` briefs so an adoption need not land unfiled and\n * cost a second command. The same two provenances the unfiled spelling\n * combines, one level down: the subsystem directory came off the tree walk's\n * readdir, and the id passed the service grammar before it reached here.\n */\nexport function servicePathsUnder(subsystemDir: string, service: PathableService): ServicePaths {\n return servicePathsAt(serviceDirOf(join(subsystemDir, service)));\n}\n\n/**\n * The directory a SUBSYSTEM occupies (or will occupy) under a parent — the\n * spelling `subsystem new` creates and `subsystem rename` renames into. The\n * same guarantee-by-brand as the service spellings above: the parent came off\n * the tree walk's readdir (or is `servicesDir` itself), and demanding\n * `SubsystemName` here is what makes the validator unskippable — a raw argv\n * string at this call site does not compile, so no future verb can hoist the\n * join above the check that earns it.\n */\nexport function subsystemPathUnder(parentDir: string, name: SubsystemName): string {\n return join(parentDir, name);\n}\n\n/**\n * The pair of requirement-carrying spec files, living and delta alike: the\n * business spec and the architecture spec. Same grammar, same delta algebra,\n * same merge — everything that walks \"the spec files\" of a service walks this\n * list, so the two axes cannot drift apart in what handles them. `key` indexes\n * ServicePaths/FeatureSpecPaths; `label` is how the axis names itself in prose.\n */\nexport const SPEC_AXES = [\n { key: \"spec\", file: \"spec.md\", label: \"requirements\" },\n { key: \"archSpec\", file: \"arch.spec.md\", label: \"arch requirements\" },\n] as const;\nexport type SpecAxis = (typeof SPEC_AXES)[number];\n\nexport interface FeaturePaths {\n dir: string;\n intent: string;\n delta: string;\n specsDir: string;\n adrsDir: string;\n}\n\nexport function featurePaths(featureDir: FeatureDir): FeaturePaths {\n return {\n dir: featureDir,\n intent: join(featureDir, \"intent.md\"),\n delta: join(featureDir, \"delta.likec4\"),\n specsDir: join(featureDir, \"specs\"),\n adrsDir: join(featureDir, \"adrs\"),\n };\n}\n\nexport interface FeatureSpecPaths {\n dir: string;\n spec: string;\n archSpec: string;\n openapi: string;\n /** The async contract delta — the event axis's sibling of `openapi`. */\n asyncapi: string;\n}\n\nexport function featureSpecPaths(featureDir: FeatureDir, service: PathableService): FeatureSpecPaths {\n const dir = join(featureDir, \"specs\", service);\n return {\n dir,\n spec: join(dir, \"spec.md\"),\n archSpec: join(dir, \"arch.spec.md\"),\n openapi: join(dir, \"openapi.yaml\"),\n asyncapi: join(dir, \"asyncapi.yaml\"),\n };\n}\n\nexport function landscapePath(docsDir: DocsDir): string {\n return join(docsDir, \"architecture\", LANDSCAPE_FILE);\n}\n\n/** The fleet's authorization vocabulary — beside the fleet map, for the same reason. */\nexport function permissionsPath(docsDir: DocsDir): string {\n return join(docsDir, \"architecture\", \"permissions.yaml\");\n}\n\n/** The fleet's capability vocabulary — beside the other two fleet documents, for the same reason. */\nexport function capabilitiesPath(docsDir: DocsDir): string {\n return join(docsDir, \"architecture\", \"capabilities.yaml\");\n}\n\n/**\n * The fleet's ARCHITECTURAL OBLIGATION vocabulary — beside the landscape it tags\n * and the two vocabularies it sits with, for their reason: it is a fact about\n * the fleet that no single service owns.\n */\nexport function obligationsPath(docsDir: DocsDir): string {\n return join(docsDir, \"architecture\", \"obligations.yaml\");\n}\n\n/**\n * The FLEET's decision records — `architecture/adrs/`, beside the landscape and\n * the two vocabularies.\n *\n * ADRs used to exist at two altitudes only, `services/<id>/adrs/` and\n * `features/<FEAT>/adrs/`, so a decision about the fleet itself — \"event\n * publishers use a transactional outbox\", \"cross-service calls carry a circuit\n * breaker\" — had no home but one arbitrary service's directory, where the next\n * reader of the other fifty services never finds it. This is not a new axis: it\n * is the same `ADRS_DIR` name one level up, spelled through the same constant so\n * the two altitudes cannot drift into `adrs/` and `decisions/`.\n *\n * PRESENCE-TRACKED AND NOTHING MORE, exactly like the service directory it\n * mirrors: `loam list` counts the files (`repo.ts`'s `fleetAdrCount`) and no\n * check anywhere reads one. A fleet with no fleet-level ADRs owes nothing and\n * must produce no finding — which is also why `loam init` does NOT scaffold the\n * directory. git does not carry an empty one, so it would vanish on the first\n * clone and come back as a diff on the next `init`; and an empty `adrs/` in a\n * fresh repo reads as an obligation nobody has met, when there is no obligation\n * at all.\n */\nexport function fleetAdrsDir(docsDir: DocsDir): string {\n return join(docsDir, \"architecture\", ADRS_DIR);\n}\n\n/**\n * The GENERATED subsystem views — beside the fleet map because the LikeC4\n * renderer merges the whole `architecture/` project, and a view can only\n * `include` elements the landscape beside it defines. `loam subsystem sync`\n * owns the bytes (`core/repo/tree/views.ts` renders them); nothing in loam\n * ever parses the file, and staleness against the tree is a byte compare.\n */\nexport function subsystemViewsPath(docsDir: DocsDir): string {\n return join(docsDir, \"architecture\", SUBSYSTEM_VIEWS_FILE);\n}\n\n/** The generated views' file name, spelled once because the reservation below reads it too. */\nconst SUBSYSTEM_VIEWS_FILE = \"subsystems.likec4\";\n\n/**\n * Names inside `architecture/` that LOAM owns, and what goes wrong if a\n * feature's own document claims one.\n *\n * `features/<FEAT>/deployment/<name>.likec4` merges as a whole-file copy\n * straight into `architecture/` — unlike a flow, which lands under\n * `usecases/` — so its `<name>` is free to be any of these, and two of them\n * fail in ways nothing downstream would report:\n *\n * - `subsystems.likec4` is GENERATED and is EXCLUDED from every project load\n * (docs/DESIGN.md rule 26 keeps it a byte compare), so topology landing\n * there is invisible to loam from the moment it archives — and the next\n * `loam subsystem sync` overwrites the file wholesale. Silent both ways,\n * which is the worst shape a failure can have;\n * - `landscape.likec4` is the living map the archive SPLICES; a whole-file\n * copy over it would discard the fleet;\n * - `usecases/` is the flow tree, walked by the use-case scan. A deployment\n * document filed there parses (one project) and is then looked for by a\n * reader asking a different question.\n *\n * Returns the reason, or null when the name is the author's to choose. Takes\n * the `/`-separated rel a directory walk produced, never a string from argv.\n */\nexport function reservedArchitectureName(rel: string): string | null {\n const parts = rel.split(\"/\");\n if (parts[0] === USECASE_DIR) {\n return `\\`${USECASE_DIR}/\\` is the use-case tree — a flow belongs in \\`features/<FEAT>/usecases/\\`, which merges there`;\n }\n if (rel === SUBSYSTEM_VIEWS_FILE) {\n return \"`subsystems.likec4` is generated by `loam subsystem sync`, which overwrites it wholesale — and nothing in loam ever parses it, so anything landing there is invisible from the moment it archives\";\n }\n if (rel === LANDSCAPE_FILE) {\n return \"`landscape.likec4` is the living fleet map, which the archive SPLICES into rather than replaces\";\n }\n return null;\n}\n\n/** The living map's file name, and the flow tree's directory — spelled once, read by the reservation. */\nconst LANDSCAPE_FILE = \"landscape.likec4\";\nconst USECASE_DIR = \"usecases\";\n\nexport function featuresDir(docsDir: DocsDir): string {\n return join(docsDir, \"features\");\n}\n\n/**\n * The root of the tree that IS the fleet — where an unfiled service and a\n * root-level subsystem both sit. The `subsystem` verbs join validated names\n * under it; the enumerating walk spells the same join for itself\n * (`repo/tree/walk.ts`), because it cannot import this module.\n */\nexport function servicesDir(docsDir: DocsDir): string {\n return join(docsDir, \"services\");\n}\n\n/**\n * The agent contract's filename, and where it sits in a docs repo.\n *\n * Both spellings are exported because the two questions are different: `init`,\n * `doctor` and `validate` want a path under a docsDir they already hold, while\n * `docs.ts` builds the scaffold as `[relative path, bytes]` pairs and needs the\n * bare name. Four modules spelled the literal out for themselves, and one of\n * them — `init` — uses it to decide whether a directory IS a docs repo at all,\n * so a rename that reached three of the four would leave `init` recognising a\n * file nothing else writes any more.\n *\n * `openspec-inventory.ts`'s \"AGENTS.md\" is deliberately NOT this constant: it\n * names a file in a foreign OpenSpec tree that happens to share the name.\n */\nexport const AGENTS_FILENAME = \"AGENTS.md\";\n\nexport function agentsPath(docsDir: DocsDir): string {\n return join(docsDir, AGENTS_FILENAME);\n}\n\n/**\n * Where shipped features live. `features/archive/` is spelled here and nowhere\n * else for the same reason every other artifact path is: `archive` and\n * `unarchive` move directories in and out of it, and a layout string that two\n * commands re-derive is a layout two commands can disagree about. The\n * `features/archive/…` strings in refusal PROSE are not this path — they are\n * what a reader types into `ls`, and they stay spelled out.\n */\nexport function archiveDir(docsDir: DocsDir): string {\n return join(featuresDir(docsDir), ARCHIVE_DIR);\n}\n"]}
|
|
1
|
+
{"version":3,"file":"paths.js","sourceRoot":"","sources":["../../../src/core/repo/paths.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * Where every artifact lives. Artifact filenames are spelled here and nowhere\n * else, so a rename is one edit rather than a grep.\n *\n * This is also the module that carries loam's one path guarantee.\n * `unfiledServicePaths(docsDir, service)` spells `<docsDir>/services/<service>/`,\n * so both parts are caller-controlled path input, and `node:path` cannot help:\n * `join(...paths: string[])` accepts every string there is. The guarantee IS\n * the parameter brands and nothing else — `PathableService` for the id (a name\n * whose provenance is the repository, unconstructible from document text),\n * `DocsDir`/`FeatureDir` for the roots (a resolved config or validated\n * `--docs`; a directory an enumeration read), and `ServiceDir` for\n * `servicePathsAt` (the enumeration's own resolved directory, narrower still)\n * — the `kernel/ids/` package holds the only casts, so an unchecked value at\n * these call sites does not compile.\n * Code that spells `services/<id>/` or a feature layout with a bare join is\n * outside the guarantee — `commands/new.ts` and the openspec migrator both do,\n * held instead by `resolveInside` at the write.\n */\nimport { join } from \"node:path\";\nimport { serviceDirOf, type DocsDir, type FeatureDir, type ServiceDir } from \"../kernel/ids/dirs.js\";\nimport type { PathableService } from \"../kernel/ids/service.js\";\nimport type { SubsystemName } from \"../kernel/ids/subsystem.js\";\n\n/** Directory under features/ holding shipped features. Never a feature itself. */\nexport const ARCHIVE_DIR = \"archive\";\n\n/* ------------------------------------------------------------------ */\n/* Paths — artifact filenames are spelled here and nowhere else */\n/* ------------------------------------------------------------------ */\n\n/**\n * The living artifact filenames, as one table, because three questions read\n * them: `servicePathsAt` below spells the paths, the tree walk classifies\n * a directory as a service by whether any of these names sit in it, and\n * `core/diff/base-state.ts` classifies the SAME way over a base git ref's\n * tree listing (exported for that reader — a filename respelled there would\n * be the drift this table exists to prevent). The walk cannot import this\n * module — `repo → repo/tree` is the package direction, and an edge back\n * would be the cycle `scripts/package-graph.mjs` refuses — so `repo.ts`\n * injects `isServiceArtifactName` into the walk request instead.\n */\nexport const ARTIFACT_FILES = {\n model: \"model.likec4\",\n spec: \"spec.md\",\n archSpec: \"arch.spec.md\",\n openapi: \"openapi.yaml\",\n asyncapi: \"asyncapi.yaml\",\n runbook: \"runbook.md\",\n health: \"health.yaml\",\n steps: \"steps.yaml\",\n} as const;\nconst ADRS_DIR = \"adrs\";\n\n/** Does a directory entry name a living service artifact? The classification half of the table above. */\nexport function isServiceArtifactName(name: string, kind: \"file\" | \"dir\"): boolean {\n if (kind === \"dir\") return name === ADRS_DIR;\n return Object.values(ARTIFACT_FILES).some((file) => file === name);\n}\n\nexport interface ServicePaths {\n dir: ServiceDir;\n model: string;\n spec: string;\n archSpec: string;\n openapi: string;\n /** The async contract — the event axis's sibling of `openapi`. */\n asyncapi: string;\n runbook: string;\n health: string;\n /**\n * The step catalogue — which phrases this service's suite has DECIDED to\n * define. Authored and opt-in; its CONTENTS are read by `loam steps` alone and\n * nothing in `validate` consults them\n * (`core/gherkin/steps/catalogue.ts` records why).\n *\n * Its EXISTENCE is read more widely, and that is not the same statement: the\n * filename is in `ARTIFACT_FILES`, so `isServiceArtifactName` classifies it\n * and the tree walk treats a directory holding one as a service. That is\n * correct — it is a service artifact — and it is worth writing down, because\n * it means a `steps.yaml` written at the wrong path mints a service.\n */\n steps: string;\n adrsDir: string;\n}\n\n/**\n * The artifact paths of a service whose directory is already KNOWN — the\n * enumeration's `dir`, at whatever depth the tree walk found it. This is the\n * spelling every reader of an existing service must use: joining\n * `services/<id>/` at the root is only true for a fleet nobody has filed, and\n * a moved service would silently grade as absent through it.\n */\nexport function servicePathsAt(dir: ServiceDir): ServicePaths {\n return {\n dir,\n model: join(dir, ARTIFACT_FILES.model),\n spec: join(dir, ARTIFACT_FILES.spec),\n archSpec: join(dir, ARTIFACT_FILES.archSpec),\n openapi: join(dir, ARTIFACT_FILES.openapi),\n asyncapi: join(dir, ARTIFACT_FILES.asyncapi),\n runbook: join(dir, ARTIFACT_FILES.runbook),\n health: join(dir, ARTIFACT_FILES.health),\n steps: join(dir, ARTIFACT_FILES.steps),\n adrsDir: join(dir, ADRS_DIR),\n };\n}\n\n/**\n * The UNFILED spelling — `<docsDir>/services/<id>/`, the tree's root level.\n * This is where creation lands (`adopt`'s brief, a new service materialised by\n * `archive`, the OpenSpec migration) and the honest fallback for a service the\n * enumeration does not answer; for a service that EXISTS, resolve through the\n * enumeration instead (`servicePathsAt(entry.dir)`, or\n * `locateServicePaths` in `service-target.ts`, which chooses between the two).\n * Renamed from `servicePaths` when the tree landed, precisely so the compiler\n * would put every remaining caller in front of a reviewer: a root join that\n * survives unreviewed is a moved service silently grading as absent.\n */\nexport function unfiledServicePaths(docsDir: DocsDir, service: PathableService): ServicePaths {\n return servicePathsAt(serviceDirOf(join(docsDir, \"services\", service)));\n}\n\n/**\n * The creation spelling INSIDE a subsystem — `<subsystemDir>/<id>/`, the\n * target `adopt --subsystem` briefs so an adoption need not land unfiled and\n * cost a second command. The same two provenances the unfiled spelling\n * combines, one level down: the subsystem directory came off the tree walk's\n * readdir, and the id passed the service grammar before it reached here.\n */\nexport function servicePathsUnder(subsystemDir: string, service: PathableService): ServicePaths {\n return servicePathsAt(serviceDirOf(join(subsystemDir, service)));\n}\n\n/**\n * The directory a SUBSYSTEM occupies (or will occupy) under a parent — the\n * spelling `subsystem new` creates and `subsystem rename` renames into. The\n * same guarantee-by-brand as the service spellings above: the parent came off\n * the tree walk's readdir (or is `servicesDir` itself), and demanding\n * `SubsystemName` here is what makes the validator unskippable — a raw argv\n * string at this call site does not compile, so no future verb can hoist the\n * join above the check that earns it.\n */\nexport function subsystemPathUnder(parentDir: string, name: SubsystemName): string {\n return join(parentDir, name);\n}\n\n/**\n * The pair of requirement-carrying spec files, living and delta alike: the\n * business spec and the architecture spec. Same grammar, same delta algebra,\n * same merge — everything that walks \"the spec files\" of a service walks this\n * list, so the two axes cannot drift apart in what handles them. `key` indexes\n * ServicePaths/FeatureSpecPaths; `label` is how the axis names itself in prose.\n */\nexport const SPEC_AXES = [\n { key: \"spec\", file: \"spec.md\", label: \"requirements\" },\n { key: \"archSpec\", file: \"arch.spec.md\", label: \"arch requirements\" },\n] as const;\nexport type SpecAxis = (typeof SPEC_AXES)[number];\n\nexport interface FeaturePaths {\n dir: string;\n intent: string;\n delta: string;\n specsDir: string;\n adrsDir: string;\n}\n\nexport function featurePaths(featureDir: FeatureDir): FeaturePaths {\n return {\n dir: featureDir,\n intent: join(featureDir, \"intent.md\"),\n delta: join(featureDir, \"delta.likec4\"),\n specsDir: join(featureDir, \"specs\"),\n adrsDir: join(featureDir, \"adrs\"),\n };\n}\n\nexport interface FeatureSpecPaths {\n dir: string;\n spec: string;\n archSpec: string;\n openapi: string;\n /** The async contract delta — the event axis's sibling of `openapi`. */\n asyncapi: string;\n}\n\nexport function featureSpecPaths(featureDir: FeatureDir, service: PathableService): FeatureSpecPaths {\n const dir = join(featureDir, \"specs\", service);\n return {\n dir,\n spec: join(dir, \"spec.md\"),\n archSpec: join(dir, \"arch.spec.md\"),\n openapi: join(dir, \"openapi.yaml\"),\n asyncapi: join(dir, \"asyncapi.yaml\"),\n };\n}\n\nexport function landscapePath(docsDir: DocsDir): string {\n return join(docsDir, \"architecture\", LANDSCAPE_FILE);\n}\n\n/** The fleet's authorization vocabulary — beside the fleet map, for the same reason. */\nexport function permissionsPath(docsDir: DocsDir): string {\n return join(docsDir, \"architecture\", \"permissions.yaml\");\n}\n\n/** The fleet's capability vocabulary — beside the other two fleet documents, for the same reason. */\nexport function capabilitiesPath(docsDir: DocsDir): string {\n return join(docsDir, \"architecture\", \"capabilities.yaml\");\n}\n\n/**\n * The fleet's ARCHITECTURAL OBLIGATION vocabulary — beside the landscape it tags\n * and the two vocabularies it sits with, for their reason: it is a fact about\n * the fleet that no single service owns.\n */\nexport function obligationsPath(docsDir: DocsDir): string {\n return join(docsDir, \"architecture\", \"obligations.yaml\");\n}\n\n/**\n * The FLEET's decision records — `architecture/adrs/`, beside the landscape and\n * the two vocabularies.\n *\n * ADRs used to exist at two altitudes only, `services/<id>/adrs/` and\n * `features/<FEAT>/adrs/`, so a decision about the fleet itself — \"event\n * publishers use a transactional outbox\", \"cross-service calls carry a circuit\n * breaker\" — had no home but one arbitrary service's directory, where the next\n * reader of the other fifty services never finds it. This is not a new axis: it\n * is the same `ADRS_DIR` name one level up, spelled through the same constant so\n * the two altitudes cannot drift into `adrs/` and `decisions/`.\n *\n * PRESENCE-TRACKED AND NOTHING MORE, exactly like the service directory it\n * mirrors: `loam list` counts the files (`repo.ts`'s `fleetAdrCount`) and no\n * check anywhere reads one. A fleet with no fleet-level ADRs owes nothing and\n * must produce no finding — which is also why `loam init` does NOT scaffold the\n * directory. git does not carry an empty one, so it would vanish on the first\n * clone and come back as a diff on the next `init`; and an empty `adrs/` in a\n * fresh repo reads as an obligation nobody has met, when there is no obligation\n * at all.\n */\nexport function fleetAdrsDir(docsDir: DocsDir): string {\n return join(docsDir, \"architecture\", ADRS_DIR);\n}\n\n/**\n * The GENERATED subsystem views — beside the fleet map because the LikeC4\n * renderer merges the whole `architecture/` project, and a view can only\n * `include` elements the landscape beside it defines. `loam subsystem sync`\n * owns the bytes (`core/repo/tree/render/views.ts` renders them); nothing in loam\n * ever parses the file, and staleness against the tree is a byte compare.\n */\nexport function subsystemViewsPath(docsDir: DocsDir): string {\n return join(docsDir, \"architecture\", SUBSYSTEM_VIEWS_FILE);\n}\n\n/** The generated views' file name, spelled once because the reservation below reads it too. */\nconst SUBSYSTEM_VIEWS_FILE = \"subsystems.likec4\";\n\n/**\n * Names inside `architecture/` that LOAM owns, and what goes wrong if a\n * feature's own document claims one.\n *\n * `features/<FEAT>/deployment/<name>.likec4` merges as a whole-file copy\n * straight into `architecture/` — unlike a flow, which lands under\n * `usecases/` — so its `<name>` is free to be any of these, and two of them\n * fail in ways nothing downstream would report:\n *\n * - `subsystems.likec4` is GENERATED and is EXCLUDED from every project load\n * (docs/DESIGN.md rule 26 keeps it a byte compare), so topology landing\n * there is invisible to loam from the moment it archives — and the next\n * `loam subsystem sync` overwrites the file wholesale. Silent both ways,\n * which is the worst shape a failure can have;\n * - `landscape.likec4` is the living map the archive SPLICES; a whole-file\n * copy over it would discard the fleet;\n * - `usecases/` is the flow tree, walked by the use-case scan. A deployment\n * document filed there parses (one project) and is then looked for by a\n * reader asking a different question.\n *\n * Returns the reason, or null when the name is the author's to choose. Takes\n * the `/`-separated rel a directory walk produced, never a string from argv.\n */\nexport function reservedArchitectureName(rel: string): string | null {\n const parts = rel.split(\"/\");\n if (parts[0] === USECASE_DIR) {\n return `\\`${USECASE_DIR}/\\` is the use-case tree — a flow belongs in \\`features/<FEAT>/usecases/\\`, which merges there`;\n }\n if (rel === SUBSYSTEM_VIEWS_FILE) {\n return \"`subsystems.likec4` is generated by `loam subsystem sync`, which overwrites it wholesale — and nothing in loam ever parses it, so anything landing there is invisible from the moment it archives\";\n }\n if (rel === LANDSCAPE_FILE) {\n return \"`landscape.likec4` is the living fleet map, which the archive SPLICES into rather than replaces\";\n }\n return null;\n}\n\n/** The living map's file name, and the flow tree's directory — spelled once, read by the reservation. */\nconst LANDSCAPE_FILE = \"landscape.likec4\";\nconst USECASE_DIR = \"usecases\";\n\nexport function featuresDir(docsDir: DocsDir): string {\n return join(docsDir, \"features\");\n}\n\n/**\n * The root of the tree that IS the fleet — where an unfiled service and a\n * root-level subsystem both sit. The `subsystem` verbs join validated names\n * under it; the enumerating walk spells the same join for itself\n * (`repo/tree/walk.ts`), because it cannot import this module.\n */\nexport function servicesDir(docsDir: DocsDir): string {\n return join(docsDir, \"services\");\n}\n\n/**\n * The agent contract's filename, and where it sits in a docs repo.\n *\n * Both spellings are exported because the two questions are different: `init`,\n * `doctor` and `validate` want a path under a docsDir they already hold, while\n * `docs.ts` builds the scaffold as `[relative path, bytes]` pairs and needs the\n * bare name. Four modules spelled the literal out for themselves, and one of\n * them — `init` — uses it to decide whether a directory IS a docs repo at all,\n * so a rename that reached three of the four would leave `init` recognising a\n * file nothing else writes any more.\n *\n * `openspec-inventory.ts`'s \"AGENTS.md\" is deliberately NOT this constant: it\n * names a file in a foreign OpenSpec tree that happens to share the name.\n */\nexport const AGENTS_FILENAME = \"AGENTS.md\";\n\nexport function agentsPath(docsDir: DocsDir): string {\n return join(docsDir, AGENTS_FILENAME);\n}\n\n/**\n * Where shipped features live. `features/archive/` is spelled here and nowhere\n * else for the same reason every other artifact path is: `archive` and\n * `unarchive` move directories in and out of it, and a layout string that two\n * commands re-derive is a layout two commands can disagree about. The\n * `features/archive/…` strings in refusal PROSE are not this path — they are\n * what a reader types into `ls`, and they stay spelled out.\n */\nexport function archiveDir(docsDir: DocsDir): string {\n return join(featuresDir(docsDir), ARCHIVE_DIR);\n}\n\n/**\n * The LikeC4 project file — the one thing in a docs repo loam writes for a\n * tool other than itself — and it lives at two altitudes.\n *\n * At the ROOT, `loam init --create` writes the project `fleet`, scoped to\n * `architecture/` (`core/docs.ts` composes those bytes). Beside each\n * `services/<…>/<id>/model.likec4`, `loam subsystem sync` writes a project of\n * the service's own: loam parses every `.likec4` file ALONE, so each model\n * declares its own `specification` block and the root project must exclude\n * `services/**` — which left every adopted service a box on the fleet map with\n * nothing renderable inside it, in the renderer opened at the docs root.\n * `core/repo/tree/render/projects.ts` composes the per-service bytes and\n * records what was measured.\n *\n * Spelled here and NOT in `ARTIFACT_FILES`, and the distinction is\n * load-bearing: a filename in that table classifies a directory as a service,\n * and this file is the renderer's — a stray one must mint nothing. The root's\n * NAME sits beside the filename because the per-service rule needs it too (a\n * service id equal to the root project's name would be silently renamed by\n * the renderer), and two spellings of the word are how the scaffold and the\n * rule drift apart.\n */\nexport const LIKEC4_PROJECT_FILENAME = \"likec4.config.json\";\n\n/** The root project's name, exactly as `loam init --create` scaffolds it. */\nexport const LIKEC4_ROOT_PROJECT = \"fleet\";\n\nexport function rootProjectPath(docsDir: DocsDir): string {\n return join(docsDir, LIKEC4_PROJECT_FILENAME);\n}\n\n/**\n * The two files the per-service project question is asked of — the model that\n * needs rendering and the project file that renders it — in one spelling for\n * the writer (`subsystem sync`) and the grader (`validate --all`) alike. Handed\n * INTO `render/projects.ts` rather than imported there, because that package\n * cannot import this module: `render/stale.ts` records the cycle.\n */\nexport function serviceRenderPaths(dir: ServiceDir): { model: string; project: string } {\n return { model: join(dir, ARTIFACT_FILES.model), project: join(dir, LIKEC4_PROJECT_FILENAME) };\n}\n"]}
|
|
@@ -0,0 +1,219 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* What one subsystem's generated view SHOWS: which boxes it draws around the
|
|
3
|
+
* services beneath a directory, and the one thing it may say about the map.
|
|
4
|
+
*
|
|
5
|
+
* Split from `../views.ts` when that file crossed the 400-line limit: the file
|
|
6
|
+
* render (header, view ids, staleness comparison) and the body render (the
|
|
7
|
+
* boxes) are two subjects, and the seam is exactly where the second one
|
|
8
|
+
* consults the LANDSCAPE. Everything here reads element ids and nothing else —
|
|
9
|
+
* no view is ever parsed (docs/DESIGN.md rule 26), and the generated file
|
|
10
|
+
* stays a pure function of the tree plus the committed map.
|
|
11
|
+
*/
|
|
12
|
+
import {} from "../../../c4/likec4.js";
|
|
13
|
+
import { serviceResolver } from "../../../c4/resolve/service.js";
|
|
14
|
+
import { servicesUnder, withinSubsystem } from "../find.js";
|
|
15
|
+
import { subsystemLabel } from "./text.js";
|
|
16
|
+
export function renderContext(tree, elements) {
|
|
17
|
+
const known = new Set(tree.services.map((s) => s.id));
|
|
18
|
+
const resolve = serviceResolver(elements, known);
|
|
19
|
+
return {
|
|
20
|
+
tree,
|
|
21
|
+
elementOf: elementByService(tree, elements, resolve),
|
|
22
|
+
elementIds: new Set(elements.map((e) => e.id)),
|
|
23
|
+
isServiceElement: (id) => known.has(resolve(id)),
|
|
24
|
+
};
|
|
25
|
+
}
|
|
26
|
+
/**
|
|
27
|
+
* Three shapes, and which one applies is a fact about the landscape rather
|
|
28
|
+
* than a setting:
|
|
29
|
+
*
|
|
30
|
+
* 1. **The model draws this group.** Some ancestor element holds exactly this
|
|
31
|
+
* subsystem's members and nothing else, so the view includes it beside the
|
|
32
|
+
* leaves and the renderer nests them inside the box the AUTHOR drew, with
|
|
33
|
+
* the author's own title. Measured: including the ancestor alone renders
|
|
34
|
+
* one empty box and the leaves alone render flat — both lines are needed.
|
|
35
|
+
* 2. **The model is flat.** No member element is nested at all, so there is
|
|
36
|
+
* no authored box to agree with, and the view wraps the members in a
|
|
37
|
+
* `group '<title>'` of its own — a real named box in the renderer, derived
|
|
38
|
+
* from the DIRECTORY the services already sit in, mirroring the subtree so
|
|
39
|
+
* a parent subsystem shows its children as boxes rather than as one pile.
|
|
40
|
+
* Nothing is invented: the grouping is the tree, and the tree is what this
|
|
41
|
+
* file has always mirrored.
|
|
42
|
+
* 3. **The model nests them, differently.** The members ARE nested but no
|
|
43
|
+
* ancestor owns exactly them — the directory tree and the map disagree
|
|
44
|
+
* about this group. loam draws no box (a synthetic one would silently
|
|
45
|
+
* replace the author's boundary with the folder's, and the picture would
|
|
46
|
+
* look the same whether or not the two agree) and writes ONE comment line
|
|
47
|
+
* saying so, outside the view body. That comment is the whole of loam's
|
|
48
|
+
* answer to the drift: it grades nothing, it fails nothing, and it appears
|
|
49
|
+
* in the diff of the very commit that caused it — placement stays a fact
|
|
50
|
+
* nothing branches on.
|
|
51
|
+
*/
|
|
52
|
+
export function viewBody(ctx, sub) {
|
|
53
|
+
const members = memberElements(ctx, sub);
|
|
54
|
+
if (members.length === 0)
|
|
55
|
+
return { lines: [], disclosure: null };
|
|
56
|
+
if (members.every((id) => !id.includes(".")))
|
|
57
|
+
return { lines: groupedBody(ctx, sub, " "), disclosure: null };
|
|
58
|
+
const lines = [...ownedAncestors(ctx, sub), ...members].map((id) => ` include ${id}`);
|
|
59
|
+
if (owningAncestor(ctx, sub) !== null)
|
|
60
|
+
return { lines, disclosure: null };
|
|
61
|
+
const shared = sharedAncestor(ctx, members);
|
|
62
|
+
return {
|
|
63
|
+
lines,
|
|
64
|
+
disclosure: shared === null
|
|
65
|
+
? "model: no boundary — these members do not share a containing element."
|
|
66
|
+
: `model: no boundary — ${shared} also holds services outside this subsystem.`,
|
|
67
|
+
};
|
|
68
|
+
}
|
|
69
|
+
/**
|
|
70
|
+
* The flat-model body: this subsystem as a box, its child subsystems as boxes
|
|
71
|
+
* inside it, its own services as leaves. Recursive, and the recursion is the
|
|
72
|
+
* point — a parent subsystem's view is otherwise one undifferentiated pile of
|
|
73
|
+
* every service beneath it.
|
|
74
|
+
*
|
|
75
|
+
* An empty child renders an empty named box, which is what the tree says: the
|
|
76
|
+
* group exists and holds nothing yet.
|
|
77
|
+
*/
|
|
78
|
+
function groupedBody(ctx, sub, indent) {
|
|
79
|
+
const inner = `${indent} `;
|
|
80
|
+
const out = [`${indent}group '${subsystemLabel(sub)}' {`];
|
|
81
|
+
for (const child of childSubsystems(ctx, sub))
|
|
82
|
+
out.push(...groupedBody(ctx, child, inner));
|
|
83
|
+
for (const id of directMemberElements(ctx, sub))
|
|
84
|
+
out.push(`${inner}include ${id}`);
|
|
85
|
+
out.push(`${indent}}`);
|
|
86
|
+
return out;
|
|
87
|
+
}
|
|
88
|
+
/** The child subsystems one level down, ordered by name. */
|
|
89
|
+
function childSubsystems(ctx, sub) {
|
|
90
|
+
return ctx.tree.subsystems
|
|
91
|
+
.filter((c) => c.path.length === sub.path.length + 1 && withinSubsystem(sub.path, c.path))
|
|
92
|
+
.sort((a, b) => (a.name < b.name ? -1 : 1));
|
|
93
|
+
}
|
|
94
|
+
/** The drawn elements of the services filed in THIS directory, not in a child. */
|
|
95
|
+
function directMemberElements(ctx, sub) {
|
|
96
|
+
return drawnElements(ctx, membersOf(ctx.tree, sub).filter((s) => s.subsystem.length === sub.path.length));
|
|
97
|
+
}
|
|
98
|
+
/** The drawn elements of every service beneath this subsystem, ordered by service id. */
|
|
99
|
+
function memberElements(ctx, sub) {
|
|
100
|
+
return drawnElements(ctx, membersOf(ctx.tree, sub));
|
|
101
|
+
}
|
|
102
|
+
/**
|
|
103
|
+
* The element ids these services resolve to. A service the map does not draw
|
|
104
|
+
* is omitted rather than invented: the omission is already an error
|
|
105
|
+
* (`landscape.service-unmodelled`), and rendering only what the committed
|
|
106
|
+
* documents resolve keeps the output a function of committed bytes alone.
|
|
107
|
+
*/
|
|
108
|
+
function drawnElements(ctx, services) {
|
|
109
|
+
return services.map((s) => ctx.elementOf.get(s.id)).filter((id) => id !== undefined);
|
|
110
|
+
}
|
|
111
|
+
/**
|
|
112
|
+
* The boxes a nested-model view draws: this subsystem's own owning ancestor
|
|
113
|
+
* when it has one, plus the owning ancestor of every subsystem beneath it — so
|
|
114
|
+
* a parent view keeps its children's boundaries instead of flattening them
|
|
115
|
+
* into one box. Sorted and de-duplicated, because two subsystems resolve to
|
|
116
|
+
* one element only in a tree that already reports a collision.
|
|
117
|
+
*/
|
|
118
|
+
function ownedAncestors(ctx, sub) {
|
|
119
|
+
const found = new Set();
|
|
120
|
+
for (const candidate of ctx.tree.subsystems) {
|
|
121
|
+
if (!withinSubsystem(sub.path, candidate.path))
|
|
122
|
+
continue;
|
|
123
|
+
const owner = owningAncestor(ctx, candidate);
|
|
124
|
+
if (owner !== null)
|
|
125
|
+
found.add(owner);
|
|
126
|
+
}
|
|
127
|
+
return [...found].sort();
|
|
128
|
+
}
|
|
129
|
+
/**
|
|
130
|
+
* The deepest ancestor element that holds exactly this subsystem's members —
|
|
131
|
+
* or null when no element does.
|
|
132
|
+
*
|
|
133
|
+
* "Exactly" is the whole guard, and dropping it was measured on loam's own
|
|
134
|
+
* `examples/docs`: the members' longest common ancestor there is a group of
|
|
135
|
+
* five services while the subsystem holds two, so an unguarded include would
|
|
136
|
+
* generate a box labelled for the group with two of its five children drawn
|
|
137
|
+
* inside it — a diagram asserting something false, in a file the reader is
|
|
138
|
+
* told not to edit. Tightness is judged over ELEMENTS rather than over
|
|
139
|
+
* documented services: mid-adoption a group commonly holds systems no
|
|
140
|
+
* `services/<id>/` exists for yet, and counting only the documented ones would
|
|
141
|
+
* draw that same false box for the whole of an adoption.
|
|
142
|
+
*
|
|
143
|
+
* An ancestor that is itself a service's element is refused separately: one
|
|
144
|
+
* service's box containing another's is a picture of nothing.
|
|
145
|
+
*/
|
|
146
|
+
function owningAncestor(ctx, sub) {
|
|
147
|
+
const members = memberElements(ctx, sub);
|
|
148
|
+
if (members.length === 0)
|
|
149
|
+
return null;
|
|
150
|
+
const owned = new Set(members);
|
|
151
|
+
for (const candidate of ancestorChain(members)) {
|
|
152
|
+
if (!ctx.elementIds.has(candidate) || ctx.isServiceElement(candidate))
|
|
153
|
+
continue;
|
|
154
|
+
if (ownsExactly(ctx, candidate, owned))
|
|
155
|
+
return candidate;
|
|
156
|
+
}
|
|
157
|
+
return null;
|
|
158
|
+
}
|
|
159
|
+
/** The deepest common ancestor the map declares at all — the disclosure's subject. */
|
|
160
|
+
function sharedAncestor(ctx, members) {
|
|
161
|
+
return ancestorChain(members).find((id) => ctx.elementIds.has(id)) ?? null;
|
|
162
|
+
}
|
|
163
|
+
/** The common proper ancestors of every member id, deepest first. */
|
|
164
|
+
function ancestorChain(members) {
|
|
165
|
+
const segments = members.map((id) => id.split("."));
|
|
166
|
+
let common = Math.min(...segments.map((s) => s.length - 1));
|
|
167
|
+
for (let i = 0; i < common; i++) {
|
|
168
|
+
if (!segments.every((s) => s[i] === segments[0][i])) {
|
|
169
|
+
common = i;
|
|
170
|
+
break;
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
const out = [];
|
|
174
|
+
for (let depth = common; depth > 0; depth--)
|
|
175
|
+
out.push(segments[0].slice(0, depth).join("."));
|
|
176
|
+
return out;
|
|
177
|
+
}
|
|
178
|
+
/**
|
|
179
|
+
* Does every element beneath `ancestor` belong to this subsystem — as a
|
|
180
|
+
* member, as one of a member's own containers, or as a container of one?
|
|
181
|
+
*/
|
|
182
|
+
function ownsExactly(ctx, ancestor, members) {
|
|
183
|
+
const prefix = `${ancestor}.`;
|
|
184
|
+
for (const id of ctx.elementIds) {
|
|
185
|
+
if (!id.startsWith(prefix))
|
|
186
|
+
continue;
|
|
187
|
+
const claimed = [...members].some((m) => id === m || id.startsWith(`${m}.`) || m.startsWith(`${id}.`));
|
|
188
|
+
if (!claimed)
|
|
189
|
+
return false;
|
|
190
|
+
}
|
|
191
|
+
return true;
|
|
192
|
+
}
|
|
193
|
+
/**
|
|
194
|
+
* The landscape element standing for each service id: for every element,
|
|
195
|
+
* resolve it through the shared join, and let the SHALLOWEST element win
|
|
196
|
+
* (document order breaking ties). Shallowest, because `serviceResolver`
|
|
197
|
+
* resolves a service's own containers to the service too, and a view should
|
|
198
|
+
* include the service-level box, not whichever nested container happened to be
|
|
199
|
+
* declared first.
|
|
200
|
+
*/
|
|
201
|
+
function elementByService(tree, elements, resolve) {
|
|
202
|
+
const known = new Set(tree.services.map((s) => s.id));
|
|
203
|
+
const depth = (id) => id.split(".").length;
|
|
204
|
+
const out = new Map();
|
|
205
|
+
for (const e of elements) {
|
|
206
|
+
const service = resolve(e.id);
|
|
207
|
+
if (!known.has(service))
|
|
208
|
+
continue;
|
|
209
|
+
const held = out.get(service);
|
|
210
|
+
if (held === undefined || depth(e.id) < depth(held))
|
|
211
|
+
out.set(service, e.id);
|
|
212
|
+
}
|
|
213
|
+
return out;
|
|
214
|
+
}
|
|
215
|
+
/** Services beneath this subsystem at any depth, ordered by id — the transitive membership. */
|
|
216
|
+
function membersOf(tree, sub) {
|
|
217
|
+
return [...servicesUnder(tree, sub)].sort((a, b) => (a.id < b.id ? -1 : 1));
|
|
218
|
+
}
|
|
219
|
+
//# sourceMappingURL=body.js.map
|