@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":"targets.js","sourceRoot":"","sources":["../../../src/core/brief/targets.ts"],"names":[],"mappings":"AAAA;;;;;;;;GAQG;AAiDH,iEAAiE;AACjE,MAAM,CAAC,MAAM,SAAS,GAA8E;IAClG;QACE,QAAQ,EAAE,cAAc;QACxB,GAAG,EAAE,OAAO;QACZ,QAAQ,EAAE,IAAI;QACd,OAAO,EAAE,oEAAoE;QAC7E,uEAAuE;QACvE,2EAA2E;QAC3E,4EAA4E;QAC5E,qEAAqE;QACrE,oEAAoE;QACpE,8CAA8C;QAC9C,KAAK,EAAE;YACL,2OAA2O;YAC3O,0NAA0N;YAC1N,oYAAoY;YACpY,6YAA6Y;SAC9Y;QACD,OAAO,EAAE;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;CA8BZ;KACE;IACD;QACE,QAAQ,EAAE,SAAS;QACnB,GAAG,EAAE,MAAM;QACX,QAAQ,EAAE,IAAI;QACd,OAAO,EAAE,mFAAmF;QAC5F,KAAK,EAAE;YACL,wDAAwD;YACxD,kQAAkQ;YAClQ,wMAAwM;YACxM,wtBAAwtB;YACxtB,o0BAAo0B;YACp0B,wjBAAwjB;YACxjB,4fAA4f;YAC5f,slBAAslB;YACtlB,6RAA6R;YAC7R,6HAA6H;SAC9H;QACD,OAAO,EAAE;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;CAuCZ;KACE;IACD;QACE,QAAQ,EAAE,cAAc;QACxB,GAAG,EAAE,UAAU;QACf,QAAQ,EAAE,KAAK;QACf,OAAO,EACL,uIAAuI;QACzI,KAAK,EAAE;YACL,kJAAkJ;YAClJ,uRAAuR;YACvR,kNAAkN;YAClN,+MAA+M;SAChN;QACD,OAAO,EAAE;;;;;;;;;;;;;;;;;;;;;;CAsBZ;KACE;IACD;QACE,QAAQ,EAAE,cAAc;QACxB,GAAG,EAAE,SAAS;QACd,0EAA0E;QAC1E,6DAA6D;QAC7D,QAAQ,EAAE,IAAI;QACd,OAAO,EAAE,wDAAwD;QACjE,KAAK,EAAE;YACL,iCAAiC;YACjC,+NAA+N;YAC/N,yXAAyX;YACzX,kKAAkK;YAClK,wEAAwE;YACxE,uEAAuE;YACvE,0EAA0E;YAC1E,wEAAwE;YACxE,uEAAuE;YACvE,kCAAkC;YAClC,kXAAkX;SACnX;KACF;IACD;QACE,QAAQ,EAAE,eAAe;QACzB,GAAG,EAAE,UAAU;QACf,0EAA0E;QAC1E,6EAA6E;QAC7E,uEAAuE;QACvE,kDAAkD;QAClD,QAAQ,EAAE,KAAK;QACf,OAAO,EAAE,8EAA8E;QACvF,KAAK,EAAE;YACL,8SAA8S;YAC9S,qRAAqR;YACrR,wPAAwP;YACxP,4bAA4b;YAC5b,gQAAgQ;YAChQ,+FAA+F;SAChG;KACF;IACD;QACE,QAAQ,EAAE,OAAO;QACjB,GAAG,EAAE,SAAS;QACd,QAAQ,EAAE,KAAK;QACf,OAAO,EAAE,+CAA+C;QACxD,KAAK,EAAE;YACL,+FAA+F;YAC/F,2MAA2M;YAC3M,oJAAoJ;SACrJ;KACF;IACD;QACE,QAAQ,EAAE,YAAY;QACtB,GAAG,EAAE,SAAS;QACd,QAAQ,EAAE,KAAK;QACf,OAAO,EAAE,iEAAiE;QAC1E,KAAK,EAAE;YACL,kIAAkI;YAClI,2JAA2J;YAC3J,4PAA4P;SAC7P;KACF;IACD;QACE,QAAQ,EAAE,aAAa;QACvB,GAAG,EAAE,QAAQ;QACb,QAAQ,EAAE,KAAK;QACf,OAAO,EAAE,2DAA2D;QACpE,KAAK,EAAE;YACL,uFAAuF;YACvF,uHAAuH;SACxH;KACF;CACF,CAAC","sourcesContent":["/**\n * The artifacts of the adoption brief: what each file under `services/<svc>/`\n * is for, the grammar it has to be written in, and a minimal valid example\n * where showing one is shorter than describing it. Every `shape` rule here is\n * one a later check depends on, and test/agent-contract.test.ts pins the\n * mapping. This module is the list alone — `serviceBrief` (brief.ts) resolves\n * it against a docs repo, and the fleet map's own target, whose block names\n * the service, is assembled per service in landscape.ts.\n */\n\n/* ------------------------------------------------------------------ */\n/* The artifacts */\n/* ------------------------------------------------------------------ */\n\nexport interface BriefTarget {\n /** File name as it appears under `services/<svc>/`. */\n artifact: string;\n /** Repo-relative path to write. */\n path: string;\n exists: boolean;\n /**\n * `create` a missing artifact; `diff` an existing one — never overwrite it.\n *\n * `edit` is the third case and belongs to exactly one target: the fleet map.\n * `architecture/landscape.likec4` is a SHARED document that already holds\n * every other service, so \"diff it and report what disagrees\" is the wrong\n * instruction — the file is not this service's baseline, it is the fleet's,\n * and what this service owes it is an addition inside the existing\n * `model { … }` block. Spelling that as `diff` taught agents to leave the\n * fleet map alone; spelling it as `create` would have taught them to\n * overwrite ten other services.\n */\n action: \"create\" | \"diff\" | \"edit\";\n /**\n * False for artifacts a baseline can legitimately ship without. True means\n * required for a COMPLETE baseline — validate grades a missing spec.md or\n * openapi.yaml as a warn (`service.no-spec` / `service.no-openapi`), not an\n * error: partial adoption is a supported state, and the warns are the\n * progress meter.\n *\n * Constant per artifact except one: `openapi.yaml` is required only where the\n * fleet map shows an API is expected (see `apiExpected` in brief.ts), because\n * the brief is read as an instruction and \"MISSING openapi.yaml\" told an agent\n * to write a contract for a service that serves no requests.\n */\n required: boolean;\n /** What the artifact is for, in one line. */\n purpose: string;\n /** The grammar. Every rule here is one a later check depends on. */\n shape: string[];\n /** A minimal valid instance, where showing one is shorter than describing it. */\n example?: string;\n}\n\n/** Where an artifact lives — `repo/paths.ts` spells the filenames, this only points at them. */\ntype PathKey = \"model\" | \"spec\" | \"archSpec\" | \"openapi\" | \"asyncapi\" | \"adrsDir\" | \"runbook\" | \"health\";\n\n/** The artifacts, in the order a baseline is best written in. */\nexport const ARTIFACTS: Array<Omit<BriefTarget, \"path\" | \"exists\" | \"action\"> & { key: PathKey }> = [\n {\n artifact: \"model.likec4\",\n key: \"model\",\n required: true,\n purpose: \"the service's C4 — what it is, what is inside it, what it talks to\",\n // Every rule below is one a later check depends on, and the mapping is\n // pinned in test/agent-contract.test.ts. Rules nobody checks — the `views`\n // block, \"exactly one top-level element\" — used to sit here reading exactly\n // like the enforced ones, which is how a brief teaches an agent that\n // `loam validate` passing means more than it does. They now live in\n // UNCHECKED, where their status is the point.\n shape: [\n \"A `specification { ... }` block declaring every element kind you use (`element softwareSystem`, `element container`). LikeC4 rejects an undeclared kind, and `loam validate --service <id>` reports it as `c4.invalid` with line numbers.\",\n \"A `model { ... }` block holding the service's element, with containers and components nested inside it. The whole file has to parse — `loam validate` runs LikeC4 in-process and reports `c4.invalid` with line numbers.\",\n \"The service element binds to this directory: `metadata { service '<id>' }`. Without a binding the element's TITLE has to equal the directory name — and then renaming the box silently unlinks every check that joined the two. The same binding is what makes the FLEET map resolve an element to `services/<id>/`; until one does, `loam validate --all` reports `landscape.service-unmodelled`.\",\n \"A call to another service is an edge that names the operation it uses: `a -> b 'Calls createSplit' { metadata { op 'createSplit' } }`. Cross-service edges belong in `architecture/landscape.likec4` (a target of its own, below) — this file cannot even resolve the other service's element — and there the `op` must be an operationId the TARGET's openapi.yaml defines, or `spine.op-undefined` fires.\",\n ],\n example: `specification {\n element softwareSystem\n element container\n}\n\nmodel {\n paymentService = softwareSystem 'payment-service' {\n description 'Owns payment authorization and capture'\n metadata { service 'payment-service' }\n\n api = container 'HTTP API'\n ledger = container 'Ledger store'\n }\n}\n\n// Views are LikeC4's, not loam's: loam parses this file and computes no view,\n// and it never reads a STATIC view's contents in any document, so nothing below\n// is read by any check (docs/DESIGN.md rule 26 — the one thing loam does read is\n// a dynamic view's declared steps, and only in the fleet landscape).\n// Keep it for the renderer, which does draw it — 'npx likec4 start services/<id>' from the docs repo, pointed at THIS\n// directory. Not the repo root: every service model declares its own\n// 'specification' block, so the root is scoped to architecture/ (likec4.config.json)\n// and one model at a time is how these files are meant to be read.\n// Scoped to one service 'include *' is cheap; the same line in\n// architecture/landscape.likec4 walks every call in the fleet and takes minutes.\nviews {\n view of paymentService {\n include *\n }\n}\n`,\n },\n {\n artifact: \"spec.md\",\n key: \"spec\",\n required: true,\n purpose: \"the living requirements — what this service is required to do, as it stands today\",\n shape: [\n \"Frontmatter (below), then a `## Requirements` heading.\",\n \"`### Requirement: <name>` — ONE observable behaviour, written with SHALL, testable without reading the code. Add `Requirement-ID: <id>` in its body for stable identity across heading renames; legacy requirements without one continue to match by exact name.\",\n \"At least one `#### Scenario: <name>` per requirement, with Given/When/Then bullets. This is gated (`requirements.missing-scenarios`) — scenarios are the acceptance criteria and the source for tests.\",\n \"A markdown TABLE in a scenario body becomes the `Examples` of a `Scenario Outline`: the header names the placeholders the steps use (`<permission>`), one row per case. This is how a matrix is written — a permission matrix, a validation matrix, a status-code table is ONE scenario with twenty rows, never twenty scenarios. `loam gherkin` emits the outline, cucumber reports one result per row carrying that scenario's digest tag, and `loam verify --results` confirms the claim only when EVERY row passed. Every row must have the header's column count and a header needs at least one row: a table loam cannot read stays in the description, the scenario then runs once instead of once per case, and the emission says so out loud.\",\n \"A step takes an ARGUMENT by carrying it INDENTED under the bullet, and this is what lets one scenario assert a whole data pass rather than a sentence. An indented table becomes that step's Gherkin DATA TABLE — `- **Then** the outbox holds:` followed by the rows it must hold; a fenced block becomes its DOCSTRING, indentation intact — `- **When** the caller posts:` followed by the JSON body. Indentation is the entire disambiguation: indented under a step means this step's rows, at the margin means the scenario's cases, and both ride into the scenario digest. Write the ASSERTIONS OUT, one bullet each, including the negative ones (`no row is created in ...`, `... is not invoked`) and each meter separately: a scenario that summarises them reads complete, emits valid Gherkin, grades green, and tests a fraction of what it claims.\",\n \"`Requires: <subject>/<permission>[, ...]` in the requirement body names the authorization it is gated on, resolved against the FLEET vocabulary in `architecture/permissions.yaml`. An entry that is not declared there is `permissions.unknown` (error) — the same grade as `Operations:`, because an invented permission reads exactly like a real one everywhere downstream. Add the permission to that file when this service enforces one nobody has written down yet; the subject is the entity it is checked ON (`user`, `profile`, `service`), which is not always the caller.\",\n \"`Capability: <id>[, ...]` in the requirement body names the fleet capabilities this requirement realizes part of, declared in `architecture/capabilities.yaml`. A LIST, because the relation is many-to-many: one requirement commonly closes part of two capabilities. The FILE is this axis's opt-in — a fleet without it gets no capability findings — and once it exists an undeclared name is `capability.unknown` (error). Write the line only where the requirement genuinely carries part of the named capability.\",\n \"Write the requirement body in EARS form — `WHEN <trigger> THE SYSTEM SHALL <response>`, `IF <precondition> THEN THE SYSTEM SHALL <response>`, `WHERE <feature is included> THE SYSTEM SHALL <response>`, or a bare `THE SYSTEM SHALL <response>` when it is unconditional. **Nothing checks this** — it is a writing convention, and it is here because the grammar forces the trigger and the response to be named separately, while free prose lets a requirement read as complete while stating no condition at all. That is the exact shape a guard collapses into when it is summarised rather than enumerated.\",\n \"`Operations: <operationId>[, <operationId>]` in the requirement body names the API operations it governs. Every one must exist in this service's openapi.yaml — a name that does not is `spec-api.op-undefined` (error), reported against the LIVING spec, not only inside a feature delta.\",\n \"Document what the service DOES today, not what it should do. This is the baseline; changes to it belong in a feature delta.\",\n ],\n example: `---\nservice: payment-service\nstatus: draft\nowner: payments-team\nsources:\n - src/main/java/com/shop/payment/\n---\n\n# payment-service\n\n## Requirements\n\n### Requirement: Authorize a payment\nWHEN authorization is requested for a captured-eligible order THE SYSTEM SHALL\nauthorize it against the issuer before any capture.\n\nOperations: authorizePayment\n\n#### Scenario: Successful authorization\n- **Given** a valid card and an amount within its limit\n- **When** authorization is requested\n- **Then** the payment is authorized and an authorization id is returned\n\n### Requirement: Refunding is permission-gated\nIF the caller lacks the refund permission THEN THE SYSTEM SHALL refuse the\nrequest and leave the payment untouched.\n\nOperations: refundPayment\n\n#### Scenario: Refund by permission\n- **Given** a captured payment\n- **When** a caller holding <permission> requests a refund\n- **Then** the response is <status> and the payment is <state>\n\n| permission | status | state |\n|-----------------|--------|----------|\n| payments:refund | 200 | refunded |\n| payments:read | 403 | captured |\n| (none) | 401 | captured |\n`,\n },\n {\n artifact: \"arch.spec.md\",\n key: \"archSpec\",\n required: false,\n purpose:\n \"the living architecture requirements — the obligations the business spec never carries: outbox, retries, idempotency, metrics, alerts\",\n shape: [\n \"Same grammar and frontmatter as spec.md: `## Requirements`, `### Requirement:` + `#### Scenario:` with Given/When/Then. The same checks read it.\",\n \"A `Covers:` line per requirement names what its scenarios exercise: a C4 element id (`paymentService.db`), an edge (`paymentService -> kafka`), or a health signal from health.yaml (`alert:<id>` / `sli:<id>`). Every entry must resolve, or `covers.unknown` (warn) flags the typo.\",\n \"Optional, but expected for a service with real architecture: every alert/SLI health.yaml declares wants a covering requirement here (`health.uncovered`, warn) — a signal nothing tests is dashboard decoration.\",\n \"Write the obligations no business requirement states — the transactional outbox, what a caller's retry may assume, what pages whom. An arch scenario becomes an integration/ops test, not an acceptance test.\",\n ],\n example: `---\nservice: payment-service\nstatus: draft\nowner: payments-team\nsources:\n - src/main/java/com/shop/payment/\n---\n\n# payment-service — architecture\n\n## Requirements\n\n### Requirement: Events leave through the transactional outbox\nThe service SHALL write a domain event and its state change in one transaction,\npublished by an outbox relay — never a dual write.\n\nCovers: paymentService.db, paymentService -> kafka\n\n#### Scenario: Broker down at commit time\n- **Given** an event in the outbox\n- **When** the broker is unavailable\n- **Then** the state stays committed and the event is published once it returns\n`,\n },\n {\n artifact: \"openapi.yaml\",\n key: \"openapi\",\n // Overridden per service in `serviceBrief` (brief.ts): an API is required\n // only where the fleet expects one. See `apiExpected` there.\n required: true,\n purpose: \"the API contract — the operations this service exposes\",\n shape: [\n \"OpenAPI 3.x with a `paths` map.\",\n \"EVERY operation carries an `operationId`. That token is the spine: the C4 edge's `metadata { op }`, the requirement's `Operations:` line and this `operationId` are one name spelled three times, and they must be identical.\",\n \"loam reads operationIds, and probes the rest by PRESENCE only: an operation whose responses declare no schema warns (`openapi.response-undescribed`), an internal `$ref` to nothing warns (`openapi.ref-unresolved`). What a declared schema SAYS is still never checked — the schemas are for humans, codegen and review, so getting them wrong is invisible here and expensive later.\",\n \"Document the endpoints that exist. An operation the fleet already calls (see `landscape.expects`) must be in here, or the contract breaks the moment this lands.\",\n // The instruction the brief was missing, and the one that matters most:\n // it used to mark this artifact required unconditionally, in capitals,\n // which for a UI, a worker or a cron told the agent to invent an API into\n // the source of truth. `required` is derived now; this says so in prose\n // as well, because an agent reading the shape rules must not re-derive\n // \"MISSING\" as \"write something\".\n \"If this service exposes no HTTP API, there is no file to write. Do not invent one — an empty or imagined contract is a claim the whole fleet then joins against. `required` above is derived from the fleet map: it is false exactly when the landscape parses and no edge calls an operation on this service, which is the same evidence that keeps `service.no-openapi` quiet.\",\n ],\n },\n {\n artifact: \"asyncapi.yaml\",\n key: \"asyncapi\",\n // Never required. Unlike the HTTP contract, whose necessity the fleet map\n // can PROVE (an op-linked edge points here), most services in a legacy fleet\n // touch no topic at all — and a required artifact nobody owes is how a\n // whole adoption wave learns to ignore the brief.\n required: false,\n purpose: \"the async contract — the messages this service puts on and takes off the bus\",\n shape: [\n \"AsyncAPI **3.0** with `channels`, `operations` and `components.messages`. Only 3.0 is read: its operations are named top-level objects carrying `action: send|receive`, which is what makes a message's direction legible. A 2.x document declares no `operations` and reads as a contract with no messages.\",\n \"EVERY message carries a `name` (or is declared under the key you intend to reference). That token is the spine: the C4 edge's `metadata { publishes }` / `metadata { consumes }`, the requirement's `Publishes:` / `Consumes:` line and this name are one name spelled three times.\",\n \"Namespace the name by the domain that owns it — `payment.PaymentAuthorized`. One message has exactly one producer; two services declaring they send one name is `asyncapi.message-contested`, and then which contract a consumer reads is a coin flip.\",\n \"loam reads message names and the `action` of the operation carrying them. **It never joins on anything inside `payload`** — the one look inside is a presence probe: a payload declaring no shape at all warns (`asyncapi.payload-undescribed`), and a payload with a non-JSON `schemaFormat` is skipped outright. Write the payload as JSON Schema today; if the fleet later adopts Avro, that is a `schemaFormat` line here and nothing in loam changes.\",\n \"Keep payloads in `components.schemas` in THIS file rather than `$ref`-ing an external `.avsc`. External references are out of scope for the merge, exactly as on the OpenAPI axis, so a schema in another file will not travel with the message that needs it.\",\n \"If this service publishes and consumes nothing, there is no file to write. Do not invent one.\",\n ],\n },\n {\n artifact: \"adrs/\",\n key: \"adrsDir\",\n required: false,\n purpose: \"the decisions behind the shape of the service\",\n shape: [\n \"One file per decision: `adrs/NNNN-<slug>.md`, MADR-style (context · decision · consequences).\",\n \"Only decisions whose consequences are still visible in the code. A reconstructed rationale for something nobody decided is fiction, and it is the kind that survives review because it sounds reasonable.\",\n \"If a decision is evident but its reasoning is not, write the decision and say the reasoning is unknown. That is a fact; a plausible motive is not.\",\n ],\n },\n {\n artifact: \"runbook.md\",\n key: \"runbook\",\n required: false,\n purpose: \"how it is run — deploys, dependencies, what to do when it pages\",\n shape: [\n \"Frontmatter, then how it is deployed and configured, what it depends on, and what to do about the failures that actually happen.\",\n \"Written from what is in the repo (pipelines, manifests, alert definitions). Whoever carries the pager verifies it; leave `status: draft` until they have.\",\n \"A 'configured but not a dependency, and why' list: every capability the configuration wires up that the deploy manifest switches off. Without it, the next reader re-derives the dependency from the config and re-adds what the manifest already removed.\",\n ],\n },\n {\n artifact: \"health.yaml\",\n key: \"health\",\n required: false,\n purpose: \"SLIs, SLOs, checks and the dependencies that are critical\",\n shape: [\n \"SLIs and SLOs as they have been AGREED, plus health checks and critical dependencies.\",\n \"If no SLO has ever been agreed, say so and leave it out. An invented 99.9% becomes a number other teams plan against.\",\n ],\n },\n];\n"]}
|
|
1
|
+
{"version":3,"file":"targets.js","sourceRoot":"","sources":["../../../src/core/brief/targets.ts"],"names":[],"mappings":"AAAA;;;;;;;;GAQG;AAiDH,iEAAiE;AACjE,MAAM,CAAC,MAAM,SAAS,GAA8E;IAClG;QACE,QAAQ,EAAE,cAAc;QACxB,GAAG,EAAE,OAAO;QACZ,QAAQ,EAAE,IAAI;QACd,OAAO,EAAE,oEAAoE;QAC7E,uEAAuE;QACvE,2EAA2E;QAC3E,4EAA4E;QAC5E,qEAAqE;QACrE,oEAAoE;QACpE,8CAA8C;QAC9C,KAAK,EAAE;YACL,2OAA2O;YAC3O,0NAA0N;YAC1N,oYAAoY;YACpY,6YAA6Y;SAC9Y;QACD,OAAO,EAAE;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;CAiCZ;KACE;IACD;QACE,QAAQ,EAAE,SAAS;QACnB,GAAG,EAAE,MAAM;QACX,QAAQ,EAAE,IAAI;QACd,OAAO,EAAE,mFAAmF;QAC5F,KAAK,EAAE;YACL,wDAAwD;YACxD,kQAAkQ;YAClQ,wMAAwM;YACxM,wtBAAwtB;YACxtB,o0BAAo0B;YACp0B,wjBAAwjB;YACxjB,4fAA4f;YAC5f,slBAAslB;YACtlB,6RAA6R;YAC7R,6HAA6H;SAC9H;QACD,OAAO,EAAE;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;CAuCZ;KACE;IACD;QACE,QAAQ,EAAE,cAAc;QACxB,GAAG,EAAE,UAAU;QACf,QAAQ,EAAE,KAAK;QACf,OAAO,EACL,uIAAuI;QACzI,KAAK,EAAE;YACL,kJAAkJ;YAClJ,uRAAuR;YACvR,kNAAkN;YAClN,+MAA+M;SAChN;QACD,OAAO,EAAE;;;;;;;;;;;;;;;;;;;;;;CAsBZ;KACE;IACD;QACE,QAAQ,EAAE,cAAc;QACxB,GAAG,EAAE,SAAS;QACd,0EAA0E;QAC1E,6DAA6D;QAC7D,QAAQ,EAAE,IAAI;QACd,OAAO,EAAE,wDAAwD;QACjE,KAAK,EAAE;YACL,iCAAiC;YACjC,+NAA+N;YAC/N,yXAAyX;YACzX,kKAAkK;YAClK,wEAAwE;YACxE,uEAAuE;YACvE,0EAA0E;YAC1E,wEAAwE;YACxE,uEAAuE;YACvE,kCAAkC;YAClC,kXAAkX;SACnX;KACF;IACD;QACE,QAAQ,EAAE,eAAe;QACzB,GAAG,EAAE,UAAU;QACf,0EAA0E;QAC1E,6EAA6E;QAC7E,uEAAuE;QACvE,kDAAkD;QAClD,QAAQ,EAAE,KAAK;QACf,OAAO,EAAE,8EAA8E;QACvF,KAAK,EAAE;YACL,8SAA8S;YAC9S,qRAAqR;YACrR,wPAAwP;YACxP,4bAA4b;YAC5b,gQAAgQ;YAChQ,+FAA+F;SAChG;KACF;IACD;QACE,QAAQ,EAAE,OAAO;QACjB,GAAG,EAAE,SAAS;QACd,QAAQ,EAAE,KAAK;QACf,OAAO,EAAE,+CAA+C;QACxD,KAAK,EAAE;YACL,+FAA+F;YAC/F,2MAA2M;YAC3M,oJAAoJ;SACrJ;KACF;IACD;QACE,QAAQ,EAAE,YAAY;QACtB,GAAG,EAAE,SAAS;QACd,QAAQ,EAAE,KAAK;QACf,OAAO,EAAE,iEAAiE;QAC1E,KAAK,EAAE;YACL,kIAAkI;YAClI,2JAA2J;YAC3J,4PAA4P;SAC7P;KACF;IACD;QACE,QAAQ,EAAE,aAAa;QACvB,GAAG,EAAE,QAAQ;QACb,QAAQ,EAAE,KAAK;QACf,OAAO,EAAE,2DAA2D;QACpE,KAAK,EAAE;YACL,uFAAuF;YACvF,uHAAuH;SACxH;KACF;CACF,CAAC","sourcesContent":["/**\n * The artifacts of the adoption brief: what each file under `services/<svc>/`\n * is for, the grammar it has to be written in, and a minimal valid example\n * where showing one is shorter than describing it. Every `shape` rule here is\n * one a later check depends on, and test/agent-contract.test.ts pins the\n * mapping. This module is the list alone — `serviceBrief` (brief.ts) resolves\n * it against a docs repo, and the fleet map's own target, whose block names\n * the service, is assembled per service in landscape.ts.\n */\n\n/* ------------------------------------------------------------------ */\n/* The artifacts */\n/* ------------------------------------------------------------------ */\n\nexport interface BriefTarget {\n /** File name as it appears under `services/<svc>/`. */\n artifact: string;\n /** Repo-relative path to write. */\n path: string;\n exists: boolean;\n /**\n * `create` a missing artifact; `diff` an existing one — never overwrite it.\n *\n * `edit` is the third case and belongs to exactly one target: the fleet map.\n * `architecture/landscape.likec4` is a SHARED document that already holds\n * every other service, so \"diff it and report what disagrees\" is the wrong\n * instruction — the file is not this service's baseline, it is the fleet's,\n * and what this service owes it is an addition inside the existing\n * `model { … }` block. Spelling that as `diff` taught agents to leave the\n * fleet map alone; spelling it as `create` would have taught them to\n * overwrite ten other services.\n */\n action: \"create\" | \"diff\" | \"edit\";\n /**\n * False for artifacts a baseline can legitimately ship without. True means\n * required for a COMPLETE baseline — validate grades a missing spec.md or\n * openapi.yaml as a warn (`service.no-spec` / `service.no-openapi`), not an\n * error: partial adoption is a supported state, and the warns are the\n * progress meter.\n *\n * Constant per artifact except one: `openapi.yaml` is required only where the\n * fleet map shows an API is expected (see `apiExpected` in brief.ts), because\n * the brief is read as an instruction and \"MISSING openapi.yaml\" told an agent\n * to write a contract for a service that serves no requests.\n */\n required: boolean;\n /** What the artifact is for, in one line. */\n purpose: string;\n /** The grammar. Every rule here is one a later check depends on. */\n shape: string[];\n /** A minimal valid instance, where showing one is shorter than describing it. */\n example?: string;\n}\n\n/** Where an artifact lives — `repo/paths.ts` spells the filenames, this only points at them. */\ntype PathKey = \"model\" | \"spec\" | \"archSpec\" | \"openapi\" | \"asyncapi\" | \"adrsDir\" | \"runbook\" | \"health\";\n\n/** The artifacts, in the order a baseline is best written in. */\nexport const ARTIFACTS: Array<Omit<BriefTarget, \"path\" | \"exists\" | \"action\"> & { key: PathKey }> = [\n {\n artifact: \"model.likec4\",\n key: \"model\",\n required: true,\n purpose: \"the service's C4 — what it is, what is inside it, what it talks to\",\n // Every rule below is one a later check depends on, and the mapping is\n // pinned in test/agent-contract.test.ts. Rules nobody checks — the `views`\n // block, \"exactly one top-level element\" — used to sit here reading exactly\n // like the enforced ones, which is how a brief teaches an agent that\n // `loam validate` passing means more than it does. They now live in\n // UNCHECKED, where their status is the point.\n shape: [\n \"A `specification { ... }` block declaring every element kind you use (`element softwareSystem`, `element container`). LikeC4 rejects an undeclared kind, and `loam validate --service <id>` reports it as `c4.invalid` with line numbers.\",\n \"A `model { ... }` block holding the service's element, with containers and components nested inside it. The whole file has to parse — `loam validate` runs LikeC4 in-process and reports `c4.invalid` with line numbers.\",\n \"The service element binds to this directory: `metadata { service '<id>' }`. Without a binding the element's TITLE has to equal the directory name — and then renaming the box silently unlinks every check that joined the two. The same binding is what makes the FLEET map resolve an element to `services/<id>/`; until one does, `loam validate --all` reports `landscape.service-unmodelled`.\",\n \"A call to another service is an edge that names the operation it uses: `a -> b 'Calls createSplit' { metadata { op 'createSplit' } }`. Cross-service edges belong in `architecture/landscape.likec4` (a target of its own, below) — this file cannot even resolve the other service's element — and there the `op` must be an operationId the TARGET's openapi.yaml defines, or `spine.op-undefined` fires.\",\n ],\n example: `specification {\n element softwareSystem\n element container\n}\n\nmodel {\n paymentService = softwareSystem 'payment-service' {\n description 'Owns payment authorization and capture'\n metadata { service 'payment-service' }\n\n api = container 'HTTP API'\n ledger = container 'Ledger store'\n }\n}\n\n// Views are LikeC4's, not loam's: loam parses this file and computes no view,\n// and it never reads a STATIC view's contents in any document, so nothing below\n// is read by any check (docs/DESIGN.md rule 26 — the one thing loam does read is\n// a dynamic view's declared steps, and only in the fleet landscape).\n// Keep it for the renderer, which does draw it — 'npx likec4 start' from the docs\n// repo ROOT, once 'loam subsystem sync' has written services/<id>/likec4.config.json:\n// that file is loam's, never yours, and it is what registers this model as a\n// LikeC4 project of its own beside the fleet map. The root project stays scoped\n// to architecture/ (the root likec4.config.json) because every model declares\n// its own 'specification' block, and a root that merged them would read each\n// declaration as a duplicate.\n// Scoped to one service 'include *' is cheap; the same line in\n// architecture/landscape.likec4 walks every call in the fleet and takes minutes.\nviews {\n view of paymentService {\n include *\n }\n}\n`,\n },\n {\n artifact: \"spec.md\",\n key: \"spec\",\n required: true,\n purpose: \"the living requirements — what this service is required to do, as it stands today\",\n shape: [\n \"Frontmatter (below), then a `## Requirements` heading.\",\n \"`### Requirement: <name>` — ONE observable behaviour, written with SHALL, testable without reading the code. Add `Requirement-ID: <id>` in its body for stable identity across heading renames; legacy requirements without one continue to match by exact name.\",\n \"At least one `#### Scenario: <name>` per requirement, with Given/When/Then bullets. This is gated (`requirements.missing-scenarios`) — scenarios are the acceptance criteria and the source for tests.\",\n \"A markdown TABLE in a scenario body becomes the `Examples` of a `Scenario Outline`: the header names the placeholders the steps use (`<permission>`), one row per case. This is how a matrix is written — a permission matrix, a validation matrix, a status-code table is ONE scenario with twenty rows, never twenty scenarios. `loam gherkin` emits the outline, cucumber reports one result per row carrying that scenario's digest tag, and `loam verify --results` confirms the claim only when EVERY row passed. Every row must have the header's column count and a header needs at least one row: a table loam cannot read stays in the description, the scenario then runs once instead of once per case, and the emission says so out loud.\",\n \"A step takes an ARGUMENT by carrying it INDENTED under the bullet, and this is what lets one scenario assert a whole data pass rather than a sentence. An indented table becomes that step's Gherkin DATA TABLE — `- **Then** the outbox holds:` followed by the rows it must hold; a fenced block becomes its DOCSTRING, indentation intact — `- **When** the caller posts:` followed by the JSON body. Indentation is the entire disambiguation: indented under a step means this step's rows, at the margin means the scenario's cases, and both ride into the scenario digest. Write the ASSERTIONS OUT, one bullet each, including the negative ones (`no row is created in ...`, `... is not invoked`) and each meter separately: a scenario that summarises them reads complete, emits valid Gherkin, grades green, and tests a fraction of what it claims.\",\n \"`Requires: <subject>/<permission>[, ...]` in the requirement body names the authorization it is gated on, resolved against the FLEET vocabulary in `architecture/permissions.yaml`. An entry that is not declared there is `permissions.unknown` (error) — the same grade as `Operations:`, because an invented permission reads exactly like a real one everywhere downstream. Add the permission to that file when this service enforces one nobody has written down yet; the subject is the entity it is checked ON (`user`, `profile`, `service`), which is not always the caller.\",\n \"`Capability: <id>[, ...]` in the requirement body names the fleet capabilities this requirement realizes part of, declared in `architecture/capabilities.yaml`. A LIST, because the relation is many-to-many: one requirement commonly closes part of two capabilities. The FILE is this axis's opt-in — a fleet without it gets no capability findings — and once it exists an undeclared name is `capability.unknown` (error). Write the line only where the requirement genuinely carries part of the named capability.\",\n \"Write the requirement body in EARS form — `WHEN <trigger> THE SYSTEM SHALL <response>`, `IF <precondition> THEN THE SYSTEM SHALL <response>`, `WHERE <feature is included> THE SYSTEM SHALL <response>`, or a bare `THE SYSTEM SHALL <response>` when it is unconditional. **Nothing checks this** — it is a writing convention, and it is here because the grammar forces the trigger and the response to be named separately, while free prose lets a requirement read as complete while stating no condition at all. That is the exact shape a guard collapses into when it is summarised rather than enumerated.\",\n \"`Operations: <operationId>[, <operationId>]` in the requirement body names the API operations it governs. Every one must exist in this service's openapi.yaml — a name that does not is `spec-api.op-undefined` (error), reported against the LIVING spec, not only inside a feature delta.\",\n \"Document what the service DOES today, not what it should do. This is the baseline; changes to it belong in a feature delta.\",\n ],\n example: `---\nservice: payment-service\nstatus: draft\nowner: payments-team\nsources:\n - src/main/java/com/shop/payment/\n---\n\n# payment-service\n\n## Requirements\n\n### Requirement: Authorize a payment\nWHEN authorization is requested for a captured-eligible order THE SYSTEM SHALL\nauthorize it against the issuer before any capture.\n\nOperations: authorizePayment\n\n#### Scenario: Successful authorization\n- **Given** a valid card and an amount within its limit\n- **When** authorization is requested\n- **Then** the payment is authorized and an authorization id is returned\n\n### Requirement: Refunding is permission-gated\nIF the caller lacks the refund permission THEN THE SYSTEM SHALL refuse the\nrequest and leave the payment untouched.\n\nOperations: refundPayment\n\n#### Scenario: Refund by permission\n- **Given** a captured payment\n- **When** a caller holding <permission> requests a refund\n- **Then** the response is <status> and the payment is <state>\n\n| permission | status | state |\n|-----------------|--------|----------|\n| payments:refund | 200 | refunded |\n| payments:read | 403 | captured |\n| (none) | 401 | captured |\n`,\n },\n {\n artifact: \"arch.spec.md\",\n key: \"archSpec\",\n required: false,\n purpose:\n \"the living architecture requirements — the obligations the business spec never carries: outbox, retries, idempotency, metrics, alerts\",\n shape: [\n \"Same grammar and frontmatter as spec.md: `## Requirements`, `### Requirement:` + `#### Scenario:` with Given/When/Then. The same checks read it.\",\n \"A `Covers:` line per requirement names what its scenarios exercise: a C4 element id (`paymentService.db`), an edge (`paymentService -> kafka`), or a health signal from health.yaml (`alert:<id>` / `sli:<id>`). Every entry must resolve, or `covers.unknown` (warn) flags the typo.\",\n \"Optional, but expected for a service with real architecture: every alert/SLI health.yaml declares wants a covering requirement here (`health.uncovered`, warn) — a signal nothing tests is dashboard decoration.\",\n \"Write the obligations no business requirement states — the transactional outbox, what a caller's retry may assume, what pages whom. An arch scenario becomes an integration/ops test, not an acceptance test.\",\n ],\n example: `---\nservice: payment-service\nstatus: draft\nowner: payments-team\nsources:\n - src/main/java/com/shop/payment/\n---\n\n# payment-service — architecture\n\n## Requirements\n\n### Requirement: Events leave through the transactional outbox\nThe service SHALL write a domain event and its state change in one transaction,\npublished by an outbox relay — never a dual write.\n\nCovers: paymentService.db, paymentService -> kafka\n\n#### Scenario: Broker down at commit time\n- **Given** an event in the outbox\n- **When** the broker is unavailable\n- **Then** the state stays committed and the event is published once it returns\n`,\n },\n {\n artifact: \"openapi.yaml\",\n key: \"openapi\",\n // Overridden per service in `serviceBrief` (brief.ts): an API is required\n // only where the fleet expects one. See `apiExpected` there.\n required: true,\n purpose: \"the API contract — the operations this service exposes\",\n shape: [\n \"OpenAPI 3.x with a `paths` map.\",\n \"EVERY operation carries an `operationId`. That token is the spine: the C4 edge's `metadata { op }`, the requirement's `Operations:` line and this `operationId` are one name spelled three times, and they must be identical.\",\n \"loam reads operationIds, and probes the rest by PRESENCE only: an operation whose responses declare no schema warns (`openapi.response-undescribed`), an internal `$ref` to nothing warns (`openapi.ref-unresolved`). What a declared schema SAYS is still never checked — the schemas are for humans, codegen and review, so getting them wrong is invisible here and expensive later.\",\n \"Document the endpoints that exist. An operation the fleet already calls (see `landscape.expects`) must be in here, or the contract breaks the moment this lands.\",\n // The instruction the brief was missing, and the one that matters most:\n // it used to mark this artifact required unconditionally, in capitals,\n // which for a UI, a worker or a cron told the agent to invent an API into\n // the source of truth. `required` is derived now; this says so in prose\n // as well, because an agent reading the shape rules must not re-derive\n // \"MISSING\" as \"write something\".\n \"If this service exposes no HTTP API, there is no file to write. Do not invent one — an empty or imagined contract is a claim the whole fleet then joins against. `required` above is derived from the fleet map: it is false exactly when the landscape parses and no edge calls an operation on this service, which is the same evidence that keeps `service.no-openapi` quiet.\",\n ],\n },\n {\n artifact: \"asyncapi.yaml\",\n key: \"asyncapi\",\n // Never required. Unlike the HTTP contract, whose necessity the fleet map\n // can PROVE (an op-linked edge points here), most services in a legacy fleet\n // touch no topic at all — and a required artifact nobody owes is how a\n // whole adoption wave learns to ignore the brief.\n required: false,\n purpose: \"the async contract — the messages this service puts on and takes off the bus\",\n shape: [\n \"AsyncAPI **3.0** with `channels`, `operations` and `components.messages`. Only 3.0 is read: its operations are named top-level objects carrying `action: send|receive`, which is what makes a message's direction legible. A 2.x document declares no `operations` and reads as a contract with no messages.\",\n \"EVERY message carries a `name` (or is declared under the key you intend to reference). That token is the spine: the C4 edge's `metadata { publishes }` / `metadata { consumes }`, the requirement's `Publishes:` / `Consumes:` line and this name are one name spelled three times.\",\n \"Namespace the name by the domain that owns it — `payment.PaymentAuthorized`. One message has exactly one producer; two services declaring they send one name is `asyncapi.message-contested`, and then which contract a consumer reads is a coin flip.\",\n \"loam reads message names and the `action` of the operation carrying them. **It never joins on anything inside `payload`** — the one look inside is a presence probe: a payload declaring no shape at all warns (`asyncapi.payload-undescribed`), and a payload with a non-JSON `schemaFormat` is skipped outright. Write the payload as JSON Schema today; if the fleet later adopts Avro, that is a `schemaFormat` line here and nothing in loam changes.\",\n \"Keep payloads in `components.schemas` in THIS file rather than `$ref`-ing an external `.avsc`. External references are out of scope for the merge, exactly as on the OpenAPI axis, so a schema in another file will not travel with the message that needs it.\",\n \"If this service publishes and consumes nothing, there is no file to write. Do not invent one.\",\n ],\n },\n {\n artifact: \"adrs/\",\n key: \"adrsDir\",\n required: false,\n purpose: \"the decisions behind the shape of the service\",\n shape: [\n \"One file per decision: `adrs/NNNN-<slug>.md`, MADR-style (context · decision · consequences).\",\n \"Only decisions whose consequences are still visible in the code. A reconstructed rationale for something nobody decided is fiction, and it is the kind that survives review because it sounds reasonable.\",\n \"If a decision is evident but its reasoning is not, write the decision and say the reasoning is unknown. That is a fact; a plausible motive is not.\",\n ],\n },\n {\n artifact: \"runbook.md\",\n key: \"runbook\",\n required: false,\n purpose: \"how it is run — deploys, dependencies, what to do when it pages\",\n shape: [\n \"Frontmatter, then how it is deployed and configured, what it depends on, and what to do about the failures that actually happen.\",\n \"Written from what is in the repo (pipelines, manifests, alert definitions). Whoever carries the pager verifies it; leave `status: draft` until they have.\",\n \"A 'configured but not a dependency, and why' list: every capability the configuration wires up that the deploy manifest switches off. Without it, the next reader re-derives the dependency from the config and re-adds what the manifest already removed.\",\n ],\n },\n {\n artifact: \"health.yaml\",\n key: \"health\",\n required: false,\n purpose: \"SLIs, SLOs, checks and the dependencies that are critical\",\n shape: [\n \"SLIs and SLOs as they have been AGREED, plus health checks and critical dependencies.\",\n \"If no SLO has ever been agreed, say so and leave it out. An invented 99.9% becomes a number other teams plan against.\",\n ],\n },\n];\n"]}
|
|
@@ -22,7 +22,7 @@ export const UNCHECKED = [
|
|
|
22
22
|
// grades their absence, and the failure this comment records — agents
|
|
23
23
|
// learning that a views block is owed to loam — is the one rule 26 is
|
|
24
24
|
// written to prevent recurring.
|
|
25
|
-
"Whether model.likec4 declares a `views { ... }` block, or any view at all — and nothing in loam ever will. What loam reads, when a `dynamic view` IS present, is only what its author declared: the view's tags and its ordered steps. It computes no view and renders nothing, and it never grades a model for lacking one — a fleet that draws no diagrams owes loam no views block, today or ever. Views belong to LikeC4's own renderer: write them if you want diagrams, and read them with `npx likec4 start
|
|
25
|
+
"Whether model.likec4 declares a `views { ... }` block, or any view at all — and nothing in loam ever will. What loam reads, when a `dynamic view` IS present, is only what its author declared: the view's tags and its ordered steps. It computes no view and renders nothing, and it never grades a model for lacking one — a fleet that draws no diagrams owes loam no views block, today or ever. Views belong to LikeC4's own renderer: write them if you want diagrams, and read them with `npx likec4 start` at the docs repo root, which shows the fleet map (the root project, which `likec4.config.json` scopes to `architecture/`) and one project per service model — the `services/<id>/likec4.config.json` that `loam subsystem sync` writes beside each model.likec4, loam's file and never yours. A feature's delta.likec4 is the one document still pointed at alone (`npx likec4 start features/<FEAT>`). The renderer merges every `.likec4` file a project holds into one model, and loam parses each of them alone, so each declares its own `specification` block: that is why the root project excludes `services/**`, and why a model with no project file of its own renders nowhere from the docs root. Scope your views too — computing a view is superlinear in the number of edges, and an `include *` over `architecture/landscape.likec4` is the expensive one, because that file holds every call in the fleet.",
|
|
26
26
|
// The readability half of the entry above. They are separate entries because
|
|
27
27
|
// the failures are: one is a view that takes minutes to compute, the other is
|
|
28
28
|
// a view that computes instantly and cannot be read. A fleet acquires the
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"unchecked.js","sourceRoot":"","sources":["../../../src/core/brief/unchecked.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;GAWG;AACH,MAAM,CAAC,MAAM,SAAS,GAAa;IACjC,4EAA4E;IAC5E,uEAAuE;IACvE,4EAA4E;IAC5E,2EAA2E;IAC3E,qDAAqD;IACrD,EAAE;IACF,6EAA6E;IAC7E,2EAA2E;IAC3E,sEAAsE;IACtE,sEAAsE;IACtE,gCAAgC;IAChC,
|
|
1
|
+
{"version":3,"file":"unchecked.js","sourceRoot":"","sources":["../../../src/core/brief/unchecked.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;GAWG;AACH,MAAM,CAAC,MAAM,SAAS,GAAa;IACjC,4EAA4E;IAC5E,uEAAuE;IACvE,4EAA4E;IAC5E,2EAA2E;IAC3E,qDAAqD;IACrD,EAAE;IACF,6EAA6E;IAC7E,2EAA2E;IAC3E,sEAAsE;IACtE,sEAAsE;IACtE,gCAAgC;IAChC,i3CAAi3C;IACj3C,6EAA6E;IAC7E,8EAA8E;IAC9E,0EAA0E;IAC1E,6EAA6E;IAC7E,6EAA6E;IAC7E,kDAAkD;IAClD,oqCAAoqC;IACpqC,wUAAwU;IACxU,8HAA8H;IAC9H,yJAAyJ;IACzJ,oHAAoH;IACpH,wUAAwU;IACxU,yFAAyF;IACzF,6LAA6L;IAC7L,uGAAuG;IACvG,gnBAAgnB;IAChnB,yJAAyJ;IACzJ,mXAAmX;IACnX,2RAA2R;IAC3R,mjBAAmjB;IACnjB,gIAAgI;CACjI,CAAC","sourcesContent":["/**\n * What no check will ever tell you. Read it as the list of ways to be wrong\n * quietly. The checks that do exist — the table an adoption baseline actually\n * meets — live in checks.ts; this module is that table's counterpart, split\n * out so each half can grow without crowding the other.\n *\n * The first two entries are here because they used to be `shape` rules on\n * model.likec4, phrased exactly like the enforced ones. A brief that states an\n * unenforced rule beside four enforced ones is not merely useless — it is the\n * way an agent learns that a green `loam validate` means more than it does.\n * Anything nothing checks belongs on this list, where its status IS the point.\n */\nexport const UNCHECKED: string[] = [\n // Not merely unchecked — unread. This entry used to end \"write one\", on the\n // theory that a view is what makes a model legible. It is, to LikeC4's\n // renderer; loam computes none. Saying otherwise taught agents that a views\n // block was owed to loam, and an `include *` over the FLEET map is the one\n // shape that costs minutes rather than milliseconds.\n //\n // Still exactly true of PRESENCE, which is what this entry is about. Rule 26\n // lets loam read a dynamic view's declared steps WHERE ONE EXISTS; nothing\n // grades their absence, and the failure this comment records — agents\n // learning that a views block is owed to loam — is the one rule 26 is\n // written to prevent recurring.\n \"Whether model.likec4 declares a `views { ... }` block, or any view at all — and nothing in loam ever will. What loam reads, when a `dynamic view` IS present, is only what its author declared: the view's tags and its ordered steps. It computes no view and renders nothing, and it never grades a model for lacking one — a fleet that draws no diagrams owes loam no views block, today or ever. Views belong to LikeC4's own renderer: write them if you want diagrams, and read them with `npx likec4 start` at the docs repo root, which shows the fleet map (the root project, which `likec4.config.json` scopes to `architecture/`) and one project per service model — the `services/<id>/likec4.config.json` that `loam subsystem sync` writes beside each model.likec4, loam's file and never yours. A feature's delta.likec4 is the one document still pointed at alone (`npx likec4 start features/<FEAT>`). The renderer merges every `.likec4` file a project holds into one model, and loam parses each of them alone, so each declares its own `specification` block: that is why the root project excludes `services/**`, and why a model with no project file of its own renders nowhere from the docs root. Scope your views too — computing a view is superlinear in the number of edges, and an `include *` over `architecture/landscape.likec4` is the expensive one, because that file holds every call in the fleet.\",\n // The readability half of the entry above. They are separate entries because\n // the failures are: one is a view that takes minutes to compute, the other is\n // a view that computes instantly and cannot be read. A fleet acquires the\n // second one the day its second service starts publishing, and every service\n // after that makes it worse by exactly one edge — which is why it arrives as\n // a surprise on a map that was fine last quarter.\n \"Whether the fleet map is LEGIBLE — nothing in loam draws it, so nothing in loam notices. The shape that goes first is a shared broker: Kafka, a bus, a gateway, drawn as ONE element, is the node every service in the fleet points at, and a view over it is a star with sixty spokes. Model the TOPIC rather than the broker — `paymentEvents = topic 'payment.events'` nested inside the broker's element, with the edges pointing at `kafka.paymentEvents` — and one hub of degree sixty becomes twelve of degree five, which is also the truer model: the contract a producer and a consumer share is the topic, never the broker. Declare `element topic { #external, style { shape queue } }` in the `specification` block and give the nested elements that kind: LikeC4 does not inherit tags, so a topic under an `#external` broker is NOT external itself and `loam validate --all` asks for a `services/payment.events/` nobody owes (`landscape.service-undocumented`). Then scope the views, which is where the rest of the legibility lives: `exclude element.tag = #external` keeps the fleet overview to the calls between our own services, and a second view over the broker draws the event spine on its own.\",\n \"Whether the model has exactly ONE top-level element for the service, with its containers nested inside. Five top-level boxes for one service parse, validate and bind exactly as well as one — and then the fleet map, which joins elements to directories, has five candidates for the same service and no way to say which is wrong.\",\n \"Whether the model is the architecture the code actually has. loam parses model.likec4; it never reads a line of the service.\",\n \"Whether a requirement is TRUE of the service. `loam validate` checks that a requirement has a scenario, never that either one describes real behaviour.\",\n \"Whether the scenarios are acceptance criteria somebody could test, or the requirement restated in Given/When/Then.\",\n \"Whether an operationId is the one the code serves, and whether the request and response schemas resemble what the endpoint really accepts and returns. Presence is probed — an operation with no response schema at all is warned about — but what a declared schema SAYS is never compared with anything; only the operationId joins.\",\n \"Whether the runbook's steps work, or the health.yaml SLOs are numbers anyone agreed to.\",\n \"Whether an arch scenario really exercises what its `Covers:` line names. The entries are resolved against the model and health.yaml — never against code, tests, dashboards or alert rules.\",\n \"Whether an ADR records a decision that was made, or one reconstructed afterwards to justify the code.\",\n \"Whether the DEPLOYMENT model is the topology the fleet actually runs. Every join over it is between documents: `instanceOf` resolves against the C4 model, a `#obl-` tag resolves against the obligation vocabulary, and a `Covers: node:` entry resolves against the nodes the map declares. Nothing here knows whether the second cluster exists, whether it is reachable, whether replication is configured, or whether it holds the data a requirement claims — and no check counts datacenters, because how many a service needs is an architectural decision the fleet makes and writes down as a requirement, never one loam evaluates.\",\n \"COMPLETENESS. Forty behaviours documented as one requirement passes every check loam has. So does a service with one endpoint documented out of thirty.\",\n \"REPRODUCIBILITY — the bar the artifact set aims at: could a reader answer, without the code, what the service exposes, what it reaches, what shapes it exchanges, how it is run and what pages whom. The depth probes catch an EMPTY contract; nothing measures a shallow one against the service, so green means the files agree with each other, never that they reach the bar.\",\n \"Whether an arch requirement recording effective configuration or a library's semantics ('retries: -1 is unbounded, proven from the bytecode') is still true of the chart and the classpath. The convention names arch.spec.md as the home for such facts; loam never verifies the values.\",\n \"Whether a permission in `architecture/permissions.yaml` is one the system actually has. `Requires:` is checked against that file and the file against nothing: a vocabulary that agrees with every requirement citing it can still name three permissions the identity provider retired last year, and `owned_by` / `enforced_by` are read as prose — neither is resolved against `services/`, and a realm is never opened. The check is a spelling guard between documents, exactly as `Operations:` is; whether the word is real is a question only the running system answers.\",\n \"Whether `sources` names the files you read. loam checks those paths exist — not that they are the ones the document came from.\",\n];\n"]}
|
package/dist/core/docs.js
CHANGED
|
@@ -14,7 +14,7 @@ import { join, resolve } from "node:path";
|
|
|
14
14
|
import { AGENTS_MD } from "./agent/agents-md.js";
|
|
15
15
|
import { LANDSCAPE_STUB } from "./scaffold/landscape.js";
|
|
16
16
|
import { README_FILENAME, README_MD } from "./scaffold/readme.js";
|
|
17
|
-
import { AGENTS_FILENAME } from "./repo/paths.js";
|
|
17
|
+
import { AGENTS_FILENAME, LIKEC4_PROJECT_FILENAME, LIKEC4_ROOT_PROJECT } from "./repo/paths.js";
|
|
18
18
|
/**
|
|
19
19
|
* Top-level layout of the shared docs repo, and part of its identity rather
|
|
20
20
|
* than a convenience: `services/` is what makes a directory a docs repo —
|
|
@@ -50,18 +50,22 @@ const EMPTY_SUBDIRS = [
|
|
|
50
50
|
* So the root is declared as one LikeC4 project holding the landscape, and the
|
|
51
51
|
* two directories whose files are meant to be read alone are excluded from it.
|
|
52
52
|
* That makes `npx likec4 start` in the docs repo — the command loam's own docs
|
|
53
|
-
* have always recommended — render the fleet map instead of failing.
|
|
54
|
-
*
|
|
55
|
-
*
|
|
53
|
+
* have always recommended — render the fleet map instead of failing. What the
|
|
54
|
+
* exclusion cost went unstated for a release: every adopted service was then a
|
|
55
|
+
* box on that map with nothing renderable inside it. A service model renders
|
|
56
|
+
* from the SAME root once `loam subsystem sync` has written a project file of
|
|
57
|
+
* its own beside it — `repo/tree/render/projects.ts` records the measurement
|
|
58
|
+
* and the rule — and only a feature's `delta.likec4`, which is transient, is
|
|
59
|
+
* still rendered by pointing the renderer at its own directory.
|
|
56
60
|
*
|
|
57
61
|
* `**\/node_modules/**` is repeated because naming `exclude` replaces LikeC4's
|
|
58
62
|
* default rather than adding to it. loam reads none of this file: it is written
|
|
59
63
|
* once, never re-read, and a team that wants a different project layout owns it
|
|
60
|
-
* like every other scaffolded file.
|
|
64
|
+
* like every other scaffolded file. The filename and the root project's name
|
|
65
|
+
* are spelled in `repo/paths.ts`, beside every other file loam writes.
|
|
61
66
|
*/
|
|
62
|
-
export const LIKEC4_PROJECT_FILENAME = "likec4.config.json";
|
|
63
67
|
export const LIKEC4_PROJECT_CONFIG = `${JSON.stringify({
|
|
64
|
-
name:
|
|
68
|
+
name: LIKEC4_ROOT_PROJECT,
|
|
65
69
|
title: "Fleet landscape",
|
|
66
70
|
exclude: ["**/node_modules/**", "services/**", "features/**"],
|
|
67
71
|
}, null, 2)}\n`;
|
package/dist/core/docs.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"docs.js","sourceRoot":"","sources":["../../src/core/docs.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * Scaffolding for the shared docs repo.\n *\n * There is deliberately no manifest. `init` used to write a `loam.docs.json`\n * listing the repo's services; nothing ever read it — `repo.ts` enumerates from\n * the filesystem, because files are the source of truth — and nothing ever\n * updated it, so it named an empty fleet forever. A second list of services is\n * exactly the drift `loam validate` now cross-checks the landscape for; the\n * cheapest way to keep it honest is not to have it.\n */\nimport { mkdir, writeFile } from \"node:fs/promises\";\nimport { existsSync } from \"node:fs\";\nimport { join, resolve } from \"node:path\";\nimport { AGENTS_MD } from \"./agent/agents-md.js\";\nimport { LANDSCAPE_STUB } from \"./scaffold/landscape.js\";\nimport { README_FILENAME, README_MD } from \"./scaffold/readme.js\";\nimport { AGENTS_FILENAME } from \"./repo/paths.js\";\n\n/**\n * Top-level layout of the shared docs repo, and part of its identity rather\n * than a convenience: `services/` is what makes a directory a docs repo —\n * `init` will not join one without it, and every enumerating command refuses a\n * `docsDir` that has none. So it is laid down even when nothing is in it yet.\n */\nexport const DOCS_SUBDIRS = [\"architecture\", \"services\", \"features\"] as const;\n\n/**\n * The scaffolded directories that start out empty, and the file that keeps each\n * of them in version control. See {@link GITKEEP}. Spelled as pairs so the\n * marker's name is written once — `listServices` and `listFeatures` enumerate\n * subdirectories only, so a file here is invisible to both.\n */\nconst EMPTY_SUBDIRS = [\n [\"services\", \".gitkeep\"],\n [\"features\", \".gitkeep\"],\n] as const;\n\n/**\n * The LikeC4 project file, and the one thing in the docs repo loam writes for a\n * tool other than itself.\n *\n * LikeC4's workspace loader merges EVERY `.likec4` file under a directory tree\n * into one model. loam's layout is the opposite by design: the landscape,\n * each `services/<id>/model.likec4` and each `features/<FEAT>/delta.likec4` is\n * parsed in isolation (`loadSource`, one file, one throwaway workspace), so\n * every one of them legitimately declares its own `specification { … }` block\n * and re-declares the elements it needs to talk about. Point the real renderer\n * at the repository root and those declarations collide: on loam's own\n * `examples/docs` — four files — `npx likec4 start` reported 16 errors, sixteen\n * duplicate kinds and names, while `loam validate --all` reported none. Both\n * were right; only one of them was reading the tree as a workspace.\n *\n * So the root is declared as one LikeC4 project holding the landscape, and the\n * two directories whose files are meant to be read alone are excluded from it.\n * That makes `npx likec4 start` in the docs repo — the command loam's own docs\n * have always recommended — render the fleet map instead of failing. A service\n * model or a feature delta is still rendered by pointing the renderer at its own\n * directory, which is what parsing them in isolation meant all along.\n *\n * `**\\/node_modules/**` is repeated because naming `exclude` replaces LikeC4's\n * default rather than adding to it. loam reads none of this file: it is written\n * once, never re-read, and a team that wants a different project layout owns it\n * like every other scaffolded file.\n */\nexport const LIKEC4_PROJECT_FILENAME = \"likec4.config.json\";\n\nexport const LIKEC4_PROJECT_CONFIG = `${JSON.stringify(\n {\n name: \"fleet\",\n title: \"Fleet landscape\",\n exclude: [\"**/node_modules/**\", \"services/**\", \"features/**\"],\n },\n null,\n 2,\n)}\\n`;\n\n/**\n * The placeholder that makes an empty scaffolded directory survive a clone.\n *\n * git tracks files, not directories, so `services/` and `features/` — created\n * empty by `loam init --create` — were absent for everyone after the first push:\n * the person who ran init had a green repo, and the second person to clone it\n * got `doctor.services-missing`, a BLOCKER, on a repository nobody had touched.\n * A repo that cannot survive being cloned is not a shared docs repo.\n */\nconst GITKEEP = \"\";\n\n/**\n * The docs repo's own loam.json. It makes the docs repo self-describing: a\n * command run from inside it (or from any directory under it) finds this file\n * first and resolves the fleet to the repo it is standing in, instead of\n * walking out to whatever service repo happens to be above.\n *\n * `\".\"` and not an absolute path for the same reason every other docsDir is\n * stored as written: this file is committed and cloned to machines whose\n * directory layout nobody here can predict.\n */\nconst DOCS_SELF_CONFIG = `${JSON.stringify({ docsDir: \".\" }, null, 2)}\\n`;\n\nexport interface ScaffoldResult {\n root: string;\n created: string[];\n}\n\n/** Idempotently create the docs-repo skeleton. Existing files/dirs are left untouched. */\nexport async function scaffoldDocs(docsDir: string): Promise<ScaffoldResult> {\n const root = resolve(docsDir);\n const created: string[] = [];\n\n await mkdir(root, { recursive: true });\n\n for (const dir of DOCS_SUBDIRS) {\n const p = join(root, dir);\n if (!existsSync(p)) {\n await mkdir(p, { recursive: true });\n created.push(p);\n }\n }\n\n // Never overwritten, all three of them — a team's own house rules, their own\n // map and their own config outrank the template. The order here IS the order\n // `init` probes for what it will skip; keep the two lists in step.\n for (const [rel, content] of docsRepoFiles()) {\n const path = join(root, rel);\n if (!existsSync(path)) {\n await writeFile(path, content, \"utf8\");\n created.push(path);\n }\n }\n\n return { root, created };\n}\n\nexport interface DocsRepoFileOptions {\n /**\n * Comment lines prepended to the landscape stub, for a scaffold with a reason\n * of its own to hand over an empty map — `migrate-openspec` stages a docs repo\n * out of a corpus that HAS no topology, and says so where the migrator reads.\n *\n * A parameter and not a second template on purpose. The rest of the stub —\n * how to declare a service, how to bind an edge to an operationId, why there\n * is no `views` block — is the same advice however the repo came to exist, and\n * the migration used to carry its own shorter copy of it: two files that were\n * both correct on the day they were written and could only diverge after.\n */\n landscapePreamble?: string;\n}\n\n/**\n * The files every docs repo starts with, relative to its root, in creation\n * order. `init` scaffolds them directly; `migrate-openspec` stages the same\n * bytes into its target, because the whole point of migrating into a docs repo\n * is that it be the docs repo `loam init` makes.\n *\n * Exported as paths only (via `plannedDocsFiles`) where `init` reports what it\n * will skip — a duplicate list is exactly the drift that made `created +\n * skipped` disagree with reality.\n */\nexport function docsRepoFiles(opts: DocsRepoFileOptions = {}): Array<[string, string]> {\n const preamble = opts.landscapePreamble === undefined ? \"\" : `${opts.landscapePreamble.trimEnd()}\\n//\\n`;\n return [\n // The one file addressed to a person, first because a forge renders it as\n // the landing page and a reader meets it before anything else here.\n [README_FILENAME, README_MD],\n // The process contract lives with the docs it describes, so an agent handed\n // only the docs repo still knows the cycle.\n [AGENTS_FILENAME, AGENTS_MD],\n [join(\"architecture\", \"landscape.likec4\"), `${preamble}${LANDSCAPE_STUB}`],\n [\"loam.json\", DOCS_SELF_CONFIG],\n [LIKEC4_PROJECT_FILENAME, LIKEC4_PROJECT_CONFIG],\n // The two directories git would otherwise drop on the way to the next\n // clone. `architecture/` needs none — the landscape above is in it.\n ...EMPTY_SUBDIRS.map(([dir, keep]) => [join(dir, keep), GITKEEP] as [string, string]),\n ];\n}\n\n/** Everything `scaffoldDocs(docsDir)` would create, in the order it creates it. */\nexport function plannedDocsFiles(docsDir: string): string[] {\n const root = resolve(docsDir);\n return [\n ...DOCS_SUBDIRS.map((d) => join(root, d)),\n ...docsRepoFiles().map(([rel]) => join(root, rel)),\n ];\n}\n"]}
|
|
1
|
+
{"version":3,"file":"docs.js","sourceRoot":"","sources":["../../src/core/docs.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * Scaffolding for the shared docs repo.\n *\n * There is deliberately no manifest. `init` used to write a `loam.docs.json`\n * listing the repo's services; nothing ever read it — `repo.ts` enumerates from\n * the filesystem, because files are the source of truth — and nothing ever\n * updated it, so it named an empty fleet forever. A second list of services is\n * exactly the drift `loam validate` now cross-checks the landscape for; the\n * cheapest way to keep it honest is not to have it.\n */\nimport { mkdir, writeFile } from \"node:fs/promises\";\nimport { existsSync } from \"node:fs\";\nimport { join, resolve } from \"node:path\";\nimport { AGENTS_MD } from \"./agent/agents-md.js\";\nimport { LANDSCAPE_STUB } from \"./scaffold/landscape.js\";\nimport { README_FILENAME, README_MD } from \"./scaffold/readme.js\";\nimport { AGENTS_FILENAME, LIKEC4_PROJECT_FILENAME, LIKEC4_ROOT_PROJECT } from \"./repo/paths.js\";\n\n/**\n * Top-level layout of the shared docs repo, and part of its identity rather\n * than a convenience: `services/` is what makes a directory a docs repo —\n * `init` will not join one without it, and every enumerating command refuses a\n * `docsDir` that has none. So it is laid down even when nothing is in it yet.\n */\nexport const DOCS_SUBDIRS = [\"architecture\", \"services\", \"features\"] as const;\n\n/**\n * The scaffolded directories that start out empty, and the file that keeps each\n * of them in version control. See {@link GITKEEP}. Spelled as pairs so the\n * marker's name is written once — `listServices` and `listFeatures` enumerate\n * subdirectories only, so a file here is invisible to both.\n */\nconst EMPTY_SUBDIRS = [\n [\"services\", \".gitkeep\"],\n [\"features\", \".gitkeep\"],\n] as const;\n\n/**\n * The LikeC4 project file, and the one thing in the docs repo loam writes for a\n * tool other than itself.\n *\n * LikeC4's workspace loader merges EVERY `.likec4` file under a directory tree\n * into one model. loam's layout is the opposite by design: the landscape,\n * each `services/<id>/model.likec4` and each `features/<FEAT>/delta.likec4` is\n * parsed in isolation (`loadSource`, one file, one throwaway workspace), so\n * every one of them legitimately declares its own `specification { … }` block\n * and re-declares the elements it needs to talk about. Point the real renderer\n * at the repository root and those declarations collide: on loam's own\n * `examples/docs` — four files — `npx likec4 start` reported 16 errors, sixteen\n * duplicate kinds and names, while `loam validate --all` reported none. Both\n * were right; only one of them was reading the tree as a workspace.\n *\n * So the root is declared as one LikeC4 project holding the landscape, and the\n * two directories whose files are meant to be read alone are excluded from it.\n * That makes `npx likec4 start` in the docs repo — the command loam's own docs\n * have always recommended — render the fleet map instead of failing. What the\n * exclusion cost went unstated for a release: every adopted service was then a\n * box on that map with nothing renderable inside it. A service model renders\n * from the SAME root once `loam subsystem sync` has written a project file of\n * its own beside it — `repo/tree/render/projects.ts` records the measurement\n * and the rule — and only a feature's `delta.likec4`, which is transient, is\n * still rendered by pointing the renderer at its own directory.\n *\n * `**\\/node_modules/**` is repeated because naming `exclude` replaces LikeC4's\n * default rather than adding to it. loam reads none of this file: it is written\n * once, never re-read, and a team that wants a different project layout owns it\n * like every other scaffolded file. The filename and the root project's name\n * are spelled in `repo/paths.ts`, beside every other file loam writes.\n */\nexport const LIKEC4_PROJECT_CONFIG = `${JSON.stringify(\n {\n name: LIKEC4_ROOT_PROJECT,\n title: \"Fleet landscape\",\n exclude: [\"**/node_modules/**\", \"services/**\", \"features/**\"],\n },\n null,\n 2,\n)}\\n`;\n\n/**\n * The placeholder that makes an empty scaffolded directory survive a clone.\n *\n * git tracks files, not directories, so `services/` and `features/` — created\n * empty by `loam init --create` — were absent for everyone after the first push:\n * the person who ran init had a green repo, and the second person to clone it\n * got `doctor.services-missing`, a BLOCKER, on a repository nobody had touched.\n * A repo that cannot survive being cloned is not a shared docs repo.\n */\nconst GITKEEP = \"\";\n\n/**\n * The docs repo's own loam.json. It makes the docs repo self-describing: a\n * command run from inside it (or from any directory under it) finds this file\n * first and resolves the fleet to the repo it is standing in, instead of\n * walking out to whatever service repo happens to be above.\n *\n * `\".\"` and not an absolute path for the same reason every other docsDir is\n * stored as written: this file is committed and cloned to machines whose\n * directory layout nobody here can predict.\n */\nconst DOCS_SELF_CONFIG = `${JSON.stringify({ docsDir: \".\" }, null, 2)}\\n`;\n\nexport interface ScaffoldResult {\n root: string;\n created: string[];\n}\n\n/** Idempotently create the docs-repo skeleton. Existing files/dirs are left untouched. */\nexport async function scaffoldDocs(docsDir: string): Promise<ScaffoldResult> {\n const root = resolve(docsDir);\n const created: string[] = [];\n\n await mkdir(root, { recursive: true });\n\n for (const dir of DOCS_SUBDIRS) {\n const p = join(root, dir);\n if (!existsSync(p)) {\n await mkdir(p, { recursive: true });\n created.push(p);\n }\n }\n\n // Never overwritten, all three of them — a team's own house rules, their own\n // map and their own config outrank the template. The order here IS the order\n // `init` probes for what it will skip; keep the two lists in step.\n for (const [rel, content] of docsRepoFiles()) {\n const path = join(root, rel);\n if (!existsSync(path)) {\n await writeFile(path, content, \"utf8\");\n created.push(path);\n }\n }\n\n return { root, created };\n}\n\nexport interface DocsRepoFileOptions {\n /**\n * Comment lines prepended to the landscape stub, for a scaffold with a reason\n * of its own to hand over an empty map — `migrate-openspec` stages a docs repo\n * out of a corpus that HAS no topology, and says so where the migrator reads.\n *\n * A parameter and not a second template on purpose. The rest of the stub —\n * how to declare a service, how to bind an edge to an operationId, why there\n * is no `views` block — is the same advice however the repo came to exist, and\n * the migration used to carry its own shorter copy of it: two files that were\n * both correct on the day they were written and could only diverge after.\n */\n landscapePreamble?: string;\n}\n\n/**\n * The files every docs repo starts with, relative to its root, in creation\n * order. `init` scaffolds them directly; `migrate-openspec` stages the same\n * bytes into its target, because the whole point of migrating into a docs repo\n * is that it be the docs repo `loam init` makes.\n *\n * Exported as paths only (via `plannedDocsFiles`) where `init` reports what it\n * will skip — a duplicate list is exactly the drift that made `created +\n * skipped` disagree with reality.\n */\nexport function docsRepoFiles(opts: DocsRepoFileOptions = {}): Array<[string, string]> {\n const preamble = opts.landscapePreamble === undefined ? \"\" : `${opts.landscapePreamble.trimEnd()}\\n//\\n`;\n return [\n // The one file addressed to a person, first because a forge renders it as\n // the landing page and a reader meets it before anything else here.\n [README_FILENAME, README_MD],\n // The process contract lives with the docs it describes, so an agent handed\n // only the docs repo still knows the cycle.\n [AGENTS_FILENAME, AGENTS_MD],\n [join(\"architecture\", \"landscape.likec4\"), `${preamble}${LANDSCAPE_STUB}`],\n [\"loam.json\", DOCS_SELF_CONFIG],\n [LIKEC4_PROJECT_FILENAME, LIKEC4_PROJECT_CONFIG],\n // The two directories git would otherwise drop on the way to the next\n // clone. `architecture/` needs none — the landscape above is in it.\n ...EMPTY_SUBDIRS.map(([dir, keep]) => [join(dir, keep), GITKEEP] as [string, string]),\n ];\n}\n\n/** Everything `scaffoldDocs(docsDir)` would create, in the order it creates it. */\nexport function plannedDocsFiles(docsDir: string): string[] {\n const root = resolve(docsDir);\n return [\n ...DOCS_SUBDIRS.map((d) => join(root, d)),\n ...docsRepoFiles().map(([rel]) => join(root, rel)),\n ];\n}\n"]}
|
|
@@ -12,10 +12,10 @@ import { constants, existsSync } from "node:fs";
|
|
|
12
12
|
import { dirname, isAbsolute, join, resolve } from "node:path";
|
|
13
13
|
import { CONFIG_FILENAME, configPath } from "../envelope/config.js";
|
|
14
14
|
import { LOAM_VERSION } from "../envelope/version.js";
|
|
15
|
-
import { landscapePath } from "../repo/paths.js";
|
|
15
|
+
import { landscapePath, LIKEC4_PROJECT_FILENAME } from "../repo/paths.js";
|
|
16
16
|
import { docsRepoState } from "../repo/state.js";
|
|
17
17
|
import { listFeatures, listServices } from "../repo/repo.js";
|
|
18
|
-
import { LIKEC4_PROJECT_CONFIG
|
|
18
|
+
import { LIKEC4_PROJECT_CONFIG } from "../docs.js";
|
|
19
19
|
import { gherkinRoot } from "../gherkin/stamp.js";
|
|
20
20
|
import { UnsafePathError } from "../kernel/path-safety.js";
|
|
21
21
|
import { scanWritePathResidue } from "../staging/recovery/residue.js";
|
|
@@ -124,8 +124,16 @@ export async function diagnose(cwd = process.cwd()) {
|
|
|
124
124
|
// start` — the command loam's own brief recommends — refused to load at all.
|
|
125
125
|
// `loam init --create` writes the project file that scopes the root project to
|
|
126
126
|
// the landscape; a docs repo created before it exists has to be told, because
|
|
127
|
-
// nothing else will ever mention it.
|
|
128
|
-
//
|
|
127
|
+
// nothing else will ever mention it. The root file is also the gate on the
|
|
128
|
+
// per-service ones: `loam subsystem sync` writes a create-only
|
|
129
|
+
// likec4.config.json beside every service model — the LikeC4 project that
|
|
130
|
+
// makes each model render from the same root, beside the fleet map — and it
|
|
131
|
+
// writes none of them until this file exists, so a tree missing it renders
|
|
132
|
+
// nothing from the docs root, not only the map. (Only a feature delta still renders from
|
|
133
|
+
// its own directory; an archived feature would otherwise keep a dead project
|
|
134
|
+
// forever.) The fix below says both, because this finding is the one place a
|
|
135
|
+
// repo that predates either file is ever told. A warning, not a blocker: no
|
|
136
|
+
// loam check reads this file, and a repo without it is only unrenderable.
|
|
129
137
|
if (docsDir !== null && exists && readable && !existsSync(join(docsDir, LIKEC4_PROJECT_FILENAME))) {
|
|
130
138
|
findings.push({
|
|
131
139
|
severity: "warning",
|
|
@@ -135,8 +143,11 @@ export async function diagnose(cwd = process.cwd()) {
|
|
|
135
143
|
+ "renderer at the repo root merges them into one model and reports every declaration as a duplicate.",
|
|
136
144
|
fix: `Write ${docsDir}/${LIKEC4_PROJECT_FILENAME} with:\n${LIKEC4_PROJECT_CONFIG.trimEnd()}\n`
|
|
137
145
|
+ "That scopes the root project to architecture/, so `npx likec4 start` in the docs repo renders "
|
|
138
|
-
+ "the fleet map.
|
|
139
|
-
+ "
|
|
146
|
+
+ "the fleet map. Then run `loam subsystem sync`: it writes one create-only likec4.config.json "
|
|
147
|
+
+ "beside every service model, so each model renders from that same root as a project of its own "
|
|
148
|
+
+ "(the root project excludes services/**, and a model with no project file renders nowhere from the docs root). "
|
|
149
|
+
+ "Only a feature delta still renders from its own directory (`npx likec4 start features/<FEAT>`) — "
|
|
150
|
+
+ "being readable alone is what these files are for.",
|
|
140
151
|
});
|
|
141
152
|
}
|
|
142
153
|
// What a killed writer left behind. Read from `scanWritePathResidue`, whose
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"doctor.js","sourceRoot":"","sources":["../../../src/core/doctor/doctor.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * The preflight: can loam run here at all, and can its answers be trusted?\n *\n * The walk only. Each question it asks lives in its own module — `./config.ts`\n * for the config and the fleet map, `./agents.ts` for the agent surface,\n * `./residue.ts` for what a killed writer left — and every one of them returns\n * findings rather than printing. That is the whole contract this module keeps:\n * `doctor` is the command that must be able to describe a broken repository\n * without becoming the next thing that breaks in it.\n */\nimport { constants, existsSync } from \"node:fs\";\nimport { dirname, isAbsolute, join, resolve } from \"node:path\";\nimport { CONFIG_FILENAME, configPath } from \"../envelope/config.js\";\nimport { LOAM_VERSION } from \"../envelope/version.js\";\nimport { landscapePath } from \"../repo/paths.js\";\nimport { docsRepoState } from \"../repo/state.js\";\nimport { listFeatures, listServices } from \"../repo/repo.js\";\nimport { LIKEC4_PROJECT_CONFIG, LIKEC4_PROJECT_FILENAME } from \"../docs.js\";\nimport { gherkinRoot } from \"../gherkin/stamp.js\";\nimport { UnsafePathError } from \"../kernel/path-safety.js\";\nimport { scanWritePathResidue } from \"../staging/recovery/residue.js\";\nimport { type DoctorFinding, type DoctorReport } from \"./report.js\";\nimport { canAccess, inspectConfig, inspectLandscape } from \"./config.js\";\nimport { inspectAgentSurface } from \"./agents.js\";\nimport { gradeWritePathResidue } from \"./residue.js\";\n\nexport async function diagnose(cwd = process.cwd()): Promise<DoctorReport> {\n const inspected = await inspectConfig(cwd);\n const path = configPath(cwd);\n const findings: DoctorFinding[] = [];\n if (inspected.status === \"missing\") {\n findings.push({\n severity: \"blocker\",\n code: \"doctor.config-missing\",\n message: `No ${CONFIG_FILENAME} found at or above ${cwd}.`,\n fix: \"Run `loam init --docs <path-to-docs-repo>` here (add `--create` to make a new docs repo).\",\n });\n } else if (inspected.status === \"invalid\") {\n findings.push({\n severity: \"blocker\",\n code: \"doctor.config-invalid\",\n message: `Invalid ${CONFIG_FILENAME}: ${inspected.error ?? \"unknown error\"}`,\n fix: `Repair ${path} — or delete it and re-run \\`loam init\\`.`,\n });\n }\n\n const docsDir = inspected.config?.docsDir ?? null;\n const state = docsDir === null ? null : docsRepoState(docsDir);\n const exists = state !== null && state.kind !== \"missing\";\n const readable = exists && docsDir !== null ? await canAccess(docsDir, constants.R_OK) : false;\n const writable = exists && docsDir !== null ? await canAccess(docsDir, constants.W_OK) : false;\n const servicesDir = state?.kind === \"ok\";\n const landscapeFile = docsDir === null ? null : landscapePath(docsDir);\n const landscape = landscapeFile === null ? false : await canAccess(landscapeFile, constants.F_OK);\n\n // The docs-repo verdict comes from `docsRepoState`, the same function\n // `listServices` refuses on, so doctor cannot call a repo healthy that the\n // enumeration will not read.\n if (state?.kind === \"missing\") {\n findings.push({\n severity: \"blocker\",\n code: \"doctor.docs-missing\",\n message: `Configured docsDir is missing or is not a directory: ${docsDir}`,\n fix: `Fix \"docsDir\" in ${path}, clone the docs repo to ${docsDir}, or run \\`loam init --docs <dir> --create\\`.`,\n });\n } else if (state?.kind === \"no-services\") {\n findings.push({\n severity: \"blocker\",\n code: \"doctor.services-missing\",\n message: `Required services/ directory is missing under ${docsDir} — that path is not a docs repo.`,\n fix: `Point \"docsDir\" in ${path} at the shared docs repo, or run \\`loam init --docs ${docsDir} --create\\`.`,\n });\n }\n\n // An absolute docsDir in a committed config names a directory that exists on\n // exactly one machine. It is a warning and not a blocker because it works\n // perfectly — for the person who ran `loam init`, and for nobody who clones\n // the repo afterwards.\n const asWritten = inspected.config?.docsDirAsWritten;\n const configRoot = inspected.config?.root;\n if (\n asWritten !== undefined\n && configRoot !== undefined\n && isAbsolute(asWritten)\n && resolve(asWritten) !== resolve(configRoot)\n ) {\n findings.push({\n severity: \"warning\",\n code: \"doctor.docs-absolute\",\n message:\n `\"docsDir\" is stored as an absolute path (${asWritten}); ` +\n `${CONFIG_FILENAME} is committed, so it will not resolve on anyone else's machine.`,\n fix: `Rewrite \"docsDir\" in ${path} as a path relative to that file (e.g. \"../docs\").`,\n });\n }\n\n if (exists && !readable) {\n findings.push({\n severity: \"warning\",\n code: \"doctor.docs-unreadable\",\n message: `docsDir is not readable by this process: ${docsDir}`,\n fix: `Grant read access to ${docsDir} (check ownership and mode).`,\n });\n }\n if (exists && !writable) {\n findings.push({\n severity: \"warning\",\n code: \"doctor.docs-readonly\",\n message: `docsDir is not writable; read-only commands work, archive/init do not: ${docsDir}`,\n fix: `Grant write access to ${docsDir} if you need \\`loam new\\`, \\`loam adopt\\` or \\`loam archive\\`.`,\n });\n }\n if (exists && !landscape) {\n findings.push({\n severity: \"warning\",\n code: \"doctor.landscape-missing\",\n message: `${landscapeFile} is missing — nothing cross-service can be checked without it.`,\n fix: \"Create architecture/landscape.likec4 with one element per service and an edge per call (`loam init --create` writes the empty map).\",\n });\n } else if (exists && landscapeFile !== null && readable) {\n await inspectLandscape(landscapeFile, findings);\n }\n\n // The renderer's view of the repo, which is not loam's. loam parses every\n // `.likec4` file ALONE, so each one declares its own `specification` block and\n // re-declares whatever elements it names; LikeC4's own workspace loader merges\n // the whole tree into one model, and those declarations then collide. The\n // result was two tools disagreeing in the most confusing possible direction:\n // `loam validate --all` reported zero errors on a tree where `npx likec4\n // start` — the command loam's own brief recommends — refused to load at all.\n // `loam init --create` writes the project file that scopes the root project to\n // the landscape; a docs repo created before it exists has to be told, because\n // nothing else will ever mention it. A warning, not a blocker: no loam check\n // reads this file, and a repo without it is only unrenderable.\n if (docsDir !== null && exists && readable && !existsSync(join(docsDir, LIKEC4_PROJECT_FILENAME))) {\n findings.push({\n severity: \"warning\",\n code: \"doctor.likec4-config-missing\",\n message:\n `${docsDir}/${LIKEC4_PROJECT_FILENAME} is missing, so this tree is not a loadable LikeC4 workspace — `\n + \"every service model and feature delta declares its own `specification` block, and pointing a \"\n + \"renderer at the repo root merges them into one model and reports every declaration as a duplicate.\",\n fix:\n `Write ${docsDir}/${LIKEC4_PROJECT_FILENAME} with:\\n${LIKEC4_PROJECT_CONFIG.trimEnd()}\\n`\n + \"That scopes the root project to architecture/, so `npx likec4 start` in the docs repo renders \"\n + \"the fleet map. A service model or feature delta is rendered by pointing the renderer at its own \"\n + \"directory (`npx likec4 start services/<id>`) — being readable alone is what those files are for.\",\n });\n }\n\n // What a killed writer left behind. Read from `scanWritePathResidue`, whose\n // spellings of `.loam-lock` / `.loam-commit` / the temp-file pattern are\n // staging's own — doctor grades, it does not re-spell. This is the surface\n // that used to report `healthy: true` over half-merged docs.\n const writePath = docsDir !== null && exists && readable\n ? await scanWritePathResidue(docsDir)\n : null;\n if (docsDir !== null && writePath !== null) gradeWritePathResidue(docsDir, writePath, findings);\n\n // The one writer whose journal is NOT in the docs repo: `loam gherkin`\n // commits into the service repo's owned `<gherkinDir>/loam/` root, with the\n // same lock and journal files. When this repo IS a service repo, that root\n // gets the same scan and the same grades — the codes are the contract, the\n // prose says where — because \"doctor reports an interrupted commit until\n // recovery succeeds\" has to hold for every writer, not only the docs-repo\n // ones. Scanning it is cheap: the root is loam's own flat directory.\n let serviceWritePath = null;\n if (inspected.config?.service !== undefined && inspected.config.root !== undefined) {\n try {\n const emissionRoot = gherkinRoot(inspected.config.root, inspected.config.gherkinDir);\n if (existsSync(emissionRoot)) {\n serviceWritePath = await scanWritePathResidue(emissionRoot);\n gradeWritePathResidue(emissionRoot, serviceWritePath, findings);\n }\n } catch (err) {\n // An unsafe gherkinDir is `loam gherkin`'s refusal to make, with its own\n // sentence; doctor only declines to scan what it cannot trust.\n if (!(err instanceof UnsafePathError)) throw err;\n }\n }\n\n let services: Awaited<ReturnType<typeof listServices>> = [];\n let activeFeatures: Awaited<ReturnType<typeof listFeatures>> = [];\n // Whether the fleet was actually READ, which is a narrower question than\n // whether reading it was attempted — hence set inside the try and not from\n // the guard, so an enumeration that threw leaves this false alongside its own\n // `doctor.inventory-unreadable`.\n let inventoried = false;\n if (docsDir !== null && readable && servicesDir) {\n try {\n [services, activeFeatures] = await Promise.all([\n listServices(docsDir),\n listFeatures(docsDir),\n ]);\n inventoried = true;\n } catch (error) {\n findings.push({\n severity: \"warning\",\n code: \"doctor.inventory-unreadable\",\n message: `Could not inventory the docs repo: ${error instanceof Error ? error.message : String(error)}`,\n fix: `Check that ${docsDir}/services and ${docsDir}/features are readable directories.`,\n });\n }\n }\n\n const configuredService = inspected.config?.service ?? null;\n const currentService: DoctorReport[\"currentService\"] = configuredService === null\n ? { configured: null, status: \"unbound\" }\n : services.some((service) => service.id === configuredService)\n ? { configured: configuredService, status: \"matched\" }\n : { configured: configuredService, status: \"unknown\" };\n // Standing INSIDE the docs repo, an absent service binding is the correct\n // state, not a gap: the docs repo is the fleet, it is not any one service, and\n // the fix this finding prints — `loam init --service <id>` — would bind the\n // shared repo to one of the services it holds, which is not a thing loam has\n // a meaning for. doctor already knew where it was standing (it prints the\n // fleet count two lines up) and reported the warning anyway, so the first\n // preflight anyone ran in a freshly created docs repo came back yellow with\n // advice that made it worse. The docs repo's own loam.json is the one that\n // resolves `docsDir` to the directory holding it — that identity, not a\n // filename, is the test.\n const inDocsRepo = configRoot !== undefined && docsDir !== null\n && resolve(configRoot) === resolve(docsDir);\n if (currentService.status === \"unbound\" && !inDocsRepo) {\n findings.push({\n severity: \"warning\",\n code: \"doctor.service-unbound\",\n message: \"loam.json has no service binding; service-repo checks need `service`.\",\n fix: `Run \\`loam init --docs ${inspected.config?.docsDirAsWritten ?? \"<dir>\"} --service <id>\\` here.`,\n });\n } else if (currentService.status === \"unknown\" && inventoried) {\n // Only when there was an inventory to be absent from. This finding is a\n // claim about a fleet that was read; with no docsDir, an unreadable one, or\n // an enumeration that threw, `services` is empty for a reason that has\n // nothing to do with `service`, and doctor pointed the reader at the one\n // config field that was fine while the blocker it had already filed named\n // the real one. `currentService.status` still reports `unknown` — the\n // envelope describes what loam could and could not match, and an unread\n // fleet matches nothing.\n findings.push({\n severity: \"warning\",\n code: \"doctor.service-unknown\",\n message: `Configured service '${configuredService}' is not present under services/.`,\n // `--service <id>`, not a positional: `loam adopt` takes no argument, so\n // the spelling this finding used to print was refused by commander with\n // \"too many arguments\" — as the FIRST instruction a freshly bound service\n // repo ever receives. test/agent-commands-runnable.test.ts parses every\n // `loam …` loam prints against the real program so it cannot recur.\n fix: `Run \\`loam adopt --service ${configuredService}\\` to onboard it, or fix \"service\" in ${path}.`,\n });\n }\n\n // The slash commands (and everything else `init` lays down for a tool) belong\n // to the repo `init` ran in — the directory holding loam.json, which is not\n // the cwd now that config discovery walks upward.\n const agents = await inspectAgentSurface(dirname(path), inspected.config, docsDir, findings);\n\n return {\n healthy: !findings.some((finding) => finding.severity === \"blocker\"),\n runtime: {\n package: \"@ybotok/loam\",\n version: LOAM_VERSION,\n node: process.version,\n platform: process.platform,\n arch: process.arch,\n },\n config: { path, status: inspected.status, error: inspected.error },\n docs: {\n path: docsDir,\n exists,\n readable,\n writable,\n servicesDir,\n landscape,\n },\n counts: { services: services.length, activeFeatures: activeFeatures.length },\n currentService,\n agents,\n writePath,\n serviceWritePath,\n findings,\n };\n}\n"]}
|
|
1
|
+
{"version":3,"file":"doctor.js","sourceRoot":"","sources":["../../../src/core/doctor/doctor.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * The preflight: can loam run here at all, and can its answers be trusted?\n *\n * The walk only. Each question it asks lives in its own module — `./config.ts`\n * for the config and the fleet map, `./agents.ts` for the agent surface,\n * `./residue.ts` for what a killed writer left — and every one of them returns\n * findings rather than printing. That is the whole contract this module keeps:\n * `doctor` is the command that must be able to describe a broken repository\n * without becoming the next thing that breaks in it.\n */\nimport { constants, existsSync } from \"node:fs\";\nimport { dirname, isAbsolute, join, resolve } from \"node:path\";\nimport { CONFIG_FILENAME, configPath } from \"../envelope/config.js\";\nimport { LOAM_VERSION } from \"../envelope/version.js\";\nimport { landscapePath, LIKEC4_PROJECT_FILENAME } from \"../repo/paths.js\";\nimport { docsRepoState } from \"../repo/state.js\";\nimport { listFeatures, listServices } from \"../repo/repo.js\";\nimport { LIKEC4_PROJECT_CONFIG } from \"../docs.js\";\nimport { gherkinRoot } from \"../gherkin/stamp.js\";\nimport { UnsafePathError } from \"../kernel/path-safety.js\";\nimport { scanWritePathResidue } from \"../staging/recovery/residue.js\";\nimport { type DoctorFinding, type DoctorReport } from \"./report.js\";\nimport { canAccess, inspectConfig, inspectLandscape } from \"./config.js\";\nimport { inspectAgentSurface } from \"./agents.js\";\nimport { gradeWritePathResidue } from \"./residue.js\";\n\nexport async function diagnose(cwd = process.cwd()): Promise<DoctorReport> {\n const inspected = await inspectConfig(cwd);\n const path = configPath(cwd);\n const findings: DoctorFinding[] = [];\n if (inspected.status === \"missing\") {\n findings.push({\n severity: \"blocker\",\n code: \"doctor.config-missing\",\n message: `No ${CONFIG_FILENAME} found at or above ${cwd}.`,\n fix: \"Run `loam init --docs <path-to-docs-repo>` here (add `--create` to make a new docs repo).\",\n });\n } else if (inspected.status === \"invalid\") {\n findings.push({\n severity: \"blocker\",\n code: \"doctor.config-invalid\",\n message: `Invalid ${CONFIG_FILENAME}: ${inspected.error ?? \"unknown error\"}`,\n fix: `Repair ${path} — or delete it and re-run \\`loam init\\`.`,\n });\n }\n\n const docsDir = inspected.config?.docsDir ?? null;\n const state = docsDir === null ? null : docsRepoState(docsDir);\n const exists = state !== null && state.kind !== \"missing\";\n const readable = exists && docsDir !== null ? await canAccess(docsDir, constants.R_OK) : false;\n const writable = exists && docsDir !== null ? await canAccess(docsDir, constants.W_OK) : false;\n const servicesDir = state?.kind === \"ok\";\n const landscapeFile = docsDir === null ? null : landscapePath(docsDir);\n const landscape = landscapeFile === null ? false : await canAccess(landscapeFile, constants.F_OK);\n\n // The docs-repo verdict comes from `docsRepoState`, the same function\n // `listServices` refuses on, so doctor cannot call a repo healthy that the\n // enumeration will not read.\n if (state?.kind === \"missing\") {\n findings.push({\n severity: \"blocker\",\n code: \"doctor.docs-missing\",\n message: `Configured docsDir is missing or is not a directory: ${docsDir}`,\n fix: `Fix \"docsDir\" in ${path}, clone the docs repo to ${docsDir}, or run \\`loam init --docs <dir> --create\\`.`,\n });\n } else if (state?.kind === \"no-services\") {\n findings.push({\n severity: \"blocker\",\n code: \"doctor.services-missing\",\n message: `Required services/ directory is missing under ${docsDir} — that path is not a docs repo.`,\n fix: `Point \"docsDir\" in ${path} at the shared docs repo, or run \\`loam init --docs ${docsDir} --create\\`.`,\n });\n }\n\n // An absolute docsDir in a committed config names a directory that exists on\n // exactly one machine. It is a warning and not a blocker because it works\n // perfectly — for the person who ran `loam init`, and for nobody who clones\n // the repo afterwards.\n const asWritten = inspected.config?.docsDirAsWritten;\n const configRoot = inspected.config?.root;\n if (\n asWritten !== undefined\n && configRoot !== undefined\n && isAbsolute(asWritten)\n && resolve(asWritten) !== resolve(configRoot)\n ) {\n findings.push({\n severity: \"warning\",\n code: \"doctor.docs-absolute\",\n message:\n `\"docsDir\" is stored as an absolute path (${asWritten}); ` +\n `${CONFIG_FILENAME} is committed, so it will not resolve on anyone else's machine.`,\n fix: `Rewrite \"docsDir\" in ${path} as a path relative to that file (e.g. \"../docs\").`,\n });\n }\n\n if (exists && !readable) {\n findings.push({\n severity: \"warning\",\n code: \"doctor.docs-unreadable\",\n message: `docsDir is not readable by this process: ${docsDir}`,\n fix: `Grant read access to ${docsDir} (check ownership and mode).`,\n });\n }\n if (exists && !writable) {\n findings.push({\n severity: \"warning\",\n code: \"doctor.docs-readonly\",\n message: `docsDir is not writable; read-only commands work, archive/init do not: ${docsDir}`,\n fix: `Grant write access to ${docsDir} if you need \\`loam new\\`, \\`loam adopt\\` or \\`loam archive\\`.`,\n });\n }\n if (exists && !landscape) {\n findings.push({\n severity: \"warning\",\n code: \"doctor.landscape-missing\",\n message: `${landscapeFile} is missing — nothing cross-service can be checked without it.`,\n fix: \"Create architecture/landscape.likec4 with one element per service and an edge per call (`loam init --create` writes the empty map).\",\n });\n } else if (exists && landscapeFile !== null && readable) {\n await inspectLandscape(landscapeFile, findings);\n }\n\n // The renderer's view of the repo, which is not loam's. loam parses every\n // `.likec4` file ALONE, so each one declares its own `specification` block and\n // re-declares whatever elements it names; LikeC4's own workspace loader merges\n // the whole tree into one model, and those declarations then collide. The\n // result was two tools disagreeing in the most confusing possible direction:\n // `loam validate --all` reported zero errors on a tree where `npx likec4\n // start` — the command loam's own brief recommends — refused to load at all.\n // `loam init --create` writes the project file that scopes the root project to\n // the landscape; a docs repo created before it exists has to be told, because\n // nothing else will ever mention it. The root file is also the gate on the\n // per-service ones: `loam subsystem sync` writes a create-only\n // likec4.config.json beside every service model — the LikeC4 project that\n // makes each model render from the same root, beside the fleet map — and it\n // writes none of them until this file exists, so a tree missing it renders\n // nothing from the docs root, not only the map. (Only a feature delta still renders from\n // its own directory; an archived feature would otherwise keep a dead project\n // forever.) The fix below says both, because this finding is the one place a\n // repo that predates either file is ever told. A warning, not a blocker: no\n // loam check reads this file, and a repo without it is only unrenderable.\n if (docsDir !== null && exists && readable && !existsSync(join(docsDir, LIKEC4_PROJECT_FILENAME))) {\n findings.push({\n severity: \"warning\",\n code: \"doctor.likec4-config-missing\",\n message:\n `${docsDir}/${LIKEC4_PROJECT_FILENAME} is missing, so this tree is not a loadable LikeC4 workspace — `\n + \"every service model and feature delta declares its own `specification` block, and pointing a \"\n + \"renderer at the repo root merges them into one model and reports every declaration as a duplicate.\",\n fix:\n `Write ${docsDir}/${LIKEC4_PROJECT_FILENAME} with:\\n${LIKEC4_PROJECT_CONFIG.trimEnd()}\\n`\n + \"That scopes the root project to architecture/, so `npx likec4 start` in the docs repo renders \"\n + \"the fleet map. Then run `loam subsystem sync`: it writes one create-only likec4.config.json \"\n + \"beside every service model, so each model renders from that same root as a project of its own \"\n + \"(the root project excludes services/**, and a model with no project file renders nowhere from the docs root). \"\n + \"Only a feature delta still renders from its own directory (`npx likec4 start features/<FEAT>`) — \"\n + \"being readable alone is what these files are for.\",\n });\n }\n\n // What a killed writer left behind. Read from `scanWritePathResidue`, whose\n // spellings of `.loam-lock` / `.loam-commit` / the temp-file pattern are\n // staging's own — doctor grades, it does not re-spell. This is the surface\n // that used to report `healthy: true` over half-merged docs.\n const writePath = docsDir !== null && exists && readable\n ? await scanWritePathResidue(docsDir)\n : null;\n if (docsDir !== null && writePath !== null) gradeWritePathResidue(docsDir, writePath, findings);\n\n // The one writer whose journal is NOT in the docs repo: `loam gherkin`\n // commits into the service repo's owned `<gherkinDir>/loam/` root, with the\n // same lock and journal files. When this repo IS a service repo, that root\n // gets the same scan and the same grades — the codes are the contract, the\n // prose says where — because \"doctor reports an interrupted commit until\n // recovery succeeds\" has to hold for every writer, not only the docs-repo\n // ones. Scanning it is cheap: the root is loam's own flat directory.\n let serviceWritePath = null;\n if (inspected.config?.service !== undefined && inspected.config.root !== undefined) {\n try {\n const emissionRoot = gherkinRoot(inspected.config.root, inspected.config.gherkinDir);\n if (existsSync(emissionRoot)) {\n serviceWritePath = await scanWritePathResidue(emissionRoot);\n gradeWritePathResidue(emissionRoot, serviceWritePath, findings);\n }\n } catch (err) {\n // An unsafe gherkinDir is `loam gherkin`'s refusal to make, with its own\n // sentence; doctor only declines to scan what it cannot trust.\n if (!(err instanceof UnsafePathError)) throw err;\n }\n }\n\n let services: Awaited<ReturnType<typeof listServices>> = [];\n let activeFeatures: Awaited<ReturnType<typeof listFeatures>> = [];\n // Whether the fleet was actually READ, which is a narrower question than\n // whether reading it was attempted — hence set inside the try and not from\n // the guard, so an enumeration that threw leaves this false alongside its own\n // `doctor.inventory-unreadable`.\n let inventoried = false;\n if (docsDir !== null && readable && servicesDir) {\n try {\n [services, activeFeatures] = await Promise.all([\n listServices(docsDir),\n listFeatures(docsDir),\n ]);\n inventoried = true;\n } catch (error) {\n findings.push({\n severity: \"warning\",\n code: \"doctor.inventory-unreadable\",\n message: `Could not inventory the docs repo: ${error instanceof Error ? error.message : String(error)}`,\n fix: `Check that ${docsDir}/services and ${docsDir}/features are readable directories.`,\n });\n }\n }\n\n const configuredService = inspected.config?.service ?? null;\n const currentService: DoctorReport[\"currentService\"] = configuredService === null\n ? { configured: null, status: \"unbound\" }\n : services.some((service) => service.id === configuredService)\n ? { configured: configuredService, status: \"matched\" }\n : { configured: configuredService, status: \"unknown\" };\n // Standing INSIDE the docs repo, an absent service binding is the correct\n // state, not a gap: the docs repo is the fleet, it is not any one service, and\n // the fix this finding prints — `loam init --service <id>` — would bind the\n // shared repo to one of the services it holds, which is not a thing loam has\n // a meaning for. doctor already knew where it was standing (it prints the\n // fleet count two lines up) and reported the warning anyway, so the first\n // preflight anyone ran in a freshly created docs repo came back yellow with\n // advice that made it worse. The docs repo's own loam.json is the one that\n // resolves `docsDir` to the directory holding it — that identity, not a\n // filename, is the test.\n const inDocsRepo = configRoot !== undefined && docsDir !== null\n && resolve(configRoot) === resolve(docsDir);\n if (currentService.status === \"unbound\" && !inDocsRepo) {\n findings.push({\n severity: \"warning\",\n code: \"doctor.service-unbound\",\n message: \"loam.json has no service binding; service-repo checks need `service`.\",\n fix: `Run \\`loam init --docs ${inspected.config?.docsDirAsWritten ?? \"<dir>\"} --service <id>\\` here.`,\n });\n } else if (currentService.status === \"unknown\" && inventoried) {\n // Only when there was an inventory to be absent from. This finding is a\n // claim about a fleet that was read; with no docsDir, an unreadable one, or\n // an enumeration that threw, `services` is empty for a reason that has\n // nothing to do with `service`, and doctor pointed the reader at the one\n // config field that was fine while the blocker it had already filed named\n // the real one. `currentService.status` still reports `unknown` — the\n // envelope describes what loam could and could not match, and an unread\n // fleet matches nothing.\n findings.push({\n severity: \"warning\",\n code: \"doctor.service-unknown\",\n message: `Configured service '${configuredService}' is not present under services/.`,\n // `--service <id>`, not a positional: `loam adopt` takes no argument, so\n // the spelling this finding used to print was refused by commander with\n // \"too many arguments\" — as the FIRST instruction a freshly bound service\n // repo ever receives. test/agent-commands-runnable.test.ts parses every\n // `loam …` loam prints against the real program so it cannot recur.\n fix: `Run \\`loam adopt --service ${configuredService}\\` to onboard it, or fix \"service\" in ${path}.`,\n });\n }\n\n // The slash commands (and everything else `init` lays down for a tool) belong\n // to the repo `init` ran in — the directory holding loam.json, which is not\n // the cwd now that config discovery walks upward.\n const agents = await inspectAgentSurface(dirname(path), inspected.config, docsDir, findings);\n\n return {\n healthy: !findings.some((finding) => finding.severity === \"blocker\"),\n runtime: {\n package: \"@ybotok/loam\",\n version: LOAM_VERSION,\n node: process.version,\n platform: process.platform,\n arch: process.arch,\n },\n config: { path, status: inspected.status, error: inspected.error },\n docs: {\n path: docsDir,\n exists,\n readable,\n writable,\n servicesDir,\n landscape,\n },\n counts: { services: services.length, activeFeatures: activeFeatures.length },\n currentService,\n agents,\n writePath,\n serviceWritePath,\n findings,\n };\n}\n"]}
|
|
@@ -62,7 +62,7 @@ const DOCTOR = {
|
|
|
62
62
|
"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." },
|
|
63
63
|
"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`." },
|
|
64
64
|
"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." },
|
|
65
|
-
"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.
|
|
65
|
+
"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." },
|
|
66
66
|
"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." },
|
|
67
67
|
"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." },
|
|
68
68
|
"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." },
|