Skip to content

Diagnostics & rules reference

English(this file) · 日本語

karasu reports problems with two layers of vocabulary:

  • A rule (規則) is a concept — what the language allows and forbids (“an edge originates within its enclosing block”). Rules are how authors and this spec talk about the constraint.
  • A diagnostic (診断) is a mechanism — a specific, named check that fires when one concrete violation of a rule is detected (edge-source-mismatch).

One rule is often enforced by several diagnostics, and a single diagnostic belongs to exactly one rule. This document is the catalog that maps the two.

  • Diagnostic codes are stable API. The code string (e.g. edge-source-mismatch) is consumed by the LSP, the app, and downstream tooling. Codes are not renamed to match a rule’s wording; the rule name is conceptual, the code is the contract. When a rule reads more naturally under a different name than its diagnostic, that is expected — they sit at different altitudes.
  • Every diagnostic code lives under exactly one rule family below, and every code defined in core (DiagnosticParamsByCode, WarningKind) appears here. This completeness is enforced by a meta-test (see Catalog completeness), so a new code cannot ship without a catalog entry.
  • The fires when column states the concrete trigger. Severities are listed as emitted by core.

A diagnostic has a severity: error, warning, or info.

  • error — the model is malformed; the offending construct is rejected.
  • warning — a real defect the author should fix (a dangling reference, a conflicting style).
  • info — a fact, not a defect. karasu surfaces something true about the model that an external school of thought may call a smell (a shared database, a dispersed domain), without asserting it is wrong. This is the fact vs. style register split — see TPL-1386.

karasu also follows warn-don’t-error for unresolved references (spec §S6): an unresolved relation is dropped while the node it points from is preserved, and the drop is reported as a warning rather than failing the whole render.

A diagnostic may carry a source location (loc): a start and an end position, and the document they index into.

  • Positions are 1-based. line and column count from 1, as an author sees them in an editor. A surface that needs another base converts once, at its own boundary (the LSP’s 0-based ranges); a tool that prints a location prints the numbers as they are.
  • file names the document a position indexes into. A project spans files. A diagnostic from an imported file, or a verdict decided on the merged model (the cross-file multiplicity checks under Identifier uniqueness, the reference checks under Cross-reference resolution), anchors on whichever file declared the construct, which is not necessarily the entry. Every diagnostic produced while resolving a project sets file, as an absolute path, whenever it sets loc. A .krs.style diagnostic names the sheet.
  • An absent file means the consumer’s own document. Only a single-document context produces one: the LSP parses each open document by itself, karasu lint-style reads one sheet, and compile takes its model as source text. When such a compile is also handed a style sheet as text, it has a path for neither document, so the sheet’s parse diagnostics carry no loc there rather than a position that would read as the model’s.
  • A diagnostic without loc names no position. One that concerns a missing file carries the path in its message instead (file-not-found, style-file-not-found). Merge-time facts that name ids rather than declarations (infra-redeclared-across-files, system-property-conflict, and the like) carry neither.

Each surface prints a location as follows.

Surface Location shown
CLI (karasu render, and the commands that share its error report) <file>:<line>:<column>. <file> is the entry, in the spelling the user typed, when the position has no file or its file is the entry (compared as canonical paths); otherwise the file, relative to the working directory.
CLI (karasu diff) <line>:<column>, with no file: the command compiles two inputs.
App preview banner and warning panel The UI locale’s line label (Line <line> in English, <line> 行目 in Japanese) for a position in the open document or with no file; <path>:<line> for any other file. The path is relative to the project root, or to the entry’s directory in a mode without a project; a snapshot being compared is named by the project path it was taken of.
LSP The document’s own range, 0-based. The LSP is single-document, so no file arises.

Related TPLs: TPL-2715 (a position is an address only together with the document it indexes, so the parse attaches the file where ranges are built and every surface reads it from there).

Where a construct may be declared, and what an edge’s origin may be. An edge declared inside a service / domain block originates from that block’s id; infra blocks and legend have fixed placement; sync edges must not form a cycle.

