Skip to content

Repository files navigation

plancheck

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 TCVN
plancheck — 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.


The four decisions that make this different from a linter with a hardcoded rulebook

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.ids
18/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.


Install

pip install -e .          # pulls ifc-spf, the IFC reader

The 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.json

Reading .ifc is the one path that needs the dependency; without it, plancheck says so instead of failing obscurely.

Use

# 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.

How a model gets in

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.

Adding your jurisdiction

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/sg

Then 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.

Status, and what this is not

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.

About

Building code as machine-readable, testable rules — TOML rule files with fixtures, checked against IFC models, exportable to buildingSMART IDS.

Topics

Resources

Contributing

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages