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.
- 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. - There is no comparator.
services.comparative_assertionis a Phase 2 line in ROADMAP. No code runs two variants of one building and differences them. - 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_row5embeds82.412,_a_vii_1embedsD29 = 18.6,_a_ix_2embeds289.15). Others reproduce a workbook bug on purpose (E3–E14, E20). The dispatcher_run_keur_songhois the only caller.explode_boq()reads onlyproject_name: it ignores every line item and raisesNotImplementedErrorfor 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 andbackend/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 andopenapi.jsonmust 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
QuantitiesResultfor 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.jsonrecords the SHA-256 of each record's canonical JSON. CI fails if a record's content changes without itsversionbeing 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[].idresolves to an ID in the quantities spec's Errors table; - every
BLOCKEDparameter 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_sourcerecords 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
MaterialCategorygets 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 onEARTH. 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-ENTEREDtag 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 oneProjectContextand 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:
- both run totals for that stage and indicator are non-
None; - both runs used the same catalog version;
- 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 = COMPUTEDonmain: 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 datatag (#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) andkg_CO2_eq. Two exceptions follow existing contracts: A4 distances inkm, and study periods as integer years (rsp_years). Every API field is aQuantity(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 (UnitEnumis extended by migration + PR review).
6.2 Roles¶
- Two roles:
lca_user(enters a project, computes, compares, reads provenance) andadmin(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_countandinterim_geography_count;- for a comparison, the delta and band, or
Nonewith 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):
- No partial totals. A total is
Noneif any contributing row is not COMPUTED. This is the existinglca._common.aggregaterule, applied again at summary level. - Never zero-filled, never hidden. A BLOCKED row renders as BLOCKED with its reason, and it
counts in
blocked_count. NOT_PROVIDEDis notBLOCKED. An optional user input left empty (A4 distances, A5 plant hours,L_iw) givesNOT_PROVIDED. A missing defensible parameter givesBLOCKED. The UI distinguishes them: the user can fixNOT_PROVIDED;BLOCKEDneeds sourcing.- Tags propagate upward. If any row feeding a summary number is
USER-ENTERED,PROVISIONALorÖKOBAUDAT-interim, the summary number carries that tag. - No delta without both totals and a band (§5.4).
- 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 |