Code Severity Fires when
edge-source-mismatch error An explicit edge source inside a service / domain / entity block does not equal the enclosing block id (the edge origin scope rule; for an entity this enforces the relation direction — origin = the reference-holding entity).
edge-endpoint-not-at-scope warning An edge endpoint the declaring scope cannot reach (the edge endpoint scope rule). A bare endpoint must be a peer of the block the edge is declared in — e.g. A -> B written at system scope where A and B are domains inside a service; author it inside its source block, or qualify a cross-domain entity target. A qualified endpoint must be anchored at a top-level root — the whole path from a system down to the target — so the message asks instead for the anchored spelling and names where the target is declared. A relation inside an entity block is exempt: the entity view resolves those against its own pool and draws the foreign entity as a ghost. Skipped for ids that resolve nowhere (unresolved-edge-endpoint for a bare id, cross-system-ref-unresolved for a dotted one); a bare domain → domain edge is exempt because it is derived up to an implicit service edge.
edge-target-ambiguous warning A qualified edge endpoint resolves, within its scope’s reach, to two or more nodes that are not uniform in (kind, depth) — the shared #2088 discriminator, so a uniform multi-match stays silent as intentional broadcast. The message lists each candidate’s full path so the author can qualify further. Bare endpoints draw no ambiguity verdict: they keep the peer binding, where a multi-match is pre-existing broadcast rather than a new question.
ambiguous-edge-base warning Multiple edges share the same from → to base with no distinguishing author id.
duplicate-edge-label error An edge writes its label twice: positionally (A -> B "calls") and as a label property inside its property block. Neither form silently wins; keep one.
service-outside-system warning A service is declared outside any system.
infra-not-in-context error An infra block (database / queue / storage) is not a direct child of system.
boundary-not-in-context error A boundary block is declared inside a node kind that draws no canvas of its own (entity, resource, user, client, or an infra leaf), so it would have no peers to frame.
entity-not-in-domain error An entity is declared somewhere other than as a child of a domain.
node-not-in-context warning A logical node is nested inside a kind whose May contain column does not list it (e.g. a usecase inside a client). The node is kept and still renders; it simply carries no defined meaning there. Scheduled to become an error in .krs language v2.0 (see roadmap §Syntax 2.0).
legend-not-top-level error A legend block is declared somewhere other than the top level.
top-level-declaration error A user or an edge is declared at the top level instead of inside a system block.
system-property-conflict warning A system label / description conflicts between merged imports.
cyclic-dependency warning Sync edges (->) form a dependency cycle.

Related TPLs: TPL-2542 (once one meaning has two spellings, the same PR pins that both land on one AST and that writing it in both forms at once is diagnosed; duplicate-edge-label is that diagnostic).

Ids must be unique within their declaring scope; ownership assigns at most one primary owner.

