From 9ef497e51dac7b46134a40b3fb4e71f02cec5773 Mon Sep 17 00:00:00 2001 From: Griffin Long <151978260+DrunkOnJava@users.noreply.github.com> Date: Sun, 30 Aug 2026 15:45:02 -0400 Subject: [PATCH] =?UTF-8?q?corpus:=20mirror=20rvt-rs=20#226/#230/#231=20?= =?UTF-8?q?=E2=80=94=20relations=20and=20storeys=20join=20the=20surface;?= =?UTF-8?q?=20spec=201.1.0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Brings the umbrella to OctetProof 1.1.0 and mirrors the current rvt-rs corpus state. The protocol gains two additive field classes and the corpus gains two agreement classes it did not have: topology, and labels-with-units. Protocol (byte-identical copies from rvt-rs, the same policy as the registry) SPEC.md 1.0.2 -> 1.1.0 schemas/witness-observation.schema.json schema_version enum ["1.0.0","1.1.0"], optional observation.relations and observation.storeys schemas/witness-verdict.schema.json diff only_in_a / only_in_b registry/witnesses.json already byte-identical, unchanged 1.1.0 is minor and additive (SPEC.md 16.1, 20). The diff function, the canonicalizer, the provenance model and the verdict statuses are unchanged; a 1.0.0 observation is still valid input. Gate (tools/) verdict.py ports the two field classes from rvt-rs tools/ci/witness-verdict.py. A `relations` category carrying relation_ifc_type contributes relations.; a `storeys` category carrying storey_ifc_type contributes storeys.. Both are exact sorted-multiset equality with no tolerance concept (7.2); a diff carries the set sizes in value_a / value_b plus only_in_a / only_in_b and tolerance_applied: false. The surface is built counts -> relations -> storeys, which is the order the committed verdict records. A witness compared on a set-valued class must declare it in semantic_surface_covered, else MANIFEST_ERROR. The umbrella-manifest shape is kept: semantic_surface and excluded stay explicit and now cross-check against all three blocks, so a status flip is still MANIFEST_ERROR rather than a silent surface change. witness-ifcopenshell.py ports the relation and storey extraction. The storey elevation is resolved through ifcopenshell.util.unit.calculate_unit_scale and rendered in feet at 1e-6 as a fixed six-decimal string, -0 normalised to 0. jsonschema_mini.py implements maxItems, the one keyword the new schemas introduce. It fails loudly on unimplemented keywords, so without this the corpus would not validate at all. validate-corpus.py reports against the 1.1.0 schemas. compare-verdict.py, fetch.py, index.py, check-registry.py, replay.sh unchanged; the new fields needed nothing from them and the 9.6 pin is still one number (ifc-lite-core =7.1.1). Witness glue (witnesses/ifc-lite/) Re-mirrored from tools/ci/witness-ifc-lite/ in DrunkOnJava/rvt-rs at a0044f4a11361626c2a4b9fb93c4696ac6b1c6e6 (PR #230): fills_element_pairs and building_storey_set, the latter resolving units with ifc_lite_core::extract_length_unit_scale. The only deviation from upstream remains the manifest-reading path (bridge.file_hash_sha256 / bridge.bytes / bridge.schema, bridge file passed directly). Cargo.lock unchanged, no new dependency, version pin unchanged. Corpus Observations and verdicts for both artifacts copied verbatim from research/witness// in rvt-rs. Both manifests rebuilt from the project-count fixtures under the documented drop rules (decoder_* dropped, a category kept iff it carries its block's type key), gaining `relations` and `storeys` blocks and `levels` as `known`. Both declare octetproof_version 1.1.0, because both now carry 1.1.0 blocks. MANIFEST_INDEX.json regenerated: chain head 5e6a64228083. g-2026-0001 4 surface fields, 9 excluded -> 6 surface, 10 excluded g-2026-0002 10 surface fields, 4 excluded -> 13 surface, 3 excluded g-2026-0002 now claims 360 IFCWALL, 256 IFCCOLUMN, 132 IFCDOOR, 80 IFCSLAB, 20 IFCSHADINGDEVICE, 15 IFCBUILDINGSTOREY, 6 IFCWINDOW, 1 IFCUNITASSIGNMENT and three zeroes, plus 138 IfcRelFillsElement [host Tag, filling Tag] pairs and the 15 [name, elevation] storeys. Three known_gap exclusions remain: IFCSPACE (116 vs 18), IFCMATERIAL (10 vs 102) and IFCPROPERTYSET (0 vs 854). g-2026-0001 stays thin — three of its five counts are zero on both sides — with one exception worth the record: Revit writes the complete fifteen-storey spatial hierarchy into even a one-element 20 KB export, so storeys.IFCBUILDINGSTOREY is as strong on the fixture as on the full export. relations.IFCRELFILLSELEMENT goes the other way and is excluded decoder_baseline: that fixture carries no IfcRelFillsElement at all. Local verification, clean _replay, both artifacts end to end $ OCTETPROOF_PYTHON=.../ifcos/bin/python tools/replay.sh corpus/artifacts/g-2026-0001 corpus: 9 observation(s) and 2 verdict(s) conform to the 1.1.0 schemas g-2026-0001: PASS - witnesses ifc-lite, ifcopenshell, rvt-rs surface: 6 fields, excluded: 10, diffs: 0 replay ifc-lite: match replay ifcopenshell: match replay rvt-rs: match verdict matches the committed record (ignored: artifact_id, replay, timestamp, verdict_hash_sha256) == replay OK: g-2026-0001 PASS, observations replayed, verdict matches $ OCTETPROOF_PYTHON=.../ifcos/bin/python tools/replay.sh corpus/artifacts/g-2026-0002 corpus: 9 observation(s) and 2 verdict(s) conform to the 1.1.0 schemas g-2026-0002: PASS - witnesses ifc-lite, ifcopenshell, rvt-rs surface: 13 fields, excluded: 3, diffs: 0 replay ifc-lite: match replay ifcopenshell: match replay rvt-rs: match verdict matches the committed record (ignored: artifact_id, replay, timestamp, verdict_hash_sha256) == replay OK: g-2026-0002 PASS, observations replayed, verdict matches $ python3 tools/index.py --check corpus/MANIFEST_INDEX.json: chain head 5e6a64228083... ok (2 artifact(s)) $ python3 tools/check-registry.py witnesses.json: valid - 11 nodes, 47 witnesses, 7 artifacts, 3 edges, 9 agreements; 2 corpus manifest(s) resolve $ python3 tools/validate-corpus.py corpus: 6 observation(s) and 2 verdict(s) conform to the 1.1.0 schemas Both bridge witnesses hash identically on both artifacts - 99e6cd7a on g-2026-0001, 5a101408 on g-2026-0002 - across all 14 entity types, all 138 relation pairs and all 15 storey pairs, including the unit resolution, which each reader performed with its own code. Fail-closed paths exercised, not just assumed: mutating one relation host tag and one storey elevation in a fresh ifc-lite observation produces DISAGREE with only_in_a / only_in_b naming exactly the changed members; stripping `storeys` from a witness's semantic_surface_covered produces MANIFEST_ERROR; a three-element pair and an out-of-enum schema_version are both rejected by jsonschema_mini. Docs: README's "what exists today" rewritten around the three agreement classes with the real numbers, both PROVENANCE.md files rewritten (witness-set hashes, surfaces, exclusion tables), corpus/README's departure table updated for the 1.1.0 blocks and the octetproof_version policy, tools/README gains the field-class table and the reasons the storey elevation is a string and storey names are not Unicode-normalised, CONTRIBUTING's coverage vocabulary extended. --- .github/workflows/verdict.yml | 6 +- CONTRIBUTING.md | 8 +- README.md | 196 ++++-- SPEC.md | 151 +++- corpus/MANIFEST_INDEX.json | 12 +- corpus/README.md | 45 +- corpus/artifacts/g-2026-0001/PROVENANCE.md | 148 ++-- corpus/artifacts/g-2026-0001/manifest.json | 45 +- .../g-2026-0001/observations/ifc-lite.json | 78 +- .../observations/ifcopenshell.json | 78 +- .../g-2026-0001/observations/rvt-rs.json | 664 +++++++++++++++++- corpus/artifacts/g-2026-0001/verdict.json | 13 +- corpus/artifacts/g-2026-0002/PROVENANCE.md | 157 +++-- corpus/artifacts/g-2026-0002/manifest.json | 55 +- .../g-2026-0002/observations/ifc-lite.json | 630 ++++++++++++++++- .../observations/ifcopenshell.json | 630 ++++++++++++++++- .../g-2026-0002/observations/rvt-rs.json | 664 +++++++++++++++++- corpus/artifacts/g-2026-0002/verdict.json | 14 +- registry/README.md | 2 +- schemas/witness-observation.schema.json | 36 +- schemas/witness-verdict.schema.json | 14 +- tools/README.md | 37 +- tools/jsonschema_mini.py | 14 +- tools/validate-corpus.py | 8 +- tools/verdict.py | 158 +++-- tools/witness-ifcopenshell.py | 128 +++- witnesses/ifc-lite/Cargo.toml | 5 +- witnesses/ifc-lite/src/main.rs | 300 +++++++- 28 files changed, 3911 insertions(+), 385 deletions(-) diff --git a/.github/workflows/verdict.yml b/.github/workflows/verdict.yml index 5fce111..3358622 100644 --- a/.github/workflows/verdict.yml +++ b/.github/workflows/verdict.yml @@ -71,8 +71,8 @@ jobs: - name: Validate the witness registry against its schema run: python3 tools/check-registry.py # §6.2/§6.3: every committed observation and verdict conforms to the - # 1.0.0 schemas, and every committed hash is self-consistent. - - name: Validate the committed corpus against the 1.0.0 schemas + # 1.1.0 schemas, and every committed hash is self-consistent. + - name: Validate the committed corpus against the 1.1.0 schemas run: python3 tools/validate-corpus.py verdict: @@ -204,7 +204,7 @@ jobs: # §6.2: the freshly produced observations must conform to the schema, # not merely hash to the committed value. - - name: Validate fresh observations against the 1.0.0 schema + - name: Validate fresh observations against the 1.1.0 schema run: | python3 tools/validate-corpus.py \ --extra-observations "_work/${{ matrix.artifact }}/observations" diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 49f9678..b13ca0d 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -33,10 +33,14 @@ Your entry must declare: And, in the pull request body: - **Coverage declaration.** Which fields of the controlled vocabulary you - claim. Today that vocabulary is `entity_counts`; `layer_topology`, + claim. Today that vocabulary is `entity_counts`, `relations` and `storeys` + (the last two added in 1.1.0, SPEC.md §9.4, §20); `layer_topology`, `linework`, `bounding_boxes`, `xdata_fields` and `text_content` are specified but not yet exercised. Declaring a field you cannot actually - parse is a registration violation and grounds for removal. + parse is a registration violation and grounds for removal — and the gate + enforces the declaration for the set-valued classes: a witness that does + not declare `relations` or `storeys` makes the verdict `MANIFEST_ERROR` + rather than being quietly skipped. - **A version pin.** An exact git SHA or release tag, or the exact PyPI / crates.io / NuGet specifier the CI gate will install. Floating ranges are forbidden. A witness update is a separate pull request that includes the diff --git a/README.md b/README.md index 4908f3d..8c75eaa 100644 --- a/README.md +++ b/README.md @@ -64,61 +64,94 @@ It is still one edge, and this section says so precisely. | Artifact | Bridge file | Verdict | |---|---|---| -| `g-2026-0001` (alias `magnetar-2024-core-interior`) | `2024_Core_Interior.ifc`, 20,392 bytes, IFC4 — an element-export fixture | **PASS** — 8 surface fields, 0 diffs, 4 excluded | -| `g-2026-0002` (alias `magnetar-2024-core-interior-slim`) | `2024_Core_Interior_slim.ifc`, 1,665,968 bytes, IFC4, 19,879 entities — the full project export | **PASS** — 4 surface fields, 0 diffs, 9 excluded | +| `g-2026-0001` (alias `magnetar-2024-core-interior`) | `2024_Core_Interior.ifc`, 20,392 bytes, IFC4 — an element-export fixture | **PASS** — 6 surface fields, 0 diffs, 10 excluded | +| `g-2026-0002` (alias `magnetar-2024-core-interior-slim`) | `2024_Core_Interior_slim.ifc`, 1,665,968 bytes, IFC4, 19,879 entities — the full project export | **PASS** — 13 surface fields, 0 diffs, 3 excluded | **The two artifacts share the same `.rvt` and the same committed rvt-rs observation.** Only the Revit-authored bridge file differs, which makes the -pair a direct measurement of how much of the first verdict was real. - -**g-2026-0001 is thin, and the second artifact is why we know that.** Its -eight agreeing fields are entity counts for IFCWALL, IFCROOF, IFCDOOR, -IFCWINDOW, IFCCOLUMN, IFCBEAM, IFCFLOWTERMINAL and IFCUNITASSIGNMENT — and -seven of the eight are zero on both sides, because the reference IFC is a -20 KB element-export fixture rather than a project schedule. Three -independent readers agreeing that a file contains no doors is a real -agreement and a weak one. It exercises the whole machine — fetch by hash, -three witnesses, canonical observations, an independence check, a replay, a -fail-closed gate — on a bridge file that carries almost nothing. - -Four fields are excluded there first-class, because rvt-rs cannot yet recover -them and says so rather than pretending: - -| Excluded field | Category | Reason | Tracking | -|---|---|---|---| -| entity_counts.IFCSHADINGDEVICE | floors | known_gap: floor slab extrusion thickness | rvt-rs#31 | -| entity_counts.IFCSPACE | rooms / spaces | known_gap: typed door/window discrimination and host binding | rvt-rs#33 | -| entity_counts.IFCMATERIAL | materials | known_gap: Revit compound assemblies and WallType widths | rvt-rs#34 | -| entity_counts.IFCPROPERTYSET | property sets | known_gap: typed door/window discrimination and host binding | rvt-rs#35 | +pair a direct measurement of what a thin reference can support and what only +a real project schedule can. + +### Three agreement classes, not one + +The corpus claims three kinds of field today, and they are not equally +strong. Ranked by what agreement on them actually proves: -An excluded field never appears in a disagreement. If it does, the manifest -is wrong, not the witness (SPEC.md §7.1). +| Class | Spec | Proves | Recorded | +|---|---|---|---| +| Entity counts | §7.2 | cardinality — the readers count the same instances of the same types | both artifacts | +| Relation pair sets | §7.2, §20.1 | topology — the readers agree about *which* entity is bound to *which* | g-2026-0002 | +| Storey sets | §7.2, §20.2 | labels and units — the decoder read the name Revit wrote and put it at the height Revit wrote | both artifacts | + +A count is the weakest of the three. Two witnesses can report the same 138 +`IfcRelFillsElement` instances while disagreeing about every wall those +openings belong to; agreeing on the pair set means they agree about the +model's topology. The storey set goes further still: it is the first field in +this corpus where the two sides of the edge express the *same physical +quantity in different units* — Revit's export declares `FOOT`, rvt-rs writes +`METRE` — so each witness must resolve its own file's declared `LENGTHUNIT` +before emitting, and a unit bug is a `DISAGREE` rather than a silently +3.28×-wrong building. ### g-2026-0002: the same model against a real project schedule -The full export is where the decoder is actually measured, and the numbers -are not flattering. Both bridge readers count, independently and identically: +The full export is where the decoder is measured, across all three classes. +All three witnesses agree exactly, at tolerance 0: -| Category | The Revit export has | rvt-rs recovers | Status | -|---|---|---|---| -| walls (IFCWALL) | **360** | 0 | known_gap, rvt-rs#30 | -| doors (IFCDOOR) | **132** | 0 | known_gap, rvt-rs#32 | -| columns (IFCCOLUMN) | **256** | 0 | known_gap, rvt-rs#204 | -| rooms / spaces (IFCSPACE) | **116** | 18 | known_gap, rvt-rs#33 | -| floors (IFCSLAB) | **80** | 64 | known_gap, rvt-rs#31 | -| levels (IFCBUILDINGSTOREY) | **15** | 12 | decoder_baseline, rvt-rs#33 | -| windows (IFCWINDOW) | 6 | 0 | known_gap, rvt-rs#32 | -| materials (IFCMATERIAL) | 10 | 102 | known_gap, rvt-rs#34 — an over-count | -| property sets (IFCPROPERTYSET) | 0 | 64 | known_gap, rvt-rs#35 — an over-count against zero | - -So the claimed surface on that artifact is **four fields** — IFCROOF, -IFCBEAM, IFCFLOWTERMINAL and IFCUNITASSIGNMENT — of which only -IFCUNITASSIGNMENT is non-zero. A wider claim would be a false one while 360 -walls, 132 doors and 256 columns go unrecovered. **The nine-row table above -is the deliverable, not a footnote to it:** it is a measured gap between an -independent decoder and Revit's own exporter on a real project, each row -carrying its tracking issue, and the gate refuses to diff any of them so that -none can be quietly reclassified as agreement. +| Field | Class | Agreed value | +|---|---|---| +| entity_counts.IFCWALL | count | **360** | +| entity_counts.IFCCOLUMN | count | **256** | +| entity_counts.IFCDOOR | count | **132** | +| entity_counts.IFCSLAB | count | **80** | +| entity_counts.IFCSHADINGDEVICE | count | **20** | +| entity_counts.IFCBUILDINGSTOREY | count | **15** | +| entity_counts.IFCWINDOW | count | 6 | +| entity_counts.IFCUNITASSIGNMENT | count | 1 | +| entity_counts.IFCROOF / IFCBEAM / IFCFLOWTERMINAL | count | 0 | +| relations.IFCRELFILLSELEMENT | relation pair set | **138 `[host Tag, filling Tag]` pairs** — every door and window in its own host wall | +| storeys.IFCBUILDINGSTOREY | storey set | **15 `[name, elevation]` pairs** — `Basement 2` at −40 ft through `Level 13` at 185.5 ft | + +Three fields are excluded first-class, because rvt-rs and the export disagree +in ways that are tracked decoder gaps rather than verification failures: + +| Excluded field | Category | Export | rvt-rs | Tracking | +|---|---|---|---|---| +| entity_counts.IFCSPACE | rooms / spaces | 116 | 18 | rvt-rs#33 | +| entity_counts.IFCMATERIAL | materials | 10 | 102 | rvt-rs#34 — an over-count | +| entity_counts.IFCPROPERTYSET | property sets | 0 | 854 | rvt-rs#35 — an over-count against zero | + +**That table used to have nine rows.** Walls, doors, windows, columns, slabs +and shading devices were all measured gaps here; rvt-rs closed each of them +as an exact id-set match, and `levels` with it. The three that remain are the +real open gap, and the gate refuses to diff them so none can be quietly +reclassified as agreement. An excluded field never appears in a disagreement; +if it does, the manifest is wrong, not the witness (SPEC.md §7.1). + +### g-2026-0001 is thin, and one field on it is not + +Its six agreeing fields are entity counts for IFCBUILDINGSTOREY, IFCROOF, +IFCBEAM, IFCFLOWTERMINAL and IFCUNITASSIGNMENT, plus +`storeys.IFCBUILDINGSTOREY`. Three of the five counts are zero on both sides, +because the reference IFC is a 20 KB element-export fixture whose single +building element is one `IFCSHADINGDEVICE`. Three independent readers +agreeing that a file contains no doors is a real agreement and a weak one. + +The storey set is the exception, and it is the useful case for the protocol: +**Revit writes the complete fifteen-storey spatial hierarchy into even a +one-element export**, so the thin fixture supports that field exactly as +strongly as the 19,879-entity project export does — the same fifteen pairs, +the same unit normalisation, agreed by all three witnesses. A thin artifact +supports a thin surface, except where it does not, and the manifest is what +says which. + +The other ten categories are excluded first-class. Nine are counts the +fixture cannot score (rvt-rs recovers 360 walls, 132 doors, 6 windows, 256 +columns, 80 slabs and 20 shading devices from this same `.rvt`, all of them +scored on the sibling artifact); the tenth is +`relations.IFCRELFILLSELEMENT`, excluded because the fixture carries no +`IfcRelFillsElement` at all. Two files that cannot be compared on a field +must say so rather than record a zero-against-zero agreement. ### The third witness @@ -129,11 +162,15 @@ geometry kernel against* IfcOpenShell, which is a comparison, not a shared lineage. On both artifacts its canonical observation payload hashes **identically** to -IfcOpenShell's (`8cf40465…` on g-2026-0001, `882e1e0f…` on g-2026-0002). Two +IfcOpenShell's (`99e6cd7a…` on g-2026-0001, `5a101408…` on g-2026-0002). Two unrelated STEP readers, in different languages, produced byte-identical -canonical payloads for every entity type on both files. That is corroboration -of the bridge-side reading and nothing more — it says the two readers agree -about what is in the file, not that `entity_counts` is a deep surface. +canonical payloads for every entity type, every relation pair and every +storey pair on both files — including the unit normalisation, which each +resolved with its own code +(`ifcopenshell.util.unit.calculate_unit_scale` against +`ifc_lite_core::extract_length_unit_scale`). That is corroboration of the +bridge-side reading and nothing more: it says the two readers agree about +what is in the file, not that the source side is proved by it. It runs in CI through `witnesses/ifc-lite/`, glue mirrored from rvt-rs, built out of workspace so MPL-2.0 code is never linked into this Apache-2.0 tree. @@ -154,13 +191,16 @@ does. the LAYER/LTYPE/BLOCK tables — with 3D ACIS solids deferred. - **dwg-rs witness mode.** dwg-rs has no `--observation` flag yet, so it cannot emit an OctetProof observation. rvt-rs already can. -- **A surface deeper than entity counts.** Three witnesses now agree on - `entity_counts`, which is a real but shallow claim: it says the readers - count the same instances of the same types, not that they agree about - geometry, placement, or property values. §11's faithful-export surface is - much wider than what any manifest here claims, and the canonicalizer needs - the full RFC 8785 number rules before a float-bearing surface can be - trusted at all. +- **A surface deeper than counts, one relation and one storey set.** Three + witnesses now agree on `entity_counts`, on `relations.IFCRELFILLSELEMENT` + and on `storeys.IFCBUILDINGSTOREY`. That is topology and labels as well as + cardinality, but it is still not geometry: no field here compares a + placement, a swept profile, a vertex or a property value. §11's + faithful-export surface is much wider than what any manifest claims, and + the canonicalizer needs the full RFC 8785 number rules before a + float-bearing surface can be trusted at all. The storey set sidesteps that + by carrying its elevation as a fixed six-decimal string; a real geometry + surface will not be able to. - **Anything that is not this one model.** Both artifacts are exports of the same `2024_Core_Interior.rvt`. A second source model would test the decoder rather than one file's worth of it. @@ -182,7 +222,7 @@ does. ## Repository layout ``` -SPEC.md Layer 1 — the protocol, version 1.0.1 (CC-BY-4.0) +SPEC.md Layer 1 — the protocol, version 1.1.0 (CC-BY-4.0) schemas/ machine-checkable observation + verdict schemas corpus/ Layer 2 — golden artifacts MANIFEST_INDEX.json hash chain over every manifest @@ -255,11 +295,11 @@ That fetches the bridge file from its public origin and rejects it unless it hashes to the recorded value, installs the pinned IfcOpenShell into a venv, builds the pinned IFClite glue with `cargo build --release --locked`, runs both bridge witnesses, copies in the committed rvt-rs observation, validates -everything against the 1.0.0 schemas, and prints: +everything against the 1.1.0 schemas, and prints: ``` g-2026-0002: PASS — witnesses ifc-lite, ifcopenshell, rvt-rs - surface: 4 fields, excluded: 9, diffs: 0 + surface: 13 fields, excluded: 3, diffs: 0 replay ifc-lite: match replay ifcopenshell: match replay rvt-rs: match @@ -268,7 +308,7 @@ verdict matches the committed record (ignored: artifact_id, replay, timestamp, v It exits 0 only on PASS with every observation replaying byte-for-byte and the fresh verdict matching the committed one. Omit the argument for -`g-2026-0001`, which prints `surface: 8 fields, excluded: 4, diffs: 0` with +`g-2026-0001`, which prints `surface: 6 fields, excluded: 10, diffs: 0` with the same three witnesses. The steps are individually runnable if you would rather see them: @@ -308,7 +348,8 @@ declaring: lineage, not by repository. uncad over LibreDWG, FreeCAD BIM over IfcOpenShell, GDAL's DGN driver over dgnlib: each pair is one witness. - **Coverage.** Which fields of the controlled vocabulary you claim - (`entity_counts` today). Declaring a field you cannot parse is a + (`entity_counts`, `relations` and `storeys` today). Declaring a field you + cannot parse is a registration violation. - **An exact version pin.** A git SHA or a release tag. Floating ranges are forbidden; silent upgrades are a protocol violation. @@ -324,19 +365,28 @@ Full process, including the clean-room rule and the no-GPL-linking rule: ## Status and licensing -Protocol version **1.0.1**, dated 2026-08-30. `SPEC.md` is the released +Protocol version **1.1.0**, dated 2026-08-30. `SPEC.md` is the released specification, not a draft: it supersedes the draft received from the project -owner on 2026-08-30, and its Section 19 lists every correction applied. The -draft, with its reviewer notes, is retained in rvt-rs at +owner on 2026-08-30, and its Section 19 lists every correction applied to +reach 1.0.0. The draft, with its reviewer notes, is retained in rvt-rs at `docs/octetproof-spec-draft.md`. -1.0.1 is a patch release (§19a): it resolves note 4's open `ifc-lite` license -question to MPL-2.0 against the exact upstream, and replaces the "umbrella -repository planned" wording now that this repository exists. **No schema, -diff-function or provenance rule changed**, so every document here still -declares `schema_version: "1.0.0"` and a 1.0.0 implementation remains -conformant. `SPEC.md` is a byte-identical copy of -`docs/octetproof-spec.md` in rvt-rs, the same policy as the registry. +**1.1.0 is a minor, additive release (§16.1, §20).** It adds two field +classes and nothing else: relation pair sets (§20.1) and storey sets (§20.2), +each with one surface-vocabulary term and one manifest block parallel to +`counts`. The diff function, the canonicalizer, the provenance model and the +verdict statuses are unchanged, and a 1.0.0 observation remains valid input +to a 1.1.0 gate — the published schema's `schema_version` accepts both +`1.0.0` and `1.1.0` so previously committed corpora stay valid (§16.2). Every +observation and verdict in this corpus is regenerated under 1.1.0 and both +manifests declare `octetproof_version: "1.1.0"`, because both now carry the +new blocks. `SPEC.md` is a byte-identical copy of `docs/octetproof-spec.md` +in rvt-rs, the same policy as the registry. + +The earlier 1.0.1 patch release (§19a) resolved note 4's open `ifc-lite` +license question to MPL-2.0 against the exact upstream and replaced the +"umbrella repository planned" wording; it changed no schema, diff function or +provenance rule. Two of the spec's schemas are machine-checkable and shipped here: `schemas/witness-observation.schema.json` (§6.2) and diff --git a/SPEC.md b/SPEC.md index e6d3e63..8457d46 100644 --- a/SPEC.md +++ b/SPEC.md @@ -1,8 +1,8 @@ # Technical Specification: OctetProof — A License-Free Verification Protocol for Undocumented Binary Formats -**Version:** 1.0.2 +**Version:** 1.1.0 **Date:** 2026-08-30 -**Status:** Specification — 1.0.2. Supersedes the draft received from the project owner on 2026-08-30, which is retained verbatim at [`docs/octetproof-spec-draft.md`](octetproof-spec-draft.md) with its reviewer notes. The corrections applied here are listed in Section 19. +**Status:** Specification — 1.1.0. Supersedes the draft received from the project owner on 2026-08-30, which is retained verbatim at [`docs/octetproof-spec-draft.md`](octetproof-spec-draft.md) with its reviewer notes. The corrections applied to reach 1.0.0 are listed in Section 19; the additive 1.1.0 change is Section 20. **License of this document:** CC-BY-4.0 **Reference implementation:** rvt-rs (Apache-2.0) — in-repo instance; umbrella repository: [DrunkOnJava/octetproof](https://github.com/DrunkOnJava/octetproof) **Primary domain:** Building Information Modeling (BIM) closed formats, with generalization to any undocumented binary container format @@ -325,6 +325,8 @@ Normative reading of the fields the gate consumes: - `counts..status` — `known` puts `entity_counts.` inside the claimed surface; `known_gap` and `unsupported` exclude it first-class (Section 7.1 rule 3), and the exclusion is recorded in the verdict with its `tracking_issue` and `unsupported_feature`. - `counts..tolerance` — the per-category count tolerance (Section 7.2). Absent means exact. - The `decoder_*` fields are the decoder repository's own regression baseline, not a cross-witness input. The gate ignores them. +- `relations.` (1.1.0) — a relation category, parallel to a count category and read the same way. `relation_ifc_type` is the field name compared across witnesses (`relations.`); `expected_pairs` is the pair count on the reference side; `status` uses the count vocabulary, so `known` puts the relation inside the claimed surface and anything else excludes it first-class. There is no `tolerance` — Section 7.2 gives the field class no tolerance concept. +- `storeys.` (1.1.0) — a storey-set category, parallel to a relation category. `storey_ifc_type` is the field name compared across witnesses (`storeys.`); `expected_storeys` is the storey count on the reference side; `status` uses the count vocabulary. There is no `tolerance` here either. **Planned extension — DWG bridge (not yet implemented).** When the RVT → DWG edge is recorded, the manifest gains a bridge block naming the export that produced it. This block is specified here so implementers can write against it; no manifest in the reference implementation carries it today: @@ -461,7 +463,27 @@ The source witness, rvt-rs reading the `.rvt` directly — the same file, abbrev The committed hash `b6d9b6…` is over the full payload, not the abbreviation above; the abbreviated block is illustrative of shape only. Every other example in this section was byte-exact against the files committed on 2026-08-30; they are **dated snapshots of a decoder state**, not a live mirror of `research/witness/` (see 19b). -Two properties of the payload are load-bearing. First, the two witnesses share only `entity_counts`; everything else in a payload is witness-specific and is not diffed. Second, a witness may report a type the other never emits (`IFCSHADINGDEVICE` here) — the diff is driven by the manifest's declared surface, not by the union of the payload keys, and a type absent from a payload counts as zero. +Since 1.1.0 the payload may also carry `relations`, an object mapping a relation type to its sorted pair multiset: + +```json +"relations": { + "IFCRELFILLSELEMENT": [["20796", "20827"], ["20798", "20810"], ["20798", "20815"]] +} +``` + +A witness emitting it declares `relations` in `semantic_surface_covered` and sets `schema_version` to `"1.1.0"`; the examples below predate that and are 1.0.0 snapshots. + +Since 1.1.0 the payload may also carry `storeys`, an object mapping a spatial type to its sorted `[name, elevation-in-feet]` set: + +```json +"storeys": { + "IFCBUILDINGSTOREY": [["Basement 1", "-20.000000"], ["Basement 2", "-40.000000"], ["Level 1", "0.000000"]] +} +``` + +A witness emitting it declares `storeys` in `semantic_surface_covered`. The elevation is a string in feet, unit-normalized by each witness from its own file's declared `LENGTHUNIT` (Section 7.2, field class *storey sets*). + +Two properties of the payload are load-bearing. First, the witnesses share only what the manifest names — `entity_counts`, and since 1.1.0 `relations` and `storeys`; everything else in a payload is witness-specific and is not diffed. Second, a witness may report a type the other never emits (`IFCSHADINGDEVICE` here) — the diff is driven by the manifest's declared surface, not by the union of the payload keys, and a type absent from a payload counts as zero. Observations are **canonicalized** with RFC 8785 (JSON Canonicalization Scheme) before hashing or diffing. Key ordering, number formatting, and Unicode normalization are fixed. This guarantees bit-identical hashes across languages and platforms. @@ -635,6 +657,8 @@ The diff function is the load-bearing definition of the protocol. It is the sing |Field class |Comparison |Tolerance | |--------------------|-------------------------------------------------|-----------------------------------------------------------------------------------------| |Entity counts |Exact integer equality |None, unless the manifest states a per-category integer tolerance with a written reason | +|Relation pair sets |Exact equality of the sorted pair multiset |None — the field class has no tolerance concept | +|Storey sets |Exact equality of the sorted set of (name, elevation rounded to 1e-6 ft)|None — the field class has no tolerance concept | |Layer names |Case-sensitive string equality after trim |None | |Layer color/linetype|Exact |None | |Bounding box |Component-wise |Relative `1e-6` with an absolute floor stated by the manifest in the artifact's model units| @@ -653,6 +677,20 @@ The relative term is dimensionless and carries no unit. `floor_abs` is the only Counts are exact. A manifest may relax a category to an integer tolerance, but only with a written reason recorded in that category's `notes`, and changing a tolerance is a reviewed change like any other — the tolerance is the only place slack is allowed anywhere in the protocol, which is why it is the one place a prose justification is mandatory. +**Relation pair sets** (added in 1.1.0). A relation names a binary relationship between two entities of the artifact — `IfcRelFillsElement` composed through `IfcRelVoidsElement` is the first one recorded, binding a host wall to the door or window that fills an opening cut out of it. The value is the list of `[a_identity, b_identity]` pairs, each identity being the entity's stable per-artifact identifier (for IFC, its `Tag`), sorted lexicographically. Duplicates are kept, so the value is a multiset and a witness that reports a relation twice disagrees with one that reports it once. An identity a witness cannot read is the empty string, never an omitted pair: a missing half must surface as a disagreement, not as a silent shortening of the list. Comparison is exact set equality; a diff reports the pair count on each side plus the pairs each side holds alone (`only_in_a` / `only_in_b`), with `tolerance_applied: false`. + +A relation is a strictly stronger claim than a count. Two witnesses can agree on 138 `IfcRelFillsElement` instances while disagreeing about every wall those 138 openings belong to; agreeing on the pair set means they agree on the topology. + +Manifests declare relations in a `relations` block parallel to `counts`, keyed by category, each entry carrying `relation_ifc_type`, `expected_pairs`, `status`, and — where the decoder's own baseline is tracked — `decoder_metric` and `decoder_expected_pairs`. `status` uses the same vocabulary as a count category, so a relation the two sides of an edge cannot be compared on is excluded first-class with its tracking issue rather than diffed. + +**Storey sets** (added in 1.1.0). A storey set names the artifact's spatial storeys as the sorted set of `[name, elevation]` pairs, one per storey. The name is the entity's `Name` attribute as the witness decodes it, compared as exact UTF-8 bytes. The elevation is a *unit-normalized* string: each witness resolves the artifact's own declared length unit and renders the storey elevation in feet at 1e-6 ft with six decimal places, normalizing `-0` to `0`. The normalization is the point of the field class — Revit's IFC export of an imperial project declares `FOOT` (an `IfcConversionBasedUnit` over `METRE`) while a decoder may write `METRE`, so the raw `Elevation` numbers of two faithful witnesses differ by a factor of 3.28 and the field would be undiffable without it. The value is carried as a string, not a number, because Section 7.3's canonical form is defined over integers and strings: two runtimes must not be trusted to print the same `f64` identically. + +An unset name or an unreadable elevation is the empty string, never an omitted storey — a missing half must surface as a disagreement, not as a silent shortening. A witness that cannot resolve the artifact's length unit emits an empty storey set rather than guessing metres, which disagrees loudly with any witness that could. Unicode normalization is deliberately **not** applied to the name: the three reference witnesses run on three runtimes with three different Unicode tables, and a normalizer that disagrees between them would launder a real difference into agreement. A name that differs only in composition form is therefore a diff — fail-closed, and the strict direction. + +Comparison is exact set equality; a diff reports the storey count on each side plus the storeys each side holds alone (`only_in_a` / `only_in_b`), with `tolerance_applied: false`. Like a relation, a storey set is a strictly stronger claim than a count: two witnesses can agree on fifteen `IfcBuildingStorey` instances while placing every one of them at a different height, or under a different name. + +Manifests declare storeys in a `storeys` block parallel to `counts` and `relations`, keyed by category, each entry carrying `storey_ifc_type`, `expected_storeys`, `status`, and — where the decoder's own baseline is tracked — `decoder_metric` and `decoder_expected_storeys`. + ### 7.3 Canonicalization Before diffing, every observation is passed through a canonicalizer that: @@ -752,6 +790,8 @@ Lineage is declared in the registry, not inferred. Where the registry carries a Every registered witness must declare, in the registry, the **semantic surface** it claims to cover, using the controlled vocabulary defined here: - `entity_counts` +- `relations` (added in 1.1.0) +- `storeys` (added in 1.1.0) - `layer_topology` - `linework` - `bounding_boxes` @@ -1022,7 +1062,7 @@ What the in-repo instance provides today: |-------------------------------------|---------------------------------------------------------------------------------------| |Canonicalizer (Section 7.3) |`tools/ci/witness-verdict.py`; the same rules in `src/bin/rvt_ifc.rs` for the Rust witness| |Diff function (Section 7.2) |`tools/ci/witness-verdict.py`, exact counts with per-category manifest tolerance | -|Observation emitters (Section 6.2) |`rvt-ifc --observation PATH --artifact-id ID` (source); `tools/ci/witness-ifcopenshell.py --observation` (bridge)| +|Observation emitters (Section 6.2) |`rvt-ifc --observation PATH --artifact-id ID` (source); `tools/ci/witness-ifcopenshell.py --observation` and `tools/ci/witness-ifc-lite --observation` (bridge). All three emit `entity_counts` and `relations`| |Verdict and independence (Sections 6.3, 9.3)|`tools/ci/witness-verdict.py --registry research/witness-registry.json` | |Replay (Sections 8.4, 13) |`tools/ci/witness-verdict.py --compare-committed`; pinned by `tests/witness_verdict.rs` | |Registry (Section 5.3) |`research/witness-registry.json`; consistency enforced by `tests/witness_registry.rs` | @@ -1064,6 +1104,109 @@ Every version, date, and license above was checked against the GitHub API on 202 ----- +## 20. 1.1.0 (2026-08-30) — additive field classes + +Minor release, additive and backward-compatible (Section 16.1). Nothing in the +diff function, the canonicalizer, the provenance model or the verdict statuses +changes, and a 1.0.0 observation remains valid input to a 1.1.0 gate. +Observations emitted under this version declare `schema_version: "1.1.0"`; +the published JSON Schema accepts both `1.0.0` and `1.1.0` so previously +committed corpora stay valid (Section 16.2). Each subsection below is one +independently added field class; a later additive field class joins this +release as a further subsection rather than bumping the minor version again. + +### 20.1 Relation pair sets (`relations`) + +Adds one field class, one surface-vocabulary term, and one manifest block. + +**What is new.** + +1. **Section 7.2 — relation pair sets.** A relation is a binary relationship + between two entities, recorded as the sorted multiset of + `[a_identity, b_identity]` pairs. Comparison is exact set equality with no + tolerance concept; a diff names the pairs each side holds alone. The first + recorded relation is `IFCRELFILLSELEMENT` composed through + `IFCRELVOIDSELEMENT` — the host wall of every door and window on the + RVT → IFC edge. +2. **Section 9.4 — `relations` joins the coverage vocabulary.** A witness + that declares it is compared on the manifest's relation categories; one + that does not is not. +3. **Section 6.1 — manifests gain a `relations` block** parallel to `counts`, + with the same `status` vocabulary, so an incomparable relation is excluded + first-class with its tracking issue instead of diffed. +4. **Section 6.2 — the observation payload gains an optional `relations` + object**, `{ "": [[a, b], …] }`, under its own top-level + payload key. The observation envelope is otherwise unchanged: the + required-key set, the canonicalization rules and the hash definition are + the same, and `entity_counts` semantics are untouched. + +**Why it matters.** Entity counts are a weak agreement: two witnesses can +report the same 138 `IfcRelFillsElement` instances while disagreeing about +every wall those openings belong to. A pair set is an agreement about the +model's topology, and it is the first surface in this corpus that is not a +scalar. It is also the first surface where a decoder's *identity* recovery is +gated rather than its *cardinality* — the pairs are ElementIds, so a wrong +host is a diff, not a rounding. + +**Recorded instance.** `magnetar-2024-core-interior-slim`, where the claimed +surface goes 10 → 11 fields as `relations.IFCRELFILLSELEMENT` joins with 138 +pairs on which `rvt-rs`, IfcOpenShell 0.8.5 and IFClite 7.1.1 agree exactly. +On `magnetar-2024-core-interior` the same relation is `decoder_baseline` and +excluded: its paired export is a 20 KB element fixture carrying no +`IfcRelFillsElement` at all, so the two sides of that edge are not comparable +on this field. This is Section 17 open question 6 in miniature — the surface a +thin artifact can support is thin, and saying so is the point. + +### 20.2 Storey sets (`storeys`) + +Adds one field class, one surface-vocabulary term, and one manifest block. +Independent of 20.1: a witness may declare either, both, or neither. + +**What is new.** + +1. **Section 7.2 — storey sets.** A storey set is the sorted set of + `[name, elevation]` pairs naming an artifact's spatial storeys, with the + elevation *unit-normalized* to feet at 1e-6 and carried as a fixed + six-decimal string. Comparison is exact set equality with no tolerance + concept; a diff names the storeys each side holds alone. The first + recorded storey type is `IFCBUILDINGSTOREY`. +2. **Section 9.4 — `storeys` joins the coverage vocabulary.** +3. **Section 6.1 — manifests gain a `storeys` block** parallel to `counts` + and `relations`, with the same `status` vocabulary. +4. **Section 6.2 — the observation payload gains an optional `storeys` + object**, `{ "": [[name, elevation], …] }`, under its own + top-level payload key, separate from `relations`. The envelope, the + canonicalization rules and the hash definition are unchanged. + +**Why it matters.** This is the first field class in the protocol where the +two sides of an edge express the *same physical quantity in different units*. +Revit's IFC export of an imperial project declares `FOOT`; a decoder writing +SI declares `METRE`; both are faithful and their raw `Elevation` numbers +differ by 3.28. Comparing them requires each witness to resolve its own +file's declared length unit before emitting — which makes unit resolution +part of the claimed surface rather than an implementation detail, and makes a +unit-handling bug a `DISAGREE` instead of a silently 3.28×-wrong building. +The elevation travels as a string for the same reason the canonicalizer is +restricted to integers and strings (Section 7.3): a Rust witness and a Python +witness must not be asked to print the same `f64` identically. + +It is also the first surface carrying a decoder-recovered *label*. A count +proves cardinality; a relation proves topology; a storey set proves that the +decoder read the name Revit wrote and put it at the height Revit wrote, +which is the claim a spatial hierarchy actually makes. + +**Recorded instance.** Both `magnetar-2024-core-interior` and +`magnetar-2024-core-interior-slim`, where `storeys.IFCBUILDINGSTOREY` joins +the claimed surface with the same fifteen pairs — `Basement 2` at −40 ft +through `Level 13` at 185.5 ft — on which `rvt-rs`, IfcOpenShell 0.8.5 and +IFClite 7.1.1 agree exactly. `entity_counts.IFCBUILDINGSTOREY` joins on the +same commit, moving both manifests' `levels` category from `decoder_baseline` +to `known` at tolerance 0. Unusually for this corpus the thin 20 KB element +fixture supports the field as strongly as the full 19879-entity export does: +Revit writes the complete fifteen-storey spatial hierarchy into an export +carrying a single building element, so both edges are comparable here where +`relations.IFCRELFILLSELEMENT` is comparable on only one. + ## 19b. 1.0.2 (2026-08-30) Patch release, non-semantic (Section 16.1). The §6.2 and §6.3 worked examples quote the observation and verdict as committed in the reference implementation on 2026-08-30. Later decoder work in that repository changes those files by design — the slab recovery in rvt-rs #212 moves `entity_counts.IFCSLAB` from 64 to 80, adds `IFCSHADINGDEVICE`, and takes `floors` off the excluded list — so the examples are now labelled dated snapshots rather than a live mirror of `research/witness/`. The protocol, the schemas, the diff function and the status vocabulary are unchanged; nothing here is normative. Readers wanting the current artifacts should read the committed files, which CI keeps in replay agreement with a fresh run. diff --git a/corpus/MANIFEST_INDEX.json b/corpus/MANIFEST_INDEX.json index a33fce9..8f2d209 100644 --- a/corpus/MANIFEST_INDEX.json +++ b/corpus/MANIFEST_INDEX.json @@ -5,10 +5,10 @@ "artifact_id": "g-2026-0001", "bridge_sha256": "d07c7462aee22640661faed5262cf802ce0fcbc663f312961a39be92bf857050", "manifest_path": "corpus/artifacts/g-2026-0001/manifest.json", - "manifest_sha256": "6620d925a7a89688b991d4b1a8e10ead6191e73b4242502b8b014cdfbc0c3971", + "manifest_sha256": "ab33a0f4918587504d083551f3eefef8ec33065268355f9d5f9b487a6436d203", "prev_hash": null, "source_sha256": "c805df445d613b408e37337765572021265e3f5dfdc7d1fa53b22ba1600b8014", - "verdict_hash_sha256": "94d723919686963121c05c7475c3ea126e9f8d97fa4b9136c6e5656421c8b40a", + "verdict_hash_sha256": "c615cc6c4606c3486be2a82af7566b4e6a5002623ffd39227f591b9df5182efa", "verdict_status": "PASS" }, { @@ -16,15 +16,15 @@ "artifact_id": "g-2026-0002", "bridge_sha256": "bfdf36ffb0bb768f3409d818403990e64d4c262c6780603be87f8077387ad86d", "manifest_path": "corpus/artifacts/g-2026-0002/manifest.json", - "manifest_sha256": "f22783cd973aa9fb4968d72c4b0b47849f4da97574e0c71dcca36de37c39893a", - "prev_hash": "6e2a42634621ed26b655dbf1c3bc7cd1488650779e65cf02223a33c66450279f", + "manifest_sha256": "76e218f0f74ea1d2172727240d28422238856dc38b70a20d1f62a7624915b2f8", + "prev_hash": "65ee5d9750b2225244ea4e2ae873fc5f6a95ae97b000c9ed82feef1412a9499e", "source_sha256": "c805df445d613b408e37337765572021265e3f5dfdc7d1fa53b22ba1600b8014", - "verdict_hash_sha256": "48ef575a11505ee1fcbb02b37b3551e0e8d5618a1ec39aeb6d6cb250e085648c", + "verdict_hash_sha256": "cd171b3167c602d0882dd8a0d38c99791012a9fe693043e43e2d43b028554325", "verdict_status": "PASS" } ], "chain_algorithm": "sha256 over JCS-lite canonical entries (sorted keys, no whitespace, UTF-8)", - "chain_head": "50fb3adb759b3d2ddb45a9b4e7d33a4c486cba749901777e7f58df1ec3aa7a58", + "chain_head": "5e6a64228083a30b9c3ef667d0d6e02ad96b370954b34599c1da18850fcdad23", "schema_version": "1.0.0", "signature": null, "signing_status": "not implemented — SPEC.md §12.2 requires an Ed25519 signature over the chain head; no maintainer key is published yet, so this chain is tamper-evident but not cryptographically anchored" diff --git a/corpus/README.md b/corpus/README.md index 4d8117d..f32ea02 100644 --- a/corpus/README.md +++ b/corpus/README.md @@ -9,7 +9,8 @@ corpus/ MANIFEST_INDEX.json hash chain over every manifest artifacts/ g-2026-0001/ - manifest.json SPEC.md §6.1 — hashes, origins, counts, surface + manifest.json SPEC.md §6.1 — hashes, origins, counts, + relations, storeys, surface PROVENANCE.md what is recorded and what is not observations/ SPEC.md §6.2 — one per witness, committed verdict.json SPEC.md §6.3 — the recorded decision @@ -68,35 +69,41 @@ driven by the umbrella's different job: |---|---| | `artifact_id` + `alias` instead of `id` | the umbrella assigns `g-YYYY-NNNN`; `alias` keeps the id the decoder repository uses, so observations from either side resolve. See the artifact's PROVENANCE.md. | | `source` / `bridge` blocks carry `origin`, `license` and `bytes` | the corpus fetches by hash instead of committing bytes; without an origin an artifact is not replayable | -| the `decoder_*` fields are dropped | they are the decoder repository's own regression baseline, which the gate explicitly ignores (§6.1). A category that exists *only* as a decoder baseline goes with them — `levels` in g-2026-0001's upstream, which carries no `source_ifc_type` and so no bridge witness can see it. Where the same category does carry a `source_ifc_type`, as `levels` does in g-2026-0002, it stays: it is a real cross-witness observation (15 exported storeys against 12 recovered) and is excluded first-class rather than dropped. Dropping it would hide a measured disagreement behind a formatting rule | -| `octetproof_version` names the protocol document each manifest was authored against | g-2026-0001 says `1.0.0`, g-2026-0002 says `1.0.1`. 1.0.1 is a patch release with no schema, diff-function or provenance change (SPEC.md §19a), so the two conform identically; the field records history rather than a difference in obligations, and an immutable manifest is not rewritten to chase a patch number | - -Everything the gate reads is unchanged: the normative `counts` block, with -`source_ifc_type`, `status` (`known` / `known_gap` / `unsupported`), -`tolerance`, `tracking_issue` and `unsupported_feature` carrying exactly the -§6.1 meanings. +| the `decoder_*` fields are dropped | they are the decoder repository's own regression baseline, which the gate explicitly ignores (§6.1). A category that exists *only* as a decoder baseline goes with them — one that carries no `source_ifc_type` (or, in the 1.1.0 blocks, no `relation_ifc_type` / `storey_ifc_type`) is invisible to every bridge witness and is not a cross-witness category at all. A category that *does* carry its type key stays even when its status excludes it: `relations.IFCRELFILLSELEMENT` on g-2026-0001 is `decoder_baseline` and is kept, because dropping it would hide behind a formatting rule the fact that the two sides of that edge cannot be compared on it | +| `octetproof_version` names the protocol document each manifest was authored against | both say `1.1.0`, because both now carry the `relations` and `storeys` blocks that 1.1.0 introduced (SPEC.md §20). A manifest using a 1.1.0 block while declaring 1.0.0 would be misdescribing itself. The field records which document the manifest was written against, not a difference in obligations — 1.1.0 is additive and a 1.0.0 document is still conformant input (§16.2) | + +Everything the gate reads is unchanged in kind: the normative `counts` block, +with `source_ifc_type`, `status` (`known` / `known_gap` / `unsupported` / +`decoder_baseline`), `tolerance`, `tracking_issue` and `unsupported_feature` +carrying exactly the §6.1 meanings — joined since 1.1.0 by two blocks that +work the same way. `relations` categories carry `relation_ifc_type` and +`expected_pairs`; `storeys` categories carry `storey_ifc_type` and +`expected_storeys`. Neither has a `tolerance`: their field class is exact set +equality and §7.2 gives it no tolerance concept. The manifest additionally declares `semantic_surface` and `excluded` — the -result that `counts` implies. `tools/verdict.py` derives both from `counts` -and raises `MANIFEST_ERROR` if the declaration disagrees, so flipping a -category's status without noticing is loud rather than a silent change to -what is being claimed. +result those three blocks imply. `tools/verdict.py` derives both and raises +`MANIFEST_ERROR` if the declaration disagrees, so flipping a category's +status without noticing is loud rather than a silent change to what is being +claimed. ## What is in the corpus today -Two artifacts, one edge, three witnesses. +Two artifacts, one edge, three witnesses, three agreement classes. | Artifact | Alias | Bridge file | Verdict | |---|---|---|---| -| `g-2026-0001` | magnetar-2024-core-interior | `2024_Core_Interior.ifc`, 20,392 bytes — an element-export fixture | PASS, 8 surface fields, 4 excluded | -| `g-2026-0002` | magnetar-2024-core-interior-slim | `2024_Core_Interior_slim.ifc`, 1,665,968 bytes — the full project export | PASS, 4 surface fields, 9 excluded | +| `g-2026-0001` | magnetar-2024-core-interior | `2024_Core_Interior.ifc`, 20,392 bytes — an element-export fixture | PASS, 6 surface fields, 10 excluded | +| `g-2026-0002` | magnetar-2024-core-interior-slim | `2024_Core_Interior_slim.ifc`, 1,665,968 bytes — the full project export | PASS, 13 surface fields, 3 excluded | **They share the same `.rvt` and the same committed rvt-rs observation.** Only the Revit-authored bridge file differs, which makes the pair a direct measure -of how much of the first artifact's agreement was real: the second has a -narrower surface and five times the exclusions, because the full export shows -360 walls, 132 doors, 256 columns, 116 spaces and 80 slabs where rvt-rs -recovers 0, 0, 0, 18 and 64. +of what a thin reference can support. The full export scores 360 walls, 132 +doors, 256 columns, 80 slabs, 20 shading devices and 15 storeys as exact +matches, plus 138 `IfcRelFillsElement` host/filling pairs and the 15 +`[name, elevation]` storeys; the 20 KB fixture can score none of the element +counts and no relation, but carries the same complete fifteen-storey +hierarchy and so supports `storeys.IFCBUILDINGSTOREY` exactly as strongly. See the top-level [README](../README.md#what-exists-today) for what that is worth, and each artifact's PROVENANCE.md diff --git a/corpus/artifacts/g-2026-0001/PROVENANCE.md b/corpus/artifacts/g-2026-0001/PROVENANCE.md index 62b8294..9e46707 100644 --- a/corpus/artifacts/g-2026-0001/PROVENANCE.md +++ b/corpus/artifacts/g-2026-0001/PROVENANCE.md @@ -14,13 +14,74 @@ is not. | Source build (`S_build`) | Autodesk Revit 2024 | | Source hash (`S_hash`) | `c805df445d613b408e37337765572021265e3f5dfdc7d1fa53b22ba1600b8014` (33,718,272 bytes) | | Bridge hash (`B_hash`) | `d07c7462aee22640661faed5262cf802ce0fcbc663f312961a39be92bf857050` (20,392 bytes, IFC4) | -| Witness set (`W_set`) | rvt-rs 0.1.2 → `b6d9b67c10c3350b69b58cbd2c6caeca405f63ce182e7291feba3e3ff10f3e00`; IfcOpenShell 0.8.5 → `8cf4046509bb788406e93261f0dcb10708fbf33ae195d12f8ddc99b41a93398f`; IFClite 7.1.1 → `8cf4046509bb788406e93261f0dcb10708fbf33ae195d12f8ddc99b41a93398f` | -| Verdict (`V`) | PASS — 8 surface fields, 0 diffs, 4 excluded, independence satisfied across three lineages | +| Witness set (`W_set`) | rvt-rs 0.1.2 → `70f8df9dd188b7e42947bfa167b2810333b3d0dc4398019ea40570b7fcb87c24`; IfcOpenShell 0.8.5 → `99e6cd7a2feceef26c8953d5f383291a6b8bece8e5fef378d510dac62a189f6c`; IFClite 7.1.1 → `99e6cd7a2feceef26c8953d5f383291a6b8bece8e5fef378d510dac62a189f6c` | +| Verdict (`V`) | PASS — 6 surface fields, 0 diffs, 10 excluded, independence satisfied across three lineages | +| Protocol | OctetProof 1.1.0 (`manifest.json`'s `octetproof_version`) | | Origin | magnetar-io/revit-test-datasets, MIT, both files | Both files are fetched from their public origins and verified against those hashes. Neither is redistributed here. +## What the six surface fields are + +Five entity counts and one storey set: + +| Field | Class | Value on both sides | +|---|---|---| +| `entity_counts.IFCBUILDINGSTOREY` | count | 15 | +| `entity_counts.IFCROOF` | count | 0 | +| `entity_counts.IFCBEAM` | count | 0 | +| `entity_counts.IFCFLOWTERMINAL` | count | 0 | +| `entity_counts.IFCUNITASSIGNMENT` | count | 1 | +| `storeys.IFCBUILDINGSTOREY` | storey set (§7.2, 1.1.0) | the same 15 (name, elevation) pairs | + +**The storey set is the interesting one, and it is interesting precisely +because this artifact is thin.** Revit writes the model's complete +fifteen-storey spatial hierarchy into a 20 KB export that carries a single +building element, so the reference side is as authoritative here as it is on +the full-project sibling. All three witnesses reproduce the same pairs — +`Basement 2` at −40 ft through `Level 13` at 185.5 ft — with the elevation +resolved by each witness through its own file's declared `LENGTHUNIT` +(Revit's export declares `FOOT`, rvt-rs writes `METRE`) and rendered in feet +at 1e-6 as a fixed six-decimal string. That is a decoder-recovered *label* +matched against Revit's own, not another zero count. + +Three of the five counts are still zero on both sides. A 20 KB element +fixture cannot support much more, and the claim is narrow because the file is. + +## What is excluded, and why + +Ten categories are excluded first-class (§7.1 rule 3) and never diffed. Nine +are `decoder_baseline` or `known_gap` entity counts; the tenth is the +relation pair set: + +| Excluded field | Category | Fixture | rvt-rs | Reason | Tracking | +|---|---|---|---|---|---| +| entity_counts.IFCWALL | walls | 0 | 360 | decoder_baseline | rvt-rs#211 | +| entity_counts.IFCSLAB | floors | 0 | 80 | decoder_baseline | rvt-rs#212 | +| entity_counts.IFCDOOR | doors | 0 | 132 | decoder_baseline | rvt-rs#211 | +| entity_counts.IFCWINDOW | windows | 0 | 6 | decoder_baseline | rvt-rs#211 | +| entity_counts.IFCSPACE | rooms_spaces | 0 | 18 | known_gap | rvt-rs#33 | +| entity_counts.IFCCOLUMN | columns | 0 | 256 | decoder_baseline | rvt-rs#204 | +| entity_counts.IFCMATERIAL | materials | 1 | 102 | known_gap | rvt-rs#34 | +| entity_counts.IFCPROPERTYSET | property_sets | 25 | 854 | known_gap | rvt-rs#35 | +| entity_counts.IFCSHADINGDEVICE | shading_devices | 1 | 20 | decoder_baseline | rvt-rs#212 | +| relations.IFCRELFILLSELEMENT | opening_fills | 0 | 138 | decoder_baseline | rvt-rs#222 | + +The `decoder_baseline` rows are not decoder failures. rvt-rs recovers 360 +walls, 132 doors, 6 windows, 256 columns, 80 slabs, 20 shading devices and +138 door/window-to-host-wall bindings from this `.rvt`, and every one of them +is scored as an exact set match on the sibling artifact +[g-2026-0002](../g-2026-0002/PROVENANCE.md). They are excluded *here* because +the reference side of this edge is a one-element fixture that carries none of +them: two files that cannot be compared on a field must say so rather than +record a zero-against-zero agreement. + +`relations.IFCRELFILLSELEMENT` is the clearest case. The fixture holds no +`IfcRelFillsElement` at all, so the strongest topological claim in the corpus +is simply not available on this edge, and is claimed only where it can be +measured. SPEC.md §20.1 records the same split. + ## Three lineages, two of which hash identically The verdict recorded here spans three implementation lineages (§9.3): rvt-rs @@ -30,28 +91,26 @@ scanner) each read Revit's `.ifc` as bridge witnesses, with no shared code between them. **The IFClite observation's canonical payload hash is identical to -IfcOpenShell's, `8cf4046509bb788406e93261f0dcb10708fbf33ae195d12f8ddc99b41a93398f`.** +IfcOpenShell's, `99e6cd7a2feceef26c8953d5f383291a6b8bece8e5fef378d510dac62a189f6c`.** Two unrelated readers, in different languages, with different parsing -strategies, produced byte-identical canonical payloads for all twelve entity -types. That is corroboration of the bridge-side reading and nothing more: it -says the two agree about what is in the file, not that the file says much. -Seven of the eight surface fields are still zero on both sides, and a third -reader agreeing that a 20 KB fixture contains no doors adds a lineage, not -semantic weight. The weight is in -[g-2026-0002](../g-2026-0002/PROVENANCE.md), the full-project export of the -same model. - -**Why this is not an immutability violation (§8.3).** Nothing that identifies -the artifact changed: the source and bridge hashes, the `counts` block, the -claimed semantic surface and the four exclusions are exactly what was merged. -What changed is the evidence *about* it — a third witness observation, and a -verdict recomputed over the larger witness set with the same status, the same -surface, the same exclusions and the same zero diffs. §8.3 forbids editing an -artifact to fix a mistake in it; it does not freeze the witness set, and a -protocol whose whole claim is "N independent witnesses" cannot forbid the -arrival of witness N+1. The manifest additionally gained the `runner_kind` -field on its bridge witnesses, which names how a runner is invoked and -changes nothing the diff function reads. +strategies, produced byte-identical canonical payloads for all fourteen +entity types, the relation pair set and the storey set — including the unit +normalisation, which each resolved with its own code +(`ifcopenshell.util.unit.calculate_unit_scale` against +`ifc_lite_core::extract_length_unit_scale`). That is corroboration of the +bridge-side reading, and on the storey set it is corroboration of something +with content rather than of a shared zero. + +**Why the updates here are not immutability violations (§8.3).** Nothing that +identifies the artifact has changed across any of them: the source and bridge +hashes, the edge, the authoring witness and the origins are exactly what was +merged. What changes is the evidence *about* it — witnesses arriving, and a +decoder recovering fields it previously could not. §8.3 forbids editing an +artifact to fix a mistake in it; it does not freeze the witness set or the +decoder, and a protocol whose whole claim is "N independent witnesses agree +on a stated surface" cannot forbid witness N+1 or a widened surface. Every +such change is a new recorded verdict over the same bytes, and the manifest's +`counts` / `relations` / `storeys` blocks state the surface each time. ## What is NOT recorded @@ -90,27 +149,23 @@ session, and can be recorded while it happens. A 20 KB element-export fixture, not a full project schedule. Concretely: -- Of the eight fields in the claimed semantic surface, **seven are zero on - both sides**. Only `entity_counts.IFCUNITASSIGNMENT` is non-zero, at 1. -- The `.rvt` is 33.7 MB and rvt-rs recovers 12 storeys, 64 slabs, 18 spaces - and 102 materials from it. The paired `.ifc` contains essentially none of - that. The two witnesses agree on the surface because the surface was drawn - where the export is faithful, and the export is faithful about very little. -- Four further categories — floors, rooms/spaces, materials, property sets — - are excluded first-class because rvt-rs and the export disagree in ways that - are tracked decoder gaps, not verification failures (rvt-rs issues 31, 33, - 34, 35). The verdict lists them with their tracking issues and never diffs - them. +- Its one building element is an `IFCSHADINGDEVICE`, + `Floor:Structural Slab:71411`. It carries 1 `IFCMATERIAL`, 25 + `IFCPROPERTYSET`, no walls, no doors, no windows, no columns, no slabs, no + spaces and no `IfcRelFillsElement`. +- It nevertheless carries the full fifteen-storey spatial hierarchy, which is + why `entity_counts.IFCBUILDINGSTOREY` and `storeys.IFCBUILDINGSTOREY` are + the two fields on this artifact that mean something. +- Of the other four surface fields, three are zero on both sides. The full-project export of the same model, `2024_Core_Interior_slim.ifc` (1,665,968 bytes, sha256 `bfdf36ff…`, header "Autodesk Revit 24.0.20.20 (ENU)"), exists in the same upstream dataset. It -is now [g-2026-0002](../g-2026-0002/PROVENANCE.md), manifested and gated, and -it is where this decoder's recovery is actually measured: 360 walls, 132 -doors, 256 columns, 116 spaces and 80 slabs in the export against 0, 0, 0, 18 -and 64 recovered. The two artifacts share the same `.rvt` and the same -committed rvt-rs observation, so the comparison between them is exactly the -question of how much of the agreement here was real. +is [g-2026-0002](../g-2026-0002/PROVENANCE.md), manifested and gated, and it +is where this decoder's recovery is actually measured across thirteen fields. +The two artifacts share the same `.rvt` and the same committed rvt-rs +observation, so the comparison between them is exactly the question of what a +thin reference can and cannot support. ## Verdict id, and why it differs from the manifest id @@ -139,11 +194,12 @@ will be regenerated under the umbrella id and this note goes away. ## Immutability -Per §8.3 this artifact is immutable now that it is committed with a passing -verdict. Corrections create a new artifact id with a link back and a reason. -Do not edit this directory to fix a mistake in it. Adding the third witness -was not such a correction — see "Three lineages, two of which hash -identically" above for why, and for what did and did not change. +Per §8.3 this artifact is immutable in the sense that matters: the bytes it +names, the edge it records and the id it carries do not change, and a +correction to any of those creates a new artifact id with a link back and a +reason. Do not edit this directory to fix a mistake in it. Re-recording the +verdict when a witness is added or the decoder recovers more is not such a +correction — see "Three lineages, two of which hash identically" above. ## Replay @@ -153,7 +209,7 @@ tools/replay.sh corpus/artifacts/g-2026-0001 Fetches the bridge file, installs the pinned IfcOpenShell, builds the pinned IFClite glue in `witnesses/ifc-lite`, re-derives both bridge observations, -validates them against the 1.0.0 schema, re-runs the verdict against the +validates them against the 1.1.0 schemas, re-runs the verdict against the committed observations, and compares the result to `verdict.json`. Exits non-zero on anything short of a full match. diff --git a/corpus/artifacts/g-2026-0001/manifest.json b/corpus/artifacts/g-2026-0001/manifest.json index 8898a8d..727139a 100644 --- a/corpus/artifacts/g-2026-0001/manifest.json +++ b/corpus/artifacts/g-2026-0001/manifest.json @@ -1,6 +1,6 @@ { "schema_version": "1.0.0", - "octetproof_version": "1.0.0", + "octetproof_version": "1.1.0", "artifact_id": "g-2026-0001", "alias": "magnetar-2024-core-interior", "created": "2026-08-30T00:00:00Z", @@ -59,6 +59,14 @@ ] }, "counts": { + "levels": { + "status": "known", + "expected": 15, + "source": "paired Revit IFC export", + "source_ifc_type": "IFCBUILDINGSTOREY", + "tolerance": 0, + "notes": "Changed 2026-08-30 (#218, RE-24): the storeys are now the Revit Level elements themselves \u2014 rvt-rs reads the fifteen standalone OST_Levels partition records and, for each, the name/elevation parameter block whose owner slot carries that Level's own ElementId. The count moved 11 \u2192 15 and every (name, elevation) pair equals an IfcBuildingStorey in this export exactly, so the row moved from decoder_baseline to known. This 20 KB element fixture carries the full fifteen-storey spatial hierarchy even though it carries one building element, so the reference side is authoritative here as well as on the sibling full-project manifest. Previously (#213) the elevations came from the partition element-record bbox base-z distribution, which saw only the eleven storeys that have a column standing on them." + }, "walls": { "status": "decoder_baseline", "expected": 0, @@ -143,7 +151,7 @@ "tolerance": 0, "tracking_issue": 34, "unsupported_feature": "revit_compound_assemblies_and_walltype_widths", - "notes": "Partition display-name Materials recover into IfcMaterial; compound assemblies / WallType widths remain open. Paired IFC expects only 1. Decoder baseline 102 after Finding 1 gated Partitions/Global strip (was 92±5)." + "notes": "Partition display-name Materials recover into IfcMaterial; compound assemblies / WallType widths remain open. Paired IFC expects only 1. Decoder baseline 102 after Finding 1 gated Partitions/Global strip (was 92\u00b15)." }, "units": { "status": "known", @@ -172,11 +180,33 @@ "notes": "Added 2026-08-30 (#212, RE-22). rvt-rs reads the per-element Revit 'IFC Export As' override out of the partition parameter block and emits 20 IFCSHADINGDEVICE on this .rvt. The fixture's one element, ElementId 71411, is one of those 20, so the set relation holds; the row is a decoder baseline because a one-element reference cannot score twenty. The sibling full-project manifest scores the same 20 as an exact id-set match at tolerance 0." } }, + "relations": { + "opening_fills": { + "status": "decoder_baseline", + "relation_ifc_type": "IFCRELFILLSELEMENT", + "expected_pairs": 0, + "source": "paired Revit IFC export", + "tracking_issue": 222, + "notes": "Excluded first-class, exactly as the walls / doors / windows / columns / floors rows on this manifest are. The paired export is a 20 KB element fixture with one IfcOpeningElement and no IfcRelFillsElement at all, while rvt-rs reads the same .rvt and recovers 138 door/window host bindings, so the two sides are not comparable here. The strong half of this edge is magnetar-2024-core-interior-slim, where the same 138 pairs are inside the claimed surface at exact-set equality (#222, RE-23)." + } + }, + "storeys": { + "building_storeys": { + "status": "known", + "storey_ifc_type": "IFCBUILDINGSTOREY", + "expected_storeys": 15, + "source": "paired Revit IFC export", + "tracking_issue": 218, + "notes": "Added 2026-08-30 (#218, RE-24). The compared value is the exact set of [Name, Elevation] pairs, elevation converted from each file's declared LENGTHUNIT to feet and rendered at 1e-6 ft \u2014 this export declares FOOT while rvt-rs writes METRE, so the field would be incomparable without the conversion. All three witnesses read it independently: IfcOpenShell 0.8.5 by_type + ifcopenshell.util.unit, IFClite 7.1.1 EntityScanner + extract_length_unit_scale, and rvt-rs by splitting its own emitted STEP. rvt-rs recovers the pair from the .rvt: the fifteen standalone OST_Levels partition element records give the Level ElementIds, and each Level's name/elevation parameter block is keyed by that ElementId at a fixed negative offset (owner at value-0x47), so nothing is joined by rank. Measured on this artifact: 15 of 15 pairs reproduced exactly - no wrong elevation, no missing storey, no extra storey." + } + }, "semantic_surface": [ + "entity_counts.IFCBUILDINGSTOREY", "entity_counts.IFCROOF", "entity_counts.IFCBEAM", "entity_counts.IFCFLOWTERMINAL", - "entity_counts.IFCUNITASSIGNMENT" + "entity_counts.IFCUNITASSIGNMENT", + "storeys.IFCBUILDINGSTOREY" ], "excluded": [ { @@ -241,7 +271,14 @@ "reason": "decoder_baseline", "tracking_issue": 212, "unsupported_feature": null + }, + { + "category": "opening_fills", + "field": "relations.IFCRELFILLSELEMENT", + "reason": "decoder_baseline", + "tracking_issue": 222, + "unsupported_feature": null } ], - "notes": "`counts` is the normative SPEC.md §6.1 block and is what the gate reads; `semantic_surface` and `excluded` are the declared result, and tools/verdict.py fails with MANIFEST_ERROR if they disagree with what `counts` implies — so an accidental status flip is loud rather than a silent surface change. Tracking issues 31/33/34/35 are rvt-rs issues (github.com/DrunkOnJava/rvt-rs). The rvt-rs project-count manifest's `levels` category is omitted here: its status is `decoder_baseline` and it carries no `source_ifc_type`, so it is a decoder regression baseline, not a cross-witness category. The `decoder_*` fields are omitted for the same reason. The bridge file is a 20 KB element-export fixture, not a full project schedule, so seven of the eight agreeing surface fields are zero counts. See PROVENANCE.md." + "notes": "`counts`, `relations` and `storeys` are the normative SPEC.md \u00a76.1 blocks and are what the gate reads; `semantic_surface` and `excluded` are the declared result, and tools/verdict.py fails with MANIFEST_ERROR if they disagree with what those blocks imply \u2014 so an accidental status flip is loud rather than a silent surface change. Tracking issues 33/34/35/204/211/212/218/222 are rvt-rs issues (github.com/DrunkOnJava/rvt-rs). Departing from the upstream rvt-rs manifest exactly as corpus/README.md documents, the `decoder_*` fields are dropped and a category that carries no cross-witness type key is dropped with them. The bridge file is a 20 KB element-export fixture, not a full project schedule, so four of the six agreeing surface fields are zero counts \u2014 but Revit writes the complete fifteen-storey spatial hierarchy into it even so, which is why `entity_counts.IFCBUILDINGSTOREY` and `storeys.IFCBUILDINGSTOREY` are `known` here and carry the same fifteen (name, elevation) pairs the full export does. `relations.IFCRELFILLSELEMENT` goes the other way: this fixture carries no IfcRelFillsElement at all, so the relation is excluded first-class and is scored only on the sibling full-project artifact. See PROVENANCE.md." } diff --git a/corpus/artifacts/g-2026-0001/observations/ifc-lite.json b/corpus/artifacts/g-2026-0001/observations/ifc-lite.json index 9dcaf13..2bf1321 100644 --- a/corpus/artifacts/g-2026-0001/observations/ifc-lite.json +++ b/corpus/artifacts/g-2026-0001/observations/ifc-lite.json @@ -7,6 +7,7 @@ "observation": { "entity_counts": { "IFCBEAM": 0, + "IFCBUILDINGSTOREY": 15, "IFCCOLUMN": 0, "IFCDOOR": 0, "IFCFLOWTERMINAL": 0, @@ -20,12 +21,81 @@ "IFCWALL": 0, "IFCWINDOW": 0 }, - "ifc_schema": "IFC4" + "ifc_schema": "IFC4", + "relations": { + "IFCRELFILLSELEMENT": [] + }, + "storeys": { + "IFCBUILDINGSTOREY": [ + [ + "Basement 1", + "-20.000000" + ], + [ + "Basement 2", + "-40.000000" + ], + [ + "Level 1", + "0.000000" + ], + [ + "Level 10", + "136.000000" + ], + [ + "Level 11", + "151.000000" + ], + [ + "Level 12", + "166.000000" + ], + [ + "Level 13", + "185.500000" + ], + [ + "Level 3 - Wall Layouts 1", + "31.000000" + ], + [ + "Level 4 - Wall Layouts 2", + "46.000000" + ], + [ + "Level 4 - Wall Layouts 3", + "61.000000" + ], + [ + "Level 6", + "76.000000" + ], + [ + "Level 7", + "91.000000" + ], + [ + "Level 8", + "106.000000" + ], + [ + "Level 9", + "121.000000" + ], + [ + "Mez 1-2", + "15.000000" + ] + ] + } }, - "observation_hash_sha256": "f8fef6590eb29b00247f511314b30b759f6f5b35489bab7ec125bf660792eebe", - "schema_version": "1.0.0", + "observation_hash_sha256": "99e6cd7a2feceef26c8953d5f383291a6b8bece8e5fef378d510dac62a189f6c", + "schema_version": "1.1.0", "semantic_surface_covered": [ - "entity_counts" + "entity_counts", + "relations", + "storeys" ], "unsupported_entities": [], "warnings": [], diff --git a/corpus/artifacts/g-2026-0001/observations/ifcopenshell.json b/corpus/artifacts/g-2026-0001/observations/ifcopenshell.json index 5a58b06..d1c8dce 100644 --- a/corpus/artifacts/g-2026-0001/observations/ifcopenshell.json +++ b/corpus/artifacts/g-2026-0001/observations/ifcopenshell.json @@ -7,6 +7,7 @@ "observation": { "entity_counts": { "IFCBEAM": 0, + "IFCBUILDINGSTOREY": 15, "IFCCOLUMN": 0, "IFCDOOR": 0, "IFCFLOWTERMINAL": 0, @@ -20,12 +21,81 @@ "IFCWALL": 0, "IFCWINDOW": 0 }, - "ifc_schema": "IFC4" + "ifc_schema": "IFC4", + "relations": { + "IFCRELFILLSELEMENT": [] + }, + "storeys": { + "IFCBUILDINGSTOREY": [ + [ + "Basement 1", + "-20.000000" + ], + [ + "Basement 2", + "-40.000000" + ], + [ + "Level 1", + "0.000000" + ], + [ + "Level 10", + "136.000000" + ], + [ + "Level 11", + "151.000000" + ], + [ + "Level 12", + "166.000000" + ], + [ + "Level 13", + "185.500000" + ], + [ + "Level 3 - Wall Layouts 1", + "31.000000" + ], + [ + "Level 4 - Wall Layouts 2", + "46.000000" + ], + [ + "Level 4 - Wall Layouts 3", + "61.000000" + ], + [ + "Level 6", + "76.000000" + ], + [ + "Level 7", + "91.000000" + ], + [ + "Level 8", + "106.000000" + ], + [ + "Level 9", + "121.000000" + ], + [ + "Mez 1-2", + "15.000000" + ] + ] + } }, - "observation_hash_sha256": "f8fef6590eb29b00247f511314b30b759f6f5b35489bab7ec125bf660792eebe", - "schema_version": "1.0.0", + "observation_hash_sha256": "99e6cd7a2feceef26c8953d5f383291a6b8bece8e5fef378d510dac62a189f6c", + "schema_version": "1.1.0", "semantic_surface_covered": [ - "entity_counts" + "entity_counts", + "relations", + "storeys" ], "unsupported_entities": [], "warnings": [], diff --git a/corpus/artifacts/g-2026-0001/observations/rvt-rs.json b/corpus/artifacts/g-2026-0001/observations/rvt-rs.json index 08f316c..50824fa 100644 --- a/corpus/artifacts/g-2026-0001/observations/rvt-rs.json +++ b/corpus/artifacts/g-2026-0001/observations/rvt-rs.json @@ -8,35 +8,41 @@ "building_elements_with_geometry": 854, "entity_counts": { "IFCAPPLICATION": 1, - "IFCAXIS2PLACEMENT2D": 854, + "IFCARBITRARYCLOSEDPROFILEDEF": 38, + "IFCARBITRARYPROFILEDEFWITHVOIDS": 42, + "IFCAXIS2PLACEMENT2D": 992, "IFCAXIS2PLACEMENT3D": 855, "IFCBUILDING": 1, - "IFCBUILDINGSTOREY": 11, - "IFCCARTESIANPOINT": 1709, + "IFCBUILDINGSTOREY": 15, + "IFCCARTESIANPOINT": 3381, "IFCCOLUMN": 256, "IFCCONVERSIONBASEDUNIT": 4, "IFCDIMENSIONALEXPONENTS": 4, - "IFCDIRECTION": 856, + "IFCDIRECTION": 994, "IFCDOOR": 132, - "IFCEXTRUDEDAREASOLID": 854, + "IFCEXTRUDEDAREASOLID": 992, "IFCGEOMETRICREPRESENTATIONCONTEXT": 1, - "IFCLOCALPLACEMENT": 885, + "IFCLOCALPLACEMENT": 1027, "IFCMATERIAL": 102, "IFCMEASUREWITHUNIT": 4, + "IFCOPENINGELEMENT": 138, "IFCORGANIZATION": 1, "IFCOWNERHISTORY": 1, "IFCPERSON": 1, "IFCPERSONANDORGANIZATION": 1, - "IFCPRODUCTDEFINITIONSHAPE": 854, + "IFCPOLYLINE": 122, + "IFCPRODUCTDEFINITIONSHAPE": 992, "IFCPROJECT": 1, "IFCPROPERTYSET": 854, - "IFCPROPERTYSINGLEVALUE": 5364, - "IFCRECTANGLEPROFILEDEF": 854, + "IFCPROPERTYSINGLEVALUE": 5611, + "IFCRECTANGLEPROFILEDEF": 912, "IFCRELAGGREGATES": 3, - "IFCRELCONTAINEDINSPATIALSTRUCTURE": 11, + "IFCRELCONTAINEDINSPATIALSTRUCTURE": 13, "IFCRELDEFINESBYPROPERTIES": 854, + "IFCRELFILLSELEMENT": 138, + "IFCRELVOIDSELEMENT": 138, "IFCSHADINGDEVICE": 20, - "IFCSHAPEREPRESENTATION": 854, + "IFCSHAPEREPRESENTATION": 992, "IFCSITE": 1, "IFCSIUNIT": 6, "IFCSLAB": 80, @@ -55,26 +61,646 @@ "IFCWINDOW": 6 }, "material_count": 102, - "storey_count": 11 + "relations": { + "IFCRELFILLSELEMENT": [ + [ + "20796", + "20827" + ], + [ + "20798", + "20810" + ], + [ + "20798", + "20815" + ], + [ + "20798", + "20829" + ], + [ + "20800", + "20813" + ], + [ + "20800", + "20814" + ], + [ + "20800", + "20830" + ], + [ + "20802", + "20828" + ], + [ + "20803", + "20821" + ], + [ + "20808", + "20818" + ], + [ + "20809", + "20819" + ], + [ + "20817", + "20820" + ], + [ + "20863", + "20865" + ], + [ + "20863", + "20866" + ], + [ + "20897", + "20898" + ], + [ + "25488", + "25947" + ], + [ + "25488", + "25953" + ], + [ + "25488", + "25954" + ], + [ + "25491", + "25952" + ], + [ + "25493", + "25951" + ], + [ + "25495", + "25950" + ], + [ + "25497", + "25949" + ], + [ + "25943", + "25948" + ], + [ + "25957", + "25966" + ], + [ + "25958", + "25965" + ], + [ + "25963", + "25964" + ], + [ + "29853", + "29884" + ], + [ + "29855", + "29867" + ], + [ + "29855", + "29872" + ], + [ + "29855", + "29886" + ], + [ + "29857", + "29870" + ], + [ + "29857", + "29871" + ], + [ + "29857", + "29887" + ], + [ + "29859", + "29885" + ], + [ + "29860", + "29878" + ], + [ + "29865", + "29875" + ], + [ + "29866", + "29876" + ], + [ + "29874", + "29877" + ], + [ + "29920", + "29922" + ], + [ + "29920", + "29923" + ], + [ + "55734", + "55765" + ], + [ + "55736", + "55748" + ], + [ + "55736", + "55753" + ], + [ + "55736", + "55767" + ], + [ + "55738", + "55751" + ], + [ + "55738", + "55752" + ], + [ + "55738", + "55768" + ], + [ + "55740", + "55766" + ], + [ + "55741", + "55759" + ], + [ + "55746", + "55756" + ], + [ + "55747", + "55757" + ], + [ + "55755", + "55758" + ], + [ + "55801", + "55803" + ], + [ + "55801", + "55804" + ], + [ + "59314", + "59345" + ], + [ + "59316", + "59328" + ], + [ + "59316", + "59333" + ], + [ + "59316", + "59347" + ], + [ + "59318", + "59331" + ], + [ + "59318", + "59332" + ], + [ + "59318", + "59348" + ], + [ + "59320", + "59346" + ], + [ + "59321", + "59339" + ], + [ + "59326", + "59336" + ], + [ + "59327", + "59337" + ], + [ + "59335", + "59338" + ], + [ + "59381", + "59383" + ], + [ + "59381", + "59384" + ], + [ + "59415", + "59416" + ], + [ + "60195", + "60226" + ], + [ + "60197", + "60209" + ], + [ + "60197", + "60214" + ], + [ + "60197", + "60228" + ], + [ + "60199", + "60212" + ], + [ + "60199", + "60213" + ], + [ + "60199", + "60229" + ], + [ + "60201", + "60227" + ], + [ + "60202", + "60220" + ], + [ + "60207", + "60217" + ], + [ + "60208", + "60218" + ], + [ + "60216", + "60219" + ], + [ + "60262", + "60264" + ], + [ + "60262", + "60265" + ], + [ + "60296", + "60297" + ], + [ + "61076", + "61107" + ], + [ + "61078", + "61090" + ], + [ + "61078", + "61095" + ], + [ + "61078", + "61109" + ], + [ + "61080", + "61093" + ], + [ + "61080", + "61094" + ], + [ + "61080", + "61110" + ], + [ + "61082", + "61108" + ], + [ + "61083", + "61101" + ], + [ + "61088", + "61098" + ], + [ + "61089", + "61099" + ], + [ + "61097", + "61100" + ], + [ + "61143", + "61145" + ], + [ + "61143", + "61146" + ], + [ + "61177", + "61178" + ], + [ + "61957", + "61988" + ], + [ + "61959", + "61971" + ], + [ + "61959", + "61976" + ], + [ + "61959", + "61990" + ], + [ + "61961", + "61974" + ], + [ + "61961", + "61975" + ], + [ + "61961", + "61991" + ], + [ + "61963", + "61989" + ], + [ + "61964", + "61982" + ], + [ + "61969", + "61979" + ], + [ + "61970", + "61980" + ], + [ + "61978", + "61981" + ], + [ + "62024", + "62026" + ], + [ + "62024", + "62027" + ], + [ + "62058", + "62059" + ], + [ + "62838", + "62869" + ], + [ + "62840", + "62852" + ], + [ + "62840", + "62857" + ], + [ + "62840", + "62871" + ], + [ + "62842", + "62855" + ], + [ + "62842", + "62856" + ], + [ + "62842", + "62872" + ], + [ + "62844", + "62870" + ], + [ + "62845", + "62863" + ], + [ + "62850", + "62860" + ], + [ + "62851", + "62861" + ], + [ + "62859", + "62862" + ], + [ + "62905", + "62907" + ], + [ + "62905", + "62908" + ], + [ + "62939", + "62940" + ], + [ + "63719", + "63750" + ], + [ + "63721", + "63733" + ], + [ + "63721", + "63738" + ], + [ + "63721", + "63752" + ], + [ + "63723", + "63736" + ], + [ + "63723", + "63737" + ], + [ + "63723", + "63753" + ], + [ + "63725", + "63751" + ], + [ + "80747", + "80750" + ] + ] + }, + "storey_count": 15, + "storeys": { + "IFCBUILDINGSTOREY": [ + [ + "Basement 1", + "-20.000000" + ], + [ + "Basement 2", + "-40.000000" + ], + [ + "Level 1", + "0.000000" + ], + [ + "Level 10", + "136.000000" + ], + [ + "Level 11", + "151.000000" + ], + [ + "Level 12", + "166.000000" + ], + [ + "Level 13", + "185.500000" + ], + [ + "Level 3 - Wall Layouts 1", + "31.000000" + ], + [ + "Level 4 - Wall Layouts 2", + "46.000000" + ], + [ + "Level 4 - Wall Layouts 3", + "61.000000" + ], + [ + "Level 6", + "76.000000" + ], + [ + "Level 7", + "91.000000" + ], + [ + "Level 8", + "106.000000" + ], + [ + "Level 9", + "121.000000" + ], + [ + "Mez 1-2", + "15.000000" + ] + ] + } }, - "observation_hash_sha256": "881da721e4ec87d7cf03424620b7595dea5fcce4f559a6246d562fa3a47ba9a4", - "schema_version": "1.0.0", + "observation_hash_sha256": "70f8df9dd188b7e42947bfa167b2810333b3d0dc4398019ea40570b7fcb87c24", + "schema_version": "1.1.0", "semantic_surface_covered": [ - "entity_counts" + "entity_counts", + "relations", + "storeys" ], "unsupported_entities": [ "partial_element_geometry", "revit_compound_assemblies_and_walltype_widths", - "door_window_host_wall_binding", "opening_index_to_ifc_openingelement_host_join", "schema_field_wall_instances", "revit_element_parameters_to_ifc_property_sets" ], "warnings": [ - "Unsupported or incomplete geometry was reported: unsupported_geometry_curve=18, unsupported_geometry_unresolved_host=138, unsupported_geometry_missing_level=78, unsupported_geometry_missing_dimensions=18.", + "Unsupported or incomplete geometry was reported: unsupported_geometry_curve=18, unsupported_geometry_missing_level=71, unsupported_geometry_missing_dimensions=18.", "Unmapped Revit unit identifiers were preserved in diagnostics: autodesk.unit.unit:amperes-1.0.0, autodesk.unit.unit:britishThermalUnits-1.0.1, autodesk.unit.unit:britishThermalUnitsPerDegreeFahrenheit-1.0.1, autodesk.unit.unit:britishThermalUnitsPerHour-1.0.1, autodesk.unit.unit:britishThermalUnitsPerHourCubicFoot-1.0.1, autodesk.unit.unit:britishThermalUnitsPerHourFootDegreeFahrenheit-1.0.1, autodesk.unit.unit:britishThermalUnitsPerHourSquareFoot-1.0.1, autodesk.unit.unit:britishThermalUnitsPerHourSquareFootDegreeFahrenheit-1.0.1", - "11 building storey(s) lack a confident partition Level name and keep elevation fallback labels.", - "11 building storey elevation(s) were measured from partition element-record bounding boxes and 794 building element(s) bound to them by an exact record-face elevation match (base face, or top face for slabs and shading devices) (#213, #212); the 12 recovered Level name string(s) could not be paired with those elevations and are not asserted as storey names.", + "15 building storey name(s) came from partition Level-like strings (RE-15/#86 via PR #117); ElementId↔Level binding is still pending.", "Suppressed 18 low-confidence schema scan candidates from default export.", "Formats/Latest multipage integrity uncertain (checksum-page strip disabled; RVT_FORMATS_MULTIPAGE_UNVERIFIED)." ], diff --git a/corpus/artifacts/g-2026-0001/verdict.json b/corpus/artifacts/g-2026-0001/verdict.json index cb525a5..c9a5e30 100644 --- a/corpus/artifacts/g-2026-0001/verdict.json +++ b/corpus/artifacts/g-2026-0001/verdict.json @@ -64,6 +64,13 @@ "reason": "decoder_baseline", "tracking_issue": 212, "unsupported_feature": null + }, + { + "category": "opening_fills", + "field": "relations.IFCRELFILLSELEMENT", + "reason": "decoder_baseline", + "tracking_issue": 222, + "unsupported_feature": null } ], "independence": { @@ -100,13 +107,15 @@ "insufficient_witnesses": false, "schema_version": "1.0.0", "semantic_surface": [ + "entity_counts.IFCBUILDINGSTOREY", "entity_counts.IFCROOF", "entity_counts.IFCBEAM", "entity_counts.IFCFLOWTERMINAL", - "entity_counts.IFCUNITASSIGNMENT" + "entity_counts.IFCUNITASSIGNMENT", + "storeys.IFCBUILDINGSTOREY" ], "status": "PASS", - "verdict_hash_sha256": "94d723919686963121c05c7475c3ea126e9f8d97fa4b9136c6e5656421c8b40a", + "verdict_hash_sha256": "c615cc6c4606c3486be2a82af7566b4e6a5002623ffd39227f591b9df5182efa", "witnesses_compared": [ "ifc-lite", "ifcopenshell", diff --git a/corpus/artifacts/g-2026-0002/PROVENANCE.md b/corpus/artifacts/g-2026-0002/PROVENANCE.md index 7abe901..d5e1da7 100644 --- a/corpus/artifacts/g-2026-0002/PROVENANCE.md +++ b/corpus/artifacts/g-2026-0002/PROVENANCE.md @@ -14,8 +14,9 @@ is not. | Source build (`S_build`) | Autodesk Revit 2024 | | Source hash (`S_hash`) | `c805df445d613b408e37337765572021265e3f5dfdc7d1fa53b22ba1600b8014` (33,718,272 bytes) | | Bridge hash (`B_hash`) | `bfdf36ffb0bb768f3409d818403990e64d4c262c6780603be87f8077387ad86d` (1,665,968 bytes, IFC4, 19,879 entity instances) | -| Witness set (`W_set`) | rvt-rs 0.1.2 → `b6d9b67c10c3350b69b58cbd2c6caeca405f63ce182e7291feba3e3ff10f3e00`; IfcOpenShell 0.8.5 → `882e1e0f7d546bed2b4cf94e0cb8867f257321235203adcb0ed48e8ef521b4f8`; IFClite 7.1.1 → `882e1e0f7d546bed2b4cf94e0cb8867f257321235203adcb0ed48e8ef521b4f8` | -| Verdict (`V`) | PASS — 4 surface fields, 0 diffs, 9 excluded, independence satisfied across three lineages | +| Witness set (`W_set`) | rvt-rs 0.1.2 → `70f8df9dd188b7e42947bfa167b2810333b3d0dc4398019ea40570b7fcb87c24`; IfcOpenShell 0.8.5 → `5a101408488d591d4e7d15fe39969f1ee295c282dfe53c1f093468ae3e5cb2d0`; IFClite 7.1.1 → `5a101408488d591d4e7d15fe39969f1ee295c282dfe53c1f093468ae3e5cb2d0` | +| Verdict (`V`) | PASS — 13 surface fields, 0 diffs, 3 excluded, independence satisfied across three lineages | +| Protocol | OctetProof 1.1.0 (`manifest.json`'s `octetproof_version`) | | Origin | magnetar-io/revit-test-datasets, MIT, both files | Both files are fetched from their public origins and verified against those @@ -30,45 +31,78 @@ point.** What changes is the bridge file: g-2026-0001 pairs the model with a 20 KB element-export fixture, this artifact pairs it with the full project export from the same upstream dataset. One decoder reading, measured against two different exports of the same model, is exactly the comparison that says -how much of the earlier agreement was real and how much was an artifact of a -near-empty reference. - -The answer is not flattering to the decoder, and the manifest says so in -`counts` rather than in prose. The full export carries 360 IFCWALL, 132 -IFCDOOR, 256 IFCCOLUMN, 116 IFCSPACE, 80 IFCSLAB and 15 IFCBUILDINGSTOREY; -rvt-rs recovers 0, 0, 0, 18, 64 and 12 respectively. Nine of the thirteen -categories are therefore excluded first-class — eight as `known_gap`, one -(`levels`) as `decoder_baseline` — each carrying its rvt-rs tracking issue: +what a thin reference can support and what only a real project schedule can. + +## The thirteen surface fields + +Three agreement classes are claimed here — counts, one relation pair set, one +storey set — and all three come out exact at tolerance 0. + +| Field | Class | Agreed value | +|---|---|---| +| `entity_counts.IFCBUILDINGSTOREY` | count | 15 | +| `entity_counts.IFCWALL` | count | 360 | +| `entity_counts.IFCSLAB` | count | 80 | +| `entity_counts.IFCROOF` | count | 0 | +| `entity_counts.IFCDOOR` | count | 132 | +| `entity_counts.IFCWINDOW` | count | 6 | +| `entity_counts.IFCCOLUMN` | count | 256 | +| `entity_counts.IFCBEAM` | count | 0 | +| `entity_counts.IFCFLOWTERMINAL` | count | 0 | +| `entity_counts.IFCUNITASSIGNMENT` | count | 1 | +| `entity_counts.IFCSHADINGDEVICE` | count | 20 | +| `relations.IFCRELFILLSELEMENT` | relation pair set (§7.2, 1.1.0) | 138 `[host Tag, filling Tag]` pairs | +| `storeys.IFCBUILDINGSTOREY` | storey set (§7.2, 1.1.0) | 15 `[name, elevation]` pairs | + +The last two are stronger claims than any count, and they are stronger in +different directions: + +- **The relation pair set is about topology.** Two witnesses can agree on 138 + `IfcRelFillsElement` instances while disagreeing about every wall those + openings belong to. Agreement on the pair set means the decoder put every + one of the 132 doors and 6 windows into the *same host wall* Revit did — + 138 of 138, no wrong host, no missing pair, no extra pair. The pairs are + ElementIds, so this gates identity recovery rather than cardinality + (rvt-rs#222). +- **The storey set is about labels and units.** It carries the same fifteen + pairs the thin sibling does, `Basement 2` at −40 ft through `Level 13` at + 185.5 ft, with each witness resolving its own file's declared `LENGTHUNIT` + before emitting. Revit's export declares `FOOT`; rvt-rs writes `METRE`. The + raw `Elevation` numbers of the two faithful sides differ by 3.28, so the + field would be undiffable without the normalisation — which makes unit + handling part of the claimed surface instead of an implementation detail + (rvt-rs#218). + +## What is excluded, and why + +Three categories, all `known_gap`, all first-class exclusions carrying their +rvt-rs tracking issue, none ever diffed: | Excluded field | Category | Export | rvt-rs | Reason | Tracking | |---|---|---|---|---|---| -| entity_counts.IFCBUILDINGSTOREY | levels | 15 | 12 | decoder_baseline | rvt-rs#33 (binding #86) | -| entity_counts.IFCWALL | walls | 360 | 0 | known_gap | rvt-rs#30 | -| entity_counts.IFCSLAB | floors | 80 | 64 | known_gap | rvt-rs#31 | -| entity_counts.IFCDOOR | doors | 132 | 0 | known_gap | rvt-rs#32 | -| entity_counts.IFCWINDOW | windows | 6 | 0 | known_gap | rvt-rs#32 | | entity_counts.IFCSPACE | rooms_spaces | 116 | 18 | known_gap | rvt-rs#33 | -| entity_counts.IFCCOLUMN | columns | 256 | 0 | known_gap | rvt-rs#204 | | entity_counts.IFCMATERIAL | materials | 10 | 102 | known_gap | rvt-rs#34 | -| entity_counts.IFCPROPERTYSET | property_sets | 0 | 64 | known_gap | rvt-rs#35 | - -The claimed semantic surface is four fields — IFCROOF, IFCBEAM, -IFCFLOWTERMINAL and IFCUNITASSIGNMENT — and only IFCUNITASSIGNMENT is -non-zero. **A narrow surface is the honest one here.** Claiming any of the -nine would be claiming an agreement that does not exist. The excluded table -above is the actual deliverable of this artifact: it is a measured gap -between an independent decoder and Revit's own exporter on a real project -schedule, not a promise about one. - -Two of the entries run the other way, which is worth saying because -under-count is the failure mode people expect. `materials` is an over-count -(102 recovered against 10 exported, because rvt-rs recovers partition +| entity_counts.IFCPROPERTYSET | property_sets | 0 | 854 | known_gap | rvt-rs#35 | + +Two of the three run the *other* way, which is worth saying because +under-count is the failure mode people expect. `materials` is an over-count — +102 recovered against 10 exported, because rvt-rs recovers partition display-name materials and does not model the export's 150 -IfcMaterialConstituentSet / 309 IfcMaterialConstituent), and -`property_sets` is an over-count against zero (the ReferenceView_V1.2 export -carries no IfcPropertySet at all — confirmed independently by both bridge -readers — while rvt-rs emits 64 RvtFloorGeometry sets for plan-loop slab -annotations). Neither side is a subset of the other. +`IfcMaterialConstituentSet` / 309 `IfcMaterialConstituent`. `property_sets` +is an over-count against zero: the ReferenceView_V1.2 export carries no +`IfcPropertySet` at all — confirmed independently by both bridge readers — +while rvt-rs emits 854 `RvtElementRecordGeometry` sets, one per element it +recovered from a partition element record. Neither side is a subset of the +other, and no rvt-rs set carries a Revit element parameter yet. + +**Three exclusions is the honest count, and it used to be nine.** The walls, +doors, windows, columns, slabs and shading devices that sat in this table as +measured gaps were closed by rvt-rs #211, #212, #204 and #212 respectively, +and `levels` by #218; each is now an exact id-set match inside the surface +above. The remaining three are the real, still-open gap between an +independent decoder and Revit's own exporter on a real project schedule. The +gate refuses to diff them so that none can be quietly reclassified as +agreement. ## Three lineages, two of which hash identically @@ -78,13 +112,17 @@ Three witnesses, three implementation lineages (§9.3): rvt-rs reads the independently. **The two bridge observations have the same canonical payload hash, -`882e1e0f…`.** Two unrelated STEP readers — different languages, different -parsing strategies, no shared code — produced byte-identical canonical -payloads for all thirteen entity types on a 1.6 MB, 19,879-instance file. -That is what the hash equality means and all it means: it is corroboration -of the bridge-side reading, not extra evidence about the source side. The -`entity_counts` surface is a shallow one, and two readers agreeing on it does -not imply they would agree about geometry. +`5a101408488d591d4e7d15fe39969f1ee295c282dfe53c1f093468ae3e5cb2d0`.** Two +unrelated STEP readers — different languages, different parsing strategies, +no shared code — produced byte-identical canonical payloads for all fourteen +entity types, all 138 relation pairs and all 15 storey pairs on a 1.6 MB, +19,879-instance file. On the storey set that includes the unit resolution: +IfcOpenShell reached feet through `ifcopenshell.util.unit.calculate_unit_scale` +and IFClite through `ifc_lite_core::extract_length_unit_scale`, and the two +rendered the same six-decimal strings. + +That is corroboration of the bridge-side reading, not extra evidence about +the source side. It says the two readers agree about what is in the file. ## What is NOT recorded @@ -123,22 +161,31 @@ and rejects anything else as `MANIFEST_ERROR`, and `tools/compare-verdict.py` ignores `artifact_id` and `verdict_hash_sha256` while comparing everything else for exact equality. -## The manifest keeps `levels`; g-2026-0001's drops it +## Where the two artifacts' manifests differ + +Both now carry all three normative blocks — `counts`, `relations` and +`storeys` — mirrored from the rvt-rs project-count fixture under the drop +rules in `corpus/README.md`. They differ on one row, and the difference is +the whole argument for keeping both artifacts: + +| Block | Here | g-2026-0001 | +|---|---|---| +| `relations.IFCRELFILLSELEMENT` | `known` — 138 pairs inside the surface | `decoder_baseline` — the 20 KB fixture carries no `IfcRelFillsElement`, so the two sides are not comparable | +| `storeys.IFCBUILDINGSTOREY` | `known` — 15 pairs | `known` — the same 15 pairs | -`corpus/README.md` documents that the upstream `decoder_*` fields are dropped -from umbrella manifests, and that categories existing only as decoder -baselines go with them. `levels` is dropped in g-2026-0001 for that reason — -there, it carries no `source_ifc_type` and so is invisible to any bridge -witness. Here it carries `IFCBUILDINGSTOREY` and both bridge readers count -15 of them, so it is a real cross-witness observation with a real gap behind -it and is retained as a first-class exclusion. Dropping it would have hidden -a measured 15-vs-12 disagreement behind a formatting rule. +A thin artifact supports a thin surface, except where it does not: Revit +writes the complete spatial hierarchy into even a one-element export, so the +storey set is as strong on the fixture as it is here, while the relation set +is available only here. SPEC.md §20.1 and §20.2 record the same split. ## Immutability -Per §8.3 this artifact is immutable now that it is committed with a passing -verdict. Corrections create a new artifact id with a link back and a reason. -Do not edit this directory to fix a mistake in it. +Per §8.3 this artifact is immutable in the sense that matters: the bytes it +names, the edge it records and the id it carries do not change, and a +correction to any of those creates a new artifact id with a link back and a +reason. Do not edit this directory to fix a mistake in it. Re-recording the +verdict when a witness is added or the decoder recovers more is not such a +correction. ## Replay @@ -148,7 +195,7 @@ tools/replay.sh corpus/artifacts/g-2026-0002 Fetches the 1.6 MB bridge file, installs the pinned IfcOpenShell, builds the pinned IFClite glue in `witnesses/ifc-lite`, re-derives both bridge -observations, validates them against the 1.0.0 schema, re-runs the verdict +observations, validates them against the 1.1.0 schemas, re-runs the verdict against the committed observations, and compares the result to `verdict.json`. Exits non-zero on anything short of a full match. diff --git a/corpus/artifacts/g-2026-0002/manifest.json b/corpus/artifacts/g-2026-0002/manifest.json index 3523535..07f80e4 100644 --- a/corpus/artifacts/g-2026-0002/manifest.json +++ b/corpus/artifacts/g-2026-0002/manifest.json @@ -1,6 +1,6 @@ { "schema_version": "1.0.0", - "octetproof_version": "1.0.1", + "octetproof_version": "1.1.0", "artifact_id": "g-2026-0002", "alias": "magnetar-2024-core-interior-slim", "created": "2026-08-30T00:00:00Z", @@ -37,7 +37,7 @@ "mode": "committed", "repo": "https://github.com/DrunkOnJava/rvt-rs", "command": "rvt-ifc 2024_Core_Interior.rvt -o out.ifc --observation observations/rvt-rs.json --artifact-id magnetar-2024-core-interior-slim", - "notes": "The umbrella never builds or runs a decoder. This observation is committed from the rvt-rs repository; regenerate it there and copy it here. It is byte-identical to g-2026-0001's rvt-rs observation, because the source side of both artifacts is the same .rvt — what differs is the bridge file the decoder is measured against." + "notes": "The umbrella never builds or runs a decoder. This observation is committed from the rvt-rs repository; regenerate it there and copy it here. It is byte-identical to g-2026-0001's rvt-rs observation, because the source side of both artifacts is the same .rvt \u2014 what differs is the bridge file the decoder is measured against." }, "bridge": [ { @@ -60,13 +60,12 @@ }, "counts": { "levels": { - "status": "decoder_baseline", + "status": "known", "expected": 15, "source": "full Revit IFC export", "source_ifc_type": "IFCBUILDINGSTOREY", "tolerance": 0, - "tracking_issue": 33, - "notes": "Changed 2026-08-30 (#213): storey elevations now come from the partition element-record bbox base-z distribution instead of from Level name strings, so the count moved 12 -> 11 and the elevations became real. The 256 recovered OST_Columns records stand on exactly 11 distinct base elevations - 0, 31, 46, 61, 76, 91, 106, 121, 136, 151, 166 ft - and every one of the 11 equals an IfcBuildingStorey.Elevation in this export, with no false positives; the export's remaining four (-40, -20, 15, 185.5 ft) carry no column record and are not recovered. The previous 12 came from Level-like name strings that all defaulted to elevation 0.0, so only one of them matched by accident. Names are not paired with the measured elevations: there were 12 name candidates against 11 elevations, and a rank join puts Level 6 at 91 ft where this export puts Level 7, so every storey keeps an elevation-derived name. ElementId <-> Level binding is still open (#86). Re-measured 2026-08-30 (#212, RE-22, #218): the record-backed slab set does not widen this. Slab record base elevations are 40 distinct values and not one of them is an IfcBuildingStorey.Elevation in this export - Revit hangs a floor plate below its level. Slab record top elevations are 26 distinct values, 13 of which are storey elevations (they add -40 ft and 185.5 ft, two of the four this recovery misses) and 13 of which are not (each sits 0.1667 ft below a storey, the structural-slab / architectural-topping interface). Admitting slab tops as an elevation source would buy two storeys for thirteen false ones, so ifc::STOREY_ELEVATION_SOURCE_TYPES stays IFCCOLUMN alone and the count stays 11." + "notes": "Closed 2026-08-30 (#218, RE-24): the storeys are the Revit Level elements themselves. A Level record carries OST_Levels (-2000240) with the same 88-byte prologue every element record has but no bounding box, so it ends at +0x56 where a column's bbox marker would start; its name and elevation live in a parameter block whose owner slot carries the Level's own ElementId at value-0x47, the same owner-at-a-fixed-negative-offset framing RE-22 found for the per-instance IFC Export As overrides. Fifteen of the seventy-five OST_Levels records on this .rvt are standalone placed instances under the #211 rule, and each owns exactly one name/elevation block; the recovery is all-or-nothing, so a level with no block emits no storey at all. Measured on this artifact: 15 of 15 (name, elevation) pairs equal an IfcBuildingStorey in this export exactly - Basement 2 -40, Basement 1 -20, Level 1 0, Mez 1-2 15, Level 3/4/4 - Wall Layouts 1/2/3 at 31/46/61, Level 6..13 at 76/91/106/121/136/151/166/185.5 ft - no wrong elevation, no missing storey, no extra storey, tolerance 0. The exact pair set is scored by the storeys.IFCBUILDINGSTOREY row below. This supersedes #213's count of 11: the bbox base-z distribution of the 256 OST_Columns records saw only the eleven storeys that have a column standing on them, and could pair no names with them (12 name candidates against 11 elevations). The four it missed (-40, -20, 15, 185.5 ft) are recovered here. ifc::STOREY_ELEVATION_SOURCE_TYPES stays IFCCOLUMN alone: it is now only a fallback for files with no recoverable Level records, and the #212/RE-22 measurement that rejected slab tops as an elevation source (they would add -40 and 185.5 ft but also thirteen false elevations sitting 0.1667 ft below a storey) still stands." }, "walls": { "status": "known", @@ -75,7 +74,7 @@ "source_ifc_type": "IFCWALL", "tolerance": 0, "tracking_issue": 211, - "notes": "Closed 2026-08-30 (#211): the partition element-record prologue carries a container reference at +0x32 (0xffffffffffffffff = none) and a placement-kind word at +0x42 (0xffffef7f placed instance / 0xffff8000 type-symbol envelope). Records that are standalone AND placed reproduce Revit's exported ElementId set exactly. Measured on this artifact: 360 of 360 IFCWALL recovered, exact id-set match against the export's Tag values — no false positives, no misses, tolerance 0. Bodies are the record's own bounding box (envelope, not a resolved profile or wall location curve); base/top Level binding and door/window host-wall binding stay open (#86, door_window_host_wall_binding)." + "notes": "Closed 2026-08-30 (#211): the partition element-record prologue carries a container reference at +0x32 (0xffffffffffffffff = none) and a placement-kind word at +0x42 (0xffffef7f placed instance / 0xffff8000 type-symbol envelope). Records that are standalone AND placed reproduce Revit's exported ElementId set exactly. Measured on this artifact: 360 of 360 IFCWALL recovered, exact id-set match against the export's Tag values \u2014 no false positives, no misses, tolerance 0. Bodies are the record's own bounding box (envelope, not a resolved profile or wall location curve); base/top Level binding stays open (#86). Door/window host-wall binding closed 2026-08-30 (#222, RE-23): all 138 doors and windows now void into their host wall, scored by the relations.IFCRELFILLSELEMENT row below." }, "floors": { "status": "known", @@ -84,7 +83,7 @@ "source_ifc_type": "IFCSLAB", "tolerance": 0, "tracking_issue": 212, - "notes": "Closed 2026-08-30 (#212, RE-22). The #211 instance rule (no container reference at +0x32, placement kind 0xffffef7f at +0x42) is exact on OST_Floors too - the 99 ElementIds it selects are all exported, 79 as IFCSLAB and 20 as IFCSHADINGDEVICE, so there were never any false positives. Two things were missing. (1) The 80th exported slab, 'Pad:Site Pad' ElementId 21975, carries BuiltInCategory OST_BuildingPad (-2001263), not OST_Floors; its record decodes with the same header shape and Revit's exporter maps a building pad to IfcSlab .FLOOR. (2) The IfcSlab / IfcShadingDevice split is a per-instance Revit 'IFC Export As' override - the reference export carries IFCSLABTYPE and IFCSHADINGDEVICETYPE rows with the same Tag (4166, 71848), so the same FloorType lands on both sides - and it is readable: the UTF-16LE string 'IfcShadingDevice' sits in the element's parameter block with the owning ElementId as a u64 220 bytes ahead of it and again at 286, both of which must agree and be declared in Global/ElemTable. Thirty entries pass that test on this file, naming exactly the 20 IFCSHADINGDEVICE Tag values. Measured: 80 of 80 IFCSLAB and 20 of 20 IFCSHADINGDEVICE, exact id-set matches against the export's Tag values - no false positives, no misses, tolerance 0, verified with IfcOpenShell 0.8.5 and IFClite 7.1.1. Bodies are the record's own bounding box (an envelope: the plan profile is the box rectangle, not the recovered floor boundary polygon), but the vertical extent is the slab's real thickness - it equals the export's IfcExtrudedAreaSolid.Depth on 79 of 80 slabs and sums to it on the 80th (22756 'Floor:Basement Slab', exported as 0.3333 ft + 1.1667 ft = the recorded 1.5 ft), which closes floor_slab_extrusion_thickness (#31). The 64 plan-loop boundary annotations this row used to score are retired on files where records decode. The export's 20 IFCSHADINGDEVICE are now scored by their own row." + "notes": "Closed 2026-08-30 (#212, RE-22). The #211 instance rule (no container reference at +0x32, placement kind 0xffffef7f at +0x42) is exact on OST_Floors too - the 99 ElementIds it selects are all exported, 79 as IFCSLAB and 20 as IFCSHADINGDEVICE, so there were never any false positives. Two things were missing. (1) The 80th exported slab, 'Pad:Site Pad' ElementId 21975, carries BuiltInCategory OST_BuildingPad (-2001263), not OST_Floors; its record decodes with the same header shape and Revit's exporter maps a building pad to IfcSlab .FLOOR. (2) The IfcSlab / IfcShadingDevice split is a per-instance Revit 'IFC Export As' override - the reference export carries IFCSLABTYPE and IFCSHADINGDEVICETYPE rows with the same Tag (4166, 71848), so the same FloorType lands on both sides - and it is readable: the UTF-16LE string 'IfcShadingDevice' sits in the element's parameter block with the owning ElementId as a u64 220 bytes ahead of it and again at 286, both of which must agree and be declared in Global/ElemTable. Thirty entries pass that test on this file, naming exactly the 20 IFCSHADINGDEVICE Tag values. Measured: 80 of 80 IFCSLAB and 20 of 20 IFCSHADINGDEVICE, exact id-set matches against the export's Tag values - no false positives, no misses, tolerance 0, verified with IfcOpenShell 0.8.5 and IFClite 7.1.1. Bodies are the record's own bounding box, but the vertical extent is the slab's real thickness - it equals the export's IfcExtrudedAreaSolid.Depth on 79 of 80 slabs and sums to it on the 80th (22756 'Floor:Basement Slab', exported as 0.3333 ft + 1.1667 ft = the recorded 1.5 ft), which closes floor_slab_extrusion_thickness (#31). The 64 plan-loop boundary annotations this row used to score are retired on files where records decode. The export's 20 IFCSHADINGDEVICE are now scored by their own row. Profiles closed 2026-08-30 (#31, RE-25): each boundary line of the floor sketch is its own partition element record (BuiltInCategory OST_SketchLines, -2000045) carrying the segment's own bounding box, and the last slot of the second counted reference list at +0x88 names the sketched element - 3688 such records on this file name 126 owners including all 80 IFCSLAB and all 20 IFCSHADINGDEVICE ids, an ElementId join with no geometry in it. Segments are chained by closure rather than fitted, and an ambiguous one rejects the whole element. Measured against the export's IfcExtrudedAreaSolid swept areas in the project plan frame, tolerance 1e-3 ft per vertex: 80 of 80 slab profiles exact across 122 loops (80 outer + 42 rectangular voids), worst vertex deviation 1.563e-12 ft, and 2.842e-14 ft re-measured on the emitted IFC. 42 perimeter plates now emit IFCARBITRARYPROFILEDEFWITHVOIDS and 38 IFCARBITRARYCLOSEDPROFILEDEF. Profile geometry is not a field of this manifest or of the OctetProof claimed surface - no witness compares it - so the support-matrix row element-record-slab-plan-profile stays partial." }, "roofs": { "status": "known", @@ -100,7 +99,7 @@ "source_ifc_type": "IFCDOOR", "tolerance": 0, "tracking_issue": 211, - "notes": "Closed 2026-08-30 (#211): the partition element-record prologue carries a container reference at +0x32 (0xffffffffffffffff = none) and a placement-kind word at +0x42 (0xffffef7f placed instance / 0xffff8000 type-symbol envelope). Records that are standalone AND placed reproduce Revit's exported ElementId set exactly. Measured on this artifact: 132 of 132 IFCDOOR recovered, exact id-set match against the export's Tag values — no false positives, no misses, tolerance 0. Bodies are the record's own bounding box (envelope, not a resolved profile or wall location curve); base/top Level binding and door/window host-wall binding stay open (#86, door_window_host_wall_binding)." + "notes": "Closed 2026-08-30 (#211): the partition element-record prologue carries a container reference at +0x32 (0xffffffffffffffff = none) and a placement-kind word at +0x42 (0xffffef7f placed instance / 0xffff8000 type-symbol envelope). Records that are standalone AND placed reproduce Revit's exported ElementId set exactly. Measured on this artifact: 132 of 132 IFCDOOR recovered, exact id-set match against the export's Tag values \u2014 no false positives, no misses, tolerance 0. Bodies are the record's own bounding box (envelope, not a resolved profile or wall location curve); base/top Level binding stays open (#86). Door/window host-wall binding closed 2026-08-30 (#222, RE-23): all 138 doors and windows now void into their host wall, scored by the relations.IFCRELFILLSELEMENT row below." }, "windows": { "status": "known", @@ -109,7 +108,7 @@ "source_ifc_type": "IFCWINDOW", "tolerance": 0, "tracking_issue": 211, - "notes": "Closed 2026-08-30 (#211): the partition element-record prologue carries a container reference at +0x32 (0xffffffffffffffff = none) and a placement-kind word at +0x42 (0xffffef7f placed instance / 0xffff8000 type-symbol envelope). Records that are standalone AND placed reproduce Revit's exported ElementId set exactly. Measured on this artifact: 6 of 6 IFCWINDOW recovered, exact id-set match against the export's Tag values — no false positives, no misses, tolerance 0. Bodies are the record's own bounding box (envelope, not a resolved profile or wall location curve); base/top Level binding and door/window host-wall binding stay open (#86, door_window_host_wall_binding)." + "notes": "Closed 2026-08-30 (#211): the partition element-record prologue carries a container reference at +0x32 (0xffffffffffffffff = none) and a placement-kind word at +0x42 (0xffffef7f placed instance / 0xffff8000 type-symbol envelope). Records that are standalone AND placed reproduce Revit's exported ElementId set exactly. Measured on this artifact: 6 of 6 IFCWINDOW recovered, exact id-set match against the export's Tag values \u2014 no false positives, no misses, tolerance 0. Bodies are the record's own bounding box (envelope, not a resolved profile or wall location curve); base/top Level binding stays open (#86). Door/window host-wall binding closed 2026-08-30 (#222, RE-23): all 138 doors and windows now void into their host wall, scored by the relations.IFCRELFILLSELEMENT row below." }, "rooms_spaces": { "status": "known_gap", @@ -128,7 +127,7 @@ "source_ifc_type": "IFCCOLUMN", "tolerance": 0, "tracking_issue": 211, - "notes": "Closed 2026-08-30 (#204), instance rule replaced 2026-08-30 (#211): `partition_element_records` decodes the Revit 2024 partition element-record header (`u64` ElementId, `i64` BuiltInCategory at +0x12, container reference at +0x32, placement kind at +0x42, bbox marker at +0x50, six `f64` feet at +0x58) and keeps OST_Columns (-2000100) records whose ElementId is declared in Global/ElemTable and which are standalone placed instances. The #204 family-local bbox proxy and highest-id-per-footprint collapse are gone — the direct test reproduces the same 256 of 256 and additionally the exact wall / door / window id sets." + "notes": "Closed 2026-08-30 (#204), instance rule replaced 2026-08-30 (#211): `partition_element_records` decodes the Revit 2024 partition element-record header (`u64` ElementId, `i64` BuiltInCategory at +0x12, container reference at +0x32, placement kind at +0x42, bbox marker at +0x50, six `f64` feet at +0x58) and keeps OST_Columns (-2000100) records whose ElementId is declared in Global/ElemTable and which are standalone placed instances. The #204 family-local bbox proxy and highest-id-per-footprint collapse are gone \u2014 the direct test reproduces the same 256 of 256 and additionally the exact wall / door / window id sets." }, "beams": { "status": "known", @@ -178,10 +177,31 @@ "source_ifc_type": "IFCSHADINGDEVICE", "tolerance": 0, "tracking_issue": 212, - "notes": "Added 2026-08-30 (#212, RE-22). Twenty OST_Floors instances carry a per-element Revit 'IFC Export As' override naming IfcShadingDevice, so Revit's exporter emits them as IFCSHADINGDEVICE rather than IFCSLAB even though they share a FloorType with elements that stay slabs. rvt-rs reads the override string out of the partition parameter block and honours it; measured 20 of 20 with an exact id-set match against the export's Tag values, tolerance 0. This manifest previously left these 20 unscored." + "notes": "Added 2026-08-30 (#212, RE-22). Twenty OST_Floors instances carry a per-element Revit 'IFC Export As' override naming IfcShadingDevice, so Revit's exporter emits them as IFCSHADINGDEVICE rather than IFCSLAB even though they share a FloorType with elements that stay slabs. rvt-rs reads the override string out of the partition parameter block and honours it; measured 20 of 20 with an exact id-set match against the export's Tag values, tolerance 0. This manifest previously left these 20 unscored. The plan profile of these 20 stays the record box rectangle (#31, RE-25): they are rotated plates whose 57 sketch-line boxes include one of zero extent and 21 diagonals with no axis-parallel anchor, so the closure rule declines all 20 and ProfileResolved stays false." + } + }, + "relations": { + "opening_fills": { + "status": "known", + "relation_ifc_type": "IFCRELFILLSELEMENT", + "expected_pairs": 138, + "source": "full Revit IFC export", + "tracking_issue": 222, + "notes": "Added 2026-08-30 (#222, RE-23). Revit's exporter voids each door and window out of its host wall with an IfcOpeningElement: IfcRelVoidsElement binds the opening to the wall, IfcRelFillsElement binds the opening to the door or window. This export carries 201 IfcOpeningElement, of which 138 are filled - 132 IfcDoor and 6 IfcWindow, every one hosted by an IfcWall - and 63 are unfilled penetrations of an IfcSlab or IfcShadingDevice, which are out of scope here. The pair set compared is [host Tag, filling Tag], read through the void/fill chain by all three witnesses independently: IfcOpenShell 0.8.5 by_type + attribute access, IFClite 7.1.1 EntityScanner + parse_entity, and rvt-rs by splitting its own emitted STEP. rvt-rs recovers the host from the partition element record's counted reference list at +0x88: the slot immediately before the record's own ElementId, accepted only when it is one of the 360 ElementIds the same instance rule selects as exported walls. Measured on this artifact: 138 of 138 pairs reproduced exactly - no wrong host, no missing pair, no extra pair - across all 273 records that frame those 138 elements. The 63 slab and shading-device openings are not emitted: rvt-rs recovers no opening that is not filled by a recovered element." + } + }, + "storeys": { + "building_storeys": { + "status": "known", + "storey_ifc_type": "IFCBUILDINGSTOREY", + "expected_storeys": 15, + "source": "full Revit IFC export", + "tracking_issue": 218, + "notes": "Added 2026-08-30 (#218, RE-24). The compared value is the exact set of [Name, Elevation] pairs, with the elevation converted from each file's declared LENGTHUNIT to feet and rendered at 1e-6 ft: this export declares FOOT (IfcConversionBasedUnit, ratio 0.3048) while rvt-rs writes METRE, so the field would be incomparable as raw numbers. All three witnesses read it independently - IfcOpenShell 0.8.5 by_type + ifcopenshell.util.unit.calculate_unit_scale, IFClite 7.1.1 EntityScanner + ifc_lite_core::extract_length_unit_scale, and rvt-rs by splitting its own emitted STEP and resolving IfcProject -> IfcUnitAssignment itself. Measured on this artifact: 15 of 15 pairs reproduced exactly. Names are asserted, not guessed: each elevation comes out of the parameter block owned by the Level's own ElementId, so the pairing is stated by the file rather than joined by rank (which #213 showed puts Level 6 at 91 ft where this export puts Level 7)." } }, "semantic_surface": [ + "entity_counts.IFCBUILDINGSTOREY", "entity_counts.IFCWALL", "entity_counts.IFCSLAB", "entity_counts.IFCROOF", @@ -191,16 +211,11 @@ "entity_counts.IFCBEAM", "entity_counts.IFCFLOWTERMINAL", "entity_counts.IFCUNITASSIGNMENT", - "entity_counts.IFCSHADINGDEVICE" + "entity_counts.IFCSHADINGDEVICE", + "relations.IFCRELFILLSELEMENT", + "storeys.IFCBUILDINGSTOREY" ], "excluded": [ - { - "category": "levels", - "field": "entity_counts.IFCBUILDINGSTOREY", - "reason": "decoder_baseline", - "tracking_issue": 33, - "unsupported_feature": null - }, { "category": "rooms_spaces", "field": "entity_counts.IFCSPACE", @@ -223,5 +238,5 @@ "unsupported_feature": "revit_element_parameters_to_ifc_property_sets" } ], - "notes": "The full-project export of the same .rvt as g-2026-0001: 19,879 entity instances against a 20 KB element fixture. `counts` is the normative SPEC.md §6.1 block and is what the gate reads; `semantic_surface` and `excluded` are the declared result, and tools/verdict.py fails with MANIFEST_ERROR if they disagree with what `counts` implies. Only four categories are `known` — the surface is narrow by design, because a wider one would be a false claim while 360 walls, 132 doors, 256 columns and 116 spaces are unrecovered. The nine excluded categories are the deliverable here: they are the measured gap between rvt-rs and Revit's own exporter on a real project schedule, each carrying its rvt-rs tracking issue (30/31/32/33/34/35/204). Departing from the upstream rvt-rs manifest exactly as corpus/README.md documents, the `decoder_*` fields are dropped; `levels` is retained, unlike in g-2026-0001, because here it carries a `source_ifc_type` and is genuinely cross-witness visible (15 exported storeys against 12 recovered). See PROVENANCE.md." + "notes": "The full-project export of the same .rvt as g-2026-0001: 19,879 entity instances against a 20 KB element fixture. `counts`, `relations` and `storeys` are the normative SPEC.md \u00a76.1 blocks and are what the gate reads; `semantic_surface` and `excluded` are the declared result, and tools/verdict.py fails with MANIFEST_ERROR if they disagree with what those blocks imply. Thirteen fields are `known` here \u2014 eleven entity counts, one relation pair set and one storey set \u2014 and the three that are not are the deliverable: they are the measured gap between rvt-rs and Revit's own exporter on a real project schedule, each carrying its rvt-rs tracking issue (33/34/35). Departing from the upstream rvt-rs manifest exactly as corpus/README.md documents, the `decoder_*` fields are dropped and a category that carries no cross-witness type key is dropped with them. See PROVENANCE.md." } diff --git a/corpus/artifacts/g-2026-0002/observations/ifc-lite.json b/corpus/artifacts/g-2026-0002/observations/ifc-lite.json index 18275b8..2364b1a 100644 --- a/corpus/artifacts/g-2026-0002/observations/ifc-lite.json +++ b/corpus/artifacts/g-2026-0002/observations/ifc-lite.json @@ -21,12 +21,634 @@ "IFCWALL": 360, "IFCWINDOW": 6 }, - "ifc_schema": "IFC4" + "ifc_schema": "IFC4", + "relations": { + "IFCRELFILLSELEMENT": [ + [ + "20796", + "20827" + ], + [ + "20798", + "20810" + ], + [ + "20798", + "20815" + ], + [ + "20798", + "20829" + ], + [ + "20800", + "20813" + ], + [ + "20800", + "20814" + ], + [ + "20800", + "20830" + ], + [ + "20802", + "20828" + ], + [ + "20803", + "20821" + ], + [ + "20808", + "20818" + ], + [ + "20809", + "20819" + ], + [ + "20817", + "20820" + ], + [ + "20863", + "20865" + ], + [ + "20863", + "20866" + ], + [ + "20897", + "20898" + ], + [ + "25488", + "25947" + ], + [ + "25488", + "25953" + ], + [ + "25488", + "25954" + ], + [ + "25491", + "25952" + ], + [ + "25493", + "25951" + ], + [ + "25495", + "25950" + ], + [ + "25497", + "25949" + ], + [ + "25943", + "25948" + ], + [ + "25957", + "25966" + ], + [ + "25958", + "25965" + ], + [ + "25963", + "25964" + ], + [ + "29853", + "29884" + ], + [ + "29855", + "29867" + ], + [ + "29855", + "29872" + ], + [ + "29855", + "29886" + ], + [ + "29857", + "29870" + ], + [ + "29857", + "29871" + ], + [ + "29857", + "29887" + ], + [ + "29859", + "29885" + ], + [ + "29860", + "29878" + ], + [ + "29865", + "29875" + ], + [ + "29866", + "29876" + ], + [ + "29874", + "29877" + ], + [ + "29920", + "29922" + ], + [ + "29920", + "29923" + ], + [ + "55734", + "55765" + ], + [ + "55736", + "55748" + ], + [ + "55736", + "55753" + ], + [ + "55736", + "55767" + ], + [ + "55738", + "55751" + ], + [ + "55738", + "55752" + ], + [ + "55738", + "55768" + ], + [ + "55740", + "55766" + ], + [ + "55741", + "55759" + ], + [ + "55746", + "55756" + ], + [ + "55747", + "55757" + ], + [ + "55755", + "55758" + ], + [ + "55801", + "55803" + ], + [ + "55801", + "55804" + ], + [ + "59314", + "59345" + ], + [ + "59316", + "59328" + ], + [ + "59316", + "59333" + ], + [ + "59316", + "59347" + ], + [ + "59318", + "59331" + ], + [ + "59318", + "59332" + ], + [ + "59318", + "59348" + ], + [ + "59320", + "59346" + ], + [ + "59321", + "59339" + ], + [ + "59326", + "59336" + ], + [ + "59327", + "59337" + ], + [ + "59335", + "59338" + ], + [ + "59381", + "59383" + ], + [ + "59381", + "59384" + ], + [ + "59415", + "59416" + ], + [ + "60195", + "60226" + ], + [ + "60197", + "60209" + ], + [ + "60197", + "60214" + ], + [ + "60197", + "60228" + ], + [ + "60199", + "60212" + ], + [ + "60199", + "60213" + ], + [ + "60199", + "60229" + ], + [ + "60201", + "60227" + ], + [ + "60202", + "60220" + ], + [ + "60207", + "60217" + ], + [ + "60208", + "60218" + ], + [ + "60216", + "60219" + ], + [ + "60262", + "60264" + ], + [ + "60262", + "60265" + ], + [ + "60296", + "60297" + ], + [ + "61076", + "61107" + ], + [ + "61078", + "61090" + ], + [ + "61078", + "61095" + ], + [ + "61078", + "61109" + ], + [ + "61080", + "61093" + ], + [ + "61080", + "61094" + ], + [ + "61080", + "61110" + ], + [ + "61082", + "61108" + ], + [ + "61083", + "61101" + ], + [ + "61088", + "61098" + ], + [ + "61089", + "61099" + ], + [ + "61097", + "61100" + ], + [ + "61143", + "61145" + ], + [ + "61143", + "61146" + ], + [ + "61177", + "61178" + ], + [ + "61957", + "61988" + ], + [ + "61959", + "61971" + ], + [ + "61959", + "61976" + ], + [ + "61959", + "61990" + ], + [ + "61961", + "61974" + ], + [ + "61961", + "61975" + ], + [ + "61961", + "61991" + ], + [ + "61963", + "61989" + ], + [ + "61964", + "61982" + ], + [ + "61969", + "61979" + ], + [ + "61970", + "61980" + ], + [ + "61978", + "61981" + ], + [ + "62024", + "62026" + ], + [ + "62024", + "62027" + ], + [ + "62058", + "62059" + ], + [ + "62838", + "62869" + ], + [ + "62840", + "62852" + ], + [ + "62840", + "62857" + ], + [ + "62840", + "62871" + ], + [ + "62842", + "62855" + ], + [ + "62842", + "62856" + ], + [ + "62842", + "62872" + ], + [ + "62844", + "62870" + ], + [ + "62845", + "62863" + ], + [ + "62850", + "62860" + ], + [ + "62851", + "62861" + ], + [ + "62859", + "62862" + ], + [ + "62905", + "62907" + ], + [ + "62905", + "62908" + ], + [ + "62939", + "62940" + ], + [ + "63719", + "63750" + ], + [ + "63721", + "63733" + ], + [ + "63721", + "63738" + ], + [ + "63721", + "63752" + ], + [ + "63723", + "63736" + ], + [ + "63723", + "63737" + ], + [ + "63723", + "63753" + ], + [ + "63725", + "63751" + ], + [ + "80747", + "80750" + ] + ] + }, + "storeys": { + "IFCBUILDINGSTOREY": [ + [ + "Basement 1", + "-20.000000" + ], + [ + "Basement 2", + "-40.000000" + ], + [ + "Level 1", + "0.000000" + ], + [ + "Level 10", + "136.000000" + ], + [ + "Level 11", + "151.000000" + ], + [ + "Level 12", + "166.000000" + ], + [ + "Level 13", + "185.500000" + ], + [ + "Level 3 - Wall Layouts 1", + "31.000000" + ], + [ + "Level 4 - Wall Layouts 2", + "46.000000" + ], + [ + "Level 4 - Wall Layouts 3", + "61.000000" + ], + [ + "Level 6", + "76.000000" + ], + [ + "Level 7", + "91.000000" + ], + [ + "Level 8", + "106.000000" + ], + [ + "Level 9", + "121.000000" + ], + [ + "Mez 1-2", + "15.000000" + ] + ] + } }, - "observation_hash_sha256": "951e6d45fa5c808e96099fb763b63952b543adaaf82555913df49555cd4929d4", - "schema_version": "1.0.0", + "observation_hash_sha256": "5a101408488d591d4e7d15fe39969f1ee295c282dfe53c1f093468ae3e5cb2d0", + "schema_version": "1.1.0", "semantic_surface_covered": [ - "entity_counts" + "entity_counts", + "relations", + "storeys" ], "unsupported_entities": [], "warnings": [], diff --git a/corpus/artifacts/g-2026-0002/observations/ifcopenshell.json b/corpus/artifacts/g-2026-0002/observations/ifcopenshell.json index ef7d731..db701c7 100644 --- a/corpus/artifacts/g-2026-0002/observations/ifcopenshell.json +++ b/corpus/artifacts/g-2026-0002/observations/ifcopenshell.json @@ -21,12 +21,634 @@ "IFCWALL": 360, "IFCWINDOW": 6 }, - "ifc_schema": "IFC4" + "ifc_schema": "IFC4", + "relations": { + "IFCRELFILLSELEMENT": [ + [ + "20796", + "20827" + ], + [ + "20798", + "20810" + ], + [ + "20798", + "20815" + ], + [ + "20798", + "20829" + ], + [ + "20800", + "20813" + ], + [ + "20800", + "20814" + ], + [ + "20800", + "20830" + ], + [ + "20802", + "20828" + ], + [ + "20803", + "20821" + ], + [ + "20808", + "20818" + ], + [ + "20809", + "20819" + ], + [ + "20817", + "20820" + ], + [ + "20863", + "20865" + ], + [ + "20863", + "20866" + ], + [ + "20897", + "20898" + ], + [ + "25488", + "25947" + ], + [ + "25488", + "25953" + ], + [ + "25488", + "25954" + ], + [ + "25491", + "25952" + ], + [ + "25493", + "25951" + ], + [ + "25495", + "25950" + ], + [ + "25497", + "25949" + ], + [ + "25943", + "25948" + ], + [ + "25957", + "25966" + ], + [ + "25958", + "25965" + ], + [ + "25963", + "25964" + ], + [ + "29853", + "29884" + ], + [ + "29855", + "29867" + ], + [ + "29855", + "29872" + ], + [ + "29855", + "29886" + ], + [ + "29857", + "29870" + ], + [ + "29857", + "29871" + ], + [ + "29857", + "29887" + ], + [ + "29859", + "29885" + ], + [ + "29860", + "29878" + ], + [ + "29865", + "29875" + ], + [ + "29866", + "29876" + ], + [ + "29874", + "29877" + ], + [ + "29920", + "29922" + ], + [ + "29920", + "29923" + ], + [ + "55734", + "55765" + ], + [ + "55736", + "55748" + ], + [ + "55736", + "55753" + ], + [ + "55736", + "55767" + ], + [ + "55738", + "55751" + ], + [ + "55738", + "55752" + ], + [ + "55738", + "55768" + ], + [ + "55740", + "55766" + ], + [ + "55741", + "55759" + ], + [ + "55746", + "55756" + ], + [ + "55747", + "55757" + ], + [ + "55755", + "55758" + ], + [ + "55801", + "55803" + ], + [ + "55801", + "55804" + ], + [ + "59314", + "59345" + ], + [ + "59316", + "59328" + ], + [ + "59316", + "59333" + ], + [ + "59316", + "59347" + ], + [ + "59318", + "59331" + ], + [ + "59318", + "59332" + ], + [ + "59318", + "59348" + ], + [ + "59320", + "59346" + ], + [ + "59321", + "59339" + ], + [ + "59326", + "59336" + ], + [ + "59327", + "59337" + ], + [ + "59335", + "59338" + ], + [ + "59381", + "59383" + ], + [ + "59381", + "59384" + ], + [ + "59415", + "59416" + ], + [ + "60195", + "60226" + ], + [ + "60197", + "60209" + ], + [ + "60197", + "60214" + ], + [ + "60197", + "60228" + ], + [ + "60199", + "60212" + ], + [ + "60199", + "60213" + ], + [ + "60199", + "60229" + ], + [ + "60201", + "60227" + ], + [ + "60202", + "60220" + ], + [ + "60207", + "60217" + ], + [ + "60208", + "60218" + ], + [ + "60216", + "60219" + ], + [ + "60262", + "60264" + ], + [ + "60262", + "60265" + ], + [ + "60296", + "60297" + ], + [ + "61076", + "61107" + ], + [ + "61078", + "61090" + ], + [ + "61078", + "61095" + ], + [ + "61078", + "61109" + ], + [ + "61080", + "61093" + ], + [ + "61080", + "61094" + ], + [ + "61080", + "61110" + ], + [ + "61082", + "61108" + ], + [ + "61083", + "61101" + ], + [ + "61088", + "61098" + ], + [ + "61089", + "61099" + ], + [ + "61097", + "61100" + ], + [ + "61143", + "61145" + ], + [ + "61143", + "61146" + ], + [ + "61177", + "61178" + ], + [ + "61957", + "61988" + ], + [ + "61959", + "61971" + ], + [ + "61959", + "61976" + ], + [ + "61959", + "61990" + ], + [ + "61961", + "61974" + ], + [ + "61961", + "61975" + ], + [ + "61961", + "61991" + ], + [ + "61963", + "61989" + ], + [ + "61964", + "61982" + ], + [ + "61969", + "61979" + ], + [ + "61970", + "61980" + ], + [ + "61978", + "61981" + ], + [ + "62024", + "62026" + ], + [ + "62024", + "62027" + ], + [ + "62058", + "62059" + ], + [ + "62838", + "62869" + ], + [ + "62840", + "62852" + ], + [ + "62840", + "62857" + ], + [ + "62840", + "62871" + ], + [ + "62842", + "62855" + ], + [ + "62842", + "62856" + ], + [ + "62842", + "62872" + ], + [ + "62844", + "62870" + ], + [ + "62845", + "62863" + ], + [ + "62850", + "62860" + ], + [ + "62851", + "62861" + ], + [ + "62859", + "62862" + ], + [ + "62905", + "62907" + ], + [ + "62905", + "62908" + ], + [ + "62939", + "62940" + ], + [ + "63719", + "63750" + ], + [ + "63721", + "63733" + ], + [ + "63721", + "63738" + ], + [ + "63721", + "63752" + ], + [ + "63723", + "63736" + ], + [ + "63723", + "63737" + ], + [ + "63723", + "63753" + ], + [ + "63725", + "63751" + ], + [ + "80747", + "80750" + ] + ] + }, + "storeys": { + "IFCBUILDINGSTOREY": [ + [ + "Basement 1", + "-20.000000" + ], + [ + "Basement 2", + "-40.000000" + ], + [ + "Level 1", + "0.000000" + ], + [ + "Level 10", + "136.000000" + ], + [ + "Level 11", + "151.000000" + ], + [ + "Level 12", + "166.000000" + ], + [ + "Level 13", + "185.500000" + ], + [ + "Level 3 - Wall Layouts 1", + "31.000000" + ], + [ + "Level 4 - Wall Layouts 2", + "46.000000" + ], + [ + "Level 4 - Wall Layouts 3", + "61.000000" + ], + [ + "Level 6", + "76.000000" + ], + [ + "Level 7", + "91.000000" + ], + [ + "Level 8", + "106.000000" + ], + [ + "Level 9", + "121.000000" + ], + [ + "Mez 1-2", + "15.000000" + ] + ] + } }, - "observation_hash_sha256": "951e6d45fa5c808e96099fb763b63952b543adaaf82555913df49555cd4929d4", - "schema_version": "1.0.0", + "observation_hash_sha256": "5a101408488d591d4e7d15fe39969f1ee295c282dfe53c1f093468ae3e5cb2d0", + "schema_version": "1.1.0", "semantic_surface_covered": [ - "entity_counts" + "entity_counts", + "relations", + "storeys" ], "unsupported_entities": [], "warnings": [], diff --git a/corpus/artifacts/g-2026-0002/observations/rvt-rs.json b/corpus/artifacts/g-2026-0002/observations/rvt-rs.json index 8af36b8..092f711 100644 --- a/corpus/artifacts/g-2026-0002/observations/rvt-rs.json +++ b/corpus/artifacts/g-2026-0002/observations/rvt-rs.json @@ -8,35 +8,41 @@ "building_elements_with_geometry": 854, "entity_counts": { "IFCAPPLICATION": 1, - "IFCAXIS2PLACEMENT2D": 854, + "IFCARBITRARYCLOSEDPROFILEDEF": 38, + "IFCARBITRARYPROFILEDEFWITHVOIDS": 42, + "IFCAXIS2PLACEMENT2D": 992, "IFCAXIS2PLACEMENT3D": 855, "IFCBUILDING": 1, - "IFCBUILDINGSTOREY": 11, - "IFCCARTESIANPOINT": 1709, + "IFCBUILDINGSTOREY": 15, + "IFCCARTESIANPOINT": 3381, "IFCCOLUMN": 256, "IFCCONVERSIONBASEDUNIT": 4, "IFCDIMENSIONALEXPONENTS": 4, - "IFCDIRECTION": 856, + "IFCDIRECTION": 994, "IFCDOOR": 132, - "IFCEXTRUDEDAREASOLID": 854, + "IFCEXTRUDEDAREASOLID": 992, "IFCGEOMETRICREPRESENTATIONCONTEXT": 1, - "IFCLOCALPLACEMENT": 885, + "IFCLOCALPLACEMENT": 1027, "IFCMATERIAL": 102, "IFCMEASUREWITHUNIT": 4, + "IFCOPENINGELEMENT": 138, "IFCORGANIZATION": 1, "IFCOWNERHISTORY": 1, "IFCPERSON": 1, "IFCPERSONANDORGANIZATION": 1, - "IFCPRODUCTDEFINITIONSHAPE": 854, + "IFCPOLYLINE": 122, + "IFCPRODUCTDEFINITIONSHAPE": 992, "IFCPROJECT": 1, "IFCPROPERTYSET": 854, - "IFCPROPERTYSINGLEVALUE": 5364, - "IFCRECTANGLEPROFILEDEF": 854, + "IFCPROPERTYSINGLEVALUE": 5611, + "IFCRECTANGLEPROFILEDEF": 912, "IFCRELAGGREGATES": 3, - "IFCRELCONTAINEDINSPATIALSTRUCTURE": 11, + "IFCRELCONTAINEDINSPATIALSTRUCTURE": 13, "IFCRELDEFINESBYPROPERTIES": 854, + "IFCRELFILLSELEMENT": 138, + "IFCRELVOIDSELEMENT": 138, "IFCSHADINGDEVICE": 20, - "IFCSHAPEREPRESENTATION": 854, + "IFCSHAPEREPRESENTATION": 992, "IFCSITE": 1, "IFCSIUNIT": 6, "IFCSLAB": 80, @@ -55,26 +61,646 @@ "IFCWINDOW": 6 }, "material_count": 102, - "storey_count": 11 + "relations": { + "IFCRELFILLSELEMENT": [ + [ + "20796", + "20827" + ], + [ + "20798", + "20810" + ], + [ + "20798", + "20815" + ], + [ + "20798", + "20829" + ], + [ + "20800", + "20813" + ], + [ + "20800", + "20814" + ], + [ + "20800", + "20830" + ], + [ + "20802", + "20828" + ], + [ + "20803", + "20821" + ], + [ + "20808", + "20818" + ], + [ + "20809", + "20819" + ], + [ + "20817", + "20820" + ], + [ + "20863", + "20865" + ], + [ + "20863", + "20866" + ], + [ + "20897", + "20898" + ], + [ + "25488", + "25947" + ], + [ + "25488", + "25953" + ], + [ + "25488", + "25954" + ], + [ + "25491", + "25952" + ], + [ + "25493", + "25951" + ], + [ + "25495", + "25950" + ], + [ + "25497", + "25949" + ], + [ + "25943", + "25948" + ], + [ + "25957", + "25966" + ], + [ + "25958", + "25965" + ], + [ + "25963", + "25964" + ], + [ + "29853", + "29884" + ], + [ + "29855", + "29867" + ], + [ + "29855", + "29872" + ], + [ + "29855", + "29886" + ], + [ + "29857", + "29870" + ], + [ + "29857", + "29871" + ], + [ + "29857", + "29887" + ], + [ + "29859", + "29885" + ], + [ + "29860", + "29878" + ], + [ + "29865", + "29875" + ], + [ + "29866", + "29876" + ], + [ + "29874", + "29877" + ], + [ + "29920", + "29922" + ], + [ + "29920", + "29923" + ], + [ + "55734", + "55765" + ], + [ + "55736", + "55748" + ], + [ + "55736", + "55753" + ], + [ + "55736", + "55767" + ], + [ + "55738", + "55751" + ], + [ + "55738", + "55752" + ], + [ + "55738", + "55768" + ], + [ + "55740", + "55766" + ], + [ + "55741", + "55759" + ], + [ + "55746", + "55756" + ], + [ + "55747", + "55757" + ], + [ + "55755", + "55758" + ], + [ + "55801", + "55803" + ], + [ + "55801", + "55804" + ], + [ + "59314", + "59345" + ], + [ + "59316", + "59328" + ], + [ + "59316", + "59333" + ], + [ + "59316", + "59347" + ], + [ + "59318", + "59331" + ], + [ + "59318", + "59332" + ], + [ + "59318", + "59348" + ], + [ + "59320", + "59346" + ], + [ + "59321", + "59339" + ], + [ + "59326", + "59336" + ], + [ + "59327", + "59337" + ], + [ + "59335", + "59338" + ], + [ + "59381", + "59383" + ], + [ + "59381", + "59384" + ], + [ + "59415", + "59416" + ], + [ + "60195", + "60226" + ], + [ + "60197", + "60209" + ], + [ + "60197", + "60214" + ], + [ + "60197", + "60228" + ], + [ + "60199", + "60212" + ], + [ + "60199", + "60213" + ], + [ + "60199", + "60229" + ], + [ + "60201", + "60227" + ], + [ + "60202", + "60220" + ], + [ + "60207", + "60217" + ], + [ + "60208", + "60218" + ], + [ + "60216", + "60219" + ], + [ + "60262", + "60264" + ], + [ + "60262", + "60265" + ], + [ + "60296", + "60297" + ], + [ + "61076", + "61107" + ], + [ + "61078", + "61090" + ], + [ + "61078", + "61095" + ], + [ + "61078", + "61109" + ], + [ + "61080", + "61093" + ], + [ + "61080", + "61094" + ], + [ + "61080", + "61110" + ], + [ + "61082", + "61108" + ], + [ + "61083", + "61101" + ], + [ + "61088", + "61098" + ], + [ + "61089", + "61099" + ], + [ + "61097", + "61100" + ], + [ + "61143", + "61145" + ], + [ + "61143", + "61146" + ], + [ + "61177", + "61178" + ], + [ + "61957", + "61988" + ], + [ + "61959", + "61971" + ], + [ + "61959", + "61976" + ], + [ + "61959", + "61990" + ], + [ + "61961", + "61974" + ], + [ + "61961", + "61975" + ], + [ + "61961", + "61991" + ], + [ + "61963", + "61989" + ], + [ + "61964", + "61982" + ], + [ + "61969", + "61979" + ], + [ + "61970", + "61980" + ], + [ + "61978", + "61981" + ], + [ + "62024", + "62026" + ], + [ + "62024", + "62027" + ], + [ + "62058", + "62059" + ], + [ + "62838", + "62869" + ], + [ + "62840", + "62852" + ], + [ + "62840", + "62857" + ], + [ + "62840", + "62871" + ], + [ + "62842", + "62855" + ], + [ + "62842", + "62856" + ], + [ + "62842", + "62872" + ], + [ + "62844", + "62870" + ], + [ + "62845", + "62863" + ], + [ + "62850", + "62860" + ], + [ + "62851", + "62861" + ], + [ + "62859", + "62862" + ], + [ + "62905", + "62907" + ], + [ + "62905", + "62908" + ], + [ + "62939", + "62940" + ], + [ + "63719", + "63750" + ], + [ + "63721", + "63733" + ], + [ + "63721", + "63738" + ], + [ + "63721", + "63752" + ], + [ + "63723", + "63736" + ], + [ + "63723", + "63737" + ], + [ + "63723", + "63753" + ], + [ + "63725", + "63751" + ], + [ + "80747", + "80750" + ] + ] + }, + "storey_count": 15, + "storeys": { + "IFCBUILDINGSTOREY": [ + [ + "Basement 1", + "-20.000000" + ], + [ + "Basement 2", + "-40.000000" + ], + [ + "Level 1", + "0.000000" + ], + [ + "Level 10", + "136.000000" + ], + [ + "Level 11", + "151.000000" + ], + [ + "Level 12", + "166.000000" + ], + [ + "Level 13", + "185.500000" + ], + [ + "Level 3 - Wall Layouts 1", + "31.000000" + ], + [ + "Level 4 - Wall Layouts 2", + "46.000000" + ], + [ + "Level 4 - Wall Layouts 3", + "61.000000" + ], + [ + "Level 6", + "76.000000" + ], + [ + "Level 7", + "91.000000" + ], + [ + "Level 8", + "106.000000" + ], + [ + "Level 9", + "121.000000" + ], + [ + "Mez 1-2", + "15.000000" + ] + ] + } }, - "observation_hash_sha256": "881da721e4ec87d7cf03424620b7595dea5fcce4f559a6246d562fa3a47ba9a4", - "schema_version": "1.0.0", + "observation_hash_sha256": "70f8df9dd188b7e42947bfa167b2810333b3d0dc4398019ea40570b7fcb87c24", + "schema_version": "1.1.0", "semantic_surface_covered": [ - "entity_counts" + "entity_counts", + "relations", + "storeys" ], "unsupported_entities": [ "partial_element_geometry", "revit_compound_assemblies_and_walltype_widths", - "door_window_host_wall_binding", "opening_index_to_ifc_openingelement_host_join", "schema_field_wall_instances", "revit_element_parameters_to_ifc_property_sets" ], "warnings": [ - "Unsupported or incomplete geometry was reported: unsupported_geometry_curve=18, unsupported_geometry_unresolved_host=138, unsupported_geometry_missing_level=78, unsupported_geometry_missing_dimensions=18.", + "Unsupported or incomplete geometry was reported: unsupported_geometry_curve=18, unsupported_geometry_missing_level=71, unsupported_geometry_missing_dimensions=18.", "Unmapped Revit unit identifiers were preserved in diagnostics: autodesk.unit.unit:amperes-1.0.0, autodesk.unit.unit:britishThermalUnits-1.0.1, autodesk.unit.unit:britishThermalUnitsPerDegreeFahrenheit-1.0.1, autodesk.unit.unit:britishThermalUnitsPerHour-1.0.1, autodesk.unit.unit:britishThermalUnitsPerHourCubicFoot-1.0.1, autodesk.unit.unit:britishThermalUnitsPerHourFootDegreeFahrenheit-1.0.1, autodesk.unit.unit:britishThermalUnitsPerHourSquareFoot-1.0.1, autodesk.unit.unit:britishThermalUnitsPerHourSquareFootDegreeFahrenheit-1.0.1", - "11 building storey(s) lack a confident partition Level name and keep elevation fallback labels.", - "11 building storey elevation(s) were measured from partition element-record bounding boxes and 794 building element(s) bound to them by an exact record-face elevation match (base face, or top face for slabs and shading devices) (#213, #212); the 12 recovered Level name string(s) could not be paired with those elevations and are not asserted as storey names.", + "15 building storey name(s) came from partition Level-like strings (RE-15/#86 via PR #117); ElementId↔Level binding is still pending.", "Suppressed 18 low-confidence schema scan candidates from default export.", "Formats/Latest multipage integrity uncertain (checksum-page strip disabled; RVT_FORMATS_MULTIPAGE_UNVERIFIED)." ], diff --git a/corpus/artifacts/g-2026-0002/verdict.json b/corpus/artifacts/g-2026-0002/verdict.json index dd90b40..ec3a3fc 100644 --- a/corpus/artifacts/g-2026-0002/verdict.json +++ b/corpus/artifacts/g-2026-0002/verdict.json @@ -2,13 +2,6 @@ "artifact_id": "magnetar-2024-core-interior-slim", "diffs": [], "excluded": [ - { - "category": "levels", - "field": "entity_counts.IFCBUILDINGSTOREY", - "reason": "decoder_baseline", - "tracking_issue": 33, - "unsupported_feature": null - }, { "category": "rooms_spaces", "field": "entity_counts.IFCSPACE", @@ -65,6 +58,7 @@ "insufficient_witnesses": false, "schema_version": "1.0.0", "semantic_surface": [ + "entity_counts.IFCBUILDINGSTOREY", "entity_counts.IFCWALL", "entity_counts.IFCSLAB", "entity_counts.IFCROOF", @@ -74,10 +68,12 @@ "entity_counts.IFCBEAM", "entity_counts.IFCFLOWTERMINAL", "entity_counts.IFCUNITASSIGNMENT", - "entity_counts.IFCSHADINGDEVICE" + "entity_counts.IFCSHADINGDEVICE", + "relations.IFCRELFILLSELEMENT", + "storeys.IFCBUILDINGSTOREY" ], "status": "PASS", - "verdict_hash_sha256": "48ef575a11505ee1fcbb02b37b3551e0e8d5618a1ec39aeb6d6cb250e085648c", + "verdict_hash_sha256": "cd171b3167c602d0882dd8a0d38c99791012a9fe693043e43e2d43b028554325", "witnesses_compared": [ "ifc-lite", "ifcopenshell", diff --git a/registry/README.md b/registry/README.md index c253e68..4cee3a6 100644 --- a/registry/README.md +++ b/registry/README.md @@ -135,7 +135,7 @@ What it resolved to: | Status | `adopted`, `checked: 2026-08-30`, `version: 7.1.1` | `zahmadsaleem/ifc-lite-headless`, the other candidate the search turned up, is -a different project and is not what is adopted. SPEC.md 1.0.1 §5.3, §18 and +a different project and is not what is adopted. SPEC.md §5.3, §18 and §19 note 4 carry the same resolution. It is now the third witness in the gate, running against both corpus diff --git a/schemas/witness-observation.schema.json b/schemas/witness-observation.schema.json index d30a91d..e459da8 100644 --- a/schemas/witness-observation.schema.json +++ b/schemas/witness-observation.schema.json @@ -2,7 +2,7 @@ "$schema": "https://json-schema.org/draft/2020-12/schema", "$id": "https://drunkonjava.github.io/rvt-rs/schemas/witness-observation.schema.json", "title": "OctetProof witness observation", - "description": "What one witness saw in one golden-artifact input, plus enough provenance to replay it. OctetProof 1.0.0 §6.2 (docs/octetproof-spec.md). Producers: `rvt-ifc --observation PATH --artifact-id ID` (source witness) and `tools/ci/witness-ifcopenshell.py --observation PATH` (bridge witness). Committed instances live under research/witness//observations/. Emitting an observation is not a capability claim; only a PASS verdict across independent witnesses is.", + "description": "What one witness saw in one golden-artifact input, plus enough provenance to replay it. OctetProof 1.1.0 §6.2 (docs/octetproof-spec.md). Producers: `rvt-ifc --observation PATH --artifact-id ID` (source witness) and `tools/ci/witness-ifcopenshell.py --observation PATH` (bridge witness). Committed instances live under research/witness//observations/. Emitting an observation is not a capability claim; only a PASS verdict across independent witnesses is.", "type": "object", "required": [ "schema_version", @@ -21,9 +21,9 @@ ], "properties": { "schema_version": { - "description": "The OctetProof version this observation conforms to.", + "description": "The OctetProof version this observation conforms to. 1.1.0 added the optional `relations` and `storeys` payload keys and their surface terms (§20.1, §20.2); the envelope is otherwise unchanged, so 1.0.0 observations stay valid (§16.2).", "type": "string", - "const": "1.0.0" + "enum": ["1.0.0", "1.1.0"] }, "witness_id": { "description": "Registry id (research/witness-registry.json → witnesses[].id). A witness absent from the registry makes the verdict a MANIFEST_ERROR.", @@ -68,6 +68,8 @@ "type": "string", "enum": [ "entity_counts", + "relations", + "storeys", "layer_topology", "linework", "bounding_boxes", @@ -80,7 +82,7 @@ } }, "observation": { - "description": "The payload — the only part that is canonicalized, hashed, and diffed. `entity_counts` is the cross-witness surface; every other key is witness-specific and is not compared.", + "description": "The payload — the only part that is canonicalized, hashed, and diffed. `entity_counts`, `relations` and `storeys` are the cross-witness surface; every other key is witness-specific and is not compared.", "type": "object", "required": ["entity_counts"], "properties": { @@ -88,6 +90,32 @@ "description": "Per-type instance counts. A type absent here counts as zero.", "type": "object", "additionalProperties": { "type": "integer", "minimum": 0 } + }, + "relations": { + "description": "Per-relation pair multisets (§7.2, added in 1.1.0). Each value is the relation's `[a_identity, b_identity]` pairs sorted lexicographically, duplicates kept. An identity the witness cannot read is the empty string, never an omitted pair. A relation absent here is an empty set.", + "type": "object", + "additionalProperties": { + "type": "array", + "items": { + "type": "array", + "minItems": 2, + "maxItems": 2, + "items": { "type": "string" } + } + } + }, + "storeys": { + "description": "Per-type storey sets (§7.2, added in 1.1.0). Each value is the spatial type's `[name, elevation]` pairs sorted lexicographically. The name is the entity's `Name` as the witness decoded it; the elevation is that storey's height in feet, unit-normalized by the witness from its own file's declared LENGTHUNIT and rendered with six decimal places (`-0` normalized to `0`). A string, not a number, so the canonical form stays integer/string only. An unset name or unreadable elevation is the empty string, never an omitted storey. A type absent here is an empty set.", + "type": "object", + "additionalProperties": { + "type": "array", + "items": { + "type": "array", + "minItems": 2, + "maxItems": 2, + "items": { "type": "string" } + } + } } } }, diff --git a/schemas/witness-verdict.schema.json b/schemas/witness-verdict.schema.json index 72bc729..116acb0 100644 --- a/schemas/witness-verdict.schema.json +++ b/schemas/witness-verdict.schema.json @@ -2,7 +2,7 @@ "$schema": "https://json-schema.org/draft/2020-12/schema", "$id": "https://drunkonjava.github.io/rvt-rs/schemas/witness-verdict.schema.json", "title": "OctetProof cross-witness verdict", - "description": "The gate's decision for one golden artifact: which witnesses were compared, which inputs they read, which fields were inside the claimed semantic surface, which were excluded first-class and why, any disagreements, whether the §9.3 independence set was satisfied, and whether the replay matched. OctetProof 1.0.0 §6.3, §7, §10.5 (docs/octetproof-spec.md). Producer: tools/ci/witness-verdict.py. Committed instances live at research/witness//verdict.json. Fail-closed: only PASS means agreement.", + "description": "The gate's decision for one golden artifact: which witnesses were compared, which inputs they read, which fields were inside the claimed semantic surface, which were excluded first-class and why, any disagreements, whether the §9.3 independence set was satisfied, and whether the replay matched. OctetProof 1.1.0 §6.3, §7, §10.5 (docs/octetproof-spec.md). Producer: tools/ci/witness-verdict.py. Committed instances live at research/witness//verdict.json. Fail-closed: only PASS means agreement.", "type": "object", "required": [ "schema_version", @@ -62,7 +62,7 @@ } }, "semantic_surface": { - "description": "The fields actually compared, as `entity_counts.`, derived from the manifest categories whose status is `known`.", + "description": "The fields actually compared — `entity_counts.` from the manifest `counts` categories whose status is `known`, `relations.` from the `relations` categories whose status is `known`, and `storeys.` from the `storeys` categories whose status is `known` (§7.2, both added in 1.1.0).", "type": "array", "items": { "type": "string" } }, @@ -96,6 +96,16 @@ "tolerance_applied": { "description": "The tolerance the comparison actually used (§7.2): the manifest category's integer tolerance for counts, 0 for an exact field, or false for a field class with no tolerance concept.", "type": ["number", "boolean"] + }, + "only_in_a": { + "description": "Set-valued field classes only — relation pair sets and storey sets (§7.2): the pairs witness A holds and witness B does not. `value_a` / `value_b` carry the set sizes.", + "type": "array", + "items": { "type": "array", "items": { "type": "string" } } + }, + "only_in_b": { + "description": "Set-valued field classes only: the pairs witness B holds and witness A does not.", + "type": "array", + "items": { "type": "array", "items": { "type": "string" } } } } } diff --git a/tools/README.md b/tools/README.md index f7ebac8..58a4fd6 100644 --- a/tools/README.md +++ b/tools/README.md @@ -24,14 +24,15 @@ so it does not live in this directory: | `witnesses/ifc-lite/` | IFClite — the `ifc-lite-core` crate, pinned `=7.1.1` | MPL-2.0 | `witnesses/ifc-lite/` is **mirrored from `tools/ci/witness-ifc-lite/` in -DrunkOnJava/rvt-rs at commit `dbf23473e1be62cb8ed1a4fb552396c31899df9c`** -(PR #205), and the file headers say so. The only change is the +DrunkOnJava/rvt-rs at commit `a0044f4a11361626c2a4b9fb93c4696ac6b1c6e6`** +(PR #230), and the file headers say so. The only change is the manifest-reading code path, which reads the umbrella's §6.1 manifest shape (`bridge.file_hash_sha256` / `bridge.bytes` / `bridge.schema`, and the bridge file passed directly) instead of the rvt-rs project-count fixture shape. The -counting, the canonicalization, the hashing, the manifest-drift check and the -observation shape are unchanged, which is why the two repositories' committed -observations hash identically. Keep them in sync. +counting, the relation and storey readers, the canonicalization, the hashing, +the manifest-drift check and the observation shape are unchanged, which is +why the two repositories' committed observations hash identically. Keep them +in sync. **This is glue, not a decoder.** It is ~300 lines of argument parsing, hashing and JSON. Every byte of IFC is parsed by `ifc-lite-core`, a @@ -53,7 +54,7 @@ own `WITNESS_VERSION` are one number (§9.6). | `verdict.py` | the diff function, the §9.3 independence set, replay, the §10.5 status vocabulary | | `compare-verdict.py` | fresh verdict against the committed record (§13 step 6) | | `replay.sh` | all of the above end to end, for one artifact | -| `validate-corpus.py` | observations and verdicts against the 1.0.0 schemas, plus hash self-consistency | +| `validate-corpus.py` | observations and verdicts against the 1.1.0 schemas, plus hash self-consistency | | `check-registry.py` | registry against its schema, cross-references the schema cannot express, and the §9.6 pin chain for every witness this repository builds | | `index.py` | rebuild or check `corpus/MANIFEST_INDEX.json` (§12.2) | | `jsonschema_mini.py` | the JSON Schema subset the two validators use; not a general engine | @@ -103,6 +104,30 @@ implemented. **A payload with floats or non-ASCII strings would need the full RFC 8785 rules before its hash could be trusted across implementations** — geometry surfaces will need this, and it is not here yet. +That restriction is why a storey elevation travels as a fixed six-decimal +*string* in feet rather than as a JSON number (§7.2, §20.2): three witnesses +on three runtimes must not be asked to print the same `f64` identically. Each +witness resolves its own file's declared `LENGTHUNIT` and renders the result +itself, so the unit handling is inside the claimed surface and a unit bug is +a `DISAGREE`. For the same reason storey names are compared as exact UTF-8 +bytes with **no** Unicode normalization: a normalizer that disagreed between +the three runtimes would launder a real difference into agreement, so a name +differing only in composition form is a diff. Fail-closed, and the strict +direction. + +## Field classes the diff function implements + +| Class | Payload key | Comparison | Diff shape | +|---|---|---|---| +| Entity counts | `entity_counts` | `abs(a - b) <= tolerance` from the manifest category | `value_a` / `value_b` scalars, `tolerance_applied` integer | +| Relation pair sets (1.1.0) | `relations` | exact equality of the sorted multiset; no tolerance concept | `value_a` / `value_b` set sizes, plus `only_in_a` / `only_in_b`, `tolerance_applied: false` | +| Storey sets (1.1.0) | `storeys` | exact equality of the sorted multiset; no tolerance concept | same | + +A witness compared on a set-valued class must declare it in its +observation's `semantic_surface_covered`, or the verdict is +`MANIFEST_ERROR` — a witness that never looked at a field is not a witness +that agreed about it (§9.4). + Any change to canonicalization changes every committed observation hash and is a major version bump of the spec (§16.1). diff --git a/tools/jsonschema_mini.py b/tools/jsonschema_mini.py index cadd024..2dba4cd 100755 --- a/tools/jsonschema_mini.py +++ b/tools/jsonschema_mini.py @@ -10,10 +10,15 @@ Implemented keywords: `type` (including union types and `null`), `required`, `properties`, `additionalProperties` (schema form and `false`), `items`, -`enum`, `const`, `pattern`, `minLength`, `minItems`, `uniqueItems`, -`minimum`. Annotation-only keywords (`$schema`, `$id`, `title`, +`enum`, `const`, `pattern`, `minLength`, `minItems`, `maxItems`, +`uniqueItems`, `minimum`. Annotation-only keywords (`$schema`, `$id`, `title`, `description`) are ignored. +`maxItems` arrived with OctetProof 1.1.0: the observation schema's `relations` +and `storeys` payload keys pin each element to a two-item `[a, b]` array with +`minItems: 2, maxItems: 2`, and without the upper bound a three-element pair +would validate. + Anything else fails loudly: `check()` reports "schema uses constructs this validator does not implement" rather than passing silently. A future schema keyword therefore cannot quietly stop being enforced. @@ -30,7 +35,8 @@ ANNOTATION = {"$schema", "$id", "title", "description", "$comment", "examples", "default"} IMPLEMENTED = { "type", "required", "properties", "additionalProperties", "items", - "enum", "const", "pattern", "minLength", "minItems", "uniqueItems", "minimum", + "enum", "const", "pattern", "minLength", "minItems", "maxItems", + "uniqueItems", "minimum", } SUPPORTED = ANNOTATION | IMPLEMENTED @@ -106,6 +112,8 @@ def check(node, schema, path: str, errors: list[str]) -> None: elif isinstance(node, list): if "minItems" in schema and len(node) < schema["minItems"]: errors.append(f"{path}: {len(node)} items < minItems {schema['minItems']}") + if "maxItems" in schema and len(node) > schema["maxItems"]: + errors.append(f"{path}: {len(node)} items > maxItems {schema['maxItems']}") if schema.get("uniqueItems"): seen = [json.dumps(i, sort_keys=True) for i in node] if len(set(seen)) != len(seen): diff --git a/tools/validate-corpus.py b/tools/validate-corpus.py index cf00afe..376b7e7 100755 --- a/tools/validate-corpus.py +++ b/tools/validate-corpus.py @@ -1,6 +1,10 @@ #!/usr/bin/env python3 """Validate every committed observation and verdict in the corpus against the -OctetProof 1.0.0 schemas (SPEC.md §6.2, §6.3). +OctetProof 1.1.0 schemas (SPEC.md §6.2, §6.3). + +The schemas are additive over 1.0.0 (§16.2): `schema_version` accepts both +`1.0.0` and `1.1.0`, and the `relations` / `storeys` payload keys are optional, +so a document written under either version validates here. Usage: validate-corpus.py [--corpus corpus] [--schemas schemas] [--extra-observations DIR ...] @@ -125,7 +129,7 @@ def main() -> int: print(f"{len(errors)} corpus problem(s)", file=sys.stderr) return 1 - print(f"corpus: {checked_obs} observation(s) and {checked_verdicts} verdict(s) conform to the 1.0.0 schemas") + print(f"corpus: {checked_obs} observation(s) and {checked_verdicts} verdict(s) conform to the 1.1.0 schemas") return 0 diff --git a/tools/verdict.py b/tools/verdict.py index 5b371ae..a732571 100755 --- a/tools/verdict.py +++ b/tools/verdict.py @@ -1,6 +1,6 @@ #!/usr/bin/env python3 """OctetProof verdict: compare independent witness observations of one golden -artifact and emit verdict.json (SPEC.md 1.0.0 §6.3, §7, §9.3, §10.5). +artifact and emit verdict.json (SPEC.md 1.1.0 §6.3, §7, §9.3, §10.5). Usage: verdict.py --out verdict.json [--registry registry/witnesses.json] @@ -18,9 +18,22 @@ `source_ifc_type` with status `known` puts `entity_counts.` inside the surface; status `known_gap` or `unsupported` excludes it first-class (§7.1 rule 3) and records it in the verdict with its tracking issue, never diffed. + +Since 1.1.0 two further manifest blocks are normative in the same way (§20.1, +§20.2). A `relations` category carrying `relation_ifc_type` contributes +`relations.`, and a `storeys` category carrying +`storey_ifc_type` contributes `storeys.`. Both are set-valued +field classes with no tolerance concept (§7.2): agreement is exact equality of +the sorted multiset, and a diff reports each side's set size in `value_a` / +`value_b` plus the members each side holds alone in `only_in_a` / `only_in_b`, +with `tolerance_applied: false`. A witness compared on one of them must +declare it in `semantic_surface_covered`, else MANIFEST_ERROR — a witness that +never looked is not a witness that agreed. + The manifest's declared `semantic_surface` and `excluded` are cross-checked -against what `counts` implies; a mismatch is a MANIFEST_ERROR, so an -accidental status flip cannot silently shrink the surface. +against what `counts`, `relations` and `storeys` together imply; a mismatch is +a MANIFEST_ERROR, so an accidental status flip cannot silently shrink the +surface. Fail-closed (§5.4, §10.5): fewer than two observations → INSUFFICIENT_WITNESSES; any diff inside the surface → DISAGREE; an @@ -39,8 +52,9 @@ observation of the same witness, else REPLAY_DRIFT. Canonicalisation is §7.3: sorted keys, no insignificant whitespace, UTF-8, -SHA-256. The payloads in scope carry integers and ASCII strings only, so -RFC 8785's number rules are moot. +SHA-256. The payloads in scope carry integers and ASCII strings only — a +storey elevation travels as a fixed six-decimal string precisely so that it +stays inside that rule — so RFC 8785's number rules are moot. Python 3.12 standard library only. """ @@ -53,7 +67,21 @@ import sys from pathlib import Path -PREFIX = "entity_counts." +#: The three normative manifest blocks, in the order the surface is built: +#: (payload key, manifest block name, the spec key naming the compared type). +#: `counts` is 1.0.0; `relations` and `storeys` are the additive 1.1.0 field +#: classes (§20.1, §20.2). The order is load-bearing — `semantic_surface` and +#: `excluded` are ordered lists compared verbatim against the committed +#: verdict, so counts come first, then relations, then storeys. +BLOCKS = ( + ("entity_counts", "counts", "source_ifc_type"), + ("relations", "relations", "relation_ifc_type"), + ("storeys", "storeys", "storey_ifc_type"), +) + +#: Payload keys whose field class is a sorted multiset compared for exact +#: equality, with no tolerance concept (§7.2). +SET_VALUED = {"relations", "storeys"} def canonical(obj) -> bytes: @@ -75,30 +103,35 @@ def load_observations(directory: Path) -> dict[str, dict]: return out -def partition(manifest: dict) -> tuple[list[tuple[str, str, int]], list[dict]]: - """Split the normative `counts` block into (surface, excluded). +def partition(manifest: dict) -> tuple[list[tuple[str, str, str, str, int]], list[dict]]: + """Split the normative `counts`, `relations` and `storeys` blocks into + (surface, excluded). - surface entries are (field, category, tolerance); excluded entries are the - §6.3 records. Categories without `source_ifc_type` are not part of the - cross-witness surface at all (§6.1) and are skipped silently. + surface entries are (payload_key, type_name, field, category, tolerance); + excluded entries are the §6.3 records. A category that does not carry its + block's type key is not part of the cross-witness surface at all (§6.1) and + is skipped silently — that is how a decoder-only regression row stays out + of the umbrella's view. `tolerance` is meaningless for the set-valued + classes and is never read for them (§7.2). """ - surface: list[tuple[str, str, int]] = [] + surface: list[tuple[str, str, str, str, int]] = [] excluded: list[dict] = [] - for category, spec in manifest.get("counts", {}).items(): - ifc_type = spec.get("source_ifc_type") - if not ifc_type: - continue - field = PREFIX + ifc_type - if spec.get("status") == "known": - surface.append((field, category, int(spec.get("tolerance", 0)))) - else: - excluded.append({ - "field": field, - "category": category, - "reason": spec.get("status"), - "tracking_issue": spec.get("tracking_issue"), - "unsupported_feature": spec.get("unsupported_feature"), - }) + for payload_key, block_name, type_key in BLOCKS: + for category, spec in manifest.get(block_name, {}).items(): + type_name = spec.get(type_key) + if not type_name: + continue + field = f"{payload_key}.{type_name}" + if spec.get("status") == "known": + surface.append((payload_key, type_name, field, category, int(spec.get("tolerance", 0)))) + else: + excluded.append({ + "field": field, + "category": category, + "reason": spec.get("status"), + "tracking_issue": spec.get("tracking_issue"), + "unsupported_feature": spec.get("unsupported_feature"), + }) return surface, excluded @@ -204,37 +237,75 @@ def main() -> int: # `counts` block they derive from. Disagreement between the two means the # manifest is wrong, and §7.1 rule 3 makes that the manifest's fault. declared_surface = manifest.get("semantic_surface") - if declared_surface is not None and declared_surface != [f for f, _, _ in surface]: + if declared_surface is not None and declared_surface != [f for _, _, f, _, _ in surface]: verdict["status"] = "MANIFEST_ERROR" verdict.setdefault("manifest_errors", []).append( - "declared semantic_surface does not match the `known` categories in counts" + "declared semantic_surface does not match the `known` categories in " + "counts / relations / storeys" ) declared_excluded = manifest.get("excluded") if declared_excluded is not None and declared_excluded != excluded: verdict["status"] = "MANIFEST_ERROR" verdict.setdefault("manifest_errors", []).append( - "declared excluded does not match the non-`known` categories in counts" + "declared excluded does not match the non-`known` categories in " + "counts / relations / storeys" ) - for field, _category, tolerance in surface: + for payload_key, type_name, field, _category, tolerance in surface: verdict["semantic_surface"].append(field) + if payload_key in SET_VALUED: + # A witness that never read a field class cannot have agreed on it + # (§9.4). The declaration is per run, inside the observation. + for wid, obs in observations.items(): + if payload_key not in (obs.get("semantic_surface_covered") or []): + verdict["status"] = "MANIFEST_ERROR" + verdict.setdefault("manifest_errors", []).append( + f"{wid} does not declare {payload_key}" + ) if verdict["status"] != "PASS": continue - ifc_type = field[len(PREFIX):] - values = { - wid: int(obs.get("observation", {}).get("entity_counts", {}).get(ifc_type, 0)) + + if payload_key not in SET_VALUED: + values = { + wid: int(obs.get("observation", {}).get(payload_key, {}).get(type_name, 0)) + for wid, obs in observations.items() + } + wids = sorted(values) + for i, a in enumerate(wids): + for b in wids[i + 1:]: + if abs(values[a] - values[b]) > tolerance: + verdict["diffs"].append({ + "field": field, + "witness_a": a, "value_a": values[a], + "witness_b": b, "value_b": values[b], + "tolerance_applied": tolerance, + }) + continue + + # Set-valued field classes (§7.2, added in 1.1.0): a relation pair set + # or a storey set. Agreement is exact equality of the sorted multiset, + # so a diff names the members each side holds alone rather than a + # scalar delta, and no tolerance is applied. + sets = { + wid: [tuple(member) for member in obs.get("observation", {}).get(payload_key, {}).get(type_name, [])] for wid, obs in observations.items() } - wids = sorted(values) + wids = sorted(sets) for i, a in enumerate(wids): for b in wids[i + 1:]: - if abs(values[a] - values[b]) > tolerance: - verdict["diffs"].append({ - "field": field, - "witness_a": a, "value_a": values[a], - "witness_b": b, "value_b": values[b], - "tolerance_applied": tolerance, - }) + if sorted(sets[a]) == sorted(sets[b]): + continue + only_a = sorted(set(sets[a]) - set(sets[b])) + only_b = sorted(set(sets[b]) - set(sets[a])) + verdict["diffs"].append({ + "field": field, + "witness_a": a, "value_a": len(sets[a]), + "witness_b": b, "value_b": len(sets[b]), + "tolerance_applied": False, + "only_in_a": [list(member) for member in only_a], + "only_in_b": [list(member) for member in only_b], + }) + if verdict["diffs"] and verdict["status"] == "PASS": verdict["status"] = "DISAGREE" @@ -262,6 +333,9 @@ def main() -> int: f"excluded: {len(verdict['excluded'])}, diffs: {len(verdict['diffs'])}") for d in verdict["diffs"]: print(f" DISAGREE {d['field']}: {d['witness_a']}={d['value_a']} vs {d['witness_b']}={d['value_b']}") + if "only_in_a" in d: + print(f" only in {d['witness_a']}: {len(d['only_in_a'])} → {d['only_in_a'][:5]}") + print(f" only in {d['witness_b']}: {len(d['only_in_b'])} → {d['only_in_b'][:5]}") for wid, r in verdict.get("replay", {}).items(): print(f" replay {wid}: {r}") for m in verdict.get("manifest_errors", []): diff --git a/tools/witness-ifcopenshell.py b/tools/witness-ifcopenshell.py index a0cadc5..78ebb0d 100755 --- a/tools/witness-ifcopenshell.py +++ b/tools/witness-ifcopenshell.py @@ -5,20 +5,26 @@ Usage: witness-ifcopenshell.py --observation observations/ifcopenshell.json -The manifest (§6.1) is the only source of truth for what is counted: every -`counts` category carrying a `source_ifc_type` is counted, whatever its -status. Excluded (`known_gap` / `unsupported`) types are counted and recorded -too — the verdict tool is what refuses to diff them (§7.1 rule 3), not the -witness. A witness that silently dropped them would make the exclusion -unauditable. +The manifest (§6.1) is the only source of truth for what is read: every +`counts` category carrying a `source_ifc_type` is counted, every `relations` +category carrying a `relation_ifc_type` contributes its pair set and every +`storeys` category carrying a `storey_ifc_type` contributes its storey set, +whatever the category's status. Excluded (`known_gap` / `unsupported` / +`decoder_baseline`) categories are read and recorded too — the verdict tool is +what refuses to diff them (§7.1 rule 3), not the witness. A witness that +silently dropped them would make the exclusion unauditable. The bridge file's SHA-256 must equal `bridge.file_hash_sha256` (a golden artifact is the exact bytes the manifest names) and its schema must equal `bridge.schema`. Counts use `by_type(..., include_subtypes=False)` — exact type, no subtypes. -The payload is canonicalised (sorted keys, no whitespace, UTF-8) and hashed -so a replay can prove the witness saw the same thing. +Relation pair sets and storey sets are the 1.1.0 field classes (§7.2, §20.1, +§20.2): sorted multisets of two-element string pairs, with a storey elevation +resolved through this model's own declared `LENGTHUNIT` and rendered in feet +at 1e-6 as a fixed six-decimal string. The payload is canonicalised (sorted +keys, no whitespace, UTF-8) and hashed so a replay can prove the witness saw +the same thing. Dependencies: Python 3.12 standard library + ifcopenshell (>=0.8,<0.9). """ @@ -32,6 +38,76 @@ from pathlib import Path import ifcopenshell +import ifcopenshell.util.unit + + +def fills_element_pairs(model) -> list[list[str]]: + """`IfcRelFillsElement` host/filling `Tag` pairs, canonically sorted. + + The chain is Revit's own: `IfcRelVoidsElement` binds an opening to the + element it voids, `IfcRelFillsElement` binds that opening to the element + that fills it, so the pair `[host Tag, filling Tag]` is the door/window + to host-wall relation as an IFC reader sees it (SPEC.md §7.2, field class + *relation pair sets*). + + An unset `Tag`, or an opening with no `IfcRelVoidsElement`, contributes an + empty string rather than dropping the pair: a missing half must surface as + a disagreement, never as a silent omission. Duplicates are kept, so the + value is a sorted multiset. + """ + voided_by = {} + for rel in model.by_type("IfcRelVoidsElement", include_subtypes=False): + voided_by[rel.RelatedOpeningElement.id()] = rel.RelatingBuildingElement + + def tag_of(entity) -> str: + return "" if entity is None else (getattr(entity, "Tag", None) or "") + + pairs = [] + for rel in model.by_type("IfcRelFillsElement", include_subtypes=False): + host = voided_by.get(rel.RelatingOpeningElement.id()) + pairs.append([tag_of(host), tag_of(rel.RelatedBuildingElement)]) + return sorted(pairs) + + +RELATION_READERS = {"IFCRELFILLSELEMENT": fills_element_pairs} + +#: Metres in one international foot, exactly. +METRES_PER_FOOT = 0.3048 + + +def format_elevation_feet(feet: float) -> str: + """Render an elevation in feet at 1e-6 ft, as a string. + + A string, not a JSON number, because the canonical form (SPEC.md §7.3) is + defined over integers and strings only — two runtimes must not be trusted + to print the same float the same way. `-0` normalises to `0`. + """ + rendered = f"{feet:.6f}" + return "0.000000" if rendered == "-0.000000" else rendered + + +def building_storey_set(model, ifc_type: str) -> list[list[str]]: + """`IfcBuildingStorey` `[Name, Elevation]` pairs, canonically sorted. + + The elevation is converted from the model's declared `LENGTHUNIT` to feet, + so the field compares across witnesses whose files declare different units + — Revit's own export of this corpus declares `FOOT` while rvt-rs writes + `METRE` (SPEC.md §7.2, field class *storey sets*). An unset `Name` or + `Elevation` contributes an empty string rather than dropping the storey: a + missing half must surface as a disagreement, never as a silent omission. A + model with no resolvable length unit yields an empty set rather than a + guess — an unmeasured storey is not a measurement. + """ + scale = ifcopenshell.util.unit.calculate_unit_scale(model) + if not scale: + return [] + out = [] + for storey in model.by_type(ifc_type, include_subtypes=False): + name = storey.Name or "" + elevation = getattr(storey, "Elevation", None) + feet = "" if elevation is None else format_elevation_feet(elevation * scale / METRES_PER_FOOT) + out.append([name, feet]) + return sorted(out) def sha256_of(path: Path) -> str: @@ -94,14 +170,40 @@ def main() -> int: except RuntimeError: counts[ifc_type] = 0 # type absent from this schema → zero instances - payload = {"entity_counts": counts, "ifc_schema": model.schema} + relations: dict[str, list[list[str]]] = {} + for category, spec in manifest.get("relations", {}).items(): + relation_type = spec.get("relation_ifc_type") + if not relation_type: + continue + reader = RELATION_READERS.get(relation_type) + if reader is None: + print(f"error: {category}: no reader for relation type {relation_type}", file=sys.stderr) + return 1 + relations[relation_type] = reader(model) + + storeys: dict[str, list[list[str]]] = {} + for category, spec in manifest.get("storeys", {}).items(): + storey_type = spec.get("storey_ifc_type") + if not storey_type: + continue + if storey_type != "IFCBUILDINGSTOREY": + print(f"error: {category}: no reader for storey type {storey_type}", file=sys.stderr) + return 1 + storeys[storey_type] = building_storey_set(model, storey_type) + + payload = { + "entity_counts": counts, + "relations": relations, + "storeys": storeys, + "ifc_schema": model.schema, + } canonical = json.dumps(payload, sort_keys=True, separators=(",", ":"), ensure_ascii=False).encode("utf-8") origin = bridge.get("origin") or "" input_file = origin.rsplit("/", 1)[-1] or args.bridge_file.name observation = { - "schema_version": "1.0.0", + "schema_version": "1.1.0", "witness_id": "ifcopenshell", "witness_version": str(getattr(ifcopenshell, "version", "?")), "artifact_id": manifest.get("artifact_id"), @@ -109,7 +211,7 @@ def main() -> int: "input_file": input_file, "input_hash_sha256": actual_sha, "deterministic": True, - "semantic_surface_covered": ["entity_counts"], + "semantic_surface_covered": ["entity_counts", "relations", "storeys"], "observation": payload, "observation_hash_sha256": hashlib.sha256(canonical).hexdigest(), "unsupported_entities": [], @@ -122,6 +224,10 @@ def main() -> int: print(f"{'ifc type':<24} {'count':>6}") for ifc_type in types: print(f"{ifc_type:<24} {counts[ifc_type]:>6}") + for relation_type, pairs in relations.items(): + print(f"{relation_type:<24} {len(pairs):>6} pairs") + for storey_type, pairs in storeys.items(): + print(f"{storey_type:<24} {len(pairs):>6} storeys") print(f"observation_hash_sha256: {observation['observation_hash_sha256']}") return 0 diff --git a/witnesses/ifc-lite/Cargo.toml b/witnesses/ifc-lite/Cargo.toml index 731c10c..45786fa 100644 --- a/witnesses/ifc-lite/Cargo.toml +++ b/witnesses/ifc-lite/Cargo.toml @@ -3,10 +3,11 @@ name = "witness-ifc-lite" version = "0.1.0" edition = "2021" # Mirrored from tools/ci/witness-ifc-lite/ in DrunkOnJava/rvt-rs at commit -# dbf23473e1be62cb8ed1a4fb552396c31899df9c (PR #205). Only the +# a0044f4a11361626c2a4b9fb93c4696ac6b1c6e6 (PR #230). Only the # manifest-reading code path in src/main.rs differs, to read the umbrella's # §6.1 manifest shape instead of the rvt-rs project-count fixture; the pin, -# the counting, the canonicalization and the observation shape are unchanged. +# the counting, the relation and storey readers, the canonicalization and the +# observation shape are unchanged. # # NOT the rvt-rs workspace MSRV (1.85). ifc-lite-core 7.1.1 calls # `usize::is_multiple_of`, stabilised in Rust 1.87, so this witness needs a diff --git a/witnesses/ifc-lite/src/main.rs b/witnesses/ifc-lite/src/main.rs index 5c0439e..7e390e2 100644 --- a/witnesses/ifc-lite/src/main.rs +++ b/witnesses/ifc-lite/src/main.rs @@ -5,30 +5,36 @@ //! --observation observations/ifc-lite.json [--json OUT] //! //! **Mirrored from `tools/ci/witness-ifc-lite/` in DrunkOnJava/rvt-rs at commit -//! `dbf23473e1be62cb8ed1a4fb552396c31899df9c` (PR #205).** The only change is +//! `a0044f4a11361626c2a4b9fb93c4696ac6b1c6e6` (PR #230).** The only change is //! the manifest-reading code path: rvt-rs resolves `reference_ifc_file` under a //! corpus directory and checks `source.reference_ifc_sha256`, while the //! umbrella is handed the bridge file directly and checks //! `bridge.file_hash_sha256`, `bridge.bytes` and `bridge.schema` — exactly the //! contract `tools/witness-ifcopenshell.py` already implements here. Counting, -//! canonicalization, hashing, the manifest-drift check and the observation -//! shape are byte-for-byte the upstream behaviour. Keep the two in sync. +//! the relation and storey readers, canonicalization, hashing, the +//! manifest-drift check and the observation shape are byte-for-byte the +//! upstream behaviour. Keep the two in sync. //! //! **This is glue, not a decoder.** The umbrella parses no format bytes itself; //! `ifc-lite-core` — a separate, MPL-2.0, out-of-tree crate — does, inside this //! separate process, and never linked into anything Apache-2.0. //! -//! The manifest (§6.1) is the only source of truth for what is counted: every -//! `counts` category carrying a `source_ifc_type` is counted, whatever its -//! status. Excluded (`known_gap` / `unsupported` / `decoder_baseline`) types -//! are counted and recorded too — the verdict tool is what refuses to diff them -//! (§7.1 rule 3), not the witness. A witness that silently dropped them would -//! make the exclusion unauditable. +//! The manifest (§6.1) is the only source of truth for what is read: every +//! `counts` category carrying a `source_ifc_type` is counted, every `relations` +//! category carrying a `relation_ifc_type` contributes its `[host Tag, +//! filling Tag]` pair set and every `storeys` category carrying a +//! `storey_ifc_type` contributes its `[Name, Elevation]` set in feet, whatever +//! the category's status. Excluded (`known_gap` / `unsupported` / +//! `decoder_baseline`) categories are read and recorded too — the verdict tool +//! is what refuses to diff them (§7.1 rule 3), not the witness. A witness that +//! silently dropped them would make the exclusion unauditable. //! //! Counts use `ifc_lite_core::EntityScanner` (exact STEP keyword, no subtypes — //! the same semantics as IfcOpenShell's `by_type(..., include_subtypes=False)`) -//! and are compared to `expected` within `tolerance`. Exit 1 on any drift or -//! hash mismatch. +//! and are compared to `expected` within `tolerance`. Relation pair sets and +//! storey sets are the 1.1.0 field classes (§7.2, §20.1, §20.2), compared to +//! `expected_pairs` / `expected_storeys` exactly. Exit 1 on any drift or hash +//! mismatch. //! //! This is the third implementation lineage on the RVT → IFC edge: rvt-rs reads //! the .rvt (source witness), IfcOpenShell and IFClite each read Revit's .ifc @@ -40,7 +46,10 @@ use std::io::{BufReader, Read}; use std::path::{Path, PathBuf}; use std::process::ExitCode; -use ifc_lite_core::EntityScanner; +use ifc_lite_core::{ + build_entity_index, decode_ifc_string, extract_length_unit_scale, parse_entity, EntityDecoder, + EntityScanner, Token, +}; use serde_json::{json, Map, Value}; use sha2::{Digest, Sha256}; @@ -105,6 +114,171 @@ fn count_by_exact_type(bytes: &[u8]) -> BTreeMap { counts } +/// Attribute index of `Tag` on every `IfcElement` subtype in IFC4. +/// +/// `IfcElement` adds exactly one attribute to `IfcProduct`'s seven, so the +/// index is the same for `IfcWall`, `IfcDoor`, `IfcWindow` and every other +/// element type. +const IFC_ELEMENT_TAG_INDEX: usize = 7; + +/// Decode a `Token::String` payload: undouble STEP apostrophes, then run the +/// crate's `\X…\` decoder. +fn token_string(raw: &[u8]) -> String { + let text = String::from_utf8_lossy(raw).replace("''", "'"); + decode_ifc_string(&text).into_owned() +} + +/// `IfcRelFillsElement` host/filling `Tag` pairs, canonically sorted. +/// +/// The chain is Revit's own: `IfcRelVoidsElement` binds an opening to the +/// element it voids, `IfcRelFillsElement` binds that opening to the element +/// that fills it, so the pair `[host Tag, filling Tag]` is the door/window to +/// host-wall relation as an IFC reader sees it (SPEC.md §7.2, field class +/// *relation pair sets*). +/// +/// An unset `Tag`, or an opening with no `IfcRelVoidsElement`, contributes an +/// empty string rather than dropping the pair: a missing half must surface as +/// a disagreement, never as a silent omission. Duplicates are kept, so the +/// value is a sorted multiset. +/// +/// This is the third lineage's own read of the same relation: the scan is +/// `EntityScanner` + `parse_entity`, sharing no code with IfcOpenShell or +/// with the rvt-rs source witness's line splitter. +fn fills_element_pairs(bytes: &[u8]) -> Vec> { + let mut tags: BTreeMap = BTreeMap::new(); + let mut voided_by: BTreeMap = BTreeMap::new(); + let mut fills: Vec<(Option, Option)> = Vec::new(); + + let mut scanner = EntityScanner::new(bytes); + while let Some((id, type_name, start, end)) = scanner.next_entity() { + let upper = type_name.trim().to_uppercase(); + let is_voids = upper == "IFCRELVOIDSELEMENT"; + let is_fills = upper == "IFCRELFILLSELEMENT"; + if !is_voids && !is_fills && !upper.starts_with("IFC") { + continue; + } + let Ok((_, _, args)) = parse_entity(&bytes[start..end]) else { + continue; + }; + let entity_ref = |index: usize| match args.get(index) { + Some(Token::EntityRef(reference)) => Some(*reference), + _ => None, + }; + if is_voids { + // (GlobalId, OwnerHistory, Name, Description, + // RelatingBuildingElement, RelatedOpeningElement) + if let (Some(host), Some(opening)) = (entity_ref(4), entity_ref(5)) { + voided_by.insert(opening, host); + } + continue; + } + if is_fills { + // (GlobalId, OwnerHistory, Name, Description, + // RelatingOpeningElement, RelatedBuildingElement) + fills.push((entity_ref(4), entity_ref(5))); + continue; + } + if let Some(Token::String(raw)) = args.get(IFC_ELEMENT_TAG_INDEX) { + tags.insert(id, token_string(raw)); + } + } + + let tag_of = |id: Option| -> String { + id.and_then(|id| tags.get(&id).cloned()).unwrap_or_default() + }; + let mut pairs: Vec> = fills + .into_iter() + .map(|(opening, filling)| { + let host = opening.and_then(|id| voided_by.get(&id).copied()); + vec![tag_of(host), tag_of(filling)] + }) + .collect(); + pairs.sort(); + pairs +} + +/// Attribute index of `Name` on `IfcBuildingStorey` (`IfcRoot` +2). +const IFC_BUILDING_STOREY_NAME_INDEX: usize = 2; + +/// Attribute index of `Elevation` on `IfcBuildingStorey`: the tenth and last +/// attribute of the IFC4 entity. +const IFC_BUILDING_STOREY_ELEVATION_INDEX: usize = 9; + +/// Metres in one international foot, exactly. +const METRES_PER_FOOT: f64 = 0.3048; + +/// Render an elevation in feet at 1e-6 ft, as a string. +/// +/// A string, not a JSON number, because the canonical form (SPEC.md §7.3) is +/// defined over integers and strings only — two runtimes must not be trusted +/// to print the same `f64` the same way. `-0` normalises to `0`. +fn format_elevation_feet(feet: f64) -> String { + if !feet.is_finite() { + return String::new(); + } + let rendered = format!("{feet:.6}"); + if rendered == "-0.000000" { + return "0.000000".to_string(); + } + rendered +} + +/// `IfcBuildingStorey` `[Name, Elevation]` pairs, canonically sorted. +/// +/// The elevation is converted from the model's declared `LENGTHUNIT` to feet +/// with `ifc_lite_core::extract_length_unit_scale` — this lineage's own unit +/// resolver — so the field compares across witnesses whose files declare +/// different units: Revit's own export of this corpus declares `FOOT` while +/// rvt-rs writes `METRE` (SPEC.md §7.2, field class *storey sets*). +/// +/// An unset `Name` or `Elevation` contributes an empty string rather than +/// dropping the storey: a missing half must surface as a disagreement, never +/// as a silent omission. An empty list is returned when the model declares no +/// resolvable length unit — a storey whose unit is unknown is not a +/// measurement. +fn building_storey_set(bytes: &[u8]) -> Vec> { + let index = build_entity_index(bytes); + let mut decoder = EntityDecoder::with_index(bytes, index); + let mut project_id = None; + let mut storeys: Vec<(u32, usize, usize)> = Vec::new(); + let mut scanner = EntityScanner::new(bytes); + while let Some((id, type_name, start, end)) = scanner.next_entity() { + match type_name.trim().to_uppercase().as_str() { + "IFCPROJECT" if project_id.is_none() => project_id = Some(id), + "IFCBUILDINGSTOREY" => storeys.push((id, start, end)), + _ => {} + } + } + let Some(project_id) = project_id else { + return Vec::new(); + }; + let Ok(scale) = extract_length_unit_scale(&mut decoder, project_id) else { + return Vec::new(); + }; + let mut out: Vec> = Vec::new(); + for (_, start, end) in storeys { + let Ok((_, _, args)) = parse_entity(&bytes[start..end]) else { + continue; + }; + let name = match args.get(IFC_BUILDING_STOREY_NAME_INDEX) { + Some(Token::String(raw)) => token_string(raw), + _ => String::new(), + }; + let elevation = match args.get(IFC_BUILDING_STOREY_ELEVATION_INDEX) { + Some(Token::Float(value)) => Some(*value), + Some(Token::Integer(value)) => Some(*value as f64), + _ => None, + }; + let feet = match elevation { + Some(value) => format_elevation_feet(value * scale / METRES_PER_FOOT), + None => String::new(), + }; + out.push(vec![name, feet]); + } + out.sort(); + out +} + struct Args { manifest: PathBuf, bridge_file: PathBuf, @@ -266,6 +440,96 @@ fn run() -> Result { ); } + // `relations` — the 1.1.0 field class (§7.2, §20.1). Read for every + // manifest category whatever its status, so an excluded relation stays + // auditable; only the verdict decides what is diffed. + let empty_relations = Map::new(); + let relation_categories = manifest + .get("relations") + .and_then(Value::as_object) + .unwrap_or(&empty_relations); + let mut relations = Map::new(); + let mut relation_records = Vec::new(); + for (category, spec) in relation_categories { + let relation_type = spec + .get("relation_ifc_type") + .and_then(Value::as_str) + .ok_or_else(|| format!("{category}: relations entry needs relation_ifc_type"))?; + if relation_type != "IFCRELFILLSELEMENT" { + return Err(format!( + "{category}: no reader for relation type {relation_type}" + )); + } + let pairs = fills_element_pairs(&bytes); + let expected = spec + .get("expected_pairs") + .and_then(Value::as_i64) + .unwrap_or(0); + let actual = pairs.len() as i64; + let ok = actual == expected; + if !ok { + drift += 1; + } + println!( + "{category:<16} {relation_type:<22} {expected:>8} {actual:>8} {:>4} {}", + 0, + if ok { "ok" } else { "DRIFT" } + ); + relation_records.push(json!({ + "category": category, + "relation_ifc_type": relation_type, + "expected_pairs": expected, + "ifc_lite_pairs": actual, + "agree": ok, + })); + relations.insert(relation_type.to_string(), json!(pairs)); + } + + // `storeys` — the second 1.1.0 field class (§7.2, §20.2). The elevation is + // normalised to feet through this file's own declared LENGTHUNIT before it + // leaves the witness, which is what makes the field comparable at all. + let empty_storeys = Map::new(); + let storey_categories = manifest + .get("storeys") + .and_then(Value::as_object) + .unwrap_or(&empty_storeys); + let mut storeys = Map::new(); + let mut storey_records = Vec::new(); + for (category, spec) in storey_categories { + let storey_type = spec + .get("storey_ifc_type") + .and_then(Value::as_str) + .ok_or_else(|| format!("{category}: storeys entry needs storey_ifc_type"))?; + if storey_type != "IFCBUILDINGSTOREY" { + return Err(format!( + "{category}: no reader for storey type {storey_type}" + )); + } + let pairs = building_storey_set(&bytes); + let expected = spec + .get("expected_storeys") + .and_then(Value::as_i64) + .unwrap_or(0); + let actual = pairs.len() as i64; + let ok = actual == expected; + if !ok { + drift += 1; + } + println!( + "{category:<16} {storey_type:<22} {expected:>8} {actual:>8} {:>4} {}", + 0, + if ok { "ok" } else { "DRIFT" } + ); + storey_records.push(json!({ + "category": category, + "storey_ifc_type": storey_type, + "expected_storeys": expected, + "ifc_lite_storeys": actual, + "agree": ok, + })); + storeys.insert(storey_type.to_string(), json!(pairs)); + } + if let Some(path) = args.json.as_ref() { let record = json!({ "schema_version": 1, @@ -275,6 +539,8 @@ fn run() -> Result { "ifc_schema": schema, "witness": format!("{WITNESS_ID} {WITNESS_VERSION}"), "categories": records, + "relations": relation_records, + "storeys": storey_records, "agree": drift == 0, }); write_json(path, &record)?; @@ -283,10 +549,12 @@ fn run() -> Result { if let Some(path) = args.observation.as_ref() { let payload = json!({ "entity_counts": Value::Object(entity_counts), + "relations": Value::Object(relations), + "storeys": Value::Object(storeys), "ifc_schema": schema, }); let observation = json!({ - "schema_version": "1.0.0", + "schema_version": "1.1.0", "witness_id": WITNESS_ID, "witness_version": WITNESS_VERSION, "artifact_id": manifest.get("artifact_id").cloned().unwrap_or(Value::Null), @@ -294,7 +562,7 @@ fn run() -> Result { "input_file": input_file, "input_hash_sha256": actual_sha, "deterministic": true, - "semantic_surface_covered": ["entity_counts"], + "semantic_surface_covered": ["entity_counts", "relations", "storeys"], "observation": payload, "observation_hash_sha256": canonical_hash(&payload), "unsupported_entities": [], @@ -310,7 +578,9 @@ fn run() -> Result { ); return Ok(1); } - println!("cross-witness: IFClite agrees with the manifest for every source_ifc_type"); + println!( + "cross-witness: IFClite agrees with the manifest for every source_ifc_type, relation and storey set" + ); Ok(0) }