feat(seed): expand industrial process catalogue with per-process subcategories - #609
Open
mrivas00 wants to merge 7 commits into
Open
feat(seed): expand industrial process catalogue with per-process subcategories#609mrivas00 wants to merge 7 commits into
mrivas00 wants to merge 7 commits into
Conversation
mrivas00
force-pushed
the
feat/mati/industrial-processes-per-process
branch
from
August 24, 2026 14:36
6b6dc8e to
07922f7
Compare
mrivas00
changed the base branch from
main
to
fix/mati/industrial-process-factor-units
August 24, 2026 15:14
mrivas00
force-pushed
the
feat/mati/industrial-processes-per-process
branch
4 times, most recently
from
August 24, 2026 18:42
64b2fcd to
1b07c95
Compare
Base automatically changed from
fix/mati/industrial-process-factor-units
to
main
August 25, 2026 11:46
mrivas00
force-pushed
the
feat/mati/industrial-processes-per-process
branch
from
August 25, 2026 13:32
63bc3a9 to
450f510
Compare
…ategories
The Alcance 1 catalogue exposed four industrial processes as sibling
subcategories (Cemento, Vidrio, Acero, Cinc). That set is an arbitrary subset of
the IPCC IPPU tree: it includes glass and zinc, which the GHG Protocol's own
sector table does not list, and omits lime, aluminium, chemicals and pulp &
paper, which it does. A company doing anything else - chemicals being the case
that surfaced this - had nowhere to declare it.
Keeps the four existing subcategories untouched and adds one per missing IPPU
leaf, plus a residual:
Procesos industriales - Cal (2A2)
Procesos industriales - Aluminio (2C3)
Procesos industriales - Quimica (2B)
Procesos industriales - Ferroaleaciones y otros metales (2C2, 2C4, 2C5)
Procesos industriales - Papel y celulosa (2H1)
Procesos industriales - Ceramica y otros carbonatos (2A4)
Procesos industriales - Otros (2D, 2E, 2G, 2H2)
Coverage goes from 4 processes to 31. Alcance 1 grows from 11 subcategories to
18, of which 11 are industrial processes; the preselection screen lists every
subcategory of the methodology, so all users scroll past them.
Emission factors are IPCC 2006 Vol.3 Tier 1 defaults in kg/ton: lime Table 2.4,
aluminium Table 4.10, ferroalloys Table 4.5, lead from its generic factor table.
Quimica, Papel y celulosa, Ceramica and Otros seed no defaults on purpose - the
IPCC values there are either per-gas (N2O for nitric and adipic acid) or depend
on the carbonate content of the specific feedstock, so a single seeded kgCO2e
figure would be misleading. Those require "Factor Propio", and each explanation
walks through deriving it.
Aluminium seeds only the anode-consumption CO2 factor, which is what Table 4.10
covers; the explanation states that PFC emissions are excluded and must be
declared in a separate line, since the seed has no way to express a per-gas
breakdown (EmissionFactor.gasDetails exists but the seeder hardcodes it to {}).
Adds reduction-plan initiatives and one explanation guide per new subcategory.
…ries
The open-ended dimensions added for the expanded industrial catalogue named
their residual value after the process ("Otro proceso quimico", "Otro uso de
carbonatos", ...). The repo convention is a bare "Otro": the dimension name
already supplies the context, and the capture dropdown sorts that exact literal
last via EMISSION_FACTOR_DIMENSION_OTHER_VALUE, so a longer name would land
alphabetically in the middle of the catalogue instead of at the end.
Only factor-less values are renamed. Closed taxonomies (lime types, aluminium
cell technology, steel route) keep no escape hatch, and values that own a real
emission factor keep their own name.
The expanded industrial process explanations were written against the old
factor source selector and told users to pick "Factor propio", an option
that no longer exists after the custom factor sources were unified.
Where the note also sits next to the residual "Otro" process value, the
field name is spelled out ("Otro" como "Fuente factor") so the two are not
read as the same selector. Generic prose about using "un factor propio" is
left as-is: it names the concept, not the option.
Table 4.5 of the IPCC 2006 Guidelines, Volume 3, gives ferrochromium as "1.3 (1.6 with sinter plant)" — the sintering of chrome ore releases CO2 of its own. Only the lower figure was seeded, so a plant with its own sinter line understated the process by 23%. Split into two options, "sin planta de sínter" (1300) and "con planta de sínter" (1600), with the guide explaining which to pick.
The seven new subcategories allowed only kilograms and tonnes, which made them the only mass-denominated subcategories in the methodology without grams: every subcategory that offers kilograms offers grams as well. Nobody will declare clinker or ferroalloys in grams, but leaving the unit out buys nothing — the editor resolves factors by exact rate-unit denominator, so an unused unit costs a dropdown row and no more — while an inconsistent unit list is a real surprise for whoever edits this catalogue next.
Same treatment the four existing subcategories get in the base branch: Cal, Aluminio and Ferroaleaciones now seed each IPCC default twice, in kg/ton and in kg/kg, so a declaration in tonnes and one in kilograms both resolve to the library factor instead of falling through to a custom one. The subcategories that deliberately seed no defaults — Química, Papel y celulosa, Cerámica y otros carbonatos, Otros — are unaffected.
…egories
The catalogue lives in seed data, but the seed cannot carry an addition to an
installed deployment. seedEmissionFactors runs createMany({ skipDuplicates:
true }) over every factor in the dataset, and skipDuplicates cannot deduplicate
these rows: the partial unique index covers (subcategory, dimension_value_1,
dimension_value_2, source), one of those dimension columns is NULL on most
factors, and Postgres treats NULLs as distinct in a unique index. Re-running
the seed duplicates every existing factor and then aborts on its own row-count
check. So RD and demo would never receive these seven subcategories.
Adds them here: the subcategory rows with their explanation guides, allowed
units, dimensions and values, the IPCC factors in both kg/ton and kg/kg, and
the reduction-plan initiatives.
The SQL is generated from tools/seed/src/data/base — methodologies.json,
initiatives.json and explanations/subcategories — so it installs the same
catalogue as the seed rather than a hand-copy that can drift from it.
Purely additive and guarded by NOT EXISTS throughout. On an empty database the
category lookup matches nothing and every statement inserts zero rows, so an
integration-test container is unaffected.
Verified by replaying it against the running seeded database inside a
transaction, on top of the correction migration: the resulting 70 factors and
70 dimension values across the 11 industrial subcategories match the base
dataset exactly, and applying both migrations twice leaves the same state.
mrivas00
force-pushed
the
feat/mati/industrial-processes-per-process
branch
from
August 25, 2026 19:46
450f510 to
4fb0aff
Compare
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Context
Two reviewers flagged the same gap in Alcance 1: the industrial-process catalogue only covers cement, glass, steel and zinc, so a company running any other process has nowhere to declare it.
They are right, and the standards say why. The current set of four is an arbitrary subset of the IPCC IPPU tree: it includes glass and zinc, which the GHG Protocol's own sector table does not list, and omits lime, aluminium, chemicals and pulp & paper, which it does (GHG Protocol Corporate Standard, Appendix D "Industry Sectors and Scopes", pp. 92–93).
Nothing here needs code. The catalogue is seed data plus the maintainer screens, and no migration has ever touched methodology content — the one data migration in this repo,
20260819120000_unify_custom_factors, relabels a source string on captured inventory lines.Two options, and why there are two PRs
Neither standard requires a subcategory per material. The GHG Protocol's own Alcance 1 taxonomy has a single "process emissions" bucket (ch. 6, p. 41), and subdividing by source type is explicitly optional information (ch. 9, p. 63). The IPCC, meanwhile, structures IPPU as a two-level hierarchy — branch (2A mineral, 2B chemical, 2C metal, 2H other) then leaf (2A1 cement, 2A2 lime, …), with an explicit
Other (please specify)leaf on every branch (2006 GL Vol. 3, Fig. 1.1, p. 1.5).So the real question is only where the boundary between subcategory and dimension falls. Both options cover the same IPPU leaves.
feat/mati/industrial-processes-ippu-branchesfeat/mati/industrial-processes-per-processProceso→Material o métodocascadeThe costs are real on both sides and neither is free:
carbon_inventory_subtotals_viewaggregates by subcategory) and blunts initiative targeting, becausegetSuggestedReductionPlanfilters only bysubcategoryId.ReductionPlanInitiativealready hasdimensionValue1Id/dimensionValue2Idcolumns, so scoping initiatives to the process later is a query change, not a schema change.useSubcategoryPreselectionDatalists every subcategory of the methodology (recommended ones merely sort first), so every user — a bakery, a law firm — scrolls past all of them.Recommendation: A, and this PR is the alternative. A matches the IPCC hierarchy, keeps the subcategory count flat, and turns "there is nowhere to put my process" into a maintainer-screen edit rather than a recurring release. This PR is the right call if the priority is not touching data that already exists in RD/demo — it is purely additive, so no existing line, factor or initiative changes shape.
Worth naming its long-term cost honestly: it leaves mixed levels of the IPPU tree side by side as siblings.
Cemento(leaf 2A1) sits next toQuímica(branch 2B) andCerámica y otros carbonatos(leaf 2A4) — a distinction that is invisible to a user and permanent once inventories reference it.These two PRs are mutually exclusive — merge one and close the other. Option A: #608
What this PR does
Keeps
Cemento,Vidrio,AceroandCincexactly as they are and adds seven subcategories:Procesos industriales - CalProcesos industriales - AluminioProcesos industriales - QuímicaProcesos industriales - Ferroaleaciones y otros metalesProcesos industriales - Papel y celulosaProcesos industriales - Cerámica y otros carbonatosProcesos industriales - OtrosOtro, matching the convention established onfeat/mati/machinery-catalog-otro-option(the dimension name supplies the context, and the capture dropdown sorts that exact literal last). Closed taxonomies — lime types, aluminium cell technology — get none, sinceOtrocarries no methodological meaning there.g,kgandton, matching every other mass-denominated subcategory in the methodology.Emission factors
All values are IPCC 2006 Guidelines, Volume 3 (IPPU) Tier 1 defaults, read from the published PDFs, in
kg/ton:Química,Papel y celulosa,Cerámica y otros carbonatosandOtrosseed no defaults, deliberately. The IPCC values there are either per-gas (N₂O for nitric and adipic acid) or depend on the carbonate content of the specific feedstock, so a single seeded kgCO₂e figure would be misleading. Those require a custom factor, and each explanation walks through deriving it.Dolomitic lime uses 0.77, the value IPCC Table 2.4 recommends for developing countries, rather than 0.86.
Ferrochromium is offered twice. Table 4.5 lists it as
1.3 (1.6 with sinter plant)— sintering chrome ore releases CO₂ of its own — so seeding only 1.3 understated a plant with its own sinter line by 23%. Split intoFerrocromo (sin planta de sínter)1300 andFerrocromo (con planta de sínter)1600, with the guide explaining which applies. Every other value in this PR was re-read against the published IPCC PDFs and matched its table exactly; the audit of the four pre-existing subcategories, and the four corrections it produced, is in #619EmissionFactor.gasDetailsexists and is validated against the total, butseedEmissionFactorshardcodes it to{}.Factor normalization moved out
The
kg/tonnormalization of the four existing subcategories now lives in its own PR cut frommain, and this PR is based on that branch: #619It is a behaviour change — steel and zinc process factors were understated by 1000×, and cement and glass now resolve in tonnes rather than kilograms — and it is the one part of this work that is not purely additive. It also applies regardless of which catalogue shape is chosen, which is why it was split out: it fixes
mainon its own. Read it first; the reasoning and the impact on existing inventories are spelled out there.Installed deployments get it through a migration
Purely additive to the seed data, but on a database that already holds the catalogue the seed cannot install it:
seedEmissionFactorsrunscreateMany({ skipDuplicates: true })over every factor, andskipDuplicatesdeduplicates nothing here — the partial unique index covers(subcategory, dimension_value_1, dimension_value_2, source), one of those dimension columns is NULL on most factors, and Postgres treats NULLs as distinct in a unique index. A reseed would duplicate every existing factor and then abort on its own row-count check, so RD and demo would never receive these subcategories.20260824130000_add_industrial_process_subcategoriesinstalls them: subcategory rows with their explanation guides, allowed units, dimensions and values, the IPCC factors in bothkg/tonandkg/kg, and the reduction-plan initiatives. The SQL is generated fromtools/seed/src/data/baserather than hand-copied, so it cannot drift from the seed. Guarded byNOT EXISTSthroughout, and on an empty database — an integration-test container — the category lookup matches nothing and every statement inserts zero rows.Verified by replaying it against a seeded database inside a transaction, on top of the correction migration from #619: the resulting 70 factors and 70 dimension values across the 11 industrial subcategories match the base dataset exactly, and applying both migrations twice leaves the same state.
Verification
pnpm format:check,pnpm lint,pnpm type-check— all clean.FullMethodologyDataSchema: 30 subcategories, 11 industrial, 66 dimension values, 33 factors, identical inbaseandtesting.(subcategory, code)and(subcategory, position)on dimensions, unique(dimension, value)on values, unique(subcategory, dv1, dv2, source)on factors, every factor's rate-unit denominator present in the subcategory's allowed units, everyparentValueand factor reference resolving, and one explanation file per subcategory.Out of scope, found on the way
emission_factor_unique_subcategory_dims_sourcecovers(subcategory, dimension_value_1, dimension_value_2, source), and one of those dimension columns is NULL on most rows — Postgres treats NULLs as distinct in a unique index, socreateMany({ skipDuplicates: true })deduplicates nothing. Running the seed twice duplicates those factors and then trips its own row-count check. (An earlier revision of this section claimed 25 of 255 factors were being dropped by that index; they are not — the seeded database holds all of them, counted directly.) Adding the rate unit to the index and making the NULL columns non-distinct would fix both.gasDetailsis never seeded. The column supports a per-gas breakdown and is validated against the total, but the seeder writes{}. Populating it would let aluminium carry its CO₂/PFC split and satisfy the per-gas reporting both ISO 14064-1 and the GHG Protocol require.refactor/mati/remove-factor-propio-source. That branch relabels the custom factor source toOtro. The explanation guides here say "Factor propio", matchingmaintoday — whichever lands second needs a pass over the new guides.CI
22 of 24 checks pass.
AuditandTrivy Image Scan (api)fail ondeepmerge-ts7.1.5 (CVE-2026-40345), a transitive dependency of@prisma/client. Pre-existing onmainand unrelated to seed data — already addressed onfix/mati/deepmerge-ts-cve.