Skip to content

feat(seed): expand industrial process catalogue with per-process subcategories - #609

Merged
mrivas00 merged 7 commits into
mainfrom
feat/mati/industrial-processes-per-process
Sep 9, 2026
Merged

mrivas00 merged 7 commits into
mainfrom
feat/mati/industrial-processes-per-process

Conversation

@mrivas00

@mrivas00 mrivas00 commented Aug 19, 2026 •

Copy link
Copy Markdown
Collaborator

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.

Gabriela: si la empresa realiza procesos industriales distintos a acero, fundición de metales, cemento o vidrio (por ejemplo industria química), no hay dónde incorporarlo.

Gianluca: en emisiones directas, ¿no es muy restrictivo solo acero, cinc y vidrio?

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.

Approach: one subcategory per process

Neither standard requires a subcategory per material. The GHG Protocol's Alcance 1 taxonomy has a single "process emissions" bucket (ch. 6, p. 41), and subdividing by source type is explicitly optional (ch. 9, p. 63). The IPCC 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 only real question was where the boundary between subcategory and dimension falls. The alternative — subcategory = IPPU branch, with a Proceso → Material o método cascade underneath — was built out in full and has been discarded: #608 (closed).

It lost on two counts:

  • It collapses the per-process breakdown in reports. carbon_inventory_subtotals_view aggregates by subcategory, so cement and glass would share one bucket, and getSuggestedReductionPlan filters only by subcategoryId, which blunts initiative targeting the same way.
  • It reworks data that already exists. The four current subcategories would have been replaced, so RD and demo would need a pass over the maintainer screens. This PR is purely additive — no existing line, factor or initiative changes shape.

Worth naming this shape's long-term cost honestly, since it is the one the discarded option did not have:

  • It leaves mixed levels of the IPPU tree side by side as siblings. Cemento (leaf 2A1) sits next to Química (branch 2B) and Cerámica y otros carbonatos (leaf 2A4) — a distinction invisible to the user and permanent once inventories reference it.
  • Alcance 1 grows to 18 subcategories, 11 of them industrial, and AddSubcategoryModal lists every subcategory of the methodology (recommended ones merely sort first), so a bakery or a law firm scrolls past all of them.
  • Adding a process later needs a new subcategory, an explanation guide and a release, rather than a dimension value added from the maintainer UI.

What this PR does

Keeps Cemento, Vidrio, Acero and Cinc exactly as they are and adds seven subcategories:

Subcategory IPPU Dimension Values Seeded factors
Procesos industriales - Cal 2A2 Tipo de cal 3 3
Procesos industriales - Aluminio 2C3 Tecnología de celda 2 2
Procesos industriales - Química 2B Proceso 16 —
Procesos industriales - Ferroaleaciones y otros metales 2C2, 2C4, 2C5 Producto o proceso 16 14
Procesos industriales - Papel y celulosa 2H1 Proceso 4 —
Procesos industriales - Cerámica y otros carbonatos 2A4 Proceso 5 —
Procesos industriales - Otros 2D, 2E, 2G, 2H2 Proceso 6 —
  • Open-ended dimensions end in a bare Otro, matching the convention established in feat(seed): expand mobile machinery catalog and add an "Otro" option #604 (the dimension name supplies the context, and the capture dropdown sorts that exact literal last). Closed taxonomies — lime types, aluminium cell technology — get none, since Otro carries no methodological meaning there.
  • Adds one reduction-plan initiative and one explanation guide per new subcategory.
  • Allowed measurement units are g, kg and ton, matching the four existing industrial subcategories and every other mass-denominated subcategory.
  • Each seeded factor is stored in both kg/ton and kg/kg, again matching what the existing four carry, so capture resolves whichever unit the user picks.

Emission factors

All values are IPCC 2006 Guidelines, Volume 3 (IPPU) Tier 1 defaults, read from the published PDFs, in kg/ton:

Process Source Values
Cal Table 2.4 high-calcium 750 · dolomitic 770 · hydraulic 590
Aluminio Table 4.10 Prebake 1600 · Søderberg 1700
Ferroaleaciones Table 4.5 2500 · 3600 · 4000 · 4800 · 1300 · 1500 · 1400 · 5000 · 1300 / 1600
Plomo generic factors by furnace ISF 590 · DS 250 · secondary 200 · default 520

Química, Papel y celulosa, Cerámica y otros carbonatos and Otros seed 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 into Ferrocromo (sin planta de sínter) 1300 and Ferrocromo (con planta de sínter) 1600, with the guide explaining which applies. Every other value 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 #619 — now merged, so this PR sits on top of it in main rather than on a branch.

⚠️ Aluminium seeds only the anode-consumption CO₂ factor, which is all Table 4.10 covers. PFC emissions from the anode effect are excluded. The explanation states this and instructs a separate line, because the seeder cannot express a per-gas split — EmissionFactor.gasDetails exists and is validated against the total, but seedEmissionFactors hardcodes it to {}.

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: seedEmissionFactors runs createMany({ skipDuplicates: true }) over every factor, and skipDuplicates deduplicates 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.

