Skip to content

Option B/C future modules: assembly catalog, geometry estimator and comparator

Deferred spec — not Phase-1 core (Session 33)

Session 33 replaced the Phase-1 plan this page was written for. Phase 1 is now LCI-first (decisions #31–#33 in PROJECT.md). The tool stores, gates and applies Chiwara's LCI factors over material quantities in (Option A), for A1–A3, B4 and B6, GWP only. The roadmap has the new sequence.

This page is kept, unchanged in substance, as the spec for the later input options:

Option What it is Sections here Status
B — works → materials Parametric assembly catalog as versioned data, generic dispatcher §2, §3, §5.1–5.2 Fast-follow after the Phase-1 core (decision #33; #28 resequenced)
C — geometry → works Geometry estimator, same-geometry comparison §1.2–1.3, §4, §5.3–5.4 Deferred (decision #33; #29 amended)

What changed in the decisions this page relies on — each with its original text kept in PROJECT.md and pending review in #53:

  • #27 (two-path model) stands. Path E stays the parity anchor. Path G is Options B + C, so it is not built in Phase 1.
  • #28 (catalog as data) is resequenced to the Option-B fast-follow. Its content is unchanged.
  • #29 (same-geometry comparator) is amended. The Phase-1 comparison is the same engine run on two material sets Chiwara provides. The same-geometry auto-generation of §5.3 is deferred with Option C. The delta rules of §5.4 still apply.
  • #30 (CMU default) stands, as the catalog default once Option B exists.
  • #10 (frontend) is amended. The Phase-1 frontend is a thin Option-A input with two-tier results. The two-tier contract of §6.3 carries over; the wizard of §6.4 waits for Options B/C.

Where this page says "Phase 1", "P1-n" or "the reshaped Phase 1", read it as the Session 31 plan, now superseded. The PR sequence in §7 is the order for building Options B and C, not the Phase-1 order.

Status (Session 31, 2026-09-28): design blueprint, docs-only. No code implements this page yet. Session 31 added four decisions (#27 to #30 in PROJECT.md). The decision-review issue covering them is #53 (AR-73).

Original framing (Session 31, superseded by Session 33). This page was the design for the reshaped Phase 1:

assembly catalog + generic dispatcher → geometry estimator → conventional assemblies + same-geometry comparator → wizard frontend → earth data.

It explains what each piece is, how it respects the decisions already locked, and which PR builds it (§7). A later PR must not change anything decided here without first updating this page.


0 · Why the reshape, and where the current code stands

Recon on main at fcfe5ad shows three facts the reshape has to deal with.

  1. There is no geometry. Every quantity is a typed-in D-column value from the Keur Songho workbook, held in _KS_QUANTITIES (backend/app/domains/quantities/__init__.py). No code derives a wall area, a floor area or a volume from a building's dimensions.
  2. There is no comparator. services.comparative_assertion is a Phase 2 line in ROADMAP. No code runs two variants of one building and differences them.
  3. The catalog is tangled into the reference project. The 45 recipe functions (_a_v_5, _rc_350, …) are good per-unit build-ups. Several, though, hard-code a Keur Songho number (_a_iii_1_row5 embeds 82.412, _a_vii_1 embeds D29 = 18.6, _a_ix_2 embeds 289.15). Others reproduce a workbook bug on purpose (E3–E14, E20). The dispatcher _run_keur_songho is the only caller. explode_boq() reads only project_name: it ignores every line item and raises NotImplementedError for any other project.

The reshape separates these. The reference path stays exactly as it is (§1). The recipes become versioned data (§2), which a generic dispatcher reads (§3). Quantities come from geometry (§4). One engine runs twice for the comparison (§5).

Locked decisions this design is bound by

# Decision How this design complies
8 Modular monolith; domains quantities, thermal, energy, lca; shared services The catalog, the dispatcher and the estimator live inside quantities. The comparator is services.comparative_assertion, the service ROADMAP already names. No new domain module.
10 Minimal React (Vite + Tailwind) frontend The wizard (§6) is built on the existing frontend/ scaffold with the same stack. See the tension note below.
15 RSP parametric, default 30 a RSP is a project-context input with a default of 30 a. The comparator requires both runs to share it. The workbook's 50 a (B1!C5) stays a documented divergence (B4/C3/C4/D spec).
18 Comparative assertions: same GIA, RSP, climate and programme, with a Monte Carlo band The comparator builds that lock-in: one geometry and one context go into two runs. A delta is emitted only with an uncertainty band (§5.4).
19 MaterialCategory spine; external codes optional and format-only Every catalog record carries a MaterialCategory. Uniclass and IFC codes are optional and format-validated (§2.2).
22 No vendored ND tables The catalog holds no classification table. Codes are user-supplied strings.
23 Two-axis verdicts The exact path keeps its parity tests. Every catalog parameter carries an Axis-2 verdict. Each clean-form quirk resolution needs a reconciliation row (§2.4).
24 Installed quantity is one-time The estimator outputs installed quantities only. Recurrence stays in lca.b4.
26 Per-stage geography; earth is local data only Earth A1–A3 stays BLOCKED. The conventional side uses ÖKOBAUDAT-interim factors with their tags (§5.5).

Tension note: decision #10, pre-existing and not introduced here

Decision #10 calls the frontend "minimal … for testing convenience … not the focus in v1". ROADMAP Phase 3 already describes a BoQ input form as "the tool's front door", and Sessions 22–29 built a results view on top of it. The wizard in §6 continues that drift, but this page does not amend #10. The stack stays the same, and the wizard stays a thin client over the API. The decision-review issue #53 (AR-73) covers #10 as well, so the drift is settled on the record rather than by accumulation.

Decision #19 and UniFormat

The reshape brief suggested attaching "Uniclass/UniFormat" codes under ISO 12006-2. Decision

19 names Uniclass 2015, IFC and UN CPC as the permitted optional external codes. Adding

UniFormat would change #19, so it is not in this design. ISO 12006-2 is cited only as the framework Uniclass conforms to. If UniFormat is wanted, it needs a decision-review first.


1 · The two-path model (decision #27)

The tool computes quantities along two paths that never share a code path for new projects.

graph LR
    subgraph "Path E: exact reference (frozen)"
        KSQ["_KS_QUANTITIES\n(D-column values)"] --> RKS["_run_keur_songho\n45 recipe fns, quirks reproduced"]
        RKS --> QE["QuantitiesResult\n(Keur Songho)"]
    end
    subgraph "Path G: geometry estimator (new projects)"
        GEO["Geometry input\nTier 0 / 1 / 2"] --> DRV["Derived drivers\nareas, lengths, volumes"]
        SEL["Assembly selections\n(catalog ids)"] --> DSP["Generic dispatcher"]
        DRV --> DSP
        CAT[("Assembly catalog\nversioned JSON")] --> DSP
        DSP --> QG["QuantitiesResult\n(any project)"]
    end
    QE -. "calibration test C-2\n(itemised Δ)" .- QG

1.1 Path E: exact reference

  • What it is: compute_keur_songho() over _KS_QUANTITIES, unchanged. It reproduces the workbook as written, including every kept quirk (E2–E14, E20). It applies C1–C4 and records the C3 BLOCKED rebar.
  • What it is for: parity under decision #23. It is the regression anchor that proves the recipes were transcribed faithfully. GET /quantities/keur-songho, its golden, the A1–A3/A4/A5 Keur Songho goldens and backend/tests/regression/ stay pinned to it.
  • What it is not: a way to compute a new building. No new-project request ever reaches Path E.
  • Frozen: Path E is moved to domains/quantities/keur_songho.py (PR-1). After the move, any byte of change to its output is a defect. The goldens and openapi.json must not diff.

1.2 Path G: geometry estimator

  • What it is: geometry (§4) → derived driver quantities → the assemblies the user selected (catalog ids) → the generic dispatcher (§3) → a QuantitiesResult for any project.
  • Recipes: the clean form of each catalog record (§2.4). No workbook quirk is reproduced on Path G. A quirk either has a reconciled clean form or blocks the record.
  • Its outputs carry provenance flags. Each driver quantity says whether it was derived (Tier 0/1) or user-entered (Tier 2). Results carry the catalog version hash.

1.3 The Keur Songho calibration test

Path G is checked against Path E by running it on the Keur Songho geometry. The check has two layers, because they fail for different reasons.

C-1: recipe-level (catalog migration is lossless), PR-1. Each Path-E line's driver D value goes to its catalog record, with no geometry involved. - Assert: per material, the Path G total minus the itemised quirk Δ equals the Path E total within 0.1 % (the decision #23 parity band). - Each itemised Δ is computed in closed form from the quirk's definition (table below) and asserted to 1e-9 relative. An un-itemised residual above 0.1 % fails the test. - Rows whose record is blocked on Path G (§2.4) are left out of both sides, and the test lists their Path E per-row contribution. A blocked record therefore can never pass by coincidence.

C-2: geometry-level (the estimator is fit for purpose), PR-3. Keur Songho's Tier-1 inputs are derived from the workbook and run through the estimator. - Inputs: internal perimeter, internal floor area, wall height, storeys and internal wall length. Each derivation cites the 1a_boq_input_lca.xlsx cell(s) it comes from; none is invented. - Assert: per dominant material (≥ 5 % of Keur Songho installed mass on Path E), the Path G total lies within ±15 % of Path E, after the itemised quirk Δ is removed. - Every divergence beyond ±15 % must appear in the test's itemised-divergence list, and each entry names either (a) an Excel quirk ID or (b) a named estimator simplification (§4.4). An unlisted divergence fails the test. Non-dominant materials are reported but not asserted. - The ±15 % is a design target. PR-3 confirms or revises it with evidence and records the final value in the quantities reconciliation doc. It is never widened to make a failing test pass; a failure is itemised or fixed.

Quirk itemisation on the calibration run. The kept quirks are those the reconciliation marks "reproduce as written".

Quirk Where (Path E) Clean form on Path G Δ on calibration (Path G − Path E)
E2 rows 5, 6: 0.21 concrete fraction, text says 0.20 Unresolved: the right value is unknown. The record carries concrete_fraction as a BLOCKED parameter until attributed None: records blocked, rows 5/6 excluded from C-1
E3 K16 uses D15 Disappears: a dispatch bug, not a recipe bug. The record reads its own driver water: (D16 − D15) × (41.67·0.0043 + 0.21·0.45)
E4 row 24 missing parentheses Parenthesised form, identical to sibling row 26 earth, water: (D24 − 1) × joint term
E5 J38 missing parentheses Parenthesised (as K38 already is) Record blocked (AR-15), row excluded. Once unblocked: earth (D38 − 1) × 0.04·1.5
E6 K32: 0.043 0.0043, as in every sibling formula Record blocked (AR-15), row excluded. Once unblocked: water D32 × 66.9 × (0.0043 − 0.043)
E7 J29 hard-codes 18.6 Reads the driver 0 (numerically inert)
E8 K30: D30 × 0.2585 J30 × 0.2585, the sheet's documented method water: (J30 − D30) × 0.2585
E9 U36 hard-codes 289.15 Driver = foundation contact area (C5, AR-16 pending) Record blocked (AR-16, AR-52), row excluded
E10 H53: width 0.45 0.35, as on the same row gravel: D53 × 0.04 × 0.75 × (0.35 − 0.45)
E11 O60: 0.2·0.9 2·0.9 steel profile: D60 × (2·0.9 − 0.2·0.9) × 4·0.05·0.004
E12 N64: 2.7, 2.3 2.79, 1.3 per the E-note steel sheet: D64 × 0.9 × (2.79·1.3 − 2.7·2.3) × 0.004
E13 rows 45–47: no cement Cement added at 400 kg/m³ × 0.016 m, per the rows' own E-note cement: (D45 + D46 + D47) × 0.016 × 400
E14 row 42: gravel named, never computed Unresolved: no coefficient exists anywhere. Gravel is a BLOCKED parameter None: record blocked, row 42 excluded
E20 row 63: title 3.12 m, formulas 3.2 m Door height is a parameter; Keur Songho record keeps 3.2 (formula-consistent) 0

E15, E16, E21 and E22 are label or unit-string defects with no numeric effect. The catalog removes them by construction: units are typed, and record ids are unique.


2 · Assembly catalog data model (decision #28)

2.1 Catalog as data

  • One JSON file per record under backend/app/data/assemblies/<id>.json. Records are data, reviewed by PR, not code. There is no write endpoint in Phase 1 (see §6.2 on roles).
  • A lockfile backend/app/data/assemblies/catalog.lock.json records the SHA-256 of each record's canonical JSON. CI fails if a record's content changes without its version being bumped.
  • The catalog version is the hash of the lockfile. It is stamped on every result, so a number can always be traced to the exact recipes that produced it.
  • A CI gate modelled on the seed-EPD gate validates the following for every record:
  • schema;
  • one provenance block;
  • an Axis-2 verdict on every parameter, with named attribution on every EXPERT-JUDGEMENT;
  • every quirks[].id resolves to an ID in the quantities spec's Errors table;
  • every BLOCKED parameter has an action-register row.

2.2 Record schema

{
  "id": "wall.adobe-38",                  // stable slug, unique (removes E16 by construction)
  "version": 1,                           // integer; bump on any buildup/param/provenance change
  "label": { "en": "Adobe wall, 38 cm", "fr": "Mur en adobe, 38 cm" },
  "term_source": "chiwara",               // chiwara | craterre | rilem-tc-274-tce | conventional
  "category": "raw_earth",                // MaterialCategory: decision #19 canonical spine
  "element": "wall",                      // ElementSlot (below): project-owned, drives mapping
  "variant": "earth",                     // earth | conventional | shared
  "code_display": "A.V.5",                // Devis Descriptif label, display only (#19)
  "external_codes": {                     // optional, format-validated only (#19, #22)
    "uniclass": null,                     // UniclassCode regex (schemas/classification.py)
    "ifc": "IfcWall"                      // IfcEntityCode regex
  },
  "driver": { "quantity": "wall_area_net", "unit": "m2" },
  "params": {
    "bricks_per_unit":  { "value": 27.78,  "unit": "u",  "verdict": "EXPERT-JUDGEMENT",
                          "attribution": { "who": null, "date": null, "basis": "OMPU (pending)" } },
    "earth_per_brick":  { "value": 0.0143, "unit": "m3", "verdict": "EXPERT-JUDGEMENT", "...": "..." },
    "water_per_brick":  { "value": 0.0043, "unit": "m3", "verdict": "EXPERT-JUDGEMENT", "...": "..." },
    "mortar_per_unit":  { "value": 0.12,   "unit": "m3", "verdict": "EXPERT-JUDGEMENT", "...": "..." }
  },
  "buildup": [                            // per ONE driver unit; the dispatcher multiplies by d
    { "material": "raw_earth", "unit": "m3",
      "k": "bricks_per_unit * earth_per_brick + mortar_per_unit * 1.5" },
    { "material": "water",     "unit": "m3",
      "k": "bricks_per_unit * water_per_brick + mortar_per_unit * 0.45" }
  ],
  "paths": ["exact", "estimator"],        // which paths may use this record
  "quirks": [],                           // [{ id: "E3", exact: "reproduce", clean: "<form>", reconciliation_ref }]
  "status": "active",                     // active | blocked: derived, true iff any param is BLOCKED
  "provenance": {
    "source_citation": "reference/excel/1a_boq_input_lca.xlsx, Quantitatifs row 18",
    "justification_text": "…≥ 280 chars, why this build-up represents the target context…",
    "pedigree": {                         // Weidema 5 axes, 1–5 (schemas/proxy_provenance.PedigreeScore)
      "reliability": 4, "completeness": 3, "temporal_correlation": 2,
      "geographical_correlation": 1, "further_technological_correlation": 2
    },
    "created_by": "…", "created_at": "…"
  }
}

ElementSlot is a new project-owned StrEnum. It follows #19's pattern and is extended by PR review: foundation, ground_floor, intermediate_floor, wall_external, wall_internal, opening_head (arch or lintel), roof, roof_finish, wall_finish_ext, wall_finish_int, floor_finish, opening (windows and doors), rainwater, treatment, external_works.

Build-up expressions (k) are linear in the driver, which holds for every recipe on Path E. They are parsed by a whitelisted-AST evaluator that allows numbers, parameter names, pi, + − × ÷ and ref:<material> (for "water = earth × 0.2585"). There is no eval. The evaluator raises ValueError if it meets a BLOCKED parameter.

Relation to the existing Assembly schema. schemas/assembly.py makes uniclass_code and ifc_entity_code required, and its comment cites decision #19. But #19 makes them optional, and the schema lacks the MaterialCategory that #19 requires. PR-1 replaces it with AssemblyRecord, which conforms to #19. This corrects drift in the code; #19 itself is unchanged.

2.3 Terminology

  • Earth records: labels use the Chiwara team's terms as they appear in the Keur Songho Devis: adobe, voûte nubienne (Nubian vault), contrefort (buttress), acrotère (acroterion / parapet), arc (arch), enduit terre-sable (earth-sand plaster), BBSC (acronym still unexpanded, AR-15).
  • Generic earth-material names follow CRAterre (Houben & Guillaud, Earth Construction: A Comprehensive Guide) and RILEM TC 274-TCE: adobe, CEB, CSEB, rammed earth, cob / banco. term_source records which vocabulary a label comes from.
  • Conventional records: CMU (concrete masonry unit, parpaing / agglo creux), RC (béton armé), cement plaster (enduit ciment).

2.4 Mapping the existing recipes to records

The inventory below covers every recipe on Path E, confirmed against _run_keur_songho on main. Clean records can move onto Path G unchanged. Quirk records carry an explicit quirks[] entry: Path E reproduces the quirk, and Path G uses the clean form once its reconciliation row exists. Blocked records exist in the catalog but raise on Path G.

Record id KS row(s) Driver (unit) Path G status Notes
fdn.cyclopean-earth-concrete 5 foundation volume (m³) Blocked E1 (82.412 external constant) is replaced by a geometry driver; E2 concrete_fraction BLOCKED
fdn.cyclopean-concrete-250 6 foundation volume (m³) Blocked C1 applied; E2 as above
fdn.blinding-150 7 m³ Clean
fdn.capillary-barrier-pe 8 ml → m² (roll width 1 m as a param) Clean
struct.rc-350 9, 11, 12 m³ Clean Reused by conventional footings, lintels and chaînages
wall.adobe-80 14 wall area (m²) Clean
wall.adobe-80-bbsc 15 m² Blocked BBSC rock-only model (AR-15)
wall.adobe-65 16 m² Clean E3 is dispatch-only; the record is unaffected
wall.adobe-38-bbsc 17 m² Blocked AR-15
wall.adobe-38 18 m² Clean Earth default wall
wall.adobe-20 19 m² Clean Internal partitions
arch.090-on-80, arch.090-on-65, arch.120-on-38, arch.040-on-38, arch.090-on-20, arch.180-on-38 21, 22, 23, 25, 26, 27 u Clean (6 records) E15 (row 27 unit) and E16 (duplicate code) removed by construction
arch.090-on-38 24 u Quirk (E4)
roof.nubian-vault 29, 30 vault length (ml); params span, brick_thickness Quirk (E7, E8) C2 half-arc applied; one parametric record replaces two
buttress.lateral-330-bbsc 32 ml Blocked E6 quirk + AR-15
buttress.central-260 33 ml Clean
roof.earth-slope-waterproofing 35 roof area (m²) Clean
treat.anti-termite 36 foundation contact area (m²) Blocked E9/C5 area (AR-16); PESTICIDE A1–A3 BLOCKED (AR-52)
parapet.acroterion-adobe-bbsc 38 ml Blocked E5 quirk + AR-15
finish.cement-plaster-16 41, 43, 45, 46, 47 m² Clean (rows 45–47: Quirk E13) Shared earth/conventional finish
roof.earth-sand-tar-coating 42 m² Blocked E14 gravel coefficient BLOCKED
finish.earth-sand-plaster-16 44 m² Clean Earth default wall finish
floor.compacted-earth-fill-15 50 m² Clean Shared ground floor
floor.rc-slab-300 51 m³ Blocked C3 rebar BLOCKED (AR-14)
parapet.coping-slab-250 53 ml Quirk (E10)
sill.window-250 55 m³ Clean
ext.perimeter-slope-earth 57 ml Clean
opening.window-louvred-steel 60, 61 u; params w, h Quirk (E11 on row 60) C4 glass × count applied
opening.door-louvred-steel 63, 64 u; params w, h Quirk (E12, E20) C4 applied
opening.door-metal 65 u Clean
rainwater.gutter-metal 67 ml Clean
finish.wall-paint 69, 70 m² Clean
finish.antirust-paint 71 u (joinery item) Clean A1–A3 factor PROVISIONAL (issue #15)
finish.tyrolean-plaster 72 m² Clean Sand density 1600 kg/m³ from the I72 comment
finish.floor-tiles 74, 76 m² Clean
finish.tile-skirting 75 ml Clean

Totals: 45 recipe functions become 41 records. Recipes shared across rows merge (_rc_350, the two plaster functions, the two vault rows, the window and door families). Of the 41, 27 are clean, 5 carry a quirk and 9 are blocked on Path G. The E-quirk clean forms need reconciliation rows before PR-1 ships them, per CLAUDE.md's rule that a module needs reconciliation before it is implemented. PR-1 adds those rows to quantities-formula-reconciliation.md as an addendum; the Path-E rows are untouched.

Parameter provenance. Every coefficient transcribed from the workbook inherits the spec's PROVENANCE UNKNOWN / "OMPU" status. On the records it is EXPERT-JUDGEMENT (pending), with the attribution fields null until Chiwara names a source (see attribution requests). That is the status those numbers already carry in the reported results today. The catalog makes it visible per parameter rather than per spec.


3 · Generic dispatcher and endpoints

3.1 Dispatcher

dispatch(selections: list[Selection], drivers: DriverSet, catalog: Catalog) -> QuantitiesResult
  Selection = { slot: ElementSlot, assembly_id: str, driver_override?: Quantity }
  for each selection:
      rec  = catalog[assembly_id]                  # KeyError → 422
      d    = driver_override or drivers[rec.driver.quantity]   # unit must equal rec.driver.unit
      for line in rec.buildup: acc[line.material] += d × eval(line.k, rec.params)
      BLOCKED param → record the reason in blocked_parameters (the line contributes nothing),
                      same contract as row 51 today
  • The dispatcher also does the volume → mass seam that A1–A3 needs (AR-46). Each MaterialCategory gets a density with its own Axis-2 verdict, and earth density stays BLOCKED (AR-47). The seam is shared with the Path-E mass-path rebuild; it is not duplicated.
  • It emits MaterialCategory.RAW_EARTH, and the stages key on EARTH. AR-53 must be closed in PR-2 before quantities can feed stages generically.

3.2 Endpoints

The API is stateless, taking inputs in the body. Stored projects are not part of Phase 1.

Method and path Body → response Notes
GET /catalog/assemblies ?variant=&element= → list[AssemblyRecord] + catalog_version Read-only
GET /catalog/assemblies/{id} → AssemblyRecord 404 for an unknown id
GET /catalog/mapping → earth↔conventional slot mapping (§5.2) Read-only data
POST /geometry/estimate GeometryInput → DerivedGeometry (§4) Pure function; no catalog needed except wall thickness per selection
POST /quantities/explode BoQInput → QuantitiesResult Now reads its line items: each carries assembly_id (or a code_display that resolves uniquely) plus quantity. The Keur Songho special case is removed; the exact reference is only GET /quantities/keur-songho. Behaviour change, recorded in PR-2's CHANGELOG.
POST /projects/compute ProjectContext + GeometryInput + selections → ResultEnvelope (§6.3) Runs quantities → A1–A3 (→ A4/A5 if their inputs are given)
POST /projects/compare ProjectContext + GeometryInput + selections_a + selections_b? → ComparisonEnvelope (§5) selections_b defaults to the mapped conventional set
GET /quantities/keur-songho, GET /lca/{stage}/keur-songho unchanged Path E reference aliases; goldens pinned

AR-72 is folded in here. The hard-coded keur-songho slugs stop being the only way to get a result: every new-project computation takes its inputs in the body. The slug routes remain as named references to the seeded project, which keeps the Session 25 goldens and the frontend mocks valid. AR-72 closes with PR-2.


4 · Geometry estimator

Everything the estimator derives is an installed-quantity driver (decision #24), in SI canonical units (§6.1).

4.1 Canonical basis: internal dimensions

The functional unit in decision #18 is locked on GIA. Earth and conventional walls differ in thickness (adobe 38 cm vs CMU 20 cm). So if the external footprint were held fixed, the two runs would have different GIAs and the comparison would break #18.

The canonical geometry is therefore internal: internal perimeter P and internal floor area A per storey. Each variant then derives its own external quantities from its wall thickness t. GIA is identical across the two runs by construction.

The wizard may accept dimensions measured externally (basis: "external"). It converts them to internal once, using the thickness of the study variant's wall, and records the basis. From then on the internal geometry is held fixed.

4.2 Tier 0: presets

Preset Inputs (internal, m) A P
Square a a² 4a
Rectangle a, b a·b 2(a + b)
L-shape bounding a, b; notch c < a, d < b a·b − c·d 2(a + b)

For an orthogonal L notched at one corner, the perimeter equals that of its bounding rectangle.

4.3 Tier 1: parametric

Inputs: P (m), A (m²), storey height h (m), storeys n (integer ≥ 1), internal wall length L_iw (m, optional; absent means not provided, and the result says so), and openings (a list of {assembly_id, count} or an opening-area override).

Derivations per variant, where t is the thickness of the selected wall_external record. The offset identities hold for any simple orthogonal polygon, because it has four more convex corners than reflex ones:

Driver Formula Unit
GIA n · A m²
Centreline perimeter P_c P + 4t m
External perimeter P_e P + 8t m
External footprint A_e A + P·t + 4t² m²
Ground-floor area A m²
Intermediate-floor area (n − 1) · A m²
Roof area (flat) A_e m²
Vault length (per span s) A / s ml
Gross external wall area P_c · h · n m²
Opening area Σ count · w · h_open (from opening records) m²
Net external wall area gross − opening area m²
External-wall volume net area · t m³
Internal-wall area L_iw · h · n − internal openings m²
External plaster area P_e · h · n − opening area m²
Internal plaster area (P · h · n) − opening area + 2 · internal-wall area m²
Foundation length P_c + L_iw m
Foundation volume foundation length · w_f · d_f (params of the foundation record) m³
Foundation contact area foundation length · w_f m²
Building volume (for B6, later) n · A · h m³

4.4 Named estimator simplifications

These are the only acceptable explanations for a C-2 divergence other than an Excel quirk. - S1: opening reveals, columns and beams are not modelled as separate plaster areas. - S2: arches and buttresses are counted only from explicit {assembly_id, count} inputs and are never inferred. - S3: a single storey height applies to every storey. - S4: the parapet is a separate optional element, with its length defaulting to P_e only when selected. - S5: there are no stairs, no roof overhang, and no ring beam unless a record for it is selected.

4.5 Tier 2: editable overrides

  • Any derived driver can be overridden in the wizard.
  • The override is stored as {value, unit, source: "user_entered", derived_value}. The original stays in the audit trail.
  • Every result that depends on an override shows a USER-ENTERED tag in both tiers. An override is not a verdict: it carries no Axis-2 status of its own, and it cannot un-BLOCK a parameter.

4.6 Earth runs with n > 1

The Keur Songho data has no earth intermediate-floor record. An earth run with n > 1 has an unfilled intermediate_floor slot, so the run reports "no earth record for slot". It does not substitute a conventional slab.


5 · Conventional set and the same-geometry comparator (decisions #29, #30)

5.1 Conventional default assemblies (decision #30)

The conventional baseline is CMU hollow-block walls, an RC strip foundation, an RC flat-slab roof and cement plaster. That is the alternative the project already names on its home page and in the stakeholder brief ("concrete hollow block and reinforced-concrete frame").

Record id Slot Build-up basis Parameters still to source
conv.fdn.rc-strip foundation struct.rc-350 + fdn.blinding-150 w_f, d_f footing section
conv.wall.cmu-hollow-150, -200 wall_external, wall_internal blocks/m² × block composition + mortar/m² block composition (cement : aggregate : water per block): BLOCKED (AR-74). Blocks/m² and mortar m³/m² are sourceable as joint geometry (sourcing round 1)
conv.lintel.rc opening_head struct.rc-350 lintel section
conv.roof.rc-slab-150 roof cement/aggregate per m³ + rebar slab rebar kg/m³: BLOCKED (AR-74); needs structural-engineer EXPERT-JUDGEMENT, lower bound ≈ 17 kg/m³ (sourcing round 1). The struct.rc-350 100 kg/m³ may not be borrowed: AR-14 shows that move shifts project steel by +163 %
conv.roof.bitumen-membrane roof_finish membrane kg/m² BITUMEN A1–A3 factor BLOCKED today
finish.cement-plaster-16 wall_finish_ext / _int shared with earth none

5.2 Earth ↔ conventional mapping

This is data (backend/app/data/assemblies/mapping.json), versioned with the catalog.

Slot Earth default Conventional default
foundation fdn.cyclopean-earth-concrete + fdn.cyclopean-concrete-250 conv.fdn.rc-strip
wall_external wall.adobe-38 conv.wall.cmu-hollow-200
wall_internal wall.adobe-20 conv.wall.cmu-hollow-150
opening_head arch.* (by span and wall) conv.lintel.rc
roof roof.nubian-vault + buttress.* conv.roof.rc-slab-150
roof_finish roof.earth-slope-waterproofing conv.roof.bitumen-membrane
wall_finish_ext finish.cement-plaster-16 finish.cement-plaster-16
wall_finish_int finish.earth-sand-plaster-16 finish.cement-plaster-16
ground_floor, floor_finish, opening, rainwater shared shared

Shared slots use the same record in both runs, so their contribution to the delta is exactly zero by construction.

5.3 Comparator: one engine, two runs, one delta

graph LR
    CTX["ProjectContext\nRSP · climate · programme · country"] --> R1
    GEO["GeometryInput\n(internal basis)"] --> R1
    CTX --> R2
    GEO --> R2
    SA["selections_a (earth)"] --> R1["compute()"]
    SB["selections_b (conventional)\n= mapping(selections_a) unless given"] --> R2["compute()"]
    R1 --> CMP["compare()\nFU lock-in check · delta · band"]
    R2 --> CMP
    CMP --> OUT["ComparisonEnvelope"]
  • The same compute() runs twice. There is no comparison-specific arithmetic apart from the subtraction.
  • Functional-unit lock-in (#18) is enforced, not trusted. compare() asserts that GIA, RSP, climate zone and programme are equal across the two runs, and raises otherwise. Because both runs share one ProjectContext and one internal geometry, this cannot fail unless there is a bug, and the assertion exists to catch that bug.

5.4 When a delta may be shown

The delta is computed per stage and per indicator. It is emitted only when all three hold:

  1. both run totals for that stage and indicator are non-None;
  2. both runs used the same catalog version;
  3. an uncertainty band exists for the delta (#18: "Monte Carlo uncertainty band on every comparison").

Otherwise delta = None, with incomplete_reason naming the failed condition(s). Until services.pedigree and services.uncertainty land, condition 3 fails. That costs nothing in Phase 1, because condition 1 fails anyway: the earth A1–A3 total is None under #26. The ISO 14044 §6.2 comparative-assertion documentation remains Phase 2; the comparator is the engine that documentation will sit on.

5.5 What is computable today

  • Conventional A1–A3, factor half: computable now. Every material the conventional set decomposes into has an A1–A3 row with status = COMPUTED on main: cement, sand and rebar are seed-resolved; gravel is an inline literal (AR-68); water is 0.
  • Each non-zero factor carries the ÖKOBAUDAT-interim, EU-contextualised — pending African data tag (#26). Rebar is the wire-not-bar proxy (AR-67).
  • Conventional quantities half: blocked on the three AR-74 recipe parameters (block composition, mortar rate, slab rebar), plus the non-earth densities of the AR-46 seam.
  • Earth side: BLOCKED pending field data. Earth A1–A3 (#26, attribution item 12), earth density (AR-47), BBSC composition (AR-15) and the E2/E14 parameters are all outstanding.

6 · Units, roles, results contract and wizard IA

6.1 Units

  • SI canonical everywhere in the backend: m, m2, m3, kg, L (liquids already declared that way) and kg_CO2_eq. Two exceptions follow existing contracts: A4 distances in km, and study periods as integer years (rsp_years). Every API field is a Quantity(value, unit) with a per-field allowed unit set. The API rejects any other unit with 422 and does no conversion, so there is one conversion point.
  • Display conversion happens only in the frontend (frontend/src/lib/units.ts, PR-6): mm/cm ↔ m for thicknesses, t ↔ kg, L ↔ m³. Conversion is applied on input before submit and on output for display. Stored and echoed values are always canonical. Adding a canonical unit still follows the existing rule (UnitEnum is extended by migration + PR review).

6.2 Roles

  • Two roles: lca_user (enters a project, computes, compares, reads provenance) and admin (curates the catalog and sees catalog validation state).
  • Phase 1 has a role flag, not authentication. The flag is a frontend UI mode that shows the admin catalog views. It is not a security boundary, so Phase 1 exposes no write endpoint: catalog changes are PRs against backend/app/data/assemblies/ and pass the CI gate (§2.1).
  • Authentication stays with Phase 3 API polish, as Auth & versioning already says. A spoofable header must never gate a mutation.

6.3 Two-tier results contract

ResultEnvelope
  summary:  SummaryTier     — the "sleek" view
  detail:   DetailTier      — every row, verdict, provenance, pedigree, Δ, tag
  inputs_echo, catalog_version, engine_version
  • Summary tier, per stage and indicator:
  • total: Quantity | None;
  • status: COMPLETE | INCOMPLETE | NOT_PROVIDED;
  • blocked_count, user_entered_count and interim_geography_count;
  • for a comparison, the delta and band, or None with reasons.
  • Detail tier: the existing per-row models (MaterialA1A3Result, …), unchanged in shape, plus the driver provenance of each quantity (derived / Tier / user-entered) and the record id@version that produced it.

Honest-BLOCKED rules (binding on both tiers and on the UI):

  1. No partial totals. A total is None if any contributing row is not COMPUTED. This is the existing lca._common.aggregate rule, applied again at summary level.
  2. Never zero-filled, never hidden. A BLOCKED row renders as BLOCKED with its reason, and it counts in blocked_count.
  3. NOT_PROVIDED is not BLOCKED. An optional user input left empty (A4 distances, A5 plant hours, L_iw) gives NOT_PROVIDED. A missing defensible parameter gives BLOCKED. The UI distinguishes them: the user can fix NOT_PROVIDED; BLOCKED needs sourcing.
  4. Tags propagate upward. If any row feeding a summary number is USER-ENTERED, PROVISIONAL or ÖKOBAUDAT-interim, the summary number carries that tag.
  5. No delta without both totals and a band (§5.4).
  6. A CORRECTED row shows its reference value and Δ in the detail tier (AR-71, already shipped).

NOT_PROVIDED and ResultEnvelope are schema additions (PR-2), recorded in the schema docs when they land.

6.4 Wizard information architecture

Superseded for Phase 1

For Phase 1 (Option A, no geometry step), the wizard is specified in the Phase-1 UI spec. The page list below stays as the proposal for the Option-B/C wizard.

Page list to confirm

The brief refers to an agreed page list. That list is not recorded anywhere in the repository. The eight pages below are this page's proposal, built from the brief's components. Before PR-6 starts, confirm them against the agreed list and correct this section if it differs.

# Page Purpose
1 Project Context that locks the functional unit
2 Geometry Tier 0 / 1 input, derived preview, Tier 2 overrides
3 Assemblies: study variant Earth selections per slot
4 Baseline: conventional Auto-mapped selections, editable; FU lock-in banner
5 Transport and site (optional) A4 legs, A5 plant; left empty → NOT_PROVIDED
6 Review Echo of every input and every flag; compute
7 Results Summary tier plus comparison
8 Detail and audit Detail tier, provenance drill-down, catalog versions
A Catalog (admin mode) Read-only record browser with validation state

Per-field inventory. "Canonical" is the API unit; "display" is what the wizard may show and accept. Tags: R required, O optional, D derived (read-only), U user-entered (Tier 2).

Page 1: Project. This page fills the existing ProjectIdentity and FunctionalUnit (schemas/project.py). ProjectContext in this page is the existing ProjectCreate; no parallel model is added.

Field Schema field Canonical Display Tag Validation
name identity.name text — R 1–120 chars
country identity.iso_country ISO 3166-1 alpha-3 country name R CountryAlpha3
location identity.latitude/longitude degrees map pin O Existing bounds
climate zone identity.koppen_zone KoppenZone code + label R KoppenZone members only (#16)
programme identity.typology Typology label R Typology members only
date of assessment identity.date_of_assessment ISO date locale date R
RSP functional_unit.rsp_years integer years years R, default 30 1–200, the existing bound (#15)
GIA functional_unit.gia m2 m² D Set from page 2. compute / compare reject a body whose gia differs from the derived GIA

Page 2: Geometry

Field Canonical Display Tag Validation
tier enum preset / parametric — R
basis enum internal / external — R, default internal
preset shape enum square / rectangle / L icon R if preset
a, b, c, d m m / cm R per shape > 0; L: c < a, d < b
perimeter P m m R if parametric > 0; isoperimetric check P² ≥ 16A (a square is the orthogonal minimum)
floor area A m2 m² R if parametric > 0
storey height h m m / cm R 2.0–6.0 m (soft warning outside)
storeys n integer — R 1–4; earth with n > 1 shows §4.6 notice
internal wall length L_iw m m O ≥ 0; empty → NOT_PROVIDED
openings list {assembly_id, count} — O Count integer ≥ 0; the id must be an opening record
any derived driver as §4.3 as §4.3 D → U on override Override > 0; shows the derived value beside it

Pages 3–4: Assemblies and baseline

Field Canonical Tag Validation
selection per slot assembly_id R per required slot Must exist in the catalog; variant must match the page (or be shared); a blocked record may be selected, and the review page lists it as BLOCKED
record params exposed as user-editable (e.g. vault span) per record O → U Within the record's declared bounds

Page 5: Transport and site

Field Canonical Display Tag Validation
distance per material group km km O ≥ 0
transport mode enum (A4 spec) label O
plant item, power, hours kW, h — O ≥ 0

Transport distance stays in km, the existing A4 leg contract (UnitEnum.KM), rather than being converted to m. kW and h are already in UnitEnum for A5 / C1.


7 · Implementation sequence

(Session 33: this is now the build order for Options B and C after the Phase-1 core, not the Phase-1 order. PR-2's volume→mass seam (AR-46) and AR-53 fix move forward into Phase-1 step 2; AR-72 moves into Phase-1 step 6.)

Each PR is cut from a fresh origin/main, runs the full 6-step loop where it adds a module or service, and updates module-map.md / data-flow.md when it does.

PR Branch Scope Gate
PR-1 feat/p1-assembly-catalog Move Path E to quantities/keur_songho.py (byte-identical output). Add the AssemblyRecord + ElementSlot schema (replacing the #19-non-conformant Assembly), JSON records for all 41 ids, the lockfile, and the catalog CI gate. Add the reconciliation addendum for the clean forms (§1.3 table), and calibration C-1. Goldens and openapi.json unchanged; C-1 green; gate green
PR-2 feat/p1-generic-dispatcher Dispatcher, whitelisted evaluator, volume→mass seam (AR-46, non-earth densities sourced), AR-53 fix, the GET /catalog/* and POST /quantities/explode changes, POST /projects/compute, ResultEnvelope + NOT_PROVIDED. Closes AR-72. New goldens for a synthetic project; Keur Songho goldens unchanged
PR-3 feat/p1-geometry-estimator Tier 0/1/2 (§4), POST /geometry/estimate, Keur Songho Tier-1 input derivation (cell-cited), calibration C-2 with the itemised-divergence list; tolerance confirmed. C-2 green; property tests on the orthogonal-offset identities
PR-4 feat/p1-conventional-assemblies Conventional records (§5.1) with the AR-74 parameters BLOCKED until sourced, and mapping.json. Gate green; the records raise on BLOCKED params
PR-5 feat/p1-same-geometry-comparator services.comparative_assertion engine (§5.3–5.4), POST /projects/compare, FU lock-in assertion, delta gating. Unit tests for each §5.4 condition; comparison golden with delta = None + reasons
PR-6 feat/p1-wizard-* (≥ 2 PRs) Wizard pages (§6.4) once the page list is confirmed, lib/units.ts, admin-mode catalog browser, mock fixtures synced from the new goldens. Frontend suite + coverage floor; mocks:check
— sourcing track Earth data (AR-47, attribution item 12, AR-15), AR-74 conventional parameters, E2/E14 attribution. It runs in parallel from day one; its position in the sequence is where it integrates, not where it starts. Seed-EPD / catalog provenance gates