Code Severity Fires when
duplicate-edge-id error An author-supplied edge id collides with another edge’s id.
duplicate-node-id-parent error A node id is duplicated within its immediate parent (covers a usecase and entity sharing an id under one domain).
entity-anchor-collision warning An id is claimed by more than one target in the entity deep-link namespace ({all domain ids} ∪ {all entity ids}) — an entity id duplicated across domains, or an entity id equal to a domain id. Deep-links resolve ambiguously; the model still renders.
duplicate-node-in-system error A node id is duplicated within a system.
duplicate-node-in-deploy error A node id is duplicated within a deploy block.
duplicate-team-id error A team id is duplicated.
duplicate-team-in-organization error A team id is duplicated within an organization.
duplicate-resource-operation warning A CRUD verb is listed more than once on one resource.
duplicate-crud-decoration-target warning A CRUD decoration targets the same operation more than once.
duplicate-owner-assignment info A node is assigned as owned by more than one team (a fact; see ADR-1566).
duplicate-boundary-assignment info A node belongs to more than one boundary (a fact; membership is 1:N — see syntax.md for how a view resolves it).
boundary-membership-not-drawn info Under Group by: boundary, a boundary’s frame could not be widened to enclose one of its members without covering a non-member, so the membership is marked on the card as a ◇ tab instead. States what this drawing did, unlike duplicate-boundary-assignment, which states a fact about the model; it therefore carries no source location and appears only on that axis.
duplicate-boundary-id error Two boundary blocks in the same enclosing node declare the same id, so the second cannot be addressed. Top-level blocks are unaffected.
duplicate-facet-id error Two facet blocks declare the same id, so a facets reference cannot say whose metadata it means. Decided on the merged model, so a duplicate split across two files is caught; the first declaration is the one references resolve to.
positional-label-removed error A boundary, facet, organization, team or member id is followed by a positional label string. ADR-19 made label a property, and the positional form was never in the spec: boundary / facet lost it outright as experimental constructs (#2133), the rest after a deprecation window (#2208). Recovery differs — boundary / facet discard the string, while organization / team / member keep it as the label so the org chart still reads while the file is fixed.
node-id-multiple-locations warning Two or more logical-layer nodes (service / domain / client) declare the same id at different paths. The warning depends neither on declaration order nor on how the model is split across files: the parse path decides it on the file (#2550) and the project path re-decides it on the merged model (#2596), so moving a block to another file no longer silences it. Being decided on the merged model is also its limit — the verdict covers the declarations that reach the model, so a collision confined to the part of a file a named import leaves behind is not reported for the project (those nodes are not drawn or navigable there; the editor still reports them per file). One declaration is counted once however many merge paths carry it: a file imported both wholesale and by name, or a service mounted into two systems by their edges, is not a collision. Same names across the logical/physical boundary and within the physical layer (database / queue / storage and their sub-resources) are tolerated silently, since physical references are dot-qualified (resource OrderDB.users). Domain-vs-domain multiplicity is excluded (domain-dispersal’s (info) business), and a repeated id under one parent defers to duplicate-node-id-parent (error) — the top-level same-id case has no parent scope to report it, so a parked service X alongside a client X is reported here instead. nodePathIndex keeps a single winner per id: the @migration_target-priority candidate (infra leaves inherit their block’s annotations), ties keeping the first declaration in traversal order (systems, then top-level domains / services / clients, then top-level infra; across files, the merged model’s order, which is the entry file’s own declarations followed by the imported ones). The entry is therefore file-layout independent whenever the candidates differ in priority, which is the migration-coexistence case it exists for; an equal-priority tie follows the import graph, so two unannotated declarations swap winners if they swap files. Each losing logical declaration carries one warning at its own location. The multi-system root view draws every declaration (#2917): each card keeps the bare data-node-id and adds data-node-path (Shop.Api), so a click drills into and describes the card’s own node, while a hand-over that carries only the bare id (highlight, outline, permalink) lands on the index winner or the first matching element.

Related TPLs: TPL-1583 (1:1 index winner rules stay consistent across indices; node-id-multiple-locations’ winner rule is one of them), TPL-2221 (a duplicate split across two files is a fact neither file can see, so the verdict and the index it explains are derived once, on the merged model), TPL-2920 (a canvas that draws the same element id twice names each consumer’s landing, and hands a path to the consumers that need one node).

Cross-reference resolution (warn-don’t-error, §S6)

Section titled “Cross-reference resolution (warn-don’t-error, §S6)”

A referenced id must resolve to a declared node. When it does not, the source node is preserved and the unresolved relation is reported (it is not a fatal error) — see syntax spec §S6.

Code Severity Fires when
owns-target-not-found warning A team owns an id that names no node in the merged model — any kind, any depth, so a declared user or entity is found here and refused by kind in invalid-owns instead. A capability is a property rather than a node, so it resolves to nothing and lands here. Derived from the merged tree, so the verdict depends neither on the import form nor on where the block was declared. Import-coupled: a document that still has imports to resolve does not decide it at all (the LSP’s single-document context stays silent; the App / CLI decide it on the merged model).
invalid-owns warning An owns target resolves to a node whose kind cannot be owned; the message names that kind. An id that resolves to nothing is not this diagnostic’s business — owns-target-not-found reports that instead, so one mistake draws exactly one of the two. Import-coupled as a consequence: in a single-document context a cross-file target resolves to nothing, so nothing is reported. The ownable kinds are service / domain / client / infra blocks at any depth (OWNS_TARGET_KINDS, which only this check reads — existence asks nothing about kinds); an infra leaf (table / queue-item / bucket) and a capability exist but are not ownership units, so this is the check that rejects them. Note the system-view card draws a team chip only for the logical kinds; an owned infra block still reads under Group by: team (frames resolve by id) and in the org view.
contains-target-not-found warning Import-coupled: a document that still has imports to resolve does not decide it at all — the member may be declared in an imported file, and a cross-file system reopen can add the very child a scoped contains names. Otherwise: a boundary contains a node that does not exist — for a top-level block, anywhere in the merged system hierarchy (existence is checked after cross-file merge, not per file); for a scoped block, among the declaring node’s direct children (matched by the suffix rule of the node reference path notation).
owns-target-ambiguous warning An owns reference (suffix path, bare id included) resolves to two or more nodes that are not uniform in (kind, depth). A uniform multi-match is intentional broadcast (migration coexistence, multi-tenant) and stays silent. The message lists each candidate’s full path so the author can qualify with a longer path. Import-coupled like owns-target-not-found: decided only on the merged model.
contains-target-ambiguous warning The contains counterpart of owns-target-ambiguous, for top-level boundary blocks: a reference matching nodes of mixed kind or depth is reported with the candidate full paths; a uniform multi-match broadcasts silently. Structurally unreachable in the scoped form — members resolve among sibling-unique direct children. Import-coupled: decided only on the merged model.
realizes-target-ambiguous warning A realizes reference (suffix path, bare id included) resolves to two or more realizable nodes (service / domain / client / infra blocks) not uniform in (kind, depth); candidates are listed as full paths. Uniform multi-matches stay silent. Existence stays with unresolved-realizes. Import-coupled: decided only on the merged model.
import-target-ambiguous warning A multi-segment import { … } entry, resolved by the suffix rule, matches two or more nodes not uniform in (kind, depth). Every match is still imported — bare-id imports have always broadcast — and the warning lists the candidate full paths so the author can qualify.
facet-not-declared warning A facets reference names no declared facet block (existence is checked on the merged model, so a declaration in an imported file counts). Unlike the near-miss annotation hint, the declared set makes this check complete: a typo between two author-defined names is caught too. Reported on the element that carries the reference — the declaring node or edge, not the facets line itself — so the range is the same one every other element-level diagnostic uses. It names a node by its id and an edge by its canonical base form (A-->B), an edge having no id of its own; that is the string edge#<id> addresses the same edge with.
import-id-not-found error A named import id path fails to resolve.
import-path-not-found error An import path fails to resolve at some segment.
unresolved-edge-endpoint warning An edge endpoint id is not found anywhere in the merged model.
unresolved-handles warning A handles domain is not reachable through the one-hop expose rule. The reference is a node reference path resolved against the domains that are direct children of a system-level child, and the warning anchors on the reference itself, so one entry of handles A, B can fail while the other resolves. handles has no *-target-ambiguous twin, unlike owns / contains / realizes: every candidate in that pool is a domain at the same depth, so a multi-match is uniform by construction (the multi-tenant broadcast pattern) and a longer path is not a remedy the author could apply. A reference naming a domain outside the pool is reported here rather than as an ambiguity.
unresolved-realizes warning A deploy node realizes a target absent from the logical layer.
legend-ref-unresolved warning A legend ref matches no style rule and no node.
cross-system-ref-unresolved warning A cross-system edge (Sys.Svc) target is not found.
cross-system-ref-implicit-external warning A cross-system edge crosses into a system not tagged [external].
delivers-target-not-client warning A delivers target is not a client node.
unresolved-resource-ref warning A dot-notation resource <Infra>.<Leaf> names an infra block the merged model does not declare, or a leaf that block does not declare. The message says which half is missing, because the repairs differ: a missing block usually means the whole database / queue / storage declaration was lost, a missing leaf that only the sub-resource is absent. [external] resources are exempt, matching unassigned-resource. Import-coupled — shared infra is canonically declared in a file each slice imports (§S4.5), so a single document does not decide it.
unresolved-table-ref warning An entity’s table <Infra>.<Leaf> mapping names an undeclared block or leaf; same missing split, same [external] exemption, same import-coupling as unresolved-resource-ref. An entity with no table mapping is a legitimate state (forward design, read-model projection, KV-backed aggregate) and is never reported here — karasu coverage counts it instead.

Infra nodes are declared once; a store referenced by several services is a fact worth surfacing.

Code Severity Fires when
infra-redeclared-across-files info The same database / queue / storage id is declared in more than one merged file. Reported whichever import form brought the second declaration in: a whole-file import, or a named entry whose path roots at the block (import { UserDB.users }). Two entries naming one declaration are one import, not a reopen.
infra-leaf-redeclared-silently info A table / queue-item / bucket leaf is redeclared within its parent infra.
shared-infra-fan-in info Two or more services depend on the same store within one system (a fact, not a defect).
cross-domain-store-access info A usecase reads/writes an infra leaf owned by another domain, in one system (a boundary-crossing fact, not a defect). Ownership is derived from entity mappings; keyed at leaf granularity; [external] and role-tagged ([index] / [cache] / [analytics]) stores excluded. Orthogonal to shared-infra-fan-in.

The grammar of operation / CRUD decoration on resources.

Code Severity Fires when
invalid-crud-decoration error A CRUD decoration uses an unrecognised verb / letter.
empty-crud-decoration warning A verb: decoration has an empty right-hand side.
unknown-resource-operation warning A resource operation verb is not one of create / read / update / delete.

Whether structural nodes are assigned to an owner / parent, and cohesion facts about how domains and deploy targets are wired.

Code Severity Fires when
unassigned-service warning A service sits at top level with no team assignment.
unassigned-domain warning A domain is not assigned to a service — it sits at top level, or directly inside a system. Both placements express the same modelling state, so both fire (#2184); only the top-level form is additionally wrapped in the (Unassigned) pseudo-system.
unassigned-usecase warning A usecase is a direct child of a service with no domain parent.
unassigned-client warning A client sits at top level with no team assignment.
unassigned-database warning A database sits at top level with no team assignment.
unassigned-queue warning A queue sits at top level with no team assignment.
unassigned-storage warning A storage sits at top level with no team assignment.
unassigned-resource warning A bare resource <id> resolves to no store: not dot-notation, not [external], and not a unique entity. Model-wide (resolver, not parser), so it is promoted away — with zero edits — once a matching entity is declared. An ambiguous bare id (>1 matching entity) stays unresolved; the collision is reported by entity-anchor-collision.
domain-dispersal info One domain id appears under multiple services in scope (a fact).
missing-realizes info A deploy node lacks a realizes property.
missing-runtime info A deploy node lacks a runtime property.

Annotation parameters, removed / deprecated properties, and the v1.x deprecation of non-builtin tag / annotation vocabulary (syntax v2.0 accepts tool vocabulary only — see tags-annotations.md).

Code Severity Fires when
annotation-param-unsupported warning An annotation parameter key is not recognised for that annotation.
annotation-param-value-unreadable warning A recognised annotation parameter’s value is not one string literal or bare word (until: 2026-12-31, from: system, from: Shop.Legacy). Nothing is recorded. Rendering is unaffected; karasu fmt refuses to rewrite the file, because printing the AST would drop the value.
annotation-param-conflict warning One element gives the same annotation parameter two different values, across repeated annotations or inside one. The first value is kept, and karasu fmt refuses to rewrite the file rather than print the first over the second.
duplicate-annotation warning The same annotation is written more than once on one element. The repeat has no effect.
annotation-possible-typo info An annotation name is a near-match to a builtin (typo hint).
tag-not-builtin warning A tag name is outside the tool vocabulary (builtin + system-assigned tags). Deprecated in v1.x; no suppression condition.
tag-not-applicable warning A builtin tag is written on a node kind outside its applicability (e.g. service Api [index] — [index] applies to database). The tag has no effect there. Never fires together with tag-not-builtin: a non-builtin name has no applicability to violate.
annotation-not-builtin warning An annotation name is outside the builtin set. Deprecated in v1.x; no suppression condition.
style-tag-selector-not-builtin warning A .krs.style selector targets a tag name outside the tool vocabulary (e.g. [pci] { … }). Deprecated in v1.x — the rule still matches; syntax v2.0 matches tool vocabulary only. Migrate to a facet selector ([facets=<id>]). Fires per selector, independently of the model-side tag-not-builtin: the two name two edits, and warning once would leave the other site unfound. Never fires for the builtin theme or injected system sheets.
style-annotation-selector-not-builtin warning A .krs.style selector targets an annotation name outside the builtin set (e.g. @canary { … }). Same contract as style-tag-selector-not-builtin.
team-property-removed error The removed team property is used (see ADR-1564).

Resolving import declarations and style imports against the filesystem.

Code Severity Fires when
circular-import warning A node import forms a cycle.
circular-style-import warning A style import forms a cycle.
file-not-found error An imported file does not exist.
directory-not-found error An imported directory does not exist.
style-file-not-found warning An imported style file does not exist.

Validating .krs.style property names and values.

Code Severity Fires when
style-unknown-property warning A style property name is not recognised.
style-unknown-icon warning A shape: url("<name>") names no registered icon, so the node is drawn as box. Fires on every surface, because the built-in icon set is registered by core itself (TPL-2802).
style-invalid-enum-value error A style value is not in the allowed enum.
style-invalid-hex-color error A style hex color is malformed.
style-invalid-length-unit error A style length uses a disallowed unit.
style-missing-length-unit error A style length is missing its required unit.
style-out-of-range error A style numeric value is outside its min / max bounds.
style-token-type-mismatch error A style token does not match the expected type.
expected-style-property-name error The style parser expected a property name.
expected-semicolon-between-properties error The style parser expected a ; between properties.
unknown-edge-selector-attribute error A selector uses a bracket attribute other than from / to / facets (e.g. edge[source=X]). The code name predates facets, which is accepted on node selectors too, not only on edge.
style-conflict warning A selector is defined in more than one user style sheet.
style-column-invalid-value warning A style column value is not left / center / right.
style-column-ignored-non-system-view warning A column hint is applied to a deploy / org view (ignored).
style-grid-columns-invalid-value warning A style grid-columns value is not a positive integer (the hint is dropped; layout auto-balances).

Related TPLs: TPL-2802 (style-unknown-icon reads the shape registry, whose built-in contents core fills itself, so the verdict is the same on every surface), TPL-1503 (a value the parser accepts has an effect or a diagnostic, never silence).

The client sub-language: storage kinds and capabilities.

Code Severity Fires when
client-resource-invalid-kind error A client resource storage kind is not one of the reserved values.
client-capability-duplicate warning A client declares the same capability name twice.

Low-level parser errors raised when tokens do not form a valid construct. These are mechanism-level by nature; the “rule” is the grammar itself.

Code Severity Fires when
token-type-mismatch error A token does not match the type the parser expected.
unexpected-token-root error An unexpected token appears at the root level.
unexpected-token-in-block error An unexpected token appears inside a block.
expected-brace-or-string error The parser expected a { or a string literal.
expected-identifier error The parser expected an identifier.
expected-string-after error The parser expected a string after a property keyword.
expected-id-or-string error The parser expected an id or a string.
expected-node-id error The parser expected a node id.
expected-property-value error The parser expected a property value.
expected-id-after error The parser expected an id after a property keyword.
invalid-node-kind error A node kind keyword is not recognised.
property-not-for-node-kind error A property is not valid for the node kind it appears on.
link-url-scheme-not-allowed warning A link URL scheme is not in the allowed set (http / https / mailto).

Synthetic codes the app uses when wrapping a thrown compile / parse error.

Code Severity Fires when
app-project-compile-error error compile() threw and the app reports a generic compile failure.
app-org-parse-error error Org parsing threw and the app reports a generic parse failure.
generic-text error A pre-built fallback message string with no structured params.

Every member of DiagnosticParamsByCode and WarningKind (in packages/core/src/types) must appear as a code in this document. A meta-test (packages/core/src/types/diagnostics-catalog.test.ts) asserts this in both directions, so the catalog cannot silently drift from the emitted codes. The discipline behind it is recorded as TPL-1623.

Related TPLs: TPL-1623 (catalog ↔ code completeness), TPL-1386 (fact vs style register), TPL-2171 (spec-promised diagnostics are implemented), TPL-1296 (spec ↔ source-of-truth sync).

© 2026 Hiroki Kondo · Licensed underApache-2.0

Built with Cloudflare