20260909120000_add_industrial_process_subcategories installs them: 7 subcategory rows with their explanation guides, their allowed units, 7 dimensions with 52 values, 38 factor rows (19 values × kg/ton and kg/kg), and 7 reduction-plan initiatives. The SQL is generated from tools/seed/src/data/base rather than hand-copied, so it cannot drift from the seed. Guarded by NOT EXISTS throughout, 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: the resulting factors and dimension values across the seven new subcategories match the base dataset exactly, and applying it twice leaves the same state.

Screenshots

Captured against a database seeded from main and then migrated with 20260909120000_add_industrial_process_subcategories — i.e. the installed-deployment path that RD and demo will take, not a fresh reset + seed.

1 · Alcance 1 — the industrial catalogue

Proves: the seven subcategories are installed and selectable. 11 industrial entries where main has 4, and the four existing ones are untouched.

image

2 · Procesos industriales - Cal — dimension and factor

Proves: the Tipo de cal dimension resolves its 3 values, and the seeded factor auto-fills at 750 kg/ton; switching the unit to kg resolves 0.75, which is the dual kg/ton + kg/kg seeding working end to end.

image

3 · Ferroaleaciones y otros metales — the ferrochromium split

Proves: Ferrocromo (sin planta de sínter) 1300 and Ferrocromo (con planta de sínter) 1600 are both offered, so a plant with its own sinter line is no longer understated by 23%.

image

4 · Procesos industriales - Química — no seeded default

Proves: the deliberate omission is a usable state, not a hole — capture falls through to a custom factor with source Otro, as the guide instructs.

image

5 · Explanation guide

Proves: the per-subcategory guide renders in the (i) panel, including the Otro escape-hatch wording.

image

Verification

  • pnpm format:check, pnpm lint, pnpm type-check, @repo/seed unit tests — all clean, re-run after rebasing onto main.
  • All 24 CI checks pass. (The Audit / Trivy failure this PR previously carried was the deepmerge-ts CVE on main, fixed by fix(deps): force deepmerge-ts to 8.x to patch CVE-2026-40345 #602.)
  • Both datasets parse against the real FullMethodologyDataSchema. Catalogue goes from 23 to 30 subcategories, industrial from 4 to 11; +52 dimension values and +38 factor rows. The seven new subcategories are byte-identical in base and testing.
  • Checked against the constraints that only bite at seed time or in the editor: unique (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, every parentValue and factor reference resolving, and one explanation file per subcategory (11 industrial files ↔ 11 industrial subcategories).
  • Not run: the seed itself against a live database.

Out of scope, found on the way

  1. The factor seed is not idempotent. emission_factor_unique_subcategory_dims_source covers (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, so createMany({ 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.
  2. gasDetails is 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.
  3. No new sector recommendations. Chemical subsectors still do not recommend the chemical process subcategory. Worth a follow-up so the feature is discoverable rather than merely present.
  4. base and testing disagree on two values. Procesos industriales - Cemento → Material → Otro and Procesos industriales - Vidrio → Tipo de vidrio → Otro exist in base but not in testing. Pre-existing on main — it arrived with feat(seed): expand mobile machinery catalog and add an "Otro" option #604 and is untouched here; the seven subcategories this PR adds are identical in both datasets.

@mrivas00 mrivas00 added type: feature new user-facing capability or enhancement area: database packages/database (Prisma schema / migrations) labels Aug 19, 2026
@mrivas00
mrivas00 force-pushed the feat/mati/industrial-processes-per-process branch from 6b6dc8e to 07922f7 Compare August 24, 2026 14:36
@mrivas00
mrivas00 changed the base branch from main to fix/mati/industrial-process-factor-units August 24, 2026 15:14
@mrivas00
mrivas00 force-pushed the feat/mati/industrial-processes-per-process branch 4 times, most recently from 64b2fcd to 1b07c95 Compare August 24, 2026 18:42
Base automatically changed from fix/mati/industrial-process-factor-units to main August 25, 2026 11:46
@mrivas00
mrivas00 force-pushed the feat/mati/industrial-processes-per-process branch 2 times, most recently from 450f510 to 4fb0aff Compare August 25, 2026 19:46
@mrivas00
mrivas00 force-pushed the feat/mati/industrial-processes-per-process branch from 4fb0aff to 21a8589 Compare September 9, 2026 14:27
…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
mrivas00 force-pushed the feat/mati/industrial-processes-per-process branch from 21a8589 to ae227d2 Compare September 9, 2026 17:23
@mrivas00
mrivas00 merged commit 9ccbf3a into main Sep 9, 2026
24 checks passed
@mrivas00
mrivas00 deleted the feat/mati/industrial-processes-per-process branch September 9, 2026 18:29
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

area: database packages/database (Prisma schema / migrations) type: feature new user-facing capability or enhancement

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants