Building code as machine-readable rules. A small DSL for testable code requirements — egress widths, stair riser/tread ratios, corridor clearances, setbacks, parking ratios, daylight — with fixtures for every rule and a runner that checks a model against a selected jurisdiction.
plancheck check my-building.ifc --jurisdiction TCVNplancheck — my-building.ifc (TCVN)
===================================
FAIL TCVN-QCVN06-CORRIDOR-WIDTH [Space C-01]
C-01: rộng thông thủy 1100 mm, nhỏ hơn mức tối thiểu 1200 mm
source: QCVN 06:2022/BXD § 3.3.5
FAIL TCVN-QCVN06-DOOR-CLEAR-WIDTH [Door D-02]
Cửa thoát nạn rộng thông thủy 750 mm, nhỏ hơn mức tối thiểu 800 mm
source: QCVN 06:2022/BXD § 3.2.9
SKIP TCVN-2622-LEGACY-ESCAPE-WIDTH [Space C-01]
cannot tell whether this rule applies: model lacks the data
source: TCVN 2622:1995 § 5.4
…
2 failed, 13 skipped (missing data), 6 passed, 0 rule errors — 26 rules over 6 entities
Three jurisdictions ship encoded — TCVN (Vietnam), IBC (US) and EN (Eurocode and the EN standards around it) — because two would look like a coincidence and one would look like a product. The rest arrive from the people who need them: a jurisdiction is a directory of TOML files, not a code change.
Python 3.11+. One dependency — ifc-spf, a standard-library IFC reader — and only for the IFC path: rules, fixtures and JSON models need nothing at all.
1. A rule is a file, and the file carries its own tests.
id = "TCVN-QCVN06-DOOR-CLEAR-WIDTH"
title = "Chiều rộng thông thủy của cửa trên lối ra thoát nạn"
jurisdiction = "TCVN"
severity = "error"
status = "draft"
tags = ["egress", "door", "fire"]
[source]
document = "QCVN 06:2022/BXD"
clause = "3.2.9"
[applies_to]
entity = "Door"
where = "props.is_egress == true"
[check]
assert = "props.clear_width_mm >= 800"
message = "Cửa thoát nạn rộng thông thủy {props.clear_width_mm} mm, nhỏ hơn mức tối thiểu 800 mm"
[[fixtures]]
name = "cửa 900 mm — đạt"
expect = "pass"
entity = { type = "Door", props = { is_egress = true, clear_width_mm = 900 } }
[[fixtures]]
name = "cửa 700 mm — không đạt"
expect = "fail"
entity = { type = "Door", props = { is_egress = true, clear_width_mm = 700 } }A rule without fixtures is an opinion. A rule whose fixtures show 700 mm failing
and 800 mm passing is a testable reading of a clause — which is the only kind
worth merging from a stranger. plancheck rules test runs all 187 fixtures in
the bundled library; CI runs the same function on every pull request.
2. Missing data is its own outcome.
pass / fail / skip / error. A checker that quietly passes a door
because the model never recorded its width is worse than no checker, so absent
data is counted, reported, and — with --strict — fatal. Where a threshold
depends on unknown context, the rules take the strict branch: an unknown never
buys a relaxation.
3. Rules are more expressive than a threshold table, and the tool is honest about the cost.
The expression language handles the shapes real codes actually have:
| shape | example | rule |
|---|---|---|
| a plain threshold | props.clear_width_mm >= 800 |
TCVN egress door |
| a conjunction | props.riser_height_mm <= 178 and props.tread_depth_mm >= 279 |
IBC 1011.5.2 |
| proportional to another entity | props.clear_width_mm >= 5.08 * rel.space.props.occupant_load |
IBC 1005.3.2 |
| a conditional branch | 914 if default(props.occupant_load, 50) < 50 else 1118 |
IBC 1011.2 |
| a two-variable table | setback from road width × building height | QCVN 01:2021 Bảng 2.7 |
| a step function | accessible parking by lot size | IBC Table 1106.1 |
4. It extends buildingSMART IDS rather than competing with it.
plancheck ids export --jurisdiction TCVN -o tcvn.ids18/43 rules expressible as IDS
not exported: IBC-1005-3-2-DOOR-CAPACITY — right-hand side is not a literal:
props.clear_width_mm >= 5.08 * rel.space.props.occupant_load
not exported: TCVN-QCVN01-SETBACK-FRONT — right-hand side is not a literal: …
Every rule that reduces to property restrictions becomes an IDS 1.0
specification, with units converted to SI as IDS requires (800 mm exports as
minInclusive value="0.8"). Every rule that does not is named, with the
reason. A checker that silently drops half your rules on export is how people
get hurt.
The reverse works too — plancheck ids import existing.ids -o plancheck/rules/mine/ turns
an IDS library into draft rule files, so IDS is a starting point rather than a
competitor.
pip install -e . # pulls ifc-spf, the IFC readerThe rule engine itself needs nothing, so a clone works as-is for JSON models,
rules test, rules lint and ids export:
python -m plancheck check examples/mixed-use-hanoi.jsonReading .ifc is the one path that needs the dependency; without it, plancheck
says so instead of failing obscurely.
# check a model against a jurisdiction (.ifc or .json)
plancheck check examples/small-office.ifc -j IBC
# a report you can hand to someone
plancheck check model.ifc -j TCVN -f html -o report.html
plancheck check model.ifc -j TCVN -f json | jq '.summary'
# narrow the run
plancheck check model.ifc -t egress -t stair
plancheck check model.ifc --rule IBC-1011-5-2-RISER-TREAD
# the rule library
plancheck rules list -j EN
plancheck rules show TCVN-QCVN06-DOOR-CLEAR-WIDTH
plancheck rules test # every fixture in every rule
plancheck rules lint # the editorial checklist
plancheck rules coverage # which properties an IFC file can actually supply
# IFC and IDS
plancheck ifc inspect model.ifc # normalised model JSON — start debugging here
plancheck ids export -j EN -o en.ids
plancheck ids import vendor.ids -o plancheck/rules/vendor/Exit codes: 0 clean · 1 a rule of severity error failed (or anything failed
under --strict) · 2 a rule or an input is broken. Drop it into CI as-is.
Rules never see IFC. They see a flat neutral model — entities with props and
named relationships — and anything that can produce that shape can feed them:
{
"project": { "jurisdiction": "TCVN" },
"entities": [
{ "id": "COR-1", "type": "Space", "name": "Hành lang tầng 1",
"props": { "space_type": "corridor", "clear_width_mm": 1100 },
"rel": { "storey": "L1" } },
{ "id": "D-02", "type": "Door",
"props": { "is_egress": true, "clear_width_mm": 750 },
"rel": { "space": "COR-1" } }
]
}ifc-spf does the reading:
property sets, quantity sets, spatial containment and the unit assignment, with
no ifcopenshell and no compiled dependency. It ignores geometry on purpose —
code rules ask how wide is this door, and the answer lives in a property set,
not in a swept solid — and it handles the things hand-rolled readers miss:
property sets inherited from an IfcDoorType, a property whose own Unit
overrides the project's, .ifc.gz, and IFC2X3 files whose attribute order
differs from IFC4.
If you already run ifcopenshell, write a 30-line exporter to the JSON above and skip the reader entirely; the rule library does not care which importer filled the model.
Which IFC property feeds which rule property is itself data
(plancheck/profiles/ifc_map.toml), so a national IFC profile or
a vendor's non-standard pset is a pull request, not a fork:
[[property]]
prop = "clear_width_mm"
entity = "Door"
pset = "Pset_PlancheckDoor"
name = "ClearWidth"
unit = "length"
target_unit = "mm"
note = "Preferred: unobstructed opening width with the leaf at 90°."plancheck rules coverage reports which properties the mapping can fill from an
IFC file and which it cannot — the gap between "we have a model" and "we can
check this" is a fact worth printing.
mkdir -p plancheck/rules/sg
$EDITOR plancheck/rules/sg/corridor-width.toml # copy the shape above
plancheck rules test --rules plancheck/rules/sg
plancheck rules lint --rules plancheck/rules/sgThen open a pull request. One file, one clause, at least one passing and one failing fixture, a citation to the document and clause. See CONTRIBUTING.md and docs/RULE_SPEC.md; the property vocabulary is in docs/VOCABULARY.md.
Every bundled rule is marked status = "draft". The numbers were transcribed
from published codes as a demonstration of the mechanism, and they carry
notes where the reading is uncertain. Nothing here has been reviewed by a
licensed engineer, a fire officer or a building control authority, and several
values are Nationally Determined Parameters or local-authority discretion that
differ from the figures encoded.
Do not put a plancheck report in a submission. Use it to catch the boring
half of the problems early, in a form you can diff, test and argue with — and
check the clause yourself before you rely on the answer. A rule reviewed against
the official text can be promoted to status = "reviewed"; that promotion is
itself a reviewable pull request.
MIT licensed.