Corpus crosscheck — 3.1 structural recapture (2026-07-28)
Human review artifact for the regeneration that replaced the migrated-zip capture with a parser reading the official IFC-SG glossary pages.
| Before | After | |
|---|---|---|
| Components | 74 | 74 |
| IFC bindings | 145 | 153 |
| Parameters | 687 | 723 |
Parameters with allowed_values |
206 | 213 |
Parameters with reference_values |
51 | 52 |
| Input-limited parameters still awaiting values | 3 | 4 |
How this was produced
python scripts/parse_glossary.py— parses the structural layer (bindings, subtypes, S/N, property, type, applies_to, unit, input_limited, examples) out of the cached official glossary pages. Carries the non-derivable fields forward from the previousparameters.json(discipline, authorities, source*, page_range,header_kind, bindingpages, and the editorial annotationsexample_format_acmv/prefix/glossary_key/glossary_mandatory).python scripts/resolve_enums.py— re-applies the value layer from_meta/enum_resolutions.json. The parser never writesallowed_values/reference_values.scratchpad/validate_against_cop.py— cross-validates every parsed property against the CoP v3.1 PDF (sha256 1f9f7197…fbc2a, now recorded inmanifest.json).python scripts/validate.py→ 0 errors, 0 warnings.python scripts/build.pytwice → byte-identical output.
Anti-silent-drop contract. A dropped row is the failure that produced the previous capture's losses, so the parser checks for it three ways, two of them measured independently of the code doing the work:
<tr>census — each table's expanded grid row count is compared against a plain count of<tr>tags in the source HTML, so a row lost during rowspan/colspan expansion cannot pass.- S/N continuity — the official page numbers its own rows, so the parameters read off one table must carry S/N 1..n with no gaps. This catches a real data row misread as blank, as a repeated header, or as a footnote. It is the check that would have caught the previous capture: Lift's two surviving rows were S/N 2 and 3 of 8.
- Row accounting — every row below a header must be classified as exactly one of blank / repeated header / declarative sentinel / parameter.
- Blank-cell resolution — an empty (as opposed to
-) requirement cell is a page defect and must be answered from the CoP PDF; an unanswered one aborts. See below. The guarded set includes Input Limitation, the column that decides whether a hardallowed_valuesconstraint attaches at all — a blank there can no longer default to "not limited", and an unrecognised value aborts rather than falling through tofalse. - No silent exemptions — a table whose S/N column cannot be established aborts instead of skipping the continuity check, and the Pollution Control listing table gets the same row reconciliation as the main tables.
All ten guards are verified against synthetic failing inputs on every change.
Those, plus any unparseable table or unrecognised Input Limitation value, abort the entire
run with the component named. A partial or degraded file is never written. All three guards
plus five others were verified against synthetic failing inputs (8/8).
Components added / removed
None. The official index lists 75 components; the corpus holds 74. The gap is fully
explained and unchanged: Space Usage (Others) is not a separate component here — it is the
page that publishes the 31-property IfcSpace / SPACE table that arc/space_usage already
carries. The parser reads that component's structure from page
d0a72875-f11a-4a53-9a1b-bfd84b9ea775 while keeping source_id on the parent Space Usage page
(this is declared in arc/space_usage/parameters.json → glossary_pages).
Properties added (36 net)
| Component | Added | Source |
|---|---|---|
| Lift | BarrierFreeAccessbility, ClearDepth, ClearHeight, ClearWidth, FireFightingLift, LiftType |
glossary page + CoP p.310 (2 → 8 properties) |
| Pile | DA1-1_CompressionCapacity, DA1-1_CompressionDesignLoad, DA1-1_TensionCapacity, DA1-1_TensionDesignLoad, DA1-2_CompressionCapacity, DA1-2_CompressionDesignLoad, DA1-2_TensionCapacity, DA1-2_TensionDesignLoad, MinEmbedmentIntoBearingLayer_SPT_MoreThan_100N, MinEmbedmentIntoBearingLayer_SPT_MoreThan_60N |
glossary page + CoP pp.316–317 |
| Footing / Pilecap | DA1-1_BearingCapacity, DA1-2_BearingCapacity, WorkingLoad_DA1-1, WorkingLoad_DA1-2 |
glossary page + CoP p.295 |
| Column | WorkingLoad_DA1-1, WorkingLoad_DA1-2 |
glossary page + CoP p.269 |
| Wall | WorkingLoad_DA1-1, WorkingLoad_DA1-2 |
glossary page + CoP p.435 |
| Staircase | ConstructionMethod, MaterialGrade on the second IfcStair binding (15 → 17 params) |
glossary page |
| Road | LoadingCapacity |
glossary page (carries footnote ^) |
| Tank | CompactionRatio |
glossary page |
| Refuse Chute / Recyclables Chute | NominalCapacity |
glossary page (carries footnote ^) |
| Space (Area Requirements) | ACN_ConnectivityType (replaces the misspelled ACN_ConncectivityType) |
glossary page; the CoP PDF p.363 carries the same misspelling — the live page corrects it |
Properties removed (1)
| Component | Removed | Why |
|---|---|---|
| Pile | Real |
Not a property. The previous capture mis-read two wrapped table rows and emitted a parameter literally named Real (property_raw: "Real MoreThan_100N MinEmbedmentIntoBearingLayer_SPT_"). Those rows now parse correctly as MinEmbedmentIntoBearingLayer_SPT_MoreThan_100N and …_60N. |
Binding changes
| Component | Before | After | Reading |
|---|---|---|---|
| Accessible Route | 1 binding, IfcSpace |
5 bindings — IfcBuildingElementProxy, IfcSlab, IfcCivilElement, IfcRamp, IfcSpace (all ACCESSIBLEROUTE, same 2 properties) |
⚠️ Live/CoP conflict — CoP p.251 lists only IfcSpace. The live page's banner names five entities; kept the live reading. |
| Space (Area Requirements) | 1 merged binding, 23 params | 5 bindings, 8+4+7+3+1 = 23 params | Presentation change only. The page publishes five separate IfcSpace / AREA_GFA tables (AGF, AST, ACN, ALS, AVF); one binding per table keeps the official S/N verbatim instead of restarting it inside a single binding. prefix_groups still documents the five groups. |
| Fire Access Opening | subtype OPENING ×3 |
subtype null ×3 |
⚠️ Live/CoP conflict — the live SubType cell reads -; CoP p.287 reads OPENING. |
| Slab | FLOOR, LANDING |
N.A., LANDING |
⚠️ Live/CoP conflict — CoP p.353 reads N.A., FLOOR, LANDING; the live page omits FLOOR. |
| Window | …, WINDOW, SKYLIGHT |
…, WINDOW |
⚠️ Live/CoP conflict — CoP p.439 includes SKYLIGHT; the live page omits it. |
| Refuse Chute / Recyclables Chute | IfcTank …, REFUSEHANDLINGEQUIPMENT |
IfcTank … (subtype dropped) |
⚠️ Live/CoP conflict — CoP p.335 has it here. It now appears on the Tank page instead (next row). |
| Tank | IfcTank …, SECTIONAL, VESSEL, … |
…, SECTIONAL, REFUSEHANDLINGEQUIPMENT, VESSEL, … |
⚠️ Live/CoP conflict — CoP p.428 does not list it on Tank. Read together with the row above, the live pages appear to have moved this subtype. |
| Prefabricated Building Systems and MEP Components | IfcSpace, 5 subtypes |
IfcSpace, 9 subtypes (+PREFABRICATEDANDPREFINISHEDWALL / FLOOR / CEILING, +PRECASTEXTERNALWALLWITHCAST-INWINDOWS) |
⚠️ Live-only — the four added subtypes appear nowhere in the CoP v3.1 PDF. |
| Prefabricated Building Systems and MEP Components | IfcPipeSegment/IfcDuctSegment carried RIGIDSEGMENT, FLEXIBLESEGMENT; the Fitting entities carried none |
IfcPipeFitting and IfcPipeSegment carry RIGIDSEGMENT, FLEXIBLESEGMENT; IfcDuctFitting/IfcDuctSegment carry none |
⚠️ Recorded as published. The live page prints one SubType banner over a two-entity table (IfcPipeFitting, IfcPipeSegment), so both entities inherit it; the previous capture had editorially assigned the segment subtypes to the segment entity only. Needs a human ruling — RIGIDSEGMENT/FLEXIBLESEGMENT are not IfcPipeFitting predefined types. |
| Railing | BOLLARD, GUARDRAIL |
N.A., BOLLARD, GUARDRAIL |
Fix. CoP p.331 reads N.A., BOLLARD, GUARDRAIL; the previous capture dropped the leading N.A. alternative. |
| Road | PARKINGACCESSWAY*, … |
PARKINGACCESSWAY, … |
Fix — the * was a footnote marker, not part of the subtype. |
| Fire Alarm | subtypes alphabetised | subtypes in page order | Ordering only; same 5 values. |
Multi-entity table banners (IFC Entity: IfcWall, IfcSlab) are split into one binding per
entity, which is the convention the previous corpus already used for Fire Access Opening,
Household Shelter, Pipes/Ducts and Prefabricated. Accessible Route is the only component where
applying that convention uniformly changes the binding count.
Type / unit / input-limited changes
| Component | Property | Field | Before | After | Reading |
|---|---|---|---|---|---|
| Distribution Chamber | InvertLevel, TopLevel |
type | Real |
Text |
Live page and CoP agree on Text; also makes these consistent with the identical Interceptor fields. |
| Wall | PrefinishedFacade |
type | Boolean |
Boolean |
Live page prints a truncated Boole; normalised to Boolean per CoP p.435. |
| Space (Area Requirements) | AVF_IncludeAsGFA |
type | Boolean |
Boolean |
Live page prints Booelan; normalised per CoP p.373. |
| Tank | CompactionRatio |
type | (new property) | Text |
⚠️ Live/CoP conflict — the live Tank page states Text; the PDF states Real (p.335, under Refuse Chute). Recorded as published; see the subtype move above. |
| Refuse Chute / Recyclables Chute | StructuralWidth, StructuralHeight, Thickness |
unit | (none) | mm |
Recovered — the previous capture had mis-aligned these rows (their examples read ["No"]). |
| Interceptor | SystemType |
unit | (none) | mm |
⚠️ Live page prints Unit mm on a Text SystemType row; the CoP has -. Recorded as published — looks like a page data error. |
| Site Boundary | Area |
unit | m2 |
m² |
Live page uses the superscript character. |
| Site Boundary | BroadLandUse, VacantLand |
applies_to | IFC SubType = SITEBOUNDARY only |
(dropped) | The live table has no "Type of Elements" column; that text is a footnote, not an element scope. |
| Parking Lot / Pump / Window | VehicleType, Duty, Standby, FireAccessOpening |
unit | N.A. |
(dropped) | N.A. is the page's "not applicable" marker, not a unit. |
| Pile | 9 properties | applies_to | fragments (relevant, piles, All piles When required /) |
full values (when required / relevant, RC non-circular piles, All piles) |
Fix — the previous capture split wrapped cells across rows. |
| Staircase | FireExit |
applies_to | When required / relevant |
When required/ relevant |
Live page spacing, verbatim. |
| Roof | ConstructionMethod (IfcSlab) |
input_limited | true |
false |
Both the live page and CoP p.339 print Input Limitation = No, so the structural-enum list no longer applies as a hard constraint. ⚠️ The page is self-inconsistent (its example says "Refer to list*") — flagged for a human ruling. |
| System | SystemType (all 15 bindings) |
input_limited | true |
false |
Live page prints No. The 18 official System Types are now attached as reference_values (soft) rather than allowed_values (hard) — no values lost. ⚠️ Same self-inconsistency; needs a ruling. |
examples were rebuilt for every component. The previous capture read the CoP PDF's
multi-column example cells row-wise and produced garbled lists — Space (Usage)
EmergencyVoiceCommunicationSystem was ["Address System."] and is now
["1-way EVC System", "2-way EVC System", "Public Address System."]; Column SpliceDetail was
["Detail 3 1st Storey", "F2"] and is now ["Detail 3"]; Space (Area Requirements)
AGF_DevelopmentUse was a 25-item interleave of four PDF columns and is now the official
25-value list in page order. Example cells are stored split on commas, which render.py
re-joins with ", ".
Blank cells on the live page, filled from the CoP PDF
A requirement cell holding - is the page saying "not applicable". A cell that is
empty is a rendering defect, and the two must not be conflated: an earlier pass of this
regeneration wrote type: null for Column Stirrups / StirrupsType because it treated
them the same. The parser now separates them, and every empty cell must be answered by a
CoP-sourced entry (LIVE_CELL_GAPS in scripts/parse_glossary.py, each quoting its PDF
row) or the run aborts — a future recrawl cannot quietly open a new hole.
Six cells corpus-wide are blank. Two carry real values in the PDF; the other four are
blank in the PDF too (-), so they resolve to "omitted" and change nothing:
| Component | Property | Blank on the live page | Filled from the CoP | PDF row |
|---|---|---|---|---|
| Column | Stirrups |
Property Type, Type of Elements, Unit | type: Text, applies_to: RC column, unit omitted |
p.269 21 Stirrups Text RC column - No 4H13-300 |
| Column | StirrupsType |
Property Type, Unit | type: Text, unit omitted |
p.269 22 StirrupsType Text When required / relevant - Yes Refer to list^ |
| Distribution Chamber | ID |
Examples | omitted (PDF gives none) | p.276 4 ID Text - No |
| Pile | BoreholeRef |
Unit | omitted (PDF -) |
p.316 1 BoreholeRef Text All piles - No BH2, BH3, BH12-2 |
| Pile | ConstructionMethod |
Unit | omitted (PDF -) |
p.316 3 ConstructionMethod Text All piles - Yes Refer to list^ |
| Pile | ShaftR4DesignFactor |
Examples | omitted (PDF -) |
p.317 2 ShaftR4DesignFactor Real when required / relevant - No - |
Normalisations applied (the complete list)
Every one of these is a spelling/rendering defect where the CoP v3.1 PDF is unambiguous. Nothing else was changed silently.
lfc → Ifc (lower-case L rendered for capital I) — 21 IFC-entity banners across 15
components: Beam, Damper, Distribution Chamber, Interceptor, Pile, Sanitary Appliances,
Space (Usage), Staircase, Tank, Type Bedding for Pipe, Valve, Wall, Waste Terminal, Water
Meter, Window.
| Live text | Written | Component | CoP evidence |
|---|---|---|---|
RIDIGSEGMENT |
RIGIDSEGMENT |
System / IfcPipeSegment |
PDF p.423 |
PWCSINPSECTIONCHAMBER |
PWCSINSPECTIONCHAMBER |
System / IfcDistributionChamberElement |
PDF p.425 |
RAODSIDEDRAIN |
ROADSIDEDRAIN |
System / IfcCivilElement |
PDF p.426 |
PLANTING AREAS |
PLANTINGAREAS |
Planting Areas | PDF p.324 |
Shower |
SHOWER |
Sanitary Appliances | PDF p.340 |
Boole |
Boolean |
Wall / PrefinishedFacade |
PDF p.435 |
Booelan |
Boolean |
Space (Area Requirements) / AVF_IncludeAsGFA |
PDF p.373 |
Deliberately not normalised — published spellings preserved verbatim because both sources
agree: AI-DioxinsAndFurans (hyphen, Pollution Control), BarrierFreeAccessbility (Lift),
and the Space (Area Requirements) example values Assisted Living Facilitys,
Service Appartment II, Electrial Substation, plus the duplicated Mechanical Ventilation
in Space (Usage) VentilationMode.
Footnote markers stripped from property names
Markers are removed from the stored property; the original cell is kept in property_raw.
The footnote text itself has no corpus field, so it is recorded here:
| Component | Properties | Footnote published under the table |
|---|---|---|
| Lift | BarrierFreeAccessbility, ClearDepth, ClearHeight, ClearWidth, FireFightingLift, LiftType (^) |
"^Note: Properties to be provided for Ifc SubType = LIFT only" — 6 of Lift's 8 properties apply only to LIFT, not CARLIFT. |
| Refuse Chute / Recyclables Chute | NominalCapacity (^) |
"^ Note: NominalCapacity for Compactor refers to the volume of waste before compaction. For example, if the capacity of the equipment if 6m3, the NominalCapacity is 18m3 due to compaction ratio of 1:3." |
| Road | LoadingCapacity (^) |
table note: "* Note: PARKINGACCESSWAY refers to LTA's accessway to parking place" |
| Site Boundary | BroadLandUse, VacantLand (*) |
marker only; the page's note is the IFC SubType = SITEBOUNDARY only scope text |
A * on a value (e.g. Input Limitation = No*) is the page's "parameter is populated
from the dimensions of BIM elements modelled" note. It appears under Column, Slab, Wall, Beam,
Borehole, Footing / Pilecap, Pile and Household Shelter and does not change the parsed value.
Cross-validation against the CoP v3.1 PDF
scratchpad/validate_against_cop.py. Page offset verified empirically before use: corpus
page_range is Section-4-relative and PDF page = value + 246 — 722 of 723 parsed
properties land inside their own page range at +246, while the next-best offset scores 0.66.
- 0 of 723 parsed properties are absent from the PDF.
- 1 parsed property is in the PDF but outside its component's page range: Tank /
CompactionRatio, which the PDF carries on p.335 (Refuse Chute). Consistent with theREFUSEHANDLINGEQUIPMENTsubtype move above. - 0 unexplained "in the PDF but not parsed" candidates. All 81 raw candidates (across 31
components) were ruled out by inspecting their PDF context: Property Type values
(
Length,Volume,Area) —Lengthalone accounts for 26 of them — prose (Purpose,Public,Private,Vehicular,Duty,Accessway,Egress), a figure caption (Stirrups), values of other properties (TradeEffluent), worked-example tables (Space (Area Requirements) p.360), overlapping page ranges (Space Usage 114–171 spans the Space Area Requirements pages), and one explicit exclusion — CoP p.292 states thatPileModelFactor/ShaftR4DesignFactor/EndBearingR4DesignFactordo not apply to Footing / Pilecap. - Entity/subtype disagreements: exactly the ones listed in Binding changes above.
- Property Type compared for 526 parameters. Name-presence alone proved too weak — it
passed both the blank-cell defect above and the Distribution Chamber type change — so the
check now asserts our
typeagainst the type token the PDF prints in that property's row. Result: 0 parameters typednullwhere the PDF states a type, and 2 genuine mismatches, both already recorded above (Distribution ChamberTopLevel/InvertLevel, oursTextvs PDFReal). Two properties have no type token beside their name in the PDF and are explained: Space (Area Requirements)ACN_ConnectivityType(the PDF spells the nameACN_ConncectivityType) and TankCompactionRatio(the PDF prints it on the Refuse Chute page, outside Tank's page range — and types itReal, the conflict noted above). Pollution Control's 58AI_/TED_rows are exempt by name: their two-column listing table states no type on either source.
Value layer
resolve_enums.py alone could not re-apply 46 value lists that the migrated corpus carried
inline, because they had never been declared in _meta/enum_resolutions.json. 44 were
re-declared there verbatim — 43 sourced from _reference/structural_enums.json (each
asserted equal to its enum group before writing) and 1 = Site Boundary BroadLandUse
(32 values, CoP p.349). The existing resolver now reproduces them. The remaining 2 are
intentional, not losses:
- System
SystemType— nowreference_valuesinstead ofallowed_values(see above). - Roof
ConstructionMethod— same treatment, applied late; see the correction below.
Correction — Roof ConstructionMethod keeps its vocabulary
An exhaustive old-vs-new audit of every value list in the corpus found one property
that had lost its vocabulary outright: Roof ConstructionMethod carried the 7
structural-enum values as allowed_values before, and briefly carried nothing after.
Roof is the same structural case as System SystemType — the live page and CoP p.339 both
mark Input Limitation No while the example still reads "Refer to list*" — but the two
were handled inconsistently: System kept its vocabulary as a soft reference_values, Roof
kept none. Per the project's own calibration (official vocabulary on a free-input parameter
is retained as a suggestion, not a constraint), _meta/enum_resolutions.json now carries a
reference_resolutions entry for Roof ConstructionMethod with the 7 values copied
verbatim from _reference/structural_enums.json and asserted equal to what corpus v3.1
carried. All three Roof bindings now expose them as reference_values.
The audit is now clean: 0 properties lost a vocabulary, and exactly one value-layer
change remains corpus-wide — Roof ConstructionMethod allowed:7 → reference:7, which
is this intended re-calibration.
A stale entry was also removed: System / SystemType still appeared in the hard
resolved list alongside its documented soft entry. It was inert only because
resolve_enums.py gates the hard branch on input_limited; had a later recrawl flipped
that flag, the parameter would have silently regained a hard constraint against intent.
Input-limited parameters still awaiting a resolution
| Component | Property | Status |
|---|---|---|
| Road | LoadingCapacity |
New in this regeneration — no resolution entry yet. Live examples: 24, 30, 50 (tonnes). |
| Parking Lot | VentilationMode |
Pre-existing pending (enum_resolutions.json → pending). |
| Parking Lot | SpaceName |
Pre-existing pending. |
| Wall | ArrangementType |
Pre-existing pending. |
The other 11 new properties (the DA1- / WorkingLoad_ / MinEmbedment* block, NominalCapacity,
CompactionRatio) are free-input on both sources and need no resolution.
Component README.md / notes.md regenerated
Both files are shipped raw to dist/ and inlined into llms-full.txt, so after the corpus
regeneration they contradicted the very data they describe — arc/lift/README.md and the
llms-full.txt copy of it both still read "Parameters | 2" against a parameters.json
holding 8. They are now generated by scripts/generate_component_docs.py, which runs after
resolve_enums.py (notes cite allowed_values) and asserts, per component, that the stated
counts and the parameter-table row count match parameters.json — a contradiction now fails
the run instead of shipping.
75 files regenerated across 47 components: 45 README.md + 30 notes.md. 72 of them
belong to the 44 components whose parameters.json this regeneration changed. The other
three are the settled rule decisions below.
Editorial prose promoted to data. Four documents held hand-written passages. They now
live verbatim in _meta/editorial_notes.json and are emitted from there, so the generator
holds no per-file exceptions and "the corpus is generated, never hand-edited" stays literally
true. All four were verified byte-identical to the originals after regeneration:
| Document | Passage |
|---|---|
arc/space/README.md |
the parenthetical in the no-bindings sentence |
arc/space/notes.md |
the ## Notes section |
arc/space_area_requirements/notes.md |
## Property prefix groups (IFC-SG Glossary), incl. the mandatory/key subsections |
arc/space_usage/notes.md |
the - Related: provenance sub-bullet and ## Glossary page split (verified 2026-07-10) |
Corrections made while regenerating
arc/site_boundary—BroadLandUsere-filed. It sat under "Input-limited parameters resolved against structural enums" although itsallowed_values_sourceisCoP v3.1 p349 (source table), not a structural enum. Parameters are now filed by their actual source, so it moves to "Other input-limited parameters" — consistent withCeiling/FireRating, which has always been filed that way. A presentation fix; the values inparameters.jsonare unchanged.arc/pollution_control/README.md— Type column fixed. A pre-existing bug in the original generator rendered a null type as the literal stringNone, so all 58AI_/TED_rows read| None |. An absent value now renders as an empty cell.- List ordering is first-appearance / source order, mirroring the official table rather
than alphabetising it. Two files were alphabetised by the old generator and change under
the rule:
arc/green_verges/notes.mdandarc/ramp/notes.md(Ramp's list now reads in S/N order 3–8, as the page prints it).