Skip to content

Feat/metadata v2 schema - #898

Draft
AlexAxthelm wants to merge 6 commits into
mainfrom
feat/metadata-v2-schema
Draft

Feat/metadata v2 schema#898
AlexAxthelm wants to merge 6 commits into
mainfrom
feat/metadata-v2-schema

Conversation

@AlexAxthelm

Copy link
Copy Markdown
Collaborator

Summary

Related issues

Closes:

Testing

Checklist

  • PR is focused on a single concern
  • Tests pass locally and in CI
  • Docs updated for user-visible changes
  • AI-assisted portions declared (see CONTRIBUTING.md)

repro and others added 6 commits August 13, 2026 11:53
Foundation for epic #860 (best-effort / inheritance search): each of the
11 keyFeatures fields becomes an array of {sector, geography, value}
entries so a pathway can hold different values for different parts of its
coverage, and the #869 resolver can serve the most specific value for a
search scope and fall back to broader ones. Also adds coreDrivers,
dependencies, pathwayDescription and transitionAssessment.

Additive only. v1 stays present and loadable, PathwayMetadataType still
points at v1, and no data file changes here — the loader is repointed at
v2 in a later commit once data carries the v2 $schema. Nothing consumes
v2 yet.
Migrates the 4 ASEAN Centre for Energy and 3 IEA metadata files to
pathwayMetadata.v2 via a new re-runnable codemod. src/data now holds 7 v2
and 49 v1 documents, which coexist because validateData routes each by its
own $schema $id. Nothing reads v2 yet — the loader still points at v1, so
the app is unchanged.

All 7 resolve to a single widest-scope entry per keyFeature:
cross-sector/South East Asia for ACE, cross-sector/Global for IEA. Both
IEA files that carry pathwayOverview fold it into pathwayDescription as
the lead paragraph; it has no readers in the app, so nothing observable
moves.

- scripts/codemod-v1-to-v2.ts splits v1's expertOverview into its three
  sections, wraps each keyFeature as one scoped entry, and scaffolds
  coreDrivers/dependencies. It skips files already on v2, so the remaining
  49 are a re-run rather than a rewrite. A development tool only: there is
  no runtime v1 conversion, so un-migrated files simply will not load once
  the loader moves to v2.

- The splitter accepts a bare line matching a section title as a heading.
  That exists for ACE-CNS-2024, whose "Core Drivers" heading lost its ####
  markers; without it, 1.5 KB of core-drivers prose folds into
  pathwayDescription and pushes it from 1204 to 2727 chars.

