The framework answers that question in a fixed path: the full set of system types, the context measured on the ground, a screen that eliminates what cannot work and names the conditions on what can, a ranking of the survivors on fit and on cost, and a recommendation with its reasons attached.
A technology is classified by three questions, each answerable from its datasheet or a site visit, and the answers place it in one of four classes. The classes are not labels chosen by the provider or the planner. They follow from what the technology does with water and with waste, and each class carries its own regulatory route, its own residuals service and its own evidence standard, which is why the class has to be decided before anything else is.
| Class | Definition | Source | Regulatory route | Residuals service | Water demand |
|---|---|---|---|---|---|
| Dry | No water is used to flush. Excreta are handled as solids, with or without urine diversion, and stored, dried, composted or collected on site. | Compendium of Sanitation Systems (Eawag) U.1, U.2 | On-site sanitation by-laws; FSM guidelines (WRC TT 959/25) | Pit or vault emptying, or scheduled container collection | None |
| NSS, water-based | Water is used to flush and the blackwater is contained or treated on the site of generation, with the effluent infiltrated, stored for truck-out or discharged. The treated water is not recovered. | ISO 30500: sanitation not connected to a networked sewer | On-site sanitation by-laws; general authorisation for infiltration or discharge | Tank or pit desludging by truck; conservancy truck-out | Low to high, potable |
| WESS | Non-sewered sanitation that treats at or near the point of generation and recovers the treated water for flushing or reuse. | WASH Centre de-risking protocol; ISO 30500:2025 (SANS 30500); draft model by-laws | SANS 30500 performance; by-law permission to treat and reuse on site without a water-use licence | Periodic sludge removal from the treatment train; consumables supply | Low, recycled |
| Sewered · WWTP | Blackwater is piped off the site of generation to a wastewater treatment works. The works is the defining element and the municipal decision: a central works with its capacity and licence, or a decentralised plant (DEWATS, packaged plant, satellite works) serving a settlement. Two sub-classes, central and decentralised, carried as an attribute. | Compendium conveyance and treatment groups C.1 to C.6, T.3 and T.8 (decentralised), T.12 (works) | Works licence and discharge standard (general or special limit); Green Drop at central scale | Sludge handled at the works | Medium to high, potable |
| Level of service in the EWS Service Level Standards and draft Sanitation Policy | Class | Policy constraint carried as a rule |
|---|---|---|
| Conventional waterborne sanitation: connection to sewerage infrastructure | Sewered · WWTP | Subject to the receiving works' capacity and Green Drop risk class |
| Waterborne with on-site disposal: septic tank and soakaway | NSS, water-based | Emptying by the Sanitation Operations Branch's contractors |
| Waterborne with on-site collection and off-site disposal: conservancy tank, tanker | NSS, water-based | Truck-out interval and route are the cost |
| Urine-diversion double-vault toilet (dry sanitation) | Dry | Policy default for unsewered single households without a metered water connection; not permitted at metered premises; emptied every two years |
| Ventilated improved pit | Dry | No new VIPs permitted; existing ones serviced every five years. The catalogue keeps the VIP as a system and the policy rule eliminates it for new provision in eThekwini |
| Community ablution block on sewer or septic (informal settlements) | Sewered · WWTP or NSS, water-based | The policy standard for informal settlements; the class follows the pipe |
| Chemical toilet | NSS, water-based interim only | Emergency and interim provision |
| Decentralised treatment, non-sewered with treatment, WESS | WESS and decentralised Sewered · WWTP | No category in the current policy. The framework supplies the category and the strategic development plan proposes the policy amendment |
| Question | Observable | Test | Decides |
|---|---|---|---|
| Q1 · Is water used to flush? | Flush volume per use at the user interface, from the datasheet or measured | No water added for conveyance of excreta: dry. Any pour or cistern flush, including chemical units: water-based | dry vs water-based |
| Q2 · Is blackwater piped off site to treatment elsewhere? | Where the blackwater pipe ends: on the site of generation, or at a treatment step under a different location or operator | Pipe crosses the site boundary to a treatment works elsewhere, by conventional, simplified or solids-free sewer: sewered · WWTP. Record whether the works is central or decentralised. Reticulation within one site, such as a school compound or an ablution block to its own plant, is not a sewer | sewered vs non-sewered |
| Q3 · Is treated water recovered on site? | Fate of the treated effluent: recirculated to flush, reused on site, infiltrated, stored, or discharged | Treated water returned to the cistern or reused on site as the design intent, with a treatment train that reaches a reuse standard: WESS. Infiltration, truck-out or discharge without recovery: NSS, water-based | WESS vs NSS |
| System | Q1 flush | Q2 off site | Q3 recovered | Class | Component chain | Water | Scale |
|---|---|---|---|---|---|---|---|
| Ventilated improved pit (VIP) with FSM | no | — | — | Dry | U.1 dry toilet → S.3 VIP → collection → treatment | None | Household |
| Urine-diverting dry toilet (UDDT) | no | — | — | Dry | U.2 UDDT → S.7 dehydration vault · S.1 urine storage → reuse | None | Household |
| Composting system | no | — | — | Dry | U.1 dry toilet → S.8 composting chamber → reuse | None | Household or communal |
| Container-based sanitation, serviced | no | — | — | Dry | U.1 / U.2 → S.16 storage container → scheduled collection → treatment | None | Household |
| Pour-flush to twin leach pits | yes, pour | no | no | NSS, water-based | U.4 pour flush → S.6 twin pits | Low | Household |
| Septic tank with soakaway and FSM | yes | no | no | NSS, water-based | U.5 cistern flush → S.9 septic tank → S.15 soakaway · sludge to treatment | Medium to high | Household |
| Conservancy tank, scheduled truck-out | yes | no | no | NSS, water-based | U.5 cistern flush → S.17 conservancy tank → transport → treatment | High | Household |
| Recirculating household unit | yes | no | yes | WESS | U.5 / U.8 leak-free flush → on-site treatment with recirculation (field record) | Low, recycled | Household |
| Communal WESS with ablution block | yes | no | yes | WESS | Ablution block → biological and polishing treatment with recirculation (field record) | Low, recycled | Communal |
| Settled (small-bore) sewerage | yes | yes | — | Sewered · WWTP | U.5 → S.9 interceptor tank → C.2 solids-free sewer → T.2 DEWATS or satellite works | Medium | Settlement |
| DEWATS: ABR with wetland | yes | yes | — | Sewered · WWTP | U.5 → C.1 simplified sewer → S.10 ABR → constructed wetland → reuse or discharge | Medium to high | Settlement |
| Packaged treatment plant serving a settlement | yes | yes | — | Sewered · WWTP | U.5 → C.1 simplified sewer → packaged biological plant → discharge or reuse | High | Settlement |
| Simplified or condominial sewerage to a works | yes | yes | — | Sewered · WWTP | U.5 → C.1 simplified sewer → T.1 conventional works | Medium to high | Settlement to central |
| Conventional sewerage to a central works | yes | yes | — | Sewered · WWTP | U.5 → C.3 conventional gravity sewer → T.1 conventional works | High | Central |
| Community ablution block on sewer or septic | yes | yes / no | no | Sewered · WWTP or NSS, water-based | U.5 cistern flush (block) → C.3 sewer → T.1 works, or → S.9 septic → S.15 soakaway | Medium to high | Communal |
| Chemical toilet (interim) | yes, chemical | no | no | NSS, water-based | Serviced chemical unit → transport → treatment | None | Household or communal |
| Metric family | Dry | NSS, water-based | WESS | Sewered · central WWTP | Sewered · decentralised plant |
|---|---|---|---|---|---|
| Treatment performance output against the class's standard |
Pathogen inactivation in the residual: helminth ova, E. coli in dried or composted product; moisture and C:N for land application FSM guidelines WRC TT 959/25 · protocol sludge methods |
Effluent to ground or discharge against the general authorisation values: COD, TSS, E. coli, nutrients; tank sludge accumulation and settling DWS general authorisation · protocol settling and accumulation methods |
Treated water against ISO 30500 (SANS 30500) at the recirculation line, bowl and greywater outlet: E. coli, total coliforms, COD, BOD, TSS, TDS, N, P, colour; free chlorine 0.2–0.5 mg/L ISO 30500:2025 · protocol Part VII · 16-step site checklist |
Discharge against the general or special limit; works compliance and risk rating under Green Drop; capacity utilisation against design National Water Act discharge standards · Green Drop | Discharge against the general or special limit at the plant outlet; reuse quality where effluent is reused National Water Act discharge standards · protocol Part VII where reused |
| Process health the reactor, where there is one |
no reactor · composting temperature and turning record where composting | Septic and package units: settling, sludge blanket, DO and ORP where aerated protocol FMHI, subset |
Functional Microbial Health Index: COD profile, DO profile, oxygen uptake, ORP, pH stability, nitrogen transformation, turbidity/TSS, biomass structure → green / amber / red per compartment protocol Part VII, FMHI scorecard |
Works process control: sludge age, DO, settleability, nutrient removal per unit process works operator records · Green Drop process audit | Functional Microbial Health Index on the ABR, wetland or packaged reactor, as for WESS protocol FMHI |
| Operational reliability is it working, continuously |
Fill rate against design life; vault or container availability; odour and fly complaints gap | Uptime; overflow and backup events; pump availability where pumped gap | Percentage uptime; percentage of time within specification; live rating A to E with sensor liveness as the confidence term InfraTrack · protocol Part X |
Time within discharge limits; bypass and overflow events; pump-station availability on the trunk system Green Drop · works records | Percentage uptime; percentage of time within limits; live rating where sensored InfraTrack · protocol Part X |
| Service reliability scheduled tasks done and recorded |
Emptying or collection on schedule and recorded; safe handling compliance O&M sign-off machinery · FSM guidelines |
Desludging on schedule and recorded; truck-out interval kept O&M sign-off machinery |
Provider scorecard: schedule adherence, incident reporting, revision reporting; consumables provisioned; named maintenance party in place 1 Sep provider scorecard · HAZOP register closure rate |
Sewer blockage response time; works maintenance schedule kept and recorded municipal records | Plant maintenance schedule kept and recorded; desludging of the ABR on schedule; named operating party O&M sign-off machinery |
| Residuals handled safely, to a known end point |
Sludge or product removed to a licensed disposal or treatment point; fraction valorised FSM guidelines |
Sludge to a licensed treatment point; disposal route recorded FSM guidelines |
Sludge removal from the treatment train on schedule; helminth ova and pathogen load in the residual; valorisation pathway protocol sludge characterisation |
Sludge handled at the works to the works licence; sludge classification for disposal or beneficial use works licence · sludge guidelines | Sludge removed from the plant on schedule to a licensed point FSM guidelines |
| User side what the user experiences |
Cleanliness, odour, privacy and safety of shared access; user acceptance gap | Availability of flush water; odour; blockage frequency gap | Colour, odour and clarity at the interface; front-end blockage events from inserted materials; availability protocol checklist steps 13 and 16 · HAZOP categories |
Blockage and overflow complaints on the network; availability municipal complaint records | Blockage and overflow complaints; odour at the plant; availability municipal records |
| Cost per household per year, actual |
Emptying cost per event and per year; product revenue where any | Desludging cost per year; water cost of flushing | Consumables, energy and maintenance per year; water saved against a flush baseline protocol Part XII cost de-risking |
Tariff-recovered cost; energy and chemical cost at the works per household connected | Energy, chemical and maintenance cost per household served; sludge removal cost |
A settlement is described by sixteen measurements. Most are physical and can be read from a site visit, a soil test and a map. Three describe the people and the institution: water availability, operating capacity, and reuse interest. Each is stored as measured and evaluated in the band the rules read, so the record says both "water table 3.2 m" and "evaluated in the 2 to 5 m band".
| Measurement | Bands used in the screen | Role |
|---|---|---|
| Housing density | <50 · 50–150 · 150–300 · >300 households/ha | gate |
| Mean plot size | <100 · 100–200 · 200–500 · >500 m² | gate |
| Water supply type | none · borehole · tanker · standpipe · yard tap · house connection | gate |
| Water availability | <10 · 10–25 · 25–60 · 60–100 · >100 L/person/day | gate · score |
| Water table depth | <2 · 2–5 · 5–10 · 10–20 · >20 m | derived → risk |
| Soil type | twelve classes, fractured rock to silty clay | derived → risk |
| Percolation rate | >1000 · 300–1000 · 100–300 · 25–100 · 15–25 · 8–15 · <8 mm/h | gate |
| Terrain | <25° · >25° | gate |
| Flood prone | yes · no | gate |
| Vehicle access | none · peripheral · partial · full | gate |
| Distance to main sewer | <1000 · >1000 m | derived → sewer availability |
| Receiving works: operational capacity and risk | percent of design inflow; Green Drop cumulative risk class (low · medium · high · critical); microbiological compliance | gate · derived → sewer availability |
| Settlement upgrading category | A rapid formalisation · B1 incremental upgrading · B2 deferred relocation · C immediate relocation | gate: B2 and C admit interim systems only |
| Local operating capacity | low · medium · high | score |
| Reuse interest, household and community | yes · no | score |
| Cleansing practice and user-interface setting | water · soft paper · hard material; household · communal | gate: user-interface rules |
| Soil type | < 2 m | 2 – 5 m | 5 – 10 m | 10 – 20 m | > 20 m |
|---|---|---|---|---|---|
| Fractured rock | Extreme | Extreme | High | High | Medium |
| Gravel | Extreme | High | High | Medium | Low |
| Sand, loamy sand | Extreme | High | Medium | Low | Low |
| Loam, silt, sandy loam | High | Medium | Medium | Low | Negligible |
| Clay, clay loam | High | Medium | Low | Negligible | Negligible |
Every system in the catalogue is tested against every gate criterion in the context. The rules are written against components, never against systems, and each names the document it rests on: the Groundwater Protocol for pits, tanks and soak pits against the water table, the national standards for percolation and setbacks, the faecal sludge guidelines for emptying access, the Compendium's technology sheets for water requirement, flooding, terrain and density, ISO 30500 for the water-efficient units, and the Green Drop register for the works. The municipality's policy rules sit beside them. A verdict has three values. Pass means the component is appropriate on that criterion. Fail eliminates the system. Conditional lets the system through with a named condition attached, written as an obligation: a lined pit 30 m from any abstraction point, a low-flush cistern, a long-hose route from the periphery. The register holds 309 rule rows over 26 components and 13 criteria, and a context that presents a band no rule covers is an error the engine raises, never a pass.
Survivors are ranked twice and the two rankings are never blended. Fit is a weighted score on the register's per-system values, construction ease, space, further treatment, reuse potential and water required, on the two cost criteria read from the cost model against a reference cost, and on the operating-risk criteria the WASH Centre field record adds: front-end robustness to inserted materials, dependency on consumables, dependency on desludging and truck access, the skill the maintenance schedule requires against local capacity, and whether the system can be monitored continuously. The planner's three goals from five, water-sensitive design, resource reuse, low cost, minimal maintenance, local jobs, double the weight of the criteria they touch.
Cost is the twenty-year cost per household as a present value: capital, operating cost including consumables and energy, desludging or collection at the frequency and route cost the context implies, and refurbishment on each component's service life.
An illustrative dense settlement, run through the five steps. The planner chose low cost, minimal maintenance and water-sensitive design as goals.
The recommendation is the top row with its conditions, the second row as the alternative, and the eliminated list as the reason no other system is proposed. Every line traces to a criterion, a band and a rule, which is what makes it defensible in a room. The values are illustrative of the surface and are not a calibrated run.
The framework is not a new selection algorithm. Santiago generates options more completely and DMsan characterises sustainability more fully. The claim is a municipal decision method that is verifiable line by line, that takes the municipality's own policy and registers as inputs, that turns conditional verdicts into commissioning obligations, that benchmarks providers within class on the field record, and that recalibrates from monitored outcomes. The gap it addresses is the one Ramôa and colleagues documented in 2018: the process guides exist and practice does not follow them.
| Aspect | Precedent | Taken | Added |
|---|---|---|---|
| Classification | Compendium of Sanitation Systems and Technologies (Tilley et al. 2014): products, five functional groups, waterless versus water-based. ISO 30500:2025 defines non-sewered; ISO 31800:2020 the sludge units. | The component chain as the unit of composition; the waterless split, the conveyance group and the ISO definition as the three tests. | Four classes each with a decidable test; the treatment works explicit as a class; the municipality's accepted levels of service mapped onto the classes. |
| Selection tools | Santiago (Spuhler et al. 2020) generates coherent chains and screens on appropriateness; the Technology Applicability Framework (Olschewski and Casey 2015) scores a technology in context on a traffic light; MCDA for eThekwini (Salisbury et al. 2018) and DMsan (2023). | Hard screen then weighted score; the three-valued verdict; chain composition. | Verdict inheritance along the chain as a stated rule; a versioned municipal policy layer as a second rule source; conditions becoming obligations in the de-risking register. |
| Context criteria | The Groundwater Protocol (DWAF 2003), SANS 10252-2 and 10400-Q, WRC TT 959/25 and the Compendium's technology sheets as the sources of the site criteria; Santiago's appropriateness profiles; WHO Guidelines on Sanitation and Health (2018) and Sanitation Safety Planning (2022); Mitra, Narayan and Lüthi (2022) on criteria for a city-wide mix. | The physical criterion set and its bands. | Receiving works' capacity and Green Drop risk class per works; the settlement's upgrading category as a gate on permanent versus interim systems. |
| Lifecycle cost | Daudey (2018): six of fifty studies costed the whole life; O&M 6 to over 60 percent of total; methods opaque. WASHCost categories; the 2020 standard costing model; Gambrill et al. (2020) on investment drivers; the 2024 systematic review on comparability. | Twenty-year present value per household with the WASHCost categories; whole-chain costing; cost beside fit. | No unsourced cost is shown; every parameter carries a document. |
| Benchmarking | ISO 30500 and 31800 product tests; Green Drop's weighted score (effluent 30 percent) and its critics (Ntombela et al. 2016; Graham et al. 2025); Strande (2024) on integrating NSS advances with monitoring; soft-sensor and surrogate monitoring literature and its calibration rule. | ISO limits for WESS; Green Drop for central works; laboratory rotation calibrating the sensor stream. | One matrix of seven metric families by class; providers benchmarked within class only; the gaps named. |
| Operating risk | Mkhize et al. (2017): 17,499 UDDT households in eThekwini, smell, pedestal failure, emptying; Crous et al. (2013) on CAB demand; Shyu et al. (2021): 534 days of NEWgenerator operation in an eThekwini settlement; the DEWATS O&M manual; the 2023 emptying review. | The failure modes as the five operating-risk criteria. | Scoring them from a HAZOP register per category across monitored sites, and recalibrating from outcomes; the UDDT survey as the calibration case for the dry class. |
| Portfolio | Citywide Inclusive Sanitation (Schrecongost et al. 2020); Spuhler and Lüthi (2020) finding planning still one-size-fits-all; shit-flow diagrams as the baseline instrument. | The technology-mix principle; the settlement typology as the planning unit. | Shared works capacity consumed in sequence, the envelope and phasing to a named count within specification, and the homogeneity trade, computed rather than stated. |
The five steps above fix the structure. A method is a structure a second team can run and get the same answer from, and that needs five further sections, each staked out below with what has to be unpacked into it, what evidence it rests on, and the test that says it is done. Together they are the white paper's sections 4 to 7 and 10.
Each section now carries its model: the equations, the parameters they read, and a first run. Every parameter in the method has one of three statuses, printed beside any result that depends on it. sourced means a named document supplies the value. assumed means a planning assumption of stated basis, adjustable in the register and to be replaced by a sourced value. missing means no value, and anything that needs it is shown as not computable. The register at publication holds 20 sourced and 164 assumed parameters and no missing ones, so every number below computes, and the assumptions are the work that remains rather than the model. A result that rests on assumed parameters may close an assessment only when the planner accepts the assumption list as a named condition, which is the same mechanism the screen uses for any other condition.
Every gate rule in the screen is a row citing the document it rests on, with a grade, and every band a context can present to a component that reads the criterion has a rule.
PublonCore/tasks/todo-sanpath-signoff.md.The fit score has declared criteria, declared weights and declared sources, and the ranking is shown to be stable under reasonable changes to the weights.
A system \(T\) is a chain of components \(k\) and scores on twelve criteria \(i\). Two are read from the cost model against reference costs \(\kappa_k\) and \(\kappa_o\) per household assumed, so that a system at or above the reference scores zero and one at no cost scores one. Five are per-system values \(f_{T,i}\) of the register, read from the Compendium's technology sheets assumed. Five come from the operating-risk register \(q_{c,i}\) of the system's class \(c\).
The operating-risk score of a class on a criterion is a band of the hazard-register count \(n\) per site (per site-year once the monitored period is known), with cut points \(c_1 = 0.5\) and \(c_2 = 1.0\) assumed:
| Criterion | WESS sourced | Dry | NSS | Sewered central | Sewered decentralised |
|---|---|---|---|---|---|
| frontEnd | 0.25 | 0.5 | 0.5 | 0.75 | 0.5 |
| consumables | 0.75 | 1.0 | 1.0 | 1.0 | 0.75 |
| desludging | 0.75 | 0.25 | 0.25 | 1.0 | 0.5 |
| skill | 0.5 | 0.75 | 0.75 | 0.5 | 0.5 |
| telemetry | 0.75 | 0.25 | 0.5 | 0.75 | 0.75 |
The four unmonitored classes carry provisional scores assumed from the class definition, the eThekwini urine-diversion survey and the Green Drop technical-skills scores. Weights \(w_i\), sum \(100\) assumed, one sentence of justification each in the register:
| Criterion | Weight |
|---|---|
| capital | 12 |
| operational | 12 |
| constructionEase | 6 |
| space | 8 |
| furtherTreatment | 6 |
| reuse | 6 |
| waterRequired | 8 |
| frontEnd | 10 |
| consumables | 8 |
| desludging | 8 |
| skill | 10 |
| telemetry | 6 |
Stability is a fraction. Every weight is perturbed by \(\pm\varepsilon\), \(\varepsilon = 0.2\), in turn, and the index is the share of context and perturbation pairs in which the top rank holds, with a sign-off floor of \(7/9\) contexts unmoved assumed:
Run of 4 September 2026 on the primary-source register: \(\sigma = 7/9\) over nine contexts, at the sign-off floor. The two movers are near-ties: the dense settlement with the works at capacity, where the communal water-efficient system and the container-based service sit within a point, and the no-goal context, where the urine-diversion toilet and the simplified sewer do. In the dense settlement the urine-diversion toilet leads on fit at \(72\) against the communal water-efficient system at \(67\), which reverses the paper's illustration, and that is reported rather than adjusted. Two readings are open for sign-off. The register values are right and the framework agrees with the municipality's own default for unsewered households, or the dry class's operating-risk scores are too generous against the survey record. The discrepancy report of section E settles it.
Every number in the twenty-year cost per household has a named source or a stated assumption, and a cost built on assumptions is shown with its assumption list rather than withheld.
Nine parameters per system: capital \(k\), operating cost \(o\), consumables \(c\), energy \(e\), the desludging or collection interval \(\tau\), the cost per event \(p_e\), the route cost per kilometre \(p_r\), the refurbishment interval \(\tau_r\) and the refurbishment fraction \(\varphi\). A communal unit's figures are divided by the households it serves, \(N\). The route length \(L\) is a context measurement. The discount rate \(\rho\) is entered by the planner, and the two rates below serve the hand-check only.
The stance on sources changes in one respect. A cost with an unsourced parameter was to be withheld. It is now shown with its assumption list, so that the model can be checked and the planner can see which numbers move it, and the closing rule admits it only when that list is accepted as a condition. First run at 2026 prices, \(L = 20\) km, every parameter a planning assumption except the two emptying intervals of the Service Level Standards. Rand per household:
| System | \(C_T\), \(\rho = 0.06\) | \(C_T\), \(\rho = 0.10\) | \(k\) | Operating (PV) | Desludging (PV) | Refurbishment (PV) | Status |
|---|---|---|---|---|---|---|---|
| VIP with FSM | 23,426 | 19,971 | 12,000 | 2,294 | 7,122 | 2,010 | assumed |
| UDDT | 32,254 | 27,799 | 16,000 | 3,441 | 10,579 | 2,234 | assumed |
| Composting | 40,432 | 33,498 | 14,000 | 5,735 | 18,352 | 2,345 | assumed |
| Container-based | 57,278 | 43,962 | 6,000 | 10,323 | 35,786 | 5,169 | assumed |
| Pour-flush twin pits | 28,239 | 24,232 | 14,000 | 2,867 | 10,203 | 1,168 | assumed |
| Septic + soakaway | 55,143 | 49,172 | 35,000 | 4,588 | 11,904 | 3,651 | assumed |
| Conservancy tank | 422,481 | 320,898 | 30,000 | 4,588 | 385,389 | 2,504 | assumed |
| Recirculating unit | 119,779 | 99,514 | 45,000 | 37,851 | 19,488 | 17,441 | assumed |
| Communal WESS | 33,067 | 28,575 | 16,667 | 9,463 | 478 | 6,460 | assumed |
| Settled sewerage | 61,374 | 55,186 | 38,000 | 11,470 | 11,904 | 0 | assumed |
| DEWATS | 61,844 | 52,563 | 28,000 | 6,882 | 22,272 | 4,691 | assumed |
| Packaged plant (settlement) | 128,800 | 102,906 | 32,000 | 32,116 | 51,615 | 13,070 | assumed |
| Simplified sewer to works | 39,749 | 37,237 | 30,000 | 9,749 | 0 | 0 | assumed |
| Conventional sewer to works | 55,896 | 53,088 | 45,000 | 10,896 | 0 | 0 | assumed |
| CAB on sewer | 59,122 | 49,970 | 26,667 | 21,563 | 0 | 10,891 | assumed |
| CAB on septic | 63,140 | 53,256 | 28,000 | 21,563 | 2,140 | 11,436 | assumed |
Two things the first run shows about the model rather than the numbers. The conservancy tank is dominated by its monthly emptying, which is the structural reason the Service Level Standards treat it as a last resort, and the model reproduces that from the parameters alone. And the communal systems are sensitive to \(N\): a block serving \(75\) households costs nearly twice per household what the same block serving \(120\) would, so the service population is a parameter the survey must measure and the strategy must allocate. The hand-check is the model's own test: a present value computed by hand at both rates must match the table.
How the sixteen measurements are gathered, by whom, in what time, from which municipal record or field method, and how the planner closes an assessment into a decision. This is the section the municipality writes with the Centre, and it is what makes the paper a joint one.
Each of the sixteen measurements \(m\) has a source tier \(t(m)\): a municipal record, a site visit, a field test or an engagement instrument. The effort of a zone is the sum of the hours the tier implies, and its cost the hours at the day rate of the staff tier that gathers it. Hours per measurement \(h_t\) assumed: record \(0.5\), visit \(0.75\), field test \(3.0\), engagement \(2.0\). Day rates assumed: technician \(\mathrm{R}\,2{,}800\), engineer \(\mathrm{R}\,6{,}500\). The pilot replaces both with timed values.
With ten measurements from records, three from the visit, one field test and two engagement instruments, a zone comes to about \(17\) staff hours, consistent with the \(\mathrm{R}\,15{,}000\) per zone the plan carries once travel and the assessment itself are added. The evidence floor assumed separates a survey from a guess: a gate measurement must come from a record or a field test, a scoring measurement may come from a record, a visit or an engagement instrument, and a derived criterion inherits the floor of its inputs. An assessment whose gate measurements sit below the floor may be run but may not close.
The closing algorithm, as the tool runs it, with \(\beta\) the capital envelope per household:
Three checks in order, each deterministic, and the paper reports their results rather than asserting the method works.
Coverage is a count \(D\) of component, criterion and band triples a context can present that have no rule in the register, with tolerance zero sourced. Result: \(D = 0\) over \(309\) rules. The back-test has two numbers per site. Survival is whether the installed system passes the screen for the site's context. Overlap is the Jaccard similarity between the set \(R\) of categories of the conditions the method raises, from the rule conditions of the screen and from any operating-risk criterion of the class at or below the watch threshold \(\omega = 0.5\) assumed, and the set \(H\) of hazard-register categories the site recorded. Design and process entries are commissioning matters the screen never raises, so a second overlap is computed over the selection-relevant categories \(\mathcal{S} = \{\text{front end}, \text{O\&M}, \text{data}\}\).
The discrepancy report maps observed performance \(u\), percentage uptime or time within specification, to the same four bands the register uses, with cut points \(90\), \(75\) and \(50\) assumed, and flags a class whose register score differs from its observed band by \(\theta = 0.25\) or more assumed. An engineer changes the register. Nothing changes itself.
First run of the back-test on the five sites with hazard registers, each context built from the two or three measurements the site report gives and thirteen or fourteen assumptions that the baseline visit replaces:
| Site | Installed | Survives | Known / assumed | \(R\) | \(H\) | \(J\) / \(J_{\mathcal S}\) |
|---|---|---|---|---|---|---|
| PHO | Communal WESS | yes | 2 / 11 | O&M, front end | design, process | 0.0 / 0.0 |
| EKU | Communal WESS | yes | 2 / 11 | O&M, front end | O&M, design, front end, process | 0.5 / 1.0 |
| OAK | Recirculating unit | yes | 1 / 12 | O&M, front end | front end | 0.5 / 0.5 |
| JOB | Recirculating unit | yes | 2 / 11 | O&M, front end | O&M, design, front end | 0.67 / 1.0 |
| MAL | Communal WESS | yes | 3 / 10 | O&M, front end | O&M, data, design, process | 0.2 / 0.33 |
The run does what the check is for. All five installed systems survive, Ekuthuleni with a condition: its assumed water supply, a communal standpipe, makes top-up water for the recirculating units an obligation under the installation requirement of ISO 30500 rather than a failure, and the pilot measurement decides whether the assumption holds. Where the record holds selection-relevant categories, the method raises them: Nooitgedacht and Ekuthuleni at \(J_{\mathcal S} = 1\), Oakford at \(0.5\) because the method also raises operations and maintenance where the record shows only the front end. Upper Malacca's data-and-reporting entry is the one selection-relevant category the method missed, and its telemetry score for the class is \(0.75\), above the watch threshold, so the threshold or the score is the parameter to revisit. Every number here rests on assumed measurements and is replaced by the pilot's.
Every symbol used in the models, in the order the path uses them. A symbol means one thing throughout. Where the white paper used \(\rho\) for both the discount rate and the operating-risk score, the score is now \(q\).
| Symbol | Meaning | Unit |
|---|---|---|
| Screen and ranking | ||
| \(T\) | a system: a chain of components; \(|T|\) its length | – |
| \(k\) | a component of a chain (a Compendium code, or a CEN code for a component of the Centre's) | – |
| \(i, \ I\) | a criterion; the set of twelve scoring criteria | – |
| \(v_{k,i},\ v_{T,i}\) | the verdict of a component, and of a system, on a gate criterion: pass, conditional or fail, with fail below conditional below pass | ordinal |
| \(f_{T,i}\) | the register value of a system on construction ease, space, further treatment, reuse or water required | unit interval |
| \(\kappa_k,\ \kappa_o\) | the reference capital and twenty-year operating cost at which the cost criteria score zero | R per household |
| \(c(T)\) | the class of a system: dry, non-sewered water-based, WESS, sewered central, sewered decentralised | – |
| \(q_{c,i}\) | the operating-risk score of class \(c\) on criterion \(i\) | unit interval |
| \(n\) | hazard-register entries in a category per site (per site-year once known) | count |
| \(c_1, c_2\) | the cut points of the operating-risk band function \(q(n)\) | entries per site |
| \(s_{T,i}\) | the score of a system on a criterion | unit interval |
| \(w_i\) | the base weight of a criterion | points, sum 100 |
| \(g_i,\ \gamma\) | the goal factor on a criterion; its value when a selected goal touches the criterion | multiplier |
| \(S_T\) | the fit score of a system | 0 to 100 |
| \(\varepsilon\) | the weight perturbation of the stability test | fraction |
| \(Z,\ \sigma\) | the set of test contexts; the stability index | fraction |
| Cost | ||
| \(k\) | capital per household (a communal unit's \(K_{ ext{unit}}\) divided by \(N\)) | R |
| \(o,\ c,\ e\) | operating, consumables and energy cost per household per year | R per year |
| \(\tau,\ p_e\) | the desludging or collection interval; the cost per event per household | years; R |
| \(L,\ p_r\) | the route length one way (a context measurement); the route cost per kilometre per household | km; R per km |
| \(\tau_r,\ \varphi\) | the refurbishment interval; the fraction of capital spent at each refurbishment | years; fraction |
| \(N\) | the households a communal unit serves | households |
| \(\rho,\ H\) | the discount rate the planner enters; the horizon | per year; years |
| \(d_y,\ r_y,\ n_y\) | desludging and refurbishment outlay in year \(y\); events in year \(y\) | R; R; count |
| \(C_T\) | the twenty-year cost per household of a system, a present value | R |
| \(\beta\) | the capital envelope per household the strategy allows | R |
| Survey and closing | ||
| \(m,\ t(m)\) | a measurement of the sixteen; its source tier: record, visit, field test or engagement | – |
| \(h_t\) | staff hours a tier implies per measurement | hours |
| \(E_z,\ \text{cost}_z\) | the effort and cost of profiling a zone | hours; R |
| \(\mathcal{C},\ T^\star\) | the candidate set of the closing algorithm; the recommendation | – |
| Validation | ||
| \(D\) | component, criterion and band triples a context can present that have no rule (coverage) | count |
| \(R,\ H\) | the categories of conditions the method raises; the categories the site's hazard register holds | sets |
| \(\mathcal{S}\) | the selection-relevant categories: front end, operations and maintenance, data | set |
| \(J,\ J_{\mathcal S}\) | the overlap of \(R\) and \(H\), over all categories and over \(\mathcal S\) | Jaccard, 0 to 1 |
| \(\omega\) | the watch threshold: an operating-risk score at or below it raises a condition | unit interval |
| \(u,\ b(u)\) | observed uptime or time within specification; its band | percent; unit interval |
| \(\theta\) | the recalibration trigger on \(|q_{c,i} - b(u)|\) | unit interval |
Every parameter the models read, with its value at publication and a grade of how defensible that value is. The grade is separate from the status badge: a badge says whether a document supplies the value, the grade says how far the value can be defended today and what replaces it. There are \(186\) parameters, and the work that raises each grade is listed by section, including the work still owed on the sourced ones.
| Grade | Meaning | Parameters |
|---|---|---|
| A | sourced from a named document | \(19\) |
| B | derived from sourced figures by stated arithmetic | \(1\) |
| C | a bounded assumption, within a range known from practice or the literature, with a named replacement | \(86\) |
| D | a placeholder of the right order of magnitude, present so the model computes | \(80\) |
| Section | What is done, and what is planned, to develop confidence |
|---|---|
| B · Ranking | The 24 hazard entries are re-categorised at sign-off and normalised per site-year as monitoring periods close. The band cut points and the four provisional class scores are tested by the discrepancy report each quarter against uptime and time within specification. The weights carry a sentence each and are swept at \(\pm 20\) and \(\pm 50\) percent, the second sweep to find where the ranking does move. The per-system register values are replaced by field-record values as a monitored site of each class closes. |
| C · Cost, global | The discount rate is never defaulted: the planner enters the municipality's capital planning rate and both illustrative rates are re-run. The route length becomes a measured distance per zone from the GIS layer. The residual-value assumption is tested by re-running at a 20 percent residual on the sewered class, where it matters most. |
| C · Cost, per system | A hand-computed present value at both rates must match the table (the model's own test). Every placeholder is replaced by a document: provider quotations on the de-risked sites for the WESS class, eThekwini reticulation and works unit rates and the cost per kilolitre treated for the sewered class, DEWATS and packaged-plant quotations for the decentralised class, and the Sanitation Operations Branch contract rates for urine-diversion and pit emptying and the ablution-block programme's capital and caretaker costs for the dry and NSS classes. Until then each value is checked for order of magnitude against the cost review of Daudey (2018) and the costing categories of Sainati and colleagues (2020), and the whole table is re-run whenever one row changes. Even the two sourced intervals are checked against the emptying records for the intervals actually achieved. |
| D · Survey and closing | Hours and day rates are timed in the October pilot on three zones and the protocol is republished in the form the pilot leaves it. The evidence floor is tested by attempting to close an assessment with one gate measurement below it, which must be refused. |
| E · Validation | The watch threshold and the category map are tested on the eight de-risked sites once their contexts are measured, Upper Malacca's missed data category first. The discrepancy bands and trigger are exercised on the first quarter of monitored uptime, and any change to a class score is made by an engineer with the evidence recorded beside it. |
| Parameter | Value | Unit | Grade | Basis |
|---|---|---|---|---|
| B · Ranking | ||||
| systemFit | {"VIP with FSM": {"constructionEase": 1.0, "furtherTreatment": 0.33, "reuse": 0.33, "water… | unit interval | C | COMP technology sheets (construction, operation, applicability and products), ISO 30500 for the water-efficient class, the works register for the sewered class; class-level statements graded C until the field record supplies them |
| goalFactor | \(2\) | multiplier | C | a selected goal doubles the weight of the criteria it touches |
| riskBandCutPoints | [0.5, 1.0] | HAZOP entries per site | C | score 1.0 none · 0.75 up to 0.5 per site · 0.5 up to 1.0 per site · 0.25 above; per site-year once the monitored period is known |
| riskBandScores | [1.0, 0.75, 0.5, 0.25] | unit interval | C | the four bands, best to worst |
| perturbation | \(0.2\) | fraction of each weight | C | a fifth up and down on every weight in turn |
| stabilityFloor | \(0.7778\) | fraction of contexts | C | the top rank must hold in at least seven of nine contexts |
| weights | {"capital": 12, "operational": 12, "constructionEase": 6, "space": 8, "furtherTreatment": … | points, sum 100 | C | tables/operating-risk-scores.md section 3, one sentence each; owner sign-off pending |
| operatingRisk | {"wess": {"frontEnd": 0.25, "consumables": 0.75, "desludging": 0.75, "skill": 0.5, "teleme… | unit interval | B | WESS column from the 24 HAZOP entries (sourced); the other four classes from the class definition, provisional |
| C · Cost, global | ||||
| horizon | \(20\) | years | A | the twenty-year window of the method |
| discountRates | [0.06, 0.1] | per year | C | two illustrative rates for the hand-check; the planner enters the municipality's own rate and there is no default in the tool |
| routeKm | \(20\) | km, one way | C | context measurement: distance from the settlement to the disposal or treatment point |
| residualValue | \(0\) | fraction of capital at year 20 | C | no residual value credited |
| costReference | {"capital": 45000, "operating": 90000} | R per household | C | the cost at which the capital and operating fit criteria score zero: the catalogue's highest capital (conventional sewer) and twice it for the twenty-year non-capital present value; a cost of zero scores one, linear between |
| C · Cost: VIP with FSM | ||||
| capital | \(12{,}000\) | R per household | D | planning assumption at 2026 prices, order of magnitude; replace with the contract rate or quotation |
| opex | \(200\) | R per household per year | D | labour and repairs; excludes consumables and energy |
| consumables | \(0\) | R per household per year | D | chemicals, media, paper, cleaning agents |
| energy | \(0\) | R per household per year | D | electricity or solar replacement cost |
| interval | \(5\) | years between desludging or collection events (0 = none) | A | eThekwini Service Level Standards, 18th ed. (2024): pit emptying every five years |
| eventCost | \(2{,}500\) | R per event per household | D | contractor rate per emptying or collection |
| routeCostPerKm | \(25\) | R per km per event per household | C | vehicle running cost, shared by the households a unit serves; the route length is a context measurement |
| refurbInterval | \(10\) | years | C | service life of the wearing components |
| refurbFraction | \(0.3\) | fraction of capital | C | share of capital spent at each refurbishment |
| householdsPerUnit | \(1\) | households | A | household system |
| C · Cost: UDDT | ||||
| capital | \(16{,}000\) | R per household | D | planning assumption at 2026 prices, order of magnitude; replace with the contract rate or quotation |
| opex | \(300\) | R per household per year | D | labour and repairs; excludes consumables and energy |
| consumables | \(0\) | R per household per year | D | chemicals, media, paper, cleaning agents |
| energy | \(0\) | R per household per year | D | electricity or solar replacement cost |
| interval | \(2\) | years between desludging or collection events (0 = none) | A | eThekwini Service Level Standards, 18th ed. (2024): urine-diversion emptying every two years |
| eventCost | \(900\) | R per event per household | D | contractor rate per emptying or collection |
| routeCostPerKm | \(25\) | R per km per event per household | C | vehicle running cost, shared by the households a unit serves; the route length is a context measurement |
| refurbInterval | \(10\) | years | C | service life of the wearing components |
| refurbFraction | \(0.25\) | fraction of capital | C | share of capital spent at each refurbishment |
| householdsPerUnit | \(1\) | households | A | household system |
| C · Cost: Composting | ||||
| capital | \(14{,}000\) | R per household | D | planning assumption at 2026 prices, order of magnitude; replace with the contract rate or quotation |
| opex | \(400\) | R per household per year | D | labour and repairs; excludes consumables and energy |
| consumables | \(100\) | R per household per year | D | chemicals, media, paper, cleaning agents |
| energy | \(0\) | R per household per year | D | electricity or solar replacement cost |
| interval | \(1\) | years between desludging or collection events (0 = none) | C | planning assumption |
| eventCost | \(600\) | R per event per household | D | contractor rate per emptying or collection |
| routeCostPerKm | \(25\) | R per km per event per household | C | vehicle running cost, shared by the households a unit serves; the route length is a context measurement |
| refurbInterval | \(10\) | years | C | service life of the wearing components |
| refurbFraction | \(0.3\) | fraction of capital | C | share of capital spent at each refurbishment |
| householdsPerUnit | \(1\) | households | A | household system |
| C · Cost: Container-based | ||||
| capital | \(6{,}000\) | R per household | D | planning assumption at 2026 prices, order of magnitude; replace with the contract rate or quotation; collection weekly within the service fee |
| opex | \(600\) | R per household per year | D | labour and repairs; excludes consumables and energy |
| consumables | \(300\) | R per household per year | D | chemicals, media, paper, cleaning agents |
| energy | \(0\) | R per household per year | D | electricity or solar replacement cost |
| interval | \(0.0192\) | years between desludging or collection events (0 = none) | C | planning assumption |
| eventCost | \(60\) | R per event per household | D | contractor rate per emptying or collection |
| routeCostPerKm | \(0\) | R per km per event per household | C | vehicle running cost, shared by the households a unit serves; the route length is a context measurement |
| refurbInterval | \(5\) | years | C | service life of the wearing components |
| refurbFraction | \(0.5\) | fraction of capital | C | share of capital spent at each refurbishment |
| householdsPerUnit | \(1\) | households | A | household system |
| C · Cost: Pour-flush twin pits | ||||
| capital | \(14{,}000\) | R per household | D | planning assumption at 2026 prices, order of magnitude; replace with the contract rate or quotation |
| opex | \(250\) | R per household per year | D | labour and repairs; excludes consumables and energy |
| consumables | \(0\) | R per household per year | D | chemicals, media, paper, cleaning agents |
| energy | \(0\) | R per household per year | D | electricity or solar replacement cost |
| interval | \(3\) | years between desludging or collection events (0 = none) | C | planning assumption |
| eventCost | \(2{,}000\) | R per event per household | D | contractor rate per emptying or collection |
| routeCostPerKm | \(25\) | R per km per event per household | C | vehicle running cost, shared by the households a unit serves; the route length is a context measurement |
| refurbInterval | \(15\) | years | C | service life of the wearing components |
| refurbFraction | \(0.2\) | fraction of capital | C | share of capital spent at each refurbishment |
| householdsPerUnit | \(1\) | households | A | household system |
| C · Cost: Septic + soakaway | ||||
| capital | \(35{,}000\) | R per household | D | planning assumption at 2026 prices, order of magnitude; replace with the contract rate or quotation |
| opex | \(400\) | R per household per year | D | labour and repairs; excludes consumables and energy |
| consumables | \(0\) | R per household per year | D | chemicals, media, paper, cleaning agents |
| energy | \(0\) | R per household per year | D | electricity or solar replacement cost |
| interval | \(3\) | years between desludging or collection events (0 = none) | C | planning assumption |
| eventCost | \(2{,}500\) | R per event per household | D | contractor rate per emptying or collection |
| routeCostPerKm | \(25\) | R per km per event per household | C | vehicle running cost, shared by the households a unit serves; the route length is a context measurement |
| refurbInterval | \(15\) | years | C | service life of the wearing components |
| refurbFraction | \(0.25\) | fraction of capital | C | share of capital spent at each refurbishment |
| householdsPerUnit | \(1\) | households | A | household system |
| C · Cost: Conservancy tank | ||||
| capital | \(30{,}000\) | R per household | D | planning assumption at 2026 prices, order of magnitude; replace with the contract rate or quotation; emptied monthly |
| opex | \(400\) | R per household per year | D | labour and repairs; excludes consumables and energy |
| consumables | \(0\) | R per household per year | D | chemicals, media, paper, cleaning agents |
| energy | \(0\) | R per household per year | D | electricity or solar replacement cost |
| interval | \(0.0833\) | years between desludging or collection events (0 = none) | C | planning assumption |
| eventCost | \(1{,}800\) | R per event per household | D | contractor rate per emptying or collection |
| routeCostPerKm | \(25\) | R per km per event per household | C | vehicle running cost, shared by the households a unit serves; the route length is a context measurement |
| refurbInterval | \(15\) | years | C | service life of the wearing components |
| refurbFraction | \(0.2\) | fraction of capital | C | share of capital spent at each refurbishment |
| householdsPerUnit | \(1\) | households | A | household system |
| C · Cost: Recirculating unit | ||||
| capital | \(45{,}000\) | R per household | D | planning assumption at 2026 prices, order of magnitude; replace with the contract rate or quotation; household WESS unit |
| opex | \(1{,}200\) | R per household per year | D | labour and repairs; excludes consumables and energy |
| consumables | \(1{,}500\) | R per household per year | D | chemicals, media, paper, cleaning agents |
| energy | \(600\) | R per household per year | D | electricity or solar replacement cost |
| interval | \(2\) | years between desludging or collection events (0 = none) | C | planning assumption |
| eventCost | \(2{,}500\) | R per event per household | D | contractor rate per emptying or collection |
| routeCostPerKm | \(25\) | R per km per event per household | C | vehicle running cost, shared by the households a unit serves; the route length is a context measurement |
| refurbInterval | \(7\) | years | C | service life of the wearing components |
| refurbFraction | \(0.35\) | fraction of capital | C | share of capital spent at each refurbishment |
| householdsPerUnit | \(1\) | households | A | household system |
| C · Cost: Communal WESS | ||||
| capital | \(16{,}667\) | R per household | D | planning assumption at 2026 prices, order of magnitude; replace with the contract rate or quotation; one communal unit of R2.0m serving 120 households; unit desludged yearly at R4,000 |
| opex | \(500\) | R per household per year | D | labour and repairs; excludes consumables and energy |
| consumables | \(200\) | R per household per year | D | chemicals, media, paper, cleaning agents |
| energy | \(125\) | R per household per year | D | electricity or solar replacement cost |
| interval | \(1\) | years between desludging or collection events (0 = none) | C | planning assumption |
| eventCost | \(33.33\) | R per event per household | D | contractor rate per emptying or collection |
| routeCostPerKm | \(0.208\) | R per km per event per household | C | vehicle running cost, shared by the households a unit serves; the route length is a context measurement |
| refurbInterval | \(7\) | years | C | service life of the wearing components |
| refurbFraction | \(0.35\) | fraction of capital | C | share of capital spent at each refurbishment |
| householdsPerUnit | \(120\) | households | C | communal unit service population |
| C · Cost: Settled sewerage | ||||
| capital | \(38{,}000\) | R per household | D | planning assumption at 2026 prices, order of magnitude; replace with the contract rate or quotation; interceptor tank, solids-free sewer and a DEWATS share |
| opex | \(900\) | R per household per year | D | labour and repairs; excludes consumables and energy |
| consumables | \(0\) | R per household per year | D | chemicals, media, paper, cleaning agents |
| energy | \(100\) | R per household per year | D | electricity or solar replacement cost |
| interval | \(3\) | years between desludging or collection events (0 = none) | C | planning assumption |
| eventCost | \(2{,}500\) | R per event per household | D | contractor rate per emptying or collection |
| routeCostPerKm | \(25\) | R per km per event per household | C | vehicle running cost, shared by the households a unit serves; the route length is a context measurement |
| refurbInterval | \(20\) | years | C | service life of the wearing components |
| refurbFraction | \(0.3\) | fraction of capital | C | share of capital spent at each refurbishment |
| householdsPerUnit | \(1\) | households | A | household system |
| C · Cost: DEWATS | ||||
| capital | \(28{,}000\) | R per household | D | planning assumption at 2026 prices, order of magnitude; replace with the contract rate or quotation |
| opex | \(600\) | R per household per year | D | labour and repairs; excludes consumables and energy |
| consumables | \(0\) | R per household per year | D | chemicals, media, paper, cleaning agents |
| energy | \(0\) | R per household per year | D | electricity or solar replacement cost |
| interval | \(2\) | years between desludging or collection events (0 = none) | C | planning assumption |
| eventCost | \(3{,}000\) | R per event per household | D | contractor rate per emptying or collection |
| routeCostPerKm | \(25\) | R per km per event per household | C | vehicle running cost, shared by the households a unit serves; the route length is a context measurement |
| refurbInterval | \(10\) | years | C | service life of the wearing components |
| refurbFraction | \(0.3\) | fraction of capital | C | share of capital spent at each refurbishment |
| householdsPerUnit | \(1\) | households | A | household system |
| C · Cost: Packaged plant (settlement) | ||||
| capital | \(32{,}000\) | R per household | D | planning assumption at 2026 prices, order of magnitude; replace with the contract rate or quotation |
| opex | \(1{,}500\) | R per household per year | D | labour and repairs; excludes consumables and energy |
| consumables | \(400\) | R per household per year | D | chemicals, media, paper, cleaning agents |
| energy | \(900\) | R per household per year | D | electricity or solar replacement cost |
| interval | \(1\) | years between desludging or collection events (0 = none) | C | planning assumption |
| eventCost | \(3{,}500\) | R per event per household | D | contractor rate per emptying or collection |
| routeCostPerKm | \(25\) | R per km per event per household | C | vehicle running cost, shared by the households a unit serves; the route length is a context measurement |
| refurbInterval | \(8\) | years | C | service life of the wearing components |
| refurbFraction | \(0.4\) | fraction of capital | C | share of capital spent at each refurbishment |
| householdsPerUnit | \(1\) | households | A | household system |
| C · Cost: Simplified sewer to works | ||||
| capital | \(30{,}000\) | R per household | D | planning assumption at 2026 prices, order of magnitude; replace with the contract rate or quotation; reticulation plus a works share; treatment cost recovered through the tariff |
| opex | \(700\) | R per household per year | D | labour and repairs; excludes consumables and energy |
| consumables | \(0\) | R per household per year | D | chemicals, media, paper, cleaning agents |
| energy | \(150\) | R per household per year | D | electricity or solar replacement cost |
| interval | \(0\) | years between desludging or collection events (0 = none) | C | planning assumption |
| eventCost | \(0\) | R per event per household | D | contractor rate per emptying or collection |
| routeCostPerKm | \(0\) | R per km per event per household | C | vehicle running cost, shared by the households a unit serves; the route length is a context measurement |
| refurbInterval | \(25\) | years | C | service life of the wearing components |
| refurbFraction | \(0.3\) | fraction of capital | C | share of capital spent at each refurbishment |
| householdsPerUnit | \(1\) | households | A | household system |
| C · Cost: Conventional sewer to works | ||||
| capital | \(45{,}000\) | R per household | D | planning assumption at 2026 prices, order of magnitude; replace with the contract rate or quotation; reticulation plus a works share |
| opex | \(800\) | R per household per year | D | labour and repairs; excludes consumables and energy |
| consumables | \(0\) | R per household per year | D | chemicals, media, paper, cleaning agents |
| energy | \(150\) | R per household per year | D | electricity or solar replacement cost |
| interval | \(0\) | years between desludging or collection events (0 = none) | C | planning assumption |
| eventCost | \(0\) | R per event per household | D | contractor rate per emptying or collection |
| routeCostPerKm | \(0\) | R per km per event per household | C | vehicle running cost, shared by the households a unit serves; the route length is a context measurement |
| refurbInterval | \(30\) | years | C | service life of the wearing components |
| refurbFraction | \(0.3\) | fraction of capital | C | share of capital spent at each refurbishment |
| householdsPerUnit | \(1\) | households | A | household system |
| C · Cost: CAB on sewer | ||||
| capital | \(26{,}667\) | R per household | D | planning assumption at 2026 prices, order of magnitude; replace with the contract rate or quotation; one block of R2.0m serving 75 households, a caretaker at R120,000 a year |
| opex | \(1{,}600\) | R per household per year | D | labour and repairs; excludes consumables and energy |
| consumables | \(200\) | R per household per year | D | chemicals, media, paper, cleaning agents |
| energy | \(80\) | R per household per year | D | electricity or solar replacement cost |
| interval | \(0\) | years between desludging or collection events (0 = none) | C | planning assumption |
| eventCost | \(0\) | R per event per household | D | contractor rate per emptying or collection |
| routeCostPerKm | \(0\) | R per km per event per household | C | vehicle running cost, shared by the households a unit serves; the route length is a context measurement |
| refurbInterval | \(8\) | years | C | service life of the wearing components |
| refurbFraction | \(0.4\) | fraction of capital | C | share of capital spent at each refurbishment |
| householdsPerUnit | \(75\) | households | C | communal unit service population |
| C · Cost: CAB on septic | ||||
| capital | \(28{,}000\) | R per household | D | planning assumption at 2026 prices, order of magnitude; replace with the contract rate or quotation; the block on a septic tank emptied quarterly at R2,500 |
| opex | \(1{,}600\) | R per household per year | D | labour and repairs; excludes consumables and energy |
| consumables | \(200\) | R per household per year | D | chemicals, media, paper, cleaning agents |
| energy | \(80\) | R per household per year | D | electricity or solar replacement cost |
| interval | \(0.25\) | years between desludging or collection events (0 = none) | C | planning assumption |
| eventCost | \(33.33\) | R per event per household | D | contractor rate per emptying or collection |
| routeCostPerKm | \(0.333\) | R per km per event per household | C | vehicle running cost, shared by the households a unit serves; the route length is a context measurement |
| refurbInterval | \(8\) | years | C | service life of the wearing components |
| refurbFraction | \(0.4\) | fraction of capital | C | share of capital spent at each refurbishment |
| householdsPerUnit | \(75\) | households | C | communal unit service population |
| D · Survey and closing | ||||
| tiers | {"record": "GIS, billing, works register, flood lines, settlement register", "visit": "sit… | source tier | A | Appendix A of the white paper |
| hoursPerMeasurement | {"record": 0.5, "visit": 0.75, "field test": 3.0, "engagement": 2.0} | hours | C | staff time per measurement by tier, timed in the pilot |
| dayRate | {"technician": 2800, "engineer": 6500} | R per day | C | municipal cost-to-company day rates; replace with the municipality's own |
| zoneCostEstimate | \(15{,}000\) | R per zone | C | Centre estimate: two site-visit days, a percolation test, GIS and record extraction, the assessment (Research-Notes-EWS-Plan) |
| evidenceFloor | {"gate": "record or field test", "score": "record, visit or engagement", "derived": "input… | minimum tier | C | a gate measurement may not close an assessment from an assumption |
| closingRule | ["candidates = survivors minus systems the policy layer excludes for the upgrading categor… | rule | A | printed verbatim in the method |
| E · Validation | ||||
| ruleCoverage | \(1\) | fraction of (component, criterion, band) triples | A | every triple a context can present has a rule in the register; a gap is an error, never a pass |
| conditionCategories | {"front end": ["frontEnd"], "O&M": ["consumables", "desludging", "skill"], "data": ["telem… | map | C | which operating-risk criteria correspond to which HAZOP category; design and process are not selection conditions |
| watchThreshold | \(0.5\) | unit interval | C | an operating-risk score at or below this raises a watch condition for the class |
| overlapMetric | Jaccard | set similarity | C | raised condition categories against HAZOP categories recorded, per site |
| discrepancyBands | [90, 75, 50] | percent uptime or time within specification | C | observed performance mapped to the four risk bands: above 90 = 1.0, 75 to 90 = 0.75, 50 to 75 = 0.5, below 50 = 0.25 |
| recalibrationTrigger | \(0.25\) | absolute score difference | C | a class score differing from its observed band by this much is flagged for an engineer; nothing adjusts itself |
| Closing envelope | ||||
| budgetCap (\(\beta\)) | \(30{,}000\) | R capital per household | C | a working envelope per household; the plan's R1 to 2 billion over 225,799 B1 households is R4,400 to R8,900 |
Three archetypes of the incremental-upgrading register, each run through the whole path with every parameter at its register value: the screen with its deciding rules, the municipal policy layer, fit beside cost, and the closing algorithm to one recommendation. The zones are types, not named settlements, and every measurement is an assumption of the kind the survey replaces. What the runs show is the machinery: which rule eliminates what, where the policy layer bites, how far fit and cost disagree, and what the envelope removes. Read with the values section open.
A settlement of the kind that dominates the register: very high density on public standpipes, low water availability, a shallow water table over sandy clay, peripheral vehicle access, more than a kilometre from a main sewer and no works with headroom within reach.
| projectType | Communal |
| toiletLocation | Outside dwelling |
| waterType | Communal standpipes: Piped water connection for public use |
| waterAmount | Low (10-25 litres/person/day) |
| floodProne | No |
| soil | Sandy Clay |
| waterTableDepth | 2-5 metres |
| housingDensity | Very High: >300 HHs per hectare |
| percolation | 15 - 25 mm/hour |
| access | Peripheral access - access to the periphery of the area |
| terrain | <25° |
| sewerDistance | >1000 metres |
| wwtpCapacity | No |
| System | \(S_T\) | \(C_T\), \(\rho = 0.06\) | \(k\) | Conditions |
|---|---|---|---|---|
| UDDT | \(70.2\) | \(32{,}254\) | \(16{,}000\) | \(2\) |
| Composting | \(68.3\) | \(40{,}432\) | \(14{,}000\) | \(1\) |
| Communal WESS | \(65.9\) | \(33{,}067\) | \(16{,}667\) | \(2\) |
| Container-based | \(65.4\) | \(57{,}278\) | \(6{,}000\) | \(0\) |
| Recirculating unit | \(46.8\) | \(119{,}779\) | \(45{,}000\) | \(3\) |
Medium density on yard taps with medium water availability, sandy loam over a water table at five to ten metres, partial vehicle access, no sewer within a kilometre.
| projectType | Household |
| toiletLocation | Outside dwelling |
| waterType | Yard tap: Single tap provided in each plot |
| waterAmount | Medium (25-60 litres/person/day) |
| floodProne | No |
| soil | Sandy Loam |
| waterTableDepth | 5-10 metres |
| housingDensity | Medium: 50-150 HHs per hectare |
| percolation | 25 - 100 mm/hour |
| access | Partial access - Access to several households in the area |
| terrain | <25° |
| sewerDistance | >1000 metres |
| wwtpCapacity | No |
| System | \(S_T\) | \(C_T\), \(\rho = 0.06\) | \(k\) | Conditions |
|---|---|---|---|---|
| UDDT | \(73.3\) | \(32{,}254\) | \(16{,}000\) | \(0\) |
| Composting | \(71.9\) | \(40{,}432\) | \(14{,}000\) | \(0\) |
| Container-based | \(70.8\) | \(57{,}278\) | \(6{,}000\) | \(0\) |
| Pour-flush twin pits | \(65.1\) | \(28{,}239\) | \(14{,}000\) | \(1\) |
| DEWATS | \(52.8\) | \(61{,}844\) | \(28{,}000\) | \(2\) |
| Recirculating unit | \(49.9\) | \(119{,}779\) | \(45{,}000\) | \(0\) |
| Settled sewerage | \(48.7\) | \(61{,}374\) | \(38{,}000\) | \(2\) |
| CAB on septic | \(48.5\) | \(63{,}140\) | \(28{,}000\) | \(2\) |
| Septic + soakaway | \(45.9\) | \(55{,}143\) | \(35{,}000\) | \(2\) |
| Packaged plant (settlement) | \(45.1\) | \(128{,}800\) | \(32{,}000\) | \(2\) |
| Conservancy tank | \(39.0\) | \(422{,}481\) | \(30{,}000\) | \(2\) |
High density on metered house connections, loam over a deep water table, full vehicle access, a main sewer within a kilometre and a low-risk works with headroom (Northern) as the receiving works.
| projectType | Household |
| toiletLocation | Either inside or outside |
| waterType | Household connection: Metered connection into the house. |
| waterAmount | Medium high (60-100 litres/person/day) |
| floodProne | No |
| soil | Loam |
| waterTableDepth | >10 metres |
| housingDensity | High: 150-300 HHs per hectare |
| percolation | 25 - 100 mm/hour |
| access | Full access - access to all households inside the area |
| terrain | <25° |
| sewerDistance | <1000 metres |
| wwtpCapacity | Yes |
| System | \(S_T\) | \(C_T\), \(\rho = 0.06\) | \(k\) | Conditions |
|---|---|---|---|---|
| Composting | \(69.9\) | \(40{,}432\) | \(14{,}000\) | \(0\) |
| Container-based | \(68.7\) | \(57{,}278\) | \(6{,}000\) | \(0\) |
| Simplified sewer to works | \(65.5\) | \(39{,}749\) | \(30{,}000\) | \(0\) |
| Pour-flush twin pits | \(65.0\) | \(28{,}239\) | \(14{,}000\) | \(1\) |
| CAB on sewer | \(64.3\) | \(59{,}122\) | \(26{,}667\) | \(0\) |
| Conventional sewer to works | \(57.3\) | \(55{,}896\) | \(45{,}000\) | \(0\) |
| DEWATS | \(54.2\) | \(61{,}844\) | \(28{,}000\) | \(0\) |
| Settled sewerage | \(49.8\) | \(61{,}374\) | \(38{,}000\) | \(1\) |
| CAB on septic | \(49.6\) | \(63{,}140\) | \(28{,}000\) | \(1\) |
| Recirculating unit | \(47.6\) | \(119{,}779\) | \(45{,}000\) | \(0\) |
| Septic + soakaway | \(46.8\) | \(55{,}143\) | \(35{,}000\) | \(1\) |
| Packaged plant (settlement) | \(45.9\) | \(128{,}800\) | \(32{,}000\) | \(0\) |
| Conservancy tank | \(39.4\) | \(422{,}481\) | \(30{,}000\) | \(0\) |
Three observations, about the method rather than the settlements. In the dense zone the screen alone removes thirteen of sixteen systems, most on the water gate and the pit-vulnerability gate, and the ranking is among three dry systems, so the decision there is a dry-class decision whatever the weights. In the peri-urban zone the policy layer removes the ventilated pit that the screen admitted, and the envelope of \(\mathrm{R}\,30{,}000\) removes the household water-efficient unit at \(\mathrm{R}\,45{,}000\) capital, so the recommendation follows from two rules a reader can point to. In the sewered-reach zone the highest fit is the container-based system at \(69.9\) while the simplified sewer connection at \(65.5\) costs \(\mathrm{R}\,39{,}749\) against \(\mathrm{R}\,40{,}432\) over twenty years. The closing rule takes fit, and the record shows the planner the \(\mathrm{R}\,683\) that the choice forgoes, which is the trade the two-column display exists to expose. Whether the container-based system should outrank a sewer connection where a works has headroom is a question for the weights and the operating-risk scores at sign-off, and the case study is the evidence for that conversation.
The arithmetic of the envelope deserves its own line. The ten-year plan's \(\mathrm{R}\,1\) to \(\mathrm{R}\,2\) billion for underserved-community sanitation over the \(225{,}799\) households in incremental upgrading is \(\mathrm{R}\,4{,}400\) to \(\mathrm{R}\,8{,}900\) per household, below the capital of every system in the catalogue. The strategy layer is where that is confronted: phasing, the count of households actually reached in the window, and the classes whose capital sits nearest the envelope.
The fit score and the closing rule encode a view of what a municipality wants, and that view is a decision the municipality makes rather than a property of the method. The method therefore treats desirability as a named policy, and a recommendation is optimal relative to a policy. Two municipalities with the same catalogue and the same rules can hold different policies and receive different recommendations for the same zone, each with its reasoning, and one municipality can compare its own policy against alternatives before adopting it.
A policy \(\pi\) declares seven things: the base weights \(w_i^{\pi}\), the goal factor \(\gamma^{\pi}\), the goals \(G^{\pi}\) it forces on every zone, the exclusion rules \(X^{\pi}\) drawn from the municipality's own policy documents, the capital envelope \(\beta^{\pi}\), the margin \(\delta^{\pi}\) within which the cheaper of two near-equal systems is preferred, and the discount rate \(\rho^{\pi}\) it costs at. The screen is not part of a policy: a system that cannot work in a context cannot be made desirable.
With \(\delta^{\pi} = 0\) the rule is strict fit with cost as tie-break, which is the closing rule of section D. With \(\delta^{\pi} > 0\) cost decides among systems the policy regards as near-equal on fit. The alignment of two policies over a set of zones \(Z\) is the share of zones on which they recommend the same system:
Four policies are registered, every one assumed. The first, the municipality's draft policy read as exclusion clauses with the register's weights, is the working default; the other three are the comparisons a planner puts beside it.
| Policy | Weights that differ from the register | Goals forced | Exclusions | \(\beta\) | \(\delta\) |
|---|---|---|---|---|---|
| P1 eThekwini draft policy (2025) | the register weights | – | no new ventilated pits; no urine diversion at metered premises; B2 and C settlements: interim systems only | \(30{,}000\) | \(0.0\) |
| P2 Least lifecycle cost | the register weights | lowCost | B2 and C settlements: interim systems only | \(30{,}000\) | \(10.0\) |
| P3 Water-sensitive and circular | capital 9.8, operational 9.8, constructionEase 4.9, space 6.5, furtherTreatment 4.9, reuse 14, waterRequired 16, frontEnd 8.1, consumables 6.5, desludging 6.5, skill 8.1, telemetry 4.9 | waterSensitive, reuse | no new ventilated pits; no urine diversion at metered premises; B2 and C settlements: interim systems only | \(30{,}000\) | \(0.0\) |
| P4 Operations first | capital 9.1, operational 9.1, constructionEase 4.6, space 6.1, furtherTreatment 4.6, reuse 4.6, waterRequired 6.1, frontEnd 14, consumables 10, desludging 10, skill 14, telemetry 8 | minimalOM | no new ventilated pits; no urine diversion at metered premises; B2 and C settlements: interim systems only | \(30{,}000\) | \(5.0\) |
The three case-study zones under each policy, with the recommendation, its fit under that policy and its twenty-year cost:
| Zone | P1 | P2 | P3 | P4 |
|---|---|---|---|---|
| Z1 Dense B1 settlement beyond sewer reach | UDDT \(70.2\) · \(\mathrm{R}\,32{,}254\) | UDDT \(70.2\) · \(\mathrm{R}\,32{,}254\) | UDDT \(78.3\) · \(\mathrm{R}\,32{,}254\) | UDDT \(67.4\) · \(\mathrm{R}\,32{,}254\) |
| Z2 Peri-urban B1 zone on yard taps, on-site service | UDDT \(73.3\) · \(\mathrm{R}\,32{,}254\) | VIP with FSM \(69.5\) · \(\mathrm{R}\,23{,}426\) | UDDT \(81.3\) · \(\mathrm{R}\,32{,}254\) | Pour-flush twin pits \(64.8\) · \(\mathrm{R}\,28{,}239\) |
| Z3 B1 settlement within the catchment of a works with headroom | Composting \(69.9\) · \(\mathrm{R}\,40{,}432\) | VIP with FSM \(67.3\) · \(\mathrm{R}\,23{,}426\) | Composting \(80.4\) · \(\mathrm{R}\,40{,}432\) | Simplified sewer to works \(72.0\) · \(\mathrm{R}\,39{,}749\) |
Alignment \(A(\pi, \pi')\) over the three zones:
| P1 | P2 | P3 | P4 | |
|---|---|---|---|---|
| P1 | \(1.0\) | \(0.33\) | \(1.0\) | \(0.33\) |
| P2 | \(0.33\) | \(1.0\) | \(0.33\) | \(0.33\) |
| P3 | \(1.0\) | \(0.33\) | \(1.0\) | \(0.33\) |
| P4 | \(0.33\) | \(0.33\) | \(0.33\) | \(1.0\) |
Agreement counts recommendations; it does not say how far a wrong choice falls short. The graded measure follows Chandarman (2026), whose matching algorithms each embed an optimality principle and whose networks are scored under a policy's desirability index against that policy's own optimum. Here the algorithm is a decision rule applied across the zones, its network is the allocation it produces, and a policy's desirability of an allocation is the household-weighted mean fit under that policy of the systems chosen, a policy-excluded system counting zero. Alignment is that desirability relative to the policy's own closing:
Seven decision rules are scored: closing under each of the four policies, and three practices a municipality might run without a method, the eThekwini default (sewer where available and metered, urine diversion where unsewered and unmetered, container-based where nothing on-site survives), least capital, and least lifecycle cost.
| Decision rule | Align to P1 | Align to P2 | Align to P3 | Align to P4 | Allocation Z1 · Z2 · Z3 |
|---|---|---|---|---|---|
| P1 | \(1.0\) | \(1.02\) | \(1.0\) | \(0.98\) | UDDT · UDDT · Composting |
| P2 | \(0.5\) | \(1.0\) | \(0.49\) | \(0.49\) | UDDT · VIP with FSM · VIP with FSM |
| P3 | \(1.0\) | \(1.02\) | \(1.0\) | \(0.98\) | UDDT · UDDT · Composting |
| P4 | \(0.96\) | \(0.98\) | \(0.85\) | \(1.0\) | UDDT · Pour-flush twin pits · Simplified sewer to works |
| practice | \(0.98\) | \(1.0\) | \(0.89\) | \(1.01\) | UDDT · UDDT · Simplified sewer to works |
| least capital | \(0.95\) | \(0.98\) | \(0.88\) | \(0.93\) | Container-based · Container-based · Container-based |
| least lifecycle cost | \(0.5\) | \(1.0\) | \(0.49\) | \(0.49\) | UDDT · VIP with FSM · VIP with FSM |
Three readings. The municipality's current practice is already close to its own draft policy at \(0.98\) and coincides with the operations-first policy, so the method's first value to eThekwini is the reasons trail and the cost column rather than a different answer. The least-cost rule aligns at only \(0.5\) with the draft policy, because two of its three choices are systems the policy excludes, which is exactly the number a committee needs when someone proposes cost as the sole criterion. And least capital chooses the container-based system everywhere and still aligns at \(0.95\), which says the fit surface is flat among the survivors of the dense and peri-urban zones and that the decision there is being made by cost and conditions, not by fit.
The dense zone is policy-robust: every policy closes on the urine-diversion toilet, because the screen leaves three dry systems and no weighting separates them the other way. The other two zones are policy-sensitive. The least-cost policy, which carries no clause against new pits, recommends the ventilated pit in the peri-urban zone and, with no clause on metered premises, the urine-diversion toilet in the sewered-reach zone. The operations-first policy recommends the pour-flush twin pits in the peri-urban zone and the simplified sewer connection where the works has headroom, which is the answer the cost column already pointed to under the municipal policy. The municipal and the circular policies agree on every zone. Optimality aligns to a policy to the extent the alignment table shows, and the table is what a planner puts in front of a committee: which zones every policy agrees on, and on which zones the choice of policy is the decision.
This page is the method as a reader follows it; the system is the same method as a municipality runs it. Everything above was written so that a second team could arrive at the same answer, and the test of that claim is a system that holds the catalogue, the criteria, the rules, the parameters and the policies as data, takes a settlement's measurements in, and returns the verdicts, the ranking, the cost and the closing with every reason attached. The reference implementation behind this page, a folder of scripts, is the oracle: the platform must return the same numbers on the same contexts, or a release does not ship.
The system is built on the platform the de-risking programme already uses. Its discipline is short: tables are the only truth, every change goes through a service verb that emits an event, every screen is a subscriber, and nothing is ever seeded. The system version therefore derives, section by section, from this page. Each row below names the section, what the system takes from it, and where that lands.
| Section of this page | What the system takes from it | Where it lands |
|---|---|---|
| Step 1 · Types | The sixteen systems as chains of catalogue components, the four classes, the three class tests | system (name, class, chain, the three answers) · component (Compendium and CEN codes, group, family) · the Units tab (templates by category) and the Register tab |
| Step 2 · Context | The sixteen measurements, their bands and cut points, the evidence tier of each, the two derived criteria and the works they read | criterion (bands, unit, cut points, derivation, grade) · context (measurements as bands, tier per measurement, zone, works) · zone and works registers · the Sites tab with the survey form |
| Step 3 · Screen | The rule register: a row per component, criterion and band with its verdict, condition, document and grade; the worst verdict along a chain | rule (family rows expanded per component at import) · the engine verbs deriveContext and screen · the verdicts on the Suitability tab |
| Step 4 · Rank | Fit as a weighted score under a policy; the twenty-year cost per household; the two never blended | parameter (per-system fit values, operating risk, weights, cost parameters, the reference cost) · policy (weights, goals, goal factor) · the verbs score and cost · the fit–cost plot |
| Step 5 · Result | The closing algorithm, the conditions carried as obligations, the reasons trail, the exported assessment | assessment and assessmentRow · the verb close · the closing trail, exportAssessment and saveDocument on the Suitability tab |
| Policies | A desirability calculation as a named policy; exclusion clauses as data; alignment of a decision rule to a policy | policy (envelope, margin, discount, clauses as {label, when}) · the verbs compare and alignRules |
| Sections A to E | The models and their first runs: the register, the numbers on the ranking, the cost model, the survey effort and evidence floor, the three validation checks | The engine's arithmetic, the evidence floor enforced at closing, the validation harness (build step 6) |
| Variables and Values | Every adjustable number with its status, grade, basis and the work that raises the grade | parameter as an editable register with history; the Centre engineer's surface |
| Case studies | The three archetype zones under four policies, the back-test sites, the alignment matrix | The acceptance file the platform is tested against: the same contexts, the same numbers, three proofs run on every change |
| Literature and Behind the path | The framework's position and the machinery behind each step | The About area, the living documentation |
16 tables under the san_ prefix, read straight from the service declaration. A reference-shaped column names the table it points to, so every form renders it as a selector and every list can join on it; an enumeration lists its values, so a form renders a select; a date-shaped column is typed as one, so it gets a picker. Codes and names that ride beside a reference (componentCode next to componentId, systemName next to systemId) exist for the label and the register's own language; the reference is the join.
Every reference is auto-wired by the platform as a parent-to-child scope and a child-to-parent selection, which is right for a row with one parent and wrong for a row with two: an assessment row belongs to its assessment, and selecting it must not select its system and rescope the list by system. The declaration therefore names the references that are joins only: assessmentRow.systemId, allocation.systemId, allocation.assessmentId, allocation.zoneId, allocation.worksId, assessment.policyId, context.worksId, context.zoneId, context.unitId, strategy.policyId, unit.companyId, unit.systemId, observation.unitId. Each row keeps one scoping parent, the surface's.
system · Systems| Column | Type · reference · values | Label | Group | |
|---|---|---|---|---|
idx | string | ID | key | |
name | string | Name | essential | required |
class | enum {dry, nss, wess, sewered, sewdec} | Class | essential | required |
chain | json | Chain (component codes) | essential | |
flushes | boolean | Q1 flush? | contextual | |
pipedToWorks | boolean | Q2 piped to a works? | contextual | |
waterRecovered | boolean | Q3 water recovered? | contextual | |
source | string | Source | system | |
createdAt | datetime | Created | system |
component · Components| Column | Type · reference · values | Label | Group | |
|---|---|---|---|---|
idx | string | ID | key | |
code | string | Code | essential | required |
name | string | Name | essential | required |
group | enum {UI, S, C, T, D} | Group | essential | required |
family | string | Family (rules address it) | essential | |
source | string | Source | system | |
createdAt | datetime | Created | system |
criterion · Criteria| Column | Type · reference · values | Label | Group | |
|---|---|---|---|---|
idx | string | ID | key | |
key | string | Key | essential | required |
label | string | Label | essential | required |
role | enum {gate, score, derived, both} | Role | essential | required |
unit | string | Unit | essential | |
bands | json | Bands | contextual | |
cuts | string | Cut points | contextual | |
grade | enum {A, B, C, D} | Grade | contextual | |
weight | float | Weight | contextual | |
goal | string | Goal that doubles it | contextual | |
aggregation | enum {mean, min, class} | Chain aggregation | contextual | |
derivation | json | Derivation (lookup or source table) | contextual | |
source | string | Source | system | |
createdAt | datetime | Created | system |
rule · Rules| Column | Type · reference · values | Label | Group | |
|---|---|---|---|---|
idx | string | ID | key | |
componentId | string → component | Component | essential | required |
componentCode | string | Component code | essential | required |
criterionId | string → criterion | Criterion | essential | required |
criterionKey | string | Criterion key | essential | required |
band | string | Band | essential | required |
verdict | enum {pass, cond, fail} | Verdict | essential | required |
condition | string | Condition (obligation) | contextual | |
sourceKind | enum {register, fieldRecord} | Source kind | contextual | required |
sourceRef | string | Source document / clause / HAZOP id | contextual | |
grade | enum {A, B, C, D} | Grade | contextual | |
createdAt | datetime | Created | system |
parameter · Parameters| Column | Type · reference · values | Label | Group | |
|---|---|---|---|---|
idx | string | ID | key | |
section | string | Section | essential | required |
key | string | Parameter | essential | required |
value | json | Value | essential | |
unit | string | Unit | essential | |
status | enum {sourced, assumed, missing} | Status | essential | required |
grade | enum {A, B, C, D} | Grade | essential | |
basis | text | Basis | contextual | |
range | json | Range | contextual | |
source | string | Source document | contextual | |
createdAt | datetime | Created | system | |
updatedAt | datetime | Updated | system |
policy · Policies| Column | Type · reference · values | Label | Group | |
|---|---|---|---|---|
idx | string | ID | key | |
code | string | Code | essential | required |
name | string | Name | essential | required |
basis | text | Basis | contextual | |
weights | json | Weights | essential | |
goalFactor | float | Goal factor | essential | |
goals | json | Goals forced | essential | |
exclusions | json | Exclusion clauses | essential | |
envelope | float | Capital envelope per household (R) | essential | |
margin | float | Fit margin at closing | essential | |
discount | float | Discount rate | contextual | |
status | enum {sourced, assumed, missing} | Status | contextual | |
createdAt | datetime | Created | system |
company · Companies| Column | Type · reference · values | Label | Group | |
|---|---|---|---|---|
idx | string | ID | key | |
name | string | Company | essential | required |
kind | enum {manufacturer, provider, municipality, research} | Kind | essential | required |
contact | string | Contact | contextual | |
source | string | Source | system | |
createdAt | datetime | Created | system |
unit · Units| Column | Type · reference · values | Label | Group | |
|---|---|---|---|---|
idx | string | ID | key | |
companyId | string → company | Company | essential | required no cascade |
name | string | Unit | essential | required |
class | enum {dry, nss, wess, sewered, sewdec} | Category | essential | required |
systemId | string → system | Catalogued template | essential | no cascade |
capacityHouseholds | integer | Households per unit | contextual | |
capitalPerHh | float | Capital per household (R) | contextual | |
opexPerHh | float | Operating cost per household per year (R) | contextual | |
status | enum {monitored, available, withdrawn} | Status | contextual | |
source | string | Source | system | |
createdAt | datetime | Created | system |
zone · Zones| Column | Type · reference · values | Label | Group | |
|---|---|---|---|---|
idx | string | ID | key | |
name | string | Settlement | essential | required |
municipality | string | Municipality | contextual | |
planningUnit | string | Planning unit | contextual | |
ward | string | Ward | contextual | |
households | integer | Households | essential | |
upgradingCategory | string | Upgrading category | essential | |
lat | float | Latitude | contextual | |
lng | float | Longitude | contextual | |
geometrySource | string | Geometry source | contextual | |
source | string | Source | system | |
createdAt | datetime | Created | system |
works · Treatment works| Column | Type · reference · values | Label | Group | |
|---|---|---|---|---|
idx | string | ID | key | |
name | string | Works | essential | required |
technology | string | Technology | contextual | |
classOfWorks | string | Class of works | contextual | |
designCapacityKld | float | Design capacity (kL/d) | essential | |
operationalCapacityPct | float | Operational capacity (%) | essential | |
microCompliancePct | float | Microbiological compliance (%) | contextual | |
physicalCompliancePct | float | Physical compliance (%) | contextual | |
chemicalCompliancePct | float | Chemical compliance (%) | contextual | |
technicalSkillsPct | float | Technical skills (%) | contextual | |
crr2023Pct | float | Cumulative risk 2023 (%) | contextual | |
riskClass | string | Risk class | essential | |
source | string | Source | system | |
createdAt | datetime | Created | system |
context · Contexts| Column | Type · reference · values | Label | Group | |
|---|---|---|---|---|
idx | string | ID | key | |
zoneId | string → zone | Zone | essential | no cascade |
worksId | string → works | Receiving works | contextual | |
label | string | Label | essential | required |
measurements | json | Measurements (key → band) | essential | |
tiers | json | Tier per measurement | contextual | |
goals | json | Zone goals | contextual | |
metered | boolean | Metered premises | contextual | |
installedSystem | string | Installed system (monitored site) | contextual | |
unitId | string → unit | Installed unit | contextual | no cascade |
hazardCategories | json | Hazard-register categories recorded | contextual | |
surveyedAt | datetime | Surveyed | system | |
createdAt | datetime | Created | system |
assessment · Assessments| Column | Type · reference · values | Label | Group | |
|---|---|---|---|---|
idx | string | ID | key | |
contextId | string → context | Context | essential | required no cascade |
policyId | string → policy | Policy | essential | required |
policyCode | string | Policy code | essential | |
label | string | Label | essential | |
recommendation | string | Recommendation | essential | |
closingTrail | json | Closing trail | contextual | |
assumedParams | json | Assumed parameters accepted | contextual | |
runAt | datetime | Run | system | |
createdAt | datetime | Created | system |
assessmentRow · Assessment rows| Column | Type · reference · values | Label | Group | |
|---|---|---|---|---|
idx | string | ID | key | |
assessmentId | string → assessment | Assessment | essential | required |
systemId | string → system | System | essential | required |
systemName | string | System name | essential | required |
verdict | enum {pass, cond, fail} | Verdict | essential | required |
decidingRules | json | Deciding rules | contextual | |
conditions | json | Conditions | contextual | |
fit | float | Fit | essential | |
fitParts | json | Fit decomposition | contextual | |
cost | float | Twenty-year cost per household | essential | |
costParts | json | Cost parts | contextual | |
excludedBy | string | Excluded by | contextual | |
createdAt | datetime | Created | system |
observation · Observations| Column | Type · reference · values | Label | Group | |
|---|---|---|---|---|
idx | string | ID | key | |
class | enum {dry, nss, wess, sewered, sewdec} | Class | essential | required |
metric | enum {uptime, timeWithinSpec} | Metric | essential | required |
value | float | Value (%) | essential | required |
unit | string | Unit | contextual | |
sites | integer | Sites observed | contextual | |
unitId | string → unit | Unit observed | contextual | no cascade |
periodFrom | date | Period from | essential | |
periodTo | date | Period to | essential | |
source | string | Source | essential | |
createdAt | datetime | Created | system |
strategy · Strategies| Column | Type · reference · values | Label | Group | |
|---|---|---|---|---|
idx | string | ID | key | |
code | string | Code | essential | required |
name | string | Name | essential | required |
policyId | string → policy | Policy | essential | required |
policyCode | string | Policy code | essential | |
startYear | integer | Start year | essential | |
targetYear | integer | Target year | essential | |
envelope | float | Capital envelope, total (R) | essential | |
status | enum {draft, adopted, closed} | Status | contextual | |
notes | text | Notes | contextual | |
createdAt | datetime | Created | system |
allocation · Allocations| Column | Type · reference · values | Label | Group | |
|---|---|---|---|---|
idx | string | ID | key | |
strategyId | string → strategy | Strategy | essential | required |
strategyCode | string | Strategy code | essential | |
zoneId | string → zone | Zone | essential | required |
assessmentId | string → assessment | Assessment | contextual | |
systemId | string → system | System | essential | required |
systemName | string | System name | essential | required |
zoneName | string | Zone name | essential | |
class | enum {dry, nss, wess, sewered, sewdec} | Class | essential | |
worksId | string → works | Receiving works | contextual | |
year | integer | Year | essential | |
capital | float | Capital (R) | essential | |
households | integer | Households | essential | |
createdAt | datetime | Created | system |
A labeller is a declaration on the table, and every selector, card, picker and search modal inherits it at once. The canon: the title line carries what a person recognises, the subtitle carries the discriminator, and an identifier never stands as a title. Where a table's human name lives in a referenced row, the name rides beside the reference for this purpose.
| Table | Title | Subtitle | Pill | The reading |
|---|---|---|---|---|
system | {name} | {class} | class | the name a planner knows; the class beside it, since the class is what the strategy allocates |
component | {name} | {group} · {code} | group | the technology by its name; the group and Compendium code beneath, the way an engineer cites it |
criterion | {label} | {role} · {unit} | role | the measurement by its label; its role (gate, derived, score) and unit beneath |
rule | {componentCode} on {criterionKey} = {band} | {verdict} · {sourceRef} | verdict | a rule reads as a sentence, component on criterion equals band; the verdict and the document beneath |
parameter | {section} · {key} | {status} · grade {grade} | grade | section and key, since a parameter is only meaningful inside its model; status and grade beneath |
policy | {name} | {code} · margin {margin} · envelope R{envelope} | status | the policy by its name; the code and the two numbers that change a closing, margin and envelope, beneath |
company | {name} | {kind} | kind | the company by its name; its kind beneath (manufacturer, provider, municipality) |
unit | {name} | {class} · {status} | class | the unit by its name; its category and status beneath, the way a planner shortlists it |
zone | {name} | {planningUnit} · ward {ward} · {upgradingCategory} | upgradingCategory | the settlement by its name; planning unit, ward and upgrading category beneath, the way the IDP lists it |
works | {name} | {technology} · {operationalCapacityPct}% of design · {riskClass} | riskClass | the works by its name; technology, load against design and risk class beneath, the way Green Drop reports it |
context | {label} | surveyed {surveyedAt} | – | the survey by its label; the survey date beneath, never an identifier |
assessment | {label} | {recommendation} · run {runAt} | – | context and policy code as the label; the recommendation and the run time beneath |
assessmentRow | {systemName} | {verdict} · fit {fit} · cost R{cost} | verdict | the system by its name; verdict, fit and cost beneath, the three things read at a glance |
observation | {class} · {metric} | {value} {unit} · {periodFrom} to {periodTo} · {source} | class | class and metric as the label; value, period and source beneath |
strategy | {name} | {code} · target {targetYear} · envelope R{envelope} | status | the strategy by its name; code, target year and envelope beneath |
allocation | {systemName} | {zoneName} · year {year} · {households} households | class | the system allocated; the strategy, year and households beneath |
Every area is one of the platform's named layouts, populated with shared bindings and read against the same tests. The frame is the instrument, not a web page: one navigation row, the control column on the left at one quarter of the width (the platform's control-stage grid is 1 : 3, so 340 to 380 px on a laptop screen of 1366 to 1536 px), the stage at three quarters, a 24 px status line at the foot. Inside the frame the surface is continuous, divided by hairlines; cards represent objects only, never regions. The control column is one exclusive accordion ordered by dwell time: the section where the work happens opens first, a selector folds once the choice is made and stays one click away. The stage is one persistent surface that reads top to bottom: the numbers of the moment as plain chips, then the one graphical element a domain expert recognises at a glance, filling the width, then the facets of the selected record side by side in a two-column grid so they are read together, then the trail. The first render is a working render: the first or latest record is selected, and a genuinely empty table shows a named empty state, never an instruction to click. A pick drives the stage (owner ruling 5 September 2026): selecting a row in any list changes the central element and every facet on the stage to that record within the event, and nothing stays showing the previous record; a selector that folds names its choice in its own header; where a pick has nothing to show, the stage says so by name (not assessed, not placed) rather than keeping the old picture. Switching a section hides and shows; nothing is rebuilt. Primary text stays near-black at about 13 px; density comes from tight rows, not smaller type.
Layout. control-stage; the rail lists sites, the survey form folded beneath. Central element. the map, full width, centred on the picked site; beneath it the site's variables as aligned rows, every band strip the same width, the recorded band highlighted, the evidence tier beside it.
| Control column (¼), top to bottom | Holds | State |
|---|---|---|
| Sites | the settlement register, searchable, with the latest verdict as a pill (`zone.bindSelector`; `zone.bindSelectEditor` when the engineer edits, typing lat, lng and source) | open |
| Survey | the survey form for the picked site (label, receiving works, metering, goals; `uiForm` → `saveContext`); the bands are set on the variables pane | folded |
| # | Stage (¾), read top to bottom | Holds | Height |
|---|---|---|---|
| 1 | Chips | site · households · category · receiving works · variables recorded · below the evidence floor · surveyed · latest verdict · allocated · point | 8% |
| 2 | Where it is | `zone.bindMap` across the full width, centred on the picked site: click to place an unplaced site, drag to correct, both through `setZoneGeometry` | 38% |
| 3 | The site's variables | aligned rows: a fixed label column, the bands as a segmented strip of one width for every row with the recorded band filled (the survey's input), a fixed tier column; one muted caption of unit and cut points under each strip; derived variables greyed and marked derived | 40% |
| 4 | Surveys of this site | `context.bindSelector` of the site's surveys, latest first; a pick loads that survey into the variables and the form | 14% |
First render. the site of the latest assessment is selected (else the latest surveyed site, else the first). On a pick. a site → the chips, its variables with their recorded values and tiers, the map centred on its point, its surveys; a survey in the history → the variables and the form; a site with no survey → that state named, the rows unfilled for recording. Empty. a site without a survey: the callout names it and every row renders unfilled for recording.
Layout. control-stage; the rail lists the five categories. Central element. the ranges across categories: one box per category of product, the catalogued templates as its members, the metric chosen above (twenty-year cost, capital, operating, desludging).
| Control column (¼), top to bottom | Holds | State |
|---|---|---|
| Categories | the five categories of product with their counts of templates and units (a `uiCollection` list: the category is the schema's enum, not a row) | open |
| # | Stage (¾), read top to bottom | Holds | Height |
|---|---|---|---|
| 1 | Chips | category · templates · units · companies · monitored sites · twenty-year cost range · capital range · observed uptime | 8% |
| 2 | Cost across the categories | `system.bindBoxPlot` (valueField = the chosen metric, groupField = category); the metric as a segmented control above it | 40% |
| 3 | Units in this category | Catalogued templates | left: `unit.bindSelector` — unit, company, template, monitored sites, capital where stated; right: `system.bindSelector` — components, twenty-year cost, capital | 32% |
| 4 | The picked unit or template | the unit: company, category, template, status, households per unit, capital and operating per household (or "not stated"), the category's observed uptime, its monitored sites with their hazard categories; the template: cost parts, basis, the chain as a stepper, fit values, the units built on it | 20% |
First render. all five categories (the chips count the whole register), no member picked. On a pick. a category → the chips repaint to it, the units and templates filter to it, the plot keeps every category for comparison; a unit → its company, template, numbers, monitored sites and record; a template → its chain, cost parts, fit values and the units built on it. Empty. a category with no unit: the units pane names it; a number a source does not state reads "not stated", never a guess.
Layout. control-stage; the rail lists sites. Central element. the options as cards, the recommendation first then by fit: each with its verdict (suitable · with conditions · ruled out · excluded by policy), fit, twenty-year cost and capital per household, and the risks and concerns in words.
| Control column (¼), top to bottom | Holds | State |
|---|---|---|
| Sites | the settlement register with the verdict as a pill (`zone.bindSelector`; the same selection as the Sites tab) | open |
| # | Stage (¾), read top to bottom | Holds | Height |
|---|---|---|---|
| 1 | Chips | site · recommendation · suitable options · ruled out · excluded by policy · cost of the recommendation · policy · assessed | 8% |
| 2 | The options for this site | `assessmentRow.bindSelector` as cards, scoped to the run by `assessment.bindChildTable`; risks = the rules that condition or rule the option out, in words; concerns = policy exclusion, assumed cost parameters, conditions to accept | 44% |
| 3 | The evidence for the picked option | Fit beside cost | left: the rules behind the verdict as callouts, each in words with its code, document and grade, then the fit decomposition; right: `assessmentRow.bindPlot`, fit up, cost across, by verdict | 30% |
| 4 | How this was decided | policy and time of the run, the closing trail step by step; the doors: the policy list to assess under another policy, the evidence-floor switch, Run assessment, Export assessment, Allocate to the latest strategy | 18% |
First render. the site of the latest assessment is selected and its run shown, the recommendation card selected. On a pick. a site → its latest run under the policy in force (else it is assessed now, else the refusal is named); a policy in the door → the run under it; an option card → the evidence for it; the pick is shared with Sites. Empty. no survey: named, with the door to Sites; surveyed but never assessed: assessed on the pick under the policy in force; refused by the evidence floor: the refusal named with the switch in view.
Layout. control-stage. Central element. capital by year against the envelope: bars of allocated capital per year, one colour per class.
| Control column (¼), top to bottom | Holds | State |
|---|---|---|
| Strategies | named strategies with target year and envelope (`strategy.bindSelectEditor`) | open |
| Allocate | the closed assessments under the strategy's policy whose zone is not yet allocated (`assessment.bindSelector`); the year; Allocate the recommendation | folded |
| # | Stage (¾), read top to bottom | Holds | Height |
|---|---|---|---|
| 1 | Chips | strategy · policy · zones allocated · households served · capital committed of envelope · horizon | 6% |
| 2 | Capital by year | `allocation.bindPlot` bar by year grouped by class | 40% |
| 3 | Allocations | Shared resources | left: `allocation.bindSelector` by zone with class pill, year, households, capital; right: works headroom against draw, the desludging fleet against events, then the picked allocation or candidate | 36% |
| 4 | Strategy document | Export strategy → the phased plan as a document | 18% |
First render. the latest strategy that holds allocations is selected. On a pick. a strategy → chips, capital by year, allocations, balances; a candidate → what allocating it would commit; an allocation → its detail beside the balances (fit, cost, conditions, the run it rests on). Empty. no strategy yet: the named empty state with New strategy.
Layout. control-stage; one register per rail section, one pane per kind on the stage. Central element. the discrepancy report: class by criterion, the register's operating-risk score against the band the observed record puts the class in, flagged where they differ by the trigger.
| Control column (¼), top to bottom | Holds | State |
|---|---|---|
| Monitored sites | contexts carrying an installed unit and a hazard register (`context.bindSelector`), each with survives or fails and its overlap | open |
| Observed performance | `observation.bindSelectEditor` (recorded through `recordObservation`) | folded |
| Components | `component.bindSelector` | folded |
| Parameters | `parameter.bindSelectEditor` | folded |
| Policies | `policy.bindSelectEditor` | folded |
| Units and companies | `unit.bindSelectEditor` | folded |
| # | Stage (¾), read top to bottom | Holds | Height |
|---|---|---|---|
| 1 | Chips | monitored sites · installed system survives · mean overlap J · observations · criteria flagged · rules · parameters · units | 8% |
| 2 | Discrepancy report | a row per class: the observed value and its band, then each operating-risk criterion's register score beside the band, flagged ⚑ at or beyond the trigger | 32% |
| 3 | Back-test | The picked monitored site | left: every monitored site with the installed system, survives or fails, raised against recorded, J; right: the picked site's raised conditions with their categories and the watch conditions of its class | 30% |
| 4 | The picked component | The picked parameter or policy | left: the component's group, family, source, the templates using it, and `rule.bindSelector` of every rule it reads; right: the parameter's value, status, grade, basis, range and source, or the policy's weights and exclusion clauses | 30% |
First render. the report and the back-test run on render from what the tables hold; nothing picked in the register sections. On a pick. a monitored site → its raised conditions against the recorded categories; an observation → its class brought to the front of the report; a component → every rule it reads and the templates that use it; a parameter or a policy → its own pane. Empty. no monitored site or no observation: the chips say so and the report rows read "no observation"; an unpicked register pane says what to pick.
Layout. reader. Central element. the document: this page's narrative, the meta plan and the build state.
| Control column (¼), top to bottom | Holds | State |
|---|---|---|
| – | no control column: About is a reader | open |
| # | Stage (¾), read top to bottom | Holds | Height |
|---|---|---|---|
| 1 | What SanPath is for | the intention and the sources, narrated | 25% |
| 2 | The method as a document | this artefact: Open the artefact (a new tab) or Read it here (the page mounted inside the tab, style-isolated, its own tabs and equations working) | 25% |
| 3 | Demo world (engineer) | the door that builds the demo world through the verbs on a demo backend | 15% |
| 4 | Who uses it, and for what | the three user types and the five tabs as their questions | 35% |
First render. the narrative. On a pick. a section → the document scrolls to it. Empty. –.
The tests every area is read against. Crop the screenshot to the stage: the purpose is obvious in one second or the hierarchy is wrong. Flatten it to grey: navigation, controls and data are still told apart by shape. On first load the largest element is data, not an instruction. After selecting a record, its facets are visible together with no further click, and picking any other row in any list changes the central element and every facet to that row (the proof `Tests/sanpath-selection.mjs` drives one pick per list on every surface). No region is a card; no form sits on the stage; no control column exceeds a quarter at laptop width; no text below 13 px carries meaning.
A system composes services; a service owns tables, verbs and the views over them, and is influenced only through configuration. This tab maps each service the SanPath system composes to what it does for the method, the configuration it takes, and its state; then the SanPath service's own contract: every configuration knob with its default and meaning, every functional element (importers, engine verbs, export, views) with its settings, what it reads and writes, what it emits and the door that exposes it, and every event with the subscribers that consume it. The register is read from the service declaration, not retyped.
| Service | Class · prefix | What it does for SanPath | Configuration | State |
|---|---|---|---|---|
| SanPath | SanPathService · san_ | the method as data and verbs: catalogue, register, parameters, policies, companies and units, zones, works, contexts, assessments; the engine; the five surfaces | config.services.sanpath = { api: "mserp", prefix: "san_", options: {…} } — the options are the contract in the next table; the api is the municipality's own MSERP endpoint, the prefix is fixed | built |
| Member | MemberService (Service.Core.js) · mbr_ | identity and roles: the planner, the engineer and the reader are members with roles under the system's parent group; tab gating and the engineer's edit rights read them; sessions and API keys for a municipality's integration | config.services.member = { api: "mserp", prefix: "mbr_" } · config.rbac = { parentGroupCode: "sanpath", adminRoles: ["engineer", "platform-admin"] } · tabs carry roles: Sites and Suitability → planner, engineer; Units and Strategy → everyone; Register → engineer; About → everyone · applyLoginShield: true on a hosted deployment (false only on the auth-off sandbox) | wired; roles gate on post-login |
| Audit | AuditService · aud_ | the record of every domain event: the system bus is tapped, so each san: event (a run closed, a register imported, an export) lands as an aud_event row with the actor, and the engineer's register edits carry their trail | config.services.audit = { api: "mserp", prefix: "aud_" }; the tap is the default (tapBus true); nothing else to set | wired by the frame |
| Files | FilesService · fil_ | the exported assessment and the strategy document as files a municipality can circulate: Save to Files on Suitability and on Strategy calls saveDocument, which uploads the Markdown through Files.uploadFile into one 'SanPath exports' folder (created once); the server holds the bytes | config.services.files = { api: "mserp", prefix: "fil_" }; the instance's /upload route must accept a signed-in session (it does on both hosted instances) | wired; Save to Files live 5 Sep |
| Tag | TagService · tag_ | not used by any SanPath feature; the frame instantiates it by default. Reviewed and dropped from the loadout until a feature names it (a tag on a zone would be accretion) | removed from config.services | dropped |
| Option | Default | Meaning |
|---|---|---|
defaultPolicy | "P1" | the policy a verb runs under when none is named (the municipality's own policy) |
evidenceFloor | true | enforce the evidence floor at closing: a gate measurement below the floor (parameter survey.evidenceFloor) refuses close unless the planner accepts it |
acceptConditions | true | the closing default for conditions: accepted with the municipality named as responsible party (false drops every conditional survivor) |
persistAssessments | true | close writes assessment and assessmentRow rows (false returns the result only) |
discountRate | null | overrides the policy discount rate when set; null = the policy's rate, else the first illustrative rate of costGlobal.discountRates |
routeKm | null | overrides the route length when set; null = the context's measurement routeKm, else costGlobal.routeKm |
editable | false | the engineer's edit rights on the catalogue and the registers (chain editing, parameters, policies); the system sets it from the member's roles on post-login, true on the auth-off sandbox |
tiers | ["record", "visit", "field test", "engagement", "assumed"] | the evidence tiers the survey form offers per measurement (parameter survey.evidenceFloor names which may close) |
goals | ["lowCost", "minimalOM", "waterSensitive", "reuse", "localJobs"] | the goals a planner may declare for a zone; each doubles the criteria it touches |
mapCenter | [31.02, -29.86] | the Sites map's starting centre [lng, lat] when no site carries geometry (eThekwini); a deployment sets its municipality |
mapZoom | 9 | the Sites map's starting zoom |
| Element | Kind | Settings | Reads | Writes | Emits | Door |
|---|---|---|---|---|---|---|
importRules | importer | { registry: rules.json } | the register (catalogue, criteria, rules, lookup) | component · criterion · rule (new rows inserted; held rows whose verdict, condition, source or grade changed are updated) | san:rulesImported | Tests/sanpath-import.mjs; the engineer's registers (step 4) |
importSystems | importer | { registry: systems.json } | the sixteen chains | system | san:systemsImported | as above |
importParameters | importer | { registry: parameters.json } | the parameter register | parameter (new rows inserted; held rows whose value, range, unit, status, grade, basis or source changed are updated) · criterion (score rows) | san:parametersImported | as above |
importPolicies | importer | { registry: policies.json } | the policy register | policy | san:policiesImported | as above |
importZones | importer | { registry: zones.<municipality>.json } | the settlement register | zone | san:zonesImported | as above |
importUnits | importer | { registry: units.json } | the companies and their units (the de-risking field record; a supplier register later) | company · unit | san:unitsImported | as above; the Units tab reads them |
importWorks | importer | { registry: works.<municipality>.json } | the works register | works | san:worksImported | as above |
deriveContext | engine | { contextId } | context · criterion.derivation · works | context.measurements (gwRisk, worksHeadroom) | san:contextDerived | Sites on save; Suitability reads bands through screen |
screen | engine | { contextId } | context · system · component · rule | – | san:screened | Suitability (inside close) |
score | engine | { contextId, policyCode?, survivors? } | policy · parameter (systemFit, operatingRisk, costReference, cost:*) · criterion (score rows) | – | san:scored | Suitability (inside close) |
cost | engine | { systemName, policyCode? | rho?, routeKm? } | parameter (cost:*, costGlobal) | – | san:costed | Suitability (inside close); Units (template cost) |
close | engine | { contextId, policyCode?, accepted?, acceptBelowFloor?, persist? } | all of the above · survey.evidenceFloor | assessment · assessmentRow | san:assessmentClosed | Suitability · on the site pick, and Run assessment |
compare | engine | { contextId, policyCodes?, accepted?, acceptBelowFloor? } | as close | – | san:policiesCompared | Tests/sanpath-engine.mjs (the policy comparison) |
alignRules | engine | { contextIds, policyCodes?, accepted?, acceptBelowFloor? } | as close · zone.households | – | san:rulesAligned | Strategy area (step 5); Tests/sanpath-engine.mjs |
saveContext | survey | { contextId?, zoneId, label, measurements, tiers, metered, goals, worksId } | criterion (bands, tiers) | context (create or update), then deriveContext | san:contextSaved · san:contextDerived | Sites · Save survey |
deleteContext | survey | { contextId } | assessment (refuses when runs exist) | context (delete) | san:contextDeleted | Sites · Delete survey |
setZoneGeometry | zone | { zoneId, lat, lng, source } (lat and lng null to clear) | zone | zone.lat · zone.lng · zone.geometrySource | san:zoneGeometrySet | Sites · click the map to place the picked site; drag a marker; the API (updateEntry on san_zone) |
importZoneGeometry | importer | { rows: [{ name | zoneId, lat, lng }], source } | zone (matched by name or id) | zone.lat · zone.lng · zone.geometrySource per matched row | san:zoneGeometryImported | the engineer's import (a CSV from a GIS layer or a gazetteer); unmatched names are listed, never guessed |
createStrategy | strategy | { code, name, policyCode?, startYear?, targetYear?, envelope?, notes? } | policy | strategy | san:strategyCreated | Strategy · New strategy |
allocate | strategy | { strategyId, assessmentId, year? } | strategy · assessment · assessmentRow (the recommendation) · context · zone | allocation | san:allocated | Strategy · Allocate; Suitability · Allocate to the latest strategy |
deallocate | strategy | { allocationId } | allocation | allocation (delete) | san:deallocated | Strategy · the allocation list |
strategyBalance | strategy | { strategyId } | strategy · allocation · works · parameter (strategy, cost:*) | – | san:strategyBalanced | Strategy · the balances and the capital-by-year plot |
strategyDocument | export | { strategyId } | strategy · allocation · the balance | – | san:strategyExported | Strategy · Export strategy |
backtest | validation | { contextIds? } (default: every context with an installed system) | context (installedSystem, hazardCategories) · the screen · parameter (validation.conditionCategories, watchThreshold; fit.operatingRisk) | – | san:backtested | Register · the back-test table |
discrepancyReport | validation | { } | observation · parameter (validation.discrepancyBands, recalibrationTrigger; fit.operatingRisk) | – | san:discrepancyReported | Register · the discrepancy report; the engineer edits the register by hand |
recordObservation | validation | { class, metric, value, sites?, periodFrom, periodTo, source } | – | observation | san:observationRecorded | Register · Record observation; the InfraTrack import (later) |
saveDocument | export | { kind: assessment | strategy, id } | the export verbs; files.folder | files.folder (once) · files.file through Files.uploadFile | san:documentSaved | Suitability · Save to Files; Strategy · Save to Files |
exportAssessment | export | { assessmentId } | assessment · assessmentRow · context · policy | – | san:assessmentExported | Suitability · Export assessment |
renderSites | view | (stage) | zone · context · criterion · works · assessment · allocation | through saveContext / deleteContext / setZoneGeometry | – | the Sites tab |
renderUnits | view | (stage) | system · unit · company · context · observation · component · rule · parameter | – | – | the Units tab |
renderSuitability | view | (stage) | zone · context · assessment · assessmentRow · policy · strategy | through close / exportAssessment / allocate | – | the Suitability tab |
renderStrategy | view | (stage) | strategy · allocation · assessment · zone · works | through the strategy verbs | – | the Strategy tab |
renderRegister | view | (stage) | context (monitored sites) · observation · component · rule · parameter · policy · unit · company | through recordObservation and the schema editors when editable | – | the Register tab (engineer) |
| Event | Payload | Subscribers |
|---|---|---|
san:systemsImported | { count, inserted, source, version } | the import harness (counts); the Audit tap |
san:rulesImported | { count, inserted, updated, components, criteria, source, version } | the import harness (counts); the Audit tap |
san:parametersImported | { count, inserted, updated, source, version } | the import harness (counts); the Audit tap |
san:policiesImported | { count, inserted, source, version } | the import harness (counts); the Audit tap |
san:zonesImported | { count, inserted, source, version } | the import harness (counts); the Audit tap |
san:worksImported | { count, inserted, source, version } | the import harness (counts); the Audit tap |
san:unitsImported | { count, inserted, companies, sites, source, version } | the import harness (counts); the Audit tap |
san:contextDerived | { contextId, bands } | the Contexts surface (derived rows repaint, step 4); the Audit tap |
san:screened | { contextId, survivors, eliminated } | the Contexts gates preview (step 4); the Audit tap |
san:scored | { contextId, policyCode, ranked } | the Audit tap |
san:costed | { systemName, rho, total, status } | the Catalogue cost card (step 4); the Audit tap |
san:assessmentClosed | { assessmentId, contextId, policyCode, recommendation } | the Suitability surface (selects the new run and its recommendation); the Sites chips (latest verdict); the Audit tap; the Strategy area (an allocation offered for the zone) |
san:policiesCompared | { contextId, policyCodes, agreement } | the Decision run note; the Audit tap |
san:rulesAligned | { contextIds, algorithms, policies, alignment } | the Strategy area (step 5); the Audit tap |
san:assessmentExported | { assessmentId, markdown } | the Decision export pane; the Files save (step 4); the Audit tap |
san:documentSaved | { fileId, name, folderId, kind, id } | the import harness (counts); the Audit tap |
san:contextSaved | { contextId, zoneId, created } | the import harness (counts); the Audit tap |
san:backtested | { sites, survives, meanOverlap } | the import harness (counts); the Audit tap |
san:discrepancyReported | { rows, flagged } | the import harness (counts); the Audit tap |
san:observationRecorded | { observationId, class, metric } | the import harness (counts); the Audit tap |
san:contextDeleted | { contextId } | the import harness (counts); the Audit tap |
san:zoneGeometrySet | { zoneId, lat, lng, source } | the import harness (counts); the Audit tap |
san:zoneGeometryImported | { matched, unmatched, source } | the import harness (counts); the Audit tap |
san:strategyCreated | { strategyId, code } | the import harness (counts); the Audit tap |
san:allocated | { allocationId, strategyId, zoneId, systemName, year } | the import harness (counts); the Audit tap |
san:deallocated | { allocationId, strategyId } | the import harness (counts); the Audit tap |
san:strategyBalanced | { strategyId, years, works, fleet } | the import harness (counts); the Audit tap |
san:strategyExported | { strategyId, markdown } | the import harness (counts); the Audit tap |
alignRules (its surface is the Strategy area, step 5) and deriveContext on its own (the Contexts form calls it on save, step 4). Both are exercised by the engine proof until then.survey.evidenceFloor, listing them, and the planner accepts them by an explicit switch on the rail that writes step 0 of the closing trail. The engine proof asserts the refusal.config.services.sanpath.options and read by the verbs.A view composes shared bindings over the service's tables; it never builds a list or a form by hand, and it never computes. Each binding subscribes to its table's events and repaints itself; the view's own painters subscribe to selection and to the service bus. This tab lists, per view, the bindings the view requires, read from the service's register (the view reads its options from the same register, so the two cannot drift), the events each reacts to, and the option keys checked against what the shared binding actually reads. Then the event flow: every verb's event, who subscribes, and what a subscriber may do in turn; and the trace of one full cycle on the live surface, which is the proof that nothing runs away.
renderSites · control-stage; the rail lists sites, the survey form folded beneath; the stage reads map (central) · the variables as aligned rows · the surveys| Binding | Table | Shared binding | Declarative options | Functions beside the call | Reacts to | Purpose |
|---|---|---|---|---|---|---|
sites | zone | bindSelectEditor / bindSelector | {"template": "list", "searchable": true, "pageSize": 8, "editor": "modal"} | shape (name · unit · ward · category · households · latest verdict pill) | zone events/select | pick the site (rail); the schema editor when the engineer edits |
map | zone | bindMap | {"latField": "lat", "lngField": "lng", "categoryField": "recommendedClass", "draggable": true, "clickToPlace": true, "refreshOn": ["assessment", "allocation"]} | shape (title · lat · lng · recommended class), moveFn and placeFn → setZoneGeometry | zone events/select; markerDragEnd and canvasClick | where the site is; a click places, a drag corrects (stage facet) |
history | context | bindSelector | {"template": "list", "pageSize": 6} | where (surveys of the picked site; re-rendered on the pick), shape (label · surveyed · variables) | context events/select; zone select by render() | the surveys of the site (stage facet) |
Painters (hand-composed residue) and what wakes them. variables: the site's variables as aligned rows: a fixed label column, every band strip the same width (ui-segmented-fill; the survey's input), a fixed tier column, one muted caption line of unit and cut points beneath · chips: zone select · context select/updateEntry · assessment/allocation createEntry · form: the survey form in the rail (uiForm) · bus: san:contextSaved → context.select
renderUnits · control-stage; the rail lists the five categories| Binding | Table | Shared binding | Declarative options | Functions beside the call | Reacts to | Purpose |
|---|---|---|---|---|---|---|
ranges | system | bindBoxPlot | {"valueField": "value", "groupField": "category"} | shape (category label · the chosen cost metric of the template) | system resetData; the metric control by render() | the ranges across categories, the central element |
units | unit | bindSelector | {"template": "list", "searchable": true, "pageSize": 8} | where (the picked category), shape (unit · company · template · sites · capital) | unit events/select; category pick by render() | the company units of the category (stage facet) |
templates | system | bindSelector | {"template": "list", "searchable": true, "pageSize": 8} | where (the picked category), shape (name · components · cost · capital) | system events/select; category pick by render() | the catalogued templates of the category (stage facet) |
Painters (hand-composed residue) and what wakes them. categories: a uiCollection list of the five classes with counts (the enum is schema, not data) · chips: category pick · unit/system/observation resetData · detail: the picked unit or template (unit select · system select)
renderSuitability · control-stage; the rail lists sites (the pick is shared with Sites)| Binding | Table | Shared binding | Declarative options | Functions beside the call | Reacts to | Purpose |
|---|---|---|---|---|---|---|
sites | zone | bindSelector | {"template": "list", "searchable": true, "pageSize": 8} | shape (name · households · category · verdict pill) | zone events/select | pick the site (rail) |
rowsByAsm | assessment | bindChildTable | {"child": "assessmentRow", "fkField": "assessmentId"} | – | assessment select/deselect → assessmentRow setScope/clearScope | the stage shows one run's options |
options | assessmentRow | bindSelector | {"template": "cards", "minWidth": 300, "pageSize": 16, "refreshOn": ["assessment"]} | shape (system · verdict pill · fit, cost, capital · risks and concerns in words), sort (recommendation, then fit, then excluded, then ruled out) | assessmentRow createEntry/resetData/select; assessment events through refreshOn | the options for the site, the central element |
plot | assessmentRow | bindPlot | {"template": "scatter", "xField": "cost", "yField": "fit", "groupBy": "verdict", "height": 260} | shape (title · cost · fit · verdict label) | assessmentRow createEntry/resetData (scope) | fit beside cost (stage facet) |
policies | policy | bindSelector | {"template": "compact"} | shape (code · name · envelope · margin) | policy resetData/select | assess under another policy (a door in How this was decided) |
Painters (hand-composed residue) and what wakes them. follow: zone select → the site's latest run under the policy in force, else close() now (owner decision 2), else the named refusal · chips: assessment select · assessmentRow createEntry · why: assessmentRow select · how: assessment select · bus: san:assessmentClosed → assessment.select; san:allocated → chips
renderStrategy · sectioned control-stage| Binding | Table | Shared binding | Declarative options | Functions beside the call | Reacts to | Purpose |
|---|---|---|---|---|---|---|
strategies | strategy | bindSelectEditor | {"template": "list", "editor": "modal"} | shape (name · code · target · envelope), onAddClick → createStrategy | strategy events/select | pick or create the strategy (rail) |
candidates | assessment | bindSelector | {"template": "list", "searchable": true, "pageSize": 8, "ignoreScope": true, "refreshOn": ["allocation", "strategy"]} | where (closed under the strategy's policy, zone not yet allocated), shape (context label · recommendation · zone) | assessment events; allocation and strategy events through refreshOn; strategy select | what can be allocated (rail) |
allocByStrategy | strategy | bindChildTable | {"child": "allocation", "fkField": "strategyId"} | – | strategy select → allocation scope | the stage shows one strategy's allocations |
capitalByYear | allocation | bindPlot | {"template": "bar", "xField": "year", "yField": "capital", "groupBy": "class", "height": 300} | shape (year · capital · class) | allocation createEntry/deleteEntry/resetData (scope) | capital by year by class, the central element |
allocations | allocation | bindSelector | {"template": "list", "pageSize": 10, "crud": {"delete": true}} | shape (system · zone · year · households · capital), deleteFn → deallocate | allocation events/select | the allocations (stage facet) |
Painters (hand-composed residue) and what wakes them. chips: strategy select · allocation createEntry/deleteEntry/resetData → strategyBalance · balances: the same, as chips per works and the fleet · document: strategyDocument → pre
renderRegister · control-stage; one register per rail section, one pane per kind on the stage| Binding | Table | Shared binding | Declarative options | Functions beside the call | Reacts to | Purpose |
|---|---|---|---|---|---|---|
sites | context | bindSelector | {"template": "list", "pageSize": 10} | where (contexts with an installed system), shape (label · installed · survives · overlap) | context events/select | the monitored sites of the back-test (rail) |
observations | observation | bindSelectEditor / bindSelector | {"template": "list", "pageSize": 8, "editor": "modal"} | shape (class · metric · value · period · source), onAddClick → recordObservation | observation events | the monitored record, entered or imported (rail) |
components | component | bindSelector | {"template": "compact", "searchable": true, "pageSize": 10} | shape (code · name · family) | component resetData/select | the catalogue by code (rail) |
parameters | parameter | bindSelectEditor / bindSelector | {"template": "list", "searchable": true, "editor": "modal", "pageSize": 8} | shape (section · key · value · grade) | parameter events | the register, editable by the engineer (rail) |
policies | policy | bindSelectEditor / bindSelector | {"template": "list", "editor": "modal"} | shape (code · name · margin · envelope) | policy events | the policies, editable by the engineer (rail) |
units | unit | bindSelectEditor / bindSelector | {"template": "list", "searchable": true, "editor": "modal", "pageSize": 8} | shape (unit · company · class · status) | unit events | the units register, editable by the engineer (rail) |
backtestRows | context | bindSelector | {"template": "list", "pageSize": 16} | where (installed), shape (site · installed · survives · raised vs recorded · J) | context events | the back-test table (stage) |
rules | rule | bindSelector | {"template": "list", "searchable": true, "pageSize": 8} | where (rules of the picked component), shape (code on criterion = band · verdict · source) | rule resetData; component select by render() | every rule the picked component reads (stage facet) |
Painters (hand-composed residue) and what wakes them. chips: context/observation events → backtest + discrepancyReport · discrepancy: the class × criterion table with the observed band and the flag (central) · site: the picked monitored site · component: the picked component · parameter: the picked parameter or policy
The platform's bindings read a fixed set of option keys and ignore the rest without a word (a known trap: an option the class never reads renders as if it were absent). The keys used above were checked against the keys each binding reads in publon.bind.js: bindSelector reads template, searchable, shape, sort, refreshOn, pageSize and its editing options; bindSelectEditor reads its own defaults and quickAdd and forwards the rest to the selector and the editor; bindPlot reads template, xField, yField, groupBy, groupColors, height and shape; bindChildTable takes the child and the foreign key. One dead key was found on this surface and removed: ownActions, which the cheat sheet still lists and no binding reads.
Every mutating verb emits one colonised domain event after it has written (importers, close, deriveContext); the read verbs emit their result event too, so a surface can react without calling. Subscribers are of three kinds. The bindings react to table events (createEntry, updateEntry, deleteEntry, resetData, select) and only repaint. The painters of a view react to selection and to the service bus, and only select or repaint. The Audit tap records every service-bus event. No subscriber calls a verb, so no domain event can cause another: the only nesting the trace shows is the platform mirroring table events (select, deselect, resetData) onto the service bus inside the closed-run handler, which selects the new assessment; that is a mirror, not a loop. Two guards in the platform close the remaining doors: select on an already-selected row is a no-op, and setScope to the same scope emits nothing.
The two-parent trap was the one real circularity risk found: the platform auto-wires every reference as a parent-to-child scope and a child-to-parent selection, so a row with two parents could ping between them. The schema declares the join-only references (skip) and the cascade-free ones (cascade:false), and the trace asserts that selecting a system row selects no system and picking a context selects no zone.
| Action | Events counted (topic · count) | Max nesting |
|---|---|---|
| pick context Z2 | svc:record:select 6 · svc:record:resetData 3 · svc:record:reset 3 · svc:assessment:resetData 2 · svc:assessment:reset 2 · svc:assessmentRow:select 2 · svc:assessment:select 2 · assessment:resetData 2 · assessment:record:reset 2 · svc:context:deselect 2 · svc:record:deselect 2 · svc:context:select 2 · context:filterChange 1 · svc:assessmentRow:resetData 1 · svc:assessmentRow:reset 1 · assessmentRow:select 1 · assessmentRow:record:select 1 · assessmentRow:resetData 1 · assessmentRow:record:reset 1 · assessment:select 1 · assessment:record:select 1 · context:deselect 1 · context:record:deselect 1 · context:select 1 · context:record:select 1 | 1 |
| run assessment (P3) | svc:record:createEntry 17 · svc:record:create 17 · svc:assessmentRow:createEntry 16 · svc:assessmentRow:create 16 · assessmentRow:createEntry 16 · assessmentRow:record:create 16 · assessmentRow:dbSync 16 · svc:record:select 4 · svc:policy:select 2 · svc:assessmentRow:resetData 2 · svc:assessmentRow:reset 2 · svc:record:resetData 2 · svc:record:reset 2 · assessmentRow:resetData 2 · assessmentRow:record:reset 2 · svc:assessment:deselect 2 · svc:record:deselect 2 · svc:assessment:select 2 · policy:select 1 · policy:record:select 1 · svc:assessment:createEntry 1 · svc:assessment:create 1 · assessment:createEntry 1 · assessment:record:create 1 · assessment:deselect 1 · assessment:record:deselect 1 · assessment:select 1 · assessment:record:select 1 · svc:san:assessmentClosed 1 · assessment:dbSync 1 | 2 |
| select a system row | svc:assessmentRow:deselect 2 · svc:record:deselect 2 · svc:assessmentRow:select 2 · svc:record:select 2 · assessmentRow:filterChange 1 · assessmentRow:deselect 1 · assessmentRow:record:deselect 1 · assessmentRow:select 1 · assessmentRow:record:select 1 | 1 |
| compare policies | svc:san:policiesCompared 1 | 1 |
| tab away and back | 0 |
Bindings per table before and after a tab switch: {"context": 2, "policy": 1, "assessment": 2, "assessmentRow": 2} → {"context": 2, "policy": 1, "assessment": 2, "assessmentRow": 2}. Failures: 0. The proof is Tests/sanpath-bus-trace.mjs; the obligation that a closed run reaches the surface is declared in the platform's wiring register (Tests/wiring-review.mjs), which fails the estate's gate if the subscriber is ever removed.
The first build (3–4 September) shipped six tabs derived from the method's steps — Map · Catalogue · Contexts · Decision · Strategy · Validation — and the owner, reading it, called it "a bit too vague … information wading for the sake of it", and said what he expected instead: for a given site, the characterisation and the values of the variables at that site; on another tab, metrics about the units for the different companies, and the metrics and ranges for the categories of product; on another, the suitability of a site across the technology options, with the estimated costs, risks and concerns. The difference between that and the build, and the principles it teaches, are recorded here as part of building it better; the full ledger is PublonCore/tasks/todo-sanpath-rework.md.
| Tab (first build) | What it showed | Question served | The gap |
|---|---|---|---|
| Map | zones as points; the picked zone's chips | 1, partly | which sites exist and where, but no characterisation: a second tab for the variables |
| Contexts | zone → the survey as a band strip; a gates preview; the history; the form | 1 | the characterisation existed but was named for the engine (contexts, survey, bands); the gates preview was a fragment of question 3 inside question 1 |
| Catalogue | templates, components, parameters, policies — four rail sections over one stage | 2, partly | templates one at a time, never categories with ranges; no company units; the engine's internals in front of the reader; one pane serving four kinds, so a click on a component emptied the cost and fit values (the defect reported) |
| Validation (engineer) | monitored sites with their installed unit, uptime per class, back-test, discrepancy report | 2 | where the company units actually lived — behind the engineer role, framed as calibration |
| Decision | context + policy → run → plot, systems, deciding rules, trail | 3 | suitability and cost present; risks and concerns one click deeper per system, worded as rules; a policy had to be understood before anything showed |
| Strategy | allocations, capital by year, balances, document | – | a later step at the same level as the questions |
In one sentence. The tabs were derived from the engine's steps, not from the reader's questions: every question was spread over two tabs, every tab mixed two questions, and the vocabulary was the engine's. What the meta plan promised and the build skipped: "provider units attached to a system; benchmark record per class" — question 2 had no table to stand on until the company and unit tables of 4 September.
Sites (the map folded in as the site's location) · Units (new: the companies' units and the five categories with their ranges; the company and unit tables, the field record's four units as the first register) · Suitability (the options as cards with risks and concerns in words; a site with no run is assessed on the pick under the policy in force; the doors to another policy, export and allocation) · Strategy, kept as the planner's later step · Register, the engineer's door (the old Catalogue internals and Validation, one kind per section, one pane per kind). Every surface is proved by Tests/sanpath-selection.mjs (a pick drives the stage), sanpath-sites.mjs and sanpath-suitability.mjs (the round trips), sanpath-smoke.mjs (every tab filled, zero console errors) and sanpath-bus-trace.mjs (the bus: one event per verb, no loop).
Each step is complete when its round trip runs on real data, driven through the interface, with the reference implementation and the platform agreeing before the next step begins. Nothing is seeded: the importers read the registers this page publishes, and every context a proof creates is the case-study or back-test context of this page.
sanpath-smoke, sanpath-selection, sanpath-sites, sanpath-suitability, sanpath-files, sanpath-bus-trace. The comparison and the principles are the Rework tab.The five steps above are the whole of the selection method. What follows is the machinery behind them and what a municipality does after a single assessment. Each is folded because none of it is needed to follow the path.
A rule is a row: target (a component family or one component), criterion, band, verdict, condition, source, grade. The register is written by hand from the documents named in section A, and an exporter turns it into rows the platform imports, expanding each family rule to the members of the family. The evaluation knows how to look up a band, apply a rule, take the worst verdict over a chain and sum a weighted score. It knows nothing about septic tanks, so every verdict traces to a row and its document, and a revision of a standard is an edit to the rows that cite it, not a rewrite.
| target | criterion | band | verdict | condition | source | grade |
|---|---|---|---|---|---|---|
| soak | percolation | impermeable | fail | — | SANS 10252-2: no soakaway below the minimum percolation rate | A |
| soak | percolation | vslow | cond | enlarged soakaway or leach field sized from the percolation test | SANS 10252-2 | A |
| soak | percolation | vfast | fail | — | DWS03: rapid infiltration to groundwater | A |
| soak | percolation | fast | cond | groundwater risk re-assessed; setback from abstraction | DWS03 | A |
| soak | percolation | moderate | pass | — | SANS 10252-2 | A |
| soak | percolation | slow | pass | — | SANS 10252-2 | A |
A condition raised at selection becomes a cost line where it has a price and a de-risking register entry where it has a risk. The programme checks that entry at commissioning, and the monitored outcome of the site, uptime and time within specification per quarter, feeds back into the operating-risk scores by system class and context band. A system whose conditions were not met and then failed in the field is the model's calibration signal.
Run across a municipality's zones, the assessments compose into a portfolio, and the portfolio adds three constraints a single site never sees. A works with headroom for two thousand households cannot take sewer allocations from every zone within reach, so treatment capacity, desludging fleet and disposal sites are shared resources that allocations consume in sequence. Capital sums against the budget envelope by year, and phasing is the schedule that fits while reaching a named count of sites operating within specification by a named date. And a municipality will not run eleven systems, so the count of classes allocated and the operating capacity each demands stays visible.
The platform's suitability service already runs a two-phase match for InfraTrack, with thirteen gates and twelve factors written as code and binary verdicts. The path above is that engine with three changes: rules read from the register instead of code, three-valued verdicts, and systems as chains. InfraTrack's technology profiles become one-component chains, nothing it does today changes, and its matching becomes auditable at the same time. The build is therefore the register, the importer and the surfaces.
Works cited on this page, with the municipal documents the framework reads as inputs. The white paper carries the full list.