Skip to main content

Diagnostics

The surface that checks the consistency of a composition. It says only that what is written is not self-contradictory as data; it says nothing about whether the result works as architecture. That judgement is made separately by validate.

import { check, checkDiagnostics, DIAGNOSTIC_CODES } from "@kensnzk/koyu";
import type { CheckResult, Diagnostic, DiagnosticCode } from "@kensnzk/koyu";

checkDiagnostics

function checkDiagnostics(model: Model): Diagnostic[]

This is the primary form. Use it whenever you want structure.

import { checkDiagnostics, parse } from "@kensnzk/koyu";

const model = parse(`grid X 0 3600 7200
grid Y 0 4000 8000
level L1 0 h:2400
space /L1/a room X1..X2 Y1..Y2
space /L1/b room X2..X3 Y2..Y3
boundary /L1/a /L1/b t:120`);
console.log(JSON.stringify(checkDiagnostics(model), null, 1));
[
 {
  "code": "BND04",
  "severity": "error",
  "message": "The spaces do not touch, so no boundary can be derived: /L1/a | /L1/b",
  "line": 6,
  "path": [
   "/L1/a",
   "/L1/b"
  ]
 },
 {
  "code": "SUF03",
  "severity": "warning",
  "message": "Level L1 has no slab:, so not one floor is generated on this storey",
  "line": 3
 }
]

checkDiagnostics never throws. Syntax and composition errors are thrown by the parse functions as a SourceError; catch those there.

Three disciplines

The population is the written declarations. One diagnostic per written line, not per derived result.

Every diagnostic has a source. A diagnostic without line is one that cannot be attributed to any line at all — and there are very few.

The order is scan order. Diagnostics are never regrouped by code family. The same input always yields the same order.

Diagnostic

interface Diagnostic {
code: DiagnosticCode;
severity: "error" | "warning";
message: string;
line?: number;
file?: string;
path?: string[];
related?: Array<{ line: number; file?: string }>;
}
FieldContents
codethe ledger code (2–3 letters of domain plus two digits)
severity"error" or "warning"an invariant property of the code
messagethe body. It carries no position prefix
linethe source line
filethe source layer under composition (a resolved absolute path)
paththe space, zone or polygon concerned (both, for a boundary)
relatedrelated positions — the earlier of a duplicate, the other side of an overlap

severity belongs to the code and does not vary with circumstance. The same code is never an error one time and a warning the next. When the weight needs to change, a new code is cut. So you may safely branch on the DIAGNOSTIC_CODES table.

related answers "and where is the other one?" — the earlier occurrence of a duplicate uid, the space on the other side of an overlap.

const dup = parse(`grid X 0 3600 7200
grid Y 0 4000
level L1 0 h:2400
space /L1/a room X1..X2 Y1..Y2 uid:u-aaaaaaaaaaaaaaaa
space /L1/b room X2..X3 Y1..Y2 uid:u-aaaaaaaaaaaaaaaa`);
console.log(JSON.stringify(checkDiagnostics(dup)[0], null, 1));
{
 "code": "UID03",
 "severity": "error",
 "message": "Duplicate uid: u-aaaaaaaaaaaaaaaa (space /L1/a — line 4, space /L1/b — line 5)",
 "path": [
  "/L1/a",
  "/L1/b"
 ],
 "related": [
  {
   "line": 4
  },
  {
   "line": 5
  }
 ]
}

check

function check(model: Model): CheckResult

interface CheckResult {
errors: string[];
warnings: string[];
}

The compatibility string form. Same count, same order as checkDiagnostics, with the position prefix (file:line N: ) assembled into the string. For showing a human directly.

import { check } from "@kensnzk/koyu";
const { errors, warnings } = check(model);
console.log(errors, warnings);
[
  'line 6: The spaces do not touch, so no boundary can be derived: /L1/a | /L1/b'
] [
  'line 3: Level L1 has no slab:, so not one floor is generated on this storey'
]

This example was read with parse, so there is no source file and the prefix is just the line number. Read with parseFile it would be <absolute path>:line 6: .

Codes do not appear in the strings. To branch on a code, use checkDiagnostics.

DIAGNOSTIC_CODES

const DIAGNOSTIC_CODES: Record<DiagnosticCode, "error" | "warning">

type DiagnosticCode = keyof typeof DIAGNOSTIC_CODES;

Every code with its severity. The ledger is the single source, and a code that is not registered will not type-check.

import { DIAGNOSTIC_CODES } from "@kensnzk/koyu";
const codes = Object.keys(DIAGNOSTIC_CODES);
console.log(codes.length,
codes.filter((c) => DIAGNOSTIC_CODES[c] === "error").length,
codes.filter((c) => DIAGNOSTIC_CODES[c] === "warning").length);
console.log(DIAGNOSTIC_CODES["BND04"], DIAGNOSTIC_CODES["BND07"]);
65 49 16
error undefined

There are 65 codes: 49 errors and 16 warnings.

BND07 returns undefined because it is a retired number. Retired numbers are never reused, so a number left in an old log or an old configuration cannot come back meaning something else. The retired list, and what is said in their place, is at retired diagnostics.

Cause and cure for each code is in the diagnostics reference.

Using DiagnosticCode as a type

DiagnosticCode is the union of the codes. A spelling absent from the ledger will not compile.

import type { Diagnostic, DiagnosticCode } from "@kensnzk/koyu";

const FATAL: DiagnosticCode[] = ["BND04", "GEO02", "SUF02"];
const fatal = (ds: Diagnostic[]) => ds.filter((d) => FATAL.includes(d.code));

Put "BND07" in that array and it is a type error on the spot.

Turning it into an exit code

koyu check exits 1 when there is an error, and with --strict exits 1 on a warning too. Written out, the rule is:

import { checkDiagnostics } from "@kensnzk/koyu";

function exitCode(model: Model, strict = false): number {
const ds = checkDiagnostics(model);
if (ds.some((d) => d.severity === "error")) return 1;
if (strict && ds.length > 0) return 1;
return 0;
}

An empty diagnostic list does not mean the building works. Two touching spaces default to a wall, and a wall without a door cannot be passed. A two-storey house with not one door declared stays sealed shut with an empty list. Whether the circulation connects is doorsBetween; the architectural judgement is validate.

See also