- coreDrivers is scaffolded all-null per #858 rather than populated. The
  v1 "#### Core Drivers" prose does not map onto the 7 named fields
  mechanically: four paragraphs already exceed the 500-char cap, the
  italic labels ("Technology shifts", "Falling energy demand", "Economic
  growth") do not correspond 1:1 to the field names, and every section has
  unlabeled paragraphs with no destination. The codemod prints the prose
  it is not carrying so the hand-authoring ticket starts from the text.

- transitionAssessment's maxLength goes 2500 -> 3000. 2500 was chosen for
  symmetry with pathwayDescription rather than measured; the longest
  section in the corpus is 2655 chars (ACE-RAS-2024), which made the
  codemod's own output invalid. pathwayDescription's 2500 is confirmed
  correct — the longest across all 56 files is 2459.

Fixtures are added rather than converted, so the v1 fixtures stay v1 and
the new coexistence tests can assert both halves. pathwayMetadata_v2_full
carries several entries per field at different scopes, which #869 and #859
will need; _v2_minimal proves an all-empty keyFeatures document validates.

One coexistence test documents a sharp edge deliberately:
validateDataCollect filters entries to the single $id it is handed, so
documents of the other version are dropped as neither valid nor invalid.
That is what makes a mixed corpus work, and it is why repointing the
loader has to report the count it skipped.

Refs #858, #801.

Co-Authored-By: Claude Opus 5 <[email protected]>
Points the loader at pathwayMetadata.v2 and moves every consumer onto the
scoped {sector, geography, value} shape. Only v2 documents load, so the app
now shows the 7 migrated ACE/IEA pathways; the 49 still on v1 are skipped
by $schema routing until they migrate.

That skip is silent by construction — validateDataCollect drops non-matching
documents as neither valid nor invalid — so pathwayMetadata.ts counts and
logs them. Without it, 49 missing pathways look like a data bug.

New src/utils/keyFeatureScope.ts answers "which entries apply to what the
user is looking at": containment on both axes, where cross-sector means the
union of the pathway's own declared sectors (not a universal match), and a
geography scope contains a query when the query's ISO set is a subset of the
entry's. Broader answers narrower, never the reverse. Deliberately no cost
model, no ranking, no fallback — that is #869, and it is what will turn a
non-match at the queried scope into a ranked broader-scope match rather than
an exclusion.

The emissionsTrajectory and policyAmbition facets now match like the sector
and metric facets — ANY/ALL over a value list, empty list meaning absent —
restricted to the entries whose scope contains the active sector/geography
selection. The two near-identical 30-line arms collapse into one helper.
concrete.includes(v) against an array is always false, so selecting either
returned zero pathways, and option building emitted "[object Object]".

Neither arm had any test coverage before — no filterPathways test passed
either filter — which is why the whole suite stayed green while both were
broken. Adding that coverage caught a regression that would otherwise have
shipped: in v1 a missing field contributed undefined, which
buildOptionsFromValues read as the absent bucket, but in v2 an empty entry
array contributes no elements, so the "None" option disappeared from both
dropdowns while the filter still honoured the token. Fixed with
withAbsentOption, matching how the sector facet does it.

Rendering keeps its current output. KeyFeatures reads through widestValue,
a deliberately provisional stand-in for #869's resolver: it picks the value
at the broadest declared scope, which reproduces v1 exactly for
codemod-migrated data (one entry, at its widest scope). #859 replaces it and
adds the badge naming the scope.

PathwayDetailPage renders pathwayDescription and transitionAssessment under
separate subheadings. v1's single expertOverview blob was three sections, so
rendering only the description would have visibly dropped the Application
to Transition Assessment text. The "Expert Overview" heading is left alone;
naming is #859's call.

Verified against the running app: 7 pathways load, the skip warning fires
without error, and all 11 key features on IEA-NZE render values matching the
source file — including the multi-select branch, which degrades silently
rather than throwing when handed the wrong shape.

Refs #858. Enables #869, #859.

Co-Authored-By: Claude Opus 5 <[email protected]>
Closes out #858's checklist.

src/data/README.md described a format that no longer exists — and in the R
example's case, one that never validated. Beyond the expected v1 leftovers
(expertOverview, npm run json:check, a pbtar_schema.json link), the example
used `name` as a bare string, the pre-#783 flat geography array, top-level
publisher/publicationYear, and a `dataSource` field absent from every version
of the schema. It would have failed against v1 as readily as against v2.

Rewritten around v2: the two coexisting schema versions and the fact that only
v2 documents are loaded, the scoped keyFeatures shape with its sentinels and
the widest-scope rule, coreDrivers/dependencies/pathwayDescription/
transitionAssessment, the codemod for migrating an existing file, and the
commands that actually exist.

The R example is now verified rather than asserted: its blocks were extracted
from this file, executed, and the resulting JSON validated against v2. Doing
that corrected a wrong claim in an earlier draft — R's list() preserves NULL
elements; the actual pitfall is jsonlite writing NULL as {}, which is why the
helper passes null = "null".

The validate_json R helper is dropped rather than repaired. The schema is
split across common/*.json with absolute $refs that a single-URL
jsonvalidate::json_validate() cannot resolve, so it documented a validation
route that cannot succeed. Authors are pointed at npm run schema:check, which
resolves the refs and additionally runs the cross-field scope checks that JSON
Schema draft-07 cannot express.

Tests: adds v2 counterparts to the existing v1 required-field cases — all 12
required fields, each of the 7 coreDrivers keys, unknown keys, and the
dependencies enums. Also pins that pathwayDescription accepts null but not
absence, the nullable-but-required distinction v2 relies on. The v1 REQ array
still lists expertOverview on purpose: those cases validate v1 documents
against v1, where it remains required.

Finally, migrates the seven `keyFeatures: { emissionsTrajectory: "foo" }`
stubs in ComparisonPage and PathwaySearch tests to the v2 shape. They kept
their deliberately-invalid values, which exist to test degradation; the point
is that a v1-shaped scalar in a v2 fixture silently exercises nothing.

Refs #858.

Co-Authored-By: Claude Opus 5 <[email protected]>
Copilot AI lite review requested due to automatic review settings August 14, 2026 13:23
<div class="card">
<div
class="card-body"
id=""

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR introduces pathway metadata schema v2 (scoped keyFeatures entries plus coreDrivers / dependencies / pathwayDescription), migrates a first slice of pathway JSON to v2, and updates validation, search, and rendering so the app can operate on v2-shaped data.

Changes:

  • Add pathwayMetadata.v2.json + v2 scope subschemas, generated TS types, and generated HTML schema docs.
  • Add cross-field validation for scoped entries (sector/geography references) and update search facets + rendering to handle scoped keyFeature values.
  • Migrate fixtures + a subset of real data files to v2, and add a codemod + tests to support migrating the remaining corpus.

Reviewed changes

Copilot reviewed 35 out of 39 changed files in this pull request and generated 2 comments.

Show a summary per file
File Description
testdata/valid/pathwayMetadata_v2_minimal.json Adds a minimal v2-valid fixture for schema/validator coverage.
testdata/valid/pathwayMetadata_v2_full.json Adds a comprehensive v2 fixture exercising scoped keyFeatures and new fields.
src/utils/validateScopes.ts Implements cross-field scope reference validation for v2 (sector/geography must be declared).
src/utils/validateScopes.test.ts Unit tests for scoped entry validation behavior and error reporting.
src/utils/validateData.test.tsx Extends validation tests to cover v2 routing/coexistence and v2 required fields.
src/utils/searchUtils.ts Updates facet option building and filtering to support scoped keyFeature entries.
src/utils/searchUtils.scopedFacets.test.ts Adds targeted test coverage for scoped facets (emissionsTrajectory, policyAmbition).
src/utils/keyFeatureScope.ts Adds helpers for reading scoped keyFeature entries (containment + widest-value fallback).
src/utils/keyFeatureScope.test.ts Unit tests for scope containment, ISO resolution, and widest-value selection.
src/types/pathwayMetadata.v2.d.ts Adds generated TS types for the v2 schema.
src/types/index.ts Switches PathwayMetadataType to v2 and exports both v1/v2 types during migration.
src/types/common/scopeSector.v2.d.ts Adds generated TS type for the v2 sector scope sentinel enum.
src/types/common/scopeGeography.v2.d.ts Adds generated TS type for v2 geography scope (open string).
src/schema/pathwayMetadata.v2.test.ts Adds schema self-guard tests to prevent keyFeatures wrapper/value drift.
src/schema/pathwayMetadata.v2.json Introduces the v2 pathway metadata JSON Schema definition.
src/schema/common/scopeSector.v2.json Adds schema for the v2 sector scope axis (sector names + cross-sector).
src/schema/common/scopeGeography.v2.json Adds schema for the v2 geography scope axis (open string with basic guards).
src/schema/common/index.ts Registers new v2 common schemas so AJV can resolve $refs.
src/pages/PathwaySearch.test.tsx Updates integration test fixtures to use scoped v2 keyFeatures.
src/pages/PathwayDetailPage.tsx Renders v2 pathwayDescription / transitionAssessment in place of v1 expertOverview.
src/pages/ComparisonPage.test.tsx Updates comparison page test fixtures to use scoped v2 keyFeatures.
src/data/README.md Updates contributor docs for v2 format, migration approach, and validation commands.
src/data/pathwayMetadata.ts Switches loader to v2 schema and warns when v1 metadata files are skipped.
src/data/iea/IEA-STEPS-2024.json Migrates this pathway metadata file to v2 structure.
src/data/iea/IEA-NZE-2024.json Migrates this pathway metadata file to v2 structure.
src/data/iea/IEA-APS-2024.json Migrates this pathway metadata file to v2 structure.
src/data/asean-centre-for-energy/ACE-RAS-2024.json Migrates this pathway metadata file to v2 structure.
src/data/asean-centre-for-energy/ACE-CNS-2024.json Migrates this pathway metadata file to v2 structure.
src/data/asean-centre-for-energy/ACE-BAS-2024.json Migrates this pathway metadata file to v2 structure.
src/data/asean-centre-for-energy/ACE-ATS-2024.json Migrates this pathway metadata file to v2 structure.
src/components/KeyFeatures.tsx Updates rendering to read v2 scoped entries via a “widest value” helper.
src/components/KeyFeatures.test.tsx Updates keyFeatures rendering tests to use v2 scoped-entry fixtures.
scripts/schema-check-files.ts Adds a second-pass v2 scope reference check after AJV validation.
scripts/codemod-v1-to-v2.ts Adds a codemod to migrate v1 metadata docs to v2 format.
scripts/codemod-v1-to-v2.test.ts Adds tests for the codemod’s section splitting and scope selection logic.
public/schema/scopeSector.v2.html Adds generated HTML documentation for scopeSector.v2.json.
public/schema/scopeGeography.v2.html Adds generated HTML documentation for scopeGeography.v2.json.
PLAN.md Adds an implementation/migration plan and rationale for v2 schema + rollout.

💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.

Comment on lines +8 to +12
"minLength": 1,
"allOf": [
{ "pattern": "^(?!\\s*$).+" },
{ "not": { "pattern": "^[A-Za-z]{3}$" } }
],
Comment on lines +43 to +55
function allowedGeographies(pathway: PathwayMetadataV2): Set<string> {
const allowed = new Set<string>([GLOBAL_SCOPE, CROSS_REGION]);
const geo = pathway.geography;
if (!geo || typeof geo !== "object") return allowed;
if (geo.regions) {
for (const [label, members] of Object.entries(geo.regions)) {
allowed.add(label);
if (Array.isArray(members)) members.forEach((m) => allowed.add(m));
}
}
if (Array.isArray(geo.country)) geo.country.forEach((c) => allowed.add(c));
return allowed;
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants